UNPKG

react-fluid-grid

Version:

Responsive layout similar to pinterest. Items placed in columns according to their order and height.

845 lines (714 loc) 25.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = _interopDefault(require('react')); var PropTypes = _interopDefault(require('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 }; exports.default = FluidGrid;