import React, { useEffect, useRef, useState } from "react"; // icons import { ArrowLeft, ArrowRight } from "lucide-react"; // hooks import { useChart } from "../hooks"; // types import { IGanttBlock } from "../types"; type Props = { block: IGanttBlock; blockToRender: (data: any) => React.ReactNode; handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void; enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; }; export const ChartDraggable: React.FC = ({ block, blockToRender, handleBlock, enableBlockLeftResize, enableBlockRightResize, enableBlockMove, }) => { const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); const [posFromLeft, setPosFromLeft] = useState(null); const resizableRef = useRef(null); const { currentViewData, scrollLeft } = useChart(); // check if cursor reaches either end while resizing/dragging const checkScrollEnd = (e: MouseEvent): number => { const SCROLL_THRESHOLD = 70; let delWidth = 0; const ganttContainer = document.querySelector("#gantt-container") as HTMLElement; const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement; const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0; const posFromLeft = e.clientX; // manually scroll to left if reached the left end while dragging if (posFromLeft - (ganttContainer.getBoundingClientRect().left + ganttSidebar.clientWidth) <= SCROLL_THRESHOLD) { if (e.movementX > 0) return 0; delWidth = -5; scrollContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; // manually scroll to right if reached the right end while dragging const posFromRight = ganttContainer.getBoundingClientRect().right - e.clientX; if (posFromRight <= SCROLL_THRESHOLD) { if (e.movementX < 0) return 0; delWidth = 5; scrollContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; return delWidth; }; // handle block resize from the left end const handleBlockLeftResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; if (e.button !== 0) return; const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; const blockInitialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); let initialMarginLeft = parseInt(resizableDiv.style.marginLeft); const handleMouseMove = (e: MouseEvent) => { let delWidth = 0; delWidth = checkScrollEnd(e); // calculate new width and update the initialMarginLeft using -= const newWidth = Math.round((initialWidth -= delWidth) / columnWidth) * columnWidth; // calculate new marginLeft and update the initial marginLeft to the newly calculated one const newMarginLeft = initialMarginLeft - (newWidth - (block.position?.width ?? 0)); initialMarginLeft = newMarginLeft; // block needs to be at least 1 column wide if (newWidth < columnWidth) return; resizableDiv.style.width = `${newWidth}px`; resizableDiv.style.marginLeft = `${newMarginLeft}px`; if (block.position) { block.position.width = newWidth; block.position.marginLeft = newMarginLeft; } }; // remove event listeners and call block handler with the updated start date const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); const totalBlockShifts = Math.ceil((resizableDiv.clientWidth - blockInitialWidth) / columnWidth); handleBlock(totalBlockShifts, "left"); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; // handle block resize from the right end const handleBlockRightResize = (e: React.MouseEvent) => { if (!currentViewData || !resizableRef.current || !block.position) return; if (e.button !== 0) return; const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; const blockInitialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10); const handleMouseMove = (e: MouseEvent) => { let delWidth = 0; delWidth = checkScrollEnd(e); // calculate new width and update the initialMarginLeft using += const newWidth = Math.round((initialWidth += delWidth) / columnWidth) * columnWidth; // block needs to be at least 1 column wide if (newWidth < columnWidth) return; resizableDiv.style.width = `${Math.max(newWidth, 80)}px`; if (block.position) block.position.width = Math.max(newWidth, 80); }; // remove event listeners and call block handler with the updated target date const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); const totalBlockShifts = Math.ceil((resizableDiv.clientWidth - blockInitialWidth) / columnWidth); handleBlock(totalBlockShifts, "right"); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; // handle block x-axis move const handleBlockMove = (e: React.MouseEvent) => { if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return; if (e.button !== 0) return; const resizableDiv = resizableRef.current; const columnWidth = currentViewData.data.width; const blockInitialMarginLeft = parseInt(resizableDiv.style.marginLeft); let initialMarginLeft = parseInt(resizableDiv.style.marginLeft); const handleMouseMove = (e: MouseEvent) => { setIsMoving(true); let delWidth = 0; delWidth = checkScrollEnd(e); // calculate new marginLeft and update the initial marginLeft using -= const newMarginLeft = Math.round((initialMarginLeft += delWidth) / columnWidth) * columnWidth; resizableDiv.style.marginLeft = `${newMarginLeft}px`; if (block.position) block.position.marginLeft = newMarginLeft; }; // remove event listeners and call block handler with the updated dates const handleMouseUp = () => { setIsMoving(false); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); const totalBlockShifts = Math.ceil( (parseInt(resizableDiv.style.marginLeft) - blockInitialMarginLeft) / columnWidth ); handleBlock(totalBlockShifts, "move"); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; // scroll to a hidden block const handleScrollToBlock = () => { const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; if (!scrollContainer || !block.position) return; // update container's scroll position to the block's position scrollContainer.scrollLeft = block.position.marginLeft - 4; }; // update block position from viewport's left end on scroll useEffect(() => { const block = resizableRef.current; if (!block) return; setPosFromLeft(block.getBoundingClientRect().left); }, [scrollLeft]); // check if block is hidden on either side const isBlockHiddenOnLeft = block.position?.marginLeft && block.position?.width && scrollLeft > block.position.marginLeft + block.position.width; const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth; return ( <> {/* move to left side hidden block button */} {isBlockHiddenOnLeft && (
)} {/* move to right side hidden block button */} {isBlockHiddenOnRight && (
)}
{/* left resize drag handle */} {enableBlockLeftResize && ( <>
setIsLeftResizing(true)} onMouseLeave={() => setIsLeftResizing(false)} className="absolute -left-2.5 top-1/2 z-[3] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md" />
)}
{blockToRender(block.data)}
{/* right resize drag handle */} {enableBlockRightResize && ( <>
setIsRightResizing(true)} onMouseLeave={() => setIsRightResizing(false)} className="absolute -right-2.5 top-1/2 z-[2] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md" />
)}
); };