UNPKG

react-grid-layout

Version:

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

744 lines (718 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _fastEquals = require("fast-equals"); var _clsx = _interopRequireDefault(require("clsx")); var _utils = require("./utils"); var _calculateUtils = require("./calculateUtils"); var _GridItem = _interopRequireDefault(require("./GridItem")); var _ReactGridLayoutPropTypes = _interopRequireDefault(require("./ReactGridLayoutPropTypes")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /*:: import type { ChildrenArray as ReactChildrenArray, Element as ReactElement } from "react";*/ /*:: import type { CompactType, GridResizeEvent, GridDragEvent, DragOverEvent, Layout, DroppingPosition, LayoutItem } from "./utils";*/ // Types /*:: import type { PositionParams } from "./calculateUtils";*/ /*:: type State = { activeDrag: ?LayoutItem, layout: Layout, mounted: boolean, oldDragItem: ?LayoutItem, oldLayout: ?Layout, oldResizeItem: ?LayoutItem, resizing: boolean, droppingDOMNode: ?ReactElement<any>, droppingPosition?: DroppingPosition, // Mirrored props children: ReactChildrenArray<ReactElement<any>>, compactType?: CompactType, propsLayout?: Layout };*/ /*:: import type { Props, DefaultProps } from "./ReactGridLayoutPropTypes";*/ // End Types const layoutClassName = "react-grid-layout"; let isFirefox = false; // Try...catch will protect from navigator not existing (e.g. node) or a bad implementation of navigator try { isFirefox = /firefox/i.test(navigator.userAgent); } catch (e) { /* Ignore */ } /** * A reactive, fluid grid layout with draggable, resizable components. */ class ReactGridLayout extends React.Component /*:: <Props, State>*/{ constructor() { super(...arguments); _defineProperty(this, "state", { activeDrag: null, layout: (0, _utils.synchronizeLayoutWithChildren)(this.props.layout, this.props.children, this.props.cols, // Legacy support for verticalCompact: false (0, _utils.compactType)(this.props), this.props.allowOverlap), mounted: false, oldDragItem: null, oldLayout: null, oldResizeItem: null, resizing: false, droppingDOMNode: null, children: [] }); _defineProperty(this, "dragEnterCounter", 0); /** * When dragging starts * @param {String} i Id of the child * @param {Number} x X position of the move * @param {Number} y Y position of the move * @param {Event} e The mousedown event * @param {Element} node The current dragging DOM element */ _defineProperty(this, "onDragStart", (i /*: string*/, x /*: number*/, y /*: number*/, _ref /*:: */) => { let { e, node } /*: GridDragEvent*/ = _ref /*: GridDragEvent*/; const { layout } = this.state; const l = (0, _utils.getLayoutItem)(layout, i); if (!l) return; // Create placeholder (display only) const placeholder = { w: l.w, h: l.h, x: l.x, y: l.y, placeholder: true, i: i }; this.setState({ oldDragItem: (0, _utils.cloneLayoutItem)(l), oldLayout: layout, activeDrag: placeholder }); return this.props.onDragStart(layout, l, l, null, e, node); }); /** * Each drag movement create a new dragelement and move the element to the dragged location * @param {String} i Id of the child * @param {Number} x X position of the move * @param {Number} y Y position of the move * @param {Event} e The mousedown event * @param {Element} node The current dragging DOM element */ _defineProperty(this, "onDrag", (i, x, y, _ref2) => { let { e, node } = _ref2; const { oldDragItem } = this.state; let { layout } = this.state; const { cols, allowOverlap, preventCollision } = this.props; const l = (0, _utils.getLayoutItem)(layout, i); if (!l) return; // Create placeholder (display only) const placeholder = { w: l.w, h: l.h, x: l.x, y: l.y, placeholder: true, i: i }; // Move the element to the dragged location. const isUserAction = true; layout = (0, _utils.moveElement)(layout, l, x, y, isUserAction, preventCollision, (0, _utils.compactType)(this.props), cols, allowOverlap); this.props.onDrag(layout, oldDragItem, l, placeholder, e, node); this.setState({ layout: allowOverlap ? layout : (0, _utils.compact)(layout, (0, _utils.compactType)(this.props), cols), activeDrag: placeholder }); }); /** * When dragging stops, figure out which position the element is closest to and update its x and y. * @param {String} i Index of the child. * @param {Number} x X position of the move * @param {Number} y Y position of the move * @param {Event} e The mousedown event * @param {Element} node The current dragging DOM element */ _defineProperty(this, "onDragStop", (i, x, y, _ref3) => { let { e, node } = _ref3; if (!this.state.activeDrag) return; const { oldDragItem } = this.state; let { layout } = this.state; const { cols, preventCollision, allowOverlap } = this.props; const l = (0, _utils.getLayoutItem)(layout, i); if (!l) return; // Move the element here const isUserAction = true; layout = (0, _utils.moveElement)(layout, l, x, y, isUserAction, preventCollision, (0, _utils.compactType)(this.props), cols, allowOverlap); // Set state const newLayout = allowOverlap ? layout : (0, _utils.compact)(layout, (0, _utils.compactType)(this.props), cols); this.props.onDragStop(newLayout, oldDragItem, l, null, e, node); const { oldLayout } = this.state; this.setState({ activeDrag: null, layout: newLayout, oldDragItem: null, oldLayout: null }); this.onLayoutMaybeChanged(newLayout, oldLayout); }); _defineProperty(this, "onResizeStart", (i, w, h, _ref4) => { let { e, node } = _ref4; const { layout } = this.state; const l = (0, _utils.getLayoutItem)(layout, i); if (!l) return; this.setState({ oldResizeItem: (0, _utils.cloneLayoutItem)(l), oldLayout: this.state.layout, resizing: true }); this.props.onResizeStart(layout, l, l, null, e, node); }); _defineProperty(this, "onResize", (i, w, h, _ref5) => { let { e, node, size, handle } = _ref5; const { oldResizeItem } = this.state; const { layout } = this.state; const { cols, preventCollision, allowOverlap } = this.props; let shouldMoveItem = false; let finalLayout; let x; let y; const [newLayout, l] = (0, _utils.withLayoutItem)(layout, i, l => { let hasCollisions; x = l.x; y = l.y; if (["sw", "w", "nw", "n", "ne"].indexOf(handle) !== -1) { if (["sw", "nw", "w"].indexOf(handle) !== -1) { x = l.x + (l.w - w); w = l.x !== x && x < 0 ? l.w : w; x = x < 0 ? 0 : x; } if (["ne", "n", "nw"].indexOf(handle) !== -1) { y = l.y + (l.h - h); h = l.y !== y && y < 0 ? l.h : h; y = y < 0 ? 0 : y; } shouldMoveItem = true; } // Something like quad tree should be used // to find collisions faster if (preventCollision && !allowOverlap) { const collisions = (0, _utils.getAllCollisions)(layout, { ...l, w, h, x, y }).filter(layoutItem => layoutItem.i !== l.i); hasCollisions = collisions.length > 0; // If we're colliding, we need adjust the placeholder. if (hasCollisions) { // Reset layoutItem dimensions if there were collisions y = l.y; h = l.h; x = l.x; w = l.w; shouldMoveItem = false; } } l.w = w; l.h = h; return l; }); // Shouldn't ever happen, but typechecking makes it necessary if (!l) return; finalLayout = newLayout; if (shouldMoveItem) { // Move the element to the new position. const isUserAction = true; finalLayout = (0, _utils.moveElement)(newLayout, l, x, y, isUserAction, this.props.preventCollision, (0, _utils.compactType)(this.props), cols, allowOverlap); } // Create placeholder element (display only) const placeholder = { w: l.w, h: l.h, x: l.x, y: l.y, static: true, i: i }; this.props.onResize(finalLayout, oldResizeItem, l, placeholder, e, node); // Re-compact the newLayout and set the drag placeholder. this.setState({ layout: allowOverlap ? finalLayout : (0, _utils.compact)(finalLayout, (0, _utils.compactType)(this.props), cols), activeDrag: placeholder }); }); _defineProperty(this, "onResizeStop", (i, w, h, _ref6) => { let { e, node } = _ref6; const { layout, oldResizeItem } = this.state; const { cols, allowOverlap } = this.props; const l = (0, _utils.getLayoutItem)(layout, i); // Set state const newLayout = allowOverlap ? layout : (0, _utils.compact)(layout, (0, _utils.compactType)(this.props), cols); this.props.onResizeStop(newLayout, oldResizeItem, l, null, e, node); const { oldLayout } = this.state; this.setState({ activeDrag: null, layout: newLayout, oldResizeItem: null, oldLayout: null, resizing: false }); this.onLayoutMaybeChanged(newLayout, oldLayout); }); // Called while dragging an element. Part of browser native drag/drop API. // Native event target might be the layout itself, or an element within the layout. _defineProperty(this, "onDragOver", e => { e.preventDefault(); // Prevent any browser native action e.stopPropagation(); // we should ignore events from layout's children in Firefox // to avoid unpredictable jumping of a dropping placeholder // FIXME remove this hack if (isFirefox && // $FlowIgnore can't figure this out !e.nativeEvent.target?.classList.contains(layoutClassName)) { return false; } const { droppingItem, onDropDragOver, margin, cols, rowHeight, maxRows, width, containerPadding, transformScale } = this.props; // Allow user to customize the dropping item or short-circuit the drop based on the results // of the `onDragOver(e: Event)` callback. const onDragOverResult = onDropDragOver?.(e); if (onDragOverResult === false) { if (this.state.droppingDOMNode) { this.removeDroppingPlaceholder(); } return false; } const finalDroppingItem = { ...droppingItem, ...onDragOverResult }; const { layout } = this.state; // $FlowIgnore missing def const gridRect = e.currentTarget.getBoundingClientRect(); // The grid's position in the viewport // Calculate the mouse position relative to the grid const layerX = e.clientX - gridRect.left; const layerY = e.clientY - gridRect.top; const droppingPosition = { left: layerX / transformScale, top: layerY / transformScale, e }; if (!this.state.droppingDOMNode) { const positionParams /*: PositionParams*/ = { cols, margin, maxRows, rowHeight, containerWidth: width, containerPadding: containerPadding || margin }; const calculatedPosition = (0, _calculateUtils.calcXY)(positionParams, layerY, layerX, finalDroppingItem.w, finalDroppingItem.h); this.setState({ droppingDOMNode: /*#__PURE__*/React.createElement("div", { key: finalDroppingItem.i }), droppingPosition, layout: [...layout, { ...finalDroppingItem, x: calculatedPosition.x, y: calculatedPosition.y, static: false, isDraggable: true }] }); } else if (this.state.droppingPosition) { const { left, top } = this.state.droppingPosition; const shouldUpdatePosition = left != layerX || top != layerY; if (shouldUpdatePosition) { this.setState({ droppingPosition }); } } }); _defineProperty(this, "removeDroppingPlaceholder", () => { const { droppingItem, cols } = this.props; const { layout } = this.state; const newLayout = (0, _utils.compact)(layout.filter(l => l.i !== droppingItem.i), (0, _utils.compactType)(this.props), cols, this.props.allowOverlap); this.setState({ layout: newLayout, droppingDOMNode: null, activeDrag: null, droppingPosition: undefined }); }); _defineProperty(this, "onDragLeave", e => { e.preventDefault(); // Prevent any browser native action e.stopPropagation(); this.dragEnterCounter--; // onDragLeave can be triggered on each layout's child. // But we know that count of dragEnter and dragLeave events // will be balanced after leaving the layout's container // so we can increase and decrease count of dragEnter and // when it'll be equal to 0 we'll remove the placeholder if (this.dragEnterCounter === 0) { this.removeDroppingPlaceholder(); } }); _defineProperty(this, "onDragEnter", e => { e.preventDefault(); // Prevent any browser native action e.stopPropagation(); this.dragEnterCounter++; }); _defineProperty(this, "onDrop", (e /*: Event*/) => { e.preventDefault(); // Prevent any browser native action e.stopPropagation(); const { droppingItem } = this.props; const { layout } = this.state; const item = layout.find(l => l.i === droppingItem.i); // reset dragEnter counter on drop this.dragEnterCounter = 0; this.removeDroppingPlaceholder(); this.props.onDrop(layout, item, e); }); } componentDidMount() { this.setState({ mounted: true }); // Possibly call back with layout on mount. This should be done after correcting the layout width // to ensure we don't rerender with the wrong width. this.onLayoutMaybeChanged(this.state.layout, this.props.layout); } static getDerivedStateFromProps(nextProps /*: Props*/, prevState /*: State*/) /*: $Shape<State> | null*/{ let newLayoutBase; if (prevState.activeDrag) { return null; } // Legacy support for compactType // Allow parent to set layout directly. if (!(0, _fastEquals.deepEqual)(nextProps.layout, prevState.propsLayout) || nextProps.compactType !== prevState.compactType) { newLayoutBase = nextProps.layout; } else if (!(0, _utils.childrenEqual)(nextProps.children, prevState.children)) { // If children change, also regenerate the layout. Use our state // as the base in case because it may be more up to date than // what is in props. newLayoutBase = prevState.layout; } // We need to regenerate the layout. if (newLayoutBase) { const newLayout = (0, _utils.synchronizeLayoutWithChildren)(newLayoutBase, nextProps.children, nextProps.cols, (0, _utils.compactType)(nextProps), nextProps.allowOverlap); return { layout: newLayout, // We need to save these props to state for using // getDerivedStateFromProps instead of componentDidMount (in which we would get extra rerender) compactType: nextProps.compactType, children: nextProps.children, propsLayout: nextProps.layout }; } return null; } shouldComponentUpdate(nextProps /*: Props*/, nextState /*: State*/) /*: boolean*/{ return ( // NOTE: this is almost always unequal. Therefore the only way to get better performance // from SCU is if the user intentionally memoizes children. If they do, and they can // handle changes properly, performance will increase. this.props.children !== nextProps.children || !(0, _utils.fastRGLPropsEqual)(this.props, nextProps, _fastEquals.deepEqual) || this.state.activeDrag !== nextState.activeDrag || this.state.mounted !== nextState.mounted || this.state.droppingPosition !== nextState.droppingPosition ); } componentDidUpdate(prevProps /*: Props*/, prevState /*: State*/) { if (!this.state.activeDrag) { const newLayout = this.state.layout; const oldLayout = prevState.layout; this.onLayoutMaybeChanged(newLayout, oldLayout); } } /** * Calculates a pixel value for the container. * @return {String} Container height in pixels. */ containerHeight() /*: ?string*/{ if (!this.props.autoSize) return; const nbRow = (0, _utils.bottom)(this.state.layout); const containerPaddingY = this.props.containerPadding ? this.props.containerPadding[1] : this.props.margin[1]; return nbRow * this.props.rowHeight + (nbRow - 1) * this.props.margin[1] + containerPaddingY * 2 + "px"; } onLayoutMaybeChanged(newLayout /*: Layout*/, oldLayout /*: ?Layout*/) { if (!oldLayout) oldLayout = this.state.layout; if (!(0, _fastEquals.deepEqual)(oldLayout, newLayout)) { this.props.onLayoutChange(newLayout); } } /** * Create a placeholder object. * @return {Element} Placeholder div. */ placeholder() /*: ?ReactElement<any>*/{ const { activeDrag } = this.state; if (!activeDrag) return null; const { width, cols, margin, containerPadding, rowHeight, maxRows, useCSSTransforms, transformScale } = this.props; // {...this.state.activeDrag} is pretty slow, actually return /*#__PURE__*/React.createElement(_GridItem.default, { w: activeDrag.w, h: activeDrag.h, x: activeDrag.x, y: activeDrag.y, i: activeDrag.i, className: `react-grid-placeholder ${this.state.resizing ? "placeholder-resizing" : ""}`, containerWidth: width, cols: cols, margin: margin, containerPadding: containerPadding || margin, maxRows: maxRows, rowHeight: rowHeight, isDraggable: false, isResizable: false, isBounded: false, useCSSTransforms: useCSSTransforms, transformScale: transformScale }, /*#__PURE__*/React.createElement("div", null)); } /** * Given a grid item, set its style attributes & surround in a <Draggable>. * @param {Element} child React element. * @return {Element} Element wrapped in draggable and properly placed. */ processGridItem(child /*: ReactElement<any>*/, isDroppingItem /*: boolean*/) /*: ?ReactElement<any>*/{ if (!child || !child.key) return; const l = (0, _utils.getLayoutItem)(this.state.layout, String(child.key)); if (!l) return null; const { width, cols, margin, containerPadding, rowHeight, maxRows, isDraggable, isResizable, isBounded, useCSSTransforms, transformScale, draggableCancel, draggableHandle, resizeHandles, resizeHandle } = this.props; const { mounted, droppingPosition } = this.state; // Determine user manipulations possible. // If an item is static, it can't be manipulated by default. // Any properties defined directly on the grid item will take precedence. const draggable = typeof l.isDraggable === "boolean" ? l.isDraggable : !l.static && isDraggable; const resizable = typeof l.isResizable === "boolean" ? l.isResizable : !l.static && isResizable; const resizeHandlesOptions = l.resizeHandles || resizeHandles; // isBounded set on child if set on parent, and child is not explicitly false const bounded = draggable && isBounded && l.isBounded !== false; return /*#__PURE__*/React.createElement(_GridItem.default, { containerWidth: width, cols: cols, margin: margin, containerPadding: containerPadding || margin, maxRows: maxRows, rowHeight: rowHeight, cancel: draggableCancel, handle: draggableHandle, onDragStop: this.onDragStop, onDragStart: this.onDragStart, onDrag: this.onDrag, onResizeStart: this.onResizeStart, onResize: this.onResize, onResizeStop: this.onResizeStop, isDraggable: draggable, isResizable: resizable, isBounded: bounded, useCSSTransforms: useCSSTransforms && mounted, usePercentages: !mounted, transformScale: transformScale, w: l.w, h: l.h, x: l.x, y: l.y, i: l.i, minH: l.minH, minW: l.minW, maxH: l.maxH, maxW: l.maxW, static: l.static, droppingPosition: isDroppingItem ? droppingPosition : undefined, resizeHandles: resizeHandlesOptions, resizeHandle: resizeHandle }, child); } render() /*: React.Element<"div">*/{ const { className, style, isDroppable, innerRef } = this.props; const mergedClassName = (0, _clsx.default)(layoutClassName, className); const mergedStyle = { height: this.containerHeight(), ...style }; return /*#__PURE__*/React.createElement("div", { ref: innerRef, className: mergedClassName, style: mergedStyle, onDrop: isDroppable ? this.onDrop : _utils.noop, onDragLeave: isDroppable ? this.onDragLeave : _utils.noop, onDragEnter: isDroppable ? this.onDragEnter : _utils.noop, onDragOver: isDroppable ? this.onDragOver : _utils.noop }, React.Children.map(this.props.children, child => this.processGridItem(child)), isDroppable && this.state.droppingDOMNode && this.processGridItem(this.state.droppingDOMNode, true), this.placeholder()); } } exports.default = ReactGridLayout; // TODO publish internal ReactClass displayName transform _defineProperty(ReactGridLayout, "displayName", "ReactGridLayout"); // Refactored to another module to make way for preval _defineProperty(ReactGridLayout, "propTypes", _ReactGridLayoutPropTypes.default); _defineProperty(ReactGridLayout, "defaultProps", { autoSize: true, cols: 12, className: "", style: {}, draggableHandle: "", draggableCancel: "", containerPadding: null, rowHeight: 150, maxRows: Infinity, // infinite vertical growth layout: [], margin: [10, 10], isBounded: false, isDraggable: true, isResizable: true, allowOverlap: false, isDroppable: false, useCSSTransforms: true, transformScale: 1, verticalCompact: true, compactType: "vertical", preventCollision: false, droppingItem: { i: "__dropping-elem__", h: 1, w: 1 }, resizeHandles: ["se"], onLayoutChange: _utils.noop, onDragStart: _utils.noop, onDrag: _utils.noop, onDragStop: _utils.noop, onResizeStart: _utils.noop, onResize: _utils.noop, onResizeStop: _utils.noop, onDrop: _utils.noop, onDropDragOver: _utils.noop });