react-reflex
Version:
Flex layout component for advanced React web applications
665 lines (641 loc) • 27.8 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 _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
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 _react = _interopRequireDefault(require("react"));
require("./Polyfills");
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
var ReflexContainer = /*#__PURE__*/function (_React$Component) {
(0, _inherits2.default)(ReflexContainer, _React$Component);
var _super = _createSuper(ReflexContainer);
/////////////////////////////////////////////////////////
// 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 = _super.call(this, props);
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onWindowResize", function () {
_this.setState({
flexData: _this.computeFlexData()
});
});
(0, _defineProperty2.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('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');
});
(0, _defineProperty2.default)((0, _assertThisInitialized2.default)(_this), "onResize", function (data) {
var pos = data.event.changedTouches ? data.event.changedTouches[0] : data.event;
var 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) {
var availableOffset = _this.computeAvailableOffset(data.index, offset);
if (availableOffset) {
_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)(_this), "onStopResize", function (data) {
document.body.classList.remove('reflex-row-resize');
document.body.classList.remove('reflex-col-resize');
var resizedRefs = _this.elements ? _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)(_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: []
};
_this.ref = _react.default.createRef();
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();
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;
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
var children = this.getValidChildren(this.props);
if (children.length !== this.state.flexData.length || prevProps.orientation !== this.props.orientation || this.flexHasChanged(prevProps)) {
var flexData = this.computeFlexData(children, this.props);
this.setState({
flexData: 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
//
/////////////////////////////////////////////////////////
}, {
key: "flexHasChanged",
value:
/////////////////////////////////////////////////////////
// Check if flex has changed: this allows updating the
// component when different flex is passed as property
// to one or several children
//
/////////////////////////////////////////////////////////
function flexHasChanged(prevProps) {
var prevChildrenFlex = this.getValidChildren(prevProps).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 === prevChildrenFlex[idx];
});
}
/////////////////////////////////////////////////////////
// Returns size of a ReflexElement
//
/////////////////////////////////////////////////////////
}, {
key: "getSize",
value: function getSize(element) {
var _element$ref, _domElement$offsetHei, _domElement$offsetWid;
var 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
//
/////////////////////////////////////////////////////////
}, {
key: "getOffset",
value: function getOffset(pos, domElement) {
var _domElement$getBoundi = domElement.getBoundingClientRect(),
top = _domElement$getBoundi.top,
bottom = _domElement$getBoundi.bottom,
left = _domElement$getBoundi.left,
right = _domElement$getBoundi.right;
switch (this.props.orientation) {
case 'horizontal':
{
var 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:
{
var _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
//
/////////////////////////////////////////////////////////
}, {
key: "adjustFlex",
value:
/////////////////////////////////////////////////////////
// Adjusts flex after a dispatch to make sure
// total flex of modified elements remains the same
//
/////////////////////////////////////////////////////////
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 _child$props$maxSize;
var childIdx = offset < 0 ? idx + 1 : idx - 1;
var child = this.children[childIdx];
var size = this.getSize(child);
var maxSize = (_child$props$maxSize = child === null || child === void 0 ? void 0 : child.props.maxSize) !== null && _child$props$maxSize !== void 0 ? _child$props$maxSize : 0;
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 _child$props$minSize;
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 = child === null || child === void 0 ? void 0 : child.props.minSize) !== null && _child$props$minSize !== void 0 ? _child$props$minSize : 0, 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;
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
//
/////////////////////////////////////////////////////////
}, {
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 [].concat((0, _toConsumableArray2.default)(this.dispatchStretch(idx, offset)), (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) {
this.toArray(elements).forEach(function (component) {
if (component.props[event]) {
component.props[event]({
domElement: component.ref.current,
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 _this3 = 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,
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) {
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 = entry.constrained || constrainedFlex !== proposedFlex;
hasContrain = hasContrain || constrained;
return (0, _objectSpread2.default)({}, entry, {
flex: constrainedFlex,
constrained: constrained
});
});
return hasContrain && depth < _this3.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,
ref: _react.default.createRef()
};
});
}
/////////////////////////////////////////////////////////
// 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 _this4 = 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 > _this4.state.flexData.length - 1) {
return /*#__PURE__*/_react.default.createElement("div", null);
}
var flexData = _this4.state.flexData[index];
var newProps = (0, _objectSpread2.default)({}, child.props, {
maxSize: child.props.maxSize || Number.MAX_VALUE,
orientation: _this4.props.orientation,
minSize: child.props.minSize || 1,
events: _this4.events,
flex: flexData.flex,
ref: flexData.ref,
index: index
});
return _react.default.cloneElement(child, newProps);
});
return /*#__PURE__*/_react.default.createElement("div", (0, _extends2.default)({}, (0, _utilities.getDataProps)(this.props), {
style: this.props.style,
className: className,
ref: this.ref
}), 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: {}
});