import React, { useState, useRef, useEffect } from "react"; import type { Block } from "../types/exportedTypes"; import { dataModelManager } from "./data/dataModel"; import ControlPanel from "./ControlPanel"; import SwapModal from "./SwapModal"; import DataModelPanel from "./components/models/DataModelPanel"; import { addBlock } from "./functions/block/addBlock"; import { addElement } from "./functions/element/addElement"; import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize"; import { updateBlockStyle, updateBlockSize, updateBlockPosition, updateBlockPositionType, updateBlockZIndex } from "./functions/block/updateBlock"; import { updateElementStyle, updateElementSize, updateElementPosition, updateElementPositionType, updateElementZIndex, updateElementData, updateGraphData, updateGraphTitle, } from "./functions/element/updateElement"; import { swapElements } from "./functions/element/swapElements"; import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers"; import BlockGrid from "./components/block/BlockGrid"; import ElementDropdown from "./components/element/ElementDropdown"; import BlockEditor from "./components/block/BlockEditor"; import ElementEditor from "./components/element/ElementEditor"; import { handleBlockDragStart } from "./functions/block/handleBlockDragStart"; const DashboardEditor: React.FC = () => { const [blocks, setBlocks] = useState([]); const [editMode, setEditMode] = useState(false); const [selectedBlock, setSelectedBlock] = useState(null); const [selectedElement, setSelectedElement] = useState(null); const [draggingElement, setDraggingElement] = useState(null); const [resizingElement, setResizingElement] = useState(null); const [resizingBlock, setResizingBlock] = useState(null); const [resizeStart, setResizeStart] = useState<{ x: number; y: number; width: number; height: number; } | null>(null); const [showSwapUI, setShowSwapUI] = useState(false); const [swapSource, setSwapSource] = useState(null); const [dataModel, setDataModel] = useState(dataModelManager.getDataSnapshot()); const [showDataModelPanel, setShowDataModelPanel] = useState(false); const [showElementDropdown, setShowElementDropdown] = useState(null); const [dropDownPosition, setDropDownPosition] = useState({ top: 0, left: 0 }); const [draggingBlock, setDraggingBlock] = useState(null); const [elementDragOffset, setElementDragOffset] = useState({ x: 0, y: 0 }); const [blockDragOffset, setBlockDragOffset] = useState({ x: 0, y: 0 }); const editorRef = useRef(null); const blockEditorRef = useRef(null); const elementEditorRef = useRef(null); const dropdownRef = useRef(null); const blockRef = useRef(null); const currentBlock = blocks.find((b) => b.id === selectedBlock); const currentElement = currentBlock?.elements.find((el) => el.id === selectedElement); // Subscribe to data model changes useEffect(() => { const handleDataChange = (): void => { setDataModel(dataModelManager.getDataSnapshot()); }; const keys = dataModelManager.getAvailableKeys(); const subscriptions: Array<[string, () => void]> = []; keys.forEach((key) => { const callback = () => handleDataChange(); dataModelManager.subscribe(key, callback); subscriptions.push([key, callback]); }); const interval = setInterval(() => { const currentKeys = dataModelManager.getAvailableKeys(); const newKeys = currentKeys.filter((key) => !keys.includes(key)); newKeys.forEach((key) => { const callback = () => handleDataChange(); dataModelManager.subscribe(key, callback); subscriptions.push([key, callback]); }); }, 1000); return () => { subscriptions.forEach(([key, callback]) => { dataModelManager.unsubscribe(key, callback); }); clearInterval(interval); }; }, []); // Click outside handler useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { const target = event.target as Node; const isInsideBlockEditor = blockEditorRef.current?.contains(target); const isInsideElementEditor = elementEditorRef.current?.contains(target); const isInsideDropdown = dropdownRef.current?.contains(target); const isInsideEditor = editorRef.current?.contains(target); if (!isInsideEditor) { setSelectedBlock(null); setSelectedElement(null); setShowSwapUI(false); return; } if (isInsideEditor && !isInsideBlockEditor && !isInsideElementEditor && !isInsideDropdown) { const clickedElement = event.target as HTMLElement; const isBlock = clickedElement.closest("[data-block-id]"); const isElement = clickedElement.closest("[data-element-id]"); const isButton = clickedElement.closest("button"); const isResizeHandle = clickedElement.closest(".resize-handle"); if (!isBlock && !isElement && !isButton && !isResizeHandle) { setSelectedBlock(null); setSelectedElement(null); setShowSwapUI(false); setShowElementDropdown(null); } } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [selectedBlock, selectedElement]); // Drag and drop handler useEffect(() => { const handleMouseMove = (e: MouseEvent): void => { // Element dragging - use elementDragOffset if (draggingElement && selectedBlock && currentElement?.positionType === "absolute") { const blockElement = document.querySelector(`[data-block-id="${selectedBlock}"]`) as HTMLElement; if (blockElement) { const blockRect = blockElement.getBoundingClientRect(); const newX = e.clientX - blockRect.left - elementDragOffset.x; // Use elementDragOffset const newY = e.clientY - blockRect.top - elementDragOffset.y; // Use elementDragOffset updateElementPosition( selectedBlock, draggingElement, { x: Math.max(0, Math.min(blockRect.width - 50, newX)), y: Math.max(0, Math.min(blockRect.height - 30, newY)), }, setBlocks, blocks ); } } // Block dragging - use blockDragOffset if (draggingBlock && currentBlock?.positionType && (currentBlock.positionType === "absolute" || currentBlock.positionType === "fixed")) { const editorElement = editorRef.current; if (editorElement) { const editorRect = editorElement.getBoundingClientRect(); const newX = e.clientX - editorRect.left - blockDragOffset.x; // Use blockDragOffset const newY = e.clientY - editorRect.top - blockDragOffset.y; // Use blockDragOffset updateBlockPosition( draggingBlock, { x: Math.max(0, Math.min(editorRect.width - (currentBlock.size?.width || 400), newX)), y: Math.max(0, Math.min(editorRect.height - (currentBlock.size?.height || 300), newY)), }, setBlocks, blocks ); } } if ((resizingElement || resizingBlock) && resizeStart) { if (resizingElement && selectedBlock) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; const newWidth = Math.max(100, resizeStart.width + deltaX); const newHeight = Math.max(50, resizeStart.height + deltaY); updateElementSize( selectedBlock, resizingElement, { width: newWidth, height: newHeight, }, setBlocks, blocks ); } else if (resizingBlock) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; const currentBlock = blocks.find((b) => b.id === resizingBlock); const minSize = currentBlock ? calculateMinBlockSize(currentBlock) : { width: 300, height: 200 }; const newWidth = Math.max(minSize.width, resizeStart.width + deltaX); const newHeight = Math.max(minSize.height, resizeStart.height + deltaY); updateBlockSize( resizingBlock, { width: newWidth, height: newHeight, }, setBlocks, blocks ); } } }; const handleMouseUp = (): void => { setDraggingElement(null); setResizingElement(null); setDraggingBlock(null); setResizingBlock(null); setResizeStart(null); }; if (draggingElement || draggingBlock || resizingElement || resizingBlock) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [draggingElement, resizingElement, draggingBlock, resizingBlock, elementDragOffset, blockDragOffset, selectedBlock, currentElement, resizeStart, currentBlock, blocks]); // Update dropdown position when showElementDropdown changes useEffect(() => { if (showElementDropdown && blockRef.current) { const rect = blockRef.current.getBoundingClientRect(); setDropDownPosition({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, }); } }, [showElementDropdown]); return (
addBlock(setBlocks, blocks)} showDataModelPanel={showDataModelPanel} setShowDataModelPanel={setShowDataModelPanel} /> handleBlockClick(blockId, event, editMode, setSelectedBlock, setSelectedElement, setShowSwapUI)} handleElementClick={(blockId, elementId, event) => handleElementClick(blockId, elementId, event, editMode, setSelectedElement, setSelectedBlock, setShowSwapUI, setShowElementDropdown)} handleElementDragStart={(elementId, event) => handleElementDragStart(elementId, event, currentElement, setDraggingElement, setElementDragOffset)} handleElementResizeStart={(elementId, event) => handleElementResizeStart(elementId, event, setResizingElement, setResizeStart)} handleBlockResizeStart={(blockId, event) => handleBlockResizeStart(blockId, event, setResizingBlock, setResizeStart)} handleSwapStart={(elementId, event) => handleSwapStart(elementId, event, setSwapSource, setShowSwapUI)} handleSwapTarget={(elementId, event) => handleSwapTarget(elementId, event, swapSource, selectedBlock, swapElements, setBlocks, blocks, setShowSwapUI, setSwapSource)} handleBlockDragStart={(blockId, event) => handleBlockDragStart(blockId, event, setDraggingBlock, setBlockDragOffset)} setShowElementDropdown={setShowElementDropdown} showElementDropdown={showElementDropdown} blockRef={blockRef} /> addElement(blockId, type as UIType, graphType as GraphTypes, setBlocks, blocks, setShowElementDropdown)} dropdownRef={dropdownRef} /> {showDataModelPanel && editMode && } {selectedBlock && editMode && !selectedElement && currentBlock && ( updateBlockStyle(blockId, style, setBlocks, blocks)} updateBlockSize={(blockId, size) => updateBlockSize(blockId, size, setBlocks, blocks)} updateBlockPosition={(blockId, position) => updateBlockPosition(blockId, position, setBlocks, blocks)} updateBlockPositionType={(blockId, positionType) => updateBlockPositionType(blockId, positionType, setBlocks, blocks)} updateBlockZIndex={(blockId, zIndex) => updateBlockZIndex(blockId, zIndex, setBlocks, blocks)} /> )} {selectedElement && editMode && selectedBlock && currentElement && ( updateElementStyle(blockId, elementId, style, setBlocks, blocks)} updateElementSize={(blockId, elementId, size) => updateElementSize(blockId, elementId, size, setBlocks, blocks)} updateElementPosition={(blockId, elementId, position) => updateElementPosition(blockId, elementId, position, setBlocks, blocks)} updateElementPositionType={(blockId, elementId, positionType) => updateElementPositionType(blockId, elementId, positionType, setBlocks, blocks)} updateElementZIndex={(blockId, elementId, zIndex) => updateElementZIndex(blockId, elementId, zIndex, setBlocks, blocks)} updateElementData={(blockId, elementId, updates) => updateElementData(blockId, elementId, updates, setBlocks, blocks)} updateGraphData={(blockId, elementId, newData) => updateGraphData(blockId, elementId, newData, setBlocks, blocks)} updateGraphTitle={(blockId, elementId, title) => updateGraphTitle(blockId, elementId, title, setBlocks, blocks)} setSwapSource={setSwapSource} setShowSwapUI={setShowSwapUI} dataModelManager={dataModelManager} /> )} {showSwapUI && }
); }; export default DashboardEditor;