From 3af5748e3a010e4cc86b3d1ac1e1a05e504aea8b Mon Sep 17 00:00:00 2001 From: bepyan Date: Thu, 15 Aug 2024 22:03:50 +0900 Subject: [PATCH] feat: MOVE_ELEMENT --- src/components/editor/action.ts | 141 ++++++++++++++++++ .../editor/elements/element-helper.tsx | 29 +++- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/src/components/editor/action.ts b/src/components/editor/action.ts index 6646653..1bdf46c 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; }; @@ -90,6 +101,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 */ @@ -120,6 +188,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) { diff --git a/src/components/editor/elements/element-helper.tsx b/src/components/editor/elements/element-helper.tsx index 529e893..1888637 100644 --- a/src/components/editor/elements/element-helper.tsx +++ b/src/components/editor/elements/element-helper.tsx @@ -7,15 +7,14 @@ import { GripVerticalIcon, Trash2Icon, } from "lucide-react"; +import { useRef } from "react"; import { createPortal } from "react-dom"; +import { useResizeObserver } from "usehooks-ts"; import { useEditor } from "~/components/editor/provider"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; -import { useRef } from "react"; -import { useResizeObserver } from "usehooks-ts"; - export default function ElementHelper() { const { editor, dispatch } = useEditor(); const element = editor.state.selectedElement; @@ -27,6 +26,26 @@ export default function ElementHelper() { box: "border-box", }); + const handleMoveUp = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch({ + type: "MOVE_ELEMENT_UP", + payload: { + elementId: element.id, + }, + }); + }; + + const handleMoveDown = (e: React.MouseEvent) => { + e.stopPropagation(); + dispatch({ + type: "MOVE_ELEMENT_DOWN", + payload: { + elementId: element.id, + }, + }); + }; + const handleDeleteElement = () => { dispatch({ type: "DELETE_ELEMENT", @@ -60,10 +79,10 @@ export default function ElementHelper() { - + - +