UNPKG

react-windowed-list

Version:

A fast, versatile virtual-render list component for React

591 lines (505 loc) 17.9 kB
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 import debounce from 'debounce'; import raf from 'raf'; import { findDOMNode } from 'react-dom'; // constants import { OFFSET_SIZE_KEYS, OVERFLOW_KEYS, OVERFLOW_VALUES, SCROLL_START_KEYS, ADD_EVENT_LISTENER_OPTIONS, VALID_TYPES } from './constants'; // utils import { areStateValuesEqual, getCalculatedElementEnd, getCalculatedItemSizeAndItemsPerRow, getCalculatedSpaceBefore, getFromAndSize, getFromAndSizeFromListItemSize, getOffset, getScrollSize, getViewportSize, hasDeterminateSize, isFunction, isNumber, noop, setCacheSizes } from './utils'; /** * @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 */ export var 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 ? 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 */ export var getScrollOffset = function getScrollOffset(_ref2) { var outerContainer = _ref2.outerContainer, axis = _ref2.props.axis, scrollParent = _ref2.scrollParent; if (!outerContainer || !scrollParent) { return 0; } var scrollKey = SCROLL_START_KEYS[axis]; var actual = scrollParent === window ? document.body[scrollKey] || document.documentElement[scrollKey] : scrollParent[scrollKey]; var max = getScrollSize(scrollParent, axis) - getViewportSize(scrollParent, axis); var scroll = Math.max(0, Math.min(actual, max)); return getOffset(scrollParent, axis) + scroll - 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 */ export var getScrollParent = function getScrollParent(_ref3) { var outerContainer = _ref3.outerContainer, _ref3$props = _ref3.props, axis = _ref3$props.axis, getScrollParent = _ref3$props.getScrollParent; if (isFunction(getScrollParent)) { return getScrollParent(); } if (!outerContainer) { return null; } var overflowKey = OVERFLOW_KEYS[axis]; var element = outerContainer; while (element = element.parentElement) { if (~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 */ export var 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 (isFunction(getItemSize)) { return getItemSize(index); } // Try the cache. if (isNumber(cache[index])) { return cache[index]; } if (items) { var itemElements = items.children; // Try the DOM. if (itemElements.length && type === VALID_TYPES.SIMPLE && index >= from && index < from + size) { var itemEl = itemElements[index - from]; if (itemEl) { return itemEl[OFFSET_SIZE_KEYS[axis]]; } } } // Try the getEstimatedItemSize. if (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 */ export var 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 (isNumber(cache[index])) { return cache[index]; } cache[index] = itemSize ? Math.floor(index / itemsPerRow) * itemSize : 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 */ export var 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 + getViewportSize(scrollParent, axis) + threshold; return { end: 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 */ export var 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 (!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 */ export var 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 = 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} */ export var 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 */ export var scrollTo = function scrollTo(_ref13, _ref14) { var getSpaceBefore = _ref13.getSpaceBefore, initialIndex = _ref13.props.initialIndex, setScroll = _ref13.setScroll; var index = _ref14[0]; var indexToScrollTo = isNumber(index) ? index : initialIndex; if (isNumber(indexToScrollTo)) { setScroll(getSpaceBefore(indexToScrollTo)); } }; /** * @function setReconcileFrameAfterUpdate * * @description * set the frame reconciler used after componentDidUpdate * * @param {ReactComponent} instance the component instance */ export var setReconcileFrameAfterUpdate = function setReconcileFrameAfterUpdate(instance) { var debounceReconciler = instance.props.debounceReconciler; instance.reconcileFrameAfterUpdate = isNumber(debounceReconciler) ? debounce(function (updateFrame) { updateFrame(); }, debounceReconciler) : raf; }; /** * @function setScroll * * @description * set the scroll based on the current offset * * @param {number} currentOffset the current offset * @returns {void} */ export var 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 + getOffset(outerContainer, axis); if (scrollParent === window) { return window.scrollTo(0, offset); } scrollParent[SCROLL_START_KEYS[axis]] = offset - 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} */ export var setStateIfAppropriate = function setStateIfAppropriate(_ref17, _ref18) { var setState = _ref17.setState, state = _ref17.state; var nextState = _ref18[0], callback = _ref18[1]; return 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} */ export var 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 = isFunction(callback) ? callback : noop; return type === VALID_TYPES.UNIFORM ? updateUniformFrame(updateCallback) : type === 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} */ export var 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', noop); } instance.scrollParent = newScrollParent; if (newScrollParent) { newScrollParent.addEventListener('scroll', updateFrame, ADD_EVENT_LISTENER_OPTIONS); newScrollParent.addEventListener('mousewheel', noop, 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} */ export var 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 && 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} */ export var 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 = 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} */ export var 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) { setCacheSizes(currentFrom, items, props.axis, cache); } setStateIfAppropriate(getFromAndSizeFromListItemSize(getStartAndEnd(), props, getSizeOfListItem, { from: currentFrom, size: currentSize }), callback); };