import React, { useEffect, useCallback, useRef } from 'react'; import { useEditor } from '@craftjs/core'; import { findDeletableTarget } from '../../utils/craft-helpers'; import { useSitesmithModal } from '../../state/SitesmithContext'; import { buildSitesmithTarget } from '../../utils/sitesmith-target'; interface ContextMenuProps { visible: boolean; x: number; y: number; nodeId: string | null; onClose: () => void; } interface MenuItem { label: string; shortcut?: string; action: () => void; danger?: boolean; disabled?: boolean; dividerAfter?: boolean; } export const ContextMenu: React.FC = ({ visible, x, y, nodeId, onClose, }) => { const { actions, query } = useEditor(); const { open: openSitesmith } = useSitesmithModal(); const menuRef = useRef(null); const clipboardRef = useRef(null); // Close on click outside useEffect(() => { if (!visible) return; const handleClick = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { onClose(); } }; const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('mousedown', handleClick); document.addEventListener('keydown', handleEsc); return () => { document.removeEventListener('mousedown', handleClick); document.removeEventListener('keydown', handleEsc); }; }, [visible, onClose]); const getParentId = useCallback((): string | null => { if (!nodeId) return null; try { const node = query.node(nodeId).get(); return node?.data?.parent || null; } catch { return null; } }, [nodeId, query]); const duplicate = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; try { const tree = query.node(nodeId).toSerializedNode(); const parentId = getParentId(); if (!parentId) return; // Get the full subtree const freshTree = query.node(nodeId).toNodeTree(); const clonedTree = query.parseSerializedNode(freshTree.nodes[freshTree.rootNodeId].data).toNode(); actions.addNodeTree(freshTree, parentId); } catch (e) { console.error('Duplicate failed:', e); } onClose(); }, [nodeId, actions, query, getParentId, onClose]); const copyNode = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; try { clipboardRef.current = nodeId; } catch (e) { console.error('Copy failed:', e); } onClose(); }, [nodeId, onClose]); const pasteNode = useCallback(() => { const sourceId = clipboardRef.current; if (!sourceId) return; try { const targetParent = nodeId || 'ROOT'; const tree = query.node(sourceId).toNodeTree(); actions.addNodeTree(tree, targetParent); } catch (e) { console.error('Paste failed:', e); } onClose(); }, [nodeId, actions, query, onClose]); const moveUp = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; try { const parentId = getParentId(); if (!parentId) return; const parent = query.node(parentId).get(); const children = parent.data.nodes || []; const idx = children.indexOf(nodeId); if (idx > 0) { actions.move(nodeId, parentId, idx - 1); } } catch (e) { console.error('Move up failed:', e); } onClose(); }, [nodeId, actions, query, getParentId, onClose]); const moveDown = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; try { const parentId = getParentId(); if (!parentId) return; const parent = query.node(parentId).get(); const children = parent.data.nodes || []; const idx = children.indexOf(nodeId); if (idx < children.length - 1) { actions.move(nodeId, parentId, idx + 2); } } catch (e) { console.error('Move down failed:', e); } onClose(); }, [nodeId, actions, query, getParentId, onClose]); const selectParent = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; const parentId = getParentId(); if (parentId) { actions.selectNode(parentId); } onClose(); }, [nodeId, actions, getParentId, onClose]); const askSitesmith = useCallback(() => { if (!nodeId || nodeId === 'ROOT') return; try { const target = buildSitesmithTarget(query, nodeId); if (target) openSitesmith(target); } catch (e) { console.error('Ask Sitesmith failed:', e); } onClose(); }, [nodeId, query, openSitesmith, onClose]); const deleteNode = useCallback(() => { const target = findDeletableTarget(query, nodeId); if (!target) { onClose(); return; } try { actions.delete(target); } catch (e) { console.error('Delete failed:', e); } onClose(); }, [nodeId, actions, query, onClose]); if (!visible) return null; const isRoot = nodeId === 'ROOT' || !nodeId; const items: MenuItem[] = [ { label: '✨ Ask Sitesmith', action: askSitesmith, disabled: isRoot, dividerAfter: true, }, { label: 'Duplicate', shortcut: 'Ctrl+D', action: duplicate, disabled: isRoot, }, { label: 'Copy', shortcut: 'Ctrl+C', action: copyNode, disabled: isRoot, }, { label: 'Paste', shortcut: 'Ctrl+V', action: pasteNode, disabled: !clipboardRef.current, dividerAfter: true, }, { label: 'Move Up', action: moveUp, disabled: isRoot, }, { label: 'Move Down', action: moveDown, disabled: isRoot, }, { label: 'Select Parent', action: selectParent, disabled: isRoot, dividerAfter: true, }, { label: 'Delete', shortcut: 'Del', action: deleteNode, danger: true, disabled: isRoot, }, ]; // Adjust position to stay within viewport const adjustedX = Math.min(x, window.innerWidth - 200); const adjustedY = Math.min(y, window.innerHeight - items.length * 34 - 10); return (
{items.map((item, i) => ( {item.dividerAfter && (
)} ))}
); };