mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
fix(editor): android keyboard can not be opened (#10502)
Close [BS-2674](https://linear.app/affine-design/issue/BS-2674/[android]-%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%8C%BA%E5%9F%9F%E7%82%B9%E5%87%BB%E5%90%8E%E6%97%A0%E6%B3%95%E6%BF%80%E6%B4%BB%E9%94%AE%E7%9B%98) [BS-2609](https://linear.app/affine-design/issue/BS-2609/[android]-%E8%BE%93%E5%85%A5%E7%9A%84-toolbar-%E6%B2%A1%E6%9C%89%E4%BA%86)
This commit is contained in:
@@ -113,12 +113,6 @@ import {
|
|||||||
|
|
||||||
export type KeyboardToolbarConfig = {
|
export type KeyboardToolbarConfig = {
|
||||||
items: KeyboardToolbarItem[];
|
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 =
|
export type KeyboardToolbarItem =
|
||||||
@@ -1106,5 +1100,4 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
useScreenHeight: false,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getDocTitleByEditorHost } from '@blocksuite/affine-components/doc-title';
|
||||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||||
import { WidgetComponent } from '@blocksuite/block-std';
|
import { WidgetComponent } from '@blocksuite/block-std';
|
||||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||||
import { assertType } from '@blocksuite/global/utils';
|
|
||||||
import { signal } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import { html, nothing } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
|
|||||||
> {
|
> {
|
||||||
private readonly _close = (blur: boolean) => {
|
private readonly _close = (blur: boolean) => {
|
||||||
if (blur) {
|
if (blur) {
|
||||||
if (document.activeElement === this._docTitle) {
|
if (document.activeElement === this._docTitle?.inlineEditorContainer) {
|
||||||
this._docTitle?.blur();
|
this._docTitle?.inlineEditor?.setInlineRange(null);
|
||||||
} else if (document.activeElement === this.block.rootComponent) {
|
} else if (document.activeElement === this.block.rootComponent) {
|
||||||
this.block.rootComponent?.blur();
|
this.std.selection.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._show$.value = false;
|
this._show$.value = false;
|
||||||
@@ -31,12 +31,8 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
|
|||||||
|
|
||||||
private readonly _show$ = signal(false);
|
private readonly _show$ = signal(false);
|
||||||
|
|
||||||
private get _docTitle(): HTMLDivElement | null {
|
private get _docTitle() {
|
||||||
const docTitle = this.std.host
|
return getDocTitleByEditorHost(this.std.host);
|
||||||
.closest('.affine-page-viewport')
|
|
||||||
?.querySelector('doc-title rich-text .inline-editor');
|
|
||||||
assertType<HTMLDivElement | null>(docTitle);
|
|
||||||
return docTitle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config() {
|
||||||
@@ -61,10 +57,11 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._docTitle) {
|
if (this._docTitle) {
|
||||||
this.disposables.addFromEvent(this._docTitle, 'focus', () => {
|
const { inlineEditorContainer } = this._docTitle;
|
||||||
|
this.disposables.addFromEvent(inlineEditorContainer, 'focus', () => {
|
||||||
this._show$.value = true;
|
this._show$.value = true;
|
||||||
});
|
});
|
||||||
this.disposables.addFromEvent(this._docTitle, 'blur', () => {
|
this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => {
|
||||||
this._show$.value = false;
|
this._show$.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {
|
|
||||||
VirtualKeyboardController,
|
|
||||||
type VirtualKeyboardControllerConfig,
|
|
||||||
} from '@blocksuite/affine-components/virtual-keyboard';
|
|
||||||
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
|
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
|
||||||
|
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
|
||||||
import {
|
import {
|
||||||
PropTypes,
|
PropTypes,
|
||||||
requiredProperties,
|
requiredProperties,
|
||||||
@@ -17,20 +14,21 @@ import { repeat } from 'lit/directives/repeat.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { when } from 'lit/directives/when.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 {
|
import type {
|
||||||
KeyboardIconType,
|
KeyboardIconType,
|
||||||
KeyboardToolbarConfig,
|
KeyboardToolbarConfig,
|
||||||
KeyboardToolbarContext,
|
KeyboardToolbarContext,
|
||||||
KeyboardToolbarItem,
|
KeyboardToolbarItem,
|
||||||
KeyboardToolPanelConfig,
|
KeyboardToolPanelConfig,
|
||||||
} from './config.js';
|
} from './config';
|
||||||
import { keyboardToolbarStyles, TOOLBAR_HEIGHT } from './styles.js';
|
import { PositionController } from './position-controller';
|
||||||
|
import { keyboardToolbarStyles } from './styles';
|
||||||
import {
|
import {
|
||||||
isKeyboardSubToolBarConfig,
|
isKeyboardSubToolBarConfig,
|
||||||
isKeyboardToolBarActionItem,
|
isKeyboardToolBarActionItem,
|
||||||
isKeyboardToolPanelConfig,
|
isKeyboardToolPanelConfig,
|
||||||
} from './utils.js';
|
} from './utils';
|
||||||
|
|
||||||
export const AFFINE_KEYBOARD_TOOLBAR = 'affine-keyboard-toolbar';
|
export const AFFINE_KEYBOARD_TOOLBAR = 'affine-keyboard-toolbar';
|
||||||
|
|
||||||
@@ -43,11 +41,28 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
) {
|
) {
|
||||||
static override styles = keyboardToolbarStyles;
|
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 = () => {
|
private readonly _closeToolPanel = () => {
|
||||||
if (!this._isPanelOpened) return;
|
if (!this.panelOpened) return;
|
||||||
|
|
||||||
this._currentPanelIndex$.value = -1;
|
this._currentPanelIndex$.value = -1;
|
||||||
this._keyboardController.show();
|
this.keyboard.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _currentPanelIndex$ = signal(-1);
|
private readonly _currentPanelIndex$ = signal(-1);
|
||||||
@@ -55,7 +70,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
private readonly _goPrevToolbar = () => {
|
private readonly _goPrevToolbar = () => {
|
||||||
if (!this._isSubToolbarOpened) return;
|
if (!this._isSubToolbarOpened) return;
|
||||||
|
|
||||||
if (this._isPanelOpened) this._closeToolPanel();
|
if (this.panelOpened) this._closeToolPanel();
|
||||||
|
|
||||||
this._path$.value = this._path$.value.slice(0, -1);
|
this._path$.value = this._path$.value.slice(0, -1);
|
||||||
};
|
};
|
||||||
@@ -75,31 +90,25 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
this._closeToolPanel();
|
this._closeToolPanel();
|
||||||
} else {
|
} else {
|
||||||
this._currentPanelIndex$.value = index;
|
this._currentPanelIndex$.value = index;
|
||||||
this._keyboardController.hide();
|
this.keyboard.hide();
|
||||||
this.scrollCurrentBlockIntoView();
|
this._scrollCurrentBlockIntoView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._lastActiveItem$.value = item;
|
this._lastActiveItem$.value = item;
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _keyboardController = new VirtualKeyboardController(this);
|
|
||||||
|
|
||||||
private readonly _lastActiveItem$ = signal<KeyboardToolbarItem | null>(null);
|
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 _path$ = signal<number[]>([]);
|
||||||
|
|
||||||
private readonly scrollCurrentBlockIntoView = () => {
|
private readonly _scrollCurrentBlockIntoView = () => {
|
||||||
const { std } = this.rootComponent;
|
this.std.command
|
||||||
std.command
|
|
||||||
.chain()
|
.chain()
|
||||||
.pipe(getSelectedModelsCommand)
|
.pipe(getSelectedModelsCommand)
|
||||||
.pipe(({ selectedModels }) => {
|
.pipe(({ selectedModels }) => {
|
||||||
if (!selectedModels?.length) return;
|
if (!selectedModels?.length) return;
|
||||||
|
|
||||||
const block = std.view.getBlock(selectedModels[0].id);
|
const block = this.std.view.getBlock(selectedModels[0].id);
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
|
|
||||||
const { y: y1 } = this.getBoundingClientRect();
|
const { y: y1 } = this.getBoundingClientRect();
|
||||||
@@ -118,7 +127,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
|
|
||||||
private get _context(): KeyboardToolbarContext {
|
private get _context(): KeyboardToolbarContext {
|
||||||
return {
|
return {
|
||||||
std: this.rootComponent.std,
|
std: this.std,
|
||||||
rootComponent: this.rootComponent,
|
rootComponent: this.rootComponent,
|
||||||
closeToolbar: (blur = false) => {
|
closeToolbar: (blur = false) => {
|
||||||
this.close(blur);
|
this.close(blur);
|
||||||
@@ -130,7 +139,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _currentPanelConfig(): KeyboardToolPanelConfig | null {
|
private get _currentPanelConfig(): KeyboardToolPanelConfig | null {
|
||||||
if (!this._isPanelOpened) return null;
|
if (!this.panelOpened) return null;
|
||||||
|
|
||||||
const result = this._currentToolbarItems[this._currentPanelIndex$.value];
|
const result = this._currentToolbarItems[this._currentPanelIndex$.value];
|
||||||
|
|
||||||
@@ -139,9 +148,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
|
|
||||||
private get _currentToolbarItems(): KeyboardToolbarItem[] {
|
private get _currentToolbarItems(): KeyboardToolbarItem[] {
|
||||||
let items = this.config.items;
|
let items = this.config.items;
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
for (const index of this._path$.value) {
|
||||||
for (let i = 0; i < this._path$.value.length; i++) {
|
|
||||||
const index = this._path$.value[i];
|
|
||||||
if (isKeyboardSubToolBarConfig(items[index])) {
|
if (isKeyboardSubToolBarConfig(items[index])) {
|
||||||
items = items[index].items;
|
items = items[index].items;
|
||||||
} else {
|
} else {
|
||||||
@@ -156,21 +163,10 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _isPanelOpened() {
|
|
||||||
return this._currentPanelIndex$.value !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get _isSubToolbarOpened() {
|
private get _isSubToolbarOpened() {
|
||||||
return this._path$.value.length > 0;
|
return this._path$.value.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get virtualKeyboardControllerConfig(): VirtualKeyboardControllerConfig {
|
|
||||||
return {
|
|
||||||
useScreenHeight: this.config.useScreenHeight ?? false,
|
|
||||||
inputElement: this.rootComponent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderIcon(icon: KeyboardIconType) {
|
private _renderIcon(icon: KeyboardIconType) {
|
||||||
return typeof icon === 'function' ? icon(this._context) : icon;
|
return typeof icon === 'function' ? icon(this._context) : icon;
|
||||||
}
|
}
|
||||||
@@ -252,35 +248,13 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
e.preventDefault();
|
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(
|
this.disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const std = this.rootComponent.std;
|
const std = this.rootComponent.std;
|
||||||
std.selection.value;
|
std.selection.value;
|
||||||
// wait cursor updated
|
// wait cursor updated
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.scrollCurrentBlockIntoView();
|
this._scrollCurrentBlockIntoView();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -328,24 +302,14 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
document.body.style.paddingBottom = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
// workaround for the virtual keyboard showing transition animation
|
// workaround for the virtual keyboard showing transition animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.scrollCurrentBlockIntoView();
|
this._scrollCurrentBlockIntoView();
|
||||||
}, 700);
|
}, 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
this.style.bottom =
|
|
||||||
this.config.useScreenHeight && this._keyboardController.opened
|
|
||||||
? `${-this._panelHeight$.value}px`
|
|
||||||
: '0px';
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="keyboard-toolbar">
|
<div class="keyboard-toolbar">
|
||||||
${this._renderItems()}
|
${this._renderItems()}
|
||||||
@@ -355,7 +319,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
<affine-keyboard-tool-panel
|
<affine-keyboard-tool-panel
|
||||||
.config=${this._currentPanelConfig}
|
.config=${this._currentPanelConfig}
|
||||||
.context=${this._context}
|
.context=${this._context}
|
||||||
height=${this._panelHeight$.value}
|
height=${this.panelHeight$.value}
|
||||||
></affine-keyboard-tool-panel>
|
></affine-keyboard-tool-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,6 @@ export interface LinkedWidgetConfig {
|
|||||||
) => string | null;
|
) => string | null;
|
||||||
|
|
||||||
mobile: {
|
mobile: {
|
||||||
useScreenHeight?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* The linked doc menu widget will scroll the container to make sure the input cursor is visible in viewport.
|
* 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
|
* It accepts a selector string, HTMLElement or Window
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export class AffineLinkedDocWidget extends WidgetComponent<
|
|||||||
convertTriggerKey: true,
|
convertTriggerKey: true,
|
||||||
getMenus,
|
getMenus,
|
||||||
mobile: {
|
mobile: {
|
||||||
useScreenHeight: false,
|
|
||||||
scrollContainer: getViewportElement(this.std.host) ?? window,
|
scrollContainer: getViewportElement(this.std.host) ?? window,
|
||||||
scrollTopOffset: 46,
|
scrollTopOffset: 46,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import {
|
|||||||
cleanSpecifiedTail,
|
cleanSpecifiedTail,
|
||||||
getTextContentFromInlineRange,
|
getTextContentFromInlineRange,
|
||||||
} from '@blocksuite/affine-components/rich-text';
|
} from '@blocksuite/affine-components/rich-text';
|
||||||
import {
|
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
|
||||||
VirtualKeyboardController,
|
|
||||||
type VirtualKeyboardControllerConfig,
|
|
||||||
} from '@blocksuite/affine-components/virtual-keyboard';
|
|
||||||
import {
|
import {
|
||||||
createKeydownObserver,
|
createKeydownObserver,
|
||||||
getViewportElement,
|
getViewportElement,
|
||||||
@@ -43,8 +40,6 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
|
|||||||
|
|
||||||
private _firstActionItem: LinkedMenuItem | null = null;
|
private _firstActionItem: LinkedMenuItem | null = null;
|
||||||
|
|
||||||
private readonly _keyboardController = new VirtualKeyboardController(this);
|
|
||||||
|
|
||||||
private readonly _linkedDocGroup$ = signal<LinkedMenuGroup[]>([]);
|
private readonly _linkedDocGroup$ = signal<LinkedMenuGroup[]>([]);
|
||||||
|
|
||||||
private readonly _renderGroup = (group: LinkedMenuGroup) => {
|
private readonly _renderGroup = (group: LinkedMenuGroup) => {
|
||||||
@@ -159,11 +154,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get virtualKeyboardControllerConfig(): VirtualKeyboardControllerConfig {
|
get keyboard() {
|
||||||
return {
|
return this.context.std.get(VirtualKeyboardProvider);
|
||||||
useScreenHeight: this.context.config.mobile.useScreenHeight ?? false,
|
|
||||||
inputElement: this.rootComponent,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
@@ -230,8 +222,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
if (!this._keyboardController.opened) {
|
if (!this.keyboard.visible$.value) {
|
||||||
this._keyboardController.show();
|
this.keyboard.show();
|
||||||
}
|
}
|
||||||
this._scrollInputToTop();
|
this._scrollInputToTop();
|
||||||
}
|
}
|
||||||
@@ -244,11 +236,7 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
|
|||||||
|
|
||||||
this._firstActionItem = resolveSignal(groups[0].items)[0];
|
this._firstActionItem = resolveSignal(groups[0].items)[0];
|
||||||
|
|
||||||
this.style.bottom =
|
this.style.bottom = `${this.keyboard.height$.value}px`;
|
||||||
this.context.config.mobile.useScreenHeight &&
|
|
||||||
this._keyboardController.opened
|
|
||||||
? '0px'
|
|
||||||
: `max(0px, ${this._keyboardController.keyboardHeight}px)`;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${join(groups.map(this._renderGroup), html`<div class="divider"></div>`)}
|
${join(groups.map(this._renderGroup), html`<div class="divider"></div>`)}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
"./date-picker": "./src/date-picker/index.ts",
|
"./date-picker": "./src/date-picker/index.ts",
|
||||||
"./drop-indicator": "./src/drop-indicator/index.ts",
|
"./drop-indicator": "./src/drop-indicator/index.ts",
|
||||||
"./filterable-list": "./src/filterable-list/index.ts",
|
"./filterable-list": "./src/filterable-list/index.ts",
|
||||||
"./virtual-keyboard": "./src/virtual-keyboard/index.ts",
|
|
||||||
"./toggle-button": "./src/toggle-button/index.ts",
|
"./toggle-button": "./src/toggle-button/index.ts",
|
||||||
"./toggle-switch": "./src/toggle-switch/index.ts",
|
"./toggle-switch": "./src/toggle-switch/index.ts",
|
||||||
"./notification": "./src/notification/index.ts",
|
"./notification": "./src/notification/index.ts",
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
|||||||
return this._richTextElement.inlineEditor;
|
return this._richTextElement.inlineEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get inlineEditorContainer() {
|
||||||
|
return this._richTextElement.inlineEditorContainer;
|
||||||
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
import { IS_IOS } from '@blocksuite/global/env';
|
|
||||||
import type * as GlobalTypes from '@blocksuite/global/types';
|
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
|
||||||
import { signal } from '@preact/signals-core';
|
|
||||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
||||||
|
|
||||||
declare type _GLOBAL_ = typeof GlobalTypes;
|
|
||||||
|
|
||||||
function notSupportedWarning() {
|
|
||||||
console.warn('VirtualKeyboard API and VisualViewport API are not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VirtualKeyboardControllerConfig = {
|
|
||||||
useScreenHeight: boolean;
|
|
||||||
inputElement: HTMLElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class VirtualKeyboardController implements ReactiveController {
|
|
||||||
private readonly _disposables = new DisposableGroup();
|
|
||||||
|
|
||||||
private readonly _keyboardHeight$ = signal(0);
|
|
||||||
|
|
||||||
private readonly _keyboardOpened$ = signal(false);
|
|
||||||
|
|
||||||
private readonly _storeInitialInputElementAttributes = () => {
|
|
||||||
const { inputElement } = this.config;
|
|
||||||
if (navigator.virtualKeyboard) {
|
|
||||||
const { overlaysContent } = navigator.virtualKeyboard;
|
|
||||||
const { virtualKeyboardPolicy } = inputElement;
|
|
||||||
|
|
||||||
this._disposables.add(() => {
|
|
||||||
if (!navigator.virtualKeyboard) return;
|
|
||||||
navigator.virtualKeyboard.overlaysContent = overlaysContent;
|
|
||||||
inputElement.virtualKeyboardPolicy = virtualKeyboardPolicy;
|
|
||||||
});
|
|
||||||
} else if (visualViewport) {
|
|
||||||
const { inputMode } = inputElement;
|
|
||||||
this._disposables.add(() => {
|
|
||||||
inputElement.inputMode = inputMode;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _updateKeyboardHeight = () => {
|
|
||||||
const { virtualKeyboard } = navigator;
|
|
||||||
if (virtualKeyboard) {
|
|
||||||
this._keyboardOpened$.value = virtualKeyboard.boundingRect.height > 0;
|
|
||||||
this._keyboardHeight$.value = virtualKeyboard.boundingRect.height;
|
|
||||||
} else if (visualViewport) {
|
|
||||||
const windowHeight = this.config.useScreenHeight
|
|
||||||
? window.screen.height
|
|
||||||
: window.innerHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ┌───────────────┐ - window top
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* └───────────────┘ - keyboard top --
|
|
||||||
* │ │ │ keyboard height in layout viewport
|
|
||||||
* └───────────────┘ - page(html) bottom --
|
|
||||||
* │ │ │ visualViewport.offsetTop
|
|
||||||
* └───────────────┘ - window bottom --
|
|
||||||
*/
|
|
||||||
this._keyboardOpened$.value = windowHeight - visualViewport.height > 0;
|
|
||||||
this._keyboardHeight$.value =
|
|
||||||
windowHeight -
|
|
||||||
visualViewport.height -
|
|
||||||
(IS_IOS ? 0 : visualViewport.offsetTop);
|
|
||||||
} else {
|
|
||||||
notSupportedWarning();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
hide = () => {
|
|
||||||
if (navigator.virtualKeyboard) {
|
|
||||||
navigator.virtualKeyboard.hide();
|
|
||||||
} else {
|
|
||||||
this.config.inputElement.inputMode = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
host: ReactiveControllerHost & {
|
|
||||||
virtualKeyboardControllerConfig: VirtualKeyboardControllerConfig;
|
|
||||||
hasUpdated: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
show = () => {
|
|
||||||
if (navigator.virtualKeyboard) {
|
|
||||||
navigator.virtualKeyboard.show();
|
|
||||||
} else {
|
|
||||||
this.config.inputElement.inputMode = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
toggle = () => {
|
|
||||||
if (this.opened) {
|
|
||||||
this.hide();
|
|
||||||
} else {
|
|
||||||
this.show();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
get config() {
|
|
||||||
return this.host.virtualKeyboardControllerConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the height of keyboard in layout viewport
|
|
||||||
* see comment in the `_updateKeyboardHeight` method
|
|
||||||
*/
|
|
||||||
get keyboardHeight() {
|
|
||||||
return this._keyboardHeight$.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get opened() {
|
|
||||||
return this._keyboardOpened$.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(host: VirtualKeyboardController['host']) {
|
|
||||||
(this.host = host).addController(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConnected() {
|
|
||||||
this._storeInitialInputElementAttributes();
|
|
||||||
|
|
||||||
const { inputElement } = this.config;
|
|
||||||
|
|
||||||
if (navigator.virtualKeyboard) {
|
|
||||||
navigator.virtualKeyboard.overlaysContent = true;
|
|
||||||
this.config.inputElement.virtualKeyboardPolicy = 'manual';
|
|
||||||
|
|
||||||
this._disposables.addFromEvent(
|
|
||||||
navigator.virtualKeyboard,
|
|
||||||
'geometrychange',
|
|
||||||
this._updateKeyboardHeight
|
|
||||||
);
|
|
||||||
} else if (visualViewport) {
|
|
||||||
this._disposables.addFromEvent(
|
|
||||||
visualViewport,
|
|
||||||
'resize',
|
|
||||||
this._updateKeyboardHeight
|
|
||||||
);
|
|
||||||
this._disposables.addFromEvent(
|
|
||||||
visualViewport,
|
|
||||||
'scroll',
|
|
||||||
this._updateKeyboardHeight
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notSupportedWarning();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._disposables.addFromEvent(inputElement, 'focus', this.show);
|
|
||||||
this._disposables.addFromEvent(inputElement, 'blur', this.hide);
|
|
||||||
|
|
||||||
this._updateKeyboardHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
hostDisconnected() {
|
|
||||||
this._disposables.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './controller.js';
|
|
||||||
@@ -18,3 +18,4 @@ export * from './quick-search-service';
|
|||||||
export * from './sidebar-service';
|
export * from './sidebar-service';
|
||||||
export * from './telemetry-service';
|
export * from './telemetry-service';
|
||||||
export * from './theme-service';
|
export * from './theme-service';
|
||||||
|
export * from './virtual-keyboard-service';
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
|
import type { ReadonlySignal } from '@preact/signals-core';
|
||||||
|
|
||||||
|
export interface VirtualKeyboardProvider {
|
||||||
|
show: () => void;
|
||||||
|
hide: () => void;
|
||||||
|
readonly visible$: ReadonlySignal<boolean>;
|
||||||
|
readonly height$: ReadonlySignal<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VirtualKeyboardProvider =
|
||||||
|
createIdentifier<VirtualKeyboardProvider>('VirtualKeyboardProvider');
|
||||||
@@ -97,15 +97,48 @@ framework.impl(ClientSchemeProvider, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
framework.impl(VirtualKeyboardProvider, {
|
framework.impl(VirtualKeyboardProvider, {
|
||||||
addEventListener: (event, callback) => {
|
show: () => {
|
||||||
Keyboard.addListener(event as any, callback as any).catch(e => {
|
Keyboard.show().catch(console.error);
|
||||||
console.error(e);
|
},
|
||||||
|
hide: () => {
|
||||||
|
// In some cases, the keyboard will show again. for example, it will show again
|
||||||
|
// when this function is called in click event of button. It may be a bug of
|
||||||
|
// android webview or capacitor.
|
||||||
|
setTimeout(() => {
|
||||||
|
Keyboard.hide().catch(console.error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeAllListeners: () => {
|
onChange: callback => {
|
||||||
Keyboard.removeAllListeners().catch(e => {
|
let disposeRef = {
|
||||||
console.error(e);
|
dispose: () => {},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
Keyboard.addListener('keyboardWillShow', info => {
|
||||||
|
callback({
|
||||||
|
visible: true,
|
||||||
|
height: info.keyboardHeight,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
Keyboard.addListener('keyboardWillHide', () => {
|
||||||
|
callback({
|
||||||
|
visible: false,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(handlers => {
|
||||||
|
disposeRef.dispose = () => {
|
||||||
|
Promise.all(handlers.map(handler => handler.remove())).catch(
|
||||||
|
console.error
|
||||||
|
);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposeRef.dispose();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const config: CapacitorConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
Keyboard: {
|
Keyboard: {
|
||||||
resize: KeyboardResize.Native,
|
resize: KeyboardResize.None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,15 +106,40 @@ framework.impl(ValidatorProvider, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
framework.impl(VirtualKeyboardProvider, {
|
framework.impl(VirtualKeyboardProvider, {
|
||||||
addEventListener: (event, callback) => {
|
// We dose not provide show and hide because:
|
||||||
Keyboard.addListener(event as any, callback as any).catch(e => {
|
// - Keyboard.show() is not implemented
|
||||||
console.error(e);
|
// - Keyboard.hide() will blur the current editor
|
||||||
});
|
onChange: callback => {
|
||||||
},
|
let disposeRef = {
|
||||||
removeAllListeners: () => {
|
dispose: () => {},
|
||||||
Keyboard.removeAllListeners().catch(e => {
|
};
|
||||||
console.error(e);
|
|
||||||
});
|
Promise.all([
|
||||||
|
Keyboard.addListener('keyboardDidShow', info => {
|
||||||
|
callback({
|
||||||
|
visible: true,
|
||||||
|
height: info.keyboardHeight,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
Keyboard.addListener('keyboardWillHide', () => {
|
||||||
|
callback({
|
||||||
|
visible: false,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(handlers => {
|
||||||
|
disposeRef.dispose = () => {
|
||||||
|
Promise.all(handlers.map(handler => handler.remove())).catch(
|
||||||
|
console.error
|
||||||
|
);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposeRef.dispose();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
framework.impl(NavigationGestureProvider, {
|
framework.impl(NavigationGestureProvider, {
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import type { PluginListenerHandle } from '@capacitor/core/types/definitions';
|
|
||||||
import { Keyboard } from '@capacitor/keyboard';
|
|
||||||
|
|
||||||
type VirtualKeyboardCallback =
|
|
||||||
| (<K extends keyof VirtualKeyboardEventMap>(
|
|
||||||
this: VirtualKeyboard,
|
|
||||||
ev: VirtualKeyboardEventMap[K]
|
|
||||||
) => any)
|
|
||||||
| EventListenerOrEventListenerObject;
|
|
||||||
|
|
||||||
class NavigatorVirtualKeyboard implements VirtualKeyboard {
|
|
||||||
private readonly _boundingRect = new DOMRect();
|
|
||||||
|
|
||||||
private readonly _overlaysContent = false;
|
|
||||||
|
|
||||||
private readonly _listeners = new Map<
|
|
||||||
string,
|
|
||||||
Set<{
|
|
||||||
cb: VirtualKeyboardCallback;
|
|
||||||
options?: boolean | AddEventListenerOptions;
|
|
||||||
}>
|
|
||||||
>();
|
|
||||||
|
|
||||||
private _capacitorListenerHandles: PluginListenerHandle[] = [];
|
|
||||||
|
|
||||||
private async _bindListener() {
|
|
||||||
const updateBoundingRect = (info?: { keyboardHeight: number }) => {
|
|
||||||
this.boundingRect.x = 0;
|
|
||||||
this.boundingRect.y = info ? window.innerHeight - info.keyboardHeight : 0;
|
|
||||||
this.boundingRect.width = window.innerWidth;
|
|
||||||
this.boundingRect.height = info ? info.keyboardHeight : 0;
|
|
||||||
this.dispatchEvent(new Event('geometrychange'));
|
|
||||||
};
|
|
||||||
|
|
||||||
this._capacitorListenerHandles = [
|
|
||||||
await Keyboard.addListener('keyboardDidShow', updateBoundingRect),
|
|
||||||
await Keyboard.addListener('keyboardDidHide', updateBoundingRect),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent = (event: Event) => {
|
|
||||||
const listeners = this._listeners.get(event.type);
|
|
||||||
if (listeners) {
|
|
||||||
for (const l of listeners) {
|
|
||||||
if (typeof l.cb === 'function') {
|
|
||||||
l.cb.call(this, event);
|
|
||||||
} else {
|
|
||||||
l.cb.handleEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !(event.cancelable && event.defaultPrevented);
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._bindListener().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._capacitorListenerHandles.forEach(handle => {
|
|
||||||
handle.remove().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get boundingRect(): DOMRect {
|
|
||||||
return this._boundingRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
get overlaysContent(): boolean {
|
|
||||||
return this._overlaysContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
set overlaysContent(_: boolean) {
|
|
||||||
console.warn(
|
|
||||||
'overlaysContent is read-only in polyfill based on @capacitor/keyboard'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
Keyboard.hide().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
Keyboard.show().catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ongeometrychange: ((this: VirtualKeyboard, ev: Event) => any) | null = null;
|
|
||||||
|
|
||||||
addEventListener<K extends keyof VirtualKeyboardEventMap>(
|
|
||||||
type: K,
|
|
||||||
listener: VirtualKeyboardCallback,
|
|
||||||
options?: boolean | AddEventListenerOptions
|
|
||||||
) {
|
|
||||||
if (!this._listeners.has(type)) {
|
|
||||||
this._listeners.set(type, new Set());
|
|
||||||
}
|
|
||||||
|
|
||||||
const listeners = this._listeners.get(type);
|
|
||||||
if (!listeners) return;
|
|
||||||
|
|
||||||
listeners.add({ cb: listener, options });
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEventListener<K extends keyof VirtualKeyboardEventMap>(
|
|
||||||
type: K,
|
|
||||||
listener: VirtualKeyboardCallback,
|
|
||||||
options?: boolean | EventListenerOptions
|
|
||||||
) {
|
|
||||||
const listeners = this._listeners.get(type);
|
|
||||||
if (!listeners) return;
|
|
||||||
|
|
||||||
const sameCapture = (
|
|
||||||
a?: boolean | AddEventListenerOptions,
|
|
||||||
b?: boolean | EventListenerOptions
|
|
||||||
) => {
|
|
||||||
if (a === undefined && b === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
||||||
return a === b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof a === 'object' && typeof b === 'object') {
|
|
||||||
return a.capture === b.capture;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof a === 'object' && typeof b === 'boolean') {
|
|
||||||
return a.capture === b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof a === 'boolean' && typeof b === 'object') {
|
|
||||||
return a === b.capture;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let target = null;
|
|
||||||
for (const l of listeners) {
|
|
||||||
if (l.cb === listener && sameCapture(l.options, options)) {
|
|
||||||
target = l;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
listeners.delete(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error polyfill
|
|
||||||
navigator.virtualKeyboard = new NavigatorVirtualKeyboard();
|
|
||||||
@@ -2,6 +2,7 @@ import { AffineContext } from '@affine/core/components/context';
|
|||||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||||
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
||||||
|
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||||
import { router } from '@affine/core/mobile/router';
|
import { router } from '@affine/core/mobile/router';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||||
@@ -99,6 +100,44 @@ framework.impl(HapticProvider, {
|
|||||||
selectionChanged: () => Promise.reject('Not supported'),
|
selectionChanged: () => Promise.reject('Not supported'),
|
||||||
selectionEnd: () => Promise.reject('Not supported'),
|
selectionEnd: () => Promise.reject('Not supported'),
|
||||||
});
|
});
|
||||||
|
framework.impl(VirtualKeyboardProvider, {
|
||||||
|
onChange: callback => {
|
||||||
|
if (!visualViewport) {
|
||||||
|
console.warn('visualViewport is not supported');
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
if (!visualViewport) return;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ┌───────────────┐ - window top
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* └───────────────┘ - keyboard top --
|
||||||
|
* │ │ │ keyboard height in layout viewport
|
||||||
|
* └───────────────┘ - page(html) bottom --
|
||||||
|
* │ │ │ visualViewport.offsetTop
|
||||||
|
* └───────────────┘ - window bottom --
|
||||||
|
*/
|
||||||
|
callback({
|
||||||
|
visible: window.innerHeight - visualViewport.height > 0,
|
||||||
|
height: windowHeight - visualViewport.height - visualViewport.offsetTop,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
visualViewport.addEventListener('resize', listener);
|
||||||
|
visualViewport.addEventListener('scroll', listener);
|
||||||
|
return () => {
|
||||||
|
visualViewport?.removeEventListener('resize', listener);
|
||||||
|
visualViewport?.removeEventListener('scroll', listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
const frameworkProvider = framework.provider();
|
const frameworkProvider = framework.provider();
|
||||||
|
|
||||||
// setup application lifecycle events, and emit application start event
|
// setup application lifecycle events, and emit application start event
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
builder.extend([patchForAttachmentEmbedViews(reactToLit)]);
|
builder.extend([patchForAttachmentEmbedViews(reactToLit)]);
|
||||||
}
|
}
|
||||||
if (BUILD_CONFIG.isMobileEdition) {
|
if (BUILD_CONFIG.isMobileEdition) {
|
||||||
enableMobileExtension(builder);
|
enableMobileExtension(builder, framework);
|
||||||
}
|
}
|
||||||
if (BUILD_CONFIG.isElectron) {
|
if (BUILD_CONFIG.isElectron) {
|
||||||
builder.extend([patchForClipboardInElectron(framework)].flat());
|
builder.extend([patchForClipboardInElectron(framework)].flat());
|
||||||
|
|||||||
@@ -1,29 +1,36 @@
|
|||||||
import { createKeyboardToolbarConfig } from '@affine/core/blocksuite/extensions/keyboard-toolbar-config';
|
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||||
import {
|
import {
|
||||||
type BlockStdScope,
|
type BlockStdScope,
|
||||||
ConfigIdentifier,
|
ConfigIdentifier,
|
||||||
LifeCycleWatcher,
|
LifeCycleWatcher,
|
||||||
|
LifeCycleWatcherIdentifier,
|
||||||
} from '@blocksuite/affine/block-std';
|
} from '@blocksuite/affine/block-std';
|
||||||
import type {
|
import type {
|
||||||
CodeBlockConfig,
|
CodeBlockConfig,
|
||||||
ReferenceNodeConfig,
|
ReferenceNodeConfig,
|
||||||
RootBlockConfig,
|
|
||||||
SpecBuilder,
|
SpecBuilder,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import {
|
import {
|
||||||
codeToolbarWidget,
|
codeToolbarWidget,
|
||||||
|
DocModeProvider,
|
||||||
embedCardToolbarWidget,
|
embedCardToolbarWidget,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
formatBarWidget,
|
formatBarWidget,
|
||||||
imageToolbarWidget,
|
imageToolbarWidget,
|
||||||
ParagraphBlockService,
|
ParagraphBlockService,
|
||||||
ReferenceNodeConfigIdentifier,
|
ReferenceNodeConfigIdentifier,
|
||||||
RootBlockConfigExtension,
|
|
||||||
slashMenuWidget,
|
slashMenuWidget,
|
||||||
surfaceRefToolbarWidget,
|
surfaceRefToolbarWidget,
|
||||||
|
VirtualKeyboardProvider as BSVirtualKeyboardProvider,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import type { Container } from '@blocksuite/affine/global/di';
|
import type {
|
||||||
|
Container,
|
||||||
|
ServiceIdentifier,
|
||||||
|
} from '@blocksuite/affine/global/di';
|
||||||
|
import { DisposableGroup } from '@blocksuite/affine/global/utils';
|
||||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||||
|
import { batch, signal } from '@preact/signals-core';
|
||||||
|
import type { FrameworkProvider } from '@toeverything/infra';
|
||||||
|
|
||||||
class MobileSpecsPatches extends LifeCycleWatcher {
|
class MobileSpecsPatches extends LifeCycleWatcher {
|
||||||
static override key = 'mobile-patches';
|
static override key = 'mobile-patches';
|
||||||
@@ -86,28 +93,85 @@ class MobileSpecsPatches extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileExtensions: ExtensionType[] = [
|
function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType {
|
||||||
|
const affineVirtualKeyboardProvider = framework.get(VirtualKeyboardProvider);
|
||||||
|
|
||||||
|
class BSVirtualKeyboardService
|
||||||
|
extends LifeCycleWatcher
|
||||||
|
implements BSVirtualKeyboardProvider
|
||||||
{
|
{
|
||||||
setup: di => {
|
static override key = BSVirtualKeyboardProvider.identifierName;
|
||||||
const prev = di.getFactory(RootBlockConfigExtension.identifier);
|
|
||||||
|
|
||||||
di.override(RootBlockConfigExtension.identifier, provider => {
|
private readonly _disposables = new DisposableGroup();
|
||||||
return {
|
|
||||||
...prev?.(provider),
|
private get _rootContentEditable() {
|
||||||
keyboardToolbar: createKeyboardToolbarConfig(),
|
const editorMode = this.std.get(DocModeProvider).getEditorMode();
|
||||||
} satisfies RootBlockConfig;
|
if (editorMode !== 'page') return null;
|
||||||
|
|
||||||
|
if (!this.std.host.doc.root) return;
|
||||||
|
return this.std.view.getBlock(this.std.host.doc.root.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs/finnish
|
||||||
|
readonly visible$ = signal(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs/finnish
|
||||||
|
readonly height$ = signal(0);
|
||||||
|
|
||||||
|
show() {
|
||||||
|
if ('show' in affineVirtualKeyboardProvider) {
|
||||||
|
affineVirtualKeyboardProvider.show();
|
||||||
|
} else if (this._rootContentEditable) {
|
||||||
|
this._rootContentEditable.inputMode = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hide() {
|
||||||
|
if ('hide' in affineVirtualKeyboardProvider) {
|
||||||
|
affineVirtualKeyboardProvider.hide();
|
||||||
|
} else if (this._rootContentEditable) {
|
||||||
|
this._rootContentEditable.inputMode = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static override setup(di: Container) {
|
||||||
|
super.setup(di);
|
||||||
|
di.addImpl(BSVirtualKeyboardProvider, provider => {
|
||||||
|
return provider.get(
|
||||||
|
LifeCycleWatcherIdentifier(
|
||||||
|
this.key
|
||||||
|
) as ServiceIdentifier<BSVirtualKeyboardService>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
},
|
|
||||||
MobileSpecsPatches,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function enableMobileExtension(specBuilder: SpecBuilder): void {
|
override mounted() {
|
||||||
|
this._disposables.add(
|
||||||
|
affineVirtualKeyboardProvider.onChange(({ visible, height }) => {
|
||||||
|
batch(() => {
|
||||||
|
this.visible$.value = visible;
|
||||||
|
this.height$.value = height;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override unmounted() {
|
||||||
|
this._disposables.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSVirtualKeyboardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableMobileExtension(
|
||||||
|
specBuilder: SpecBuilder,
|
||||||
|
framework: FrameworkProvider
|
||||||
|
): void {
|
||||||
specBuilder.omit(formatBarWidget);
|
specBuilder.omit(formatBarWidget);
|
||||||
specBuilder.omit(embedCardToolbarWidget);
|
specBuilder.omit(embedCardToolbarWidget);
|
||||||
specBuilder.omit(slashMenuWidget);
|
specBuilder.omit(slashMenuWidget);
|
||||||
specBuilder.omit(codeToolbarWidget);
|
specBuilder.omit(codeToolbarWidget);
|
||||||
specBuilder.omit(imageToolbarWidget);
|
specBuilder.omit(imageToolbarWidget);
|
||||||
specBuilder.omit(surfaceRefToolbarWidget);
|
specBuilder.omit(surfaceRefToolbarWidget);
|
||||||
specBuilder.extend(mobileExtensions);
|
specBuilder.extend([MobileSpecsPatches, KeyboardToolbarExtension(framework)]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { type KeyboardToolbarConfig } from '@blocksuite/affine/blocks';
|
|
||||||
|
|
||||||
export function createKeyboardToolbarConfig(): Partial<KeyboardToolbarConfig> {
|
|
||||||
return {
|
|
||||||
// TODO(@L-Sun): check android following the PR
|
|
||||||
// https://github.com/toeverything/blocksuite/pull/8645
|
|
||||||
useScreenHeight: BUILD_CONFIG.isIOS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ export const AppTabs = ({
|
|||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const virtualKeyboardService = useService(VirtualKeyboardService);
|
const virtualKeyboardService = useService(VirtualKeyboardService);
|
||||||
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.show$);
|
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.visible$);
|
||||||
|
|
||||||
const tab = (
|
const tab = (
|
||||||
<SafeArea
|
<SafeArea
|
||||||
|
|||||||
@@ -6,8 +6,5 @@ import { VirtualKeyboardService } from './services/virtual-keyboard';
|
|||||||
export { VirtualKeyboardProvider, VirtualKeyboardService };
|
export { VirtualKeyboardProvider, VirtualKeyboardService };
|
||||||
|
|
||||||
export function configureMobileVirtualKeyboardModule(framework: Framework) {
|
export function configureMobileVirtualKeyboardModule(framework: Framework) {
|
||||||
framework.service(
|
framework.service(VirtualKeyboardService, [VirtualKeyboardProvider]);
|
||||||
VirtualKeyboardService,
|
|
||||||
f => new VirtualKeyboardService(f.getOptional(VirtualKeyboardProvider))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import { createIdentifier } from '@toeverything/infra';
|
import { createIdentifier } from '@toeverything/infra';
|
||||||
|
|
||||||
export type VirtualKeyboardEvent =
|
interface VirtualKeyboardInfo {
|
||||||
| 'keyboardWillShow'
|
visible: boolean;
|
||||||
| 'keyboardDidShow'
|
height: number;
|
||||||
| 'keyboardWillHide'
|
|
||||||
| 'keyboardDidHide';
|
|
||||||
|
|
||||||
export interface VirtualKeyboardEventInfo {
|
|
||||||
keyboardHeight: number;
|
|
||||||
}
|
}
|
||||||
type VirtualKeyboardEventListener = (info: VirtualKeyboardEventInfo) => void;
|
|
||||||
|
|
||||||
export interface VirtualKeyboardProvider {
|
type VirtualKeyboardAction = {
|
||||||
addEventListener: (
|
/**
|
||||||
event: VirtualKeyboardEvent,
|
* Open the virtual keyboard, the focused element should not be changed
|
||||||
callback: VirtualKeyboardEventListener
|
*/
|
||||||
) => void;
|
show: () => void;
|
||||||
removeAllListeners: () => void;
|
/**
|
||||||
}
|
* Hide the virtual keyboard, the focused element should not be changed
|
||||||
|
*/
|
||||||
|
hide: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VirtualKeyboardEvent = {
|
||||||
|
onChange: (callback: (info: VirtualKeyboardInfo) => void) => () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VirtualKeyboardProvider =
|
||||||
|
| (VirtualKeyboardEvent & VirtualKeyboardAction)
|
||||||
|
| VirtualKeyboardEvent;
|
||||||
|
|
||||||
export const VirtualKeyboardProvider =
|
export const VirtualKeyboardProvider =
|
||||||
createIdentifier<VirtualKeyboardProvider>('VirtualKeyboardProvider');
|
createIdentifier<VirtualKeyboardProvider>('VirtualKeyboardProvider');
|
||||||
|
|||||||
@@ -3,32 +3,23 @@ import { LiveData, Service } from '@toeverything/infra';
|
|||||||
import type { VirtualKeyboardProvider } from '../providers/virtual-keyboard';
|
import type { VirtualKeyboardProvider } from '../providers/virtual-keyboard';
|
||||||
|
|
||||||
export class VirtualKeyboardService extends Service {
|
export class VirtualKeyboardService extends Service {
|
||||||
show$ = new LiveData(false);
|
readonly visible$ = new LiveData(false);
|
||||||
height$ = new LiveData(0);
|
|
||||||
|
readonly height$ = new LiveData(0);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly virtualKeyboardProvider?: VirtualKeyboardProvider
|
private readonly virtualKeyboardProvider: VirtualKeyboardProvider
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._observe();
|
this._observe();
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose() {
|
|
||||||
super.dispose();
|
|
||||||
this.virtualKeyboardProvider?.removeAllListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _observe() {
|
private _observe() {
|
||||||
this.virtualKeyboardProvider?.addEventListener(
|
this.disposables.push(
|
||||||
'keyboardWillShow',
|
this.virtualKeyboardProvider.onChange(info => {
|
||||||
({ keyboardHeight }) => {
|
this.visible$.next(info.visible);
|
||||||
this.show$.next(true);
|
this.height$.next(info.height);
|
||||||
this.height$.next(keyboardHeight);
|
})
|
||||||
}
|
|
||||||
);
|
);
|
||||||
this.virtualKeyboardProvider?.addEventListener('keyboardWillHide', () => {
|
|
||||||
this.show$.next(false);
|
|
||||||
this.height$.next(0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ globalStyle('body', {
|
|||||||
globalStyle('body:has(> #app-tabs)', {
|
globalStyle('body:has(> #app-tabs)', {
|
||||||
paddingBottom: globalVars.appTabSafeArea,
|
paddingBottom: globalVars.appTabSafeArea,
|
||||||
});
|
});
|
||||||
globalStyle('body:has(#app-tabs) affine-keyboard-toolbar[data-shrink="true"]', {
|
|
||||||
paddingBottom: globalVars.appTabSafeArea,
|
|
||||||
});
|
|
||||||
globalStyle('body:has(#app-tabs) affine-keyboard-tool-panel', {
|
globalStyle('body:has(#app-tabs) affine-keyboard-tool-panel', {
|
||||||
paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom) + 8px)`,
|
paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom) + 8px)`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -322,7 +322,6 @@ export class AtMenuConfigService extends Service {
|
|||||||
|
|
||||||
private getMobileConfig(): Partial<LinkedWidgetConfig['mobile']> {
|
private getMobileConfig(): Partial<LinkedWidgetConfig['mobile']> {
|
||||||
return {
|
return {
|
||||||
useScreenHeight: BUILD_CONFIG.isIOS,
|
|
||||||
scrollContainer: window,
|
scrollContainer: window,
|
||||||
scrollTopOffset: () => {
|
scrollTopOffset: () => {
|
||||||
const header = document.querySelector('header');
|
const header = document.querySelector('header');
|
||||||
|
|||||||
Reference in New Issue
Block a user