L-Sun
2025-02-28 08:23:26 +00:00
parent f1df774188
commit d476d3b1df
27 changed files with 356 additions and 528 deletions

View File

@@ -113,12 +113,6 @@ import {
export type KeyboardToolbarConfig = {
items: KeyboardToolbarItem[];
/**
* @description Whether to use the screen height as the keyboard height when the virtual keyboard API is not supported.
* It is useful when the app is running in a webview and the keyboard is not overlaid on the content.
* @default false
*/
useScreenHeight?: boolean;
};
export type KeyboardToolbarItem =
@@ -1106,5 +1100,4 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
},
},
],
useScreenHeight: false,
};

View File

@@ -1,8 +1,8 @@
import { getDocTitleByEditorHost } from '@blocksuite/affine-components/doc-title';
import type { RootBlockModel } from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { WidgetComponent } from '@blocksuite/block-std';
import { IS_MOBILE } from '@blocksuite/global/env';
import { assertType } from '@blocksuite/global/utils';
import { signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
@@ -20,10 +20,10 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
> {
private readonly _close = (blur: boolean) => {
if (blur) {
if (document.activeElement === this._docTitle) {
this._docTitle?.blur();
if (document.activeElement === this._docTitle?.inlineEditorContainer) {
this._docTitle?.inlineEditor?.setInlineRange(null);
} else if (document.activeElement === this.block.rootComponent) {
this.block.rootComponent?.blur();
this.std.selection.clear();
}
}
this._show$.value = false;
@@ -31,12 +31,8 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
private readonly _show$ = signal(false);
private get _docTitle(): HTMLDivElement | null {
const docTitle = this.std.host
.closest('.affine-page-viewport')
?.querySelector('doc-title rich-text .inline-editor');
assertType<HTMLDivElement | null>(docTitle);
return docTitle;
private get _docTitle() {
return getDocTitleByEditorHost(this.std.host);
}
get config() {
@@ -61,10 +57,11 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
}
if (this._docTitle) {
this.disposables.addFromEvent(this._docTitle, 'focus', () => {
const { inlineEditorContainer } = this._docTitle;
this.disposables.addFromEvent(inlineEditorContainer, 'focus', () => {
this._show$.value = true;
});
this.disposables.addFromEvent(this._docTitle, 'blur', () => {
this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => {
this._show$.value = false;
});
}

View File

@@ -1,8 +1,5 @@
import {
VirtualKeyboardController,
type VirtualKeyboardControllerConfig,
} from '@blocksuite/affine-components/virtual-keyboard';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import {
PropTypes,
requiredProperties,
@@ -17,20 +14,21 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { PageRootBlockComponent } from '../../page/page-root-block.js';
import { PageRootBlockComponent } from '../../page/page-root-block';
import type {
KeyboardIconType,
KeyboardToolbarConfig,
KeyboardToolbarContext,
KeyboardToolbarItem,
KeyboardToolPanelConfig,
} from './config.js';
import { keyboardToolbarStyles, TOOLBAR_HEIGHT } from './styles.js';
} from './config';
import { PositionController } from './position-controller';
import { keyboardToolbarStyles } from './styles';
import {
isKeyboardSubToolBarConfig,
isKeyboardToolBarActionItem,
isKeyboardToolPanelConfig,
} from './utils.js';
} from './utils';
export const AFFINE_KEYBOARD_TOOLBAR = 'affine-keyboard-toolbar';
@@ -43,11 +41,28 @@ export class AffineKeyboardToolbar extends SignalWatcher(
) {
static override styles = keyboardToolbarStyles;
/** This field records the panel static height same as the virtual keyboard height */
panelHeight$ = signal(0);
positionController = new PositionController(this);
get std() {
return this.rootComponent.std;
}
get keyboard() {
return this._context.std.get(VirtualKeyboardProvider);
}
get panelOpened() {
return this._currentPanelIndex$.value !== -1;
}
private readonly _closeToolPanel = () => {
if (!this._isPanelOpened) return;
if (!this.panelOpened) return;
this._currentPanelIndex$.value = -1;
this._keyboardController.show();
this.keyboard.show();
};
private readonly _currentPanelIndex$ = signal(-1);
@@ -55,7 +70,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
private readonly _goPrevToolbar = () => {
if (!this._isSubToolbarOpened) return;
if (this._isPanelOpened) this._closeToolPanel();
if (this.panelOpened) this._closeToolPanel();
this._path$.value = this._path$.value.slice(0, -1);
};
@@ -75,31 +90,25 @@ export class AffineKeyboardToolbar extends SignalWatcher(
this._closeToolPanel();
} else {
this._currentPanelIndex$.value = index;
this._keyboardController.hide();
this.scrollCurrentBlockIntoView();
this.keyboard.hide();
this._scrollCurrentBlockIntoView();
}
}
this._lastActiveItem$.value = item;
};
private readonly _keyboardController = new VirtualKeyboardController(this);
private readonly _lastActiveItem$ = signal<KeyboardToolbarItem | null>(null);
/** This field records the panel static height, which dose not aim to control the panel opening */
private readonly _panelHeight$ = signal(0);
private readonly _path$ = signal<number[]>([]);
private readonly scrollCurrentBlockIntoView = () => {
const { std } = this.rootComponent;
std.command
private readonly _scrollCurrentBlockIntoView = () => {
this.std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(({ selectedModels }) => {
if (!selectedModels?.length) return;
const block = std.view.getBlock(selectedModels[0].id);
const block = this.std.view.getBlock(selectedModels[0].id);
if (!block) return;
const { y: y1 } = this.getBoundingClientRect();
@@ -118,7 +127,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
private get _context(): KeyboardToolbarContext {
return {
std: this.rootComponent.std,
std: this.std,
rootComponent: this.rootComponent,
closeToolbar: (blur = false) => {
this.close(blur);
@@ -130,7 +139,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
}
private get _currentPanelConfig(): KeyboardToolPanelConfig | null {
if (!this._isPanelOpened) return null;
if (!this.panelOpened) return null;
const result = this._currentToolbarItems[this._currentPanelIndex$.value];
@@ -139,9 +148,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
private get _currentToolbarItems(): KeyboardToolbarItem[] {
let items = this.config.items;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < this._path$.value.length; i++) {
const index = this._path$.value[i];
for (const index of this._path$.value) {
if (isKeyboardSubToolBarConfig(items[index])) {
items = items[index].items;
} else {
@@ -156,21 +163,10 @@ export class AffineKeyboardToolbar extends SignalWatcher(
);
}
private get _isPanelOpened() {
return this._currentPanelIndex$.value !== -1;
}
private get _isSubToolbarOpened() {
return this._path$.value.length > 0;
}
get virtualKeyboardControllerConfig(): VirtualKeyboardControllerConfig {
return {
useScreenHeight: this.config.useScreenHeight ?? false,
inputElement: this.rootComponent,
};
}
private _renderIcon(icon: KeyboardIconType) {
return typeof icon === 'function' ? icon(this._context) : icon;
}
@@ -252,35 +248,13 @@ export class AffineKeyboardToolbar extends SignalWatcher(
e.preventDefault();
});
this.disposables.add(
effect(() => {
if (this._keyboardController.opened) {
this._panelHeight$.value = this._keyboardController.keyboardHeight;
} else if (this._isPanelOpened && this._panelHeight$.peek() === 0) {
this._panelHeight$.value = 260;
}
})
);
this.disposables.add(
effect(() => {
if (this._keyboardController.opened && !this.config.useScreenHeight) {
document.body.style.paddingBottom = `${this._keyboardController.keyboardHeight + TOOLBAR_HEIGHT}px`;
} else if (this._isPanelOpened) {
document.body.style.paddingBottom = `${this._panelHeight$.value + TOOLBAR_HEIGHT}px`;
} else {
document.body.style.paddingBottom = '';
}
})
);
this.disposables.add(
effect(() => {
const std = this.rootComponent.std;
std.selection.value;
// wait cursor updated
requestAnimationFrame(() => {
this.scrollCurrentBlockIntoView();
this._scrollCurrentBlockIntoView();
});
})
);
@@ -328,24 +302,14 @@ export class AffineKeyboardToolbar extends SignalWatcher(
);
}
override disconnectedCallback() {
super.disconnectedCallback();
document.body.style.paddingBottom = '';
}
override firstUpdated() {
// workaround for the virtual keyboard showing transition animation
setTimeout(() => {
this.scrollCurrentBlockIntoView();
this._scrollCurrentBlockIntoView();
}, 700);
}
override render() {
this.style.bottom =
this.config.useScreenHeight && this._keyboardController.opened
? `${-this._panelHeight$.value}px`
: '0px';
return html`
<div class="keyboard-toolbar">
${this._renderItems()}
@@ -355,7 +319,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
<affine-keyboard-tool-panel
.config=${this._currentPanelConfig}
.context=${this._context}
height=${this._panelHeight$.value}
height=${this.panelHeight$.value}
></affine-keyboard-tool-panel>
`;
}

View File

@@ -0,0 +1,58 @@
import { type VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import type { BlockStdScope, ShadowlessElement } from '@blocksuite/block-std';
import { DisposableGroup } from '@blocksuite/global/utils';
import { effect, type Signal } from '@preact/signals-core';
import type { ReactiveController, ReactiveControllerHost } from 'lit';
import { TOOLBAR_HEIGHT } from './styles';
/**
* This controller is used to control the keyboard toolbar position
*/
export class PositionController implements ReactiveController {
private readonly _disposables = new DisposableGroup();
host: ReactiveControllerHost &
ShadowlessElement & {
std: BlockStdScope;
panelHeight$: Signal<number>;
keyboard: VirtualKeyboardProvider;
panelOpened: boolean;
};
constructor(host: PositionController['host']) {
(this.host = host).addController(this);
}
hostConnected() {
const { keyboard, panelOpened } = this.host;
this._disposables.add(
effect(() => {
if (keyboard.visible$.value) {
this.host.panelHeight$.value = keyboard.height$.value;
}
})
);
this.host.style.bottom = '0px';
this._disposables.add(
effect(() => {
if (keyboard.visible$.value) {
document.body.style.paddingBottom = `${keyboard.height$.value + TOOLBAR_HEIGHT}px`;
} else if (panelOpened) {
document.body.style.paddingBottom = `${this.host.panelHeight$.peek() + TOOLBAR_HEIGHT}px`;
} else {
document.body.style.paddingBottom = '';
}
})
);
this._disposables.add(() => {
document.body.style.paddingBottom = '';
});
}
hostDisconnected() {
this._disposables.dispose();
}
}

View File

@@ -64,7 +64,6 @@ export interface LinkedWidgetConfig {
) => string | null;
mobile: {
useScreenHeight?: boolean;
/**
* The linked doc menu widget will scroll the container to make sure the input cursor is visible in viewport.
* It accepts a selector string, HTMLElement or Window

View File

@@ -214,7 +214,6 @@ export class AffineLinkedDocWidget extends WidgetComponent<
convertTriggerKey: true,
getMenus,
mobile: {
useScreenHeight: false,
scrollContainer: getViewportElement(this.std.host) ?? window,
scrollTopOffset: 46,
},

View File

@@ -2,10 +2,7 @@ import {
cleanSpecifiedTail,
getTextContentFromInlineRange,
} from '@blocksuite/affine-components/rich-text';
import {
VirtualKeyboardController,
type VirtualKeyboardControllerConfig,
} from '@blocksuite/affine-components/virtual-keyboard';
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import {
createKeydownObserver,
getViewportElement,
@@ -43,8 +40,6 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
private _firstActionItem: LinkedMenuItem | null = null;
private readonly _keyboardController = new VirtualKeyboardController(this);
private readonly _linkedDocGroup$ = signal<LinkedMenuGroup[]>([]);
private readonly _renderGroup = (group: LinkedMenuGroup) => {
@@ -159,11 +154,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
);
}
get virtualKeyboardControllerConfig(): VirtualKeyboardControllerConfig {
return {
useScreenHeight: this.context.config.mobile.useScreenHeight ?? false,
inputElement: this.rootComponent,
};
get keyboard() {
return this.context.std.get(VirtualKeyboardProvider);
}
override connectedCallback() {
@@ -230,8 +222,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
}
override firstUpdated() {
if (!this._keyboardController.opened) {
this._keyboardController.show();
if (!this.keyboard.visible$.value) {
this.keyboard.show();
}
this._scrollInputToTop();
}
@@ -244,11 +236,7 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
this._firstActionItem = resolveSignal(groups[0].items)[0];
this.style.bottom =
this.context.config.mobile.useScreenHeight &&
this._keyboardController.opened
? '0px'
: `max(0px, ${this._keyboardController.keyboardHeight}px)`;
this.style.bottom = `${this.keyboard.height$.value}px`;
return html`
${join(groups.map(this._renderGroup), html`<div class="divider"></div>`)}