react-reflex
Version:
Flex layout component for advanced React web applications
745 lines (627 loc) • 27.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _ReflexSplitter = _interopRequireDefault(require("./ReflexSplitter"));
var _ReflexEvents = _interopRequireDefault(require("./ReflexEvents"));
var _utilities = require("./utilities");
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactDom = _interopRequireDefault(require("react-dom"));
var _react = _interopRequireDefault(require("react"));
require("./Polyfills");
///////////////////////////////////////////////////////////
// ReflexContainer
// By Philippe Leefsma
// December 2016
//
///////////////////////////////////////////////////////////
var ReflexContainer =
/*#__PURE__*/
function (_React$Component) {
(0, _inherits2.default)(ReflexContainer, _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
/////////////////////////////////////////////////////////
function ReflexContainer(props) {
var _this;
(0, _classCallCheck2.default)(this, ReflexContainer);
_this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(ReflexContainer).call(this, props));
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "onWindowResize", function () {
_this.setState({
flexData: _this.computeFlexData()
});
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "onStartResize", function (data) {
var pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event;
switch (_this.props.orientation) {
case 'horizontal':
document.body.classList.add('row-resize');
_this.previousPos = pos.pageY;
break;
case 'vertical':
default:
document.body.classList.add('col-resize');
_this.previousPos = pos.pageX;
break;
}
_this.elements = [_this.children[data.index - 1], _this.children[data.index + 1]];
_this.emitElementsEvent(_this.elements, 'onStartResize');
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "onResize", function (data) {
var offset = _this.getOffset(data.event);
var availableOffset = _this.computeAvailableOffset(data.index, offset);
if (availableOffset) {
var pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event;
switch (_this.props.orientation) {
case 'horizontal':
_this.previousPos = pos.pageY;
break;
case 'vertical':
default:
_this.previousPos = pos.pageX;
break;
}
_this.elements = _this.dispatchOffset(data.index, availableOffset);
_this.adjustFlex(_this.elements);
_this.setState({
resizing: true
}, function () {
_this.emitElementsEvent(_this.elements, 'onResize');
});
}
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "onStopResize", function (data) {
document.body.classList.remove('row-resize');
document.body.classList.remove('col-resize');
var resizedRefs = _this.elements.map(function (element) {
return element.ref;
});
var elements = _this.children.filter(function (child) {
return !_ReflexSplitter.default.isA(child) && resizedRefs.includes(child.ref);
});
_this.emitElementsEvent(elements, 'onStopResize');
_this.setState({
resizing: false
});
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), "onElementSize", function (data) {
return new Promise(function (resolve) {
try {
var idx = data.index;
var size = _this.getSize(_this.children[idx]);
var offset = data.size - size;
var dir = data.direction;
var splitterIdx = idx + dir;
var 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, function () {
_this.emitElementsEvent(_this.elements, 'onResize');
resolve();
});
} catch (ex) {
// TODO handle exception ...
console.log(ex);
}
});
});
_this.events = new _ReflexEvents.default();
_this.children = [];
_this.state = {
flexData: []
};
return _this;
} /////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
(0, _createClass2.default)(ReflexContainer, [{
key: "componentDidMount",
value: function componentDidMount() {
var flexData = this.computeFlexData();
var windowResizeAware = this.props.windowResizeAware;
if (windowResizeAware) {
window.addEventListener('resize', this.onWindowResize);
}
this.setState({
windowResizeAware: windowResizeAware,
flexData: 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);
} /////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
this.events.off();
if (this.state.windowResizeAware) {
window.removeEventListener('resize', this.onWindowResize);
}
} /////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
}, {
key: "getValidChildren",
value: function getValidChildren() {
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
return this.toArray(props.children).filter(function (child) {
return !!child;
});
} /////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////
// componentDidUpdate (prevProps, prevState) {
// const children = this.getValidChildren(this.props)
// if ((children.length !== this.state.flexData.length) ||
// (this.props.orientation !== this.props.orientation) ||
// this.flexHasChanged(this.props)) {
// 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
// })
// }
// }
}, {
key: "UNSAFE_componentWillReceiveProps",
value: function UNSAFE_componentWillReceiveProps(props) {
var children = this.getValidChildren(props);
if (children.length !== this.state.flexData.length || props.orientation !== this.props.orientation || this.flexHasChanged(props)) {
var flexData = this.computeFlexData(children, props);
this.setState({
flexData: 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
//
/////////////////////////////////////////////////////////
}, {
key: "flexHasChanged",
/////////////////////////////////////////////////////////
// Check if flex has changed: this allows updating the
// component when different flex is passed as property
// to one or several children
//
/////////////////////////////////////////////////////////
value: function flexHasChanged(props) {
var nextChildrenFlex = this.getValidChildren(props).map(function (child) {
return child.props.flex || 0;
});
var childrenFlex = this.getValidChildren().map(function (child) {
return child.props.flex || 0;
});
return !childrenFlex.every(function (flex, idx) {
return flex === nextChildrenFlex[idx];
});
} /////////////////////////////////////////////////////////
// Returns size of a ReflexElement
//
/////////////////////////////////////////////////////////
}, {
key: "getSize",
value: function getSize(element) {
var ref = element.ref ? this.refs[element.ref] : element;
var domElement = _reactDom.default.findDOMNode(ref);
switch (this.props.orientation) {
case 'horizontal':
return domElement.offsetHeight;
case 'vertical':
default:
return domElement.offsetWidth;
}
} /////////////////////////////////////////////////////////
// Computes offset from pointer position
//
/////////////////////////////////////////////////////////
}, {
key: "getOffset",
value: function getOffset(event) {
var pos = event.changedTouches ? event.changedTouches[0] : event;
switch (this.props.orientation) {
case 'horizontal':
return pos.pageY - this.previousPos;
case 'vertical':
default:
return pos.pageX - this.previousPos;
}
} /////////////////////////////////////////////////////////
// Handles startResize event
//
/////////////////////////////////////////////////////////
}, {
key: "adjustFlex",
/////////////////////////////////////////////////////////
// Adjusts flex after a dispatch to make sure
// total flex of modified elements remains the same
//
/////////////////////////////////////////////////////////
value: function adjustFlex(elements) {
var _this2 = this;
var diffFlex = elements.reduce(function (sum, element) {
var idx = element.props.index;
var previousFlex = element.props.flex;
var nextFlex = _this2.state.flexData[idx].flex;
return sum + (previousFlex - nextFlex) / elements.length;
}, 0);
elements.forEach(function (element) {
_this2.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
//
/////////////////////////////////////////////////////////
}, {
key: "computeAvailableOffset",
value: function computeAvailableOffset(idx, offset) {
var stretch = this.computeAvailableStretch(idx, offset);
var shrink = this.computeAvailableShrink(idx, offset);
var 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
//
/////////////////////////////////////////////////////////
}, {
key: "checkPropagate",
value: function checkPropagate(idx, direction) {
if (direction > 0) {
if (idx < this.children.length - 2) {
var child = this.children[idx + 2];
var typeCheck = _ReflexSplitter.default.isA(child);
return typeCheck && child.props.propagate;
}
} else {
if (idx > 2) {
var _child = this.children[idx - 2];
var _typeCheck = _ReflexSplitter.default.isA(_child);
return _typeCheck && _child.props.propagate;
}
}
return false;
} /////////////////////////////////////////////////////////
// Recursively computes available stretch at splitter
// idx for given raw offset
//
/////////////////////////////////////////////////////////
}, {
key: "computeAvailableStretch",
value: function computeAvailableStretch(idx, offset) {
var childIdx = offset < 0 ? idx + 1 : idx - 1;
var child = this.children[childIdx];
var size = this.getSize(child);
var maxSize = child.props.maxSize;
var availableStretch = maxSize - size;
if (availableStretch < Math.abs(offset)) {
if (this.checkPropagate(idx, -1 * offset)) {
var 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
//
/////////////////////////////////////////////////////////
}, {
key: "computeAvailableShrink",
value: function computeAvailableShrink(idx, offset) {
var childIdx = offset > 0 ? idx + 1 : idx - 1;
var child = this.children[childIdx];
var size = this.getSize(child);
var minSize = Math.max(child.props.minSize, 0);
var availableShrink = size - minSize;
if (availableShrink < Math.abs(offset)) {
if (this.checkPropagate(idx, offset)) {
var 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
//
/////////////////////////////////////////////////////////
}, {
key: "computePixelFlex",
value: function computePixelFlex() {
var orientation = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props.orientation;
var domElement = _reactDom.default.findDOMNode(this);
switch (orientation) {
case 'horizontal':
if (domElement.offsetHeight === 0.0) {
console.warn('Found ReflexContainer with height=0, ' + 'this will cause invalid behavior...');
console.warn(domElement);
return 0.0;
}
return 1.0 / domElement.offsetHeight;
case 'vertical':
default:
if (domElement.offsetWidth === 0.0) {
console.warn('Found ReflexContainer with width=0, ' + 'this will cause invalid behavior...');
console.warn(domElement);
return 0.0;
}
return 1.0 / domElement.offsetWidth;
}
} /////////////////////////////////////////////////////////
// Adds offset to a given ReflexElement
//
/////////////////////////////////////////////////////////
}, {
key: "addOffset",
value: function addOffset(element, offset) {
var size = this.getSize(element);
var idx = element.props.index;
var newSize = Math.max(size + offset, 0);
var currentFlex = this.state.flexData[idx].flex;
var 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
//
/////////////////////////////////////////////////////////
}, {
key: "dispatchStretch",
value: function dispatchStretch(idx, offset) {
var childIdx = offset < 0 ? idx + 1 : idx - 1;
if (childIdx < 0 || childIdx > this.children.length - 1) {
return [];
}
var child = this.children[childIdx];
var size = this.getSize(child);
var newSize = Math.min(child.props.maxSize, size + Math.abs(offset));
var dispatchedStretch = newSize - size;
this.addOffset(child, dispatchedStretch);
if (dispatchedStretch < Math.abs(offset)) {
var nextIdx = idx - Math.sign(offset) * 2;
var nextOffset = Math.sign(offset) * (Math.abs(offset) - dispatchedStretch);
return [child].concat((0, _toConsumableArray2.default)(this.dispatchStretch(nextIdx, nextOffset)));
}
return [child];
} /////////////////////////////////////////////////////////
// Recursively dispatches shrink offset across
// children elements starting at splitter idx
//
/////////////////////////////////////////////////////////
}, {
key: "dispatchShrink",
value: function dispatchShrink(idx, offset) {
var childIdx = offset > 0 ? idx + 1 : idx - 1;
if (childIdx < 0 || childIdx > this.children.length - 1) {
return [];
}
var child = this.children[childIdx];
var size = this.getSize(child);
var newSize = Math.max(child.props.minSize, size - Math.abs(offset));
var dispatchedShrink = newSize - size;
this.addOffset(child, dispatchedShrink);
if (Math.abs(dispatchedShrink) < Math.abs(offset)) {
var nextIdx = idx + Math.sign(offset) * 2;
var nextOffset = Math.sign(offset) * (Math.abs(offset) + dispatchedShrink);
return [child].concat((0, _toConsumableArray2.default)(this.dispatchShrink(nextIdx, nextOffset)));
}
return [child];
} /////////////////////////////////////////////////////////
// Dispatch offset at splitter idx
//
/////////////////////////////////////////////////////////
}, {
key: "dispatchOffset",
value: function dispatchOffset(idx, offset) {
return (0, _toConsumableArray2.default)(this.dispatchStretch(idx, offset)).concat((0, _toConsumableArray2.default)(this.dispatchShrink(idx, offset)));
} /////////////////////////////////////////////////////////
// Emits given if event for each given element
// if present in the component props
//
/////////////////////////////////////////////////////////
}, {
key: "emitElementsEvent",
value: function emitElementsEvent(elements, event) {
var _this3 = this;
this.toArray(elements).forEach(function (component) {
if (component.props[event]) {
var ref = _this3.refs[component.ref];
var domElement = _reactDom.default.findDOMNode(ref);
component.props[event]({
domElement: domElement,
component: component
});
}
});
} /////////////////////////////////////////////////////////
// Computes initial flex data based on provided flex
// properties. By default each ReflexElement gets
// evenly arranged within its container
//
/////////////////////////////////////////////////////////
}, {
key: "computeFlexData",
value: function computeFlexData() {
var _this4 = this;
var children = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getValidChildren();
var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props;
var pixelFlex = this.computePixelFlex(props.orientation);
var computeFreeFlex = function computeFreeFlex(flexData) {
return flexData.reduce(function (sum, entry) {
if (!_ReflexSplitter.default.isA(entry) && entry.constrained) {
return sum - entry.flex;
}
return sum;
}, 1.0);
};
var computeFreeElements = function computeFreeElements(flexData) {
return flexData.reduce(function (sum, entry) {
if (!_ReflexSplitter.default.isA(entry) && !entry.constrained) {
return sum + 1;
}
return sum;
}, 0.0);
};
var flexDataInit = children.map(function (child) {
var 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,
guid: props.ref || _this4.guid(),
flex: props.flex || 0,
type: child.type
};
});
var computeFlexDataRec = function computeFlexDataRec(flexDataIn) {
var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var hasContrain = false;
var freeElements = computeFreeElements(flexDataIn);
var freeFlex = computeFreeFlex(flexDataIn);
var flexDataOut = flexDataIn.map(function (entry, idx) {
if (_ReflexSplitter.default.isA(entry)) {
return entry;
}
var proposedFlex = !entry.constrained ? freeFlex / freeElements : entry.flex;
var constrainedFlex = Math.min(entry.sizeFlex, Math.min(entry.maxFlex, Math.max(entry.minFlex, proposedFlex)));
var constrained = constrainedFlex !== proposedFlex;
hasContrain = hasContrain || constrained;
return (0, _objectSpread2.default)({}, entry, {
flex: constrainedFlex,
constrained: constrained
});
});
return hasContrain && depth < _this4.props.maxRecDepth ? computeFlexDataRec(flexDataOut, depth + 1) : flexDataOut;
};
var flexData = computeFlexDataRec(flexDataInit);
return flexData.map(function (entry) {
return {
flex: !_ReflexSplitter.default.isA(entry) ? entry.flex : 0.0,
guid: entry.guid
};
});
} /////////////////////////////////////////////////////////
// Utility method that generates a new unique GUID
//
/////////////////////////////////////////////////////////
}, {
key: "guid",
value: function guid() {
var format = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'xxxx-xxxx';
var d = new Date().getTime();
return format.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : r & 0x7 | 0x8).toString(16);
});
} /////////////////////////////////////////////////////////
// Utility method to ensure given argument is
// returned as an array
//
/////////////////////////////////////////////////////////
}, {
key: "toArray",
value: function 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
//
/////////////////////////////////////////////////////////
}, {
key: "render",
value: function render() {
var _this5 = this;
var className = [this.state.resizing ? 'reflex-resizing' : ''].concat((0, _toConsumableArray2.default)(this.props.className.split(' ')), [this.props.orientation, 'reflex-container']).join(' ').trim();
this.children = _react.default.Children.map(this.getValidChildren(), function (child, index) {
if (index > _this5.state.flexData.length - 1) {
return _react.default.createElement("div", null);
}
var flexData = _this5.state.flexData[index];
var newProps = (0, _objectSpread2.default)({}, child.props, {
maxSize: child.props.maxSize || Number.MAX_VALUE,
orientation: _this5.props.orientation,
minSize: child.props.minSize || 1,
events: _this5.events,
flex: flexData.flex,
ref: flexData.guid,
index: index
});
return _react.default.cloneElement(child, newProps);
});
return _react.default.createElement("div", (0, _extends2.default)({}, (0, _utilities.getDataProps)(this.props), {
style: this.props.style,
className: className
}), this.children);
}
}]);
return ReflexContainer;
}(_react.default.Component);
exports.default = ReflexContainer;
(0, _defineProperty2.default)(ReflexContainer, "propTypes", {
windowResizeAware: _propTypes.default.bool,
orientation: _propTypes.default.oneOf(['horizontal', 'vertical']),
maxRecDepth: _propTypes.default.number,
className: _propTypes.default.string,
style: _propTypes.default.object /////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
});
(0, _defineProperty2.default)(ReflexContainer, "defaultProps", {
orientation: 'horizontal',
windowResizeAware: false,
maxRecDepth: 100,
className: '',
style: {} /////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
});