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
This commit is contained in:
@@ -182,17 +182,17 @@ export class CodeBlockComponent extends CaptionedBlockComponent<
|
|||||||
// TODO: move to service for better performance
|
// TODO: move to service for better performance
|
||||||
this.bindHotKey({
|
this.bindHotKey({
|
||||||
Backspace: ctx => {
|
Backspace: ctx => {
|
||||||
const state = ctx.get('keyboardState');
|
const event = ctx.get('defaultState').event;
|
||||||
const textSelection = selectionManager.find(TextSelection);
|
const textSelection = selectionManager.find(TextSelection);
|
||||||
if (!textSelection) {
|
if (!textSelection) {
|
||||||
state.raw.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = textSelection.from;
|
const from = textSelection.from;
|
||||||
|
|
||||||
if (from.index === 0 && from.length === 0) {
|
if (from.index === 0 && from.length === 0) {
|
||||||
state.raw.preventDefault();
|
event.preventDefault();
|
||||||
selectionManager.setGroup('note', [
|
selectionManager.setGroup('note', [
|
||||||
selectionManager.create(BlockSelection, { blockId: this.blockId }),
|
selectionManager.create(BlockSelection, { blockId: this.blockId }),
|
||||||
]);
|
]);
|
||||||
@@ -215,7 +215,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<
|
|||||||
index: index,
|
index: index,
|
||||||
length: 0,
|
length: 0,
|
||||||
});
|
});
|
||||||
state.raw.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export const ListKeymapExtension = KeymapExtension(
|
|||||||
const isStart = isCollapsed && text.from.index === 0;
|
const isStart = isCollapsed && text.from.index === 0;
|
||||||
if (!isStart) return false;
|
if (!isStart) return false;
|
||||||
|
|
||||||
ctx.get('keyboardState').raw.preventDefault();
|
ctx.get('defaultState').event.preventDefault();
|
||||||
std.command
|
std.command
|
||||||
.chain()
|
.chain()
|
||||||
.pipe(listToParagraphCommand, {
|
.pipe(listToParagraphCommand, {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
|||||||
const model = store.getBlock(text.from.blockId)?.model;
|
const model = store.getBlock(text.from.blockId)?.model;
|
||||||
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
if (!model || !matchModels(model, [ParagraphBlockModel])) return;
|
||||||
|
|
||||||
const event = ctx.get('keyboardState').raw;
|
const event = ctx.get('defaultState').event;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// When deleting at line start of a paragraph block,
|
// When deleting at line start of a paragraph block,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { toDraftModel } from '@blocksuite/store';
|
|||||||
|
|
||||||
export class PageKeyboardManager {
|
export class PageKeyboardManager {
|
||||||
private readonly _handleDelete: UIEventHandler = ctx => {
|
private readonly _handleDelete: UIEventHandler = ctx => {
|
||||||
const event = ctx.get('keyboardState').raw;
|
const event = ctx.get('defaultState').event;
|
||||||
const blockSelections = this._currentSelection.filter(sel =>
|
const blockSelections = this._currentSelection.filter(sel =>
|
||||||
sel.is(BlockSelection)
|
sel.is(BlockSelection)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
type UIEventHandler,
|
type UIEventHandler,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
UIEventStateContext,
|
UIEventStateContext,
|
||||||
} from '../base.js';
|
} from '../base.js';
|
||||||
import type { EventOptions, UIEventDispatcher } from '../dispatcher.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 { KeyboardEventState } from '../state/index.js';
|
||||||
import { EventScopeSourceType, EventSourceState } from '../state/source.js';
|
import { EventScopeSourceType, EventSourceState } from '../state/source.js';
|
||||||
|
|
||||||
@@ -72,17 +73,33 @@ export class KeyboardControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions) {
|
bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions) {
|
||||||
return this._dispatcher.add(
|
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',
|
'keyDown',
|
||||||
ctx => {
|
ctx => {
|
||||||
if (this.composition) {
|
if (this.composition) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const binding = bindKeymap(keymap);
|
const binding = bindKeymap(keymap);
|
||||||
return binding(ctx);
|
return binding(ctx);
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
return () => disposables.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
listen() {
|
listen() {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
|
import { IS_MAC } from '@blocksuite/global/env';
|
||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import { base, keyName } from 'w3c-keyname';
|
import { base, keyName } from 'w3c-keyname';
|
||||||
|
|
||||||
import type { UIEventHandler } from './base.js';
|
import type { UIEventHandler } from './base.js';
|
||||||
|
|
||||||
const mac =
|
|
||||||
typeof navigator !== 'undefined'
|
|
||||||
? /Mac|iP(hone|[oa]d)/.test(navigator.platform)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
function normalizeKeyName(name: string) {
|
function normalizeKeyName(name: string) {
|
||||||
const parts = name.split(/-(?!$)/);
|
const parts = name.split(/-(?!$)/);
|
||||||
let result = parts.at(-1);
|
let result = parts.at(-1);
|
||||||
@@ -33,7 +29,7 @@ function normalizeKeyName(name: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (/^mod$/i.test(mod)) {
|
if (/^mod$/i.test(mod)) {
|
||||||
if (mac) {
|
if (IS_MAC) {
|
||||||
meta = true;
|
meta = true;
|
||||||
} else {
|
} else {
|
||||||
ctrl = true;
|
ctrl = true;
|
||||||
@@ -107,3 +103,25 @@ export function bindKeymap(
|
|||||||
return false;
|
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<string, UIEventHandler>
|
||||||
|
): 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user