UNPKG

react-windowed-list

Version:

A fast, versatile virtual-render list component for React

474 lines (406 loc) 13.6 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 React from 'react'; // constants import { CLIENT_START_KEYS, CLIENT_SIZE_KEYS, DEFAULT_CONTAINER_STYLE, INNER_SIZE_KEYS, OFFSET_START_KEYS, OFFSET_SIZE_KEYS, SCROLL_SIZE_KEYS, SIZE_KEYS, VALID_AXES, VALID_TYPES } from './constants'; /** * @function isFunction * * @description * is the object passed a function * * @param {*} object the object to test * @returns {boolean} is teh object a function */ export var isFunction = function isFunction(object) { return typeof object === 'function'; }; /** * @function isNAN * * @description * is the object passed a NaN * * @param {*} object the object to test * @returns {boolean} is teh object a NaN */ export var isNAN = function isNAN(object) { return object !== object; }; /** * @function isNumber * * @description * is the object passed a number * * @param {*} object the object to test * @returns {boolean} is teh object a number */ export var isNumber = function isNumber(object) { return typeof object === 'number'; }; /** * @function noop * * @description * a no-op method */ export var noop = function noop() {}; /** * @function areStateValuesEqual * * @description * should the state be updated based on the values of nextPossibleState being different * * @param {Object} currentState the current state of the instance * @param {Object} nextPossibleState the state to apply * @returns {boolean} should the state be updated */ export var areStateValuesEqual = function areStateValuesEqual(currentState, nextPossibleState) { for (var key in nextPossibleState) { if (currentState[key] !== nextPossibleState[key]) { return false; } } return true; }; /** * @function coalesceToZero * * @description * return the value if truthy, else zero * * @param {*} value the value to compare * @returns {*} the value or zero */ export var coalesceToZero = function coalesceToZero(value) { return value || 0; }; /** * @function DefaultItemRenderer * * @description * the default method to create the item element * * @param {number} index the index of the item in the list * @param {number} key the key to provide to the item * @returns {ReactElement} the generated element */ export var DefaultItemRenderer = function DefaultItemRenderer(index, key) { return React.createElement( 'div', { key: key }, index ); }; /** * @function DefaultContainerRenderer * * @description * the default method to create the list container * * @param {Array<ReactElement>} items the items to render in the list * @param {function} ref the ref to provide to the list container * @returns {ReactElement} the generated element */ export var DefaultContainerRenderer = function DefaultContainerRenderer(items, ref) { return React.createElement( 'div', { ref: ref }, items ); }; /** * @function getOffset * * @description * get the offset of the element based on its position and axis * * @param {HTMLElement} element the element to get the offset of * @param {string} axis the axis of the component * @returns {number} the element offset requested */ export var getOffset = function getOffset(element, axis) { var offsetKey = OFFSET_START_KEYS[axis]; var offset = coalesceToZero(element[CLIENT_START_KEYS[axis]]) + coalesceToZero(element[offsetKey]); while (element = element.offsetParent) { offset += coalesceToZero(element[offsetKey]); } return offset; }; /** * @function getCalculatedElementEnd * * @description * get the pixel size of the window based on the last element in the list of elements passed * * @param {Array<HTMLElement>} elements the elements displayed * @param {string} axis the axis of the component * @returns {number} the pixel size of the window */ export var getCalculatedElementEnd = function getCalculatedElementEnd(elements, _ref) { var axis = _ref.axis; if (!elements.length) { return 0; } var firstItemEl = elements[0]; var lastItemEl = elements[elements.length - 1]; return getOffset(lastItemEl, axis) + lastItemEl[OFFSET_SIZE_KEYS[axis]] - getOffset(firstItemEl, axis); }; /** * @function getCalculatedSpaceBefore * * @description * get the space before the given length * * @param {Object} cache the cache of sizes in the instance * @param {number} length the length of items to get the space of * @param {function} getSizeOfListItem the method to get the size of the list item * @returns {number} the space before the given length */ export var getCalculatedSpaceBefore = function getCalculatedSpaceBefore(cache, length, getSizeOfListItem) { var from = length; while (from > 0 && !isNumber(cache[from])) { --from; } var space = coalesceToZero(cache[from]), itemSize = void 0; for (var index = from; index < length; index++) { cache[index] = space; itemSize = getSizeOfListItem(index); if (!isNumber(itemSize)) { break; } space += itemSize; } return space; }; /** * @function getCalculatedItemSizeAndItemsPerRow * * @description * get the itemSize and itemsPerRow properties based on the elements passed * * @NOTE * Firefox has a problem where it will return a *slightly* (less than * thousandths of a pixel) different size for the same element between * renders. instance can cause an infinite render loop, so only change the * itemSize when it is significantly different. * * @param {Array<HTMLElement>} elements the elements to get the itemSize for * @param {string} axis the axis of the component * @param {number} currentItemSize the itemSize currently in state * @returns {{itemSize: number, itemsPerRow: number}} the new itemSize and itemsPerRow properties */ export var getCalculatedItemSizeAndItemsPerRow = function getCalculatedItemSizeAndItemsPerRow(elements, axis, currentItemSize) { var firstEl = elements[0]; var firstElSize = firstEl[OFFSET_SIZE_KEYS[axis]]; var delta = Math.abs(firstElSize - currentItemSize); var itemSize = isNAN(delta) || delta > 0 ? firstElSize : currentItemSize; if (!itemSize) { return {}; } var startKey = OFFSET_START_KEYS[axis]; var firstStart = firstEl[startKey]; var itemsPerRow = 1, item = elements[itemsPerRow]; while (item && item[startKey] === firstStart) { item = elements[++itemsPerRow]; } return { itemSize: itemSize, itemsPerRow: itemsPerRow }; }; export var getInnerContainerStyle = function getInnerContainerStyle(axis, length, itemsPerRow, getSize) { var _extends2, _extends3; var size = getSize(Math.ceil(length / itemsPerRow) * itemsPerRow, {}); if (!size) { return DEFAULT_CONTAINER_STYLE; } var axisKey = SIZE_KEYS[axis]; return axis !== VALID_AXES.X ? _extends({}, DEFAULT_CONTAINER_STYLE, (_extends2 = {}, _extends2[axisKey] = size, _extends2)) : _extends({}, DEFAULT_CONTAINER_STYLE, (_extends3 = {}, _extends3[axisKey] = size, _extends3.overflowX = 'hidden', _extends3)); }; /** * @function getFromAndSize * * @description * calculate the from and size values based on the number of items per row * * @param {number} currentFrom the current from in state * @param {number} currentSize the current size in state * @param {number} itemsPerRow the number of items per row in state * @param {boolean} isLazy is the list lazily loaded * @param {number} length the total number of items * @param {number} pageSize the size of batches to render * @param {string} type the type of list * @returns {{from: number, size: number}} the from and size propertioes */ export var getFromAndSize = function getFromAndSize(currentFrom, currentSize, itemsPerRow, _ref2) { var isLazy = _ref2.isLazy, length = _ref2.length, minSize = _ref2.minSize, pageSize = _ref2.pageSize, type = _ref2.type; var comparator = Math.max(minSize, isLazy && type === VALID_TYPES.UNIFORM ? 1 : pageSize); var size = Math.max(currentSize, comparator), mod = size % itemsPerRow; if (mod) { size += itemsPerRow - mod; } if (size > length) { size = length; } var from = !currentFrom || type === VALID_TYPES.SIMPLE ? 0 : Math.max(Math.min(currentFrom, length - size), 0); if (mod = from % itemsPerRow) { from -= mod; size += mod; } return { from: from, size: size }; }; /** * @function getFromAndSizeFromListItemSize * * @description * get the from and size properties based on the size of the list items * * @param {Object} startAndEnd the object with starting and ending indices of the displayed window * @param {number} startAndEnd.end the ending index of the items to display * @param {number} startAndEnd.start the starting index of the items to display * @param {Object} props the current props of the component * @param {number} props.length the total size of the list * @param {number} props.pageSize the side of the page to display * @param {function} getSizeOfListItem the method to get the size of the list item * @param {Object} currentState the current state's from and size * @returns {{from: number, size: number}} the from and size properties */ export var getFromAndSizeFromListItemSize = function getFromAndSizeFromListItemSize(_ref3, _ref4, getSizeOfListItem, currentState) { var end = _ref3.end, start = _ref3.start; var length = _ref4.length, pageSize = _ref4.pageSize; var maxFrom = length - 1; var space = 0, from = 0, size = -1, itemSize = void 0; for (; from < maxFrom; from++) { itemSize = getSizeOfListItem(from); if (!isNumber(itemSize) || space + itemSize > start) { /** * @NOTE * if an alternative key is used, it causes jitter when the first item is removed from the DOM, * so render the first item if the calculated from is 1 */ if (from === 1) { from = 0; } break; } space += itemSize; } var maxSize = length - from; while (++size < maxSize && space < end) { itemSize = getSizeOfListItem(from + size); if (!isNumber(itemSize)) { size = Math.min(size + pageSize, maxSize); break; } space += itemSize; } return space ? { from: from, size: size } : currentState; }; /** * @function getListContainerStyle * * @description * get the style object to provide to the list container * * @param {string} axis the axis of the component * @param {string} usePosition should position be used instead of the transform * @param {string} useTranslate3d should translate3d be used for the transform * @param {number} firstIndex the first index being rendered * @param {function} getOffset the method to get the offset value * @returns {Object} the style object for the list container */ export var getListContainerStyle = function getListContainerStyle(axis, usePosition, useTranslate3d, firstIndex, getOffset) { var offset = getOffset(firstIndex, {}); var x = axis === VALID_AXES.X ? offset : 0; var y = axis === VALID_AXES.Y ? offset : 0; if (usePosition) { return { left: x, position: 'relative', top: y }; } var transform = useTranslate3d ? 'translate3d(' + x + 'px, ' + y + 'px, 0)' : 'translate(' + x + 'px, ' + y + 'px)'; return { msTransform: transform, WebkitTransform: transform, transform: transform }; }; /** * @function getScrollSize * * @description * get the scroll size of the element * * @param {HTMLElement} element the element to get the scroll size of * @param {string} axis the axis of the component * @returns {number} the scroll size of the element */ export var getScrollSize = function getScrollSize(element, axis) { return element === window ? Math.max(document.body[SCROLL_SIZE_KEYS[axis]], document.documentElement[SCROLL_SIZE_KEYS[axis]]) : element[SCROLL_SIZE_KEYS[axis]]; }; /** * @function getViewportSize * * @description * get the viewport size of the element * * @param {HTMLElement} element the element to get the viewport size of * @param {string} axis the axis of the component * @returns {number} the viewport size of the element */ export var getViewportSize = function getViewportSize(element, axis) { return element ? element === window ? window[INNER_SIZE_KEYS[axis]] : element[CLIENT_SIZE_KEYS[axis]] : 0; }; /** * @function hasDeterminateSize * * @description * does the element have a predetermined size calculator * * @param {string} type the type of the component * @param {function} [getItemSize] the function to calculate the item size * @returns {boolean} is the size automatically determined */ export var hasDeterminateSize = function hasDeterminateSize(type, getItemSize) { return type === VALID_TYPES.UNIFORM || isFunction(getItemSize); }; /** * @function setCacheSizes * * @description * set the rendered sizes in cache on the instance * * @param {number} from the first index to set the cache of * @param {HTMLElement} element the list element * @param {string} axis the axis of the component * @param {Object} cache the cache to save to */ export var setCacheSizes = function setCacheSizes(from, element, axis, cache) { var itemElements = element.children; var sizeKey = OFFSET_SIZE_KEYS[axis]; for (var index = 0; index < itemElements.length; index++) { cache[from + index] = itemElements[index][sizeKey]; } };