react-grid-layout
Advanced tools
Comparing version 0.0.2 to 0.1.0
'use strict'; | ||
var React = require('react/addons'); | ||
var utils = require('./utils'); | ||
var Draggable = require('react-draggable'); | ||
var Resizable = require('react-resizable').Resizable; | ||
@@ -32,2 +34,4 @@ var GridItem = module.exports = React.createClass({ | ||
onDrag: React.PropTypes.func, | ||
onResizeStop: React.PropTypes.func, | ||
onResizeStart: React.PropTypes.func, | ||
onResize: React.PropTypes.func, | ||
@@ -53,2 +57,9 @@ | ||
getInitialState() { | ||
return { | ||
resizing: false, | ||
className: '' | ||
}; | ||
}, | ||
/** | ||
@@ -66,5 +77,9 @@ * Return position on the page given an x, y, w, h. | ||
top: p.rowHeight * p.y + p.margin[1], | ||
width: (width * p.w / p.cols) - ((p.w - 1) * p.margin[0]), | ||
width: width * (p.w / p.cols) - p.margin[0], | ||
height: p.h * p.rowHeight - p.margin[1] | ||
}; | ||
if (this.state.resizing) { | ||
out.width = this.state.resizing.width; | ||
out.height = this.state.resizing.height; | ||
} | ||
return out; | ||
@@ -89,11 +104,40 @@ }, | ||
/** | ||
* Given a resize handle, figure out a new w and h for this element. | ||
* @param {DOMElement} element DOM Element (resize handle) | ||
* @return {Object} w and h. | ||
*/ | ||
calcWH(element) { | ||
calcWH({height, width}) { | ||
var w = Math.round((width / this.props.containerWidth) * this.props.cols); | ||
var h = Math.round(height / this.props.rowHeight); | ||
w = Math.max(Math.min(w, this.props.cols - this.props.x), 0); | ||
h = Math.max(h, 0); | ||
return {w, h}; | ||
}, | ||
mixinDraggable(child, position) { | ||
return ( | ||
<Draggable | ||
start={{x: position.left, y: position.top}} | ||
moveOnStartChange={this.props.moveOnStartChange} | ||
onStop={this.onDragHandler('onDragStop')} | ||
onStart={this.onDragHandler('onDragStart')} | ||
onDrag={this.onDragHandler('onDrag')} | ||
handle={this.props.handle} | ||
cancel=".react-resizable-handle" | ||
> | ||
{child} | ||
</Draggable> | ||
); | ||
}, | ||
mixinResizable(child, position) { | ||
return ( | ||
<Resizable | ||
width={position.width} | ||
height={position.height} | ||
onResizeStop={this.onResizeHandler('onResizeStop')} | ||
onResizeStart={this.onResizeHandler('onResizeStart')} | ||
onResize={this.onResizeHandler('onResize')} | ||
> | ||
{child} | ||
</Resizable> | ||
); | ||
}, | ||
/** | ||
@@ -107,3 +151,3 @@ * Wrapper around drag events to provide more useful data. | ||
*/ | ||
dragHandler(handlerName) { | ||
onDragHandler(handlerName) { | ||
var me = this; | ||
@@ -125,4 +169,4 @@ return function(e, {element, position}) { | ||
/** | ||
* Wrapper around resize events to provide more useful data. | ||
* All resize events call the function with the given handler name, | ||
* 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). | ||
@@ -133,9 +177,10 @@ * | ||
*/ | ||
resizeHandler(handlerName, element) { | ||
onResizeHandler(handlerName) { | ||
var me = this; | ||
return function(e, {element, position}) { | ||
return function(e, {element, size}) { | ||
if (!me.props[handlerName]) return; | ||
// Get new WH | ||
var {w, h} = me.calcWH(element); | ||
// Get new XY | ||
var {w, h} = me.calcWH(size); | ||
// Cap w at numCols | ||
@@ -146,2 +191,4 @@ if (w + me.props.x > me.props.cols) { | ||
me.setState({resizing: handlerName === 'onResizeStop' ? null : size}); | ||
me.props[handlerName](me.props.i, w, h); | ||
@@ -152,12 +199,12 @@ }; | ||
render() { | ||
var child = React.Children.only(this.props.children); | ||
var p = this.props, pos = this.calcPosition(); | ||
var p = this.props; | ||
var {left, top, width, height} = this.calcPosition(); | ||
child = React.addons.cloneWithProps(child, { | ||
var child = React.addons.cloneWithProps(React.Children.only(this.props.children), { | ||
// Munge a classname. Use passed in classnames, child classnames, and resizing. | ||
className: ['react-grid-item', this.props.children.props.className || '', this.props.className, | ||
this.state.resizing ? 'resizing' : ''].join(' '), | ||
// We can set the width and height on the child, but unfortunately we can't set the position. | ||
style: { | ||
width: width + 'px', | ||
height: height + 'px', | ||
width: pos.width + 'px', | ||
height: pos.height + 'px', | ||
position: 'absolute' | ||
@@ -170,54 +217,22 @@ } | ||
if (!this.isMounted()) { | ||
child.props.style.left = perc(this.props.x / p.containerWidth); | ||
child.props.style.width = perc(this.props.width / p.containerWidth); | ||
pos.left = utils.perc(pos.left / p.containerWidth); | ||
child.props.style.width = utils.perc(pos.width / p.containerWidth); | ||
} | ||
child.props.className = 'react-grid-item ' + (child.props.className || "") + " " + this.props.className; | ||
// Resizable support. This is usually on but the user can toggle it off. | ||
if (this.props.isResizable && this.isMounted()) { | ||
child.props.children = [ | ||
child.props.children, | ||
<Draggable | ||
start={{x: width - 20 + 'px', y: height - 20 + 'px'}} | ||
moveOnStartChange={true} | ||
onStop={this.resizeHandler('onResizeStop')} | ||
onStart={this.resizeHandler('onResizeStart')} | ||
onDrag={this.resizeHandler('onResize')} | ||
> | ||
<span className="react-grid-resize-handle">⌟</span> | ||
</Draggable> | ||
]; | ||
child = this.mixinResizable(child, pos); | ||
} | ||
// Draggable support. This is always on, except for with placeholders. | ||
if (this.props.isDraggable) { | ||
return ( | ||
<Draggable | ||
start={{x: left, y: top}} | ||
moveOnStartChange={this.props.moveOnStartChange} | ||
onStop={this.dragHandler('onDragStop')} | ||
onStart={this.dragHandler('onDragStart')} | ||
onDrag={this.dragHandler('onDrag')} | ||
handle={this.props.handle} | ||
cancel=".react-grid-resize-handle" | ||
> | ||
{child} | ||
</Draggable> | ||
); | ||
} else { | ||
child.props.style.left = left, child.props.style.top = top; | ||
return child; | ||
child = this.mixinDraggable(child, pos); | ||
} | ||
// Place the element directly if draggability is turned off. | ||
else { | ||
child.props.style.left = pos.left, child.props.style.top = pos.top; | ||
} | ||
return child; | ||
} | ||
}); | ||
/** | ||
* 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 + '%'; | ||
} |
@@ -5,2 +5,3 @@ 'use strict'; | ||
var GridItem = require('./GridItem.jsx'); | ||
var utils = require('./utils'); | ||
@@ -12,3 +13,3 @@ var ReactGridLayout = module.exports = React.createClass({ | ||
propTypes: { | ||
// If true, the container swells and contracts to fit contents | ||
// If true, the container height swells and contracts to fit contents | ||
autoSize: React.PropTypes.bool, | ||
@@ -29,3 +30,10 @@ // {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480} | ||
// Rows have a static height, but you can change this based on breakpoints if you like | ||
rowHeight: React.PropTypes.number | ||
rowHeight: React.PropTypes.number, | ||
// Flags | ||
isDraggable: React.PropTypes.bool, | ||
isResizable: React.PropTypes.bool, | ||
// Callback so you can save the layout | ||
onLayoutChange: React.PropTypes.func | ||
}, | ||
@@ -40,3 +48,6 @@ | ||
initialWidth: 1280, | ||
margin: [10, 10] | ||
margin: [10, 10], | ||
isDraggable: true, | ||
isResizable: true, | ||
onLayoutChange: function(){} | ||
}; | ||
@@ -55,10 +66,17 @@ }, | ||
componentDidMount() { | ||
window.addEventListener('resize', this.onResize); | ||
this.onResize(); | ||
window.addEventListener('resize', this.onWindowResize); | ||
this.onWindowResize(); | ||
}, | ||
componentWillUnmount() { | ||
window.removeEventListener('resize', this.onResize); | ||
window.removeEventListener('resize', this.onWindowResize); | ||
}, | ||
componentDidUpdate(prevProps, prevState) { | ||
// Call back so we can store the layout | ||
if (this.state.layout !== prevState.layout) { | ||
this.props.onLayoutChange(this.state.layout); | ||
} | ||
}, | ||
/** | ||
@@ -92,3 +110,5 @@ * Calculates a pixel value for the container. | ||
} | ||
return compact(layout); | ||
layout = utils.correctBounds(layout, {w: this.props.cols}); | ||
return utils.compact(layout); | ||
}, | ||
@@ -106,3 +126,3 @@ | ||
*/ | ||
onResize() { | ||
onWindowResize() { | ||
// Set breakpoint | ||
@@ -119,3 +139,3 @@ var width = this.getDOMNode().offsetWidth; | ||
var layout = this.state.layout; | ||
var l = getLayoutItem(layout, i); | ||
var l = utils.getLayoutItem(layout, i); | ||
@@ -128,6 +148,6 @@ // Create drag element (display only) | ||
// Move the element to the dragged location. | ||
layout = moveElement(layout, l, x, y); | ||
layout = utils.moveElement(layout, l, x, y); | ||
this.setState({ | ||
layout: compact(layout), | ||
layout: utils.compact(layout), | ||
activeDrag: activeDrag | ||
@@ -144,10 +164,30 @@ }); | ||
var layout = this.state.layout; | ||
var l = getLayoutItem(layout, i); | ||
var l = utils.getLayoutItem(layout, i); | ||
// Move the element here | ||
layout = moveElement(layout, l, x, y); | ||
layout = utils.moveElement(layout, l, x, y); | ||
// Set state | ||
this.setState({layout: compact(layout), activeDrag: null}); | ||
this.setState({layout: utils.compact(layout), activeDrag: null}); | ||
}, | ||
onResize(i, w, h) { | ||
var layout = this.state.layout; | ||
var l = utils.getLayoutItem(layout, i); | ||
// Create drag element (display only) | ||
var activeDrag = { | ||
w: w, h: h, x: l.x, y: l.y, placeholder: true, i: i | ||
}; | ||
l.w = w; | ||
l.h = h; | ||
// Move the element to the dragged location. | ||
// layout = utils.moveElement(layout, l, x, y); | ||
this.setState({layout: utils.compact(layout), activeDrag: activeDrag}); | ||
}, | ||
onResizeStop(e, {element, position}) { | ||
this.setState({activeDrag: null}); | ||
}, | ||
/** | ||
@@ -158,3 +198,3 @@ * Create a placeholder object. | ||
placeholder() { | ||
if (!this.state.activeDrag) return null; | ||
if (!this.state.activeDrag) return ''; | ||
@@ -184,3 +224,3 @@ return ( | ||
processGridItem(child, i) { | ||
var l = getLayoutItem(this.state.layout, i); | ||
var l = utils.getLayoutItem(this.state.layout, i); | ||
@@ -202,3 +242,8 @@ // watchStart property tells Draggable to react to changes in the start param | ||
onDragStart={this.onDragStart} | ||
onDrag={this.onDrag}> | ||
onDrag={this.onDrag} | ||
onResize={this.onResize} | ||
onResizeStop={this.onResizeStop} | ||
isDraggable={this.props.isDraggable} | ||
isResizable={this.props.isResizable} | ||
> | ||
{child} | ||
@@ -212,3 +257,3 @@ </GridItem> | ||
var {className, initialLayout, ...props} = this.props; | ||
className = 'react-grid-layout ' + (className || ''); | ||
className = 'react-grid-layout ' + (className || '') + ' ' + this.state.className; | ||
@@ -224,139 +269,1 @@ return ( | ||
/** | ||
* Given two layouts, check if they collide. | ||
* @param {Object} l1 Layout object. | ||
* @param {Object} l2 Layout object. | ||
* @return {Boolean} True if colliding. | ||
*/ | ||
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. | ||
* @return {Array} Compacted Layout. | ||
*/ | ||
function compact(layout) { | ||
// We go through the items by row and column. | ||
var sorted = getLayoutItemsByRowCol(layout); | ||
var out = _.map(getLayoutItemsByRowCol(layout), function(l, i) { | ||
// Only collide with elements before this one. | ||
var ls = sorted.slice(0, i); | ||
// Move the element up as far as it can go without colliding. | ||
do { | ||
l.y--; | ||
} | ||
while (l.y > -1 && !layoutItemCollidesWith(ls, l).length); | ||
// Move it down, and keep moving it down if it's colliding. | ||
do { | ||
l.y++; | ||
} while(layoutItemCollidesWith(ls, l).length); | ||
delete l.moved; | ||
return l; | ||
}); | ||
return _.sortBy(out, 'i'); | ||
} | ||
/** | ||
* Get a layout item by index. Used so we can override later on if necessary. | ||
* | ||
* @param {Array} layout Layout array. | ||
* @param {Number} i Index | ||
* @return {LayoutItem} Item at index. | ||
*/ | ||
function getLayoutItem(layout, i) { | ||
return layout[i]; | ||
} | ||
/** | ||
* Get layout items sorted from top left to right and down. | ||
* @return {Array} Array of layout objects. | ||
*/ | ||
function getLayoutItemsByRowCol(layout) { | ||
return [].concat(layout).sort(function(a, b) { | ||
if (a.y > b.y || (a.y === b.y && a.x > b.x)) { | ||
return 1; | ||
} | ||
return -1; | ||
}); | ||
} | ||
/** | ||
* Get layout items sorted from top left to down. | ||
* @return {Array} Array of layout objects. | ||
*/ | ||
function getLayoutItemsByColRow(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; | ||
}); | ||
} | ||
/** | ||
* Returns an array of items this layout item collides with. | ||
* @param {Object} layoutItem Layout item. | ||
* @return {Array} Array of colliding layout objects. | ||
*/ | ||
function layoutItemCollidesWith(layout, layoutItem) { | ||
return _.filter(layout, collides.bind(null, layoutItem)); | ||
} | ||
/** | ||
* Move / resize 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 {Number} [w] Width in grid units. | ||
* @param {Number} [h] Height in grid units. | ||
*/ | ||
function moveElement(layout, l, 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 | ||
_.extend(l, _.pick({x: x, y: y, w: w, h: h, moved: 1}, _.isNumber)); | ||
// Get all items this box collides with. | ||
var collisions = layoutItemCollidesWith(layout, l); | ||
// Move each item that collides away from this element. | ||
_.each(collisions, function(coll) { | ||
if (coll.moved) return; // short circuit so we don't re-move items | ||
layout = moveElementAwayFromCollision(layout, l, coll); | ||
}); | ||
return layout; | ||
} | ||
/** | ||
* This is where the magic needs to happen - given a collision, move an element away from the collision. | ||
* It's okay to cascade movements here, but be careful to not have a move b move c move a. | ||
* @param {Array} layout Full layout to modify. | ||
* @param {LayoutItem} collidesWith Layout item we're colliding with. | ||
* @param {LayoutItem} itemToMove Layout item we're moving. | ||
*/ | ||
function moveElementAwayFromCollision(layout, collidesWith, itemToMove) { | ||
var fakeItem = _.extend({}, itemToMove, {y: 0}); | ||
var sorted = getLayoutItemsByRowCol(layout); | ||
var itemsBefore = sorted.slice(0, sorted.indexOf(itemToMove)).concat(collidesWith); | ||
// While the item collides with any of the items before it, move it down. | ||
var collisions; | ||
do { | ||
collisions = layoutItemCollidesWith(itemsBefore, fakeItem); | ||
if (collisions.length) fakeItem.y = collisions[0].y + collisions[0].h; | ||
} while(collisions.length); | ||
return moveElement(layout, itemToMove, undefined, fakeItem.y); | ||
} |
{ | ||
"name": "react-grid-layout", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"description": "A draggable and resizable grid layout with responsive breakpoints, for React.", | ||
@@ -9,3 +9,3 @@ "main": "index.js", | ||
"build": "./node_modules/.bin/webpack", | ||
"dev-server": "echo 'Open http://localhost:4002/examples/1.html'; webpack-dev-server --config webpack-dev-server.config.js --hot --progress --colors --port 4002 --content-base ." | ||
"dev-server": "echo 'Open http://localhost:4002'; webpack-dev-server --config webpack-dev-server.config.js --hot --progress --colors --port 4002 --content-base ." | ||
}, | ||
@@ -32,2 +32,7 @@ "repository": { | ||
"homepage": "https://github.com/STRML/react-grid-layout", | ||
"dependencies": { | ||
"lodash": "^2.4.1", | ||
"react-draggable": "strml/react-draggable", | ||
"react-resizable": "^0.0.3" | ||
}, | ||
"devDependencies": { | ||
@@ -34,0 +39,0 @@ "css-loader": "^0.9.0", |
### React-Grid-Layout | ||
[View the Demo](https://strml.github.io/react-grid-layout/examples/1.html) | ||
[View the Demo](https://strml.github.io/react-grid-layout/examples/1-basic.html) | ||
@@ -19,2 +19,7 @@ React-Grid-Layout is a grid layout system much like [Packery](http://packery.metafizzy.co/) or | ||
#### Demos | ||
[1. Basic](https://strml.github.io/react-grid-layout/examples/1-basic.html) | ||
[1. No Dragging/Resizing (Layout Only)](https://strml.github.io/react-grid-layout/examples/2-no-dragging.html) | ||
---- | ||
@@ -29,3 +34,3 @@ | ||
- [x] Live grid packing while dragging | ||
- [x] Resizable grid items | ||
- [ ] Define grid attributes on children themselves (`_grid` key) | ||
- [ ] Resizable grid items |
@@ -6,3 +6,4 @@ 'use strict'; | ||
var contentDiv = document.getElementById('content'); | ||
React.render(React.createElement(Layout), contentDiv); | ||
var gridProps = window.gridProps || {}; | ||
React.render(React.createElement(Layout, gridProps), contentDiv); | ||
}); |
@@ -8,2 +8,4 @@ 'use strict'; | ||
require('style!css!../css/styles.css'); | ||
require('style!css!../examples/example-styles.css'); | ||
require('style!css!../node_modules/react-resizable/css/styles.css'); | ||
@@ -13,4 +15,18 @@ var TestLayout = module.exports = React.createClass({ | ||
generate() { | ||
return _.map(_.range(12), function(i) { | ||
getDefaultProps() { | ||
return { | ||
items: 12 | ||
}; | ||
}, | ||
getInitialState() { | ||
var layout = this.props.layout || this.generateLayout(); | ||
return { | ||
layout: layout, | ||
initialLayout: layout | ||
}; | ||
}, | ||
generateDOM() { | ||
return _.map(_.range(this.props.items), function(i) { | ||
return (<div key={i}><span className="text">{i}</span></div>); | ||
@@ -20,14 +36,39 @@ }); | ||
generateLayout() { | ||
var p = this.props; | ||
return _.map(new Array(p.items), function(item, i) { | ||
var w = _.result(p, 'w') || Math.ceil(Math.random() * 4); | ||
var y = _.result(p, 'y') || Math.ceil(Math.random() * 4) + 1; | ||
return {x: i * 2 % 12, y: Math.floor(i / 6) * y, w: w, h: y, i: i}; | ||
}); | ||
}, | ||
onLayoutChange: function(layout) { | ||
console.log(layout); | ||
this.setState({layout: layout}); | ||
}, | ||
stringifyLayout() { | ||
return _.map(this.state.layout, function(l) { | ||
return <div className="layoutItem"><b>{l.i}</b>: [{l.x}, {l.y}, {l.w}, {l.h}]</div>; | ||
}); | ||
}, | ||
render() { | ||
var items = this.generate(); | ||
var layout = _.map(items, function(item, i) { | ||
var y = Math.ceil(Math.random() * 8) + 1; | ||
return {x: i * 2 % 12, y: Math.floor(i / 6) * y, w: 2, h: y}; | ||
}); | ||
var {layout, ...gridProps} = this.props; | ||
return ( | ||
<ReactGridLayout className="layout" initialLayout={layout} cols={12} rowHeight={30}> | ||
{this.generate()} | ||
</ReactGridLayout> | ||
<div> | ||
<div className="layoutJSON"> | ||
Displayed as <code>[x, y, w, h]</code>: | ||
<div className="columns"> | ||
{this.stringifyLayout()} | ||
</div> | ||
</div> | ||
<ReactGridLayout className="layout" initialLayout={this.state.initialLayout} cols={12} onLayoutChange={this.onLayoutChange} | ||
rowHeight={30} {...gridProps}> | ||
{this.generateDOM()} | ||
</ReactGridLayout> | ||
</div> | ||
); | ||
} | ||
}); |
@@ -21,2 +21,3 @@ module.exports = { | ||
devtool: "#inline-source-map", | ||
publicPath: '/examples/', | ||
resolve: { | ||
@@ -23,0 +24,0 @@ extensions: ["", ".webpack.js", ".web.js", ".js", ".jsx"] |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
GitHub dependency
Supply chain riskContains a dependency which resolves to a GitHub URL. Dependencies fetched from GitHub specifiers are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29182
17
759
35
3
1
1
+ Addedlodash@^2.4.1
+ Addedreact-resizable@^0.0.3
+ Addedlodash@2.4.2(transitive)
+ Addedobject-keys@1.0.12(transitive)
+ Addedobject.assign@1.1.1(transitive)
+ Addedreact-resizable@0.0.3(transitive)