Files
AFFiNE-Mirror/blocksuite/framework/block-std/src/event/keymap.ts
L-Sun 61541a2d15 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
2025-02-28 13:03:00 +00:00

128 lines
3.2 KiB
TypeScript

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';
function normalizeKeyName(name: string) {
const parts = name.split(/-(?!$)/);
let result = parts.at(-1);
if (result === 'Space') {
result = ' ';
}
let alt, ctrl, shift, meta;
parts.slice(0, -1).forEach(mod => {
if (/^(cmd|meta|m)$/i.test(mod)) {
meta = true;
return;
}
if (/^a(lt)?$/i.test(mod)) {
alt = true;
return;
}
if (/^(c|ctrl|control)$/i.test(mod)) {
ctrl = true;
return;
}
if (/^s(hift)?$/i.test(mod)) {
shift = true;
return;
}
if (/^mod$/i.test(mod)) {
if (IS_MAC) {
meta = true;
} else {
ctrl = true;
}
return;
}
throw new BlockSuiteError(
ErrorCode.EventDispatcherError,
'Unrecognized modifier name: ' + mod
);
});
if (alt) result = 'Alt-' + result;
if (ctrl) result = 'Ctrl-' + result;
if (meta) result = 'Meta-' + result;
if (shift) result = 'Shift-' + result;
return result as string;
}
function modifiers(name: string, event: KeyboardEvent, shift = true) {
if (event.altKey) name = 'Alt-' + name;
if (event.ctrlKey) name = 'Ctrl-' + name;
if (event.metaKey) name = 'Meta-' + name;
if (shift && event.shiftKey) name = 'Shift-' + name;
return name;
}
function normalize(map: Record<string, UIEventHandler>) {
const copy: Record<string, UIEventHandler> = Object.create(null);
for (const prop in map) copy[normalizeKeyName(prop)] = map[prop];
return copy;
}
export function bindKeymap(
bindings: Record<string, UIEventHandler>
): UIEventHandler {
const map = normalize(bindings);
return ctx => {
const state = ctx.get('keyboardState');
const event = state.raw;
const name = keyName(event);
const direct = map[modifiers(name, event)];
if (direct && direct(ctx)) {
return true;
}
if (name.length !== 1 || name === ' ') {
return false;
}
if (event.shiftKey) {
const noShift = map[modifiers(name, event, false)];
if (noShift && noShift(ctx)) {
return true;
}
}
// none standard keyboard, fallback to keyCode
const special =
event.shiftKey ||
event.altKey ||
event.metaKey ||
name.charCodeAt(0) > 127;
const baseName = base[event.keyCode];
if (special && baseName && baseName !== name) {
const fromCode = map[modifiers(baseName, event)];
if (fromCode && fromCode(ctx)) {
return true;
}
}
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;
};
}