From 61541a2d15fdc346aed6ddd05f20cac078c10400 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Fri, 28 Feb 2025 13:03:00 +0000 Subject: [PATCH] fix(editor): patch android backspace key binding with beforeInput (#10523) Close [BS-1869](https://linear.app/affine-design/issue/BS-1869/[bug]-android-chrome-%E8%BE%93%E5%85%A5%E9%94%99%E8%AF%AF) ## Problem On Android devices, keyboard events do not properly capture key information, causing the backspace key and other keyboard functionalities to malfunction. This is due to the specific behavior of Android platform, as discussed in: - https://stackoverflow.com/a/68188679 - https://stackoverflow.com/a/66724830 ## Solution 1. Added special handling for Android platform in `KeyboardControl` class by using `beforeInput` event instead of `keyDown` event 2. Implemented `androidBindKeymapPatch` function to handle special key events on Android platform 3. Updated event handling logic in related components, including: - CodeBlock - ListKeymap - ParagraphKeymap - PageKeyboardManager ## Changes - Added `androidBindKeymapPatch` function for handling key events on Android platform - Modified `KeyboardControl.bindHotkey` method to add `beforeInput` event handling for Android - Unified event object access using `ctx.get('defaultState').event` instead of `keyboardState.raw` - Updated key event handling logic in multiple components ## Before https://github.com/user-attachments/assets/e8602de4-d584-4adf-816f-369f38312022 ## After https://github.com/user-attachments/assets/f9e1680e-28ff-4d52-bdab-7683cdcb6f82 --- .../affine/block-code/src/code-block.ts | 8 ++-- .../affine/block-list/src/list-keymap.ts | 2 +- .../block-paragraph/src/paragraph-keymap.ts | 2 +- .../src/keyboard/keyboard-manager.ts | 2 +- .../block-std/src/event/control/keyboard.ts | 41 +++++++++++++------ .../framework/block-std/src/event/keymap.ts | 30 +++++++++++--- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/blocksuite/affine/block-code/src/code-block.ts b/blocksuite/affine/block-code/src/code-block.ts index 77c464a12..a7a97ac49 100644 --- a/blocksuite/affine/block-code/src/code-block.ts +++ b/blocksuite/affine/block-code/src/code-block.ts @@ -182,17 +182,17 @@ export class CodeBlockComponent extends CaptionedBlockComponent< // TODO: move to service for better performance this.bindHotKey({ Backspace: ctx => { - const state = ctx.get('keyboardState'); + const event = ctx.get('defaultState').event; const textSelection = selectionManager.find(TextSelection); if (!textSelection) { - state.raw.preventDefault(); + event.preventDefault(); return; } const from = textSelection.from; if (from.index === 0 && from.length === 0) { - state.raw.preventDefault(); + event.preventDefault(); selectionManager.setGroup('note', [ selectionManager.create(BlockSelection, { blockId: this.blockId }), ]); @@ -215,7 +215,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent< index: index, length: 0, }); - state.raw.preventDefault(); + event.preventDefault(); return true; } diff --git a/blocksuite/affine/block-list/src/list-keymap.ts b/blocksuite/affine/block-list/src/list-keymap.ts index 8733b0bfe..32367780c 100644 --- a/blocksuite/affine/block-list/src/list-keymap.ts +++ b/blocksuite/affine/block-list/src/list-keymap.ts @@ -105,7 +105,7 @@ export const ListKeymapExtension = KeymapExtension( const isStart = isCollapsed && text.from.index === 0; if (!isStart) return false; - ctx.get('keyboardState').raw.preventDefault(); + ctx.get('defaultState').event.preventDefault(); std.command .chain() .pipe(listToParagraphCommand, { diff --git a/blocksuite/affine/block-paragraph/src/paragraph-keymap.ts b/blocksuite/affine/block-paragraph/src/paragraph-keymap.ts index dbdb6e9f9..423c37baa 100644 --- a/blocksuite/affine/block-paragraph/src/paragraph-keymap.ts +++ b/blocksuite/affine/block-paragraph/src/paragraph-keymap.ts @@ -42,7 +42,7 @@ export const ParagraphKeymapExtension = KeymapExtension( const model = store.getBlock(text.from.blockId)?.model; if (!model || !matchModels(model, [ParagraphBlockModel])) return; - const event = ctx.get('keyboardState').raw; + const event = ctx.get('defaultState').event; event.preventDefault(); // When deleting at line start of a paragraph block, diff --git a/blocksuite/affine/block-root/src/keyboard/keyboard-manager.ts b/blocksuite/affine/block-root/src/keyboard/keyboard-manager.ts index 1722882ea..da4a5d131 100644 --- a/blocksuite/affine/block-root/src/keyboard/keyboard-manager.ts +++ b/blocksuite/affine/block-root/src/keyboard/keyboard-manager.ts @@ -21,7 +21,7 @@ import { toDraftModel } from '@blocksuite/store'; export class PageKeyboardManager { private readonly _handleDelete: UIEventHandler = ctx => { - const event = ctx.get('keyboardState').raw; + const event = ctx.get('defaultState').event; const blockSelections = this._currentSelection.filter(sel => sel.is(BlockSelection) ); diff --git a/blocksuite/framework/block-std/src/event/control/keyboard.ts b/blocksuite/framework/block-std/src/event/control/keyboard.ts index 38d590e98..d40b649a6 100644 --- a/blocksuite/framework/block-std/src/event/control/keyboard.ts +++ b/blocksuite/framework/block-std/src/event/control/keyboard.ts @@ -1,4 +1,5 @@ -import { IS_MAC } from '@blocksuite/global/env'; +import { IS_ANDROID, IS_MAC } from '@blocksuite/global/env'; +import { DisposableGroup } from '@blocksuite/global/utils'; 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'; @@ -72,17 +73,33 @@ 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); + }, + options + ) + ); + } + + 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/block-std/src/event/keymap.ts b/blocksuite/framework/block-std/src/event/keymap.ts index 82a86ee7d..e0b6ff84a 100644 --- a/blocksuite/framework/block-std/src/event/keymap.ts +++ b/blocksuite/framework/block-std/src/event/keymap.ts @@ -1,13 +1,9 @@ +import { IS_MAC } from '@blocksuite/global/env'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { base, keyName } from 'w3c-keyname'; import type { UIEventHandler } from './base.js'; -const mac = - typeof navigator !== 'undefined' - ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) - : false; - function normalizeKeyName(name: string) { const parts = name.split(/-(?!$)/); let result = parts.at(-1); @@ -33,7 +29,7 @@ function normalizeKeyName(name: string) { return; } if (/^mod$/i.test(mod)) { - if (mac) { + if (IS_MAC) { meta = true; } else { ctrl = true; @@ -107,3 +103,25 @@ export function bindKeymap( return false; }; } + +// In Android, 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; + }; +}