react-fluid-grid
Version:
Responsive layout similar to pinterest. Items placed in columns according to their order and height.
839 lines (711 loc) • 25.3 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
function _extends() {
_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;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype.__proto__ = superClass && superClass.prototype;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var ResizeSensor = createCommonjsModule(function (module, exports) {
/**
* Copyright Marc J. Schmidt. See the LICENSE file at the top-level
* directory of this distribution and at
* https://github.com/marcj/css-element-queries/blob/master/LICENSE.
*/
(function (root, factory) {
if (typeof undefined === "function" && undefined.amd) {
undefined(factory);
} else {
module.exports = factory();
}
}(typeof window !== 'undefined' ? window : commonjsGlobal, function () {
// Make sure it does not throw in a SSR (Server Side Rendering) situation
if (typeof window === "undefined") {
return null;
}
// Only used for the dirty checking, so the event callback count is limited to max 1 call per fps per sensor.
// In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and
// would generate too many unnecessary events.
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (fn) {
return window.setTimeout(fn, 20);
};
/**
* Iterate over each of the provided element(s).
*
* @param {HTMLElement|HTMLElement[]} elements
* @param {Function} callback
*/
function forEachElement(elements, callback){
var elementsType = Object.prototype.toString.call(elements);
var isCollectionTyped = ('[object Array]' === elementsType
|| ('[object NodeList]' === elementsType)
|| ('[object HTMLCollection]' === elementsType)
|| ('[object Object]' === elementsType)
|| ('undefined' !== typeof jQuery && elements instanceof jQuery) //jquery
|| ('undefined' !== typeof Elements && elements instanceof Elements) //mootools
);
var i = 0, j = elements.length;
if (isCollectionTyped) {
for (; i < j; i++) {
callback(elements[i]);
}
} else {
callback(elements);
}
}
/**
* Get element size
* @param {HTMLElement} element
* @returns {Object} {width, height}
*/
function getElementSize(element) {
if (!element.getBoundingClientRect) {
return {
width: element.offsetWidth,
height: element.offsetHeight
}
}
var rect = element.getBoundingClientRect();
return {
width: Math.round(rect.width),
height: Math.round(rect.height)
}
}
/**
* Class for dimension change detection.
*
* @param {Element|Element[]|Elements|jQuery} element
* @param {Function} callback
*
* @constructor
*/
var ResizeSensor = function(element, callback) {
/**
*
* @constructor
*/
function EventQueue() {
var q = [];
this.add = function(ev) {
q.push(ev);
};
var i, j;
this.call = function() {
for (i = 0, j = q.length; i < j; i++) {
q[i].call();
}
};
this.remove = function(ev) {
var newQueue = [];
for(i = 0, j = q.length; i < j; i++) {
if(q[i] !== ev) newQueue.push(q[i]);
}
q = newQueue;
};
this.length = function() {
return q.length;
};
}
/**
*
* @param {HTMLElement} element
* @param {Function} resized
*/
function attachResizeEvent(element, resized) {
if (!element) return;
if (element.resizedAttached) {
element.resizedAttached.add(resized);
return;
}
element.resizedAttached = new EventQueue();
element.resizedAttached.add(resized);
element.resizeSensor = document.createElement('div');
element.resizeSensor.dir = 'ltr';
element.resizeSensor.className = 'resize-sensor';
var style = 'position: absolute; left: -10px; top: -10px; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;';
var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;';
element.resizeSensor.style.cssText = style;
element.resizeSensor.innerHTML =
'<div class="resize-sensor-expand" style="' + style + '">' +
'<div style="' + styleChild + '"></div>' +
'</div>' +
'<div class="resize-sensor-shrink" style="' + style + '">' +
'<div style="' + styleChild + ' width: 200%; height: 200%"></div>' +
'</div>';
element.appendChild(element.resizeSensor);
var position = window.getComputedStyle(element).getPropertyValue('position');
if ('absolute' !== position && 'relative' !== position && 'fixed' !== position) {
element.style.position = 'relative';
}
var expand = element.resizeSensor.childNodes[0];
var expandChild = expand.childNodes[0];
var shrink = element.resizeSensor.childNodes[1];
var dirty, rafId, newWidth, newHeight;
var size = getElementSize(element);
var lastWidth = size.width;
var lastHeight = size.height;
var reset = function() {
//set display to block, necessary otherwise hidden elements won't ever work
var invisible = element.offsetWidth === 0 && element.offsetHeight === 0;
if (invisible) {
var saveDisplay = element.style.display;
element.style.display = 'block';
}
expandChild.style.width = '100000px';
expandChild.style.height = '100000px';
expand.scrollLeft = 100000;
expand.scrollTop = 100000;
shrink.scrollLeft = 100000;
shrink.scrollTop = 100000;
if (invisible) {
element.style.display = saveDisplay;
}
};
element.resizeSensor.resetSensor = reset;
var onResized = function() {
rafId = 0;
if (!dirty) return;
lastWidth = newWidth;
lastHeight = newHeight;
if (element.resizedAttached) {
element.resizedAttached.call();
}
};
var onScroll = function() {
var size = getElementSize(element);
var newWidth = size.width;
var newHeight = size.height;
dirty = newWidth != lastWidth || newHeight != lastHeight;
if (dirty && !rafId) {
rafId = requestAnimationFrame(onResized);
}
reset();
};
var addEvent = function(el, name, cb) {
if (el.attachEvent) {
el.attachEvent('on' + name, cb);
} else {
el.addEventListener(name, cb);
}
};
addEvent(expand, 'scroll', onScroll);
addEvent(shrink, 'scroll', onScroll);
// Fix for custom Elements
requestAnimationFrame(reset);
}
forEachElement(element, function(elem){
attachResizeEvent(elem, callback);
});
this.detach = function(ev) {
ResizeSensor.detach(element, ev);
};
this.reset = function() {
element.resizeSensor.resetSensor();
};
};
ResizeSensor.reset = function(element, ev) {
forEachElement(element, function(elem){
elem.resizeSensor.resetSensor();
});
};
ResizeSensor.detach = function(element, ev) {
forEachElement(element, function(elem){
if (!elem) return;
if(elem.resizedAttached && typeof ev === "function"){
elem.resizedAttached.remove(ev);
if(elem.resizedAttached.length()) return;
}
if (elem.resizeSensor) {
if (elem.contains(elem.resizeSensor)) {
elem.removeChild(elem.resizeSensor);
}
delete elem.resizeSensor;
delete elem.resizedAttached;
}
});
};
return ResizeSensor;
}));
});
var SizeObservable =
/*#__PURE__*/
function () {
function SizeObservable(element) {
this.element = element;
this.observers = [];
this.handleResize = this.handleResize.bind(this);
this.createSizeObservable();
}
var _proto = SizeObservable.prototype;
_proto.register = function register(observer) {
this.observers.push(observer);
};
_proto.unregister = function unregister(observerToUnregister) {
this.observers = this.observers.filter(function (observer) {
return observer !== observerToUnregister;
});
};
_proto.handleResize = function handleResize() {
var size = getSize(this.element);
this.observers.forEach(function (observer) {
return observer(size);
});
};
_proto.createSizeObservable = function createSizeObservable() {
return new ResizeSensor(this.element, this.handleResize);
};
return SizeObservable;
}();
function getSize(element) {
return {
height: element.clientHeight,
width: element.clientWidth
};
}
var ResizeObservingContainer =
/*#__PURE__*/
function (_React$Component) {
function ResizeObservingContainer(props) {
var _this;
_this = _React$Component.call(this, props) || this;
_this.state = {
height: null,
width: null
};
_this.container = React.createRef();
_this.createSizeObservable = _this.createSizeObservable.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.notifyHeightChange = _this.notifyHeightChange.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.notifyWidthChange = _this.notifyWidthChange.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.updateSize = _this.updateSize.bind(_assertThisInitialized(_assertThisInitialized(_this)));
return _this;
}
var _proto = ResizeObservingContainer.prototype;
_proto.componentDidMount = function componentDidMount() {
this.createSizeObservable(this.container.current);
};
_proto.notifyHeightChange = function notifyHeightChange(_ref) {
var height = _ref.height;
if (height !== this.state.height) {
executeIfDefined(this.props.onHeightChange, height);
}
};
_proto.notifyWidthChange = function notifyWidthChange(_ref2) {
var width = _ref2.width;
if (width !== this.state.width) {
executeIfDefined(this.props.onWidthChange, width);
}
};
_proto.updateSize = function updateSize(size) {
this.setState(size);
};
_proto.createSizeObservable = function createSizeObservable(element) {
var sizeObservable = new SizeObservable(element);
sizeObservable.register(this.notifyHeightChange);
sizeObservable.register(this.notifyWidthChange);
sizeObservable.register(this.updateSize);
};
_proto.render = function render() {
var children = this.props.children;
var passedProps = Object.assign({}, this.props);
delete passedProps.onHeightChange;
delete passedProps.onWidthChange;
delete passedProps.children;
return React.createElement("div", _extends({}, passedProps, {
ref: this.container
}), children);
};
_inheritsLoose(ResizeObservingContainer, _React$Component);
return ResizeObservingContainer;
}(React.Component);
function executeIfDefined(func) {
if (typeof func === 'function') {
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
params[_key - 1] = arguments[_key];
}
func.apply(void 0, params);
}
}
ResizeObservingContainer.propTypes = {
children: PropTypes.node,
onHeightChange: PropTypes.func,
onWidthChange: PropTypes.func
};
function getSafeNumberOfColumns(numberOfColumns) {
return numberOfColumns > 0 ? numberOfColumns : 1;
}
function round(value, decimals) {
var decimalsShift = Math.pow(10, decimals);
return Math.round(value * decimalsShift) / decimalsShift;
}
var WIDTH_PRECISION = 2;
function getColumnWidth(numberOfColumns) {
var safeNumberOfColumns = getSafeNumberOfColumns(numberOfColumns);
var width = 100 / safeNumberOfColumns;
return round(width, WIDTH_PRECISION);
}
function getOuterGutterPortion(gutterWidth, numberOfColumns) {
var safeNumberOfColumns = getSafeNumberOfColumns(numberOfColumns);
return gutterWidth / safeNumberOfColumns;
}
var DEFAULT_ITEM_STYLES = {
horizontalGutterShift: 0,
left: 0,
top: 0
};
function getItemStyles(item, dridStyles) {
var mergedStyles = Object.assign({}, DEFAULT_ITEM_STYLES, item, dridStyles);
mergedStyles.outerGutterPortion = getOuterGutterPortion(dridStyles.gutterWidth, dridStyles.numberOfColumns);
return dridStyles.gridWidth ? getItemStylesForKnownWidth(mergedStyles) : getItemStylesForUnknownWidth(mergedStyles);
}
function getItemStylesForKnownWidth(_ref) {
var columnWidth = _ref.columnWidth,
horizontalGutterShift = _ref.horizontalGutterShift,
gridWidth = _ref.gridWidth,
outerGutterPortion = _ref.outerGutterPortion,
gutterWidth = _ref.gutterWidth,
left = _ref.left,
top = _ref.top,
transition = _ref.transition;
return {
left: left * gridWidth / 100 + horizontalGutterShift + "px",
position: 'absolute',
top: top + "px",
transition: transition,
width: gridWidth * columnWidth / 100 - gutterWidth + outerGutterPortion + "px"
};
}
function getItemStylesForUnknownWidth(_ref2) {
var columnWidth = _ref2.columnWidth,
horizontalGutterShift = _ref2.horizontalGutterShift,
outerGutterPortion = _ref2.outerGutterPortion,
gutterWidth = _ref2.gutterWidth,
left = _ref2.left,
top = _ref2.top,
transition = _ref2.transition;
return {
left: "calc(" + left + "% + " + horizontalGutterShift + "px)",
position: 'absolute',
top: top + "px",
transition: transition,
width: "calc(" + columnWidth + "% - " + gutterWidth + "px + " + outerGutterPortion + "px)"
};
}
function FluidGridItem(_ref) {
var children = _ref.children,
className = _ref.className,
columnWidth = _ref.columnWidth,
gridWidth = _ref.gridWidth,
gutterWidth = _ref.gutterWidth,
item = _ref.item,
index = _ref.index,
numberOfColumns = _ref.numberOfColumns,
onHeightChange = _ref.onHeightChange,
registerItem = _ref.registerItem,
transition = _ref.transition;
if (!children) {
return null;
}
var style = getItemStyles(item, {
columnWidth: columnWidth,
gridWidth: gridWidth,
gutterWidth: gutterWidth,
numberOfColumns: numberOfColumns,
transition: transition
});
return React.createElement("div", {
className: className,
ref: registerItem,
style: style
}, React.createElement(ResizeObservingContainer, {
onHeightChange: onHeightChange
}, children));
}
FluidGridItem.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
columnWidth: PropTypes.number,
gridWidth: PropTypes.number,
gutterWidth: PropTypes.number,
item: PropTypes.object,
index: PropTypes.number.isRequired,
numberOfColumns: PropTypes.number,
onHeightChange: PropTypes.func.isRequired,
registerItem: PropTypes.func.isRequired,
transition: PropTypes.string
};
FluidGridItem.defaultProps = {
className: '',
transition: 'top 200ms ease-in-out, left 200ms ease-in-out, width 200ms ease-in-out'
};
function getShortestColumn(columns) {
var getShortest = function getShortest(shortestColumn, column, index) {
return shortestColumn.height <= column.height ? shortestColumn : column;
};
return columns.reduce(getShortest, columns[0]);
}
function getLongestColumn(columns) {
var getLongest = function getLongest(longestColumn, column, index) {
return longestColumn.height > column.height ? longestColumn : column;
};
return columns.reduce(getLongest, columns[0]);
}
var INITIAL_HEIGHT = 0;
function getColumns(columnWidth, gutterWidth, numberOfColumns) {
var columns = [];
for (var i = 0; i < numberOfColumns; i++) {
columns.push({
height: INITIAL_HEIGHT,
horizontalGutterShift: gutterWidth * i / numberOfColumns,
left: columnWidth * i
});
}
return columns;
}
function getColumnHeightShift(itemHeight, gutterHeight) {
return itemHeight ? itemHeight + gutterHeight : 0;
}
var defaultStyleStrategies = [{
mediaQuery: '(max-width: 719.9px)',
style: {
numberOfColumns: 1,
gutterHeight: 8,
gutterWidth: 8
}
}, {
mediaQuery: '(min-width: 720px) and (max-width: 1023.9px)',
style: {
numberOfColumns: 2,
gutterHeight: 16,
gutterWidth: 16
}
}, {
mediaQuery: '(min-width: 1024px) and (max-width: 1919.9px)',
style: {
numberOfColumns: 3,
gutterHeight: 24,
gutterWidth: 24
}
}, {
mediaQuery: '(min-width: 1920px)',
style: {
numberOfColumns: 4,
gutterHeight: 24,
gutterWidth: 24
}
}];
var DEFAULT_STYLE = {
numberOfColumns: 1,
gutterHeight: 0,
gutterWidth: 0
};
var detectNode = createCommonjsModule(function (module) {
module.exports = false;
// Only Node.JS has a process variable that is of [[Class]] process
try {
module.exports = Object.prototype.toString.call(commonjsGlobal.process) === '[object process]';
} catch(e) {}
});
function getStyle(styleStrategies) {
if (detectNode) {
return DEFAULT_STYLE;
}
var matchingStrategy = styleStrategies.find(isMatch);
return matchingStrategy ? matchingStrategy.style : DEFAULT_STYLE;
}
function isMatch(_ref) {
var mediaQuery = _ref.mediaQuery;
return window.matchMedia(mediaQuery).matches;
}
var FluidGrid =
/*#__PURE__*/
function (_React$Component) {
function FluidGrid(props) {
var _this;
_this = _React$Component.call(this, props) || this;
_this.state = {
items: {},
height: '0px',
overflow: 'hidden',
position: 'relative'
};
_this.items = {};
_this.updateLayout = _this.updateLayout.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.renderChild = _this.renderChild.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.pushToShortestColumn = _this.pushToShortestColumn.bind(_assertThisInitialized(_assertThisInitialized(_this)));
_this.updateGridStyles = _this.updateGridStyles.bind(_assertThisInitialized(_assertThisInitialized(_this)));
return _this;
}
var _proto = FluidGrid.prototype;
_proto.componentDidMount = function componentDidMount() {
this.updateGridStyles();
};
_proto.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
if (this.refreshRequired(prevProps, prevState)) {
this.updateLayout();
}
};
_proto.refreshRequired = function refreshRequired(prevProps, prevState) {
return prevProps.children !== this.props.children || prevState.numberOfColumns !== this.state.numberOfColumns || prevState.gutterHeight !== this.state.gutterHeight || prevState.gutterWidth !== this.state.gutterWidth;
};
_proto.updateLayout = function updateLayout() {
this.initColumns();
this.arrangeItems();
this.setGridHeight();
this.applyChanges();
};
_proto.initColumns = function initColumns() {
var _this$state = this.state,
columnWidth = _this$state.columnWidth,
gutterWidth = _this$state.gutterWidth,
numberOfColumns = _this$state.numberOfColumns;
this.columns = getColumns(columnWidth, gutterWidth, numberOfColumns);
};
_proto.arrangeItems = function arrangeItems() {
Object.values(this.items).forEach(this.pushToShortestColumn);
};
_proto.pushToShortestColumn = function pushToShortestColumn(item) {
var column = getShortestColumn(this.columns);
this.setItemStyle(item, column);
this.pushToColumn(item, column);
};
_proto.pushToColumn = function pushToColumn(item, column) {
var gutterHeight = this.state.gutterHeight;
var itemHeight = item.element.clientHeight;
var heightShift = getColumnHeightShift(itemHeight, gutterHeight);
column.height = column.height + heightShift;
};
_proto.setItemStyle = function setItemStyle(item, column) {
item.top = column.height;
item.left = column.left;
item.horizontalGutterShift = column.horizontalGutterShift;
};
_proto.setGridHeight = function setGridHeight() {
var gutterHeight = this.state.gutterHeight;
var _getLongestColumn = getLongestColumn(this.columns),
height = _getLongestColumn.height;
this.gridHeight = height - gutterHeight;
};
_proto.applyChanges = function applyChanges() {
this.setState({
height: this.gridHeight + "px",
items: this.items
});
};
_proto.updateGridStyles = function updateGridStyles(gridWidth) {
var _getStyle = getStyle(this.props.styleStrategies),
gutterHeight = _getStyle.gutterHeight,
gutterWidth = _getStyle.gutterWidth,
numberOfColumns = _getStyle.numberOfColumns;
var columnWidth = getColumnWidth(numberOfColumns);
this.setState({
columnWidth: columnWidth,
gridWidth: gridWidth,
gutterHeight: gutterHeight,
gutterWidth: gutterWidth,
numberOfColumns: numberOfColumns
});
};
_proto.renderChildren = function renderChildren() {
this.clearItems();
return this.props.children.map(this.renderChild);
};
_proto.clearItems = function clearItems() {
this.items = {};
};
_proto.renderChild = function renderChild(child, index) {
var _this2 = this;
var _this$state2 = this.state,
columnWidth = _this$state2.columnWidth,
gridWidth = _this$state2.gridWidth,
gutterWidth = _this$state2.gutterWidth,
items = _this$state2.items,
numberOfColumns = _this$state2.numberOfColumns;
var _this$props = this.props,
itemClassName = _this$props.itemClassName,
transition = _this$props.transition;
var registerItem = function registerItem(element) {
_this2.items[index] = {
element: element
};
};
return React.createElement(FluidGridItem, {
className: itemClassName,
columnWidth: columnWidth,
gridWidth: gridWidth,
gutterWidth: gutterWidth,
item: items[index],
index: index,
key: index,
numberOfColumns: numberOfColumns,
onHeightChange: this.updateLayout,
registerItem: registerItem,
transition: transition
}, child);
};
_proto.render = function render() {
var className = this.props.className;
var _this$state3 = this.state,
height = _this$state3.height,
overflow = _this$state3.overflow,
position = _this$state3.position;
var style = {
height: height,
overflow: overflow,
position: position
};
return React.createElement("div", {
className: className,
style: style
}, React.createElement(ResizeObservingContainer, {
onWidthChange: this.updateGridStyles
}, this.renderChildren()));
};
_inheritsLoose(FluidGrid, _React$Component);
return FluidGrid;
}(React.Component);
var styleStrategyShape = {
mediaQuery: PropTypes.string.isRequired,
style: PropTypes.shape({
numberOfColumns: PropTypes.number.isRequired,
gutterHeight: PropTypes.number.isRequired,
gutterWidth: PropTypes.number.isRequired
})
};
FluidGrid.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
itemClassName: PropTypes.string,
styleStrategies: PropTypes.arrayOf(PropTypes.shape(styleStrategyShape)),
transition: PropTypes.string
};
FluidGrid.defaultProps = {
className: '',
itemClassName: '',
styleStrategies: defaultStyleStrategies
};
export default FluidGrid;