Skip to content

Commit

Permalink
feat: enhance drag-and-drop logic with improved position calculations…
Browse files Browse the repository at this point in the history
… and drop validation
  • Loading branch information
kgpatidar committed Dec 20, 2024
1 parent a7fdcd7 commit 03f2b10
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 48 deletions.
2 changes: 1 addition & 1 deletion src/core/components/canvas/BlockFloatingActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const BlockActionLabel = ({ block, label }: any) => {
const dnd = useFeature("dnd");
return (
<div
className="mr-10 flex cursor-default items-center space-x-1 px-1"
className="mr-10 flex cursor-grab items-center space-x-1 px-1"
draggable={dnd ? "true" : "false"}
onDragStart={(ev) => {
ev.dataTransfer.setData("text/plain", JSON.stringify(pick(block, ["_id", "_type", "_name"])));
Expand Down
108 changes: 75 additions & 33 deletions src/core/components/canvas/dnd/useDnd.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DragEvent } from "react";
import { DragEvent, useEffect } from "react";
import { has, throttle } from "lodash-es";
import { useFrame } from "../../../frame";

Expand All @@ -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;

Expand Down Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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);
Expand All @@ -143,8 +171,6 @@ export const useDnd = () => {
const [, setDropTarget] = useAtom(dropTargetBlockIdAtom);
const dnd = useFeature("dnd");

if (!dnd) return {};

const resetDragState = () => {
removePlaceholder();
setIsDragging(false);
Expand All @@ -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") {
Expand All @@ -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);
Expand All @@ -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);
Expand Down
33 changes: 19 additions & 14 deletions src/core/history/moveBlocksWithChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 03f2b10

Please sign in to comment.