diff --git a/packages/editor/src/editor/hotkeys.ts b/packages/editor/src/editor/hotkeys.ts index 3bd180b..c079572 100644 --- a/packages/editor/src/editor/hotkeys.ts +++ b/packages/editor/src/editor/hotkeys.ts @@ -1,9 +1,35 @@ import type { EditorAction } from './store'; +function isEditableTarget(target: EventTarget | null): boolean { + if (!target) return false; + if (!(target instanceof HTMLElement)) return false; + + // If the focus is inside inputs/textareas or contenteditable, let the browser handle hotkeys. + const el = target; + if (el.isContentEditable) return true; + + const tag = el.tagName.toLowerCase(); + if (tag === 'input' || tag === 'textarea' || tag === 'select') return true; + + // Some components nest the real input. + if (el.closest('input, textarea, select, [contenteditable="true"]')) return true; + + return false; +} + export function bindEditorHotkeys(getShift: () => boolean, dispatch: (a: EditorAction) => void) { const onKeyDown = (e: KeyboardEvent) => { const ctrl = e.ctrlKey || e.metaKey; + // Esc: clear selection (and closes context menu via selection parity effect). + if (e.key === 'Escape') { + dispatch({ type: 'selectSingle', id: undefined }); + return; + } + + // Do not steal common hotkeys while typing in an input/editor. + if (isEditableTarget(e.target)) return; + // Undo/redo if (ctrl && e.key.toLowerCase() === 'z') { e.preventDefault(); @@ -16,8 +42,28 @@ export function bindEditorHotkeys(getShift: () => boolean, dispatch: (a: EditorA return; } + // Select all + if (ctrl && e.key.toLowerCase() === 'a') { + e.preventDefault(); + dispatch({ type: 'selectAll' }); + return; + } + + // Copy/paste + if (ctrl && e.key.toLowerCase() === 'c') { + e.preventDefault(); + dispatch({ type: 'copySelected' }); + return; + } + if (ctrl && e.key.toLowerCase() === 'v') { + e.preventDefault(); + dispatch({ type: 'pasteClipboard' }); + return; + } + // Delete if (e.key === 'Delete' || e.key === 'Backspace') { + e.preventDefault(); dispatch({ type: 'deleteSelected' }); return; }