react-grid-layout
Version:
A draggable and resizable grid layout with responsive breakpoints, for React.
425 lines (423 loc) • 11.7 kB
JavaScript
import { verticalCompactor, sortBreakpoints, getBreakpointFromWidth, getColsFromBreakpoint, findOrGenerateResponsiveLayout } from './chunk-XYPIYYYQ.mjs';
import { correctBounds, cloneLayout, getLayoutItem, cloneLayoutItem, moveElement, bottom } from './chunk-AWM66AWF.mjs';
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { deepEqual } from 'fast-equals';
function useContainerWidth(options = {}) {
const { measureBeforeMount = false, initialWidth = 1280 } = options;
const [width, setWidth] = useState(initialWidth);
const [mounted, setMounted] = useState(!measureBeforeMount);
const containerRef = useRef(null);
const observerRef = useRef(null);
const measureWidth = useCallback(() => {
const node = containerRef.current;
if (node) {
const newWidth = node.offsetWidth;
setWidth(newWidth);
if (!mounted) {
setMounted(true);
}
}
}, [mounted]);
useEffect(() => {
const node = containerRef.current;
if (!node) return;
measureWidth();
if (typeof ResizeObserver !== "undefined") {
observerRef.current = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
const newWidth = entry.contentRect.width;
setWidth(newWidth);
}
});
observerRef.current.observe(node);
}
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
};
}, [measureWidth]);
return {
width,
mounted,
containerRef,
measureWidth
};
}
function useGridLayout(options) {
const {
layout: propsLayout,
cols,
preventCollision = false,
onLayoutChange,
compactor = verticalCompactor
} = options;
const isDraggingRef = useRef(false);
const [layout, setLayoutState] = useState(() => {
const corrected = correctBounds(cloneLayout(propsLayout), { cols });
return compactor.compact(corrected, cols);
});
const [dragState, setDragState] = useState({
activeDrag: null,
oldDragItem: null,
oldLayout: null
});
const [resizeState, setResizeState] = useState({
resizing: false,
oldResizeItem: null,
oldLayout: null
});
const [dropState, setDropState] = useState({
droppingDOMNode: null,
droppingPosition: null
});
const prevLayoutRef = useRef(layout);
const setLayout = useCallback(
(newLayout) => {
const corrected = correctBounds(cloneLayout(newLayout), { cols });
const compacted = compactor.compact(corrected, cols);
setLayoutState(compacted);
},
[cols, compactor]
);
useEffect(() => {
if (isDraggingRef.current) return;
if (!deepEqual(propsLayout, prevLayoutRef.current)) {
setLayout(propsLayout);
}
}, [propsLayout, setLayout]);
useEffect(() => {
if (!deepEqual(layout, prevLayoutRef.current)) {
prevLayoutRef.current = layout;
onLayoutChange?.(layout);
}
}, [layout, onLayoutChange]);
const onDragStart = useCallback(
(itemId, x, y) => {
const item = getLayoutItem(layout, itemId);
if (!item) return null;
isDraggingRef.current = true;
const placeholder = {
...cloneLayoutItem(item),
x,
y,
static: false,
moved: false
};
setDragState({
activeDrag: placeholder,
oldDragItem: cloneLayoutItem(item),
oldLayout: cloneLayout(layout)
});
return placeholder;
},
[layout]
);
const onDrag = useCallback(
(itemId, x, y) => {
const item = getLayoutItem(layout, itemId);
if (!item) return;
setDragState((prev) => ({
...prev,
activeDrag: prev.activeDrag ? { ...prev.activeDrag, x, y } : null
}));
const newLayout = moveElement(
layout,
item,
x,
y,
true,
// isUserAction
preventCollision,
compactor.type,
cols,
compactor.allowOverlap
);
const compacted = compactor.compact(newLayout, cols);
setLayoutState(compacted);
},
[layout, cols, compactor, preventCollision]
);
const onDragStop = useCallback(
(itemId, x, y) => {
const item = getLayoutItem(layout, itemId);
if (!item) return;
const newLayout = moveElement(
layout,
item,
x,
y,
true,
preventCollision,
compactor.type,
cols,
compactor.allowOverlap
);
const compacted = compactor.compact(newLayout, cols);
isDraggingRef.current = false;
setDragState({
activeDrag: null,
oldDragItem: null,
oldLayout: null
});
setLayoutState(compacted);
},
[layout, cols, compactor, preventCollision]
);
const onResizeStart = useCallback(
(itemId) => {
const item = getLayoutItem(layout, itemId);
if (!item) return null;
setResizeState({
resizing: true,
oldResizeItem: cloneLayoutItem(item),
oldLayout: cloneLayout(layout)
});
return item;
},
[layout]
);
const onResize = useCallback(
(itemId, w, h, x, y) => {
const newLayout = layout.map((item) => {
if (item.i === itemId) {
const updated = {
...item,
w,
h
};
if (x !== void 0) updated.x = x;
if (y !== void 0) updated.y = y;
return updated;
}
return item;
});
const corrected = correctBounds(newLayout, { cols });
const compacted = compactor.compact(corrected, cols);
setLayoutState(compacted);
},
[layout, cols, compactor]
);
const onResizeStop = useCallback(
(itemId, w, h) => {
onResize(itemId, w, h);
setResizeState({
resizing: false,
oldResizeItem: null,
oldLayout: null
});
},
[onResize]
);
const onDropDragOver = useCallback(
(droppingItem, position) => {
const existingItem = getLayoutItem(layout, droppingItem.i);
if (!existingItem) {
const newLayout = [...layout, droppingItem];
const corrected = correctBounds(newLayout, { cols });
const compacted = compactor.compact(corrected, cols);
setLayoutState(compacted);
}
setDropState({
droppingDOMNode: null,
// Will be set by component
droppingPosition: position
});
},
[layout, cols, compactor]
);
const onDropDragLeave = useCallback(() => {
const newLayout = layout.filter((item) => item.i !== "__dropping-elem__");
setLayoutState(newLayout);
setDropState({
droppingDOMNode: null,
droppingPosition: null
});
}, [layout]);
const onDrop = useCallback(
(droppingItem) => {
const newLayout = layout.map((item) => {
if (item.i === "__dropping-elem__") {
return {
...item,
i: droppingItem.i,
static: false
};
}
return item;
});
const corrected = correctBounds(newLayout, { cols });
const compacted = compactor.compact(corrected, cols);
setLayoutState(compacted);
setDropState({
droppingDOMNode: null,
droppingPosition: null
});
},
[layout, cols, compactor]
);
const containerHeight = useMemo(() => bottom(layout), [layout]);
const isInteracting = dragState.activeDrag !== null || resizeState.resizing || dropState.droppingPosition !== null;
return {
layout,
setLayout,
dragState,
resizeState,
dropState,
onDragStart,
onDrag,
onDragStop,
onResizeStart,
onResize,
onResizeStop,
onDropDragOver,
onDropDragLeave,
onDrop,
containerHeight,
isInteracting,
compactor
};
}
var DEFAULT_BREAKPOINTS = {
lg: 1200,
md: 996,
sm: 768,
xs: 480,
xxs: 0
};
var DEFAULT_COLS = {
lg: 12,
md: 10,
sm: 6,
xs: 4,
xxs: 2
};
function useResponsiveLayout(options) {
const {
width,
breakpoints = DEFAULT_BREAKPOINTS,
cols: colsConfig = DEFAULT_COLS,
layouts: propsLayouts = {},
compactor = verticalCompactor,
onBreakpointChange,
onLayoutChange,
onWidthChange
} = options;
const sortedBreakpoints = useMemo(
() => sortBreakpoints(breakpoints),
[breakpoints]
);
const initialBreakpoint = useMemo(
() => getBreakpointFromWidth(breakpoints, width),
// Only calculate on mount, not on width changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const initialCols = useMemo(
() => getColsFromBreakpoint(initialBreakpoint, colsConfig),
[initialBreakpoint, colsConfig]
);
const [breakpoint, setBreakpoint] = useState(initialBreakpoint);
const [cols, setCols] = useState(initialCols);
const [layouts, setLayoutsState] = useState(() => {
const cloned = {};
for (const bp of sortedBreakpoints) {
const layout2 = propsLayouts[bp];
if (layout2) {
cloned[bp] = cloneLayout(layout2);
}
}
return cloned;
});
const prevWidthRef = useRef(width);
const prevBreakpointRef = useRef(breakpoint);
const prevPropsLayoutsRef = useRef(propsLayouts);
const prevLayoutsRef = useRef(layouts);
const layout = useMemo(() => {
return findOrGenerateResponsiveLayout(
layouts,
breakpoints,
breakpoint,
prevBreakpointRef.current,
cols,
compactor
);
}, [layouts, breakpoints, breakpoint, cols, compactor]);
const setLayoutForBreakpoint = useCallback((bp, newLayout) => {
setLayoutsState((prev) => ({
...prev,
[bp]: cloneLayout(newLayout)
}));
}, []);
const setLayouts = useCallback((newLayouts) => {
const cloned = {};
for (const bp of Object.keys(newLayouts)) {
const layoutForBp = newLayouts[bp];
if (layoutForBp) {
cloned[bp] = cloneLayout(layoutForBp);
}
}
setLayoutsState(cloned);
}, []);
useEffect(() => {
if (prevWidthRef.current === width) return;
prevWidthRef.current = width;
const newBreakpoint = getBreakpointFromWidth(breakpoints, width);
const newCols = getColsFromBreakpoint(newBreakpoint, colsConfig);
onWidthChange?.(width, [10, 10], newCols, null);
if (newBreakpoint !== breakpoint) {
const newLayout = findOrGenerateResponsiveLayout(
layouts,
breakpoints,
newBreakpoint,
breakpoint,
newCols,
compactor
);
const updatedLayouts = {
...layouts,
[newBreakpoint]: newLayout
};
setLayoutsState(updatedLayouts);
setBreakpoint(newBreakpoint);
setCols(newCols);
onBreakpointChange?.(newBreakpoint, newCols);
prevBreakpointRef.current = newBreakpoint;
}
}, [
width,
breakpoints,
colsConfig,
breakpoint,
layouts,
compactor,
onBreakpointChange,
onWidthChange
]);
useEffect(() => {
if (!deepEqual(propsLayouts, prevPropsLayoutsRef.current)) {
setLayouts(propsLayouts);
prevPropsLayoutsRef.current = propsLayouts;
}
}, [propsLayouts, setLayouts]);
useEffect(() => {
if (!deepEqual(layouts, prevLayoutsRef.current)) {
prevLayoutsRef.current = layouts;
onLayoutChange?.(layout, layouts);
}
}, [layout, layouts, onLayoutChange]);
return {
layout,
layouts,
breakpoint,
cols,
setLayoutForBreakpoint,
setLayouts,
sortedBreakpoints
};
}
export { DEFAULT_BREAKPOINTS, DEFAULT_COLS, useContainerWidth, useGridLayout, useResponsiveLayout };
//# sourceMappingURL=chunk-YFVX5RDK.mjs.map
//# sourceMappingURL=chunk-YFVX5RDK.mjs.map