UNPKG

react-reflex

Version:

Flex layout component for advanced React web applications

600 lines (575 loc) 22.2 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectSpread from "@babel/runtime/helpers/objectSpread"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; /////////////////////////////////////////////////////////// // ReflexContainer // By Philippe Leefsma // December 2016 // /////////////////////////////////////////////////////////// import ReflexSplitter from './ReflexSplitter'; import ReflexEvents from './ReflexEvents'; import { getDataProps } from './utilities'; import PropTypes from 'prop-types'; import React from 'react'; import './Polyfills'; export default class ReflexContainer extends React.Component { ///////////////////////////////////////////////////////// // orientation: Orientation of the layout container // valid values are ['horizontal', 'vertical'] // maxRecDepth: Maximun recursion depth to solve initial flex // of layout elements based on user provided values // className: Space separated classnames to apply custom styles // to the layout container // style: allows passing inline style to the container ///////////////////////////////////////////////////////// constructor(props) { super(props); _defineProperty(this, "onWindowResize", () => { this.setState({ flexData: this.computeFlexData() }); }); _defineProperty(this, "onStartResize", data => { const pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event; switch (this.props.orientation) { case 'horizontal': document.body.classList.add('reflex-row-resize'); this.previousPos = pos.clientY; break; case 'vertical': default: document.body.classList.add('reflex-col-resize'); this.previousPos = pos.clientX; break; } this.elements = [this.children[data.index - 1], this.children[data.index + 1]]; this.emitElementsEvent(this.elements, 'onStartResize'); }); _defineProperty(this, "onResize", data => { const pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event; const offset = this.getOffset(pos, data.domElement); switch (this.props.orientation) { case 'horizontal': this.previousPos = pos.clientY; break; case 'vertical': default: this.previousPos = pos.clientX; break; } if (offset) { const availableOffset = this.computeAvailableOffset(data.index, offset); if (availableOffset) { this.elements = this.dispatchOffset(data.index, availableOffset); this.adjustFlex(this.elements); this.setState({ resizing: true }, () => { this.emitElementsEvent(this.elements, 'onResize'); }); } } }); _defineProperty(this, "onStopResize", data => { document.body.classList.remove('reflex-row-resize'); document.body.classList.remove('reflex-col-resize'); const resizedRefs = this.elements ? this.elements.map(element => { return element.ref; }) : []; const elements = this.children.filter(child => { return !ReflexSplitter.isA(child) && resizedRefs.includes(child.ref); }); this.emitElementsEvent(elements, 'onStopResize'); this.setState({ resizing: false }); }); _defineProperty(this, "onElementSize", data => { return new Promise(resolve => { try { const idx = data.index; const size = this.getSize(this.children[idx]); const offset = data.size - size; const dir = data.direction; const splitterIdx = idx + dir; const availableOffset = this.computeAvailableOffset(splitterIdx, dir * offset); this.elements = null; if (availableOffset) { this.elements = this.dispatchOffset(splitterIdx, availableOffset); this.adjustFlex(this.elements); } this.setState(this.state, () => { this.emitElementsEvent(this.elements, 'onResize'); resolve(); }); } catch (ex) { // TODO handle exception ... console.log(ex); } }); }); this.events = new ReflexEvents(); this.children = []; this.state = { flexData: [] }; this.ref = React.createRef(); } componentDidMount() { const flexData = this.computeFlexData(); const { windowResizeAware } = this.props; if (windowResizeAware) { window.addEventListener('resize', this.onWindowResize); } this.setState({ windowResizeAware, flexData }); this.events.on('element.size', this.onElementSize); this.events.on('startResize', this.onStartResize); this.events.on('stopResize', this.onStopResize); this.events.on('resize', this.onResize); } componentWillUnmount() { this.events.off(); window.removeEventListener('resize', this.onWindowResize); } getValidChildren(props = this.props) { return this.toArray(props.children).filter(child => { return !!child; }); } componentDidUpdate(prevProps, prevState) { const children = this.getValidChildren(this.props); if (children.length !== this.state.flexData.length || prevProps.orientation !== this.props.orientation || this.flexHasChanged(prevProps)) { const flexData = this.computeFlexData(children, this.props); this.setState({ flexData }); } if (this.props.windowResizeAware !== this.state.windowResizeAware) { !this.props.windowResizeAware ? window.removeEventListener('resize', this.onWindowResize) : window.addEventListener('resize', this.onWindowResize); this.setState({ windowResizeAware: this.props.windowResizeAware }); } } // UNSAFE_componentWillReceiveProps(props) { // const children = this.getValidChildren(props) // if (children.length !== this.state.flexData.length || // props.orientation !== this.props.orientation || // this.flexHasChanged(props)) // { // const flexData = this.computeFlexData( // children, props) // this.setState({ // flexData // }); // } // if (props.windowResizeAware !== this.state.windowResizeAware) { // !props.windowResizeAware // ? window.removeEventListener('resize', this.onWindowResize) // : window.addEventListener('resize', this.onWindowResize) // this.setState({ // windowResizeAware: props.windowResizeAware // }) // } // } ///////////////////////////////////////////////////////// // attempts to preserve current flex on window resize // ///////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// // Check if flex has changed: this allows updating the // component when different flex is passed as property // to one or several children // ///////////////////////////////////////////////////////// flexHasChanged(prevProps) { const prevChildrenFlex = this.getValidChildren(prevProps).map(child => { return child.props.flex || 0; }); const childrenFlex = this.getValidChildren().map(child => { return child.props.flex || 0; }); return !childrenFlex.every((flex, idx) => { return flex === prevChildrenFlex[idx]; }); } ///////////////////////////////////////////////////////// // Returns size of a ReflexElement // ///////////////////////////////////////////////////////// getSize(element) { var _element$ref, _domElement$offsetHei, _domElement$offsetWid; const domElement = element === null || element === void 0 ? void 0 : (_element$ref = element.ref) === null || _element$ref === void 0 ? void 0 : _element$ref.current; switch (this.props.orientation) { case 'horizontal': return (_domElement$offsetHei = domElement === null || domElement === void 0 ? void 0 : domElement.offsetHeight) !== null && _domElement$offsetHei !== void 0 ? _domElement$offsetHei : 0; case 'vertical': default: return (_domElement$offsetWid = domElement === null || domElement === void 0 ? void 0 : domElement.offsetWidth) !== null && _domElement$offsetWid !== void 0 ? _domElement$offsetWid : 0; } } ///////////////////////////////////////////////////////// // Computes offset from pointer position // ///////////////////////////////////////////////////////// getOffset(pos, domElement) { const { top, bottom, left, right } = domElement.getBoundingClientRect(); switch (this.props.orientation) { case 'horizontal': { const offset = pos.clientY - this.previousPos; if (offset > 0) { if (pos.clientY >= top) { return offset; } } else { if (pos.clientY <= bottom) { return offset; } } break; } case 'vertical': default: { const offset = pos.clientX - this.previousPos; if (offset > 0) { if (pos.clientX > left) { return offset; } } else { if (pos.clientX < right) { return offset; } } } break; } return 0; } ///////////////////////////////////////////////////////// // Handles startResize event // ///////////////////////////////////////////////////////// ///////////////////////////////////////////////////////// // Adjusts flex after a dispatch to make sure // total flex of modified elements remains the same // ///////////////////////////////////////////////////////// adjustFlex(elements) { const diffFlex = elements.reduce((sum, element) => { const idx = element.props.index; const previousFlex = element.props.flex; const nextFlex = this.state.flexData[idx].flex; return sum + (previousFlex - nextFlex) / elements.length; }, 0); elements.forEach(element => { this.state.flexData[element.props.index].flex += diffFlex; }); } ///////////////////////////////////////////////////////// // Returns available offset for a given raw offset value // This checks how much the panes can be stretched and // shrink, then returns the min // ///////////////////////////////////////////////////////// computeAvailableOffset(idx, offset) { const stretch = this.computeAvailableStretch(idx, offset); const shrink = this.computeAvailableShrink(idx, offset); const availableOffset = Math.min(stretch, shrink) * Math.sign(offset); return availableOffset; } ///////////////////////////////////////////////////////// // Returns true if the next splitter than the one at idx // can propagate the drag. This can happen if that // next element is actually a splitter and it has // propagate=true property set // ///////////////////////////////////////////////////////// checkPropagate(idx, direction) { if (direction > 0) { if (idx < this.children.length - 2) { const child = this.children[idx + 2]; const typeCheck = ReflexSplitter.isA(child); return typeCheck && child.props.propagate; } } else { if (idx > 2) { const child = this.children[idx - 2]; const typeCheck = ReflexSplitter.isA(child); return typeCheck && child.props.propagate; } } return false; } ///////////////////////////////////////////////////////// // Recursively computes available stretch at splitter // idx for given raw offset // ///////////////////////////////////////////////////////// computeAvailableStretch(idx, offset) { var _child$props$maxSize; const childIdx = offset < 0 ? idx + 1 : idx - 1; const child = this.children[childIdx]; const size = this.getSize(child); const maxSize = (_child$props$maxSize = child === null || child === void 0 ? void 0 : child.props.maxSize) !== null && _child$props$maxSize !== void 0 ? _child$props$maxSize : 0; const availableStretch = maxSize - size; if (availableStretch < Math.abs(offset)) { if (this.checkPropagate(idx, -1 * offset)) { const nextOffset = Math.sign(offset) * (Math.abs(offset) - availableStretch); return availableStretch + this.computeAvailableStretch(offset < 0 ? idx + 2 : idx - 2, nextOffset); } } return Math.min(availableStretch, Math.abs(offset)); } ///////////////////////////////////////////////////////// // Recursively computes available shrink at splitter // idx for given raw offset // ///////////////////////////////////////////////////////// computeAvailableShrink(idx, offset) { var _child$props$minSize; const childIdx = offset > 0 ? idx + 1 : idx - 1; const child = this.children[childIdx]; const size = this.getSize(child); const minSize = Math.max((_child$props$minSize = child === null || child === void 0 ? void 0 : child.props.minSize) !== null && _child$props$minSize !== void 0 ? _child$props$minSize : 0, 0); const availableShrink = size - minSize; if (availableShrink < Math.abs(offset)) { if (this.checkPropagate(idx, offset)) { const nextOffset = Math.sign(offset) * (Math.abs(offset) - availableShrink); return availableShrink + this.computeAvailableShrink(offset > 0 ? idx + 2 : idx - 2, nextOffset); } } return Math.min(availableShrink, Math.abs(offset)); } ///////////////////////////////////////////////////////// // Returns flex value for unit pixel // ///////////////////////////////////////////////////////// computePixelFlex(orientation = this.props.orientation) { if (!this.ref.current) { console.warn('Unable to locate ReflexContainer dom node'); return 0.0; } switch (orientation) { case 'horizontal': if (this.ref.current.offsetHeight === 0.0) { console.warn('Found ReflexContainer with height=0, ' + 'this will cause invalid behavior...'); console.warn(this.ref.current); return 0.0; } return 1.0 / this.ref.current.offsetHeight; case 'vertical': default: if (this.ref.current.offsetWidth === 0.0) { console.warn('Found ReflexContainer with width=0, ' + 'this will cause invalid behavior...'); console.warn(this.ref.current); return 0.0; } return 1.0 / this.ref.current.offsetWidth; } } ///////////////////////////////////////////////////////// // Adds offset to a given ReflexElement // ///////////////////////////////////////////////////////// addOffset(element, offset) { const size = this.getSize(element); const idx = element.props.index; const newSize = Math.max(size + offset, 0); const currentFlex = this.state.flexData[idx].flex; const newFlex = currentFlex > 0 ? currentFlex * newSize / size : this.computePixelFlex() * newSize; this.state.flexData[idx].flex = !isFinite(newFlex) || isNaN(newFlex) ? 0 : newFlex; } ///////////////////////////////////////////////////////// // Recursively dispatches stretch offset across // children elements starting at splitter idx // ///////////////////////////////////////////////////////// dispatchStretch(idx, offset) { const childIdx = offset < 0 ? idx + 1 : idx - 1; if (childIdx < 0 || childIdx > this.children.length - 1) { return []; } const child = this.children[childIdx]; const size = this.getSize(child); const newSize = Math.min(child.props.maxSize, size + Math.abs(offset)); const dispatchedStretch = newSize - size; this.addOffset(child, dispatchedStretch); if (dispatchedStretch < Math.abs(offset)) { const nextIdx = idx - Math.sign(offset) * 2; const nextOffset = Math.sign(offset) * (Math.abs(offset) - dispatchedStretch); return [child, ...this.dispatchStretch(nextIdx, nextOffset)]; } return [child]; } ///////////////////////////////////////////////////////// // Recursively dispatches shrink offset across // children elements starting at splitter idx // ///////////////////////////////////////////////////////// dispatchShrink(idx, offset) { const childIdx = offset > 0 ? idx + 1 : idx - 1; if (childIdx < 0 || childIdx > this.children.length - 1) { return []; } const child = this.children[childIdx]; const size = this.getSize(child); const newSize = Math.max(child.props.minSize, size - Math.abs(offset)); const dispatchedShrink = newSize - size; this.addOffset(child, dispatchedShrink); if (Math.abs(dispatchedShrink) < Math.abs(offset)) { const nextIdx = idx + Math.sign(offset) * 2; const nextOffset = Math.sign(offset) * (Math.abs(offset) + dispatchedShrink); return [child, ...this.dispatchShrink(nextIdx, nextOffset)]; } return [child]; } ///////////////////////////////////////////////////////// // Dispatch offset at splitter idx // ///////////////////////////////////////////////////////// dispatchOffset(idx, offset) { return [...this.dispatchStretch(idx, offset), ...this.dispatchShrink(idx, offset)]; } ///////////////////////////////////////////////////////// // Emits given if event for each given element // if present in the component props // ///////////////////////////////////////////////////////// emitElementsEvent(elements, event) { this.toArray(elements).forEach(component => { if (component.props[event]) { component.props[event]({ domElement: component.ref.current, component }); } }); } ///////////////////////////////////////////////////////// // Computes initial flex data based on provided flex // properties. By default each ReflexElement gets // evenly arranged within its container // ///////////////////////////////////////////////////////// computeFlexData(children = this.getValidChildren(), props = this.props) { const pixelFlex = this.computePixelFlex(props.orientation); const computeFreeFlex = flexData => { return flexData.reduce((sum, entry) => { if (!ReflexSplitter.isA(entry) && entry.constrained) { return sum - entry.flex; } return sum; }, 1.0); }; const computeFreeElements = flexData => { return flexData.reduce((sum, entry) => { if (!ReflexSplitter.isA(entry) && !entry.constrained) { return sum + 1; } return sum; }, 0.0); }; const flexDataInit = children.map(child => { const props = child.props; return { maxFlex: (props.maxSize || Number.MAX_VALUE) * pixelFlex, sizeFlex: (props.size || Number.MAX_VALUE) * pixelFlex, minFlex: (props.minSize || 1) * pixelFlex, constrained: props.flex !== undefined, flex: props.flex || 0, type: child.type }; }); const computeFlexDataRec = (flexDataIn, depth = 0) => { let hasContrain = false; const freeElements = computeFreeElements(flexDataIn); const freeFlex = computeFreeFlex(flexDataIn); const flexDataOut = flexDataIn.map(entry => { if (ReflexSplitter.isA(entry)) { return entry; } const proposedFlex = !entry.constrained ? freeFlex / freeElements : entry.flex; const constrainedFlex = Math.min(entry.sizeFlex, Math.min(entry.maxFlex, Math.max(entry.minFlex, proposedFlex))); const constrained = entry.constrained || constrainedFlex !== proposedFlex; hasContrain = hasContrain || constrained; return _objectSpread({}, entry, { flex: constrainedFlex, constrained }); }); return hasContrain && depth < this.props.maxRecDepth ? computeFlexDataRec(flexDataOut, depth + 1) : flexDataOut; }; const flexData = computeFlexDataRec(flexDataInit); return flexData.map(entry => { return { flex: !ReflexSplitter.isA(entry) ? entry.flex : 0.0, ref: React.createRef() }; }); } ///////////////////////////////////////////////////////// // Utility method to ensure given argument is // returned as an array // ///////////////////////////////////////////////////////// toArray(obj) { return obj ? Array.isArray(obj) ? obj : [obj] : []; } ///////////////////////////////////////////////////////// // Render container. This will clone all original child // components in order to pass some internal properties // used to handle resizing logic // ///////////////////////////////////////////////////////// render() { const className = [this.state.resizing ? 'reflex-resizing' : '', ...this.props.className.split(' '), this.props.orientation, 'reflex-container'].join(' ').trim(); this.children = React.Children.map(this.getValidChildren(), (child, index) => { if (index > this.state.flexData.length - 1) { return /*#__PURE__*/React.createElement("div", null); } const flexData = this.state.flexData[index]; const newProps = _objectSpread({}, child.props, { maxSize: child.props.maxSize || Number.MAX_VALUE, orientation: this.props.orientation, minSize: child.props.minSize || 1, events: this.events, flex: flexData.flex, ref: flexData.ref, index }); return React.cloneElement(child, newProps); }); return /*#__PURE__*/React.createElement("div", _extends({}, getDataProps(this.props), { style: this.props.style, className: className, ref: this.ref }), this.children); } } _defineProperty(ReflexContainer, "propTypes", { windowResizeAware: PropTypes.bool, orientation: PropTypes.oneOf(['horizontal', 'vertical']), maxRecDepth: PropTypes.number, className: PropTypes.string, style: PropTypes.object }); _defineProperty(ReflexContainer, "defaultProps", { orientation: 'horizontal', windowResizeAware: false, maxRecDepth: 100, className: '', style: {} });