editor: add core hotkeys (copy/paste/select-all/esc)

This commit is contained in:
clawdbot 2026-01-29 06:13:58 +08:00
parent e4b25a66a0
commit 4e4b0e428d

View File

@ -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;
}