asdf98 commited on
Commit
2d7331f
·
verified ·
1 Parent(s): 70209e3

fix: cache shortcut settings and add paste shortcut

Browse files
Files changed (1) hide show
  1. 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
- export function loadShortcutSettings(): ShortcutSettings { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '') || {version:1, shortcuts:{}}; } catch { return {version:1, shortcuts:{}}; } }
64
- export function saveShortcutSettings(s: ShortcutSettings) { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); window.dispatchEvent(new CustomEvent('lumaref:shortcuts-updated')); }
 
 
 
 
 
 
 
 
 
 
 
 
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); }