diff --git a/src/core/components/canvas/BlockFloatingActions.tsx b/src/core/components/canvas/BlockFloatingActions.tsx index 380fda60..60658c80 100644 --- a/src/core/components/canvas/BlockFloatingActions.tsx +++ b/src/core/components/canvas/BlockFloatingActions.tsx @@ -29,7 +29,7 @@ const BlockActionLabel = ({ block, label }: any) => { const dnd = useFeature("dnd"); return (
{ ev.dataTransfer.setData("text/plain", JSON.stringify(pick(block, ["_id", "_type", "_name"]))); diff --git a/src/core/components/canvas/dnd/useDnd.ts b/src/core/components/canvas/dnd/useDnd.ts index aedbde83..5fe225f6 100644 --- a/src/core/components/canvas/dnd/useDnd.ts +++ b/src/core/components/canvas/dnd/useDnd.ts @@ -1,4 +1,4 @@ -import { DragEvent } from "react"; +import { DragEvent, useEffect } from "react"; import { has, throttle } from "lodash-es"; import { useFrame } from "../../../frame"; @@ -10,9 +10,10 @@ import { useBlocksStoreUndoableActions } from "../../../history/useBlocksStoreUn import { getOrientation } from "./getOrientation.ts"; import { draggedBlockAtom, dropTargetBlockIdAtom } from "./atoms.ts"; import { useFeature } from "flagged"; +import { canAcceptChildBlock } from "../../../functions/block-helpers.ts"; let iframeDocument: null | HTMLDocument = null; -let possiblePositions: [number, number, number][] = []; +let possiblePositions: [number, number, number, number][] = []; let dropTarget: HTMLElement | null = null; let dropIndex: number | null = null; @@ -40,34 +41,35 @@ const positionPlaceholder = (target: HTMLElement, orientation: "vertical" | "hor ); const closestIndex = positions.indexOf(closest); - if (!possiblePositions[closestIndex]) return; const values = possiblePositions[closestIndex]; - placeholder.style.width = orientation === "vertical" ? values[2] + "px" : "3px"; - placeholder.style.height = orientation === "vertical" ? "3px" : values[2] + "px"; + placeholder.style.width = orientation === "vertical" ? values[2] + "px" : "4px"; + placeholder.style.height = orientation === "vertical" ? "4px" : values[2] + "px"; placeholder.style.display = "block"; if (orientation === "vertical") { - placeholder.style.top = values[0] - 1.5 + "px"; + placeholder.style.top = values[0] - 1 + "px"; placeholder.style.left = values[1] + "px"; } else { placeholder.style.top = values[1] + "px"; - placeholder.style.left = values[0] + "px"; + placeholder.style.left = values[0] - 1 + "px"; } }; -function calculateDropIndex(mousePosition: number, positions: [number, number, number][]) { - let closestIndex = 0; - let closestDistance = Infinity; - positions.forEach((position, index) => { - const distance = Math.abs(position[0] - mousePosition); - if (distance < closestDistance) { - closestDistance = distance; - closestIndex = index; - } +function calculateDropIndex(mousePosition: number, possiblePositions: [number, number, number, number][]) { + const positions = possiblePositions.map(([position]) => { + return position; }); - return closestIndex; + const closest = positions.reduce( + (prev, curr) => (Math.abs(curr - mousePosition) < Math.abs(prev - mousePosition) ? curr : prev), + 0, + ); + + const _closestIndex = positions.indexOf(closest); + if (!possiblePositions[_closestIndex]) return 0; + + return possiblePositions[_closestIndex][3]; } const calculatePossiblePositions = (target: HTMLElement) => { @@ -76,20 +78,38 @@ const calculatePossiblePositions = (target: HTMLElement) => { possiblePositions = []; - Array.from(target.children).forEach((child: HTMLElement, index) => { - // Skip elements with class 'pointer-events-none' - if (child.classList.contains("pointer-events-none")) return; + let blockIndex = 0; + Array.from(target.children).forEach((child: HTMLElement) => { + // * Skip elements with class 'pointer-events-none' + if (child.classList.contains("pointer-events-none") || !child?.getAttribute("data-block-id")) return; const position = isHorizontal ? child.offsetLeft : child.offsetTop; const size = isHorizontal ? [child.offsetTop, child.clientHeight] : [child.offsetLeft, child.clientWidth]; - possiblePositions.push([position, size[0], size[1]]); + possiblePositions.push([position, size[0], size[1], blockIndex]); + blockIndex++; + }); - // Handle last child - if (index === target.children.length - 1) { - const lastPosition = isHorizontal ? child.offsetLeft + child.clientWidth : child.offsetTop + child.clientHeight; - possiblePositions.push([lastPosition, size[0], size[1]]); + if (!isHorizontal) { + if (target?.getAttribute("data-block-id") === "canvas") { + // * Handle adding element at top of canvas if target is canvas + if (target.children.length > 3) { + const _offsetBottom = Array.from(target.children).reduce((acc, child) => acc + child.clientHeight, 0); + possiblePositions.push([0, target.offsetLeft, target.clientWidth, 0]); + possiblePositions.push([_offsetBottom, target.offsetLeft, target.clientWidth, blockIndex]); + } + } else { + possiblePositions.push([ + target.offsetTop + target.clientHeight, + target.offsetLeft, + target.clientWidth, + blockIndex, + ]); } - }); + } else { + if (target?.getAttribute("data-block-id") === "canvas") return; + + possiblePositions.push([target.offsetLeft + target.clientWidth, target.offsetTop, target.clientHeight, blockIndex]); + } }; const throttledDragOver = throttle((e: DragEvent) => { @@ -131,6 +151,14 @@ function removeDataDrop(): void { } } +function canDropInTarget(target, draggedBlock) { + if (!target || !draggedBlock) return; + + const dragBlockType = draggedBlock?.type; + const dropBlockType = target?.getAttribute("data-block-type"); + return canAcceptChildBlock(dropBlockType, dragBlockType); +} + export const useDnd = () => { const { document } = useFrame(); const [isDragging, setIsDragging] = useAtom(draggingFlagAtom); @@ -143,8 +171,6 @@ export const useDnd = () => { const [, setDropTarget] = useAtom(dropTargetBlockIdAtom); const dnd = useFeature("dnd"); - if (!dnd) return {}; - const resetDragState = () => { removePlaceholder(); setIsDragging(false); @@ -153,15 +179,32 @@ export const useDnd = () => { //@ts-ignore setDropTarget(null); possiblePositions = []; + dropTarget = null; + dropIndex = null; }; + useEffect(() => { + // * Handling dropping block outside of canvas + const rootLayout = window.document.getElementById("chaibuilder-root-layout-container"); + const handleOutsideDrop = (e) => { + e.preventDefault(); + resetDragState(); + }; + rootLayout?.addEventListener("drop", handleOutsideDrop); + return () => rootLayout?.removeEventListener("drop", handleOutsideDrop); + }, []); + + if (!dnd) return {}; + iframeDocument = document as HTMLDocument; return { isDragging, onDragOver: (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); - throttledDragOver(e); + if (canDropInTarget(dropTarget, draggedBlock)) { + throttledDragOver(e); + } }, onDrop: (ev: DragEvent) => { if (dropTarget?.id === "chaibuilder-canvas-blank-screen") { @@ -179,13 +222,13 @@ export const useDnd = () => { const id = block.getAttribute("data-block-id"); const isDropTargetAllowed = dropTarget.getAttribute("data-dnd-dragged") === "yes" ? false : true; - const canDrop = dropTarget.getAttribute("data-dnd") === "yes" ? true : false; //if the draggedItem is the same as the dropTarget, reset the drag state. - if (data?._id === id || !isDropTargetAllowed || !canDrop) { + if (data?._id === id || !isDropTargetAllowed || !canDropInTarget(dropTarget, draggedBlock)) { resetDragState(); return; } + // This is for moving blocks from the sidebar Panel and UiLibraryPanel if (!has(data, "_id")) { addCoreBlock(data, id === "canvas" ? null : id, dropIndex); @@ -212,14 +255,13 @@ export const useDnd = () => { dropTarget = target; const dropTargetId = target.getAttribute("data-block-id"); const isdropTargetAllowed = target.getAttribute("data-dnd-dragged") === "yes" ? false : true; - const canDrop = target.getAttribute("data-dnd") === "yes" ? true : false; //@ts-ignore setDropTarget(dropTargetId); event.stopPropagation(); event.preventDefault(); possiblePositions = []; - if (isdropTargetAllowed && canDrop) { + if (isdropTargetAllowed && canDropInTarget(target, draggedBlock)) { calculatePossiblePositions(target); } setIsDragging(true); diff --git a/src/core/history/moveBlocksWithChildren.ts b/src/core/history/moveBlocksWithChildren.ts index 53eee258..66fddf6a 100644 --- a/src/core/history/moveBlocksWithChildren.ts +++ b/src/core/history/moveBlocksWithChildren.ts @@ -26,22 +26,27 @@ function moveNode( ): boolean { const nodeToMove = findNodeById(rootNode, nodeIdToMove); const newParentNode = findNodeById(rootNode, newParentId); - if (nodeToMove && newParentNode) { - nodeToMove.drop(); - // Insert the node at the new parent at the specified position - if (!newParentNode.children) { - newParentNode.model.children = []; - } - try { - newParentNode.addChildAtIndex(nodeToMove, position); - } catch (error) { - console.error("Error adding child to parent:", error); - return false; - } - return true; + if (!nodeToMove || !newParentNode) return false; + + if (!newParentNode.children) { + newParentNode.model.children = []; + } + + let currentPosition = newParentNode?.children?.findIndex((child) => child.model._id === nodeIdToMove); + + nodeToMove.drop(); + + currentPosition = Math.max(currentPosition, 0); + const currentParentId = nodeToMove?.model?._parent || "root"; + const newPosition = currentParentId === newParentId && currentPosition <= position ? position - 1 : position; + + try { + newParentNode.addChildAtIndex(nodeToMove, newPosition); + } catch (error) { + return false; } - return false; + return true; } function moveBlocksWithChildren(