UNPKG

react-windowed-list

Version:

A fast, versatile virtual-render list component for React

608 lines (510 loc) 19.2 kB
'use strict'; exports.__esModule = true; exports.updateVariableFrame = exports.updateUniformFrame = exports.updateSimpleFrame = exports.updateScrollParent = exports.updateFrame = exports.setStateIfAppropriate = exports.setScroll = exports.setReconcileFrameAfterUpdate = exports.scrollTo = exports.scrollAround = exports.renderItems = exports.getVisibleRange = exports.getStartAndEnd = exports.getSpaceBefore = exports.getSizeOfListItem = exports.getScrollParent = exports.getScrollOffset = exports.getItemSizeAndItemsPerRow = undefined; var _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; }; // external dependencies // constants // utils var _debounce = require('debounce'); var _debounce2 = _interopRequireDefault(_debounce); var _raf = require('raf'); var _raf2 = _interopRequireDefault(_raf); var _reactDom = require('react-dom'); var _constants = require('./constants'); var _utils = require('./utils'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * @function getItemSizeAndItemsPerRow * * @description * get the itemSize and itemsPerRow values based on props * * @param {Array<ReactElement>} items the items passed * @param {string} axis the axis being scrolled on * @param {boolean} useStaticSize should a static size be used * @param {number} itemSize the current size of the item in state * @param {number} itemsPerRow the current number of rows in state * @returns {Object} the itemSize and itemsPerRow */ var getItemSizeAndItemsPerRow = exports.getItemSizeAndItemsPerRow = function getItemSizeAndItemsPerRow(_ref) { var items = _ref.items, _ref$props = _ref.props, axis = _ref$props.axis, useStaticSize = _ref$props.useStaticSize, _ref$state = _ref.state, itemSize = _ref$state.itemSize, itemsPerRow = _ref$state.itemsPerRow; if (useStaticSize && itemSize && itemsPerRow) { return { itemSize: itemSize, itemsPerRow: itemsPerRow }; } var itemElements = items ? items.children : []; return itemElements.length ? (0, _utils.getCalculatedItemSizeAndItemsPerRow)(itemElements, axis, itemSize) : {}; }; /** * @function getScrollOffset * * @description * get the scroll offset based on props * * @param {HTMLElement} outerContainer the outer container * @param {string} axis the axis being scrolled on * @param {HTMLElement} scrollParent the parent being scrolled * @returns {number} the scrollOffset to apply */ var getScrollOffset = exports.getScrollOffset = function getScrollOffset(_ref2) { var outerContainer = _ref2.outerContainer, axis = _ref2.props.axis, scrollParent = _ref2.scrollParent; if (!outerContainer || !scrollParent) { return 0; } var scrollKey = _constants.SCROLL_START_KEYS[axis]; var actual = scrollParent === window ? document.body[scrollKey] || document.documentElement[scrollKey] : scrollParent[scrollKey]; var max = (0, _utils.getScrollSize)(scrollParent, axis) - (0, _utils.getViewportSize)(scrollParent, axis); var scroll = Math.max(0, Math.min(actual, max)); return (0, _utils.getOffset)(scrollParent, axis) + scroll - (0, _utils.getOffset)(outerContainer, axis); }; /** * @function getScrollParent * * @description * get the scroll parent element * * @param {HTMLElement} outerContainer the outer container * @param {string} axis the axis being scrolled on * @param {function} getScrollParent the method to get the scrollParent * @returns {HTMLElement} the scroll parent */ var getScrollParent = exports.getScrollParent = function getScrollParent(_ref3) { var outerContainer = _ref3.outerContainer, _ref3$props = _ref3.props, axis = _ref3$props.axis, getScrollParent = _ref3$props.getScrollParent; if ((0, _utils.isFunction)(getScrollParent)) { return getScrollParent(); } if (!outerContainer) { return null; } var overflowKey = _constants.OVERFLOW_KEYS[axis]; var element = outerContainer; while (element = element.parentElement) { if (~_constants.OVERFLOW_VALUES.indexOf(window.getComputedStyle(element)[overflowKey])) { return element; } } return window; }; /** * @function getSizeOfListItem * * @description * get the size of the list item requested * * @param {Object} cache the current cache in state * @param {Array<ReactElement>} items the items rendered * @param {string} axis the axis being scrolled on * @param {function} getEstimatedItemSize the method used to estimate the item size * @param {function} getItemSize the method used to get the item size * @param {string} type the list type * @param {number} from the first index being rendered * @param {number} itemSize the size of each item * @param {number} size the number of items being rendered * @param {number} index the index of the list item * @returns {number} the size of the list item */ var getSizeOfListItem = exports.getSizeOfListItem = function getSizeOfListItem(_ref4, _ref5) { var cache = _ref4.cache, items = _ref4.items, _ref4$props = _ref4.props, axis = _ref4$props.axis, getEstimatedItemSize = _ref4$props.getEstimatedItemSize, getItemSize = _ref4$props.getItemSize, type = _ref4$props.type, _ref4$state = _ref4.state, from = _ref4$state.from, itemSize = _ref4$state.itemSize, size = _ref4$state.size; var index = _ref5[0]; // Try the static itemSize. if (itemSize) { return itemSize; } // Try the getItemSize. if ((0, _utils.isFunction)(getItemSize)) { return getItemSize(index); } // Try the cache. if ((0, _utils.isNumber)(cache[index])) { return cache[index]; } if (items) { var itemElements = items.children; // Try the DOM. if (itemElements.length && type === _constants.VALID_TYPES.SIMPLE && index >= from && index < from + size) { var itemEl = itemElements[index - from]; if (itemEl) { return itemEl[_constants.OFFSET_SIZE_KEYS[axis]]; } } } // Try the getEstimatedItemSize. if ((0, _utils.isFunction)(getEstimatedItemSize)) { return getEstimatedItemSize(index, cache); } }; /** * @function getSpaceBefore * * @description * get the space before the item requested * * @param {function} getSizeOfListItem method to get the size of the item * @param {number} itemSize the size of each item * @param {number} itemsPerRow the number of items per row * @param {number} index the index of the item requested * @param {Object} [cache={}] the instance cache * @returns {number} the space before the item requested */ var getSpaceBefore = exports.getSpaceBefore = function getSpaceBefore(_ref6, _ref7) { var getSizeOfListItem = _ref6.getSizeOfListItem, _ref6$state = _ref6.state, itemSize = _ref6$state.itemSize, itemsPerRow = _ref6$state.itemsPerRow; var index = _ref7[0], _ref7$ = _ref7[1], cache = _ref7$ === undefined ? {} : _ref7$; if ((0, _utils.isNumber)(cache[index])) { return cache[index]; } cache[index] = itemSize ? Math.floor(index / itemsPerRow) * itemSize : (0, _utils.getCalculatedSpaceBefore)(cache, index, getSizeOfListItem); return cache[index]; }; /** * @function getStartAndEnd * * @description * get the start and end values based on scroll position * * @param {function} getScrollOffset the method to get the scroll offset * @param {function} getSpaceBefore the method to get the space before the first rendered item * @param {string} axis the scroll axis * @param {function} getItemSize the method used to get the item size * @param {number} length the number of total items * @param {number} defaultThreshold the threshold value * @param {string} type the type of renderer used * @param {number} [threshold=defaultThreshold] the pixel threshold to scroll above and below * @returns {{end: number, start: number}} the start and end of the window */ var getStartAndEnd = exports.getStartAndEnd = function getStartAndEnd(_ref8, _ref9) { var getScrollOffset = _ref8.getScrollOffset, getSpaceBefore = _ref8.getSpaceBefore, _ref8$props = _ref8.props, axis = _ref8$props.axis, getItemSize = _ref8$props.getItemSize, length = _ref8$props.length, defaultThreshold = _ref8$props.threshold, type = _ref8$props.type, scrollParent = _ref8.scrollParent; var _ref9$ = _ref9[0], threshold = _ref9$ === undefined ? defaultThreshold : _ref9$; var scroll = getScrollOffset(); var calculatedEnd = scroll + (0, _utils.getViewportSize)(scrollParent, axis) + threshold; return { end: (0, _utils.hasDeterminateSize)(type, getItemSize) ? Math.min(calculatedEnd, getSpaceBefore(length)) : calculatedEnd, start: Math.max(0, scroll - threshold) }; }; /** * @function getVisibleRange * * @description * get the indices of the first and last items that are visible in the viewport * * @returns {Array<number>} the first and last index of the visible items */ var getVisibleRange = exports.getVisibleRange = function getVisibleRange(_ref10) { var getSizeOfListItem = _ref10.getSizeOfListItem, getSpaceBefore = _ref10.getSpaceBefore, getStartAndEnd = _ref10.getStartAndEnd, _ref10$state = _ref10.state, from = _ref10$state.from, size = _ref10$state.size; var _getStartAndEnd = getStartAndEnd(0), end = _getStartAndEnd.end, start = _getStartAndEnd.start; var cache = {}; var length = from + size; var first = void 0, last = void 0, itemStart = void 0, itemEnd = void 0; for (var index = from; index < length; index++) { itemStart = getSpaceBefore(index, cache); itemEnd = itemStart + getSizeOfListItem(index); if (!(0, _utils.isNumber)(first)) { if (itemEnd > start) { first = index; } } else if (itemStart < end) { last = index; } } return [first, last]; }; /** * @function renderItems * * @description * render the items that are currently visible * * @param {ReactComponent} instance the component instance * @returns {ReactElement} the rendered container with the items */ var renderItems = exports.renderItems = function renderItems(instance) { var _instance$props = instance.props, itemRenderer = _instance$props.itemRenderer, containerRenderer = _instance$props.containerRenderer, _instance$state = instance.state, from = _instance$state.from, size = _instance$state.size; var items = new Array(size); for (var index = 0; index < size; index++) { items[index] = itemRenderer(from + index, index); } return containerRenderer(items, function (containerRef) { return instance.items = (0, _reactDom.findDOMNode)(containerRef); }); }; /** * @function scrollAround * * @description * scroll to a point that the item is within the window, but not necessarily at the top * * @param {number} index the index to scroll to in the window * @returns {void} */ var scrollAround = exports.scrollAround = function scrollAround(_ref11, _ref12) { var getScrollOffset = _ref11.getScrollOffset, getSizeOfListItem = _ref11.getSizeOfListItem, getSpaceBefore = _ref11.getSpaceBefore, getViewportSize = _ref11.getViewportSize, setScroll = _ref11.setScroll; var index = _ref12[0]; var bottom = getSpaceBefore(index); var top = bottom - getViewportSize() + getSizeOfListItem(index); var min = Math.min(top, bottom); var current = getScrollOffset(); if (current <= min) { return setScroll(min); } var max = Math.max(top, bottom); if (current > max) { return setScroll(max); } }; /** * @function scrollTo * * @description * scroll the element to the requested initialIndex * * @param {number} index the index to scroll to */ var scrollTo = exports.scrollTo = function scrollTo(_ref13, _ref14) { var getSpaceBefore = _ref13.getSpaceBefore, initialIndex = _ref13.props.initialIndex, setScroll = _ref13.setScroll; var index = _ref14[0]; var indexToScrollTo = (0, _utils.isNumber)(index) ? index : initialIndex; if ((0, _utils.isNumber)(indexToScrollTo)) { setScroll(getSpaceBefore(indexToScrollTo)); } }; /** * @function setReconcileFrameAfterUpdate * * @description * set the frame reconciler used after componentDidUpdate * * @param {ReactComponent} instance the component instance */ var setReconcileFrameAfterUpdate = exports.setReconcileFrameAfterUpdate = function setReconcileFrameAfterUpdate(instance) { var debounceReconciler = instance.props.debounceReconciler; instance.reconcileFrameAfterUpdate = (0, _utils.isNumber)(debounceReconciler) ? (0, _debounce2.default)(function (updateFrame) { updateFrame(); }, debounceReconciler) : _raf2.default; }; /** * @function setScroll * * @description * set the scroll based on the current offset * * @param {number} currentOffset the current offset * @returns {void} */ var setScroll = exports.setScroll = function setScroll(_ref15, _ref16) { var outerContainer = _ref15.outerContainer, axis = _ref15.props.axis, scrollParent = _ref15.scrollParent; var currentOffset = _ref16[0]; if (!scrollParent || !outerContainer) { return; } var offset = currentOffset + (0, _utils.getOffset)(outerContainer, axis); if (scrollParent === window) { return window.scrollTo(0, offset); } scrollParent[_constants.SCROLL_START_KEYS[axis]] = offset - (0, _utils.getOffset)(scrollParent, axis); }; /** * @function setStateIfAppropriate * * @description * set the state if areStateValuesEqual returns true * * @param {Object} nextState the possible next state of the instance * @param {function} callback the callback to call once the state is set * @returns {void} */ var setStateIfAppropriate = exports.setStateIfAppropriate = function setStateIfAppropriate(_ref17, _ref18) { var setState = _ref17.setState, state = _ref17.state; var nextState = _ref18[0], callback = _ref18[1]; return (0, _utils.areStateValuesEqual)(state, nextState) ? callback() : setState(nextState, callback); }; /** * @function updateFrame * * @description * update the frame based on the type in props * * @param {function} callback the function to call once the frame is updated * @returns {void} */ var updateFrame = exports.updateFrame = function updateFrame(_ref19, _ref20) { var type = _ref19.props.type, updateScrollParent = _ref19.updateScrollParent, updateSimpleFrame = _ref19.updateSimpleFrame, updateUniformFrame = _ref19.updateUniformFrame, updateVariableFrame = _ref19.updateVariableFrame; var callback = _ref20[0]; updateScrollParent(); var updateCallback = (0, _utils.isFunction)(callback) ? callback : _utils.noop; return type === _constants.VALID_TYPES.UNIFORM ? updateUniformFrame(updateCallback) : type === _constants.VALID_TYPES.VARIABLE ? updateVariableFrame(updateCallback) : updateSimpleFrame(updateCallback); }; /** * @function updateScrollParent * * @description * update the scroll parent with the listeners it needs * * @param {ReactComponent} instance the component instance * @returns {void} */ var updateScrollParent = exports.updateScrollParent = function updateScrollParent(instance) { var getScrollParent = instance.getScrollParent, scrollParent = instance.scrollParent, updateFrame = instance.updateFrame; var newScrollParent = getScrollParent(); if (newScrollParent === scrollParent) { return; } if (scrollParent) { scrollParent.removeEventListener('scroll', updateFrame); scrollParent.removeEventListener('mousewheel', _utils.noop); } instance.scrollParent = newScrollParent; if (newScrollParent) { newScrollParent.addEventListener('scroll', updateFrame, _constants.ADD_EVENT_LISTENER_OPTIONS); newScrollParent.addEventListener('mousewheel', _utils.noop, _constants.ADD_EVENT_LISTENER_OPTIONS); } }; /** * @function updateSimpleFrame * * @description * update the frame when the type is 'simple' * * @param {function} callback the function to call once the frame is updated * @returns {void} */ var updateSimpleFrame = exports.updateSimpleFrame = function updateSimpleFrame(_ref21, _ref22) { var getStartAndEnd = _ref21.getStartAndEnd, items = _ref21.items, props = _ref21.props, setStateIfAppropriate = _ref21.setStateIfAppropriate, size = _ref21.state.size; var callback = _ref22[0]; return items && (0, _utils.getCalculatedElementEnd)(items.children, props) <= getStartAndEnd().end ? setStateIfAppropriate({ size: Math.min(size + props.pageSize, props.length) }, callback) : callback(); }; /** * @function updateUniformFrame * * @description * update the frame when the type is 'uniform' * * @param {function} callback the function to call once the frame is updated * @returns {void} */ var updateUniformFrame = exports.updateUniformFrame = function updateUniformFrame(_ref23, _ref24) { var getItemSizeAndItemsPerRow = _ref23.getItemSizeAndItemsPerRow, getStartAndEnd = _ref23.getStartAndEnd, props = _ref23.props, setStateIfAppropriate = _ref23.setStateIfAppropriate; var callback = _ref24[0]; var _getItemSizeAndItemsP = getItemSizeAndItemsPerRow(), itemSize = _getItemSizeAndItemsP.itemSize, itemsPerRow = _getItemSizeAndItemsP.itemsPerRow; if (!itemSize || !itemsPerRow) { return callback(); } var _getStartAndEnd2 = getStartAndEnd(), start = _getStartAndEnd2.start, end = _getStartAndEnd2.end; var calculatedFrom = Math.floor(start / itemSize) * itemsPerRow; var calulatedSize = (Math.ceil((end - start) / itemSize) + 1) * itemsPerRow; var fromAndSize = (0, _utils.getFromAndSize)(calculatedFrom, calulatedSize, itemsPerRow, props); return setStateIfAppropriate(_extends({}, fromAndSize, { itemSize: itemSize, itemsPerRow: itemsPerRow }), callback); }; /** * @function updateVariableFrame * * @description * update the frame when the type is 'variable' * * @param {function} callback the function to call once the frame is updated * @returns {void} */ var updateVariableFrame = exports.updateVariableFrame = function updateVariableFrame(_ref25, _ref26) { var cache = _ref25.cache, items = _ref25.items, getSizeOfListItem = _ref25.getSizeOfListItem, getStartAndEnd = _ref25.getStartAndEnd, props = _ref25.props, setStateIfAppropriate = _ref25.setStateIfAppropriate, _ref25$state = _ref25.state, currentFrom = _ref25$state.from, currentSize = _ref25$state.size; var callback = _ref26[0]; if (!items) { return; } if (!props.getItemSize) { (0, _utils.setCacheSizes)(currentFrom, items, props.axis, cache); } setStateIfAppropriate((0, _utils.getFromAndSizeFromListItemSize)(getStartAndEnd(), props, getSizeOfListItem, { from: currentFrom, size: currentSize }), callback); };