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 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]); }