fix: cache shortcut settings and add paste shortcut
Browse files- src/shortcutSystem.ts +16 -3
src/shortcutSystem.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
export type ShortcutCategory = 'File' | 'Panels' | 'Canvas' | 'Annotation' | 'Editing' | 'View' | 'Window';
|
| 2 |
export type ShortcutId =
|
| 3 |
-
| 'edit.undo' | 'edit.redo'
|
| 4 |
| 'file.save' | 'file.newBoard' | 'file.exportRefs' | 'file.exportJson' | 'capture.screen'
|
| 5 |
| 'view.focus' | 'view.valueMirror' | 'view.zoomLens' | 'view.fitAll' | 'view.zoom100' | 'view.zoomIn' | 'view.zoomOut' | 'view.panHome' | 'view.toggleGrid' | 'view.toggleMinimap'
|
| 6 |
| 'selection.duplicate' | 'selection.group' | 'selection.ungroup' | 'selection.delete' | 'selection.selectAll' | 'selection.flipH' | 'selection.flipV' | 'selection.desaturate' | 'selection.bringFront' | 'selection.sendBack'
|
|
@@ -20,6 +20,7 @@ export const DEFAULT_SHORTCUTS: ShortcutDef[] = [
|
|
| 20 |
{ id:'capture.screen', label:'Screen capture', category:'File', defaultCombos:[{shift:true, code:'KeyS', displayKey:'S'}] },
|
| 21 |
{ id:'edit.undo', label:'Undo', category:'Editing', defaultCombos:[{primary:true, code:'KeyZ', displayKey:'Z'}] },
|
| 22 |
{ id:'edit.redo', label:'Redo', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyZ', displayKey:'Z'}] },
|
|
|
|
| 23 |
{ id:'selection.duplicate', label:'Duplicate selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyD', displayKey:'D'}] },
|
| 24 |
{ id:'selection.group', label:'Group selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyG', displayKey:'G'}] },
|
| 25 |
{ id:'selection.ungroup', label:'Ungroup selection', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyG', displayKey:'G'}] },
|
|
@@ -60,8 +61,20 @@ export const isMac = () => navigator.platform.toLowerCase().includes('mac');
|
|
| 60 |
export function normalizeEventToCombo(e: KeyboardEvent): ShortcutCombo | null { if (e.isComposing || e.key === 'Dead' || e.getModifierState?.('AltGraph') || MODIFIER_CODES.has(e.code)) return null; const mac = isMac(); return { primary: mac ? e.metaKey || undefined : e.ctrlKey || undefined, ctrl: mac && e.ctrlKey || undefined, meta: !mac && e.metaKey || undefined, alt: e.altKey || undefined, shift: e.shiftKey || undefined, code: e.code, displayKey: e.code === 'Space' ? 'Space' : e.key.length === 1 ? e.key.toUpperCase() : e.key }; }
|
| 61 |
export function comboToCanonical(c: ShortcutCombo): string { return [c.primary?'Primary':'', c.ctrl?'Ctrl':'', c.meta?'Meta':'', c.alt?'Alt':'', c.shift?'Shift':'', c.code].filter(Boolean).join('+'); }
|
| 62 |
export function comboToDisplay(c: ShortcutCombo): string { const mac=isMac(); const parts:string[]=[]; if(c.primary)parts.push(mac?'⌘':'Ctrl'); if(c.ctrl)parts.push('Ctrl'); if(c.meta)parts.push(mac?'⌘':'Meta'); if(c.alt)parts.push(mac?'⌥':'Alt'); if(c.shift)parts.push(mac?'⇧':'Shift'); parts.push(c.displayKey || c.code.replace(/^Key/,'').replace(/^Digit/,'')); return mac?parts.join(''):parts.join('+'); }
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
export function effectiveCombos(def: ShortcutDef, settings = loadShortcutSettings()): ShortcutCombo[] { const override = settings.shortcuts[def.id]; if (override === null || override === undefined) return def.defaultCombos; return override; }
|
| 66 |
export function matchesShortcut(id: ShortcutId, e: KeyboardEvent, settings = loadShortcutSettings()): boolean { const combo = normalizeEventToCombo(e); if(!combo) return false; const def = DEFAULT_SHORTCUTS.find(d=>d.id===id); if(!def) return false; const key = comboToCanonical(combo); return effectiveCombos(def, settings).some(c=>comboToCanonical(c)===key); }
|
| 67 |
export function isEditableTarget(t: EventTarget | null) { const el = t as HTMLElement | null; return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable); }
|
|
|
|
| 1 |
export type ShortcutCategory = 'File' | 'Panels' | 'Canvas' | 'Annotation' | 'Editing' | 'View' | 'Window';
|
| 2 |
export type ShortcutId =
|
| 3 |
+
| 'edit.undo' | 'edit.redo' | 'edit.paste'
|
| 4 |
| 'file.save' | 'file.newBoard' | 'file.exportRefs' | 'file.exportJson' | 'capture.screen'
|
| 5 |
| 'view.focus' | 'view.valueMirror' | 'view.zoomLens' | 'view.fitAll' | 'view.zoom100' | 'view.zoomIn' | 'view.zoomOut' | 'view.panHome' | 'view.toggleGrid' | 'view.toggleMinimap'
|
| 6 |
| 'selection.duplicate' | 'selection.group' | 'selection.ungroup' | 'selection.delete' | 'selection.selectAll' | 'selection.flipH' | 'selection.flipV' | 'selection.desaturate' | 'selection.bringFront' | 'selection.sendBack'
|
|
|
|
| 20 |
{ id:'capture.screen', label:'Screen capture', category:'File', defaultCombos:[{shift:true, code:'KeyS', displayKey:'S'}] },
|
| 21 |
{ id:'edit.undo', label:'Undo', category:'Editing', defaultCombos:[{primary:true, code:'KeyZ', displayKey:'Z'}] },
|
| 22 |
{ id:'edit.redo', label:'Redo', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyZ', displayKey:'Z'}] },
|
| 23 |
+
{ id:'edit.paste', label:'Paste image from clipboard', category:'Editing', defaultCombos:[{primary:true, code:'KeyV', displayKey:'V'}] },
|
| 24 |
{ id:'selection.duplicate', label:'Duplicate selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyD', displayKey:'D'}] },
|
| 25 |
{ id:'selection.group', label:'Group selection', category:'Editing', defaultCombos:[{primary:true, code:'KeyG', displayKey:'G'}] },
|
| 26 |
{ id:'selection.ungroup', label:'Ungroup selection', category:'Editing', defaultCombos:[{primary:true, shift:true, code:'KeyG', displayKey:'G'}] },
|
|
|
|
| 61 |
export function normalizeEventToCombo(e: KeyboardEvent): ShortcutCombo | null { if (e.isComposing || e.key === 'Dead' || e.getModifierState?.('AltGraph') || MODIFIER_CODES.has(e.code)) return null; const mac = isMac(); return { primary: mac ? e.metaKey || undefined : e.ctrlKey || undefined, ctrl: mac && e.ctrlKey || undefined, meta: !mac && e.metaKey || undefined, alt: e.altKey || undefined, shift: e.shiftKey || undefined, code: e.code, displayKey: e.code === 'Space' ? 'Space' : e.key.length === 1 ? e.key.toUpperCase() : e.key }; }
|
| 62 |
export function comboToCanonical(c: ShortcutCombo): string { return [c.primary?'Primary':'', c.ctrl?'Ctrl':'', c.meta?'Meta':'', c.alt?'Alt':'', c.shift?'Shift':'', c.code].filter(Boolean).join('+'); }
|
| 63 |
export function comboToDisplay(c: ShortcutCombo): string { const mac=isMac(); const parts:string[]=[]; if(c.primary)parts.push(mac?'⌘':'Ctrl'); if(c.ctrl)parts.push('Ctrl'); if(c.meta)parts.push(mac?'⌘':'Meta'); if(c.alt)parts.push(mac?'⌥':'Alt'); if(c.shift)parts.push(mac?'⇧':'Shift'); parts.push(c.displayKey || c.code.replace(/^Key/,'').replace(/^Digit/,'')); return mac?parts.join(''):parts.join('+'); }
|
| 64 |
+
|
| 65 |
+
let cachedSettings: ShortcutSettings | null = null;
|
| 66 |
+
function invalidateShortcutCache() { cachedSettings = null; }
|
| 67 |
+
if (typeof window !== 'undefined') {
|
| 68 |
+
window.addEventListener('lumaref:shortcuts-updated', invalidateShortcutCache);
|
| 69 |
+
window.addEventListener('storage', e => { if (e.key === STORAGE_KEY) invalidateShortcutCache(); });
|
| 70 |
+
}
|
| 71 |
+
export function loadShortcutSettings(): ShortcutSettings {
|
| 72 |
+
if (cachedSettings) return cachedSettings;
|
| 73 |
+
try { cachedSettings = JSON.parse(localStorage.getItem(STORAGE_KEY) || '') || {version:1, shortcuts:{}}; }
|
| 74 |
+
catch { cachedSettings = {version:1, shortcuts:{}}; }
|
| 75 |
+
return cachedSettings!;
|
| 76 |
+
}
|
| 77 |
+
export function saveShortcutSettings(s: ShortcutSettings) { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); invalidateShortcutCache(); window.dispatchEvent(new CustomEvent('lumaref:shortcuts-updated')); }
|
| 78 |
export function effectiveCombos(def: ShortcutDef, settings = loadShortcutSettings()): ShortcutCombo[] { const override = settings.shortcuts[def.id]; if (override === null || override === undefined) return def.defaultCombos; return override; }
|
| 79 |
export function matchesShortcut(id: ShortcutId, e: KeyboardEvent, settings = loadShortcutSettings()): boolean { const combo = normalizeEventToCombo(e); if(!combo) return false; const def = DEFAULT_SHORTCUTS.find(d=>d.id===id); if(!def) return false; const key = comboToCanonical(combo); return effectiveCombos(def, settings).some(c=>comboToCanonical(c)===key); }
|
| 80 |
export function isEditableTarget(t: EventTarget | null) { const el = t as HTMLElement | null; return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || el.isContentEditable); }
|