react-drag-panel
Version:
Drag & drop panel grid layout for React
332 lines (326 loc) • 10.7 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
PanelGrid: () => PanelGrid_default2
});
module.exports = __toCommonJS(index_exports);
// src/components/PanelGrid.tsx
var import_react = __toESM(require("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
var import_jsx_runtime = require("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] = (0, import_react.useState)(panels);
const dragItem = (0, import_react.useRef)(null);
const containerRef = (0, import_react.useRef)(null);
const originalPanels = (0, import_react.useRef)(panels);
const lastDragOverPosRef = (0, import_react.useRef)(null);
const [dragOverPosition, setDragOverPosition] = (0, import_react.useState)(null);
const unitWidth = width / cols;
const containerHeight = calculateContainerHeight({
panels: panelList,
rowHeight,
marginY: margin[1],
paddingY: padding[1]
});
const childrenArray = (0, import_react.useMemo)(() => import_react.default.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 = (0, import_react.useMemo)(() => {
if (!dragOverPosition) return null;
return /* @__PURE__ */ (0, import_jsx_runtime.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 = (0, import_react.useMemo)(() => {
return childrenArray.map((child, index) => {
if (!(0, import_react.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 (0, import_react.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__ */ (0, import_jsx_runtime.jsxs)(
"div",
{
ref: containerRef,
style: { height: containerHeight, width },
className: PanelGrid_default.gridContainer,
"data-testid": "grid-container",
children: [
dropIndicatorElement,
styledChildren
]
}
);
};
var PanelGrid_default2 = PanelGrid;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PanelGrid
});
//# sourceMappingURL=index.js.map