react-windowed-list
Version:
A fast, versatile virtual-render list component for React
591 lines (505 loc) • 17.9 kB
JavaScript
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);
};