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