Files
site-builder/craft/src/hooks/useKeyboardShortcuts.ts
T

144 lines
4.4 KiB
TypeScript
Raw Normal View History

import { useEffect } from 'react';
import { useEditor } from '@craftjs/core';
import { findDeletableTarget } from '../utils/craft-helpers';
import { regenerateTreeIds } from '../utils/craft-tree';
import { getClipboardNodeId, setClipboardNodeId } from './clipboard';
function isInputFocused(): boolean {
const el = document.activeElement;
if (!el) return false;
const tag = el.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
if ((el as HTMLElement).isContentEditable) return true;
return false;
}
export function useKeyboardShortcuts() {
const { actions, query } = useEditor((state) => {
const selectedIds = state.events.selected;
const selectedId = selectedIds ? Array.from(selectedIds)[0] : null;
return { selectedId };
});
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Skip when typing in inputs
if (isInputFocused()) return;
const ctrl = e.ctrlKey || e.metaKey;
// Ctrl+Z: Undo
if (ctrl && !e.shiftKey && e.key === 'z') {
e.preventDefault();
try {
actions.history.undo();
} catch (err) {
// No more undo steps
}
return;
}
// Ctrl+Y or Ctrl+Shift+Z: Redo
if ((ctrl && e.key === 'y') || (ctrl && e.shiftKey && e.key === 'z') || (ctrl && e.shiftKey && e.key === 'Z')) {
e.preventDefault();
try {
actions.history.redo();
} catch (err) {
// No more redo steps
}
return;
}
// Delete/Backspace: delete selected node
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0) {
const target = findDeletableTarget(query, selected[0]);
if (target) actions.delete(target);
}
} catch (err) {
console.error('Delete failed:', err);
}
return;
}
// Ctrl+D: duplicate selected
if (ctrl && (e.key === 'd' || e.key === 'D')) {
e.preventDefault();
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0) {
const nodeId = selected[0];
if (nodeId !== 'ROOT') {
const node = query.node(nodeId).get();
const parentId = node?.data?.parent;
if (parentId) {
const tree = regenerateTreeIds(query.node(nodeId).toNodeTree());
actions.addNodeTree(tree, parentId);
}
}
}
} catch (err) {
console.error('Duplicate failed:', err);
}
return;
}
// Ctrl+C: copy selected node id to the shared clipboard
if (ctrl && (e.key === 'c' || e.key === 'C')) {
e.preventDefault();
try {
const selected = query.getEvent('selected').all();
if (selected.length > 0 && selected[0] !== 'ROOT') {
setClipboardNodeId(selected[0]);
}
} catch (err) {
console.error('Copy failed:', err);
}
return;
}
// Ctrl+V: paste the clipboard node as a sibling of the current selection
if (ctrl && (e.key === 'v' || e.key === 'V')) {
e.preventDefault();
try {
const sourceId = getClipboardNodeId();
if (!sourceId || !query.node(sourceId).get()) return;
const selected = query.getEvent('selected').all();
if (selected.length === 0) return;
const selectedId = selected[0];
let targetParent = 'ROOT';
if (selectedId !== 'ROOT') {
const node = query.node(selectedId).get();
targetParent = node?.data?.parent || 'ROOT';
}
const tree = regenerateTreeIds(query.node(sourceId).toNodeTree());
actions.addNodeTree(tree, targetParent);
} catch (err) {
console.error('Paste failed:', err);
}
return;
}
// Escape: deselect all
if (e.key === 'Escape') {
e.preventDefault();
try {
actions.clearEvents();
} catch (err) {
// Ignore
}
return;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [actions, query]);
}