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(