UNPKG

react-grid-layout

Version:

A draggable and resizable grid layout with responsive breakpoints, for React.

425 lines (423 loc) 11.7 kB
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