Created
January 23, 2015 17:02
-
-
Save richardbutler/71395b3c985d02d4f9b1 to your computer and use it in GitHub Desktop.
React Layout Management Prototype
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global TimelineLite,TweenMax,Expo,Back,Ease,BezierEasing,_ */ | |
(function () { | |
'use strict'; | |
/** | |
* Returns the expected container width and height based on | |
* the child items that it contains. Should the container not | |
* have an explicit size, we need to calculate it. | |
*/ | |
function calculateContainerBounds (items, options, container) { | |
var padding = options.padding || 0; | |
var bounds = items.reduce(function (bounds, item) { | |
bounds.left = Math.max(bounds.left, item.left + item.width); | |
bounds.top = Math.max(bounds.top, item.top + item.height); | |
return bounds; | |
}, { | |
left: 0, | |
top: 0 | |
}); | |
return { | |
width: Math.max(container.width, bounds.left + (padding * 2)), | |
height: Math.max(container.height, bounds.top + (padding * 2)) | |
}; | |
} | |
/** | |
* A final process for the laid out items - add padding and | |
* such like that we don't need to complicate to initial | |
* layout. | |
*/ | |
function postProcessItems (items, options) { | |
var padding = options.padding || 0; | |
// Apply padding to items. | |
return items.map(function (item) { | |
return _.extend({}, item, { | |
left: item.left + padding, | |
top: item.top + padding | |
}); | |
}); | |
} | |
/** | |
* Generates (curries) a linear layout - abstraction to keep | |
* behaviour DRY between horizontal and vertical layouts. | |
*/ | |
function createLinearLayout (dimension, style) { | |
return function (options) { | |
var gap = options.gap || 0; | |
return function (items, container) { | |
var coordinateValue = 0; | |
var items = items.map(function (item, index) { | |
var itemValue = coordinateValue; | |
coordinateValue += item[dimension] + (gap || 0); | |
var item = _.extend(item, { | |
left: 0, | |
top: 0 | |
}); | |
item[style] = itemValue; | |
return item; | |
}); | |
return { | |
items: postProcessItems(items, options), | |
container: calculateContainerBounds(items, options, container) | |
}; | |
}; | |
}; | |
} | |
var HorizontalLayout = createLinearLayout('width', 'left'); | |
var VerticalLayout = createLinearLayout('height', 'top'); | |
function GridLayout (options) { | |
var padding = options.padding || 0; | |
var gap = options.gap || 0; | |
var itemSize = options.itemSize; | |
var variableRowHeight = options.variableRowHeight || false; | |
// Should we fill the width of the container with equal columns? | |
var fillWidth = options.fillWidth; | |
// Stretch the height of the items to the height of the largest item | |
// in its row? | |
var stretchHeight = options.stretchHeight || false; | |
return function (items, container) { | |
// Prefer an explicit width given to the container, via CSS or inline | |
// styles, but fall back to the measured size. | |
var containerWidth = container.width || container.measuredWidth; | |
// Total width minus any padding. | |
var availableWidth = containerWidth - (padding * 2); | |
var rows = (function () { | |
// X-coordinate cursor | |
var left = 0; | |
// The items in the current row - used to find the size of the | |
// largest item when this row has finished being laid out. | |
var currentRow = []; | |
return items.reduce(function (rows, item, index) { | |
// Get the right-hand bound for this item. | |
var right = left + item.width; | |
// If we're outside the width of the container, or the last item, | |
// move to the next row. | |
if (right > availableWidth) { | |
left = 0; | |
// Begin a new row. | |
currentRow = [item]; | |
// Add the new row. | |
rows.push(currentRow); | |
} else { | |
// Append to the current row. | |
currentRow.push(item); | |
} | |
// Increment the x-coordinate. | |
left += item.width + gap; | |
return rows; | |
}, [currentRow]); | |
})(); | |
items = (function () { | |
// Y-coordinate cursor | |
var top = 0; | |
// If we're using fillWidth, we need to store a static width. | |
var itemWidth; | |
// Standard height for each item, if variableRowHeight not used. | |
var itemHeight; | |
// If we don't want a variable row height, store the height of the | |
// largest item in the set. | |
if (!variableRowHeight) { | |
itemHeight = items.reduce(function (itemHeight, item) { | |
return Math.max(itemHeight, item.height); | |
}, 0); | |
} | |
return _(rows) | |
.map(function (currentRow, rowIndex) { | |
// X-coordinate cursor | |
var left = 0; | |
// Use the fixed height, or pick the tallest item in the row... | |
var rowHeight = itemHeight || _.pluck(currentRow, 'height').sort().pop(); | |
// If we're filling the width, we need to work out the equal | |
// item width and apply it to all. Unfortunately we need to | |
// do this here, after we've assigned the items to currentRow. | |
// | |
// We can remove the !itemWidth check if we want to not use a | |
// constant width, but vary by column - i.e. a single item at | |
// the end of the list, for example, will span the entire width | |
// of the container. Maybe an additional option. | |
if (fillWidth && !itemWidth) { | |
itemWidth = (availableWidth - (gap * (currentRow.length - 1))) / currentRow.length; | |
} | |
currentRow = currentRow.map(function (item) { | |
// If we're using stretchWidth, manually set the width | |
// of each item. | |
if (itemWidth) { | |
item.width = itemWidth; | |
} | |
// If we're using stretchHeight, we have to extend the | |
// height of all items to that of the standard. | |
if (stretchHeight) { | |
item.height = rowHeight; | |
} | |
item = _.extend(item, { | |
left: left, | |
top: top | |
}); | |
// Increment the x-coordinate. | |
left += item.width + gap; | |
return item; | |
}); | |
// ...and use it to increment to top position cursor. | |
top += rowHeight + gap; | |
return currentRow; | |
}) | |
.flatten() | |
.value(); | |
})(); | |
return { | |
items: postProcessItems(items, options), | |
container: calculateContainerBounds(items, options, container) | |
}; | |
}; | |
} | |
var DataLayoutContainer = React.createClass({ | |
getDefaultProps: function () { | |
return { | |
dataProvider: [], | |
itemRenderer: 'div' | |
}; | |
}, | |
getInitialState: function () { | |
return { | |
measurePending: true | |
}; | |
}, | |
// First render flag - we can't store this in state because updating | |
// it would trigger React's render cycle. | |
firstRender: true, | |
render: function () { | |
var style = _.extend({ | |
position: 'relative' | |
}, this.getContainerMeasurements()); | |
var output = ( | |
<div { ...this.props } className="Layout" style={ style }> | |
{ this.props.dataProvider.map(this.renderItem) } | |
</div> | |
); | |
if (this.firstRender && !this.state.measurePending) { | |
this.firstRender = false; | |
} | |
return output; | |
}, | |
renderItem: function (item, index, items) { | |
var pos = this.getMeasuredItemPositionAt(index); | |
var style = { | |
position: 'absolute', | |
visibility: pos ? 'visible' : 'hidden' | |
}; | |
var classSet = React.addons.classSet({ | |
"Layout__item": true, | |
"is-selected": item.selected | |
}); | |
_.extend(style, _.pick(pos, 'width', 'height')); | |
if (pos) { | |
style.transform = 'translate(' + pos.left + 'px, ' + pos.top + 'px)'; | |
} | |
// Turn off transitions whilst we measure - we will release | |
// them back on when layout is complete. | |
if (this.firstRender || this.state.measurePending) { | |
// TODO: Not removed properly after first render. | |
style.transition = 'none'; | |
} | |
function onClick () { | |
items.forEach(function (i) { | |
i.selected = item === i; | |
}); | |
this.setProps({ dataProvider: this.props.dataProvider }); | |
} | |
return ( | |
<this.props.itemRenderer | |
className={ classSet } | |
ref={ 'item-' + index } | |
key={ index } | |
data={ item } | |
style={ style } | |
onClick={ onClick.bind(this) } | |
/> | |
); | |
}, | |
getMeasuredItemPositionAt: function (index) { | |
var measurements = this.state.measurements; | |
return measurements && measurements.items[index]; | |
}, | |
getContainerMeasurements: function () { | |
var measurements = this.state.measurements; | |
return measurements && measurements.container; | |
}, | |
componentWillReceiveProps: function (newProps) { | |
if ('dataProvider' in newProps) { | |
this.setState({ measurePending: true }); | |
} | |
}, | |
componentDidMount: function () { | |
this.measureAndLayout(); | |
}, | |
componentDidUpdate: function () { | |
this.measureAndLayout(); | |
}, | |
measureAndLayout: function () { | |
if (!this.state.measurePending) return; | |
var measuredItems = (this.props.dataProvider || []) | |
.map(function (item, index) { | |
var el = this.refs['item-' + index].getDOMNode(); | |
return { | |
width: el.offsetWidth, | |
height: el.offsetHeight | |
}; | |
}, this); | |
var el = this.getDOMNode(); | |
var computedStyle = window.getComputedStyle(el); | |
var container = { | |
width: parseInt(computedStyle.width), | |
height: parseInt(computedStyle.height), | |
measuredWidth: el.offsetWidth, | |
measuredHeight: el.offsetHeight | |
}; | |
var measurements = this.props.layout(measuredItems, container); | |
this.setState({ | |
measurements: measurements, | |
measurePending: false | |
}); | |
} | |
}); | |
var horizontal = HorizontalLayout({ gap: 10, padding: 10 }); | |
var vertical = VerticalLayout({ gap: 20, padding: 20 }); | |
var grid = GridLayout({ gap: 10, padding: 20, variableRowHeight: true, fillWidth: true, stretchHeight: true }); | |
var dataProvider = [0, 1, 2, 3, 4, 5].map(function (i) { | |
return { value: i, selected: false }; | |
}); | |
React.render( | |
<DataLayoutContainer layout={ grid } dataProvider={ dataProvider } />, | |
document.querySelector('.container') | |
); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment