diff --git a/src/components/editor/action.ts b/src/components/editor/action.ts index b788bfa..c3553ac 100644 --- a/src/components/editor/action.ts +++ b/src/components/editor/action.ts @@ -16,6 +16,17 @@ type EditorActionMap = { containerId: string; elementDetails: EditorElement; }; + MOVE_ELEMENT: { + elementId: string; + newParentId: string; + newIndex: number; + }; + MOVE_ELEMENT_UP: { + elementId: string; + }; + MOVE_ELEMENT_DOWN: { + elementId: string; + }; UPDATE_ELEMENT: { elementDetails: EditorElement; }; @@ -89,6 +100,63 @@ const traverseElements = ( }, []); }; +const findElementAndParent = ( + elements: EditorElement[], + elementId: string, +): [EditorElement | null, EditorElement | null, number] => { + for (const el of elements) { + if (el.id === elementId) { + return [el, null, -1]; + } + if (Array.isArray(el.content)) { + const index = el.content.findIndex((child) => child.id === elementId); + if (index !== -1) { + return [el.content[index], el, index]; + } + const [foundEl, foundParent, foundIndex] = findElementAndParent( + el.content, + elementId, + ); + if (foundEl) { + return [foundEl, foundParent, foundIndex]; + } + } + } + return [null, null, -1]; +}; + +const removeElementFromParent = ( + elements: EditorElement[], + elementId: string, +): [EditorElement[], EditorElement | null] => { + let removedElement: EditorElement | null = null; + const newElements = elements.map((el) => { + if (Array.isArray(el.content)) { + const index = el.content.findIndex((child) => child.id === elementId); + if (index !== -1) { + removedElement = el.content[index]; + return { + ...el, + content: [ + ...el.content.slice(0, index), + ...el.content.slice(index + 1), + ], + }; + } + const [newContent, removed] = removeElementFromParent( + el.content, + elementId, + ); + if (removed) { + removedElement = removed; + return { ...el, content: newContent }; + } + } + return el; + }) as EditorElement[]; + return [newElements, removedElement]; +}; + /** * Action Handlers */ @@ -119,6 +187,79 @@ const actionHandlers: { }); }, + MOVE_ELEMENT: (editor, payload) => { + const { elementId, newParentId, newIndex } = payload; + + const [elements, removedElement] = removeElementFromParent( + editor.state.elements, + elementId, + ); + if (!removedElement) return editor; + + const insertElement = (els: EditorElement[]): EditorElement[] => { + return els.map((el) => { + if (el.id === newParentId && Array.isArray(el.content)) { + const newContent = [...el.content]; + newContent.splice(newIndex, 0, removedElement); + return { ...el, content: newContent }; + } + if (Array.isArray(el.content)) { + return { ...el, content: insertElement(el.content) }; + } + return el; + }) as EditorElement[]; + }; + + const newElements = insertElement(elements); + + return updateEditorHistory(editor, { + ...editor.state, + elements: newElements, + }); + }, + + MOVE_ELEMENT_UP: (editor, payload) => { + const { elementId } = payload; + const [element, parent, currentIndex] = findElementAndParent( + editor.state.elements, + elementId, + ); + if ( + !element || + !parent || + !Array.isArray(parent.content) || + currentIndex <= 0 + ) + return editor; + + return actionHandlers.MOVE_ELEMENT(editor, { + elementId, + newParentId: parent.id, + newIndex: currentIndex - 1, + }); + }, + + MOVE_ELEMENT_DOWN: (editor, payload) => { + const { elementId } = payload; + const [element, parent, currentIndex] = findElementAndParent( + editor.state.elements, + elementId, + ); + if ( + !element || + !parent || + !Array.isArray(parent.content) || + currentIndex === parent.content.length - 1 + ) + return editor; + + return actionHandlers.MOVE_ELEMENT(editor, { + elementId, + newParentId: parent.id, + newIndex: currentIndex + 1, + }); + }, + UPDATE_ELEMENT: (editor, payload) => { const newElements = traverseElements(editor.state.elements, (element) => { if (element.id === payload.elementDetails.id) { @@ -163,6 +304,7 @@ const actionHandlers: { return updateEditorHistory(editor, { ...editor.state, elements: newElements, + selectedElement: emptyElement, }); }, diff --git a/src/components/editor/elements/container.tsx b/src/components/editor/elements/container.tsx index 5e3c93c..bc50056 100644 --- a/src/components/editor/elements/container.tsx +++ b/src/components/editor/elements/container.tsx @@ -16,7 +16,7 @@ type Props = { element: EditorElement }; export default function Container({ element }: Props) { const { id, content, type } = element; - const { dispatch } = useEditor(); + const { editor, dispatch } = useEditor(); const isRoot = type === "__body"; const handleDragStart = (e: React.DragEvent) => { @@ -126,8 +126,11 @@ export default function Container({ element }: Props) { ({}); + + const updateLayerStyle = useCallback((id: string) => { + const el = document.querySelector(`[data-element-id="${id}"]`); + + if (!el) { + return; + } + + const resizeObserver = new ResizeObserver(() => { + const { top, left, width, height } = el.getBoundingClientRect(); + setLayerStyle({ top, left, width, height }); + }); + + resizeObserver.observe(el, { box: "border-box" }); + return () => { + resizeObserver.unobserve(el); + }; + }, []); + + useEffect(() => { + return updateLayerStyle(element.id); + }, [element.id, updateLayerStyle]); + + const handleMoveUp = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch({ + type: "MOVE_ELEMENT_UP", + payload: { + elementId: element.id, + }, + }); + updateLayerStyle(element.id); + }; + + const handleMoveDown = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch({ + type: "MOVE_ELEMENT_DOWN", + payload: { + elementId: element.id, + }, + }); + updateLayerStyle(element.id); + }; + + const handleDeleteElement = () => { + dispatch({ + type: "DELETE_ELEMENT", + payload: { + elementDetails: element, + }, + }); + }; + + return ( + typeof window !== "undefined" && + createPortal( + !editor.state.isPreviewMode && isValidSelectEditorElement(element) && ( +
+ + {element.name} + +
+
+ + + + + + + + + +
+
+
+
+ + + + + + +
+
+
+ ), + document.body, + ) + ); +} + +function IconButton(props: React.ComponentProps<"button">) { + return