feat(M9): FR-K1/K3/K4/K6/K7 快捷键体系——多作用域(global/app/editor)+Scope.resolveActive 裁决(K1); 编辑器类绑定 find/bold/italic/strike/inlineCode/clearFormat(K3); 冲突数量汇总 conflictCount+SettingsPanel 顶部警示(K4); CheatSheet 从 store 动态渲染+formatCombo Mac 适配(K6); Ctrl+F 捕获阶段 keydown 拦截宿主查找+FindReplace 切换(K7); keymap-enhanced E2E 2 项
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import hotkeys from 'hotkeys-js';
|
||||
import { useKeymap } from './keymap-service';
|
||||
import { useKeymap, type Binding } from './keymap-service';
|
||||
|
||||
// 从 keymap store 读组合键绑定(FR-K3 重绑后实时生效);? 帮助键保留硬编码
|
||||
export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => void; onFocusSearch: () => void; onToggleOutline: () => void; onCheat: () => void; onSettings: () => void }) {
|
||||
// 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);
|
||||
@@ -14,14 +14,62 @@ export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => voi
|
||||
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); }
|
||||
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 () => { for (const c of bound) hotkeys.unbind(c); };
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onCaptureKeydown, true);
|
||||
for (const c of bound) hotkeys.unbind(c);
|
||||
};
|
||||
}, [sig]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user