fix(editor): prevent SwiftKey IME double input (#13590)

Close
[BS-3610](https://linear.app/affine-design/issue/BS-3610/bug-每次按空格会出现重复单词-,特定输入法,比如swiftkey)

#### PR Dependency Tree

* **PR #13591**
  * **PR #13590** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- Bug Fixes
- Android: More reliable Backspace/delete handling, preventing missed
inputs and double-deletions.
- Android: Cursor/selection is correctly restored after merging a
paragraph with the previous block.
- Android: Smoother IME composition input; captures correct composition
range.
- Deletion across lines and around embeds/empty lines is more
consistent.
- Chores
- Internal event handling updated to improve Android compatibility and
stability (no user-facing changes).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->





#### PR Dependency Tree


* **PR #13591**
  * **PR #13590** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
L-Sun
2025-09-16 17:02:54 +08:00
committed by GitHub
parent fd717af3db
commit 34a3c83d84
4 changed files with 109 additions and 43 deletions
@@ -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<string, UIEventHandler>, 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() {
@@ -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<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;
};
}