Files
AFFiNE-Mirror/blocksuite/framework/std/src/event/control/keyboard.ts
T
yoyoyohamapi 0df584bd5e refactor(core): add keyPress event to fix IME space detection (#11700)
### TL;DR
Refactor space-triggered AI Widget activation logic from `keydown` to `keypress` event listeners

### Background

The `keydown` event triggered by a space may originate from:
1. Normal space insertion
2. Space triggered by input method confirming candidate words

In scenarios like (2), some browsers (see [ISSUE](https://github.com/toeverything/AFFiNE/issues/11541)) and input method callbacks produce events identical to scenario (1),making it impossible to distinguish between the two.

To fix this, the space-activated AI listener uses the `keypress` event:
In scenario 2, `event.which !== 32` (may be `30430` or other values) can be used to differentiate from scenario 1.

> CLOSE BS-3081
2025-04-15 08:37:27 +00:00

150 lines
3.6 KiB
TypeScript

import { DisposableGroup } from '@blocksuite/global/disposable';
import { IS_ANDROID, IS_MAC } from '@blocksuite/global/env';
import {
type UIEventHandler,
UIEventState,
UIEventStateContext,
} from '../base.js';
import type { EventOptions, UIEventDispatcher } from '../dispatcher.js';
import { androidBindKeymapPatch, bindKeymap } from '../keymap.js';
import { KeyboardEventState } from '../state/index.js';
import { EventScopeSourceType, EventSourceState } from '../state/source.js';
export class KeyboardControl {
private readonly _down = (event: KeyboardEvent) => {
if (!this._shouldTrigger(event)) {
return;
}
const keyboardEventState = new KeyboardEventState({
event,
composing: this.composition,
});
this._dispatcher.run(
'keyDown',
this._createContext(event, keyboardEventState)
);
};
private readonly _shouldTrigger = (event: KeyboardEvent) => {
if (event.isComposing) {
return false;
}
const mod = IS_MAC ? event.metaKey : event.ctrlKey;
if (
['c', 'v', 'x'].includes(event.key) &&
mod &&
!event.shiftKey &&
!event.altKey
) {
return false;
}
return true;
};
private readonly _up = (event: KeyboardEvent) => {
if (!this._shouldTrigger(event)) {
return;
}
const keyboardEventState = new KeyboardEventState({
event,
composing: this.composition,
});
this._dispatcher.run(
'keyUp',
this._createContext(event, keyboardEventState)
);
};
private composition = false;
private readonly _press = (event: KeyboardEvent) => {
if (!this._shouldTrigger(event)) {
return;
}
const keyboardEventState = new KeyboardEventState({
event,
composing: this.composition,
});
this._dispatcher.run(
'keyPress',
this._createContext(event, keyboardEventState)
);
};
constructor(private readonly _dispatcher: UIEventDispatcher) {}
private _createContext(event: Event, keyboardState: KeyboardEventState) {
return UIEventStateContext.from(
new UIEventState(event),
new EventSourceState({
event,
sourceType: EventScopeSourceType.Selection,
}),
keyboardState
);
}
bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions) {
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() {
this._dispatcher.disposables.addFromEvent(document, 'keydown', this._down);
this._dispatcher.disposables.addFromEvent(document, 'keyup', this._up);
this._dispatcher.disposables.addFromEvent(
document,
'keypress',
this._press
);
this._dispatcher.disposables.addFromEvent(
document,
'compositionstart',
() => {
this.composition = true;
},
{
capture: true,
}
);
this._dispatcher.disposables.addFromEvent(
document,
'compositionend',
() => {
this.composition = false;
},
{
capture: true,
}
);
}
}