475 lines
19 KiB
TypeScript
475 lines
19 KiB
TypeScript
|
|
import React, { useState, useRef, useEffect } from "react";
|
||
|
|
// import "./style/style.css";
|
||
|
|
import type { Block, DataModel, GraphTypes, Position, UIType } from "../types/simulationDashboard";
|
||
|
|
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<Block[]>([]);
|
||
|
|
console.log('blocks: ', blocks);
|
||
|
|
const [editMode, setEditMode] = useState(false);
|
||
|
|
const [selectedBlock, setSelectedBlock] = useState<string | null>(null);
|
||
|
|
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
||
|
|
const [draggingElement, setDraggingElement] = useState<string | null>(null);
|
||
|
|
const [resizingElement, setResizingElement] = useState<string | null>(null);
|
||
|
|
const [resizingBlock, setResizingBlock] = useState<string | null>(null);
|
||
|
|
const [resizeStart, setResizeStart] = useState<{
|
||
|
|
x: number;
|
||
|
|
y: number;
|
||
|
|
width: number;
|
||
|
|
height: number;
|
||
|
|
} | null>(null);
|
||
|
|
const [showSwapUI, setShowSwapUI] = useState(false);
|
||
|
|
const [swapSource, setSwapSource] = useState<string | null>(null);
|
||
|
|
const [dataModel, setDataModel] = useState<DataModel>(dataModelManager.getDataSnapshot());
|
||
|
|
const [showDataModelPanel, setShowDataModelPanel] = useState(false);
|
||
|
|
const [showElementDropdown, setShowElementDropdown] = useState<string | null>(null);
|
||
|
|
const [dropDownPosition, setDropDownPosition] = useState({ top: 0, left: 0 });
|
||
|
|
const [draggingBlock, setDraggingBlock] = useState<string | null>(null);
|
||
|
|
const [elementDragOffset, setElementDragOffset] = useState<Position>({ x: 0, y: 0 });
|
||
|
|
const [blockDragOffset, setBlockDragOffset] = useState<Position>({ x: 0, y: 0 });
|
||
|
|
|
||
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
||
|
|
const blockEditorRef = useRef<HTMLDivElement>(null);
|
||
|
|
const elementEditorRef = useRef<HTMLDivElement>(null);
|
||
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||
|
|
const blockRef = useRef<HTMLDivElement>(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 (
|
||
|
|
<div ref={editorRef} className="dashboard-editor">
|
||
|
|
<ControlPanel
|
||
|
|
editMode={editMode}
|
||
|
|
setEditMode={setEditMode}
|
||
|
|
addBlock={() => addBlock(setBlocks, blocks)}
|
||
|
|
showDataModelPanel={showDataModelPanel}
|
||
|
|
setShowDataModelPanel={setShowDataModelPanel}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<BlockGrid
|
||
|
|
blocks={blocks}
|
||
|
|
editMode={editMode}
|
||
|
|
selectedBlock={selectedBlock}
|
||
|
|
selectedElement={selectedElement}
|
||
|
|
showSwapUI={showSwapUI}
|
||
|
|
swapSource={swapSource}
|
||
|
|
calculateMinBlockSize={calculateMinBlockSize}
|
||
|
|
handleBlockClick={(blockId, event) =>
|
||
|
|
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}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<ElementDropdown
|
||
|
|
showElementDropdown={showElementDropdown}
|
||
|
|
dropDownPosition={dropDownPosition}
|
||
|
|
addElement={(blockId, type, graphType) =>
|
||
|
|
addElement(
|
||
|
|
blockId,
|
||
|
|
type as UIType,
|
||
|
|
graphType as GraphTypes,
|
||
|
|
setBlocks,
|
||
|
|
blocks,
|
||
|
|
setShowElementDropdown
|
||
|
|
)
|
||
|
|
}
|
||
|
|
dropdownRef={dropdownRef}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{showDataModelPanel && editMode && (
|
||
|
|
<DataModelPanel dataModel={dataModel} dataModelManager={dataModelManager} />
|
||
|
|
)}
|
||
|
|
|
||
|
|
{selectedBlock && editMode && !selectedElement && currentBlock && (
|
||
|
|
<BlockEditor
|
||
|
|
blockEditorRef={blockEditorRef}
|
||
|
|
currentBlock={currentBlock}
|
||
|
|
selectedBlock={selectedBlock}
|
||
|
|
updateBlockStyle={(blockId, style) =>
|
||
|
|
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 && (
|
||
|
|
<ElementEditor
|
||
|
|
elementEditorRef={elementEditorRef}
|
||
|
|
currentElement={currentElement}
|
||
|
|
selectedBlock={selectedBlock}
|
||
|
|
selectedElement={selectedElement}
|
||
|
|
blocks={blocks}
|
||
|
|
setBlocks={setBlocks}
|
||
|
|
updateElementStyle={(blockId, elementId, style) =>
|
||
|
|
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 && (
|
||
|
|
<SwapModal setShowSwapUI={setShowSwapUI} setSwapSource={setSwapSource} />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default DashboardEditor;
|