UNPKG

react-drag-panel

Version:
295 lines (291 loc) 8.89 kB
// src/components/PanelGrid.tsx import React, { cloneElement, isValidElement, useMemo, useRef, useState } from "react"; // src/components/PanelGrid.module.css var PanelGrid_default = {}; // src/utils/utils.ts function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function isColliding(panel1, panel2) { const a = { x: 0, y: 0, w: 0, h: 0, ...panel1 }; const b = { x: 0, y: 0, w: 0, h: 0, ...panel2 }; return !(a.x + a.w <= b.x || a.x >= b.x + b.w || a.y + a.h <= b.y || a.y >= b.y + b.h); } // src/utils/calculateUtils.ts function calculatePanelStyle({ dimensions: { x = 0, y = 0, w = 1, h = 1 }, unitWidth, rowHeight, cols = 12, marginX = 0, marginY = 0, paddingX = 0, paddingY = 0 }) { const clampedX = Math.min(x, cols - w); const left = clampedX * (unitWidth + marginX) + paddingX; const top = y * (rowHeight + marginY) + paddingY; const width = unitWidth * w + marginX * (w - 1); const height = rowHeight * h + marginY * (h - 1); return { transform: `translate(${left}px, ${top}px)`, width, height }; } function calculateContainerHeight({ panels, rowHeight, marginY = 0, paddingY = 0 }) { if (panels.length === 0) return 0; const maxY = Math.max(...panels.map((p) => (p?.y || 0) + (p?.h || 1))); return maxY * rowHeight + (maxY - 1) * marginY + paddingY * 2; } function getDropPosition({ event, config: { containerRect, unitWidth, rowHeight, padding, margin, cols, maxRows, panelSize } }) { const mouseLeft = event.clientX - containerRect.left; const mouseTop = event.clientY - containerRect.top; const panelWidth = unitWidth * panelSize.w + margin[0] * (panelSize.w - 1); const panelHeight = rowHeight * panelSize.h + margin[1] * (panelSize.h - 1); const adjustedLeft = mouseLeft - padding[0] - panelWidth / 2; const adjustedTop = mouseTop - padding[1] - panelHeight / 2; const newX = Math.round(adjustedLeft / (unitWidth + margin[0])); const newY = Math.round(adjustedTop / (rowHeight + margin[1])); const maxX = cols - panelSize.w; const maxY = maxRows - panelSize.h; return { x: clamp(newX, 0, maxX), y: clamp(newY, 0, maxY) }; } function resolveCollisions(draggedPanel, panels) { const newPanels = panels.map( (p) => p.id === draggedPanel.id ? draggedPanel : { ...p } ); const visited = /* @__PURE__ */ new Set(); function pushDown(panel) { if (!panel.id) return; if (visited.has(panel.id)) return; visited.add(panel.id); for (const other of newPanels) { if (panel.id === other.id) continue; if (isColliding(panel, other)) { other.y = (panel.y || 0) + (panel.h || 1); pushDown(other); } } } pushDown(draggedPanel); return newPanels; } // src/components/PanelGrid.tsx import { jsx, jsxs } from "react/jsx-runtime"; var PanelGrid = ({ panels = [], cols = 12, rowHeight = 50, width = 1200, margin = [0, 0], padding = [0, 0], isDraggable = true, preventCollision = true, onLayoutChange, children }) => { const [panelList, setPanelList] = useState(panels); const dragItem = useRef(null); const containerRef = useRef(null); const originalPanels = useRef(panels); const lastDragOverPosRef = useRef(null); const [dragOverPosition, setDragOverPosition] = useState(null); const unitWidth = width / cols; const containerHeight = calculateContainerHeight({ panels: panelList, rowHeight, marginY: margin[1], paddingY: padding[1] }); const childrenArray = useMemo(() => React.Children.toArray(children), [children]); const handleDragStart = (panel) => { dragItem.current = panel; originalPanels.current = panelList.map((p) => ({ ...p })); }; const handleDrag = (event) => { event.preventDefault(); if (dragItem.current === null || !containerRef.current) return; const draggedPanel = dragItem.current; const { w = 1, h = 1 } = draggedPanel; const containerRect = containerRef.current.getBoundingClientRect(); const maxRows = Math.floor(containerHeight / rowHeight); if (event.clientX === 0 && event.clientY === 0) return; const { x, y } = getDropPosition({ event, config: { containerRect, unitWidth, rowHeight, padding, margin, cols, maxRows, panelSize: { w, h } } }); const last = lastDragOverPosRef.current; if (last && last.x === x && last.y === y) return; lastDragOverPosRef.current = { x, y }; setDragOverPosition({ x, y, w, h }); const tempDragged = { ...draggedPanel, x, y }; const isOverlap = panelList.some( (panel) => panel.id !== draggedPanel.id && isColliding(tempDragged, panel) ); let nextList = originalPanels.current.map((p) => ({ ...p })); if (preventCollision && isOverlap) { nextList = resolveCollisions(tempDragged, panelList); } const hasChanged = nextList.some((panel, i) => { const prev = panelList[i]; return panel.x !== prev.x || panel.y !== prev.y || panel.w !== prev.w || panel.h !== prev.h; }); if (hasChanged) { setPanelList(nextList); } }; const handleDrop = (event) => { if (dragItem.current === null || !containerRef.current) return; const draggedPanel = dragItem.current; const { w = 1, h = 1 } = draggedPanel; const containerRect = containerRef.current.getBoundingClientRect(); const maxRows = Math.floor(containerHeight / rowHeight); const { x, y } = getDropPosition({ event, config: { containerRect, unitWidth, rowHeight, padding, margin, cols, maxRows, panelSize: { w, h } } }); const tempDragged = { ...draggedPanel, x, y }; const isOverlap = panelList.some( (panel) => panel.id !== draggedPanel.id && isColliding(tempDragged, panel) ); let updatedPanels = panelList; if (preventCollision && isOverlap) { updatedPanels = resolveCollisions(tempDragged, panelList); } else { updatedPanels = panelList.map( (panel) => panel.id === draggedPanel.id ? { ...panel, x, y } : panel ); } const hasChanged = updatedPanels.some((panel, i) => { const prev = panelList[i]; return panel.x !== prev.x || panel.y !== prev.y || panel.w !== prev.w || panel.h !== prev.h; }); if (hasChanged) { requestAnimationFrame(() => { setPanelList(updatedPanels); onLayoutChange?.(updatedPanels); setDragOverPosition(null); lastDragOverPosRef.current = null; }); } else { setDragOverPosition(null); lastDragOverPosRef.current = null; } }; const dropIndicatorElement = useMemo(() => { if (!dragOverPosition) return null; return /* @__PURE__ */ jsx( "div", { className: PanelGrid_default.dropIndicator, style: calculatePanelStyle({ dimensions: dragOverPosition, unitWidth, rowHeight, cols, marginX: margin[0], marginY: margin[1], paddingX: padding[0], paddingY: padding[1] }) } ); }, [dragOverPosition, unitWidth, rowHeight, cols, margin, padding]); const styledChildren = useMemo(() => { return childrenArray.map((child, index) => { if (!isValidElement(child)) return null; const panel = panelList[index]; const style = calculatePanelStyle({ dimensions: { x: panel?.x, y: panel?.y, w: panel?.w, h: panel?.h }, unitWidth, rowHeight, cols, marginX: margin[0], marginY: margin[1], paddingX: padding[0], paddingY: padding[1] }); const element = child; return cloneElement(element, { "data-testid": "grid-panel", key: panel?.id || `default-key-${index}`, style: { ...style, ...element.props.style || {} }, className: [PanelGrid_default.gridItemPanel, element.props.className].filter(Boolean).join(" "), draggable: isDraggable !== false, onDragStart: isDraggable !== false ? () => handleDragStart(panel) : void 0, onDrag: isDraggable !== false ? handleDrag : void 0, onDragEnd: isDraggable !== false ? handleDrop : void 0 }); }); }, [ childrenArray, panelList, unitWidth, rowHeight, cols, margin, padding ]); return /* @__PURE__ */ jsxs( "div", { ref: containerRef, style: { height: containerHeight, width }, className: PanelGrid_default.gridContainer, "data-testid": "grid-container", children: [ dropIndicatorElement, styledChildren ] } ); }; var PanelGrid_default2 = PanelGrid; export { PanelGrid_default2 as PanelGrid }; //# sourceMappingURL=index.mjs.map