UNPKG

react-grid-layout

Version:

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

875 lines (737 loc) 23.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.bottom = bottom; exports.cloneLayout = cloneLayout; exports.modifyLayout = modifyLayout; exports.withLayoutItem = withLayoutItem; exports.cloneLayoutItem = cloneLayoutItem; exports.childrenEqual = childrenEqual; exports.fastPositionEqual = fastPositionEqual; exports.collides = collides; exports.compact = compact; exports.compactItem = compactItem; exports.correctBounds = correctBounds; exports.getLayoutItem = getLayoutItem; exports.getFirstCollision = getFirstCollision; exports.getAllCollisions = getAllCollisions; exports.getStatics = getStatics; exports.moveElement = moveElement; exports.moveElementAwayFromCollision = moveElementAwayFromCollision; exports.perc = perc; exports.setTransform = setTransform; exports.setTopLeft = setTopLeft; exports.sortLayoutItems = sortLayoutItems; exports.sortLayoutItemsByRowCol = sortLayoutItemsByRowCol; exports.sortLayoutItemsByColRow = sortLayoutItemsByColRow; exports.synchronizeLayoutWithChildren = synchronizeLayoutWithChildren; exports.validateLayout = validateLayout; exports.compactType = compactType; exports.autoBindHandlers = autoBindHandlers; exports.noop = exports.fastRGLPropsEqual = void 0; var _lodash = _interopRequireDefault(require("lodash.isequal")); var _react = _interopRequireDefault(require("react")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var isProduction = process.env.NODE_ENV === "production"; var DEBUG = false; /** * Return the bottom coordinate of the layout. * * @param {Array} layout Layout array. * @return {Number} Bottom coordinate. */ function bottom(layout /*: Layout*/ ) /*: number*/ { var max = 0, bottomY; for (var i = 0, len = layout.length; i < len; i++) { bottomY = layout[i].y + layout[i].h; if (bottomY > max) max = bottomY; } return max; } function cloneLayout(layout /*: Layout*/ ) /*: Layout*/ { var newLayout = Array(layout.length); for (var i = 0, len = layout.length; i < len; i++) { newLayout[i] = cloneLayoutItem(layout[i]); } return newLayout; } // Modify a layoutItem inside a layout. Returns a new Layout, // does not mutate. Carries over all other LayoutItems unmodified. function modifyLayout(layout /*: Layout*/ , layoutItem /*: LayoutItem*/ ) /*: Layout*/ { var newLayout = Array(layout.length); for (var i = 0, len = layout.length; i < len; i++) { if (layoutItem.i === layout[i].i) { newLayout[i] = layoutItem; } else { newLayout[i] = layout[i]; } } return newLayout; } // Function to be called to modify a layout item. // Does defensive clones to ensure the layout is not modified. function withLayoutItem(layout /*: Layout*/ , itemKey /*: string*/ , cb /*: LayoutItem => LayoutItem*/ ) /*: [Layout, ?LayoutItem]*/ { var item = getLayoutItem(layout, itemKey); if (!item) return [layout, null]; item = cb(cloneLayoutItem(item)); // defensive clone then modify // FIXME could do this faster if we already knew the index layout = modifyLayout(layout, item); return [layout, item]; } // Fast path to cloning, since this is monomorphic function cloneLayoutItem(layoutItem /*: LayoutItem*/ ) /*: LayoutItem*/ { return { w: layoutItem.w, h: layoutItem.h, x: layoutItem.x, y: layoutItem.y, i: layoutItem.i, minW: layoutItem.minW, maxW: layoutItem.maxW, minH: layoutItem.minH, maxH: layoutItem.maxH, moved: Boolean(layoutItem.moved), static: Boolean(layoutItem.static), // These can be null/undefined isDraggable: layoutItem.isDraggable, isResizable: layoutItem.isResizable, resizeHandles: layoutItem.resizeHandles, isBounded: layoutItem.isBounded }; } /** * Comparing React `children` is a bit difficult. This is a good way to compare them. * This will catch differences in keys, order, and length. */ function childrenEqual(a /*: ReactChildren*/ , b /*: ReactChildren*/ ) /*: boolean*/ { return (0, _lodash.default)(_react.default.Children.map(a, function (c) { return c.key; }), _react.default.Children.map(b, function (c) { return c.key; })); } /** * See `fastRGLPropsEqual.js`. * We want this to run as fast as possible - it is called often - and to be * resilient to new props that we add. So rather than call lodash.isEqual, * which isn't suited to comparing props very well, we use this specialized * function in conjunction with preval to generate the fastest possible comparison * function, tuned for exactly our props. */ /*:: type FastRGLPropsEqual = (Object, Object, Function) => boolean;*/ var fastRGLPropsEqual /*: FastRGLPropsEqual*/ = require("./fastRGLPropsEqual"); // Like the above, but a lot simpler. exports.fastRGLPropsEqual = fastRGLPropsEqual; function fastPositionEqual(a /*: Position*/ , b /*: Position*/ ) /*: boolean*/ { return a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height; } /** * Given two layoutitems, check if they collide. */ function collides(l1 /*: LayoutItem*/ , l2 /*: LayoutItem*/ ) /*: boolean*/ { if (l1.i === l2.i) return false; // same element if (l1.x + l1.w <= l2.x) return false; // l1 is left of l2 if (l1.x >= l2.x + l2.w) return false; // l1 is right of l2 if (l1.y + l1.h <= l2.y) return false; // l1 is above l2 if (l1.y >= l2.y + l2.h) return false; // l1 is below l2 return true; // boxes overlap } /** * Given a layout, compact it. This involves going down each y coordinate and removing gaps * between items. * * Does not modify layout items (clones). Creates a new layout array. * * @param {Array} layout Layout. * @param {Boolean} verticalCompact Whether or not to compact the layout * vertically. * @return {Array} Compacted Layout. */ function compact(layout /*: Layout*/ , compactType /*: CompactType*/ , cols /*: number*/ ) /*: Layout*/ { // Statics go in the compareWith array right away so items flow around them. var compareWith = getStatics(layout); // We go through the items by row and column. var sorted = sortLayoutItems(layout, compactType); // Holding for new items. var out = Array(layout.length); for (var i = 0, len = sorted.length; i < len; i++) { var l = cloneLayoutItem(sorted[i]); // Don't move static elements if (!l.static) { l = compactItem(compareWith, l, compactType, cols, sorted); // Add to comparison array. We only collide with items before this one. // Statics are already in this array. compareWith.push(l); } // Add to output array to make sure they still come out in the right order. out[layout.indexOf(sorted[i])] = l; // Clear moved flag, if it exists. l.moved = false; } return out; } var heightWidth = { x: "w", y: "h" }; /** * Before moving item down, it will check if the movement will cause collisions and move those items down before. */ function resolveCompactionCollision(layout /*: Layout*/ , item /*: LayoutItem*/ , moveToCoord /*: number*/ , axis /*: "x" | "y"*/ ) { var sizeProp = heightWidth[axis]; item[axis] += 1; var itemIndex = layout.map(function (layoutItem) { return layoutItem.i; }).indexOf(item.i); // Go through each item we collide with. for (var i = itemIndex + 1; i < layout.length; i++) { var otherItem = layout[i]; // Ignore static items if (otherItem.static) continue; // Optimization: we can break early if we know we're past this el // We can do this b/c it's a sorted layout if (otherItem.y > item.y + item.h) break; if (collides(item, otherItem)) { resolveCompactionCollision(layout, otherItem, moveToCoord + item[sizeProp], axis); } } item[axis] = moveToCoord; } /** * Compact an item in the layout. * * Modifies item. * */ function compactItem(compareWith /*: Layout*/ , l /*: LayoutItem*/ , compactType /*: CompactType*/ , cols /*: number*/ , fullLayout /*: Layout*/ ) /*: LayoutItem*/ { var compactV = compactType === "vertical"; var compactH = compactType === "horizontal"; if (compactV) { // Bottom 'y' possible is the bottom of the layout. // This allows you to do nice stuff like specify {y: Infinity} // This is here because the layout must be sorted in order to get the correct bottom `y`. l.y = Math.min(bottom(compareWith), l.y); // Move the element up as far as it can go without colliding. while (l.y > 0 && !getFirstCollision(compareWith, l)) { l.y--; } } else if (compactH) { // Move the element left as far as it can go without colliding. while (l.x > 0 && !getFirstCollision(compareWith, l)) { l.x--; } } // Move it down, and keep moving it down if it's colliding. var collides; while (collides = getFirstCollision(compareWith, l)) { if (compactH) { resolveCompactionCollision(fullLayout, l, collides.x + collides.w, "x"); } else { resolveCompactionCollision(fullLayout, l, collides.y + collides.h, "y"); } // Since we can't grow without bounds horizontally, if we've overflown, let's move it down and try again. if (compactH && l.x + l.w > cols) { l.x = cols - l.w; l.y++; } } return l; } /** * Given a layout, make sure all elements fit within its bounds. * * Modifies layout items. * * @param {Array} layout Layout array. * @param {Number} bounds Number of columns. */ function correctBounds(layout /*: Layout*/ , bounds /*: { cols: number }*/ ) /*: Layout*/ { var collidesWith = getStatics(layout); for (var i = 0, len = layout.length; i < len; i++) { var l = layout[i]; // Overflows right if (l.x + l.w > bounds.cols) l.x = bounds.cols - l.w; // Overflows left if (l.x < 0) { l.x = 0; l.w = bounds.cols; } if (!l.static) collidesWith.push(l);else { // If this is static and collides with other statics, we must move it down. // We have to do something nicer than just letting them overlap. while (getFirstCollision(collidesWith, l)) { l.y++; } } } return layout; } /** * Get a layout item by ID. Used so we can override later on if necessary. * * @param {Array} layout Layout array. * @param {String} id ID * @return {LayoutItem} Item at ID. */ function getLayoutItem(layout /*: Layout*/ , id /*: string*/ ) /*: ?LayoutItem*/ { for (var i = 0, len = layout.length; i < len; i++) { if (layout[i].i === id) return layout[i]; } } /** * Returns the first item this layout collides with. * It doesn't appear to matter which order we approach this from, although * perhaps that is the wrong thing to do. * * @param {Object} layoutItem Layout item. * @return {Object|undefined} A colliding layout item, or undefined. */ function getFirstCollision(layout /*: Layout*/ , layoutItem /*: LayoutItem*/ ) /*: ?LayoutItem*/ { for (var i = 0, len = layout.length; i < len; i++) { if (collides(layout[i], layoutItem)) return layout[i]; } } function getAllCollisions(layout /*: Layout*/ , layoutItem /*: LayoutItem*/ ) /*: Array<LayoutItem>*/ { return layout.filter(function (l) { return collides(l, layoutItem); }); } /** * Get all static elements. * @param {Array} layout Array of layout objects. * @return {Array} Array of static layout items.. */ function getStatics(layout /*: Layout*/ ) /*: Array<LayoutItem>*/ { return layout.filter(function (l) { return l.static; }); } /** * Move an element. Responsible for doing cascading movements of other elements. * * Modifies layout items. * * @param {Array} layout Full layout to modify. * @param {LayoutItem} l element to move. * @param {Number} [x] X position in grid units. * @param {Number} [y] Y position in grid units. */ function moveElement(layout /*: Layout*/ , l /*: LayoutItem*/ , x /*: ?number*/ , y /*: ?number*/ , isUserAction /*: ?boolean*/ , preventCollision /*: ?boolean*/ , compactType /*: CompactType*/ , cols /*: number*/ ) /*: Layout*/ { // If this is static and not explicitly enabled as draggable, // no move is possible, so we can short-circuit this immediately. if (l.static && l.isDraggable !== true) return layout; // Short-circuit if nothing to do. if (l.y === y && l.x === x) return layout; log("Moving element ".concat(l.i, " to [").concat(String(x), ",").concat(String(y), "] from [").concat(l.x, ",").concat(l.y, "]")); var oldX = l.x; var oldY = l.y; // This is quite a bit faster than extending the object if (typeof x === "number") l.x = x; if (typeof y === "number") l.y = y; l.moved = true; // If this collides with anything, move it. // When doing this comparison, we have to sort the items we compare with // to ensure, in the case of multiple collisions, that we're getting the // nearest collision. var sorted = sortLayoutItems(layout, compactType); var movingUp = compactType === "vertical" && typeof y === "number" ? oldY >= y : compactType === "horizontal" && typeof x === "number" ? oldX >= x : false; // $FlowIgnore acceptable modification of read-only array as it was recently cloned if (movingUp) sorted = sorted.reverse(); var collisions = getAllCollisions(sorted, l); // There was a collision; abort if (preventCollision && collisions.length) { log("Collision prevented on ".concat(l.i, ", reverting.")); l.x = oldX; l.y = oldY; l.moved = false; return layout; } // Move each item that collides away from this element. for (var i = 0, len = collisions.length; i < len; i++) { var collision = collisions[i]; log("Resolving collision between ".concat(l.i, " at [").concat(l.x, ",").concat(l.y, "] and ").concat(collision.i, " at [").concat(collision.x, ",").concat(collision.y, "]")); // Short circuit so we can't infinite loop if (collision.moved) continue; // Don't move static items - we have to move *this* element away if (collision.static) { layout = moveElementAwayFromCollision(layout, collision, l, isUserAction, compactType, cols); } else { layout = moveElementAwayFromCollision(layout, l, collision, isUserAction, compactType, cols); } } return layout; } /** * This is where the magic needs to happen - given a collision, move an element away from the collision. * We attempt to move it up if there's room, otherwise it goes below. * * @param {Array} layout Full layout to modify. * @param {LayoutItem} collidesWith Layout item we're colliding with. * @param {LayoutItem} itemToMove Layout item we're moving. */ function moveElementAwayFromCollision(layout /*: Layout*/ , collidesWith /*: LayoutItem*/ , itemToMove /*: LayoutItem*/ , isUserAction /*: ?boolean*/ , compactType /*: CompactType*/ , cols /*: number*/ ) /*: Layout*/ { var compactH = compactType === "horizontal"; // Compact vertically if not set to horizontal var compactV = compactType !== "horizontal"; var preventCollision = collidesWith.static; // we're already colliding (not for static items) // If there is enough space above the collision to put this element, move it there. // We only do this on the main collision as this can get funky in cascades and cause // unwanted swapping behavior. if (isUserAction) { // Reset isUserAction flag because we're not in the main collision anymore. isUserAction = false; // Make a mock item so we don't modify the item here, only modify in moveElement. var fakeItem /*: LayoutItem*/ = { x: compactH ? Math.max(collidesWith.x - itemToMove.w, 0) : itemToMove.x, y: compactV ? Math.max(collidesWith.y - itemToMove.h, 0) : itemToMove.y, w: itemToMove.w, h: itemToMove.h, i: "-1" }; // No collision? If so, we can go up there; otherwise, we'll end up moving down as normal if (!getFirstCollision(layout, fakeItem)) { log("Doing reverse collision on ".concat(itemToMove.i, " up to [").concat(fakeItem.x, ",").concat(fakeItem.y, "].")); return moveElement(layout, itemToMove, compactH ? fakeItem.x : undefined, compactV ? fakeItem.y : undefined, isUserAction, preventCollision, compactType, cols); } } return moveElement(layout, itemToMove, compactH ? itemToMove.x + 1 : undefined, compactV ? itemToMove.y + 1 : undefined, isUserAction, preventCollision, compactType, cols); } /** * Helper to convert a number to a percentage string. * * @param {Number} num Any number * @return {String} That number as a percentage. */ function perc(num /*: number*/ ) /*: string*/ { return num * 100 + "%"; } function setTransform(_ref) /*: Object*/ { var top = _ref.top, left = _ref.left, width = _ref.width, height = _ref.height; // Replace unitless items with px var translate = "translate(".concat(left, "px,").concat(top, "px)"); return { transform: translate, WebkitTransform: translate, MozTransform: translate, msTransform: translate, OTransform: translate, width: "".concat(width, "px"), height: "".concat(height, "px"), position: "absolute" }; } function setTopLeft(_ref2) /*: Object*/ { var top = _ref2.top, left = _ref2.left, width = _ref2.width, height = _ref2.height; return { top: "".concat(top, "px"), left: "".concat(left, "px"), width: "".concat(width, "px"), height: "".concat(height, "px"), position: "absolute" }; } /** * Get layout items sorted from top left to right and down. * * @return {Array} Array of layout objects. * @return {Array} Layout, sorted static items first. */ function sortLayoutItems(layout /*: Layout*/ , compactType /*: CompactType*/ ) /*: Layout*/ { if (compactType === "horizontal") return sortLayoutItemsByColRow(layout);else return sortLayoutItemsByRowCol(layout); } /** * Sort layout items by row ascending and column ascending. * * Does not modify Layout. */ function sortLayoutItemsByRowCol(layout /*: Layout*/ ) /*: Layout*/ { // Slice to clone array as sort modifies return layout.slice(0).sort(function (a, b) { if (a.y > b.y || a.y === b.y && a.x > b.x) { return 1; } else if (a.y === b.y && a.x === b.x) { // Without this, we can get different sort results in IE vs. Chrome/FF return 0; } return -1; }); } /** * Sort layout items by column ascending then row ascending. * * Does not modify Layout. */ function sortLayoutItemsByColRow(layout /*: Layout*/ ) /*: Layout*/ { return layout.slice(0).sort(function (a, b) { if (a.x > b.x || a.x === b.x && a.y > b.y) { return 1; } return -1; }); } /** * Generate a layout using the initialLayout and children as a template. * Missing entries will be added, extraneous ones will be truncated. * * Does not modify initialLayout. * * @param {Array} initialLayout Layout passed in through props. * @param {String} breakpoint Current responsive breakpoint. * @param {?String} compact Compaction option. * @return {Array} Working layout. */ function synchronizeLayoutWithChildren(initialLayout /*: Layout*/ , children /*: ReactChildren*/ , cols /*: number*/ , compactType /*: CompactType*/ ) /*: Layout*/ { initialLayout = initialLayout || []; // Generate one layout item per child. var layout /*: LayoutItem[]*/ = []; _react.default.Children.forEach(children, function (child /*: ReactElement<any>*/ , i /*: number*/ ) { // Don't overwrite if it already exists. var exists = getLayoutItem(initialLayout, String(child.key)); if (exists) { layout[i] = cloneLayoutItem(exists); } else { if (!isProduction && child.props._grid) { console.warn("`_grid` properties on children have been deprecated as of React 15.2. " + // eslint-disable-line "Please use `data-grid` or add your properties directly to the `layout`."); } var g = child.props["data-grid"] || child.props._grid; // Hey, this item has a data-grid property, use it. if (g) { if (!isProduction) { validateLayout([g], "ReactGridLayout.children"); } // FIXME clone not really necessary here layout[i] = cloneLayoutItem(_objectSpread(_objectSpread({}, g), {}, { i: child.key })); } else { // Nothing provided: ensure this is added to the bottom // FIXME clone not really necessary here layout[i] = cloneLayoutItem({ w: 1, h: 1, x: 0, y: bottom(layout), i: String(child.key) }); } } }); // Correct the layout. var correctedLayout = correctBounds(layout, { cols: cols }); return compact(correctedLayout, compactType, cols); } /** * Validate a layout. Throws errors. * * @param {Array} layout Array of layout items. * @param {String} [contextName] Context name for errors. * @throw {Error} Validation error. */ function validateLayout(layout /*: Layout*/ ) /*: void*/ { var contextName /*: string*/ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "Layout"; var subProps = ["x", "y", "w", "h"]; if (!Array.isArray(layout)) throw new Error(contextName + " must be an array!"); for (var i = 0, len = layout.length; i < len; i++) { var item = layout[i]; for (var j = 0; j < subProps.length; j++) { if (typeof item[subProps[j]] !== "number") { throw new Error("ReactGridLayout: " + contextName + "[" + i + "]." + subProps[j] + " must be a number!"); } } if (item.i && typeof item.i !== "string") { throw new Error("ReactGridLayout: " + contextName + "[" + i + "].i must be a string!"); } if (item.static !== undefined && typeof item.static !== "boolean") { throw new Error("ReactGridLayout: " + contextName + "[" + i + "].static must be a boolean!"); } } } // Legacy support for verticalCompact: false function compactType(props /*: ?{ verticalCompact: boolean, compactType: CompactType }*/ ) /*: CompactType*/ { var _ref3 = props || {}, verticalCompact = _ref3.verticalCompact, compactType = _ref3.compactType; return verticalCompact === false ? null : compactType; } // Flow can't really figure this out, so we just use Object function autoBindHandlers(el /*: Object*/ , fns /*: Array<string>*/ ) /*: void*/ { fns.forEach(function (key) { return el[key] = el[key].bind(el); }); } function log() { var _console; if (!DEBUG) return; // eslint-disable-next-line no-console (_console = console).log.apply(_console, arguments); } var noop = function noop() {}; exports.noop = noop;