Add Craft.js site builder (v2) - complete rebuild from GrapesJS
Rebuilt the visual site builder from scratch using Craft.js, React 18, and TypeScript. The new editor renders directly in the DOM (no iframe), supports 40+ components, multi-page with shared header/footer, 16 templates, full-spectrum color/gradient controls, custom head code injection, save/publish workflow, and auto-save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
|
||||
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 nodeId = selected[0];
|
||||
if (nodeId !== 'ROOT') {
|
||||
actions.delete(nodeId);
|
||||
}
|
||||
}
|
||||
} 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 = query.node(nodeId).toNodeTree();
|
||||
actions.addNodeTree(tree, parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Duplicate 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]);
|
||||
}
|
||||
Reference in New Issue
Block a user