diff --git a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts index b96de60532..d9d856ec65 100644 --- a/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts +++ b/blocksuite/affine/blocks/paragraph/src/utils/merge-with-prev.ts @@ -24,7 +24,7 @@ import { getPrevContentBlock, matchModels, } from '@blocksuite/affine-shared/utils'; -import { IS_MOBILE } from '@blocksuite/global/env'; +import { IS_ANDROID, IS_MOBILE } from '@blocksuite/global/env'; import { BlockSelection, type EditorHost } from '@blocksuite/std'; import type { BlockModel, Text } from '@blocksuite/store'; @@ -79,6 +79,28 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) { index: lengthBeforeJoin, length: 0, }).catch(console.error); + + // due to some IME like Microsoft Swift IME on Android will reset range after join text, + // for example: + // + // $ZERO_WIDTH_FOR_EMPTY_LINE <--- p1 + // |aaa <--- p2 + // + // after pressing backspace, during beforeinput event, the native range is (p1, 1) -> (p2, 0) + // and after browser and IME handle the event, the native range is (p1, 1) -> (p1, 1) + // + // a|aa <--- p1 + // + // so we need to set range again after join text. + if (IS_ANDROID) { + setTimeout(() => { + asyncSetInlineRange(editorHost.std, prevBlock, { + index: lengthBeforeJoin, + length: 0, + }).catch(console.error); + }); + } + return true; } diff --git a/blocksuite/framework/std/src/event/control/keyboard.ts b/blocksuite/framework/std/src/event/control/keyboard.ts index 9ecab2faad..84a04a6f01 100644 --- a/blocksuite/framework/std/src/event/control/keyboard.ts +++ b/blocksuite/framework/std/src/event/control/keyboard.ts @@ -1,4 +1,5 @@ -import { IS_MAC } from '@blocksuite/global/env'; +import { DisposableGroup } from '@blocksuite/global/disposable'; +import { IS_ANDROID, IS_MAC } from '@blocksuite/global/env'; import { type UIEventHandler, @@ -6,7 +7,7 @@ import { UIEventStateContext, } from '../base.js'; import type { EventOptions, UIEventDispatcher } from '../dispatcher.js'; -import { bindKeymap } from '../keymap.js'; +import { androidBindKeymapPatch, bindKeymap } from '../keymap.js'; import { KeyboardEventState } from '../state/index.js'; import { EventScopeSourceType, EventSourceState } from '../state/source.js'; @@ -87,15 +88,29 @@ export class KeyboardControl { } bindHotkey(keymap: Record, options?: EventOptions) { - return this._dispatcher.add( - 'keyDown', - ctx => { - if (this.composition) return false; - const binding = bindKeymap(keymap); - return binding(ctx); - }, - options + const disposables = new DisposableGroup(); + if (IS_ANDROID) { + disposables.add( + this._dispatcher.add('beforeInput', ctx => { + if (this.composition) return false; + const binding = androidBindKeymapPatch(keymap); + return binding(ctx); + }) + ); + } + + disposables.add( + this._dispatcher.add( + 'keyDown', + ctx => { + if (this.composition) return false; + const binding = bindKeymap(keymap); + return binding(ctx); + }, + options + ) ); + return () => disposables.dispose(); } listen() { diff --git a/blocksuite/framework/std/src/event/keymap.ts b/blocksuite/framework/std/src/event/keymap.ts index 30f78b0d62..4f682e7483 100644 --- a/blocksuite/framework/std/src/event/keymap.ts +++ b/blocksuite/framework/std/src/event/keymap.ts @@ -103,3 +103,25 @@ export function bindKeymap( return false; }; } + +// In some IME of Android like, the keypress event dose not contain +// the information about what key is pressed. See +// https://stackoverflow.com/a/68188679 +// https://stackoverflow.com/a/66724830 +export function androidBindKeymapPatch( + bindings: Record +): UIEventHandler { + return ctx => { + const event = ctx.get('defaultState').event; + if (!(event instanceof InputEvent)) return; + + if ( + event.inputType === 'deleteContentBackward' && + 'Backspace' in bindings + ) { + return bindings['Backspace'](ctx); + } + + return false; + }; +} diff --git a/blocksuite/framework/std/src/inline/services/event.ts b/blocksuite/framework/std/src/inline/services/event.ts index b48b07134e..ee81a7a083 100644 --- a/blocksuite/framework/std/src/inline/services/event.ts +++ b/blocksuite/framework/std/src/inline/services/event.ts @@ -1,3 +1,4 @@ +import { IS_ANDROID } from '@blocksuite/global/env'; import type { BaseTextAttributes } from '@blocksuite/store'; import type { InlineEditor } from '../inline-editor.js'; @@ -41,11 +42,10 @@ export class EventService { } }; - private readonly _onBeforeInput = (event: InputEvent) => { + private readonly _onBeforeInput = async (event: InputEvent) => { const range = this.editor.rangeService.getNativeRange(); if ( this.editor.isReadonly || - this._isComposing || !range || !this._isRangeCompletelyInRoot(range) ) @@ -54,33 +54,29 @@ export class EventService { let inlineRange = this.editor.toInlineRange(range); if (!inlineRange) return; + if (this._isComposing) { + if (IS_ANDROID && event.inputType === 'insertCompositionText') { + this._compositionInlineRange = inlineRange; + } + return; + } + let ifHandleTargetRange = true; - if (event.inputType.startsWith('delete')) { - if ( - isInEmbedGap(range.commonAncestorContainer) && - inlineRange.length === 0 && - inlineRange.index > 0 - ) { - inlineRange = { - index: inlineRange.index - 1, - length: 1, - }; - ifHandleTargetRange = false; - } else if ( - isInEmptyLine(range.commonAncestorContainer) && - inlineRange.length === 0 && - inlineRange.index > 0 - // eslint-disable-next-line sonarjs/no-duplicated-branches - ) { - // do not use target range when deleting across lines + if ( + event.inputType.startsWith('delete') && + (isInEmbedGap(range.commonAncestorContainer) || // https://github.com/toeverything/blocksuite/issues/5381 - inlineRange = { - index: inlineRange.index - 1, - length: 1, - }; - ifHandleTargetRange = false; - } + isInEmptyLine(range.commonAncestorContainer)) && + inlineRange.length === 0 && + inlineRange.index > 0 + ) { + // do not use target range when deleting across lines + inlineRange = { + index: inlineRange.index - 1, + length: 1, + }; + ifHandleTargetRange = false; } if (ifHandleTargetRange) { @@ -97,11 +93,24 @@ export class EventService { } } } - if (!inlineRange) return; event.preventDefault(); + if (IS_ANDROID) { + this.editor.rerenderWholeEditor(); + await this.editor.waitForUpdate(); + if ( + event.inputType === 'deleteContentBackward' && + !(inlineRange.index === 0 && inlineRange.length === 0) + ) { + // when press backspace at offset 1, double characters will be removed. + // because we mock backspace key event `androidBindKeymapPatch` in blocksuite/framework/std/src/event/keymap.ts + // so we need to stop the event propagation to prevent the double characters removal. + event.stopPropagation(); + } + } + const ctx: BeforeinputHookCtx = { inlineEditor: this.editor, raw: event, @@ -346,11 +355,9 @@ export class EventService { return; } - this.editor.disposables.addFromEvent( - eventSource, - 'beforeinput', - this._onBeforeInput - ); + this.editor.disposables.addFromEvent(eventSource, 'beforeinput', e => { + this._onBeforeInput(e).catch(console.error); + }); this.editor.disposables.addFromEvent( eventSource, 'compositionstart',