|
| 1 | +'use strict'; |
| 2 | +var React = require('react/addons'); |
| 3 | +var _ = require('lodash'); |
| 4 | +var Draggable = require('react-draggable'); |
| 5 | + |
| 6 | +var ReactGridLayout = module.exports = React.createClass({ |
| 7 | + displayName: 'ReactGridLayout', |
| 8 | + mixins: [React.addons.PureRenderMixin], |
| 9 | + |
| 10 | + propTypes: { |
| 11 | + // Layout is an array of object with the format: |
| 12 | + // {x: Number, y: Number, w: Number, h: Number} |
| 13 | + initialLayout: React.PropTypes.array, |
| 14 | + bounds: React.PropTypes.array, |
| 15 | + margin: React.PropTypes.array, |
| 16 | + // {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480} |
| 17 | + breakpoints: React.PropTypes.object |
| 18 | + }, |
| 19 | + |
| 20 | + componentDidMount() { |
| 21 | + window.addEventListener('resize', this.onResize); |
| 22 | + this.onResize(); |
| 23 | + }, |
| 24 | + |
| 25 | + componentWillUnmount() { |
| 26 | + window.removeEventListener('resize', this.onResize); |
| 27 | + }, |
| 28 | + |
| 29 | + onResize() { |
| 30 | + // Set breakpoint |
| 31 | + var width = this.refs.layout.getDOMNode().offsetWidth; |
| 32 | + var breakpoint = _(this.props.breakpoints) |
| 33 | + .pairs() |
| 34 | + .sortBy(function(val) { return -val[1];}) |
| 35 | + .find(function(val) {return width > val[1];})[0]; |
| 36 | + |
| 37 | + this.setState({width: width, lastWidth: this.state.width, breakpoint: breakpoint}); |
| 38 | + }, |
| 39 | + |
| 40 | + getDefaultProps() { |
| 41 | + return { |
| 42 | + cols: 10, // # of cols, rows |
| 43 | + rowHeight: 150, // Rows have a static height, but you can change this based on breakpoints if you like |
| 44 | + initialWidth: 1280, // This allows setting this on the server side |
| 45 | + margin: [10, 10], // margin between items (x, y) in px |
| 46 | + initialBreakpoint: 'lg', |
| 47 | + breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0} |
| 48 | + }; |
| 49 | + }, |
| 50 | + |
| 51 | + getInitialState() { |
| 52 | + return { |
| 53 | + layout: this.generateLayout(this.props.initialLayout), |
| 54 | + breakpoint: this.props.initialBreakpoint, |
| 55 | + width: this.props.initialWidth, |
| 56 | + // Fills it full of zeroes |
| 57 | + dragOffsets: _.range(0, this.props.children.length, 0) |
| 58 | + }; |
| 59 | + }, |
| 60 | + |
| 61 | + /** |
| 62 | + * Get the absolute position of a child by index. This does not include drag offsets or window resizing. |
| 63 | + * @param {Number} i Index of the child. |
| 64 | + * @return {Object} X and Y coordinates, in px. |
| 65 | + */ |
| 66 | + getSimpleAbsolutePosition(i) { |
| 67 | + var s = this.state, p = this.props; |
| 68 | + return { |
| 69 | + x: (s.layout[i].x / p.cols) * s.width, |
| 70 | + y: s.layout[i].y * p.rowHeight |
| 71 | + }; |
| 72 | + }, |
| 73 | + |
| 74 | + /** |
| 75 | + * Generate a layout using the initialLayout as a template. |
| 76 | + * Missing entries will be added, extraneous ones will be truncated. |
| 77 | + * @param {Array} initialLayout Layout passed in through props. |
| 78 | + * @return {Array} Working layout. |
| 79 | + */ |
| 80 | + generateLayout(initialLayout) { |
| 81 | + var layout = [].concat(initialLayout || []); |
| 82 | + if (layout.length !== this.props.children.length) { |
| 83 | + // Fill in the blanks |
| 84 | + } |
| 85 | + return layout; |
| 86 | + }, |
| 87 | + |
| 88 | + perc(num) { |
| 89 | + return num * 100 + '%'; |
| 90 | + }, |
| 91 | + |
| 92 | + processGridItem(child, i) { |
| 93 | + var l = this.state.layout[i]; |
| 94 | + var cols = this.props.cols; |
| 95 | + |
| 96 | + // We can set the width and height on the child, but unfortunately we can't set the position |
| 97 | + child.props.style = { |
| 98 | + width: this.perc(l.w / cols), |
| 99 | + height: l.h * this.props.rowHeight + 'px', |
| 100 | + position: 'absolute' |
| 101 | + }; |
| 102 | + |
| 103 | + // We calculate the x and y every pass, even though it's only actually used the first time. |
| 104 | + var x = this.state.width * (l.x / cols); |
| 105 | + var y = this.props.rowHeight * l.y; |
| 106 | + |
| 107 | + // If the width has changed, we need to change the x position. |
| 108 | + if (this.state.width !== this.props.initialWidth) { |
| 109 | + // This is what the x position would be without resizing. |
| 110 | + var originalX = this.props.initialWidth * (l.x / cols); |
| 111 | + |
| 112 | + // If the item has been dragged, we need to take that into account. |
| 113 | + var widthMult = this.state.width / this.props.initialWidth; |
| 114 | + x += (this.state.dragOffsets[i] * widthMult); |
| 115 | + originalX += this.state.dragOffsets[i]; |
| 116 | + |
| 117 | + // Margin the child over by the difference. Draggable doesn't mess with the margin so this is |
| 118 | + // safe to set. |
| 119 | + child.props.style.marginLeft = x - originalX + 'px'; |
| 120 | + } |
| 121 | + |
| 122 | + return ( |
| 123 | + <Draggable |
| 124 | + grid={[25, 25]} |
| 125 | + start={{x: x, y: y}} |
| 126 | + onStop={this.onDragStop.bind(this, i)}> |
| 127 | + {child} |
| 128 | + </Draggable> |
| 129 | + ); |
| 130 | + }, |
| 131 | + |
| 132 | + /** |
| 133 | + * When dragging stops, record the new position of the element in dragOffsets. This is as an x offset |
| 134 | + * from its original position. |
| 135 | + * @param {Number} i Index of the child. |
| 136 | + * @param {Event} e DOM Event. |
| 137 | + */ |
| 138 | + onDragStop(i, e) { |
| 139 | + var widthMult = this.state.width / this.props.initialWidth; |
| 140 | + // Calculate the new position by using the existing left + marginLeft, and multiplying by the reciprocal |
| 141 | + // of the width difference (so a 50px move at 1/2 screen size = 100px) |
| 142 | + var newPosition = parseInt(e.target.style.left, 10) + parseInt(e.target.style.marginLeft, 10) * (1 / widthMult); |
| 143 | + // Calculate the offset - this is the new position minus the expected position. The offset needs to be |
| 144 | + // modulated by the width multiple |
| 145 | + var offset = (newPosition - this.getSimpleAbsolutePosition(i).x) * widthMult; |
| 146 | + |
| 147 | + // Make the change. We use the immutability helpers for this so we can do a simple shouldComponentUpdate |
| 148 | + var change = {}; |
| 149 | + change[i] = {$set: offset}; |
| 150 | + var offsets = React.addons.update(this.state.dragOffsets, change); |
| 151 | + this.setState({dragOffsets: offsets}); |
| 152 | + }, |
| 153 | + |
| 154 | + render() { |
| 155 | + var {className, initialLayout, ...props} = this.props; |
| 156 | + className = (className || "") + " reactGridLayout"; |
| 157 | + var children = React.Children.map(this.props.children, this.processGridItem); |
| 158 | + return ( |
| 159 | + <div {...props} className={className} style={{position: 'relative', height: '100%'}} ref="layout"> |
| 160 | + {children} |
| 161 | + </div> |
| 162 | + ); |
| 163 | + } |
| 164 | +}); |
0 commit comments