@iharbeck/ngx-virtual-scroller
Version:
Virtual Scroll displays a virtual, "infinite" list. Supports horizontal/vertical, variable heights, & multi-column.
990 lines (989 loc) • 56.1 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, PLATFORM_ID, ElementRef, Component, Inject, Optional, Input, Output, ViewChild, ContentChild, NgModule } from '@angular/core';
import { isPlatformServer, CommonModule } from '@angular/common';
import * as tween from '@tweenjs/tween.js';
function VIRTUAL_SCROLLER_DEFAULT_OPTIONS_FACTORY() {
return {
checkResizeInterval: 1000,
modifyOverflowStyleOfParentScroll: true,
resizeBypassRefreshThreshold: 5,
scrollAnimationTime: 750,
scrollDebounceTime: 0,
scrollThrottlingTime: 0,
stripedTable: false
};
}
class VirtualScrollerComponent {
element;
renderer;
zone;
changeDetectorRef;
viewPortItems;
window = window;
get viewPortInfo() {
let pageInfo = this.previousViewPort || {};
return {
startIndex: pageInfo.startIndex || 0,
endIndex: pageInfo.endIndex || 0,
scrollStartPosition: pageInfo.scrollStartPosition || 0,
scrollEndPosition: pageInfo.scrollEndPosition || 0,
maxScrollPosition: pageInfo.maxScrollPosition || 0,
startIndexWithBuffer: pageInfo.startIndexWithBuffer || 0,
endIndexWithBuffer: pageInfo.endIndexWithBuffer || 0
};
}
executeRefreshOutsideAngularZone = false;
_enableUnequalChildrenSizes = false;
get enableUnequalChildrenSizes() {
return this._enableUnequalChildrenSizes;
}
set enableUnequalChildrenSizes(value) {
if (this._enableUnequalChildrenSizes === value) {
return;
}
this._enableUnequalChildrenSizes = value;
this.minMeasuredChildWidth = undefined;
this.minMeasuredChildHeight = undefined;
}
RTL = false;
useMarginInsteadOfTranslate = false;
modifyOverflowStyleOfParentScroll;
stripedTable;
scrollbarWidth;
scrollbarHeight;
childWidth;
childHeight;
ssrChildWidth;
ssrChildHeight;
ssrViewportWidth = 1920;
ssrViewportHeight = 1080;
_bufferAmount;
get bufferAmount() {
if (typeof (this._bufferAmount) === 'number' && this._bufferAmount >= 0) {
return this._bufferAmount;
}
else {
return this.enableUnequalChildrenSizes ? 5 : 0;
}
}
set bufferAmount(value) {
this._bufferAmount = value;
}
scrollAnimationTime;
resizeBypassRefreshThreshold;
_scrollThrottlingTime;
get scrollThrottlingTime() {
return this._scrollThrottlingTime;
}
set scrollThrottlingTime(value) {
this._scrollThrottlingTime = value;
this.updateOnScrollFunction();
}
_scrollDebounceTime;
get scrollDebounceTime() {
return this._scrollDebounceTime;
}
set scrollDebounceTime(value) {
this._scrollDebounceTime = value;
this.updateOnScrollFunction();
}
onScroll;
updateOnScrollFunction() {
if (this.scrollDebounceTime) {
this.onScroll = this.debounce(() => {
this.refresh_internal(false);
}, this.scrollDebounceTime);
}
else if (this.scrollThrottlingTime) {
this.onScroll = this.throttleTrailing(() => {
this.refresh_internal(false);
}, this.scrollThrottlingTime);
}
else {
this.onScroll = () => {
this.refresh_internal(false);
};
}
}
checkScrollElementResizedTimer;
_checkResizeInterval;
get checkResizeInterval() {
return this._checkResizeInterval;
}
set checkResizeInterval(value) {
if (this._checkResizeInterval === value) {
return;
}
this._checkResizeInterval = value;
this.addScrollEventHandlers();
}
_items = [];
get items() {
return this._items;
}
set items(value) {
if (value === this._items) {
return;
}
this._items = value || [];
this.refresh_internal(true);
}
compareItems = (item1, item2) => item1 === item2;
_horizontal;
get horizontal() {
return this._horizontal;
}
set horizontal(value) {
this._horizontal = value;
this.updateDirection();
}
revertParentOverscroll() {
const scrollElement = this.getScrollElement();
if (scrollElement && this.oldParentScrollOverflow) {
scrollElement.style['overflow-y'] = this.oldParentScrollOverflow.y;
scrollElement.style['overflow-x'] = this.oldParentScrollOverflow.x;
}
this.oldParentScrollOverflow = undefined;
}
oldParentScrollOverflow;
_parentScroll;
get parentScroll() {
return this._parentScroll;
}
set parentScroll(value) {
if (this._parentScroll === value) {
return;
}
this.revertParentOverscroll();
this._parentScroll = value;
this.addScrollEventHandlers();
const scrollElement = this.getScrollElement();
if (this.modifyOverflowStyleOfParentScroll && scrollElement !== this.element.nativeElement) {
this.oldParentScrollOverflow = { x: scrollElement.style['overflow-x'], y: scrollElement.style['overflow-y'] };
scrollElement.style['overflow-y'] = this.horizontal ? 'visible' : 'auto';
scrollElement.style['overflow-x'] = this.horizontal ? 'auto' : 'visible';
}
}
vsUpdate = new EventEmitter();
vsChange = new EventEmitter();
vsStart = new EventEmitter();
vsEnd = new EventEmitter();
contentElementRef;
invisiblePaddingElementRef;
headerElementRef;
containerElementRef;
ngOnInit() {
this.addScrollEventHandlers();
}
ngOnDestroy() {
this.removeScrollEventHandlers();
this.revertParentOverscroll();
}
ngOnChanges(changes) {
let indexLengthChanged = this.cachedItemsLength !== this.items.length;
this.cachedItemsLength = this.items.length;
const firstRun = !changes.items || !changes.items.previousValue || changes.items.previousValue.length === 0;
this.refresh_internal(indexLengthChanged || firstRun);
}
ngDoCheck() {
if (this.cachedItemsLength !== this.items.length) {
this.cachedItemsLength = this.items.length;
this.refresh_internal(true);
return;
}
if (this.previousViewPort && this.viewPortItems && this.viewPortItems.length > 0) {
let itemsArrayChanged = false;
for (let i = 0; i < this.viewPortItems.length; ++i) {
if (!this.compareItems(this.items[this.previousViewPort.startIndexWithBuffer + i], this.viewPortItems[i])) {
itemsArrayChanged = true;
break;
}
}
if (itemsArrayChanged) {
this.refresh_internal(true);
}
}
}
refresh() {
this.refresh_internal(true);
}
invalidateAllCachedMeasurements() {
this.wrapGroupDimensions = {
maxChildSizePerWrapGroup: [],
numberOfKnownWrapGroupChildSizes: 0,
sumOfKnownWrapGroupChildWidths: 0,
sumOfKnownWrapGroupChildHeights: 0
};
this.minMeasuredChildWidth = undefined;
this.minMeasuredChildHeight = undefined;
this.refresh_internal(false);
}
invalidateCachedMeasurementForItem(item) {
if (this.enableUnequalChildrenSizes) {
let index = this.items && this.items.indexOf(item);
if (index >= 0) {
this.invalidateCachedMeasurementAtIndex(index);
}
}
else {
this.minMeasuredChildWidth = undefined;
this.minMeasuredChildHeight = undefined;
}
this.refresh_internal(false);
}
invalidateCachedMeasurementAtIndex(index) {
if (this.enableUnequalChildrenSizes) {
let cachedMeasurement = this.wrapGroupDimensions.maxChildSizePerWrapGroup[index];
if (cachedMeasurement) {
this.wrapGroupDimensions.maxChildSizePerWrapGroup[index] = undefined;
--this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths -= cachedMeasurement.childWidth || 0;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights -= cachedMeasurement.childHeight || 0;
}
}
else {
this.minMeasuredChildWidth = undefined;
this.minMeasuredChildHeight = undefined;
}
this.refresh_internal(false);
}
scrollInto(item, alignToBeginning = true, additionalOffset = 0, animationMilliseconds = undefined, animationCompletedCallback = undefined) {
let index = this.items.indexOf(item);
if (index === -1) {
return;
}
this.scrollToIndex(index, alignToBeginning, additionalOffset, animationMilliseconds, animationCompletedCallback);
}
scrollToIndex(index, alignToBeginning = true, additionalOffset = 0, animationMilliseconds = undefined, animationCompletedCallback = undefined) {
let maxRetries = 5;
let retryIfNeeded = () => {
--maxRetries;
if (maxRetries <= 0) {
if (animationCompletedCallback) {
animationCompletedCallback();
}
return;
}
let dimensions = this.calculateDimensions();
let desiredStartIndex = Math.min(Math.max(index, 0), dimensions.itemCount - 1);
if (this.previousViewPort.startIndex === desiredStartIndex) {
if (animationCompletedCallback) {
animationCompletedCallback();
}
return;
}
this.scrollToIndex_internal(index, alignToBeginning, additionalOffset, 0, retryIfNeeded);
};
this.scrollToIndex_internal(index, alignToBeginning, additionalOffset, animationMilliseconds, retryIfNeeded);
}
scrollToIndex_internal(index, alignToBeginning = true, additionalOffset = 0, animationMilliseconds = undefined, animationCompletedCallback = undefined) {
animationMilliseconds = animationMilliseconds === undefined ? this.scrollAnimationTime : animationMilliseconds;
let dimensions = this.calculateDimensions();
let scroll = this.calculatePadding(index, dimensions) + additionalOffset;
if (!alignToBeginning) {
scroll -= dimensions.wrapGroupsPerPage * dimensions[this._childScrollDim];
}
this.scrollToPosition(scroll, animationMilliseconds, animationCompletedCallback);
}
scrollToPosition(scrollPosition, animationMilliseconds = undefined, animationCompletedCallback = undefined) {
scrollPosition += this.getElementsOffset();
animationMilliseconds = animationMilliseconds === undefined ? this.scrollAnimationTime : animationMilliseconds;
let scrollElement = this.getScrollElement();
let animationRequest;
if (this.currentTween) {
this.currentTween.stop();
this.currentTween = undefined;
}
if (!animationMilliseconds) {
this.renderer.setProperty(scrollElement, this._scrollType, scrollPosition);
this.refresh_internal(false, animationCompletedCallback);
return;
}
const tweenConfigObj = { scrollPosition: scrollElement[this._scrollType] };
let newTween = new tween.Tween(tweenConfigObj)
.to({ scrollPosition }, animationMilliseconds)
.easing(tween.Easing.Quadratic.Out)
.onUpdate((data) => {
if (isNaN(data.scrollPosition)) {
return;
}
this.renderer.setProperty(scrollElement, this._scrollType, data.scrollPosition);
this.refresh_internal(false);
})
.onStop(() => {
cancelAnimationFrame(animationRequest);
})
.start();
const animate = (time) => {
if (!newTween['isPlaying']()) {
return;
}
newTween.update(time);
if (tweenConfigObj.scrollPosition === scrollPosition) {
this.refresh_internal(false, animationCompletedCallback);
return;
}
this.zone.runOutsideAngular(() => {
animationRequest = requestAnimationFrame(animate);
});
};
animate();
this.currentTween = newTween;
}
isAngularUniversalSSR;
constructor(element, renderer, zone, changeDetectorRef, platformId, options) {
this.element = element;
this.renderer = renderer;
this.zone = zone;
this.changeDetectorRef = changeDetectorRef;
this.isAngularUniversalSSR = isPlatformServer(platformId);
this.checkResizeInterval = options.checkResizeInterval;
this.modifyOverflowStyleOfParentScroll = options.modifyOverflowStyleOfParentScroll;
this.resizeBypassRefreshThreshold = options.resizeBypassRefreshThreshold;
this.scrollAnimationTime = options.scrollAnimationTime;
this.scrollDebounceTime = options.scrollDebounceTime;
this.scrollThrottlingTime = options.scrollThrottlingTime;
this.scrollbarHeight = options.scrollbarHeight;
this.scrollbarWidth = options.scrollbarWidth;
this.stripedTable = options.stripedTable;
this.horizontal = false;
this.resetWrapGroupDimensions();
}
getElementSize(element) {
let result = element.getBoundingClientRect();
let styles = getComputedStyle(element);
let marginTop = parseInt(styles['margin-top'], 10) || 0;
let marginBottom = parseInt(styles['margin-bottom'], 10) || 0;
let marginLeft = parseInt(styles['margin-left'], 10) || 0;
let marginRight = parseInt(styles['margin-right'], 10) || 0;
return {
top: result.top + marginTop,
bottom: result.bottom + marginBottom,
left: result.left + marginLeft,
right: result.right + marginRight,
width: result.width + marginLeft + marginRight,
height: result.height + marginTop + marginBottom
};
}
previousScrollBoundingRect;
checkScrollElementResized() {
let boundingRect = this.getElementSize(this.getScrollElement());
let sizeChanged;
if (!this.previousScrollBoundingRect) {
sizeChanged = true;
}
else {
let widthChange = Math.abs(boundingRect.width - this.previousScrollBoundingRect.width);
let heightChange = Math.abs(boundingRect.height - this.previousScrollBoundingRect.height);
sizeChanged = widthChange > this.resizeBypassRefreshThreshold || heightChange > this.resizeBypassRefreshThreshold;
}
if (sizeChanged) {
this.previousScrollBoundingRect = boundingRect;
if (boundingRect.width > 0 && boundingRect.height > 0) {
this.refresh_internal(false);
}
}
}
_invisiblePaddingProperty;
_offsetType;
_scrollType;
_pageOffsetType;
_childScrollDim;
_translateDir;
_marginDir;
updateDirection() {
if (this.horizontal) {
this._childScrollDim = 'childWidth';
this._invisiblePaddingProperty = 'width';
this._marginDir = 'margin-left';
this._offsetType = 'offsetLeft';
this._pageOffsetType = 'pageXOffset';
this._scrollType = 'scrollLeft';
this._translateDir = 'translateX';
}
else {
this._childScrollDim = 'childHeight';
this._invisiblePaddingProperty = 'height';
this._marginDir = 'margin-top';
this._offsetType = 'offsetTop';
this._pageOffsetType = 'pageYOffset';
this._scrollType = 'scrollTop';
this._translateDir = 'translateY';
}
}
debounce(func, wait) {
const throttled = this.throttleTrailing(func, wait);
const result = function () {
throttled['cancel']();
throttled.apply(this, arguments);
};
result['cancel'] = function () {
throttled['cancel']();
};
return result;
}
throttleTrailing(func, wait) {
let timeout = undefined;
let _arguments = arguments;
const result = function () {
const _this = this;
_arguments = arguments;
if (timeout) {
return;
}
if (wait <= 0) {
func.apply(_this, _arguments);
}
else {
timeout = setTimeout(function () {
timeout = undefined;
func.apply(_this, _arguments);
}, wait);
}
};
result['cancel'] = function () {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
};
return result;
}
calculatedScrollbarWidth = 0;
calculatedScrollbarHeight = 0;
padding = 0;
previousViewPort = {};
currentTween;
cachedItemsLength;
disposeScrollHandler;
disposeResizeHandler;
refresh_internal(itemsArrayModified, refreshCompletedCallback = undefined, maxRunTimes = 2) {
//note: maxRunTimes is to force it to keep recalculating if the previous iteration caused a re-render (different sliced items in viewport or scrollPosition changed).
//The default of 2x max will probably be accurate enough without causing too large a performance bottleneck
//The code would typically quit out on the 2nd iteration anyways. The main time it'd think more than 2 runs would be necessary would be for vastly different sized child items or if this is the 1st time the items array was initialized.
//Without maxRunTimes, If the user is actively scrolling this code would become an infinite loop until they stopped scrolling. This would be okay, except each scroll event would start an additional infinte loop. We want to short-circuit it to prevent this.
if (itemsArrayModified && this.previousViewPort && this.previousViewPort.scrollStartPosition > 0) {
//if items were prepended, scroll forward to keep same items visible
let oldViewPort = this.previousViewPort;
let oldViewPortItems = this.viewPortItems;
let oldRefreshCompletedCallback = refreshCompletedCallback;
refreshCompletedCallback = () => {
let scrollLengthDelta = this.previousViewPort.scrollLength - oldViewPort.scrollLength;
if (scrollLengthDelta > 0 && this.viewPortItems) {
let oldStartItem = oldViewPortItems[0];
let oldStartItemIndex = this.items.findIndex(x => this.compareItems(oldStartItem, x));
if (oldStartItemIndex > this.previousViewPort.startIndexWithBuffer) {
let itemOrderChanged = false;
for (let i = 1; i < this.viewPortItems.length; ++i) {
if (!this.compareItems(this.items[oldStartItemIndex + i], oldViewPortItems[i])) {
itemOrderChanged = true;
break;
}
}
if (!itemOrderChanged) {
this.scrollToPosition(this.previousViewPort.scrollStartPosition + scrollLengthDelta, 0, oldRefreshCompletedCallback);
return;
}
}
}
if (oldRefreshCompletedCallback) {
oldRefreshCompletedCallback();
}
};
}
this.zone.runOutsideAngular(() => {
requestAnimationFrame(() => {
if (itemsArrayModified) {
this.resetWrapGroupDimensions();
}
let viewport = this.calculateViewport();
let startChanged = itemsArrayModified || viewport.startIndex !== this.previousViewPort.startIndex;
let endChanged = itemsArrayModified || viewport.endIndex !== this.previousViewPort.endIndex;
let scrollbarLengthChanged = viewport.scrollbarLength !== this.previousViewPort.scrollbarLength;
let paddingChanged = viewport.padding !== this.previousViewPort.padding;
let scrollPositionChanged = viewport.scrollStartPosition !== this.previousViewPort.scrollStartPosition || viewport.scrollEndPosition !== this.previousViewPort.scrollEndPosition || viewport.maxScrollPosition !== this.previousViewPort.maxScrollPosition;
this.previousViewPort = viewport;
if (scrollbarLengthChanged) {
this.renderer.setStyle(this.invisiblePaddingElementRef.nativeElement, this._invisiblePaddingProperty, `${viewport.scrollLength}px`);
}
if (paddingChanged) {
if (this.useMarginInsteadOfTranslate) {
this.renderer.setStyle(this.contentElementRef.nativeElement, this._marginDir, `${viewport.padding}px`);
}
else {
this.renderer.setStyle(this.contentElementRef.nativeElement, 'transform', `${this._translateDir}(${viewport.padding}px)`);
this.renderer.setStyle(this.contentElementRef.nativeElement, 'webkitTransform', `${this._translateDir}(${viewport.padding}px)`);
}
}
if (this.headerElementRef) {
let scrollPosition = this.getScrollElement()[this._scrollType];
let containerOffset = this.getElementsOffset();
let offset = Math.max(scrollPosition - viewport.padding - containerOffset + this.headerElementRef.nativeElement.clientHeight, 0);
this.renderer.setStyle(this.headerElementRef.nativeElement, 'transform', `${this._translateDir}(${offset}px)`);
this.renderer.setStyle(this.headerElementRef.nativeElement, 'webkitTransform', `${this._translateDir}(${offset}px)`);
}
const changeEventArg = (startChanged || endChanged) ? {
startIndex: viewport.startIndex,
endIndex: viewport.endIndex,
scrollStartPosition: viewport.scrollStartPosition,
scrollEndPosition: viewport.scrollEndPosition,
startIndexWithBuffer: viewport.startIndexWithBuffer,
endIndexWithBuffer: viewport.endIndexWithBuffer,
maxScrollPosition: viewport.maxScrollPosition
} : undefined;
if (startChanged || endChanged || scrollPositionChanged) {
const handleChanged = () => {
// update the scroll list to trigger re-render of components in viewport
this.viewPortItems = viewport.startIndexWithBuffer >= 0 && viewport.endIndexWithBuffer >= 0 ? this.items.slice(viewport.startIndexWithBuffer, viewport.endIndexWithBuffer + 1) : [];
this.vsUpdate.emit(this.viewPortItems);
if (startChanged) {
this.vsStart.emit(changeEventArg);
}
if (endChanged) {
this.vsEnd.emit(changeEventArg);
}
if (startChanged || endChanged) {
this.changeDetectorRef.markForCheck();
this.vsChange.emit(changeEventArg);
}
if (maxRunTimes > 0) {
this.refresh_internal(false, refreshCompletedCallback, maxRunTimes - 1);
return;
}
if (refreshCompletedCallback) {
refreshCompletedCallback();
}
};
if (this.executeRefreshOutsideAngularZone) {
handleChanged();
}
else {
this.zone.run(handleChanged);
}
}
else {
if (maxRunTimes > 0 && (scrollbarLengthChanged || paddingChanged)) {
this.refresh_internal(false, refreshCompletedCallback, maxRunTimes - 1);
return;
}
if (refreshCompletedCallback) {
refreshCompletedCallback();
}
}
});
});
}
getScrollElement() {
return this.parentScroll instanceof Window ? document.scrollingElement || document.documentElement || document.body : this.parentScroll || this.element.nativeElement;
}
addScrollEventHandlers() {
if (this.isAngularUniversalSSR) {
return;
}
let scrollElement = this.getScrollElement();
this.removeScrollEventHandlers();
this.zone.runOutsideAngular(() => {
if (this.parentScroll instanceof Window) {
this.disposeScrollHandler = this.renderer.listen('window', 'scroll', this.onScroll);
this.disposeResizeHandler = this.renderer.listen('window', 'resize', this.onScroll);
}
else {
this.disposeScrollHandler = this.renderer.listen(scrollElement, 'scroll', this.onScroll);
if (this._checkResizeInterval > 0) {
this.checkScrollElementResizedTimer = setInterval(() => {
this.checkScrollElementResized();
}, this._checkResizeInterval);
}
}
});
}
removeScrollEventHandlers() {
if (this.checkScrollElementResizedTimer) {
clearInterval(this.checkScrollElementResizedTimer);
}
if (this.disposeScrollHandler) {
this.disposeScrollHandler();
this.disposeScrollHandler = undefined;
}
if (this.disposeResizeHandler) {
this.disposeResizeHandler();
this.disposeResizeHandler = undefined;
}
}
getElementsOffset() {
if (this.isAngularUniversalSSR) {
return 0;
}
let offset = 0;
if (this.containerElementRef && this.containerElementRef.nativeElement) {
offset += this.containerElementRef.nativeElement[this._offsetType];
}
if (this.parentScroll) {
let scrollElement = this.getScrollElement();
let elementClientRect = this.getElementSize(this.element.nativeElement);
let scrollClientRect = this.getElementSize(scrollElement);
if (this.horizontal) {
offset += elementClientRect.left - scrollClientRect.left;
}
else {
offset += elementClientRect.top - scrollClientRect.top;
}
if (!(this.parentScroll instanceof Window)) {
offset += scrollElement[this._scrollType];
}
}
return offset;
}
countItemsPerWrapGroup() {
if (this.isAngularUniversalSSR) {
return Math.round(this.horizontal ? this.ssrViewportHeight / this.ssrChildHeight : this.ssrViewportWidth / this.ssrChildWidth);
}
let propertyName = this.horizontal ? 'offsetLeft' : 'offsetTop';
let children = ((this.containerElementRef && this.containerElementRef.nativeElement) || this.contentElementRef.nativeElement).children;
let childrenLength = children ? children.length : 0;
if (childrenLength === 0) {
return 1;
}
let firstOffset = children[0][propertyName];
let result = 1;
while (result < childrenLength && firstOffset === children[result][propertyName]) {
++result;
}
return result;
}
getScrollStartPosition() {
let windowScrollValue = undefined;
if (this.parentScroll instanceof Window) {
windowScrollValue = window[this._pageOffsetType];
}
return windowScrollValue || this.getScrollElement()[this._scrollType] || 0;
}
minMeasuredChildWidth;
minMeasuredChildHeight;
wrapGroupDimensions;
resetWrapGroupDimensions() {
const oldWrapGroupDimensions = this.wrapGroupDimensions;
this.invalidateAllCachedMeasurements();
if (!this.enableUnequalChildrenSizes || !oldWrapGroupDimensions || oldWrapGroupDimensions.numberOfKnownWrapGroupChildSizes === 0) {
return;
}
const itemsPerWrapGroup = this.countItemsPerWrapGroup();
for (let wrapGroupIndex = 0; wrapGroupIndex < oldWrapGroupDimensions.maxChildSizePerWrapGroup.length; ++wrapGroupIndex) {
const oldWrapGroupDimension = oldWrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex];
if (!oldWrapGroupDimension || !oldWrapGroupDimension.items || !oldWrapGroupDimension.items.length) {
continue;
}
if (oldWrapGroupDimension.items.length !== itemsPerWrapGroup) {
return;
}
let itemsChanged = false;
let arrayStartIndex = itemsPerWrapGroup * wrapGroupIndex;
for (let i = 0; i < itemsPerWrapGroup; ++i) {
if (!this.compareItems(oldWrapGroupDimension.items[i], this.items[arrayStartIndex + i])) {
itemsChanged = true;
break;
}
}
if (!itemsChanged) {
++this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths += oldWrapGroupDimension.childWidth || 0;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights += oldWrapGroupDimension.childHeight || 0;
this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex] = oldWrapGroupDimension;
}
}
}
calculateDimensions() {
let scrollElement = this.getScrollElement();
const maxCalculatedScrollBarSize = 25; // Note: Formula to auto-calculate doesn't work for ParentScroll, so we default to this if not set by consuming application
this.calculatedScrollbarHeight = Math.max(Math.min(scrollElement.offsetHeight - scrollElement.clientHeight, maxCalculatedScrollBarSize), this.calculatedScrollbarHeight);
this.calculatedScrollbarWidth = Math.max(Math.min(scrollElement.offsetWidth - scrollElement.clientWidth, maxCalculatedScrollBarSize), this.calculatedScrollbarWidth);
let viewportWidth = scrollElement.offsetWidth - (this.scrollbarWidth || this.calculatedScrollbarWidth || (this.horizontal ? 0 : maxCalculatedScrollBarSize));
let viewportHeight = scrollElement.offsetHeight - (this.scrollbarHeight || this.calculatedScrollbarHeight || (this.horizontal ? maxCalculatedScrollBarSize : 0));
let content = (this.containerElementRef && this.containerElementRef.nativeElement) || this.contentElementRef.nativeElement;
let itemsPerWrapGroup = this.countItemsPerWrapGroup();
let wrapGroupsPerPage;
let defaultChildWidth;
let defaultChildHeight;
if (this.isAngularUniversalSSR) {
viewportWidth = this.ssrViewportWidth;
viewportHeight = this.ssrViewportHeight;
defaultChildWidth = this.ssrChildWidth;
defaultChildHeight = this.ssrChildHeight;
let itemsPerRow = Math.max(Math.ceil(viewportWidth / defaultChildWidth), 1);
let itemsPerCol = Math.max(Math.ceil(viewportHeight / defaultChildHeight), 1);
wrapGroupsPerPage = this.horizontal ? itemsPerRow : itemsPerCol;
}
else if (!this.enableUnequalChildrenSizes) {
if (content.children.length > 0) {
if (!this.childWidth || !this.childHeight) {
if (!this.minMeasuredChildWidth && viewportWidth > 0) {
this.minMeasuredChildWidth = viewportWidth;
}
if (!this.minMeasuredChildHeight && viewportHeight > 0) {
this.minMeasuredChildHeight = viewportHeight;
}
}
let child = content.children[0];
let clientRect = this.getElementSize(child);
this.minMeasuredChildWidth = Math.min(this.minMeasuredChildWidth, clientRect.width);
this.minMeasuredChildHeight = Math.min(this.minMeasuredChildHeight, clientRect.height);
}
defaultChildWidth = this.childWidth || this.minMeasuredChildWidth || viewportWidth;
defaultChildHeight = this.childHeight || this.minMeasuredChildHeight || viewportHeight;
let itemsPerRow = Math.max(Math.ceil(viewportWidth / defaultChildWidth), 1);
let itemsPerCol = Math.max(Math.ceil(viewportHeight / defaultChildHeight), 1);
wrapGroupsPerPage = this.horizontal ? itemsPerRow : itemsPerCol;
}
else {
let scrollOffset = scrollElement[this._scrollType] - (this.previousViewPort ? this.previousViewPort.padding : 0);
let arrayStartIndex = this.previousViewPort.startIndexWithBuffer || 0;
let wrapGroupIndex = Math.ceil(arrayStartIndex / itemsPerWrapGroup);
let maxWidthForWrapGroup = 0;
let maxHeightForWrapGroup = 0;
let sumOfVisibleMaxWidths = 0;
let sumOfVisibleMaxHeights = 0;
wrapGroupsPerPage = 0;
for (let i = 0; i < content.children.length; ++i) {
++arrayStartIndex;
let child = content.children[i];
let clientRect = this.getElementSize(child);
maxWidthForWrapGroup = Math.max(maxWidthForWrapGroup, clientRect.width);
maxHeightForWrapGroup = Math.max(maxHeightForWrapGroup, clientRect.height);
if (arrayStartIndex % itemsPerWrapGroup === 0) {
let oldValue = this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex];
if (oldValue) {
--this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths -= oldValue.childWidth || 0;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights -= oldValue.childHeight || 0;
}
++this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
const items = this.items.slice(arrayStartIndex - itemsPerWrapGroup, arrayStartIndex);
this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex] = {
childWidth: maxWidthForWrapGroup,
childHeight: maxHeightForWrapGroup,
items: items
};
this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths += maxWidthForWrapGroup;
this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights += maxHeightForWrapGroup;
if (this.horizontal) {
let maxVisibleWidthForWrapGroup = Math.min(maxWidthForWrapGroup, Math.max(viewportWidth - sumOfVisibleMaxWidths, 0));
if (scrollOffset > 0) {
let scrollOffsetToRemove = Math.min(scrollOffset, maxVisibleWidthForWrapGroup);
maxVisibleWidthForWrapGroup -= scrollOffsetToRemove;
scrollOffset -= scrollOffsetToRemove;
}
sumOfVisibleMaxWidths += maxVisibleWidthForWrapGroup;
if (maxVisibleWidthForWrapGroup > 0 && viewportWidth >= sumOfVisibleMaxWidths) {
++wrapGroupsPerPage;
}
}
else {
let maxVisibleHeightForWrapGroup = Math.min(maxHeightForWrapGroup, Math.max(viewportHeight - sumOfVisibleMaxHeights, 0));
if (scrollOffset > 0) {
let scrollOffsetToRemove = Math.min(scrollOffset, maxVisibleHeightForWrapGroup);
maxVisibleHeightForWrapGroup -= scrollOffsetToRemove;
scrollOffset -= scrollOffsetToRemove;
}
sumOfVisibleMaxHeights += maxVisibleHeightForWrapGroup;
if (maxVisibleHeightForWrapGroup > 0 && viewportHeight >= sumOfVisibleMaxHeights) {
++wrapGroupsPerPage;
}
}
++wrapGroupIndex;
maxWidthForWrapGroup = 0;
maxHeightForWrapGroup = 0;
}
}
let averageChildWidth = this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths / this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
let averageChildHeight = this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights / this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;
defaultChildWidth = this.childWidth || averageChildWidth || viewportWidth;
defaultChildHeight = this.childHeight || averageChildHeight || viewportHeight;
if (this.horizontal) {
if (viewportWidth > sumOfVisibleMaxWidths) {
wrapGroupsPerPage += Math.ceil((viewportWidth - sumOfVisibleMaxWidths) / defaultChildWidth);
}
}
else {
if (viewportHeight > sumOfVisibleMaxHeights) {
wrapGroupsPerPage += Math.ceil((viewportHeight - sumOfVisibleMaxHeights) / defaultChildHeight);
}
}
}
let itemCount = this.items.length;
let itemsPerPage = itemsPerWrapGroup * wrapGroupsPerPage;
let pageCount_fractional = itemCount / itemsPerPage;
let numberOfWrapGroups = Math.ceil(itemCount / itemsPerWrapGroup);
let scrollLength = 0;
let defaultScrollLengthPerWrapGroup = this.horizontal ? defaultChildWidth : defaultChildHeight;
if (this.enableUnequalChildrenSizes) {
let numUnknownChildSizes = 0;
for (let i = 0; i < numberOfWrapGroups; ++i) {
let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];
if (childSize) {
scrollLength += childSize;
}
else {
++numUnknownChildSizes;
}
}
scrollLength += Math.round(numUnknownChildSizes * defaultScrollLengthPerWrapGroup);
}
else {
scrollLength = numberOfWrapGroups * defaultScrollLengthPerWrapGroup;
}
if (this.headerElementRef) {
scrollLength += this.headerElementRef.nativeElement.clientHeight;
}
let viewportLength = this.horizontal ? viewportWidth : viewportHeight;
let maxScrollPosition = Math.max(scrollLength - viewportLength, 0);
return {
childHeight: defaultChildHeight,
childWidth: defaultChildWidth,
itemCount: itemCount,
itemsPerPage: itemsPerPage,
itemsPerWrapGroup: itemsPerWrapGroup,
maxScrollPosition: maxScrollPosition,
pageCount_fractional: pageCount_fractional,
scrollLength: scrollLength,
viewportLength: viewportLength,
wrapGroupsPerPage: wrapGroupsPerPage,
};
}
cachedPageSize = 0;
previousScrollNumberElements = 0;
calculatePadding(arrayStartIndexWithBuffer, dimensions) {
if (dimensions.itemCount === 0) {
return 0;
}
let defaultScrollLengthPerWrapGroup = dimensions[this._childScrollDim];
let startingWrapGroupIndex = Math.floor(arrayStartIndexWithBuffer / dimensions.itemsPerWrapGroup) || 0;
if (!this.enableUnequalChildrenSizes) {
return defaultScrollLengthPerWrapGroup * startingWrapGroupIndex;
}
let numUnknownChildSizes = 0;
let result = 0;
for (let i = 0; i < startingWrapGroupIndex; ++i) {
let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];
if (childSize) {
result += childSize;
}
else {
++numUnknownChildSizes;
}
}
result += Math.round(numUnknownChildSizes * defaultScrollLengthPerWrapGroup);
return result;
}
calculatePageInfo(scrollPosition, dimensions) {
let scrollPercentage = 0;
let scrollBottomPercentage = 1;
let arrayStartIndex = 0;
let arrayEndIndex = 0;
if (this.enableUnequalChildrenSizes) {
const numberOfWrapGroups = Math.ceil(dimensions.itemCount / dimensions.itemsPerWrapGroup);
let totalScrolledLength = 0;
let defaultScrollLengthPerWrapGroup = dimensions[this._childScrollDim];
let i = 0;
for (; i < numberOfWrapGroups; ++i) {
let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];
if (childSize) {
totalScrolledLength += childSize;
}
else {
totalScrolledLength += defaultScrollLengthPerWrapGroup;
}
if (scrollPosition < totalScrolledLength) {
scrollPercentage = i / numberOfWrapGroups;
break;
}
}
let j = i + 1;
for (; j < numberOfWrapGroups; ++j) {
let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[j] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[j][this._childScrollDim];
if (childSize) {
totalScrolledLength += childSize;
}
else {
totalScrolledLength += defaultScrollLengthPerWrapGroup;
}
if (scrollPosition + dimensions.viewportLength < totalScrolledLength) {
scrollBottomPercentage = j / numberOfWrapGroups;
break;
}
}
arrayStartIndex = i * dimensions.itemsPerWrapGroup;
arrayEndIndex = j * dimensions.itemsPerWrapGroup;
}
else {
scrollPercentage = scrollPosition / dimensions.scrollLength;
let startingArrayIndex_fractional = Math.min(Math.max(scrollPercentage * dimensions.pageCount_fractional, 0), dimensions.pageCount_fractional) * dimensions.itemsPerPage;
let maxStart = dimensions.itemCount - dimensions.itemsPerPage - 1;
arrayStartIndex = Math.min(Math.floor(startingArrayIndex_fractional), maxStart);
arrayEndIndex = Math.ceil(startingArrayIndex_fractional) + dimensions.itemsPerPage - 1;
}
arrayStartIndex -= arrayStartIndex % dimensions.itemsPerWrapGroup; // round down to start of wrapGroup
if (this.stripedTable) {
let bufferBoundary = 2 * dimensions.itemsPerWrapGroup;
if (arrayStartIndex % bufferBoundary !== 0) {
arrayStartIndex = Math.max(arrayStartIndex - arrayStartIndex % bufferBoundary, 0);
}
}
let endIndexWithinWrapGroup = (arrayEndIndex + 1) % dimensions.itemsPerWrapGroup;
if (endIndexWithinWrapGroup > 0) {
arrayEndIndex += dimensions.itemsPerWrapGroup - endIndexWithinWrapGroup; // round up to end of wrapGroup
}
if (isNaN(arrayStartIndex)) {
arrayStartIndex = 0;
}
if (isNaN(arrayEndIndex)) {
arrayEndIndex = 0;
}
arrayStartIndex = Math.min(Math.max(arrayStartIndex, 0), dimensions.itemCount - 1);
arrayEndIndex = Math.min(Math.max(arrayEndIndex, 0), dimensions.itemCount - 1);
let bufferSize = this.bufferAmount * dimensions.itemsPerWrapGroup;
let startIndexWithBuffer = Math.min(Math.max(arrayStartIndex - bufferSize, 0), dimensions.itemCount - 1);
let endIndexWithBuffer = Math.min(Math.max(arrayEndIndex + bufferSize, 0), dimensions.itemCount - 1);
return {
startIndex: arrayStartIndex,
endIndex: arrayEndIndex,
startIndexWithBuffer: startIndexWithBuffer,
endIndexWithBuffer: endIndexWithBuffer,
scrollStartPosition: scrollPosition,
scrollEndPosition: scrollPosition + dimensions.viewportLength,
maxScrollPosition: dimensions.maxScrollPosition
};
}
calculateViewport() {
let dimensions = this.calculateDimensions();
let offset = this.getElementsOffset();
let scrollStartPosition = this.getScrollStartPosition();
if (scrollStartPosition > (dimensions.scrollLength + offset) && !(this.parentScroll instanceof Window)) {
scrollStartPosition = dimensions.scrollLength;
}
else {
scrollStartPosition -= offset;
}
scrollStartPosition = Math.max(0, scrollStartPosition);
let pageInfo = this.calculatePageInfo(scrollStartPosition, dimensions);
let newPadding = this.calculatePadding(pageInfo.startIndexWithBuffer, dimensions);
let newScrollLength = Math.round(dimensions.scrollLength);
return {
startIndex: pageInfo.startIndex,
endIndex: pageInfo.endIndex,
startIndexWithBuffer: pageInfo.startIndexWithBuffer,
endIndexWithBuffer: pageInfo.endIndexWithBuffer,
padding: Math.round(newPadding),
scrollLength: newScrollLength,
scrollbarLength: newScrollLength + offset,
scrollStartPosition: pageInfo.scrollStartPosition,
scrollEndPosition: pageInfo.scrollEndPosition,
maxScrollPosition: pageInfo.maxScrollPosition
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: VirtualScrollerComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: PLATFORM_ID }, { token: 'virtual-scroller-default-options', optional: true }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.3", type: VirtualScrollerComponent, isStandalone: false, selector: "virtual-scroller,[virtualScroller]", inputs: { executeRefreshOutsideAngularZone: "executeRefreshOutsideAngularZone", enableUnequalChildrenSizes: "enableUnequalChildrenSizes", RTL: "RTL", useMarginInsteadOfTranslate: "useMarginInsteadOfTranslate", modifyOverflowStyleOfParentScroll: "modifyOverflowStyleOfParentScroll", stripedTable: "stripedTable", scrollbarWidth: "scrollbarWidth", scrollbarHeight: "scrollbarHeight", childWidth: "childWidth", childHeight: "childHeight", ssrChildWidth: "ssrChildWidth", ssrChildHeight: "ssrChildHeight", ssrViewportWidth: "ssrViewportWidth", ssrViewportHeight: "ssrViewportHeight", bufferAmount: "bufferAmount", scrollAnimationTime: "scrollAnimationTime", resizeBypassRefreshThreshold: "resizeBypassRefreshThreshold", scrollThrottlingTime: "scrollThrottlingTime", scrollDebounceTime: "scrollDebounceTime", checkResizeInterval: "checkResizeInterval", items: "items", compareItems: "compareItems", horizontal: "horizontal", parentScroll: "parentScroll" }, outputs: { vsUpdate: "vsUpdate", vsChange: "vsChange", vsStart: "vsStart", vsEnd: "vsEnd" }, host: { properties: { "class.horizontal": "horizontal", "class.vertical": "!horizontal", "class.selfScroll": "!parentScroll", "class.rtl": "RTL" } }, queries: [{ propertyName: "headerElementRef", first: true, predicate: ["header"], descendants: true, read: ElementRef }, { propertyName: "containerElementRef", first: true, predicate: ["container"], descendants: true, read: ElementRef }], viewQueries: [{ propertyName: "contentElementRef", first: true, predicate: ["content"], descendants: true, read: ElementRef, static: true }, { propertyName: "invisiblePaddingElementRef", first: true, predicate: ["invisiblePadding"], descendants: true, read: ElementRef, static: true }], exportAs: ["virtualScroller"], usesOnChanges: true, ngImport: i0, template: `
<div class="total-padding" #invisiblePadding></div>
<div class="scrollable-content" #content>
<ng-content></ng-content>
</div>
`, isInline: true, styles: [":host{position:relative;display:block;-webkit-overflow-scrolling:touch}:host.horizontal.selfScroll{overflow-y:visible;overflow-x:auto}:host.horizontal.selfScroll.rtl{transform:scaleX(-1)}:host.vertical.selfScroll{overflow-y:auto;overflow-x:visible}.scrollable-content{top:0;left:0;width:100%;height:100%;max-width:100vw;max-height:100vh;position:absolute}.scrollable-content ::ng-deep>*{box-sizing:border-box}:host.horizontal{white-space:nowrap}:host.horizontal .scrollable-content{display:flex}:host.horizontal .scrollable-content ::ng-deep>*{flex-shrink:0;flex-grow:0;white-space:initial}:host.horizontal.rtl .scrollable-content ::ng-deep>*{transform:scaleX(-1)}.total-padding{widt