Added logging, changed some directory structure

This commit is contained in:
2018-01-13 21:33:40 -05:00
parent f079a5f067
commit 8e72ffb917
73656 changed files with 35284 additions and 53718 deletions

View File

@@ -0,0 +1,502 @@
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactDraggable = require('react-draggable');
var _reactResizable = require('react-resizable');
var _utils = require('./utils');
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/**
* An individual item within a ReactGridLayout.
*/
var GridItem = function (_React$Component) {
_inherits(GridItem, _React$Component);
function GridItem() {
var _temp, _this, _ret;
_classCallCheck(this, GridItem);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
resizing: null,
dragging: null,
className: ''
}, _temp), _possibleConstructorReturn(_this, _ret);
}
// Helper for generating column width
GridItem.prototype.calcColWidth = function calcColWidth() {
var _props = this.props,
margin = _props.margin,
containerPadding = _props.containerPadding,
containerWidth = _props.containerWidth,
cols = _props.cols;
return (containerWidth - margin[0] * (cols - 1) - containerPadding[0] * 2) / cols;
};
/**
* Return position on the page given an x, y, w, h.
* left, top, width, height are all in pixels.
* @param {Number} x X coordinate in grid units.
* @param {Number} y Y coordinate in grid units.
* @param {Number} w W coordinate in grid units.
* @param {Number} h H coordinate in grid units.
* @return {Object} Object containing coords.
*/
GridItem.prototype.calcPosition = function calcPosition(x, y, w, h, state) {
var _props2 = this.props,
margin = _props2.margin,
containerPadding = _props2.containerPadding,
rowHeight = _props2.rowHeight;
var colWidth = this.calcColWidth();
var out = {
left: Math.round((colWidth + margin[0]) * x + containerPadding[0]),
top: Math.round((rowHeight + margin[1]) * y + containerPadding[1]),
// 0 * Infinity === NaN, which causes problems with resize constraints;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * margin[0]),
height: h === Infinity ? h : Math.round(rowHeight * h + Math.max(0, h - 1) * margin[1])
};
if (state && state.resizing) {
out.width = Math.round(state.resizing.width);
out.height = Math.round(state.resizing.height);
}
if (state && state.dragging) {
out.top = Math.round(state.dragging.top);
out.left = Math.round(state.dragging.left);
}
return out;
};
/**
* Translate x and y coordinates from pixels to grid units.
* @param {Number} top Top position (relative to parent) in pixels.
* @param {Number} left Left position (relative to parent) in pixels.
* @return {Object} x and y in grid units.
*/
GridItem.prototype.calcXY = function calcXY(top, left) {
var _props3 = this.props,
margin = _props3.margin,
cols = _props3.cols,
rowHeight = _props3.rowHeight,
w = _props3.w,
h = _props3.h,
maxRows = _props3.maxRows;
var colWidth = this.calcColWidth();
// left = colWidth * x + margin * (x + 1)
// l = cx + m(x+1)
// l = cx + mx + m
// l - m = cx + mx
// l - m = x(c + m)
// (l - m) / (c + m) = x
// x = (left - margin) / (coldWidth + margin)
var x = Math.round((left - margin[0]) / (colWidth + margin[0]));
var y = Math.round((top - margin[1]) / (rowHeight + margin[1]));
// Capping
x = Math.max(Math.min(x, cols - w), 0);
y = Math.max(Math.min(y, maxRows - h), 0);
return { x: x, y: y };
};
/**
* Given a height and width in pixel values, calculate grid units.
* @param {Number} height Height in pixels.
* @param {Number} width Width in pixels.
* @return {Object} w, h as grid units.
*/
GridItem.prototype.calcWH = function calcWH(_ref) {
var height = _ref.height,
width = _ref.width;
var _props4 = this.props,
margin = _props4.margin,
maxRows = _props4.maxRows,
cols = _props4.cols,
rowHeight = _props4.rowHeight,
x = _props4.x,
y = _props4.y;
var colWidth = this.calcColWidth();
// width = colWidth * w - (margin * (w - 1))
// ...
// w = (width + margin) / (colWidth + margin)
var w = Math.round((width + margin[0]) / (colWidth + margin[0]));
var h = Math.round((height + margin[1]) / (rowHeight + margin[1]));
// Capping
w = Math.max(Math.min(w, cols - x), 0);
h = Math.max(Math.min(h, maxRows - y), 0);
return { w: w, h: h };
};
/**
* 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.
*/
GridItem.prototype.createStyle = function createStyle(pos) {
var _props5 = this.props,
usePercentages = _props5.usePercentages,
containerWidth = _props5.containerWidth,
useCSSTransforms = _props5.useCSSTransforms;
var style = void 0;
// CSS Transforms support (default)
if (useCSSTransforms) {
style = (0, _utils.setTransform)(pos);
}
// top,left (slow)
else {
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.
*/
GridItem.prototype.mixinDraggable = function mixinDraggable(child) {
return _react2.default.createElement(
_reactDraggable.DraggableCore,
{
onStart: this.onDragHandler('onDragStart'),
onDrag: this.onDragHandler('onDrag'),
onStop: this.onDragHandler('onDragStop'),
handle: this.props.handle,
cancel: ".react-resizable-handle" + (this.props.cancel ? "," + this.props.cancel : "") },
child
);
};
/**
* 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.
*/
GridItem.prototype.mixinResizable = function mixinResizable(child, position) {
var _props6 = this.props,
cols = _props6.cols,
x = _props6.x,
minW = _props6.minW,
minH = _props6.minH,
maxW = _props6.maxW,
maxH = _props6.maxH;
// This is the max possible width - doesn't go to infinity because of the width of the window
var maxWidth = this.calcPosition(0, 0, cols - x, 0).width;
// Calculate min/max constraints using our min & maxes
var mins = this.calcPosition(0, 0, minW, minH);
var maxes = this.calcPosition(0, 0, maxW, maxH);
var minConstraints = [mins.width, mins.height];
var maxConstraints = [Math.min(maxes.width, maxWidth), Math.min(maxes.height, Infinity)];
return _react2.default.createElement(
_reactResizable.Resizable,
{
width: position.width,
height: position.height,
minConstraints: minConstraints,
maxConstraints: maxConstraints,
onResizeStop: this.onResizeHandler('onResizeStop'),
onResizeStart: this.onResizeHandler('onResizeStart'),
onResize: this.onResizeHandler('onResize') },
child
);
};
/**
* Wrapper around drag events to provide more useful data.
* All drag events call the function with the given handler name,
* with the signature (index, x, y).
*
* @param {String} handlerName Handler name to wrap.
* @return {Function} Handler function.
*/
GridItem.prototype.onDragHandler = function onDragHandler(handlerName) {
var _this2 = this;
return function (e, _ref2) {
var node = _ref2.node,
deltaX = _ref2.deltaX,
deltaY = _ref2.deltaY;
var handler = _this2.props[handlerName];
if (!handler) return;
var newPosition = { top: 0, left: 0 };
// Get new XY
switch (handlerName) {
case 'onDragStart':
{
// ToDo this wont work on nested parents
var parentRect = node.offsetParent.getBoundingClientRect();
var clientRect = node.getBoundingClientRect();
newPosition.left = clientRect.left - parentRect.left + node.offsetParent.scrollLeft;
newPosition.top = clientRect.top - parentRect.top + node.offsetParent.scrollTop;
_this2.setState({ dragging: newPosition });
break;
}
case 'onDrag':
if (!_this2.state.dragging) throw new Error('onDrag called before onDragStart.');
newPosition.left = _this2.state.dragging.left + deltaX;
newPosition.top = _this2.state.dragging.top + deltaY;
_this2.setState({ dragging: newPosition });
break;
case 'onDragStop':
if (!_this2.state.dragging) throw new Error('onDragEnd called before onDragStart.');
newPosition.left = _this2.state.dragging.left;
newPosition.top = _this2.state.dragging.top;
_this2.setState({ dragging: null });
break;
default:
throw new Error('onDragHandler called with unrecognized handlerName: ' + handlerName);
}
var _calcXY = _this2.calcXY(newPosition.top, newPosition.left),
x = _calcXY.x,
y = _calcXY.y;
handler.call(_this2, _this2.props.i, x, y, { e: e, node: node, newPosition: newPosition });
};
};
/**
* Wrapper around drag events to provide more useful data.
* All drag events call the function with the given handler name,
* with the signature (index, x, y).
*
* @param {String} handlerName Handler name to wrap.
* @return {Function} Handler function.
*/
GridItem.prototype.onResizeHandler = function onResizeHandler(handlerName) {
var _this3 = this;
return function (e, _ref3) {
var node = _ref3.node,
size = _ref3.size;
var handler = _this3.props[handlerName];
if (!handler) return;
var _props7 = _this3.props,
cols = _props7.cols,
x = _props7.x,
i = _props7.i,
maxW = _props7.maxW,
minW = _props7.minW,
maxH = _props7.maxH,
minH = _props7.minH;
// Get new XY
var _calcWH = _this3.calcWH(size),
w = _calcWH.w,
h = _calcWH.h;
// Cap w at numCols
w = Math.min(w, cols - x);
// Ensure w is at least 1
w = Math.max(w, 1);
// Min/max capping
w = Math.max(Math.min(w, maxW), minW);
h = Math.max(Math.min(h, maxH), minH);
_this3.setState({ resizing: handlerName === 'onResizeStop' ? null : size });
handler.call(_this3, i, w, h, { e: e, node: node, size: size });
};
};
GridItem.prototype.render = function render() {
var _props8 = this.props,
x = _props8.x,
y = _props8.y,
w = _props8.w,
h = _props8.h,
isDraggable = _props8.isDraggable,
isResizable = _props8.isResizable,
useCSSTransforms = _props8.useCSSTransforms;
var pos = this.calcPosition(x, y, w, h, this.state);
var child = _react2.default.Children.only(this.props.children);
// Create the child element. We clone the existing element but modify its className and style.
var newChild = _react2.default.cloneElement(child, {
className: (0, _classnames2.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),
cssTransforms: useCSSTransforms
}),
// We can set the width and height on the child, but unfortunately we can't set the position.
style: _extends({}, this.props.style, child.props.style, this.createStyle(pos))
});
// Resizable support. This is usually on but the user can toggle it off.
if (isResizable) newChild = this.mixinResizable(newChild, pos);
// Draggable support. This is always on, except for with placeholders.
if (isDraggable) newChild = this.mixinDraggable(newChild);
return newChild;
};
return GridItem;
}(_react2.default.Component);
GridItem.propTypes = {
// Children must be only a single element
children: _propTypes2.default.element,
// General grid attributes
cols: _propTypes2.default.number.isRequired,
containerWidth: _propTypes2.default.number.isRequired,
rowHeight: _propTypes2.default.number.isRequired,
margin: _propTypes2.default.array.isRequired,
maxRows: _propTypes2.default.number.isRequired,
containerPadding: _propTypes2.default.array.isRequired,
// These are all in grid units
x: _propTypes2.default.number.isRequired,
y: _propTypes2.default.number.isRequired,
w: _propTypes2.default.number.isRequired,
h: _propTypes2.default.number.isRequired,
// All optional
minW: function minW(props, propName) {
var 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 maxW(props, propName) {
var 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 minH(props, propName) {
var 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 maxH(props, propName) {
var 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: _propTypes2.default.string.isRequired,
// Functions
onDragStop: _propTypes2.default.func,
onDragStart: _propTypes2.default.func,
onDrag: _propTypes2.default.func,
onResizeStop: _propTypes2.default.func,
onResizeStart: _propTypes2.default.func,
onResize: _propTypes2.default.func,
// Flags
isDraggable: _propTypes2.default.bool.isRequired,
isResizable: _propTypes2.default.bool.isRequired,
static: _propTypes2.default.bool,
// Use CSS transforms instead of top/left
useCSSTransforms: _propTypes2.default.bool.isRequired,
// Others
className: _propTypes2.default.string,
// Selector for draggable handle
handle: _propTypes2.default.string,
// Selector for draggable cancel (see react-draggable)
cancel: _propTypes2.default.string
};
GridItem.defaultProps = {
className: '',
cancel: '',
handle: '',
minH: 1,
minW: 1,
maxH: Infinity,
maxW: Infinity
};
exports.default = GridItem;

View File

@@ -0,0 +1,430 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import {DraggableCore} from 'react-draggable';
import {Resizable} from 'react-resizable';
import {perc, setTopLeft, setTransform} from './utils';
import classNames from 'classnames';
import type {Element as ReactElement, Node as ReactNode} from 'react';
import type {ReactDraggableCallbackData, GridDragEvent, GridResizeEvent, Position} from './utils';
type PartialPosition = {top: number, left: number};
type GridItemCallback<Data: GridDragEvent | GridResizeEvent> = (i: string, w: number, h: number, Data) => void;
type State = {
resizing: ?{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,
static?: boolean,
useCSSTransforms?: boolean,
usePercentages?: boolean,
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,
onDrag?: GridItemCallback<GridDragEvent>,
onDragStart?: GridItemCallback<GridDragEvent>,
onDragStop?: GridItemCallback<GridDragEvent>,
onResize?: GridItemCallback<GridResizeEvent>,
onResizeStart?: GridItemCallback<GridResizeEvent>,
onResizeStop?: GridItemCallback<GridResizeEvent>,
};
/**
* An individual item within a ReactGridLayout.
*/
export default class GridItem extends React.Component<Props, State> {
static propTypes = {
// Children must be only a single element
children: PropTypes.element,
// General grid attributes
cols: PropTypes.number.isRequired,
containerWidth: PropTypes.number.isRequired,
rowHeight: PropTypes.number.isRequired,
margin: PropTypes.array.isRequired,
maxRows: PropTypes.number.isRequired,
containerPadding: PropTypes.array.isRequired,
// These are all in grid units
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
w: PropTypes.number.isRequired,
h: PropTypes.number.isRequired,
// All optional
minW: function (props, propName) {
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, propName) {
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, propName) {
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, propName) {
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.string.isRequired,
// Functions
onDragStop: PropTypes.func,
onDragStart: PropTypes.func,
onDrag: PropTypes.func,
onResizeStop: PropTypes.func,
onResizeStart: PropTypes.func,
onResize: PropTypes.func,
// Flags
isDraggable: PropTypes.bool.isRequired,
isResizable: PropTypes.bool.isRequired,
static: PropTypes.bool,
// Use CSS transforms instead of top/left
useCSSTransforms: PropTypes.bool.isRequired,
// Others
className: PropTypes.string,
// Selector for draggable handle
handle: PropTypes.string,
// Selector for draggable cancel (see react-draggable)
cancel: PropTypes.string
};
static defaultProps = {
className: '',
cancel: '',
handle: '',
minH: 1,
minW: 1,
maxH: Infinity,
maxW: Infinity,
};
state: State = {
resizing: null,
dragging: null,
className: ''
};
// Helper for generating column width
calcColWidth(): number {
const {margin, containerPadding, containerWidth, cols} = this.props;
return (containerWidth - (margin[0] * (cols - 1)) - (containerPadding[0] * 2)) / cols;
}
/**
* Return position on the page given an x, y, w, h.
* left, top, width, height are all in pixels.
* @param {Number} x X coordinate in grid units.
* @param {Number} y Y coordinate in grid units.
* @param {Number} w W coordinate in grid units.
* @param {Number} h H coordinate in grid units.
* @return {Object} Object containing coords.
*/
calcPosition(x: number, y: number, w: number, h: number, state: ?Object): Position {
const {margin, containerPadding, rowHeight} = this.props;
const colWidth = this.calcColWidth();
const out = {
left: Math.round((colWidth + margin[0]) * x + containerPadding[0]),
top: Math.round((rowHeight + margin[1]) * y + containerPadding[1]),
// 0 * Infinity === NaN, which causes problems with resize constraints;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
width: w === Infinity ? w : Math.round(colWidth * w + Math.max(0, w - 1) * margin[0]),
height: h === Infinity ? h : Math.round(rowHeight * h + Math.max(0, h - 1) * margin[1])
};
if (state && state.resizing) {
out.width = Math.round(state.resizing.width);
out.height = Math.round(state.resizing.height);
}
if (state && state.dragging) {
out.top = Math.round(state.dragging.top);
out.left = Math.round(state.dragging.left);
}
return out;
}
/**
* Translate x and y coordinates from pixels to grid units.
* @param {Number} top Top position (relative to parent) in pixels.
* @param {Number} left Left position (relative to parent) in pixels.
* @return {Object} x and y in grid units.
*/
calcXY(top: number, left: number): {x: number, y: number} {
const {margin, cols, rowHeight, w, h, maxRows} = this.props;
const colWidth = this.calcColWidth();
// left = colWidth * x + margin * (x + 1)
// l = cx + m(x+1)
// l = cx + mx + m
// l - m = cx + mx
// l - m = x(c + m)
// (l - m) / (c + m) = x
// x = (left - margin) / (coldWidth + margin)
let x = Math.round((left - margin[0]) / (colWidth + margin[0]));
let y = Math.round((top - margin[1]) / (rowHeight + margin[1]));
// Capping
x = Math.max(Math.min(x, cols - w), 0);
y = Math.max(Math.min(y, maxRows - h), 0);
return {x, y};
}
/**
* Given a height and width in pixel values, calculate grid units.
* @param {Number} height Height in pixels.
* @param {Number} width Width in pixels.
* @return {Object} w, h as grid units.
*/
calcWH({height, width}: {height: number, width: number}): {w: number, h: number} {
const {margin, maxRows, cols, rowHeight, x, y} = this.props;
const colWidth = this.calcColWidth();
// width = colWidth * w - (margin * (w - 1))
// ...
// w = (width + margin) / (colWidth + margin)
let w = Math.round((width + margin[0]) / (colWidth + margin[0]));
let h = Math.round((height + margin[1]) / (rowHeight + margin[1]));
// Capping
w = Math.max(Math.min(w, cols - x), 0);
h = Math.max(Math.min(h, maxRows - y), 0);
return {w, h};
}
/**
* 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 = setTransform(pos);
}
// top,left (slow)
else {
style = setTopLeft(pos);
// This is used for server rendering.
if (usePercentages) {
style.left = perc(pos.left / containerWidth);
style.width = 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>): ReactElement<any> {
return (
<DraggableCore
onStart={this.onDragHandler('onDragStart')}
onDrag={this.onDragHandler('onDrag')}
onStop={this.onDragHandler('onDragStop')}
handle={this.props.handle}
cancel={".react-resizable-handle" + (this.props.cancel ? "," + this.props.cancel : "")}>
{child}
</DraggableCore>
);
}
/**
* 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): ReactElement<any> {
const {cols, x, minW, minH, maxW, maxH} = this.props;
// This is the max possible width - doesn't go to infinity because of the width of the window
const maxWidth = this.calcPosition(0, 0, cols - x, 0).width;
// Calculate min/max constraints using our min & maxes
const mins = this.calcPosition(0, 0, minW, minH);
const maxes = this.calcPosition(0, 0, maxW, maxH);
const minConstraints = [mins.width, mins.height];
const maxConstraints = [Math.min(maxes.width, maxWidth), Math.min(maxes.height, Infinity)];
return (
<Resizable
width={position.width}
height={position.height}
minConstraints={minConstraints}
maxConstraints={maxConstraints}
onResizeStop={this.onResizeHandler('onResizeStop')}
onResizeStart={this.onResizeHandler('onResizeStart')}
onResize={this.onResizeHandler('onResize')}>
{child}
</Resizable>
);
}
/**
* Wrapper around drag events to provide more useful data.
* All drag events call the function with the given handler name,
* with the signature (index, x, y).
*
* @param {String} handlerName Handler name to wrap.
* @return {Function} Handler function.
*/
onDragHandler(handlerName:string) {
return (e:Event, {node, deltaX, deltaY}: ReactDraggableCallbackData) => {
const handler = this.props[handlerName];
if (!handler) return;
const newPosition: PartialPosition = {top: 0, left: 0};
// Get new XY
switch (handlerName) {
case 'onDragStart': {
// ToDo this wont work on nested parents
const parentRect = node.offsetParent.getBoundingClientRect();
const clientRect = node.getBoundingClientRect();
newPosition.left = clientRect.left - parentRect.left + node.offsetParent.scrollLeft;
newPosition.top = clientRect.top - parentRect.top + node.offsetParent.scrollTop;
this.setState({dragging: newPosition});
break;
}
case 'onDrag':
if (!this.state.dragging) throw new Error('onDrag called before onDragStart.');
newPosition.left = this.state.dragging.left + deltaX;
newPosition.top = this.state.dragging.top + deltaY;
this.setState({dragging: newPosition});
break;
case 'onDragStop':
if (!this.state.dragging) throw new Error('onDragEnd called before onDragStart.');
newPosition.left = this.state.dragging.left;
newPosition.top = this.state.dragging.top;
this.setState({dragging: null});
break;
default:
throw new Error('onDragHandler called with unrecognized handlerName: ' + handlerName);
}
const {x, y} = this.calcXY(newPosition.top, newPosition.left);
handler.call(this, this.props.i, x, y, {e, node, newPosition});
};
}
/**
* Wrapper around drag events to provide more useful data.
* All drag events call the function with the given handler name,
* with the signature (index, x, y).
*
* @param {String} handlerName Handler name to wrap.
* @return {Function} Handler function.
*/
onResizeHandler(handlerName: string) {
return (e:Event, {node, size}: {node: HTMLElement, size: Position}) => {
const handler = this.props[handlerName];
if (!handler) return;
const {cols, x, i, maxW, minW, maxH, minH} = this.props;
// Get new XY
let {w, h} = this.calcWH(size);
// Cap w at numCols
w = Math.min(w, cols - x);
// Ensure w is at least 1
w = Math.max(w, 1);
// Min/max capping
w = Math.max(Math.min(w, maxW), minW);
h = Math.max(Math.min(h, maxH), minH);
this.setState({resizing: handlerName === 'onResizeStop' ? null : size});
handler.call(this, i, w, h, {e, node, size});
};
}
render(): ReactNode {
const {x, y, w, h, isDraggable, isResizable, useCSSTransforms} = this.props;
const pos = this.calcPosition(x, y, w, h, this.state);
const child = React.Children.only(this.props.children);
// Create the child element. We clone the existing element but modify its className and style.
let newChild = React.cloneElement(child, {
className: classNames('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),
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.
if (isResizable) newChild = this.mixinResizable(newChild, pos);
// Draggable support. This is always on, except for with placeholders.
if (isDraggable) newChild = this.mixinDraggable(newChild);
return newChild;
}
}

View File

@@ -0,0 +1,579 @@
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _lodash = require('lodash.isequal');
var _lodash2 = _interopRequireDefault(_lodash);
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _utils = require('./utils');
var _GridItem = require('./GridItem');
var _GridItem2 = _interopRequireDefault(_GridItem);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var noop = function noop() {};
// Types
// End Types
/**
* A reactive, fluid grid layout with draggable, resizable components.
*/
var ReactGridLayout = function (_React$Component) {
_inherits(ReactGridLayout, _React$Component);
// TODO publish internal ReactClass displayName transform
function ReactGridLayout(props, context) {
_classCallCheck(this, ReactGridLayout);
var _this = _possibleConstructorReturn(this, _React$Component.call(this, props, context));
_initialiseProps.call(_this);
(0, _utils.autoBindHandlers)(_this, ['onDragStart', 'onDrag', 'onDragStop', 'onResizeStart', 'onResize', 'onResizeStop']);
return _this;
}
ReactGridLayout.prototype.componentDidMount = function 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);
};
ReactGridLayout.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
var newLayoutBase = void 0;
// Legacy support for compactType
// Allow parent to set layout directly.
if (!(0, _lodash2.default)(nextProps.layout, this.props.layout) || nextProps.compactType !== this.props.compactType) {
newLayoutBase = nextProps.layout;
}
// 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.
else if (!(0, _utils.childrenEqual)(this.props.children, nextProps.children)) {
newLayoutBase = this.state.layout;
}
// We need to regenerate the layout.
if (newLayoutBase) {
var newLayout = (0, _utils.synchronizeLayoutWithChildren)(newLayoutBase, nextProps.children, nextProps.cols, this.compactType(nextProps));
var _oldLayout = this.state.layout;
this.setState({ layout: newLayout });
this.onLayoutMaybeChanged(newLayout, _oldLayout);
}
};
/**
* Calculates a pixel value for the container.
* @return {String} Container height in pixels.
*/
ReactGridLayout.prototype.containerHeight = function containerHeight() {
if (!this.props.autoSize) return;
var nbRow = (0, _utils.bottom)(this.state.layout);
var 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';
};
ReactGridLayout.prototype.compactType = function compactType(props) {
if (!props) props = this.props;
return props.verticalCompact === false ? null : props.compactType;
};
/**
* 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
*/
ReactGridLayout.prototype.onDragStart = function onDragStart(i, x, y, _ref) {
var e = _ref.e,
node = _ref.node;
var layout = this.state.layout;
var l = (0, _utils.getLayoutItem)(layout, i);
if (!l) return;
this.setState({ oldDragItem: (0, _utils.cloneLayoutItem)(l), oldLayout: this.state.layout });
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
*/
ReactGridLayout.prototype.onDrag = function onDrag(i, x, y, _ref2) {
var e = _ref2.e,
node = _ref2.node;
var oldDragItem = this.state.oldDragItem;
var layout = this.state.layout;
var cols = this.props.cols;
var l = (0, _utils.getLayoutItem)(layout, i);
if (!l) return;
// Create placeholder (display only)
var placeholder = {
w: l.w, h: l.h, x: l.x, y: l.y, placeholder: true, i: i
};
// Move the element to the dragged location.
var isUserAction = true;
layout = (0, _utils.moveElement)(layout, l, x, y, isUserAction, this.props.preventCollision, this.compactType(), cols);
this.props.onDrag(layout, oldDragItem, l, placeholder, e, node);
this.setState({
layout: (0, _utils.compact)(layout, this.compactType(), 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
*/
ReactGridLayout.prototype.onDragStop = function onDragStop(i, x, y, _ref3) {
var e = _ref3.e,
node = _ref3.node;
var oldDragItem = this.state.oldDragItem;
var layout = this.state.layout;
var _props = this.props,
cols = _props.cols,
preventCollision = _props.preventCollision;
var l = (0, _utils.getLayoutItem)(layout, i);
if (!l) return;
// Move the element here
var isUserAction = true;
layout = (0, _utils.moveElement)(layout, l, x, y, isUserAction, preventCollision, this.compactType(), cols);
this.props.onDragStop(layout, oldDragItem, l, null, e, node);
// Set state
var newLayout = (0, _utils.compact)(layout, this.compactType(), cols);
var oldLayout = this.state.oldLayout;
this.setState({
activeDrag: null,
layout: newLayout,
oldDragItem: null,
oldLayout: null
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
};
ReactGridLayout.prototype.onLayoutMaybeChanged = function onLayoutMaybeChanged(newLayout, oldLayout) {
if (!oldLayout) oldLayout = this.state.layout;
if (!(0, _lodash2.default)(oldLayout, newLayout)) {
this.props.onLayoutChange(newLayout);
}
};
ReactGridLayout.prototype.onResizeStart = function onResizeStart(i, w, h, _ref4) {
var e = _ref4.e,
node = _ref4.node;
var layout = this.state.layout;
var l = (0, _utils.getLayoutItem)(layout, i);
if (!l) return;
this.setState({
oldResizeItem: (0, _utils.cloneLayoutItem)(l),
oldLayout: this.state.layout
});
this.props.onResizeStart(layout, l, l, null, e, node);
};
ReactGridLayout.prototype.onResize = function onResize(i, w, h, _ref5) {
var e = _ref5.e,
node = _ref5.node;
var _state = this.state,
layout = _state.layout,
oldResizeItem = _state.oldResizeItem;
var _props2 = this.props,
cols = _props2.cols,
preventCollision = _props2.preventCollision;
var l = (0, _utils.getLayoutItem)(layout, i);
if (!l) return;
// Short circuit if there is a collision in no rearrangement mode.
if (preventCollision && (0, _utils.getFirstCollision)(layout, _extends({}, l, { w: w, h: h }))) {
return;
}
// Set new width and height.
l.w = w;
l.h = h;
// Create placeholder element (display only)
var placeholder = {
w: w, h: h, x: l.x, y: l.y, static: true, i: i
};
this.props.onResize(layout, oldResizeItem, l, placeholder, e, node);
// Re-compact the layout and set the drag placeholder.
this.setState({
layout: (0, _utils.compact)(layout, this.compactType(), cols),
activeDrag: placeholder
});
};
ReactGridLayout.prototype.onResizeStop = function onResizeStop(i, w, h, _ref6) {
var e = _ref6.e,
node = _ref6.node;
var _state2 = this.state,
layout = _state2.layout,
oldResizeItem = _state2.oldResizeItem;
var cols = this.props.cols;
var l = (0, _utils.getLayoutItem)(layout, i);
this.props.onResizeStop(layout, oldResizeItem, l, null, e, node);
// Set state
var newLayout = (0, _utils.compact)(layout, this.compactType(), cols);
var oldLayout = this.state.oldLayout;
this.setState({
activeDrag: null,
layout: newLayout,
oldResizeItem: null,
oldLayout: null
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
};
/**
* Create a placeholder object.
* @return {Element} Placeholder div.
*/
ReactGridLayout.prototype.placeholder = function placeholder() {
var activeDrag = this.state.activeDrag;
if (!activeDrag) return null;
var _props3 = this.props,
width = _props3.width,
cols = _props3.cols,
margin = _props3.margin,
containerPadding = _props3.containerPadding,
rowHeight = _props3.rowHeight,
maxRows = _props3.maxRows,
useCSSTransforms = _props3.useCSSTransforms;
// {...this.state.activeDrag} is pretty slow, actually
return _react2.default.createElement(
_GridItem2.default,
{
w: activeDrag.w,
h: activeDrag.h,
x: activeDrag.x,
y: activeDrag.y,
i: activeDrag.i,
className: 'react-grid-placeholder',
containerWidth: width,
cols: cols,
margin: margin,
containerPadding: containerPadding || margin,
maxRows: maxRows,
rowHeight: rowHeight,
isDraggable: false,
isResizable: false,
useCSSTransforms: useCSSTransforms },
_react2.default.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.
*/
ReactGridLayout.prototype.processGridItem = function processGridItem(child) {
if (!child.key) return;
var l = (0, _utils.getLayoutItem)(this.state.layout, String(child.key));
if (!l) return null;
var _props4 = this.props,
width = _props4.width,
cols = _props4.cols,
margin = _props4.margin,
containerPadding = _props4.containerPadding,
rowHeight = _props4.rowHeight,
maxRows = _props4.maxRows,
isDraggable = _props4.isDraggable,
isResizable = _props4.isResizable,
useCSSTransforms = _props4.useCSSTransforms,
draggableCancel = _props4.draggableCancel,
draggableHandle = _props4.draggableHandle;
var mounted = this.state.mounted;
// Parse 'static'. Any properties defined directly on the grid item will take precedence.
var draggable = Boolean(!l.static && isDraggable && (l.isDraggable || l.isDraggable == null));
var resizable = Boolean(!l.static && isResizable && (l.isResizable || l.isResizable == null));
return _react2.default.createElement(
_GridItem2.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,
useCSSTransforms: useCSSTransforms && mounted,
usePercentages: !mounted,
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
},
child
);
};
ReactGridLayout.prototype.render = function render() {
var _this2 = this;
var _props5 = this.props,
className = _props5.className,
style = _props5.style;
var mergedStyle = _extends({
height: this.containerHeight()
}, style);
return _react2.default.createElement(
'div',
{ className: (0, _classnames2.default)('react-grid-layout', className), style: mergedStyle },
// $FlowIgnore: Appears to think map calls back w/array
_react2.default.Children.map(this.props.children, function (child) {
return _this2.processGridItem(child);
}),
this.placeholder()
);
};
return ReactGridLayout;
}(_react2.default.Component);
ReactGridLayout.displayName = "ReactGridLayout";
ReactGridLayout.propTypes = {
//
// Basic props
//
className: _propTypes2.default.string,
style: _propTypes2.default.object,
// This can be set explicitly. If it is not set, it will automatically
// be set to the container width. Note that resizes will *not* cause this to adjust.
// If you need that behavior, use WidthProvider.
width: _propTypes2.default.number,
// If true, the container height swells and contracts to fit contents
autoSize: _propTypes2.default.bool,
// # of cols.
cols: _propTypes2.default.number,
// A selector that will not be draggable.
draggableCancel: _propTypes2.default.string,
// A selector for the draggable handler
draggableHandle: _propTypes2.default.string,
// Deprecated
verticalCompact: function verticalCompact(props) {
if (props.verticalCompact === false && process.env.NODE_ENV !== 'production') {
console.warn( // eslint-disable-line no-console
'`verticalCompact` on <ReactGridLayout> is deprecated and will be removed soon. ' + 'Use `compactType`: "horizontal" | "vertical" | null.');
}
},
// Choose vertical or hotizontal compaction
compactType: _propTypes2.default.oneOf(['vertical', 'horizontal']),
// layout is an array of object with the format:
// {x: Number, y: Number, w: Number, h: Number, i: String}
layout: function layout(props) {
var layout = props.layout;
// I hope you're setting the data-grid property on the grid items
if (layout === undefined) return;
(0, _utils.validateLayout)(layout, 'layout');
},
//
// Grid Dimensions
//
// Margin between items [x, y] in px
margin: _propTypes2.default.arrayOf(_propTypes2.default.number),
// Padding inside the container [x, y] in px
containerPadding: _propTypes2.default.arrayOf(_propTypes2.default.number),
// Rows have a static height, but you can change this based on breakpoints if you like
rowHeight: _propTypes2.default.number,
// Default Infinity, but you can specify a max here if you like.
// Note that this isn't fully fleshed out and won't error if you specify a layout that
// extends beyond the row capacity. It will, however, not allow users to drag/resize
// an item past the barrier. They can push items beyond the barrier, though.
// Intentionally not documented for this reason.
maxRows: _propTypes2.default.number,
//
// Flags
//
isDraggable: _propTypes2.default.bool,
isResizable: _propTypes2.default.bool,
// If true, grid items won't change position when being dragged over.
preventCollision: _propTypes2.default.bool,
// Use CSS transforms instead of top/left
useCSSTransforms: _propTypes2.default.bool,
//
// Callbacks
//
// Callback so you can save the layout. Calls after each drag & resize stops.
onLayoutChange: _propTypes2.default.func,
// Calls when drag starts. Callback is of the signature (layout, oldItem, newItem, placeholder, e, ?node).
// All callbacks below have the same signature. 'start' and 'stop' callbacks omit the 'placeholder'.
onDragStart: _propTypes2.default.func,
// Calls on each drag movement.
onDrag: _propTypes2.default.func,
// Calls when drag is complete.
onDragStop: _propTypes2.default.func,
//Calls when resize starts.
onResizeStart: _propTypes2.default.func,
// Calls when resize movement happens.
onResize: _propTypes2.default.func,
// Calls when resize is complete.
onResizeStop: _propTypes2.default.func,
//
// Other validations
//
// Children must not have duplicate keys.
children: function children(props, propName, _componentName) {
var children = props[propName];
// Check children keys for duplicates. Throw if found.
var keys = {};
_react2.default.Children.forEach(children, function (child) {
if (keys[child.key]) {
throw new Error("Duplicate child key \"" + child.key + "\" found! This will cause problems in ReactGridLayout.");
}
keys[child.key] = true;
});
}
};
ReactGridLayout.defaultProps = {
autoSize: true,
cols: 12,
className: '',
rowHeight: 150,
maxRows: Infinity, // infinite vertical growth
layout: [],
margin: [10, 10],
isDraggable: true,
isResizable: true,
useCSSTransforms: true,
verticalCompact: true,
compactType: 'vertical',
preventCollision: false,
onLayoutChange: noop,
onDragStart: noop,
onDrag: noop,
onDragStop: noop,
onResizeStart: noop,
onResize: noop,
onResizeStop: noop
};
var _initialiseProps = function _initialiseProps() {
this.state = {
activeDrag: null,
layout: (0, _utils.synchronizeLayoutWithChildren)(this.props.layout, this.props.children, this.props.cols,
// Legacy support for verticalCompact: false
this.compactType()),
mounted: false,
oldDragItem: null,
oldLayout: null,
oldResizeItem: null
};
};
exports.default = ReactGridLayout;

View File

@@ -0,0 +1,517 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal';
import classNames from 'classnames';
import {autoBindHandlers, bottom, childrenEqual, cloneLayoutItem, compact, getLayoutItem, moveElement,
synchronizeLayoutWithChildren, validateLayout, getFirstCollision} from './utils';
import GridItem from './GridItem';
import type {ChildrenArray as ReactChildrenArray, Element as ReactElement} from 'react';
const noop = function() {};
// Types
import type {EventCallback, CompactType, GridResizeEvent, GridDragEvent, Layout, LayoutItem} from './utils';
type State = {
activeDrag: ?LayoutItem,
layout: Layout,
mounted: boolean,
oldDragItem: ?LayoutItem,
oldLayout: ?Layout,
oldResizeItem: ?LayoutItem
};
export type Props = {
className: string,
style: Object,
width: number,
autoSize: boolean,
cols: number,
draggableCancel: string,
draggableHandle: string,
verticalCompact: boolean,
compactType: ?('horizontal' | 'vertical'),
layout: Layout,
margin: [number, number],
containerPadding: [number, number],
rowHeight: number,
maxRows: number,
isDraggable: boolean,
isResizable: boolean,
preventCollision: boolean,
useCSSTransforms: boolean,
// Callbacks
onLayoutChange: (Layout) => void,
onDrag: EventCallback,
onDragStart: EventCallback,
onDragStop: EventCallback,
onResize: EventCallback,
onResizeStart: EventCallback,
onResizeStop: EventCallback,
children: ReactChildrenArray<ReactElement<any>>,
};
// End Types
/**
* A reactive, fluid grid layout with draggable, resizable components.
*/
export default class ReactGridLayout extends React.Component<Props, State> {
// TODO publish internal ReactClass displayName transform
static displayName = "ReactGridLayout";
static propTypes = {
//
// Basic props
//
className: PropTypes.string,
style: PropTypes.object,
// This can be set explicitly. If it is not set, it will automatically
// be set to the container width. Note that resizes will *not* cause this to adjust.
// If you need that behavior, use WidthProvider.
width: PropTypes.number,
// If true, the container height swells and contracts to fit contents
autoSize: PropTypes.bool,
// # of cols.
cols: PropTypes.number,
// A selector that will not be draggable.
draggableCancel: PropTypes.string,
// A selector for the draggable handler
draggableHandle: PropTypes.string,
// Deprecated
verticalCompact: function(props) {
if (props.verticalCompact === false && process.env.NODE_ENV !== 'production') {
console.warn( // eslint-disable-line no-console
'`verticalCompact` on <ReactGridLayout> is deprecated and will be removed soon. ' +
'Use `compactType`: "horizontal" | "vertical" | null.');
}
},
// Choose vertical or hotizontal compaction
compactType: PropTypes.oneOf(['vertical', 'horizontal']),
// layout is an array of object with the format:
// {x: Number, y: Number, w: Number, h: Number, i: String}
layout: function (props) {
var layout = props.layout;
// I hope you're setting the data-grid property on the grid items
if (layout === undefined) return;
validateLayout(layout, 'layout');
},
//
// Grid Dimensions
//
// Margin between items [x, y] in px
margin: PropTypes.arrayOf(PropTypes.number),
// Padding inside the container [x, y] in px
containerPadding: PropTypes.arrayOf(PropTypes.number),
// Rows have a static height, but you can change this based on breakpoints if you like
rowHeight: PropTypes.number,
// Default Infinity, but you can specify a max here if you like.
// Note that this isn't fully fleshed out and won't error if you specify a layout that
// extends beyond the row capacity. It will, however, not allow users to drag/resize
// an item past the barrier. They can push items beyond the barrier, though.
// Intentionally not documented for this reason.
maxRows: PropTypes.number,
//
// Flags
//
isDraggable: PropTypes.bool,
isResizable: PropTypes.bool,
// If true, grid items won't change position when being dragged over.
preventCollision: PropTypes.bool,
// Use CSS transforms instead of top/left
useCSSTransforms: PropTypes.bool,
//
// Callbacks
//
// Callback so you can save the layout. Calls after each drag & resize stops.
onLayoutChange: PropTypes.func,
// Calls when drag starts. Callback is of the signature (layout, oldItem, newItem, placeholder, e, ?node).
// All callbacks below have the same signature. 'start' and 'stop' callbacks omit the 'placeholder'.
onDragStart: PropTypes.func,
// Calls on each drag movement.
onDrag: PropTypes.func,
// Calls when drag is complete.
onDragStop: PropTypes.func,
//Calls when resize starts.
onResizeStart: PropTypes.func,
// Calls when resize movement happens.
onResize: PropTypes.func,
// Calls when resize is complete.
onResizeStop: PropTypes.func,
//
// Other validations
//
// Children must not have duplicate keys.
children: function (props, propName, _componentName) {
var children = props[propName];
// Check children keys for duplicates. Throw if found.
var keys = {};
React.Children.forEach(children, function (child) {
if (keys[child.key]) {
throw new Error("Duplicate child key \"" + child.key + "\" found! This will cause problems in ReactGridLayout.");
}
keys[child.key] = true;
});
}
};
static defaultProps = {
autoSize: true,
cols: 12,
className: '',
rowHeight: 150,
maxRows: Infinity, // infinite vertical growth
layout: [],
margin: [10, 10],
isDraggable: true,
isResizable: true,
useCSSTransforms: true,
verticalCompact: true,
compactType: 'vertical',
preventCollision: false,
onLayoutChange: noop,
onDragStart: noop,
onDrag: noop,
onDragStop: noop,
onResizeStart: noop,
onResize: noop,
onResizeStop: noop
};
state: State = {
activeDrag: null,
layout: synchronizeLayoutWithChildren(this.props.layout, this.props.children, this.props.cols,
// Legacy support for verticalCompact: false
this.compactType()),
mounted: false,
oldDragItem: null,
oldLayout: null,
oldResizeItem: null,
};
constructor(props: Props, context: any): void {
super(props, context);
autoBindHandlers(this, ['onDragStart', 'onDrag', 'onDragStop', 'onResizeStart', 'onResize', 'onResizeStop']);
}
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);
}
componentWillReceiveProps(nextProps: Props) {
let newLayoutBase;
// Legacy support for compactType
// Allow parent to set layout directly.
if (!isEqual(nextProps.layout, this.props.layout) || nextProps.compactType !== this.props.compactType) {
newLayoutBase = nextProps.layout;
}
// 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.
else if (!childrenEqual(this.props.children, nextProps.children)) {
newLayoutBase = this.state.layout;
}
// We need to regenerate the layout.
if (newLayoutBase) {
const newLayout = synchronizeLayoutWithChildren(newLayoutBase, nextProps.children,
nextProps.cols, this.compactType(nextProps));
const oldLayout = this.state.layout;
this.setState({layout: newLayout});
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
}
/**
* Calculates a pixel value for the container.
* @return {String} Container height in pixels.
*/
containerHeight() {
if (!this.props.autoSize) return;
const nbRow = 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';
}
compactType(props: ?Object): CompactType {
if (!props) props = this.props;
return props.verticalCompact === false ? null : props.compactType;
}
/**
* 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
*/
onDragStart(i:string, x:number, y:number, {e, node}: GridDragEvent) {
const {layout} = this.state;
var l = getLayoutItem(layout, i);
if (!l) return;
this.setState({oldDragItem: cloneLayoutItem(l), oldLayout: this.state.layout});
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
*/
onDrag(i:string, x:number, y:number, {e, node}: GridDragEvent) {
const {oldDragItem} = this.state;
let {layout} = this.state;
const {cols} = this.props;
var l = getLayoutItem(layout, i);
if (!l) return;
// Create placeholder (display only)
var 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 = moveElement(layout, l, x, y, isUserAction, this.props.preventCollision, this.compactType(), cols);
this.props.onDrag(layout, oldDragItem, l, placeholder, e, node);
this.setState({
layout: compact(layout, this.compactType(), 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
*/
onDragStop(i:string, x:number, y:number, {e, node}: GridDragEvent) {
const {oldDragItem} = this.state;
let {layout} = this.state;
const {cols, preventCollision} = this.props;
const l = getLayoutItem(layout, i);
if (!l) return;
// Move the element here
const isUserAction = true;
layout = moveElement(layout, l, x, y, isUserAction, preventCollision, this.compactType(), cols);
this.props.onDragStop(layout, oldDragItem, l, null, e, node);
// Set state
const newLayout = compact(layout, this.compactType(), cols);
const {oldLayout} = this.state;
this.setState({
activeDrag: null,
layout: newLayout,
oldDragItem: null,
oldLayout: null,
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
onLayoutMaybeChanged(newLayout: Layout, oldLayout: ?Layout) {
if (!oldLayout) oldLayout = this.state.layout;
if (!isEqual(oldLayout, newLayout)) {
this.props.onLayoutChange(newLayout);
}
}
onResizeStart(i:string, w:number, h:number, {e, node}: GridResizeEvent) {
const {layout} = this.state;
var l = getLayoutItem(layout, i);
if (!l) return;
this.setState({
oldResizeItem: cloneLayoutItem(l),
oldLayout: this.state.layout
});
this.props.onResizeStart(layout, l, l, null, e, node);
}
onResize(i:string, w:number, h:number, {e, node}: GridResizeEvent) {
const {layout, oldResizeItem} = this.state;
const {cols, preventCollision} = this.props;
var l = getLayoutItem(layout, i);
if (!l) return;
// Short circuit if there is a collision in no rearrangement mode.
if (preventCollision && getFirstCollision(layout, {...l, w, h})) {
return;
}
// Set new width and height.
l.w = w;
l.h = h;
// Create placeholder element (display only)
var placeholder = {
w: w, h: h, x: l.x, y: l.y, static: true, i: i
};
this.props.onResize(layout, oldResizeItem, l, placeholder, e, node);
// Re-compact the layout and set the drag placeholder.
this.setState({
layout: compact(layout, this.compactType(), cols),
activeDrag: placeholder
});
}
onResizeStop(i:string, w:number, h:number, {e, node}: GridResizeEvent) {
const {layout, oldResizeItem} = this.state;
const {cols} = this.props;
var l = getLayoutItem(layout, i);
this.props.onResizeStop(layout, oldResizeItem, l, null, e, node);
// Set state
const newLayout = compact(layout, this.compactType(), cols);
const {oldLayout} = this.state;
this.setState({
activeDrag: null,
layout: newLayout,
oldResizeItem: null,
oldLayout: null
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
/**
* 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} = this.props;
// {...this.state.activeDrag} is pretty slow, actually
return (
<GridItem
w={activeDrag.w}
h={activeDrag.h}
x={activeDrag.x}
y={activeDrag.y}
i={activeDrag.i}
className="react-grid-placeholder"
containerWidth={width}
cols={cols}
margin={margin}
containerPadding={containerPadding || margin}
maxRows={maxRows}
rowHeight={rowHeight}
isDraggable={false}
isResizable={false}
useCSSTransforms={useCSSTransforms}>
<div />
</GridItem>
);
}
/**
* 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>): ?ReactElement<any> {
if (!child.key) return;
const l = getLayoutItem(this.state.layout, String(child.key));
if (!l) return null;
const {width, cols, margin, containerPadding, rowHeight,
maxRows, isDraggable, isResizable, useCSSTransforms,
draggableCancel, draggableHandle} = this.props;
const {mounted} = this.state;
// Parse 'static'. Any properties defined directly on the grid item will take precedence.
const draggable = Boolean(!l.static && isDraggable && (l.isDraggable || l.isDraggable == null));
const resizable = Boolean(!l.static && isResizable && (l.isResizable || l.isResizable == null));
return (
<GridItem
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}
useCSSTransforms={useCSSTransforms && mounted}
usePercentages={!mounted}
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}
>
{child}
</GridItem>
);
}
render() {
const {className, style} = this.props;
const mergedStyle = {
height: this.containerHeight(),
...style
};
return (
<div className={classNames('react-grid-layout', className)} style={mergedStyle}>
{
// $FlowIgnore: Appears to think map calls back w/array
React.Children.map(this.props.children, (child) => this.processGridItem(child))
}
{this.placeholder()}
</div>
);
}
}

View File

@@ -0,0 +1,228 @@
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _lodash = require('lodash.isequal');
var _lodash2 = _interopRequireDefault(_lodash);
var _utils = require('./utils');
var _responsiveUtils = require('./responsiveUtils');
var _ReactGridLayout = require('./ReactGridLayout');
var _ReactGridLayout2 = _interopRequireDefault(_ReactGridLayout);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var noop = function noop() {};
var type = function type(obj) {
return Object.prototype.toString.call(obj);
};
var ResponsiveReactGridLayout = function (_React$Component) {
_inherits(ResponsiveReactGridLayout, _React$Component);
function ResponsiveReactGridLayout() {
var _temp, _this, _ret;
_classCallCheck(this, ResponsiveReactGridLayout);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = _this.generateInitialState(), _this.onLayoutChange = function (layout) {
var _extends2;
_this.props.onLayoutChange(layout, _extends({}, _this.props.layouts, (_extends2 = {}, _extends2[_this.state.breakpoint] = layout, _extends2)));
}, _temp), _possibleConstructorReturn(_this, _ret);
}
// This should only include propTypes needed in this code; RGL itself
// will do validation of the rest props passed to it.
ResponsiveReactGridLayout.prototype.generateInitialState = function generateInitialState() {
var _props = this.props,
width = _props.width,
breakpoints = _props.breakpoints,
layouts = _props.layouts,
cols = _props.cols;
var breakpoint = (0, _responsiveUtils.getBreakpointFromWidth)(breakpoints, width);
var colNo = (0, _responsiveUtils.getColsFromBreakpoint)(breakpoint, cols);
// verticalCompact compatibility, now deprecated
var compactType = this.props.verticalCompact === false ? null : this.props.compactType;
// Get the initial layout. This can tricky; we try to generate one however possible if one doesn't exist
// for this layout.
var initialLayout = (0, _responsiveUtils.findOrGenerateResponsiveLayout)(layouts, breakpoints, breakpoint, breakpoint, colNo, compactType);
return {
layout: initialLayout,
breakpoint: breakpoint,
cols: colNo
};
};
ResponsiveReactGridLayout.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
// Allow parent to set width or breakpoint directly.
if (nextProps.width != this.props.width || nextProps.breakpoint !== this.props.breakpoint || !(0, _lodash2.default)(nextProps.breakpoints, this.props.breakpoints) || !(0, _lodash2.default)(nextProps.cols, this.props.cols)) {
this.onWidthChange(nextProps);
}
// Allow parent to set layouts directly.
else if (!(0, _lodash2.default)(nextProps.layouts, this.props.layouts)) {
var _state = this.state,
_breakpoint = _state.breakpoint,
_cols = _state.cols;
// Since we're setting an entirely new layout object, we must generate a new responsive layout
// if one does not exist.
var newLayout = (0, _responsiveUtils.findOrGenerateResponsiveLayout)(nextProps.layouts, nextProps.breakpoints, _breakpoint, _breakpoint, _cols, nextProps.compactType);
this.setState({ layout: newLayout });
}
};
// wrap layouts so we do not need to pass layouts to child
/**
* When the width changes work through breakpoints and reset state with the new width & breakpoint.
* Width changes are necessary to figure out the widget widths.
*/
ResponsiveReactGridLayout.prototype.onWidthChange = function onWidthChange(nextProps) {
var breakpoints = nextProps.breakpoints,
cols = nextProps.cols,
layouts = nextProps.layouts,
compactType = nextProps.compactType;
var newBreakpoint = nextProps.breakpoint || (0, _responsiveUtils.getBreakpointFromWidth)(nextProps.breakpoints, nextProps.width);
var lastBreakpoint = this.state.breakpoint;
// Breakpoint change
if (lastBreakpoint !== newBreakpoint || this.props.breakpoints !== breakpoints || this.props.cols !== cols) {
// Preserve the current layout if the current breakpoint is not present in the next layouts.
if (!(lastBreakpoint in layouts)) layouts[lastBreakpoint] = (0, _utils.cloneLayout)(this.state.layout);
// Find or generate a new layout.
var newCols = (0, _responsiveUtils.getColsFromBreakpoint)(newBreakpoint, cols);
var _layout = (0, _responsiveUtils.findOrGenerateResponsiveLayout)(layouts, breakpoints, newBreakpoint, lastBreakpoint, newCols, compactType);
// This adds missing items.
_layout = (0, _utils.synchronizeLayoutWithChildren)(_layout, nextProps.children, newCols, compactType);
// Store the new layout.
layouts[newBreakpoint] = _layout;
// callbacks
this.props.onLayoutChange(_layout, layouts);
this.props.onBreakpointChange(newBreakpoint, newCols);
this.props.onWidthChange(nextProps.width, nextProps.margin, newCols, nextProps.containerPadding);
this.setState({ breakpoint: newBreakpoint, layout: _layout, cols: newCols });
}
};
ResponsiveReactGridLayout.prototype.render = function render() {
// eslint-disable-next-line no-unused-vars
var _props2 = this.props,
breakpoint = _props2.breakpoint,
breakpoints = _props2.breakpoints,
cols = _props2.cols,
layouts = _props2.layouts,
onBreakpointChange = _props2.onBreakpointChange,
onLayoutChange = _props2.onLayoutChange,
onWidthChange = _props2.onWidthChange,
other = _objectWithoutProperties(_props2, ['breakpoint', 'breakpoints', 'cols', 'layouts', 'onBreakpointChange', 'onLayoutChange', 'onWidthChange']);
return _react2.default.createElement(_ReactGridLayout2.default, _extends({}, other, {
onLayoutChange: this.onLayoutChange,
layout: this.state.layout,
cols: this.state.cols
}));
};
return ResponsiveReactGridLayout;
}(_react2.default.Component);
ResponsiveReactGridLayout.propTypes = {
//
// Basic props
//
// Optional, but if you are managing width yourself you may want to set the breakpoint
// yourself as well.
breakpoint: _propTypes2.default.string,
// {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480}
breakpoints: _propTypes2.default.object,
// # of cols. This is a breakpoint -> cols map
cols: _propTypes2.default.object,
// layouts is an object mapping breakpoints to layouts.
// e.g. {lg: Layout, md: Layout, ...}
layouts: function layouts(props, propName) {
if (type(props[propName]) !== '[object Object]') {
throw new Error('Layout property must be an object. Received: ' + type(props[propName]));
}
Object.keys(props[propName]).forEach(function (key) {
if (!(key in props.breakpoints)) {
throw new Error('Each key in layouts must align with a key in breakpoints.');
}
(0, _utils.validateLayout)(props.layouts[key], 'layouts.' + key);
});
},
// The width of this component.
// Required in this propTypes stanza because generateInitialState() will fail without it.
width: _propTypes2.default.number.isRequired,
//
// Callbacks
//
// Calls back with breakpoint and new # cols
onBreakpointChange: _propTypes2.default.func,
// Callback so you can save the layout.
// Calls back with (currentLayout, allLayouts). allLayouts are keyed by breakpoint.
onLayoutChange: _propTypes2.default.func,
// Calls back with (containerWidth, margin, cols, containerPadding)
onWidthChange: _propTypes2.default.func
};
ResponsiveReactGridLayout.defaultProps = {
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 },
cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
layouts: {},
onBreakpointChange: noop,
onLayoutChange: noop,
onWidthChange: noop
};
exports.default = ResponsiveReactGridLayout;

View File

@@ -0,0 +1,200 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal';
import {cloneLayout, synchronizeLayoutWithChildren, validateLayout} from './utils';
import {getBreakpointFromWidth, getColsFromBreakpoint, findOrGenerateResponsiveLayout} from './responsiveUtils';
import ReactGridLayout from './ReactGridLayout';
import type {Props as RGLProps} from './ReactGridLayout';
import type {Layout} from './utils';
const noop = function(){};
const type = (obj) => Object.prototype.toString.call(obj);
type State = {
layout: Layout,
breakpoint: string,
cols: number
};
type Props<Breakpoint: string = string> = {
...$Exact<RGLProps>,
// Responsive config
breakpoint: Breakpoint,
breakpoints: {[key: Breakpoint]: number},
cols: {[key: Breakpoint]: number},
layouts: {[key: Breakpoint]: Layout},
width: number,
// Callbacks
onBreakpointChange: (Breakpoint, cols: number) => void,
onLayoutChange: (Layout, {[key: Breakpoint]: Layout}) => void,
onWidthChange:
(containerWidth: number, margin: [number, number], cols: number, containerPadding: [number, number]) => void
};
export default class ResponsiveReactGridLayout extends React.Component<Props<>, State> {
// This should only include propTypes needed in this code; RGL itself
// will do validation of the rest props passed to it.
static propTypes = {
//
// Basic props
//
// Optional, but if you are managing width yourself you may want to set the breakpoint
// yourself as well.
breakpoint: PropTypes.string,
// {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480}
breakpoints: PropTypes.object,
// # of cols. This is a breakpoint -> cols map
cols: PropTypes.object,
// layouts is an object mapping breakpoints to layouts.
// e.g. {lg: Layout, md: Layout, ...}
layouts(props, propName) {
if (type(props[propName]) !== '[object Object]') {
throw new Error('Layout property must be an object. Received: ' + type(props[propName]));
}
Object.keys(props[propName]).forEach((key) => {
if (!(key in props.breakpoints)) {
throw new Error('Each key in layouts must align with a key in breakpoints.');
}
validateLayout(props.layouts[key], 'layouts.' + key);
});
},
// The width of this component.
// Required in this propTypes stanza because generateInitialState() will fail without it.
width: PropTypes.number.isRequired,
//
// Callbacks
//
// Calls back with breakpoint and new # cols
onBreakpointChange: PropTypes.func,
// Callback so you can save the layout.
// Calls back with (currentLayout, allLayouts). allLayouts are keyed by breakpoint.
onLayoutChange: PropTypes.func,
// Calls back with (containerWidth, margin, cols, containerPadding)
onWidthChange: PropTypes.func
};
static defaultProps = {
breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0},
cols: {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2},
layouts: {},
onBreakpointChange: noop,
onLayoutChange: noop,
onWidthChange: noop,
};
state = this.generateInitialState();
generateInitialState(): State {
const {width, breakpoints, layouts, cols} = this.props;
const breakpoint = getBreakpointFromWidth(breakpoints, width);
const colNo = getColsFromBreakpoint(breakpoint, cols);
// verticalCompact compatibility, now deprecated
const compactType = this.props.verticalCompact === false ? null : this.props.compactType;
// Get the initial layout. This can tricky; we try to generate one however possible if one doesn't exist
// for this layout.
const initialLayout = findOrGenerateResponsiveLayout(layouts, breakpoints, breakpoint,
breakpoint, colNo, compactType);
return {
layout: initialLayout,
breakpoint: breakpoint,
cols: colNo
};
}
componentWillReceiveProps(nextProps: Props<*>) {
// Allow parent to set width or breakpoint directly.
if (
nextProps.width != this.props.width
|| nextProps.breakpoint !== this.props.breakpoint
|| !isEqual(nextProps.breakpoints, this.props.breakpoints)
|| !isEqual(nextProps.cols, this.props.cols)
) {
this.onWidthChange(nextProps);
}
// Allow parent to set layouts directly.
else if (!isEqual(nextProps.layouts, this.props.layouts)) {
const {breakpoint, cols} = this.state;
// Since we're setting an entirely new layout object, we must generate a new responsive layout
// if one does not exist.
const newLayout = findOrGenerateResponsiveLayout(
nextProps.layouts, nextProps.breakpoints,
breakpoint, breakpoint, cols, nextProps.compactType
);
this.setState({layout: newLayout});
}
}
// wrap layouts so we do not need to pass layouts to child
onLayoutChange = (layout: Layout) => {
this.props.onLayoutChange(layout, {...this.props.layouts, [this.state.breakpoint]: layout});
};
/**
* When the width changes work through breakpoints and reset state with the new width & breakpoint.
* Width changes are necessary to figure out the widget widths.
*/
onWidthChange(nextProps: Props<*>) {
const {breakpoints, cols, layouts, compactType} = nextProps;
const newBreakpoint = nextProps.breakpoint || getBreakpointFromWidth(nextProps.breakpoints, nextProps.width);
const lastBreakpoint = this.state.breakpoint;
// Breakpoint change
if (lastBreakpoint !== newBreakpoint || this.props.breakpoints !== breakpoints || this.props.cols !== cols) {
// Preserve the current layout if the current breakpoint is not present in the next layouts.
if (!(lastBreakpoint in layouts)) layouts[lastBreakpoint] = cloneLayout(this.state.layout);
// Find or generate a new layout.
const newCols: number = getColsFromBreakpoint(newBreakpoint, cols);
let layout = findOrGenerateResponsiveLayout(layouts, breakpoints, newBreakpoint,
lastBreakpoint, newCols, compactType);
// This adds missing items.
layout = synchronizeLayoutWithChildren(layout, nextProps.children, newCols, compactType);
// Store the new layout.
layouts[newBreakpoint] = layout;
// callbacks
this.props.onLayoutChange(layout, layouts);
this.props.onBreakpointChange(newBreakpoint, newCols);
this.props.onWidthChange(nextProps.width, nextProps.margin, newCols, nextProps.containerPadding);
this.setState({breakpoint: newBreakpoint, layout: layout, cols: newCols});
}
}
render() {
// eslint-disable-next-line no-unused-vars
const {breakpoint, breakpoints, cols, layouts, onBreakpointChange, onLayoutChange, onWidthChange,
...other} = this.props;
return (
<ReactGridLayout
{...other}
onLayoutChange={this.onLayoutChange}
layout={this.state.layout}
cols={this.state.cols}
/>
);
}
}

View File

@@ -0,0 +1,87 @@
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/*
* A simple HOC that provides facility for listening to container resizes.
*/
var WidthProvider = function WidthProvider(ComposedComponent) {
var _class, _temp2;
return _temp2 = _class = function (_React$Component) {
_inherits(_class, _React$Component);
function _class() {
var _temp, _this, _ret;
_classCallCheck(this, _class);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
width: 1280
}, _this.mounted = false, _this.onWindowResize = function (_event) {
if (!_this.mounted) return;
var node = _reactDom2.default.findDOMNode(_this); // Flow casts this to Text | Element
if (node instanceof HTMLElement) _this.setState({ width: node.offsetWidth });
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_class.prototype.componentDidMount = function componentDidMount() {
this.mounted = true;
window.addEventListener('resize', this.onWindowResize);
// Call to properly set the breakpoint and resize the elements.
// Note that if you're doing a full-width element, this can get a little wonky if a scrollbar
// appears because of the grid. In that case, fire your own resize event, or set `overflow: scroll` on your body.
this.onWindowResize();
};
_class.prototype.componentWillUnmount = function componentWillUnmount() {
this.mounted = false;
window.removeEventListener('resize', this.onWindowResize);
};
_class.prototype.render = function render() {
if (this.props.measureBeforeMount && !this.mounted) {
return _react2.default.createElement('div', { className: this.props.className, style: this.props.style });
}
return _react2.default.createElement(ComposedComponent, _extends({}, this.props, this.state));
};
return _class;
}(_react2.default.Component), _class.defaultProps = {
measureBeforeMount: false
}, _class.propTypes = {
// If true, will not render children until mounted. Useful for getting the exact width before
// rendering, to prevent any unsightly resizing.
measureBeforeMount: _propTypes2.default.bool
}, _temp2;
};
exports.default = WidthProvider;

View File

@@ -0,0 +1,69 @@
// @flow
import React from "react";
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import type {ComponentType as ReactComponentType} from 'react';
type Props = {
className?: string,
measureBeforeMount: boolean,
style?: Object,
};
type State = {
width: number
};
/*
* A simple HOC that provides facility for listening to container resizes.
*/
type ProviderT = (ComposedComponent: ReactComponentType<any>) => ReactComponentType<any>;
const WidthProvider: ProviderT = (ComposedComponent) => class extends React.Component<Props, State> {
static defaultProps = {
measureBeforeMount: false
};
static propTypes = {
// If true, will not render children until mounted. Useful for getting the exact width before
// rendering, to prevent any unsightly resizing.
measureBeforeMount: PropTypes.bool
};
state: State = {
width: 1280
};
mounted: boolean = false;
componentDidMount() {
this.mounted = true;
window.addEventListener('resize', this.onWindowResize);
// Call to properly set the breakpoint and resize the elements.
// Note that if you're doing a full-width element, this can get a little wonky if a scrollbar
// appears because of the grid. In that case, fire your own resize event, or set `overflow: scroll` on your body.
this.onWindowResize();
}
componentWillUnmount() {
this.mounted = false;
window.removeEventListener('resize', this.onWindowResize);
}
onWindowResize = (_event: ?Event) => {
if (!this.mounted) return;
const node = ReactDOM.findDOMNode(this); // Flow casts this to Text | Element
if (node instanceof HTMLElement) this.setState({width: node.offsetWidth});
}
render() {
if (this.props.measureBeforeMount && !this.mounted) {
return <div className={this.props.className} style={this.props.style} />;
}
return <ComposedComponent {...this.props} {...this.state} />;
}
};
export default WidthProvider;

View File

@@ -0,0 +1,87 @@
'use strict';
exports.__esModule = true;
exports.getBreakpointFromWidth = getBreakpointFromWidth;
exports.getColsFromBreakpoint = getColsFromBreakpoint;
exports.findOrGenerateResponsiveLayout = findOrGenerateResponsiveLayout;
exports.sortBreakpoints = sortBreakpoints;
var _utils = require('./utils');
/**
* Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint).
*
* @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...})
* @param {Number} width Screen width.
* @return {String} Highest breakpoint that is less than width.
*/
function getBreakpointFromWidth(breakpoints, width) {
var sorted = sortBreakpoints(breakpoints);
var matching = sorted[0];
for (var i = 1, len = sorted.length; i < len; i++) {
var breakpointName = sorted[i];
if (width > breakpoints[breakpointName]) matching = breakpointName;
}
return matching;
}
/**
* Given a breakpoint, get the # of cols set for it.
* @param {String} breakpoint Breakpoint name.
* @param {Object} cols Map of breakpoints to cols.
* @return {Number} Number of cols.
*/
function getColsFromBreakpoint(breakpoint, cols) {
if (!cols[breakpoint]) {
throw new Error("ResponsiveReactGridLayout: `cols` entry for breakpoint " + breakpoint + " is missing!");
}
return cols[breakpoint];
}
/**
* Given existing layouts and a new breakpoint, find or generate a new layout.
*
* This finds the layout above the new one and generates from it, if it exists.
*
* @param {Object} layouts Existing layouts.
* @param {Array} breakpoints All breakpoints.
* @param {String} breakpoint New breakpoint.
* @param {String} breakpoint Last breakpoint (for fallback).
* @param {Number} cols Column count at new breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} New layout.
*/
function findOrGenerateResponsiveLayout(layouts, breakpoints, breakpoint, lastBreakpoint, cols, compactType) {
// If it already exists, just return it.
if (layouts[breakpoint]) return (0, _utils.cloneLayout)(layouts[breakpoint]);
// Find or generate the next layout
var layout = layouts[lastBreakpoint];
var breakpointsSorted = sortBreakpoints(breakpoints);
var breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (var i = 0, len = breakpointsAbove.length; i < len; i++) {
var b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}
layout = (0, _utils.cloneLayout)(layout || []); // clone layout so we don't modify existing items
return (0, _utils.compact)((0, _utils.correctBounds)(layout, { cols: cols }), compactType, cols);
}
/**
* Given breakpoints, return an array of breakpoints sorted by width. This is usually
* e.g. ['xxs', 'xs', 'sm', ...]
*
* @param {Object} breakpoints Key/value pair of breakpoint names to widths.
* @return {Array} Sorted breakpoints.
*/
function sortBreakpoints(breakpoints) {
var keys = Object.keys(breakpoints);
return keys.sort(function (a, b) {
return breakpoints[a] - breakpoints[b];
});
}

View File

@@ -0,0 +1,87 @@
// @flow
import {cloneLayout, compact, correctBounds} from './utils';
import type {CompactType, Layout} from './utils';
export type ResponsiveLayout = {lg?: Layout, md?: Layout, sm?: Layout, xs?: Layout, xxs?: Layout};
type Breakpoint = string;
type Breakpoints = {lg?: number, md?: number, sm?: number, xs?: number, xxs?: number};
/**
* Given a width, find the highest breakpoint that matches is valid for it (width > breakpoint).
*
* @param {Object} breakpoints Breakpoints object (e.g. {lg: 1200, md: 960, ...})
* @param {Number} width Screen width.
* @return {String} Highest breakpoint that is less than width.
*/
export function getBreakpointFromWidth(breakpoints: Breakpoints, width: number): Breakpoint {
const sorted = sortBreakpoints(breakpoints);
let matching = sorted[0];
for (let i = 1, len = sorted.length; i < len; i++) {
const breakpointName = sorted[i];
if (width > breakpoints[breakpointName]) matching = breakpointName;
}
return matching;
}
/**
* Given a breakpoint, get the # of cols set for it.
* @param {String} breakpoint Breakpoint name.
* @param {Object} cols Map of breakpoints to cols.
* @return {Number} Number of cols.
*/
export function getColsFromBreakpoint(breakpoint: Breakpoint, cols: Breakpoints): number {
if (!cols[breakpoint]) {
throw new Error("ResponsiveReactGridLayout: `cols` entry for breakpoint " + breakpoint + " is missing!");
}
return cols[breakpoint];
}
/**
* Given existing layouts and a new breakpoint, find or generate a new layout.
*
* This finds the layout above the new one and generates from it, if it exists.
*
* @param {Object} layouts Existing layouts.
* @param {Array} breakpoints All breakpoints.
* @param {String} breakpoint New breakpoint.
* @param {String} breakpoint Last breakpoint (for fallback).
* @param {Number} cols Column count at new breakpoint.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} New layout.
*/
export function findOrGenerateResponsiveLayout(layouts: ResponsiveLayout, breakpoints: Breakpoints,
breakpoint: Breakpoint, lastBreakpoint: Breakpoint,
cols: number, compactType: CompactType): Layout {
// If it already exists, just return it.
if (layouts[breakpoint]) return cloneLayout(layouts[breakpoint]);
// Find or generate the next layout
let layout = layouts[lastBreakpoint];
const breakpointsSorted = sortBreakpoints(breakpoints);
const breakpointsAbove = breakpointsSorted.slice(breakpointsSorted.indexOf(breakpoint));
for (let i = 0, len = breakpointsAbove.length; i < len; i++) {
const b = breakpointsAbove[i];
if (layouts[b]) {
layout = layouts[b];
break;
}
}
layout = cloneLayout(layout || []); // clone layout so we don't modify existing items
return compact(correctBounds(layout, {cols: cols}), compactType, cols);
}
/**
* Given breakpoints, return an array of breakpoints sorted by width. This is usually
* e.g. ['xxs', 'xs', 'sm', ...]
*
* @param {Object} breakpoints Key/value pair of breakpoint names to widths.
* @return {Array} Sorted breakpoints.
*/
export function sortBreakpoints(breakpoints: Breakpoints): Array<Breakpoint> {
const keys: Array<string> = Object.keys(breakpoints);
return keys.sort(function(a, b) {
return breakpoints[a] - breakpoints[b];
});
}

View File

@@ -0,0 +1,511 @@
'use strict';
exports.__esModule = true;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
exports.bottom = bottom;
exports.cloneLayout = cloneLayout;
exports.cloneLayoutItem = cloneLayoutItem;
exports.childrenEqual = childrenEqual;
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.autoBindHandlers = autoBindHandlers;
var _lodash = require('lodash.isequal');
var _lodash2 = _interopRequireDefault(_lodash);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// All callbacks are of the signature (layout, oldItem, newItem, placeholder, e).
var isProduction = process.env.NODE_ENV === 'production';
/**
* Return the bottom coordinate of the layout.
*
* @param {Array} layout Layout array.
* @return {Number} Bottom coordinate.
*/
function bottom(layout) {
var max = 0,
bottomY = void 0;
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) {
var newLayout = Array(layout.length);
for (var _i2 = 0, len = layout.length; _i2 < len; _i2++) {
newLayout[_i2] = cloneLayoutItem(layout[_i2]);
}
return newLayout;
}
// Fast path to cloning, since this is monomorphic
function cloneLayoutItem(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
isDraggable: layoutItem.isDraggable, isResizable: layoutItem.isResizable
};
}
/**
* 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, b) {
// $FlowIgnore: Appears to think map calls back w/array
return (0, _lodash2.default)(_react2.default.Children.map(a, function (c) {
return c.key;
}), _react2.default.Children.map(b, function (c) {
return c.key;
}));
}
/**
* Given two layoutitems, check if they collide.
*/
function collides(l1, l2) {
if (l1 === l2) 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.
*
* @param {Array} layout Layout.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} Compacted Layout.
*/
function compact(layout, compactType, cols) {
// 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 _i3 = 0, len = sorted.length; _i3 < len; _i3++) {
var l = cloneLayoutItem(sorted[_i3]);
// Don't move static elements
if (!l.static) {
l = compactItem(compareWith, l, compactType, cols);
// 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[_i3])] = l;
// Clear moved flag, if it exists.
l.moved = false;
}
return out;
}
/**
* Compact an item in the layout.
*/
function compactItem(compareWith, l, compactType, cols) {
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) {
l.y = Math.min(bottom(compareWith), l.y);
// 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 = void 0;
while (collides = getFirstCollision(compareWith, l)) {
if (compactH) {
l.x = collides.x + collides.w;
} else {
l.y = collides.y + collides.h;
}
// 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.
*
* @param {Array} layout Layout array.
* @param {Number} bounds Number of columns.
*/
function correctBounds(layout, bounds) {
var collidesWith = getStatics(layout);
for (var _i4 = 0, len = layout.length; _i4 < len; _i4++) {
var l = layout[_i4];
// 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, id) {
for (var _i5 = 0, len = layout.length; _i5 < len; _i5++) {
if (layout[_i5].i === id) return layout[_i5];
}
}
/**
* 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, layoutItem) {
for (var _i6 = 0, len = layout.length; _i6 < len; _i6++) {
if (collides(layout[_i6], layoutItem)) return layout[_i6];
}
}
function getAllCollisions(layout, 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) {
return layout.filter(function (l) {
return l.static;
});
}
/**
* Move an element. Responsible for doing cascading movements of other elements.
*
* @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.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is
* being dragged/resized by the user.
*/
function moveElement(layout, l, x, y, isUserAction, preventCollision, compactType, cols) {
if (l.static) return layout;
// Short-circuit if nothing to do.
if (l.y === y && l.x === x) return layout;
var oldX = l.x;
var oldY = l.y;
var movingUp = y && l.y > 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);
if (movingUp) sorted = sorted.reverse();
var collisions = getAllCollisions(sorted, l);
// There was a collision; abort
if (preventCollision && collisions.length) {
l.x = oldX;
l.y = oldY;
l.moved = false;
return layout;
}
// Move each item that collides away from this element.
for (var _i7 = 0, len = collisions.length; _i7 < len; _i7++) {
var collision = collisions[_i7];
// console.log('resolving collision between', l.i, 'at', l.y, 'and', collision.i, 'at', collision.y);
// Short circuit so we can't infinite loop
if (collision.moved) continue;
// This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
if (l.y > collision.y && l.y - collision.y > collision.h / 4) continue;
if (l.x > collision.x && l.x - collision.x > collision.w / 4) 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.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is being dragged/resized
* by the user.
*/
function moveElementAwayFromCollision(layout, collidesWith, itemToMove, isUserAction, compactType, cols) {
var compactH = compactType === 'horizontal';
var compactV = compactType === 'vertical';
var preventCollision = false; // we're already colliding
// 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) {
// Make a mock item so we don't modify the item here, only modify in moveElement.
var fakeItem = {
x: compactH ? Math.max(collidesWith.x - itemToMove.w, 0) : itemToMove.x,
y: !compactH ? Math.max(collidesWith.y - itemToMove.h, 0) : itemToMove.y,
w: itemToMove.w,
h: itemToMove.h,
i: '-1'
};
if (!getFirstCollision(layout, fakeItem)) {
return moveElement(layout, itemToMove, compactH ? fakeItem.x : undefined, compactV ? fakeItem.y + 1 : undefined, isUserAction, preventCollision, compactType, cols);
}
}
// Previously this was optimized to move below the collision directly, but this can cause problems
// with cascading moves, as an item may actually leapflog a collision and cause a reversal in order.
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) {
return num * 100 + '%';
}
function setTransform(_ref) {
var top = _ref.top,
left = _ref.left,
width = _ref.width,
height = _ref.height;
// Replace unitless items with px
var translate = 'translate(' + left + 'px,' + top + 'px)';
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: width + 'px',
height: height + 'px',
position: 'absolute'
};
}
function setTopLeft(_ref2) {
var top = _ref2.top,
left = _ref2.left,
width = _ref2.width,
height = _ref2.height;
return {
top: top + 'px',
left: left + 'px',
width: width + 'px',
height: 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, compactType) {
if (compactType === 'horizontal') return sortLayoutItemsByColRow(layout);else return sortLayoutItemsByRowCol(layout);
}
function sortLayoutItemsByRowCol(layout) {
return [].concat(layout).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;
});
}
function sortLayoutItemsByColRow(layout) {
return [].concat(layout).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.
*
* @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, children, cols, compactType) {
initialLayout = initialLayout || [];
// Generate one layout item per child.
var layout = [];
_react2.default.Children.forEach(children, function (child, i) {
// 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');
}
layout[i] = cloneLayoutItem(_extends({}, g, { i: child.key }));
} else {
// Nothing provided: ensure this is added to the bottom
layout[i] = cloneLayoutItem({ w: 1, h: 1, x: 0, y: bottom(layout), i: String(child.key) });
}
}
});
// Correct the layout.
layout = correctBounds(layout, { cols: cols });
layout = compact(layout, compactType, cols);
return layout;
}
/**
* 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, contextName) {
contextName = contextName || "Layout";
var subProps = ['x', 'y', 'w', 'h'];
if (!Array.isArray(layout)) throw new Error(contextName + " must be an array!");
for (var _i8 = 0, len = layout.length; _i8 < len; _i8++) {
var item = layout[_i8];
for (var j = 0; j < subProps.length; j++) {
if (typeof item[subProps[j]] !== 'number') {
throw new Error('ReactGridLayout: ' + contextName + '[' + _i8 + '].' + subProps[j] + ' must be a number!');
}
}
if (item.i && typeof item.i !== 'string') {
throw new Error('ReactGridLayout: ' + contextName + '[' + _i8 + '].i must be a string!');
}
if (item.static !== undefined && typeof item.static !== 'boolean') {
throw new Error('ReactGridLayout: ' + contextName + '[' + _i8 + '].static must be a boolean!');
}
}
}
// Flow can't really figure this out, so we just use Object
function autoBindHandlers(el, fns) {
fns.forEach(function (key) {
return el[key] = el[key].bind(el);
});
}

View File

@@ -0,0 +1,505 @@
// @flow
import isEqual from 'lodash.isequal';
import React from 'react';
import type {ChildrenArray as ReactChildrenArray, Element as ReactElement} from 'react';
export type LayoutItem = {
w: number, h: number, x: number, y: number, i: string,
minW?: number, minH?: number, maxW?: number, maxH?: number,
moved?: boolean, static?: boolean,
isDraggable?: ?boolean, isResizable?: ?boolean
};
export type Layout = Array<LayoutItem>;
export type Position = {left: number, top: number, width: number, height: number};
export type ReactDraggableCallbackData = {
node: HTMLElement,
x: number, y: number,
deltaX: number, deltaY: number,
lastX: number, lastY: number
};
export type PartialPosition = {left: number, top: number};
export type Size = {width: number, height: number};
export type GridDragEvent = {e: Event, node: HTMLElement, newPosition: PartialPosition};
export type GridResizeEvent = {e: Event, node: HTMLElement, size: Size};
type REl = ReactElement<any>;
export type ReactChildren = ReactChildrenArray<REl>;
// All callbacks are of the signature (layout, oldItem, newItem, placeholder, e).
export type EventCallback =
(Layout, oldItem: ?LayoutItem, newItem: ?LayoutItem, placeholder: ?LayoutItem, Event, ?HTMLElement) => void;
export type CompactType = ?('horizontal' | 'vertical');
const isProduction = process.env.NODE_ENV === 'production';
/**
* Return the bottom coordinate of the layout.
*
* @param {Array} layout Layout array.
* @return {Number} Bottom coordinate.
*/
export function bottom(layout: Layout): number {
let max = 0, bottomY;
for (let i = 0, len = layout.length; i < len; i++) {
bottomY = layout[i].y + layout[i].h;
if (bottomY > max) max = bottomY;
}
return max;
}
export function cloneLayout(layout: Layout): Layout {
const newLayout = Array(layout.length);
for (let i = 0, len = layout.length; i < len; i++) {
newLayout[i] = cloneLayoutItem(layout[i]);
}
return newLayout;
}
// Fast path to cloning, since this is monomorphic
export 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
isDraggable: layoutItem.isDraggable, isResizable: layoutItem.isResizable
};
}
/**
* Comparing React `children` is a bit difficult. This is a good way to compare them.
* This will catch differences in keys, order, and length.
*/
export function childrenEqual(a: ReactChildren, b: ReactChildren): boolean {
// $FlowIgnore: Appears to think map calls back w/array
return isEqual(React.Children.map(a, (c) => c.key), React.Children.map(b, (c) => c.key));
}
/**
* Given two layoutitems, check if they collide.
*/
export function collides(l1: LayoutItem, l2: LayoutItem): boolean {
if (l1 === l2) 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.
*
* @param {Array} layout Layout.
* @param {Boolean} verticalCompact Whether or not to compact the layout
* vertically.
* @return {Array} Compacted Layout.
*/
export function compact(layout: Layout, compactType: CompactType, cols: number): Layout {
// Statics go in the compareWith array right away so items flow around them.
const compareWith = getStatics(layout);
// We go through the items by row and column.
const sorted = sortLayoutItems(layout, compactType);
// Holding for new items.
const out = Array(layout.length);
for (let i = 0, len = sorted.length; i < len; i++) {
let l = cloneLayoutItem(sorted[i]);
// Don't move static elements
if (!l.static) {
l = compactItem(compareWith, l, compactType, cols);
// 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;
}
/**
* Compact an item in the layout.
*/
export function compactItem(compareWith: Layout, l: LayoutItem, compactType: CompactType, cols: number): LayoutItem {
const compactV = compactType === 'vertical';
const 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) {
l.y = Math.min(bottom(compareWith), l.y);
// 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.
let collides;
while((collides = getFirstCollision(compareWith, l))) {
if (compactH) {
l.x = collides.x + collides.w;
} else {
l.y = collides.y + collides.h;
}
// 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.
*
* @param {Array} layout Layout array.
* @param {Number} bounds Number of columns.
*/
export function correctBounds(layout: Layout, bounds: {cols: number}): Layout {
const collidesWith = getStatics(layout);
for (let i = 0, len = layout.length; i < len; i++) {
const 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.
*/
export function getLayoutItem(layout: Layout, id: string): ?LayoutItem {
for (let 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.
*/
export function getFirstCollision(layout: Layout, layoutItem: LayoutItem): ?LayoutItem {
for (let i = 0, len = layout.length; i < len; i++) {
if (collides(layout[i], layoutItem)) return layout[i];
}
}
export function getAllCollisions(layout: Layout, layoutItem: LayoutItem): Array<LayoutItem> {
return layout.filter((l) => collides(l, layoutItem));
}
/**
* Get all static elements.
* @param {Array} layout Array of layout objects.
* @return {Array} Array of static layout items..
*/
export function getStatics(layout: Layout): Array<LayoutItem> {
return layout.filter((l) => l.static);
}
/**
* Move an element. Responsible for doing cascading movements of other elements.
*
* @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.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is
* being dragged/resized by the user.
*/
export function moveElement(layout: Layout, l: LayoutItem, x: ?number, y: ?number,
isUserAction: ?boolean, preventCollision: ?boolean,
compactType: CompactType, cols: number): Layout {
if (l.static) return layout;
// Short-circuit if nothing to do.
if (l.y === y && l.x === x) return layout;
const oldX = l.x;
const oldY = l.y;
const movingUp = y && l.y > 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.
let sorted = sortLayoutItems(layout, compactType);
if (movingUp) sorted = sorted.reverse();
const collisions = getAllCollisions(sorted, l);
// There was a collision; abort
if (preventCollision && collisions.length) {
l.x = oldX;
l.y = oldY;
l.moved = false;
return layout;
}
// Move each item that collides away from this element.
for (let i = 0, len = collisions.length; i < len; i++) {
const collision = collisions[i];
// console.log('resolving collision between', l.i, 'at', l.y, 'and', collision.i, 'at', collision.y);
// Short circuit so we can't infinite loop
if (collision.moved) continue;
// This makes it feel a bit more precise by waiting to swap for just a bit when moving up.
if (l.y > collision.y && l.y - collision.y > collision.h / 4) continue;
if (l.x > collision.x && l.x - collision.x > collision.w / 4) 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.
* @param {Boolean} [isUserAction] If true, designates that the item we're moving is being dragged/resized
* by the user.
*/
export function moveElementAwayFromCollision(layout: Layout, collidesWith: LayoutItem, itemToMove: LayoutItem,
isUserAction: ?boolean, compactType: CompactType, cols: number): Layout {
const compactH = compactType === 'horizontal';
const compactV = compactType === 'vertical';
const preventCollision = false; // we're already colliding
// 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) {
// Make a mock item so we don't modify the item here, only modify in moveElement.
const fakeItem: LayoutItem = {
x: compactH ? Math.max(collidesWith.x - itemToMove.w, 0) : itemToMove.x,
y: !compactH ? Math.max(collidesWith.y - itemToMove.h, 0) : itemToMove.y,
w: itemToMove.w,
h: itemToMove.h,
i: '-1'
};
if (!getFirstCollision(layout, fakeItem)) {
return moveElement(
layout,
itemToMove,
compactH ? fakeItem.x : undefined,
compactV ? fakeItem.y + 1 : undefined,
isUserAction,
preventCollision,
compactType,
cols
);
}
}
// Previously this was optimized to move below the collision directly, but this can cause problems
// with cascading moves, as an item may actually leapflog a collision and cause a reversal in order.
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.
*/
export function perc(num: number): string {
return num * 100 + '%';
}
export function setTransform({top, left, width, height}: Position): Object {
// Replace unitless items with px
const translate = `translate(${left}px,${top}px)`;
return {
transform: translate,
WebkitTransform: translate,
MozTransform: translate,
msTransform: translate,
OTransform: translate,
width: `${width}px`,
height: `${height}px`,
position: 'absolute'
};
}
export function setTopLeft({top, left, width, height}: Position): Object {
return {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${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.
*/
export function sortLayoutItems(layout: Layout, compactType: CompactType): Layout {
if (compactType === 'horizontal') return sortLayoutItemsByColRow(layout);
else return sortLayoutItemsByRowCol(layout);
}
export function sortLayoutItemsByRowCol(layout: Layout): Layout {
return [].concat(layout).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;
});
}
export function sortLayoutItemsByColRow(layout: Layout): Layout {
return [].concat(layout).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.
*
* @param {Array} initialLayout Layout passed in through props.
* @param {String} breakpoint Current responsive breakpoint.
* @param {?String} compact Compaction option.
* @return {Array} Working layout.
*/
export function synchronizeLayoutWithChildren(initialLayout: Layout, children: ReactChildren,
cols: number, compactType: CompactType): Layout {
initialLayout = initialLayout || [];
// Generate one layout item per child.
let layout: Layout = [];
React.Children.forEach(children, (child: ReactElement<any>, i: number) => {
// Don't overwrite if it already exists.
const 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`.');
}
const 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');
}
layout[i] = cloneLayoutItem({...g, i: child.key});
} else {
// Nothing provided: ensure this is added to the bottom
layout[i] = cloneLayoutItem({w: 1, h: 1, x: 0, y: bottom(layout), i: String(child.key)});
}
}
});
// Correct the layout.
layout = correctBounds(layout, {cols: cols});
layout = compact(layout, compactType, cols);
return layout;
}
/**
* Validate a layout. Throws errors.
*
* @param {Array} layout Array of layout items.
* @param {String} [contextName] Context name for errors.
* @throw {Error} Validation error.
*/
export function validateLayout(layout: Layout, contextName: string): void {
contextName = contextName || "Layout";
const subProps = ['x', 'y', 'w', 'h'];
if (!Array.isArray(layout)) throw new Error(contextName + " must be an array!");
for (let i = 0, len = layout.length; i < len; i++) {
const item = layout[i];
for (let 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!');
}
}
}
// Flow can't really figure this out, so we just use Object
export function autoBindHandlers(el: Object, fns: Array<string>): void {
fns.forEach((key) => el[key] = el[key].bind(el));
}