mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 07:06:28 +08:00
0df584bd5e
### 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
150 lines
3.6 KiB
TypeScript
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,
|
|
}
|
|
);
|
|
}
|
|
}
|