UNPKG

react-grid-layout

Version:

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

640 lines (615 loc) 22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireDefault(require("react")); var _reactDom = require("react-dom"); var _propTypes = _interopRequireDefault(require("prop-types")); var _reactDraggable = require("react-draggable"); var _reactResizable = require("react-resizable"); var _utils = require("./utils"); var _calculateUtils = require("./calculateUtils"); var _ReactGridLayoutPropTypes = require("./ReactGridLayoutPropTypes"); var _clsx = _interopRequireDefault(require("clsx")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } 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 { Element as ReactElement, Node as ReactNode } from "react";*/ /*:: import type { ReactDraggableCallbackData, GridDragEvent, GridResizeEvent, DroppingPosition, Position, ResizeHandleAxis } from "./utils";*/ /*:: import type { PositionParams } from "./calculateUtils";*/ /*:: import type { ResizeHandle, ReactRef } from "./ReactGridLayoutPropTypes";*/ /*:: type PartialPosition = { top: number, left: number };*/ /*:: type GridItemCallback<Data: GridDragEvent | GridResizeEvent> = ( i: string, w: number, h: number, Data ) => void;*/ /*:: type ResizeCallbackData = { node: HTMLElement, size: Position, handle: ResizeHandleAxis };*/ /*:: type GridItemResizeCallback = ( e: Event, data: ResizeCallbackData, position: Position ) => void;*/ /*:: type State = { resizing: ?{ top: number, left: number, width: number, height: number }, dragging: ?{ top: number, left: number }, className: string };*/ /*:: type Props = { children: ReactElement<any>, cols: number, containerWidth: number, margin: [number, number], containerPadding: [number, number], rowHeight: number, maxRows: number, isDraggable: boolean, isResizable: boolean, isBounded: boolean, static?: boolean, useCSSTransforms?: boolean, usePercentages?: boolean, transformScale: number, droppingPosition?: DroppingPosition, className: string, style?: Object, // Draggability cancel: string, handle: string, x: number, y: number, w: number, h: number, minW: number, maxW: number, minH: number, maxH: number, i: string, resizeHandles?: ResizeHandleAxis[], resizeHandle?: ResizeHandle, onDrag?: GridItemCallback<GridDragEvent>, onDragStart?: GridItemCallback<GridDragEvent>, onDragStop?: GridItemCallback<GridDragEvent>, onResize?: GridItemCallback<GridResizeEvent>, onResizeStart?: GridItemCallback<GridResizeEvent>, onResizeStop?: GridItemCallback<GridResizeEvent> };*/ /*:: type DefaultProps = { className: string, cancel: string, handle: string, minH: number, minW: number, maxH: number, maxW: number, transformScale: number };*/ /** * An individual item within a ReactGridLayout. */ class GridItem extends _react.default.Component /*:: <Props, State>*/{ constructor() { super(...arguments); _defineProperty(this, "state", { resizing: null, dragging: null, className: "" }); _defineProperty(this, "elementRef", /*#__PURE__*/_react.default.createRef()); /** * onDragStart event handler * @param {Event} e event data * @param {Object} callbackData an object with node, delta and position information */ _defineProperty(this, "onDragStart", (e, _ref) => { let { node } = _ref; const { onDragStart, transformScale } = this.props; if (!onDragStart) return; const newPosition /*: PartialPosition*/ = { top: 0, left: 0 }; // TODO: this wont work on nested parents const { offsetParent } = node; if (!offsetParent) return; const parentRect = offsetParent.getBoundingClientRect(); const clientRect = node.getBoundingClientRect(); const cLeft = clientRect.left / transformScale; const pLeft = parentRect.left / transformScale; const cTop = clientRect.top / transformScale; const pTop = parentRect.top / transformScale; newPosition.left = cLeft - pLeft + offsetParent.scrollLeft; newPosition.top = cTop - pTop + offsetParent.scrollTop; this.setState({ dragging: newPosition }); // Call callback with this data const { x, y } = (0, _calculateUtils.calcXY)(this.getPositionParams(), newPosition.top, newPosition.left, this.props.w, this.props.h); return onDragStart.call(this, this.props.i, x, y, { e, node, newPosition }); }); /** * onDrag event handler * @param {Event} e event data * @param {Object} callbackData an object with node, delta and position information * @param {boolean} dontFlush if true, will not call flushSync */ _defineProperty(this, "onDrag", (e, _ref2, dontFlush) => { let { node, deltaX, deltaY } = _ref2; const { onDrag } = this.props; if (!onDrag) return; if (!this.state.dragging) { throw new Error("onDrag called before onDragStart."); } let top = this.state.dragging.top + deltaY; let left = this.state.dragging.left + deltaX; const { isBounded, i, w, h, containerWidth } = this.props; const positionParams = this.getPositionParams(); // Boundary calculations; keeps items within the grid if (isBounded) { const { offsetParent } = node; if (offsetParent) { const { margin, rowHeight } = this.props; const bottomBoundary = offsetParent.clientHeight - (0, _calculateUtils.calcGridItemWHPx)(h, rowHeight, margin[1]); top = (0, _calculateUtils.clamp)(top, 0, bottomBoundary); const colWidth = (0, _calculateUtils.calcGridColWidth)(positionParams); const rightBoundary = containerWidth - (0, _calculateUtils.calcGridItemWHPx)(w, colWidth, margin[0]); left = (0, _calculateUtils.clamp)(left, 0, rightBoundary); } } const newPosition /*: PartialPosition*/ = { top, left }; // dontFlush is set if we're calling from inside if (dontFlush) { this.setState({ dragging: newPosition }); } else { (0, _reactDom.flushSync)(() => { this.setState({ dragging: newPosition }); }); } // Call callback with this data const { x, y } = (0, _calculateUtils.calcXY)(positionParams, top, left, w, h); return onDrag.call(this, i, x, y, { e, node, newPosition }); }); /** * onDragStop event handler * @param {Event} e event data * @param {Object} callbackData an object with node, delta and position information */ _defineProperty(this, "onDragStop", (e, _ref3) => { let { node } = _ref3; const { onDragStop } = this.props; if (!onDragStop) return; if (!this.state.dragging) { throw new Error("onDragEnd called before onDragStart."); } const { w, h, i } = this.props; const { left, top } = this.state.dragging; const newPosition /*: PartialPosition*/ = { top, left }; this.setState({ dragging: null }); const { x, y } = (0, _calculateUtils.calcXY)(this.getPositionParams(), top, left, w, h); return onDragStop.call(this, i, x, y, { e, node, newPosition }); }); /** * onResizeStop event handler * @param {Event} e event data * @param {Object} callbackData an object with node and size information */ _defineProperty(this, "onResizeStop", (e, callbackData, position) => this.onResizeHandler(e, callbackData, position, "onResizeStop")); // onResizeStart event handler _defineProperty(this, "onResizeStart", (e, callbackData, position) => this.onResizeHandler(e, callbackData, position, "onResizeStart")); // onResize event handler _defineProperty(this, "onResize", (e, callbackData, position) => this.onResizeHandler(e, callbackData, position, "onResize")); } shouldComponentUpdate(nextProps /*: Props*/, nextState /*: State*/) /*: boolean*/{ // We can't deeply compare children. If the developer memoizes them, we can // use this optimization. if (this.props.children !== nextProps.children) return true; if (this.props.droppingPosition !== nextProps.droppingPosition) return true; // TODO memoize these calculations so they don't take so long? const oldPosition = (0, _calculateUtils.calcGridItemPosition)(this.getPositionParams(this.props), this.props.x, this.props.y, this.props.w, this.props.h, this.state); const newPosition = (0, _calculateUtils.calcGridItemPosition)(this.getPositionParams(nextProps), nextProps.x, nextProps.y, nextProps.w, nextProps.h, nextState); return !(0, _utils.fastPositionEqual)(oldPosition, newPosition) || this.props.useCSSTransforms !== nextProps.useCSSTransforms; } componentDidMount() { this.moveDroppingItem({}); } componentDidUpdate(prevProps /*: Props*/) { this.moveDroppingItem(prevProps); } // When a droppingPosition is present, this means we should fire a move event, as if we had moved // this element by `x, y` pixels. moveDroppingItem(prevProps /*: Props*/) { const { droppingPosition } = this.props; if (!droppingPosition) return; const node = this.elementRef.current; // Can't find DOM node (are we unmounted?) if (!node) return; const prevDroppingPosition = prevProps.droppingPosition || { left: 0, top: 0 }; const { dragging } = this.state; const shouldDrag = dragging && droppingPosition.left !== prevDroppingPosition.left || droppingPosition.top !== prevDroppingPosition.top; if (!dragging) { this.onDragStart(droppingPosition.e, { node, deltaX: droppingPosition.left, deltaY: droppingPosition.top }); } else if (shouldDrag) { const deltaX = droppingPosition.left - dragging.left; const deltaY = droppingPosition.top - dragging.top; this.onDrag(droppingPosition.e, { node, deltaX, deltaY }, true // dontFLush: avoid flushSync to temper warnings ); } } getPositionParams() /*: PositionParams*/{ let props /*: Props*/ = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; return { cols: props.cols, containerPadding: props.containerPadding, containerWidth: props.containerWidth, margin: props.margin, maxRows: props.maxRows, rowHeight: props.rowHeight }; } /** * This is where we set the grid item's absolute placement. It gets a little tricky because we want to do it * well when server rendering, and the only way to do that properly is to use percentage width/left because * we don't know exactly what the browser viewport is. * Unfortunately, CSS Transforms, which are great for performance, break in this instance because a percentage * left is relative to the item itself, not its container! So we cannot use them on the server rendering pass. * * @param {Object} pos Position object with width, height, left, top. * @return {Object} Style object. */ createStyle(pos /*: Position*/) /*: { [key: string]: ?string }*/{ const { usePercentages, containerWidth, useCSSTransforms } = this.props; let style; // CSS Transforms support (default) if (useCSSTransforms) { style = (0, _utils.setTransform)(pos); } else { // top,left (slow) style = (0, _utils.setTopLeft)(pos); // This is used for server rendering. if (usePercentages) { style.left = (0, _utils.perc)(pos.left / containerWidth); style.width = (0, _utils.perc)(pos.width / containerWidth); } } return style; } /** * Mix a Draggable instance into a child. * @param {Element} child Child element. * @return {Element} Child wrapped in Draggable. */ mixinDraggable(child /*: ReactElement<any>*/, isDraggable /*: boolean*/) /*: ReactElement<any>*/{ return /*#__PURE__*/_react.default.createElement(_reactDraggable.DraggableCore, { disabled: !isDraggable, onStart: this.onDragStart, onDrag: this.onDrag, onStop: this.onDragStop, handle: this.props.handle, cancel: ".react-resizable-handle" + (this.props.cancel ? "," + this.props.cancel : ""), scale: this.props.transformScale, nodeRef: this.elementRef }, child); } /** * Utility function to setup callback handler definitions for * similarily structured resize events. */ curryResizeHandler(position /*: Position*/, handler /*: Function*/) /*: Function*/{ return (e /*: Event*/, data /*: ResizeCallbackData*/) => /*: Function*/handler(e, data, position); } /** * Mix a Resizable instance into a child. * @param {Element} child Child element. * @param {Object} position Position object (pixel values) * @return {Element} Child wrapped in Resizable. */ mixinResizable(child /*: ReactElement<any>*/, position /*: Position*/, isResizable /*: boolean*/) /*: ReactElement<any>*/{ const { cols, minW, minH, maxW, maxH, transformScale, resizeHandles, resizeHandle } = this.props; const positionParams = this.getPositionParams(); // This is the max possible width - doesn't go to infinity because of the width of the window const maxWidth = (0, _calculateUtils.calcGridItemPosition)(positionParams, 0, 0, cols, 0).width; // Calculate min/max constraints using our min & maxes const mins = (0, _calculateUtils.calcGridItemPosition)(positionParams, 0, 0, minW, minH); const maxes = (0, _calculateUtils.calcGridItemPosition)(positionParams, 0, 0, maxW, maxH); const minConstraints = [mins.width, mins.height]; const maxConstraints = [Math.min(maxes.width, maxWidth), Math.min(maxes.height, Infinity)]; return /*#__PURE__*/_react.default.createElement(_reactResizable.Resizable // These are opts for the resize handle itself , { draggableOpts: { disabled: !isResizable }, className: isResizable ? undefined : "react-resizable-hide", width: position.width, height: position.height, minConstraints: minConstraints, maxConstraints: maxConstraints, onResizeStop: this.curryResizeHandler(position, this.onResizeStop), onResizeStart: this.curryResizeHandler(position, this.onResizeStart), onResize: this.curryResizeHandler(position, this.onResize), transformScale: transformScale, resizeHandles: resizeHandles, handle: resizeHandle }, child); } /** * Wrapper around resize events to provide more useful data. */ onResizeHandler(e /*: Event*/, _ref4 /*:: */, // 'size' is updated position position /*: Position*/, // existing position handlerName /*: string*/) /*: void*/{ let { node, size, handle } /*: ResizeCallbackData*/ = _ref4 /*: ResizeCallbackData*/; const handler = this.props[handlerName]; if (!handler) return; const { x, y, i, maxH, minH, containerWidth } = this.props; const { minW, maxW } = this.props; // Clamping of dimensions based on resize direction let updatedSize = size; if (node) { updatedSize = (0, _utils.resizeItemInDirection)(handle, position, size, containerWidth); (0, _reactDom.flushSync)(() => { this.setState({ resizing: handlerName === "onResizeStop" ? null : updatedSize }); }); } // Get new XY based on pixel size let { w, h } = (0, _calculateUtils.calcWH)(this.getPositionParams(), updatedSize.width, updatedSize.height, x, y, handle); // Min/max capping. // minW should be at least 1 (TODO propTypes validation?) w = (0, _calculateUtils.clamp)(w, Math.max(minW, 1), maxW); h = (0, _calculateUtils.clamp)(h, minH, maxH); handler.call(this, i, w, h, { e, node, size: updatedSize, handle }); } render() /*: ReactNode*/{ const { x, y, w, h, isDraggable, isResizable, droppingPosition, useCSSTransforms } = this.props; const pos = (0, _calculateUtils.calcGridItemPosition)(this.getPositionParams(), x, y, w, h, this.state); const child = _react.default.Children.only(this.props.children); // Create the child element. We clone the existing element but modify its className and style. let newChild = /*#__PURE__*/_react.default.cloneElement(child, { ref: this.elementRef, className: (0, _clsx.default)("react-grid-item", child.props.className, this.props.className, { static: this.props.static, resizing: Boolean(this.state.resizing), "react-draggable": isDraggable, "react-draggable-dragging": Boolean(this.state.dragging), dropping: Boolean(droppingPosition), cssTransforms: useCSSTransforms }), // We can set the width and height on the child, but unfortunately we can't set the position. style: { ...this.props.style, ...child.props.style, ...this.createStyle(pos) } }); // Resizable support. This is usually on but the user can toggle it off. newChild = this.mixinResizable(newChild, pos, isResizable); // Draggable support. This is always on, except for with placeholders. newChild = this.mixinDraggable(newChild, isDraggable); return newChild; } } exports.default = GridItem; _defineProperty(GridItem, "propTypes", { // Children must be only a single element children: _propTypes.default.element, // General grid attributes cols: _propTypes.default.number.isRequired, containerWidth: _propTypes.default.number.isRequired, rowHeight: _propTypes.default.number.isRequired, margin: _propTypes.default.array.isRequired, maxRows: _propTypes.default.number.isRequired, containerPadding: _propTypes.default.array.isRequired, // These are all in grid units x: _propTypes.default.number.isRequired, y: _propTypes.default.number.isRequired, w: _propTypes.default.number.isRequired, h: _propTypes.default.number.isRequired, // All optional minW: function (props /*: Props*/, propName /*: string*/) { const value = props[propName]; if (typeof value !== "number") return new Error("minWidth not Number"); if (value > props.w || value > props.maxW) return new Error("minWidth larger than item width/maxWidth"); }, maxW: function (props /*: Props*/, propName /*: string*/) { const value = props[propName]; if (typeof value !== "number") return new Error("maxWidth not Number"); if (value < props.w || value < props.minW) return new Error("maxWidth smaller than item width/minWidth"); }, minH: function (props /*: Props*/, propName /*: string*/) { const value = props[propName]; if (typeof value !== "number") return new Error("minHeight not Number"); if (value > props.h || value > props.maxH) return new Error("minHeight larger than item height/maxHeight"); }, maxH: function (props /*: Props*/, propName /*: string*/) { const value = props[propName]; if (typeof value !== "number") return new Error("maxHeight not Number"); if (value < props.h || value < props.minH) return new Error("maxHeight smaller than item height/minHeight"); }, // ID is nice to have for callbacks i: _propTypes.default.string.isRequired, // Resize handle options resizeHandles: _ReactGridLayoutPropTypes.resizeHandleAxesType, resizeHandle: _ReactGridLayoutPropTypes.resizeHandleType, // Functions onDragStop: _propTypes.default.func, onDragStart: _propTypes.default.func, onDrag: _propTypes.default.func, onResizeStop: _propTypes.default.func, onResizeStart: _propTypes.default.func, onResize: _propTypes.default.func, // Flags isDraggable: _propTypes.default.bool.isRequired, isResizable: _propTypes.default.bool.isRequired, isBounded: _propTypes.default.bool.isRequired, static: _propTypes.default.bool, // Use CSS transforms instead of top/left useCSSTransforms: _propTypes.default.bool.isRequired, transformScale: _propTypes.default.number, // Others className: _propTypes.default.string, // Selector for draggable handle handle: _propTypes.default.string, // Selector for draggable cancel (see react-draggable) cancel: _propTypes.default.string, // Current position of a dropping element droppingPosition: _propTypes.default.shape({ e: _propTypes.default.object.isRequired, left: _propTypes.default.number.isRequired, top: _propTypes.default.number.isRequired }) }); _defineProperty(GridItem, "defaultProps", { className: "", cancel: "", handle: "", minH: 1, minW: 1, maxH: Infinity, maxW: Infinity, transformScale: 1 });