diff --git a/example.html b/example.html index a5cb1cbbe..32ff3f939 100644 --- a/example.html +++ b/example.html @@ -21,6 +21,8 @@ .placeholder { background: red; opacity: 0.3; + user-select: none; + z-index: 0; } .gridItem.react-draggable-dragging { transition: none; diff --git a/lib/ReactGridLayout.jsx b/lib/ReactGridLayout.jsx index 114ceb7fa..258db5794 100644 --- a/lib/ReactGridLayout.jsx +++ b/lib/ReactGridLayout.jsx @@ -17,6 +17,26 @@ var ReactGridLayout = module.exports = React.createClass({ breakpoints: React.PropTypes.object }, + getDefaultProps() { + return { + cols: 10, // # of cols, rows + rowHeight: 150, // Rows have a static height, but you can change this based on breakpoints if you like + initialWidth: 1280, // This allows setting this on the server side + margin: [10, 10], // margin between items (x, y) in px + initialBreakpoint: 'lg', + breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0} + }; + }, + + getInitialState() { + return { + layout: this.generateLayout(this.props.initialLayout), + breakpoint: this.props.initialBreakpoint, + width: this.props.initialWidth, + activeDrag: null + }; + }, + componentDidMount() { window.addEventListener('resize', this.onResize); this.onResize(); @@ -28,65 +48,41 @@ var ReactGridLayout = module.exports = React.createClass({ /** * Return position on the page given an x, y, w, h. - * x, y, w, h are all in pixels. + * left, top, width, height are all in pixels. * @param {Object} l Layout object. * @return {Object} Object containing coords. */ - calcPosition(l, usePercentage) { + calcPosition(l) { var cols = this.props.cols; var out = { - x: this.state.width * (l.x / cols), - y: this.props.rowHeight * l.y, + left: this.state.width * (l.x / cols), + top: this.props.rowHeight * l.y, width: (this.state.width * l.w / cols) - ((l.w - 1) * this.props.margin[0]) + 'px', height: l.h * this.props.rowHeight - this.props.margin[1] + 'px' }; // If we're not mounted yet, use percentages; otherwise items won't fit the window properly // because this.state.width hasn't actually been populated with a real value if (!this.isMounted()) { - out.x = this.perc(out.x / this.state.width); - out.width = this.perc(out.width / this.state.width); + out.left = perc(out.x / this.state.width); + out.width = perc(out.width / this.state.width); } return out; }, /** - * Given two layouts, check if they collide. - * @param {Object} l1 Layout object. - * @param {Object} l2 Layout object. - * @return {Boolean} True if colliding. + * Given an element, inspect its styles and generate new x,y coordinates. + * @param {DOMElement} element DOM Element. + * @return {Object} x and y coordinates. */ - collides(l1, l2) { - 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 - }, - - getDefaultProps() { - return { - cols: 10, // # of cols, rows - rowHeight: 150, // Rows have a static height, but you can change this based on breakpoints if you like - initialWidth: 1280, // This allows setting this on the server side - margin: [10, 10], // margin between items (x, y) in px - initialBreakpoint: 'lg', - breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0} - }; - }, + calcXY(element) { + var newX = parseInt(element.style.left, 10); + var newY = parseInt(element.style.top, 10); - getInitialState() { - return { - layout: this.generateLayout(this.props.initialLayout), - breakpoint: this.props.initialBreakpoint, - width: this.props.initialWidth, - // Fills it full of zeroes - dragOffsets: _.range(0, this.props.children.length, 0), - // TODO this should contain the x,y,w,h of a drag, if active, and render to an actual element - // that element should cause items around it to move - // this means that items should take up unoccupied space above themselves so that a cancelled drag doesn't - // cause an actual change - activeDrag: null - }; + var x = Math.round((newX / this.state.width) * this.props.cols); + var y = Math.round(newY / this.props.rowHeight); + x = Math.max(Math.min(x, this.props.cols), 0); + y = Math.max(y, 0); + return {x, y}; }, /** @@ -98,13 +94,13 @@ var ReactGridLayout = module.exports = React.createClass({ generateLayout(initialLayout) { var layout = [].concat(initialLayout || []); layout = layout.map(function(l, i) { - l.index = i; + l.i = i; return l; }); if (layout.length !== this.props.children.length) { // Fill in the blanks } - return layout; + return compact(layout); }, /** @@ -114,62 +110,16 @@ var ReactGridLayout = module.exports = React.createClass({ */ getSimpleAbsolutePosition(i) { var s = this.state, p = this.props; + var l = getLayoutItem(this.state.layout, i); return { - x: (s.layout[i].x / p.cols) * s.width, - y: s.layout[i].y * p.rowHeight + x: (l.x / p.cols) * s.width, + y: l.y * p.rowHeight }; }, /** - * Get layout items sorted from top right down. - * @return {Array} Array of layout objects. + * On window resize, work through breakpoints and reset state with the new width & breakpoint. */ - getLayoutsByRowCol(layouts) { - return _.sortBy(layouts || this.props.layout, function(a, b) { - if (a.y > b.y || a.y === b.y && a.x > b.x) { - return 1; - } - return -1; - }); - }, - - /** - * Returns an array of items this layout collides with. - * @param {Object} layoutItem Layout item. - * @return {Array} Array of colliding layout objects. - */ - layoutCollidesWith(layoutItem, layout) { - var sorted = this.getLayoutsByRowCol(_.without(layout, layoutItem)); - return _.filter(sorted, this.collides.bind(this, layoutItem)); - }, - - /** - * Move / resize an element. Responsible for doing cascading movements of other elements. - * @param {Array} layout Layout to modify. - * @param {Number} i Index of element. - * @param {Number} [x] X position in grid units. - * @param {Number} [y] Y position in grid units. - * @param {Number} [w] Width in grid units. - * @param {Number} [h] Height in grid units. - */ - moveElement(layout, i, x, y, w, h) { - // _.pick trickery removes undefined values from the object so we don't overwrite - // the object with attrs we didn't pass - var l = _.extend(layout[i], _.pick({x: x, y: y, w: w, h: h}, _.isNumber)); - layout[i] = l; - - var collisions = this.layoutCollidesWith(l, layout); - collisions = _.map(collisions, function(c) { return _.extend(c, {causedBy: l}); }); - - if (collisions.length) { - _.each(collisions, function(collision) { - this.moveElement(layout, collision.index, undefined, l.y + l.h); - }.bind(this)); - } - - return layout; - }, - onResize() { // Set breakpoint var width = this.getDOMNode().offsetWidth; @@ -178,24 +128,20 @@ var ReactGridLayout = module.exports = React.createClass({ .sortBy(function(val) { return -val[1];}) .find(function(val) {return width > val[1];})[0]; - this.setState({width: width, lastWidth: this.state.width, breakpoint: breakpoint}); + this.setState({width: width, breakpoint: breakpoint}); }, /** - * Helper to convert a number to a percentage string. - * @param {Number} num Any number - * @return {String} That number as a percentage. + * Create a placeholder object. + * @return {Element} Placeholder div. */ - perc(num) { - return num * 100 + '%'; - }, - placeholder() { if (!this.state.activeDrag) return null; - var {x, y, width, height} = this.calcPosition(this.state.activeDrag); + var {left, top, width, height} = this.calcPosition(this.state.activeDrag); return ( -
+ ); }, @@ -206,10 +152,9 @@ var ReactGridLayout = module.exports = React.createClass({ * @return {Element} Element wrapped in draggable and properly placed. */ processGridItem(child, i) { - var l = this.state.layout[i]; + var l = getLayoutItem(this.state.layout, i); - // We calculate the x and y every pass, even though it's only actually used the first time. - var {x, y, width, height} = this.calcPosition(l); + var {left, top, width, height} = this.calcPosition(l); // We can set the width and height on the child, but unfortunately we can't set the position child.props.style = { @@ -219,39 +164,47 @@ var ReactGridLayout = module.exports = React.createClass({ }; // watchStart property tells Draggable to react to changes in the start param + // Must be turned off on the item we're dragging as the changes in `activeDrag` cause rerenders + var drag = this.state.activeDrag; + var watchStart = drag && drag.i === i ? false : true; return (