Files
dbx/apps/desktop/src/lib/editor/queryEditorExecutionShortcut.ts
T

75 lines
2.8 KiB
TypeScript

import type { Command, EditorView, KeyBinding } from "@codemirror/view";
import { shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
const SAFARI_POST_COMPOSITION_KEY_WINDOW_MS = 100;
type BrowserNavigator = Pick<Navigator, "maxTouchPoints" | "userAgent" | "vendor">;
function isDesktopSafari(currentNavigator: BrowserNavigator | undefined): boolean {
if (!currentNavigator) return false;
return /Apple Computer/.test(currentNavigator.vendor) && !/Mobile\/\w+/.test(currentNavigator.userAgent) && currentNavigator.maxTouchPoints <= 2;
}
export function createQueryEditorPostCompositionKeyGuard(options: { navigator?: BrowserNavigator; now?: () => number } = {}) {
const currentNavigator = options.navigator ?? (typeof navigator === "undefined" ? undefined : navigator);
const now = options.now ?? Date.now;
const desktopSafari = isDesktopSafari(currentNavigator);
const blockedEvents = new WeakSet<KeyboardEvent>();
let compositionEndedAt = 0;
let compositionPendingKey = false;
function handleCompositionStart() {
compositionPendingKey = false;
}
function handleCompositionEnd() {
compositionPendingKey = desktopSafari;
compositionEndedAt = desktopSafari ? now() : 0;
}
function handleKeydown(event: KeyboardEvent) {
if (!compositionPendingKey) return;
compositionPendingKey = false;
if (now() - compositionEndedAt < SAFARI_POST_COMPOSITION_KEY_WINDOW_MS) blockedEvents.add(event);
}
return {
attach(element: HTMLElement) {
element.addEventListener("compositionstart", handleCompositionStart, true);
element.addEventListener("compositionend", handleCompositionEnd, true);
element.addEventListener("keydown", handleKeydown, true);
return () => {
element.removeEventListener("compositionstart", handleCompositionStart, true);
element.removeEventListener("compositionend", handleCompositionEnd, true);
element.removeEventListener("keydown", handleKeydown, true);
compositionPendingKey = false;
};
},
blocks(event: KeyboardEvent) {
return blockedEvents.has(event);
},
};
}
/**
* Create a query-editor execution binding that consumes the shortcut while an
* IME composition is active, without invoking the execution callback.
*
* CodeMirror may ignore keydown events after a composition has changed the
* document, so the app-level shortcut fallback also checks the editor state.
* This guard covers the keymap path when CodeMirror still dispatches it.
*/
export function createQueryEditorExecutionShortcutBindings(shortcut: string, run: Command, isComposing: (view: EditorView) => boolean): KeyBinding[] {
if (!shortcut) return [];
return [
{
key: shortcutToCodeMirrorKey(shortcut),
preventDefault: true,
run(view) {
if (isComposing(view)) return true;
return run(view);
},
},
];
}