76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import hotkeys from 'hotkeys-js';
|
|
import { useKeymap, type Binding } from './keymap-service';
|
|
|
|
// FR-K1/K3/K7:从 keymap store 读绑定,多作用域裁决,Ctrl+F 拦截宿主查找
|
|
export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => void; onFocusSearch: () => void; onToggleOutline: () => void; onCheat: () => void; onSettings: () => void; onFind: () => void }) {
|
|
const optsRef = useRef(opts);
|
|
optsRef.current = opts;
|
|
const bindings = useKeymap(s => s.bindings);
|
|
const sig = bindings.map(b => b.id + '@' + b.combo).join('|');
|
|
useEffect(() => {
|
|
const actions: Record<string, () => void> = {
|
|
newDoc: () => optsRef.current.onNewDoc(),
|
|
save: () => optsRef.current.onSave(),
|
|
focusSearch: () => optsRef.current.onFocusSearch(),
|
|
toggleOutline: () => optsRef.current.onToggleOutline(),
|
|
toggleCheat: () => optsRef.current.onCheat(),
|
|
toggleSettings: () => optsRef.current.onSettings(),
|
|
find: () => optsRef.current.onFind(),
|
|
};
|
|
|
|
// 将 combo 字符串解析为可比较的修饰键+键名
|
|
const parseCombo = (combo: string) => {
|
|
const parts = combo.toLowerCase().split('+');
|
|
const key = parts[parts.length - 1];
|
|
return {
|
|
key,
|
|
ctrl: parts.includes('ctrl'),
|
|
shift: parts.includes('shift'),
|
|
alt: parts.includes('alt'),
|
|
meta: parts.includes('meta'),
|
|
};
|
|
};
|
|
const matchCombo = (e: KeyboardEvent, combo: string) => {
|
|
const c = parseCombo(combo);
|
|
return e.key.toLowerCase() === c.key &&
|
|
e.ctrlKey === c.ctrl &&
|
|
e.shiftKey === c.shift &&
|
|
e.altKey === c.alt &&
|
|
e.metaKey === c.meta;
|
|
};
|
|
|
|
// 编辑器作用域绑定——用捕获阶段 keydown,先于 ProseMirror 拦截
|
|
const editorBindings = bindings.filter(b => b.scope === 'editor' && actions[b.id]);
|
|
const onCaptureKeydown = (e: KeyboardEvent) => {
|
|
for (const b of editorBindings) {
|
|
if (matchCombo(e, b.combo)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
actions[b.id]();
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener('keydown', onCaptureKeydown, true);
|
|
|
|
// 全局/应用级绑定——用 hotkeys-js
|
|
const bound: string[] = [];
|
|
for (const b of bindings) {
|
|
if (b.scope === 'editor') continue; // 编辑器键已在捕获阶段处理
|
|
const act = actions[b.id];
|
|
if (act) {
|
|
hotkeys(b.combo, (e) => { e.preventDefault(); act(); });
|
|
bound.push(b.combo);
|
|
}
|
|
}
|
|
hotkeys('?', (e) => { if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { e.preventDefault(); optsRef.current.onCheat(); } });
|
|
bound.push('?');
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', onCaptureKeydown, true);
|
|
for (const c of bound) hotkeys.unbind(c);
|
|
};
|
|
}, [sig]);
|
|
}
|