office-ui-fabric-react
Version: 
Reusable React components for building experiences for Office 365.
260 lines • 13.6 kB
JavaScript
define(["require", "exports", "tslib", "react", "../../Utilities", "./ResizeGroup.scss"], function (require, exports, tslib_1, React, Utilities_1, styles) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var RESIZE_DELAY = 16;
    /**
     * Returns a simple object is able to store measurements with a given key.
     */
    exports.getMeasurementCache = function () {
        var measurementsCache = {};
        return {
            /**
             * Checks if the provided data has a cacheKey. If it has a cacheKey and there is a
             * corresponding entry in the measurementsCache, then it will return that value.
             * Returns undefined otherwise.
             */
            getCachedMeasurement: function (data) {
                if (data && data.cacheKey && measurementsCache.hasOwnProperty(data.cacheKey)) {
                    return measurementsCache[data.cacheKey];
                }
                return undefined;
            },
            /**
             * Should be called whenever there is a new measurement associated with a given data object.
             * If the data has a cacheKey, store that measurement in the measurementsCache.
             */
            addMeasurementToCache: function (data, measurement) {
                if (data.cacheKey) {
                    measurementsCache[data.cacheKey] = measurement;
                }
            }
        };
    };
    /**
     * Returns a function that is able to compute the next state for the ResizeGroup given the current
     * state and any measurement updates.
     */
    exports.getNextResizeGroupStateProvider = function (measurementCache) {
        if (measurementCache === void 0) { measurementCache = exports.getMeasurementCache(); }
        var _measurementCache = measurementCache;
        var _containerWidth;
        /**
         * Gets the width of the data rendered in a hidden div.
         * @param measuredData - The data corresponding to the measurement we wish to take.
         * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
         * is not in the cache.
         */
        function _getMeasuredWidth(measuredData, getElementToMeasureWidth) {
            var cachedWidth = _measurementCache.getCachedMeasurement(measuredData);
            if (cachedWidth !== undefined) {
                return cachedWidth;
            }
            var measuredWidth = getElementToMeasureWidth();
            _measurementCache.addMeasurementToCache(measuredData, measuredWidth);
            return measuredWidth;
        }
        /**
         * Will get the next IResizeGroupState based on the current data while trying to shrink contents
         * to fit in the container.
         * @param data - The initial data point to start measuring.
         * @param onReduceData - Function that transforms the data into something that should render with less width.
         * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
         * is not in the cache.
         */
        function _shrinkContentsUntilTheyFit(data, onReduceData, getElementToMeasureWidth) {
            var dataToMeasure = data;
            var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
            while (measuredWidth > _containerWidth) {
                var nextMeasuredData = onReduceData(dataToMeasure);
                // We don't want to get stuck in an infinite render loop when there are no more
                // scaling steps, so implementations of onReduceData should return undefined when
                // there are no more scaling states to apply.
                if (nextMeasuredData === undefined) {
                    return {
                        renderedData: dataToMeasure,
                        resizeDirection: undefined,
                        dataToMeasure: undefined
                    };
                }
                measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
                // If the measurement isn't in the cache, we need to rerender with some data in a hidden div
                if (measuredWidth === undefined) {
                    return {
                        dataToMeasure: nextMeasuredData,
                        resizeDirection: 'shrink'
                    };
                }
                dataToMeasure = nextMeasuredData;
            }
            return {
                renderedData: dataToMeasure,
                resizeDirection: undefined,
                dataToMeasure: undefined
            };
        }
        /**
         * This function should be called when the state changes in a manner that might allow for more content to fit
         * on the screen, such as the window width growing.
         * @param data - The initial data point to start measuring.
         * @param onGrowData - Function that transforms the data into something that may take up more space when rendering.
         * @param getElementToMeasureWidth - A function that returns the measurement of the rendered data. Only called when the measurement
         * is not in the cache.
         */
        function _growDataUntilItDoesNotFit(data, onGrowData, getElementToMeasureWidth) {
            var dataToMeasure = data;
            var measuredWidth = _getMeasuredWidth(data, getElementToMeasureWidth);
            while (measuredWidth < _containerWidth) {
                var nextMeasuredData = onGrowData(dataToMeasure);
                // We don't want to get stuck in an infinite render loop when there are no more
                // scaling steps, so implementations of onGrowData should return undefined when
                // there are no more scaling states to apply.
                if (nextMeasuredData === undefined) {
                    return {
                        renderedData: dataToMeasure,
                        resizeDirection: undefined,
                        dataToMeasure: undefined
                    };
                }
                measuredWidth = _measurementCache.getCachedMeasurement(nextMeasuredData);
                // If the measurement isn't in the cache, we need to rerender with some data in a hidden div
                if (measuredWidth === undefined) {
                    return {
                        dataToMeasure: nextMeasuredData
                    };
                }
                dataToMeasure = nextMeasuredData;
            }
            // Once the loop is done, we should now shrink until the contents fit.
            return {
                dataToMeasure: dataToMeasure,
                resizeDirection: 'shrink'
            };
        }
        /**
         * Handles an update to the container width. Should only be called when we knew the previous container width.
         * @param newWidth - The new width of the container.
         * @param fullWidthData - The initial data passed in as a prop to resizeGroup.
         * @param renderedData - The data that was rendered prior to the container size changing.
         * @param onGrowData - Set to true if the Resize group has an onGrowData function.
         */
        function _updateContainerWidth(newWidth, fullWidthData, renderedData, onGrowData) {
            var nextState;
            if (newWidth > _containerWidth) {
                if (onGrowData) {
                    nextState = {
                        resizeDirection: 'grow',
                        dataToMeasure: onGrowData(renderedData)
                    };
                }
                else {
                    nextState = {
                        resizeDirection: 'shrink',
                        dataToMeasure: fullWidthData
                    };
                }
            }
            else {
                nextState = {
                    resizeDirection: 'shrink',
                    dataToMeasure: renderedData
                };
            }
            _containerWidth = newWidth;
            return tslib_1.__assign({}, nextState, { measureContainer: false });
        }
        function getNextState(props, currentState, getElementToMeasureWidth, newContainerWidth) {
            // If there is no new container width or data to measure, there is no need for a new state update
            if (newContainerWidth === undefined && currentState.dataToMeasure === undefined) {
                return undefined;
            }
            if (newContainerWidth) {
                // If we know what the last container size was and we rendered data at that width, we can do an optimized render
                if (_containerWidth && currentState.renderedData && !currentState.dataToMeasure) {
                    return tslib_1.__assign({}, currentState, _updateContainerWidth(newContainerWidth, props.data, currentState.renderedData, props.onGrowData));
                }
                // If we are just setting the container width for the first time, we can't do any optimizations
                _containerWidth = newContainerWidth;
            }
            var nextState = tslib_1.__assign({}, currentState, { measureContainer: false });
            if (currentState.dataToMeasure) {
                if (currentState.resizeDirection === 'grow' && props.onGrowData) {
                    nextState = tslib_1.__assign({}, nextState, _growDataUntilItDoesNotFit(currentState.dataToMeasure, props.onGrowData, getElementToMeasureWidth));
                }
                else {
                    nextState = tslib_1.__assign({}, nextState, _shrinkContentsUntilTheyFit(currentState.dataToMeasure, props.onReduceData, getElementToMeasureWidth));
                }
            }
            return nextState;
        }
        function shouldRenderDataToMeasureInHiddenDiv(dataToMeasure) {
            if (!dataToMeasure || _measurementCache.getCachedMeasurement(dataToMeasure) !== undefined) {
                return false;
            }
            return true;
        }
        return {
            getNextState: getNextState,
            shouldRenderDataToMeasureInHiddenDiv: shouldRenderDataToMeasureInHiddenDiv
        };
    };
    var ResizeGroup = (function (_super) {
        tslib_1.__extends(ResizeGroup, _super);
        function ResizeGroup(props) {
            var _this = _super.call(this, props) || this;
            _this._nextResizeGroupStateProvider = exports.getNextResizeGroupStateProvider();
            _this.state = {
                dataToMeasure: tslib_1.__assign({}, _this.props.data),
                resizeDirection: 'grow',
                measureContainer: true,
            };
            return _this;
        }
        ResizeGroup.prototype.render = function () {
            var onRenderData = this.props.onRenderData;
            var _a = this.state, dataToMeasure = _a.dataToMeasure, renderedData = _a.renderedData;
            return (React.createElement("div", { className: Utilities_1.css('ms-ResizeGroup'), ref: this._resolveRef('_root') },
                this._nextResizeGroupStateProvider.shouldRenderDataToMeasureInHiddenDiv(dataToMeasure) && (React.createElement("div", { className: Utilities_1.css(styles.measured), ref: this._resolveRef('_measured') }, onRenderData(dataToMeasure))),
                renderedData && onRenderData(renderedData)));
        };
        ResizeGroup.prototype.componentDidMount = function () {
            this._afterComponentRendered();
            this._events.on(window, 'resize', this._async.debounce(this._onResize, RESIZE_DELAY, { leading: true }));
        };
        ResizeGroup.prototype.componentWillReceiveProps = function (nextProps) {
            this.setState({
                dataToMeasure: tslib_1.__assign({}, nextProps.data),
                resizeDirection: 'grow',
                measureContainer: true // Receiving new props means the parent might rerender and the root width might change
            });
        };
        ResizeGroup.prototype.componentDidUpdate = function (prevProps) {
            if (this.state.renderedData) {
                if (this.props.dataDidRender) {
                    this.props.dataDidRender(this.state.renderedData);
                }
            }
            this._afterComponentRendered();
        };
        ResizeGroup.prototype._afterComponentRendered = function () {
            var _this = this;
            this._async.requestAnimationFrame(function () {
                var containerWidth = undefined;
                if (_this.state.measureContainer) {
                    containerWidth = _this._root.getBoundingClientRect().width;
                }
                var nextState = _this._nextResizeGroupStateProvider.getNextState(_this.props, _this.state, function () { return _this._measured.scrollWidth; }, containerWidth);
                if (nextState) {
                    _this.setState(nextState);
                }
            });
        };
        ResizeGroup.prototype._onResize = function () {
            if (this._root) {
                this.setState({ measureContainer: true });
            }
        };
        return ResizeGroup;
    }(Utilities_1.BaseComponent));
    exports.ResizeGroup = ResizeGroup;
});
//# sourceMappingURL=ResizeGroup.js.map