@iharbeck/ngx-virtual-scroller
Version:
Virtual Scroll displays a virtual, "infinite" list. Supports horizontal/vertical, variable heights, & multi-column.
1 lines • 91.6 kB
Source Map (JSON)
{"version":3,"file":"iharbeck-ngx-virtual-scroller.mjs","sources":["../../../projects/ngx-virtual-scroller/src/lib/ngx-virtual-scroller.component.ts","../../../projects/ngx-virtual-scroller/src/lib/ngx-virtual-scroller.module.ts","../../../projects/ngx-virtual-scroller/src/public-api.ts","../../../projects/ngx-virtual-scroller/src/iharbeck-ngx-virtual-scroller.ts"],"sourcesContent":["import {\n ChangeDetectorRef,\n Component,\n ContentChild,\n ElementRef,\n EventEmitter,\n Inject,\n Input,\n NgModule,\n NgZone,\n OnChanges,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n Renderer2,\n ViewChild,\n} from '@angular/core';\n\nimport {PLATFORM_ID} from '@angular/core';\nimport {isPlatformServer} from '@angular/common';\n\nimport {CommonModule} from '@angular/common';\n\nimport * as tween from '@tweenjs/tween.js'\n\nexport interface VirtualScrollerDefaultOptions {\n checkResizeInterval: number\n modifyOverflowStyleOfParentScroll: boolean,\n resizeBypassRefreshThreshold: number,\n scrollAnimationTime: number;\n scrollDebounceTime: number;\n scrollThrottlingTime: number;\n scrollbarHeight?: number;\n scrollbarWidth?: number;\n stripedTable: boolean\n}\n\nexport function VIRTUAL_SCROLLER_DEFAULT_OPTIONS_FACTORY(): VirtualScrollerDefaultOptions {\n return {\n checkResizeInterval: 1000,\n modifyOverflowStyleOfParentScroll: true,\n resizeBypassRefreshThreshold: 5,\n scrollAnimationTime: 750,\n scrollDebounceTime: 0,\n scrollThrottlingTime: 0,\n stripedTable: false\n };\n}\n\nexport interface WrapGroupDimensions {\n maxChildSizePerWrapGroup: WrapGroupDimension[];\n numberOfKnownWrapGroupChildSizes: number;\n sumOfKnownWrapGroupChildHeights: number;\n sumOfKnownWrapGroupChildWidths: number;\n}\n\nexport interface WrapGroupDimension {\n childHeight: number;\n childWidth: number;\n items: any[];\n}\n\nexport interface IDimensions {\n childHeight: number;\n childWidth: number;\n itemCount: number;\n itemsPerPage: number;\n itemsPerWrapGroup: number;\n maxScrollPosition: number;\n pageCount_fractional: number;\n scrollLength: number;\n viewportLength: number;\n wrapGroupsPerPage: number;\n}\n\nexport interface IPageInfo {\n endIndex: number;\n endIndexWithBuffer: number;\n maxScrollPosition: number;\n scrollEndPosition: number;\n scrollStartPosition: number;\n startIndex: number;\n startIndexWithBuffer: number;\n}\n\nexport interface IViewport extends IPageInfo {\n padding: number;\n scrollLength: number;\n scrollbarLength: number;\n}\n\n\n@Component({\n selector: 'virtual-scroller,[virtualScroller]',\n exportAs: 'virtualScroller',\n template: `\n <ng-content select=\"[tab-header]\"></ng-content>\n <div class=\"total-padding\" #invisiblePadding></div>\n <div class=\"scrollable-content\" #content>\n <ng-content></ng-content>\n </div>\n <ng-content select=\"[tab-footer]\"></ng-content>\n `,\n host: {\n '[class.horizontal]': 'horizontal',\n '[class.vertical]': '!horizontal',\n '[class.selfScroll]': '!parentScroll',\n '[class.rtl]': 'RTL'\n },\n styles: [`\n :host {\n position: relative;\n display: block;\n -webkit-overflow-scrolling: touch;\n }\n\n :host.horizontal.selfScroll {\n overflow-y: visible;\n overflow-x: auto;\n }\n\n :host.horizontal.selfScroll.rtl {\n transform: scaleX(-1);\n }\n\n :host.vertical.selfScroll {\n overflow-y: auto;\n overflow-x: visible;\n }\n\n .scrollable-content {\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n max-width: 100vw;\n max-height: 100vh;\n position: absolute;\n }\n\n .scrollable-content ::ng-deep > * {\n box-sizing: border-box;\n }\n\n :host.horizontal {\n white-space: nowrap;\n }\n\n :host.horizontal .scrollable-content {\n display: flex;\n }\n\n :host.horizontal .scrollable-content ::ng-deep > * {\n flex-shrink: 0;\n flex-grow: 0;\n white-space: initial;\n }\n\n :host.horizontal.rtl .scrollable-content ::ng-deep > * {\n transform: scaleX(-1);\n }\n\n .total-padding {\n width: 1px;\n opacity: 0;\n }\n\n :host.horizontal .total-padding {\n height: 100%;\n }\n `],\n standalone: false\n})\nexport class VirtualScrollerComponent implements OnInit, OnChanges, OnDestroy {\n public viewPortItems: any[];\n public window = window;\n\n public get viewPortInfo(): IPageInfo {\n let pageInfo: IViewport = this.previousViewPort || <any>{};\n return {\n startIndex: pageInfo.startIndex || 0,\n endIndex: pageInfo.endIndex || 0,\n scrollStartPosition: pageInfo.scrollStartPosition || 0,\n scrollEndPosition: pageInfo.scrollEndPosition || 0,\n maxScrollPosition: pageInfo.maxScrollPosition || 0,\n startIndexWithBuffer: pageInfo.startIndexWithBuffer || 0,\n endIndexWithBuffer: pageInfo.endIndexWithBuffer || 0\n };\n }\n\n @Input()\n public executeRefreshOutsideAngularZone: boolean = false;\n\n protected _enableUnequalChildrenSizes: boolean = false;\n @Input()\n public get enableUnequalChildrenSizes(): boolean {\n return this._enableUnequalChildrenSizes;\n }\n\n public set enableUnequalChildrenSizes(value: boolean) {\n if (this._enableUnequalChildrenSizes === value) {\n return;\n }\n\n this._enableUnequalChildrenSizes = value;\n this.minMeasuredChildWidth = undefined;\n this.minMeasuredChildHeight = undefined;\n }\n\n @Input()\n public RTL: boolean = false;\n\n @Input()\n public useMarginInsteadOfTranslate: boolean = false;\n\n @Input()\n public modifyOverflowStyleOfParentScroll: boolean;\n\n @Input()\n public stripedTable: boolean;\n\n @Input()\n public scrollbarWidth: number;\n\n @Input()\n public scrollbarHeight: number;\n\n @Input()\n public childWidth: number;\n\n @Input()\n public childHeight: number;\n\n @Input()\n public ssrChildWidth: number;\n\n @Input()\n public ssrChildHeight: number;\n\n @Input()\n public ssrViewportWidth: number = 1920;\n\n @Input()\n public ssrViewportHeight: number = 1080;\n\n protected _bufferAmount: number;\n @Input()\n public get bufferAmount(): number {\n if (typeof (this._bufferAmount) === 'number' && this._bufferAmount >= 0) {\n return this._bufferAmount;\n } else {\n return this.enableUnequalChildrenSizes ? 5 : 0;\n }\n }\n\n public set bufferAmount(value: number) {\n this._bufferAmount = value;\n }\n\n @Input()\n public scrollAnimationTime: number;\n\n @Input()\n public resizeBypassRefreshThreshold: number;\n\n protected _scrollThrottlingTime: number;\n @Input()\n public get scrollThrottlingTime(): number {\n return this._scrollThrottlingTime;\n }\n\n public set scrollThrottlingTime(value: number) {\n this._scrollThrottlingTime = value;\n this.updateOnScrollFunction();\n }\n\n protected _scrollDebounceTime: number;\n @Input()\n public get scrollDebounceTime(): number {\n return this._scrollDebounceTime;\n }\n\n public set scrollDebounceTime(value: number) {\n this._scrollDebounceTime = value;\n this.updateOnScrollFunction();\n }\n\n protected onScroll: () => void;\n\n protected updateOnScrollFunction(): void {\n if (this.scrollDebounceTime) {\n this.onScroll = <any>this.debounce(() => {\n this.refresh_internal(false);\n }, this.scrollDebounceTime);\n } else if (this.scrollThrottlingTime) {\n this.onScroll = <any>this.throttleTrailing(() => {\n this.refresh_internal(false);\n }, this.scrollThrottlingTime);\n } else {\n this.onScroll = () => {\n this.refresh_internal(false);\n };\n }\n }\n\n protected checkScrollElementResizedTimer: number;\n protected _checkResizeInterval: number;\n @Input()\n public get checkResizeInterval(): number {\n return this._checkResizeInterval;\n }\n\n public set checkResizeInterval(value: number) {\n if (this._checkResizeInterval === value) {\n return;\n }\n\n this._checkResizeInterval = value;\n this.addScrollEventHandlers();\n }\n\n protected _items: any[] = [];\n @Input()\n public get items(): any[] {\n return this._items;\n }\n\n public set items(value: any[]) {\n if (value === this._items) {\n return;\n }\n\n this._items = value || [];\n this.refresh_internal(true);\n }\n\n @Input()\n public compareItems: (item1: any, item2: any) => boolean = (item1: any, item2: any) => item1 === item2;\n\n protected _horizontal: boolean;\n @Input()\n public get horizontal(): boolean {\n return this._horizontal;\n }\n\n public set horizontal(value: boolean) {\n this._horizontal = value;\n this.updateDirection();\n }\n\n protected revertParentOverscroll(): void {\n const scrollElement = this.getScrollElement();\n if (scrollElement && this.oldParentScrollOverflow) {\n scrollElement.style['overflow-y'] = this.oldParentScrollOverflow.y;\n scrollElement.style['overflow-x'] = this.oldParentScrollOverflow.x;\n }\n\n this.oldParentScrollOverflow = undefined;\n }\n\n protected oldParentScrollOverflow: { x: string, y: string };\n protected _parentScroll: Element | Window;\n @Input()\n public get parentScroll(): Element | Window {\n return this._parentScroll;\n }\n\n public set parentScroll(value: Element | Window) {\n if (this._parentScroll === value) {\n return;\n }\n\n this.revertParentOverscroll();\n this._parentScroll = value;\n this.addScrollEventHandlers();\n\n const scrollElement = this.getScrollElement();\n if (this.modifyOverflowStyleOfParentScroll && scrollElement !== this.element.nativeElement) {\n this.oldParentScrollOverflow = {x: scrollElement.style['overflow-x'], y: scrollElement.style['overflow-y']};\n scrollElement.style['overflow-y'] = this.horizontal ? 'visible' : 'auto';\n scrollElement.style['overflow-x'] = this.horizontal ? 'auto' : 'visible';\n }\n }\n\n @Output()\n public vsUpdate: EventEmitter<any[]> = new EventEmitter<any[]>();\n\n @Output()\n public vsChange: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>();\n\n @Output()\n public vsStart: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>();\n\n @Output()\n public vsEnd: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>();\n\n @ViewChild('content', {read: ElementRef, static: true})\n protected contentElementRef: ElementRef;\n\n @ViewChild('invisiblePadding', {read: ElementRef, static: true})\n protected invisiblePaddingElementRef: ElementRef;\n\n @ContentChild('container', {read: ElementRef, static: false})\n protected containerElementRef: ElementRef;\n\n public ngOnInit(): void {\n this.addScrollEventHandlers();\n }\n\n public ngOnDestroy(): void {\n this.removeScrollEventHandlers();\n this.revertParentOverscroll();\n }\n\n public ngOnChanges(changes: any): void {\n let indexLengthChanged = this.cachedItemsLength !== this.items.length;\n this.cachedItemsLength = this.items.length;\n\n const firstRun: boolean = !changes.items || !changes.items.previousValue || changes.items.previousValue.length === 0;\n this.refresh_internal(indexLengthChanged || firstRun);\n }\n\n public ngDoCheck(): void {\n if (this.cachedItemsLength !== this.items.length) {\n this.cachedItemsLength = this.items.length;\n this.refresh_internal(true);\n return;\n }\n\n if (this.previousViewPort && this.viewPortItems && this.viewPortItems.length > 0) {\n let itemsArrayChanged = false;\n for (let i = 0; i < this.viewPortItems.length; ++i) {\n if (!this.compareItems(this.items[this.previousViewPort.startIndexWithBuffer + i], this.viewPortItems[i])) {\n itemsArrayChanged = true;\n break;\n }\n }\n if (itemsArrayChanged) {\n this.refresh_internal(true);\n }\n }\n }\n\n public refresh(): void {\n this.refresh_internal(true);\n }\n\n public invalidateAllCachedMeasurements(): void {\n this.wrapGroupDimensions = {\n maxChildSizePerWrapGroup: [],\n numberOfKnownWrapGroupChildSizes: 0,\n sumOfKnownWrapGroupChildWidths: 0,\n sumOfKnownWrapGroupChildHeights: 0\n };\n\n this.minMeasuredChildWidth = undefined;\n this.minMeasuredChildHeight = undefined;\n\n this.refresh_internal(false);\n }\n\n public invalidateCachedMeasurementForItem(item: any): void {\n if (this.enableUnequalChildrenSizes) {\n let index = this.items && this.items.indexOf(item);\n if (index >= 0) {\n this.invalidateCachedMeasurementAtIndex(index);\n }\n } else {\n this.minMeasuredChildWidth = undefined;\n this.minMeasuredChildHeight = undefined;\n }\n\n this.refresh_internal(false);\n }\n\n public invalidateCachedMeasurementAtIndex(index: number): void {\n if (this.enableUnequalChildrenSizes) {\n let cachedMeasurement = this.wrapGroupDimensions.maxChildSizePerWrapGroup[index];\n if (cachedMeasurement) {\n this.wrapGroupDimensions.maxChildSizePerWrapGroup[index] = undefined;\n --this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths -= cachedMeasurement.childWidth || 0;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights -= cachedMeasurement.childHeight || 0;\n }\n } else {\n this.minMeasuredChildWidth = undefined;\n this.minMeasuredChildHeight = undefined;\n }\n\n this.refresh_internal(false);\n }\n\n public scrollInto(item: any, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void {\n let index: number = this.items.indexOf(item);\n if (index === -1) {\n return;\n }\n\n this.scrollToIndex(index, alignToBeginning, additionalOffset, animationMilliseconds, animationCompletedCallback);\n }\n\n public scrollToIndex(index: number, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void {\n let maxRetries: number = 5;\n\n let retryIfNeeded = () => {\n --maxRetries;\n if (maxRetries <= 0) {\n if (animationCompletedCallback) {\n animationCompletedCallback();\n }\n return;\n }\n\n let dimensions = this.calculateDimensions();\n let desiredStartIndex = Math.min(Math.max(index, 0), dimensions.itemCount - 1);\n if (this.previousViewPort.startIndex === desiredStartIndex) {\n if (animationCompletedCallback) {\n animationCompletedCallback();\n }\n return;\n }\n\n this.scrollToIndex_internal(index, alignToBeginning, additionalOffset, 0, retryIfNeeded);\n };\n\n this.scrollToIndex_internal(index, alignToBeginning, additionalOffset, animationMilliseconds, retryIfNeeded);\n }\n\n protected scrollToIndex_internal(index: number, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void {\n animationMilliseconds = animationMilliseconds === undefined ? this.scrollAnimationTime : animationMilliseconds;\n\n let dimensions = this.calculateDimensions();\n let scroll = this.calculatePadding(index, dimensions) + additionalOffset;\n if (!alignToBeginning) {\n scroll -= dimensions.wrapGroupsPerPage * dimensions[this._childScrollDim];\n }\n\n this.scrollToPosition(scroll, animationMilliseconds, animationCompletedCallback);\n }\n\n public scrollToPosition(scrollPosition: number, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void {\n scrollPosition += this.getElementsOffset();\n\n animationMilliseconds = animationMilliseconds === undefined ? this.scrollAnimationTime : animationMilliseconds;\n\n let scrollElement = this.getScrollElement();\n\n let animationRequest: number;\n\n if (this.currentTween) {\n this.currentTween.stop();\n this.currentTween = undefined;\n }\n\n if (!animationMilliseconds) {\n this.renderer.setProperty(scrollElement, this._scrollType, scrollPosition);\n this.refresh_internal(false, animationCompletedCallback);\n return;\n }\n\n const tweenConfigObj = {scrollPosition: scrollElement[this._scrollType]};\n\n let newTween = new tween.Tween(tweenConfigObj)\n .to({scrollPosition}, animationMilliseconds)\n .easing(tween.Easing.Quadratic.Out)\n .onUpdate((data) => {\n if (isNaN(data.scrollPosition)) {\n return;\n }\n this.renderer.setProperty(scrollElement, this._scrollType, data.scrollPosition);\n this.refresh_internal(false);\n })\n .onStop(() => {\n cancelAnimationFrame(animationRequest);\n })\n .start();\n\n const animate = (time?: number) => {\n if (!newTween['isPlaying']()) {\n return;\n }\n\n newTween.update(time);\n if (tweenConfigObj.scrollPosition === scrollPosition) {\n this.refresh_internal(false, animationCompletedCallback);\n return;\n }\n\n this.zone.runOutsideAngular(() => {\n animationRequest = requestAnimationFrame(animate);\n });\n };\n\n animate();\n this.currentTween = newTween;\n }\n\n protected isAngularUniversalSSR: boolean;\n\n constructor(\n protected readonly element: ElementRef,\n protected readonly renderer: Renderer2,\n protected readonly zone: NgZone,\n protected changeDetectorRef: ChangeDetectorRef,\n @Inject(PLATFORM_ID) platformId: Object,\n @Optional() @Inject('virtual-scroller-default-options')\n options: VirtualScrollerDefaultOptions\n ) {\n\n this.isAngularUniversalSSR = isPlatformServer(platformId);\n\n this.checkResizeInterval = options.checkResizeInterval;\n this.modifyOverflowStyleOfParentScroll = options.modifyOverflowStyleOfParentScroll;\n this.resizeBypassRefreshThreshold = options.resizeBypassRefreshThreshold;\n this.scrollAnimationTime = options.scrollAnimationTime;\n this.scrollDebounceTime = options.scrollDebounceTime;\n this.scrollThrottlingTime = options.scrollThrottlingTime;\n this.scrollbarHeight = options.scrollbarHeight;\n this.scrollbarWidth = options.scrollbarWidth;\n this.stripedTable = options.stripedTable;\n\n this.horizontal = false;\n this.resetWrapGroupDimensions();\n }\n\n protected getElementSize(element: HTMLElement): any {\n let result = element.getBoundingClientRect();\n let styles = getComputedStyle(element);\n let marginTop = parseInt(styles['margin-top'], 10) || 0;\n let marginBottom = parseInt(styles['margin-bottom'], 10) || 0;\n let marginLeft = parseInt(styles['margin-left'], 10) || 0;\n let marginRight = parseInt(styles['margin-right'], 10) || 0;\n\n return {\n top: result.top + marginTop,\n bottom: result.bottom + marginBottom,\n left: result.left + marginLeft,\n right: result.right + marginRight,\n width: result.width + marginLeft + marginRight,\n height: result.height + marginTop + marginBottom\n };\n }\n\n protected previousScrollBoundingRect: ClientRect;\n\n protected checkScrollElementResized(): void {\n let boundingRect = this.getElementSize(this.getScrollElement());\n\n let sizeChanged: boolean;\n if (!this.previousScrollBoundingRect) {\n sizeChanged = true;\n } else {\n let widthChange = Math.abs(boundingRect.width - this.previousScrollBoundingRect.width);\n let heightChange = Math.abs(boundingRect.height - this.previousScrollBoundingRect.height);\n sizeChanged = widthChange > this.resizeBypassRefreshThreshold || heightChange > this.resizeBypassRefreshThreshold;\n }\n\n if (sizeChanged) {\n this.previousScrollBoundingRect = boundingRect;\n if (boundingRect.width > 0 && boundingRect.height > 0) {\n this.refresh_internal(false);\n }\n }\n }\n\n protected _invisiblePaddingProperty;\n protected _offsetType;\n protected _scrollType;\n protected _pageOffsetType;\n protected _childScrollDim;\n protected _translateDir;\n protected _marginDir;\n\n protected updateDirection(): void {\n if (this.horizontal) {\n this._childScrollDim = 'childWidth';\n this._invisiblePaddingProperty = 'width';\n this._marginDir = 'margin-left';\n this._offsetType = 'offsetLeft';\n this._pageOffsetType = 'pageXOffset';\n this._scrollType = 'scrollLeft';\n this._translateDir = 'translateX';\n } else {\n this._childScrollDim = 'childHeight';\n this._invisiblePaddingProperty = 'height';\n this._marginDir = 'margin-top';\n this._offsetType = 'offsetTop';\n this._pageOffsetType = 'pageYOffset';\n this._scrollType = 'scrollTop';\n this._translateDir = 'translateY';\n }\n }\n\n protected debounce(func: Function, wait: number): Function {\n const throttled = this.throttleTrailing(func, wait);\n const result = function () {\n throttled['cancel']();\n throttled.apply(this, arguments);\n };\n result['cancel'] = function () {\n throttled['cancel']();\n };\n\n return result;\n }\n\n protected throttleTrailing(func: Function, wait: number): Function {\n let timeout = undefined;\n let _arguments = arguments;\n const result = function () {\n const _this = this;\n _arguments = arguments\n\n if (timeout) {\n return;\n }\n\n if (wait <= 0) {\n func.apply(_this, _arguments);\n } else {\n timeout = setTimeout(function () {\n timeout = undefined;\n func.apply(_this, _arguments);\n }, wait);\n }\n };\n result['cancel'] = function () {\n if (timeout) {\n clearTimeout(timeout);\n timeout = undefined;\n }\n };\n\n return result;\n }\n\n protected calculatedScrollbarWidth: number = 0;\n protected calculatedScrollbarHeight: number = 0;\n\n protected padding: number = 0;\n protected previousViewPort: IViewport = <any>{};\n protected currentTween: tween.Tween<any>;\n protected cachedItemsLength: number;\n\n protected disposeScrollHandler: () => void | undefined;\n protected disposeResizeHandler: () => void | undefined;\n\n protected refresh_internal(itemsArrayModified: boolean, refreshCompletedCallback: () => void = undefined, maxRunTimes: number = 2): void {\n //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).\n //The default of 2x max will probably be accurate enough without causing too large a performance bottleneck\n //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.\n //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.\n\n if (itemsArrayModified && this.previousViewPort && this.previousViewPort.scrollStartPosition > 0) {\n //if items were prepended, scroll forward to keep same items visible\n let oldViewPort = this.previousViewPort;\n let oldViewPortItems = this.viewPortItems;\n\n let oldRefreshCompletedCallback = refreshCompletedCallback;\n refreshCompletedCallback = () => {\n let scrollLengthDelta = this.previousViewPort.scrollLength - oldViewPort.scrollLength;\n if (scrollLengthDelta > 0 && this.viewPortItems) {\n let oldStartItem = oldViewPortItems[0];\n let oldStartItemIndex = this.items.findIndex(x => this.compareItems(oldStartItem, x));\n if (oldStartItemIndex > this.previousViewPort.startIndexWithBuffer) {\n let itemOrderChanged = false;\n for (let i = 1; i < this.viewPortItems.length; ++i) {\n if (!this.compareItems(this.items[oldStartItemIndex + i], oldViewPortItems[i])) {\n itemOrderChanged = true;\n break;\n }\n }\n\n if (!itemOrderChanged) {\n this.scrollToPosition(this.previousViewPort.scrollStartPosition + scrollLengthDelta, 0, oldRefreshCompletedCallback);\n return;\n }\n }\n }\n\n if (oldRefreshCompletedCallback) {\n oldRefreshCompletedCallback();\n }\n };\n }\n\n this.zone.runOutsideAngular(() => {\n requestAnimationFrame(() => {\n\n if (itemsArrayModified) {\n this.resetWrapGroupDimensions();\n }\n let viewport = this.calculateViewport();\n\n let startChanged = itemsArrayModified || viewport.startIndex !== this.previousViewPort.startIndex;\n let endChanged = itemsArrayModified || viewport.endIndex !== this.previousViewPort.endIndex;\n let scrollbarLengthChanged = viewport.scrollbarLength !== this.previousViewPort.scrollbarLength;\n let paddingChanged = viewport.padding !== this.previousViewPort.padding;\n let scrollPositionChanged = viewport.scrollStartPosition !== this.previousViewPort.scrollStartPosition || viewport.scrollEndPosition !== this.previousViewPort.scrollEndPosition || viewport.maxScrollPosition !== this.previousViewPort.maxScrollPosition;\n\n this.previousViewPort = viewport;\n\n if (scrollbarLengthChanged) {\n this.renderer.setStyle(this.invisiblePaddingElementRef.nativeElement, this._invisiblePaddingProperty, `${viewport.scrollLength}px`);\n }\n\n if (paddingChanged) {\n if (this.useMarginInsteadOfTranslate) {\n this.renderer.setStyle(this.contentElementRef.nativeElement, this._marginDir, `${viewport.padding}px`);\n } else {\n this.renderer.setStyle(this.contentElementRef.nativeElement, 'transform', `${this._translateDir}(${viewport.padding}px)`);\n this.renderer.setStyle(this.contentElementRef.nativeElement, 'webkitTransform', `${this._translateDir}(${viewport.padding}px)`);\n }\n }\n\n const changeEventArg: IPageInfo = (startChanged || endChanged) ? {\n startIndex: viewport.startIndex,\n endIndex: viewport.endIndex,\n scrollStartPosition: viewport.scrollStartPosition,\n scrollEndPosition: viewport.scrollEndPosition,\n startIndexWithBuffer: viewport.startIndexWithBuffer,\n endIndexWithBuffer: viewport.endIndexWithBuffer,\n maxScrollPosition: viewport.maxScrollPosition\n } : undefined;\n\n\n if (startChanged || endChanged || scrollPositionChanged) {\n const handleChanged = () => {\n // update the scroll list to trigger re-render of components in viewport\n this.viewPortItems = viewport.startIndexWithBuffer >= 0 && viewport.endIndexWithBuffer >= 0 ? this.items.slice(viewport.startIndexWithBuffer, viewport.endIndexWithBuffer + 1) : [];\n this.vsUpdate.emit(this.viewPortItems);\n\n if (startChanged) {\n this.vsStart.emit(changeEventArg);\n }\n\n if (endChanged) {\n this.vsEnd.emit(changeEventArg);\n }\n\n if (startChanged || endChanged) {\n this.changeDetectorRef.markForCheck();\n this.vsChange.emit(changeEventArg);\n }\n\n if (maxRunTimes > 0) {\n this.refresh_internal(false, refreshCompletedCallback, maxRunTimes - 1);\n return;\n }\n\n if (refreshCompletedCallback) {\n refreshCompletedCallback();\n }\n };\n\n\n if (this.executeRefreshOutsideAngularZone) {\n handleChanged();\n } else {\n this.zone.run(handleChanged);\n }\n } else {\n if (maxRunTimes > 0 && (scrollbarLengthChanged || paddingChanged)) {\n this.refresh_internal(false, refreshCompletedCallback, maxRunTimes - 1);\n return;\n }\n\n if (refreshCompletedCallback) {\n refreshCompletedCallback();\n }\n }\n });\n });\n }\n\n protected getScrollElement(): HTMLElement {\n return this.parentScroll instanceof Window ? (document.scrollingElement as HTMLElement) || document.documentElement || document.body : (this.parentScroll as HTMLElement) || this.element.nativeElement;\n }\n\n protected addScrollEventHandlers(): void {\n if (this.isAngularUniversalSSR) {\n return;\n }\n\n let scrollElement = this.getScrollElement();\n\n this.removeScrollEventHandlers();\n\n this.zone.runOutsideAngular(() => {\n if (this.parentScroll instanceof Window) {\n this.disposeScrollHandler = this.renderer.listen('window', 'scroll', this.onScroll);\n this.disposeResizeHandler = this.renderer.listen('window', 'resize', this.onScroll);\n } else {\n this.disposeScrollHandler = this.renderer.listen(scrollElement, 'scroll', this.onScroll);\n if (this._checkResizeInterval > 0) {\n this.checkScrollElementResizedTimer = <any>setInterval(() => {\n this.checkScrollElementResized();\n }, this._checkResizeInterval);\n }\n }\n });\n }\n\n protected removeScrollEventHandlers(): void {\n if (this.checkScrollElementResizedTimer) {\n clearInterval(this.checkScrollElementResizedTimer);\n }\n\n if (this.disposeScrollHandler) {\n this.disposeScrollHandler();\n this.disposeScrollHandler = undefined;\n }\n\n if (this.disposeResizeHandler) {\n this.disposeResizeHandler();\n this.disposeResizeHandler = undefined;\n }\n }\n\n protected getElementsOffset(): number {\n if (this.isAngularUniversalSSR) {\n return 0;\n }\n\n let offset = 0;\n\n if (this.containerElementRef && this.containerElementRef.nativeElement) {\n offset += this.containerElementRef.nativeElement[this._offsetType];\n }\n\n if (this.parentScroll) {\n let scrollElement = this.getScrollElement();\n let elementClientRect = this.getElementSize(this.element.nativeElement);\n let scrollClientRect = this.getElementSize(scrollElement);\n if (this.horizontal) {\n offset += elementClientRect.left - scrollClientRect.left;\n } else {\n offset += elementClientRect.top - scrollClientRect.top;\n }\n\n if (!(this.parentScroll instanceof Window)) {\n offset += scrollElement[this._scrollType];\n }\n }\n\n return offset;\n }\n\n protected countItemsPerWrapGroup(): number {\n if (this.isAngularUniversalSSR) {\n return Math.round(this.horizontal ? this.ssrViewportHeight / this.ssrChildHeight : this.ssrViewportWidth / this.ssrChildWidth);\n }\n\n let propertyName = this.horizontal ? 'offsetLeft' : 'offsetTop';\n let children = ((this.containerElementRef && this.containerElementRef.nativeElement) || this.contentElementRef.nativeElement).children;\n\n let childrenLength = children ? children.length : 0;\n if (childrenLength === 0) {\n return 1;\n }\n\n let firstOffset = children[0][propertyName];\n let result = 1;\n while (result < childrenLength && firstOffset === children[result][propertyName]) {\n ++result;\n }\n\n return result;\n }\n\n protected getScrollStartPosition(): number {\n let windowScrollValue = undefined;\n if (this.parentScroll instanceof Window) {\n windowScrollValue = window[this._pageOffsetType];\n }\n\n return windowScrollValue || this.getScrollElement()[this._scrollType] || 0;\n }\n\n protected minMeasuredChildWidth: number;\n protected minMeasuredChildHeight: number;\n\n protected wrapGroupDimensions: WrapGroupDimensions;\n\n protected resetWrapGroupDimensions(): void {\n const oldWrapGroupDimensions = this.wrapGroupDimensions;\n this.invalidateAllCachedMeasurements();\n\n if (!this.enableUnequalChildrenSizes || !oldWrapGroupDimensions || oldWrapGroupDimensions.numberOfKnownWrapGroupChildSizes === 0) {\n return;\n }\n\n const itemsPerWrapGroup: number = this.countItemsPerWrapGroup();\n for (let wrapGroupIndex = 0; wrapGroupIndex < oldWrapGroupDimensions.maxChildSizePerWrapGroup.length; ++wrapGroupIndex) {\n const oldWrapGroupDimension: WrapGroupDimension = oldWrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex];\n if (!oldWrapGroupDimension || !oldWrapGroupDimension.items || !oldWrapGroupDimension.items.length) {\n continue;\n }\n\n if (oldWrapGroupDimension.items.length !== itemsPerWrapGroup) {\n return;\n }\n\n let itemsChanged = false;\n let arrayStartIndex = itemsPerWrapGroup * wrapGroupIndex;\n for (let i = 0; i < itemsPerWrapGroup; ++i) {\n if (!this.compareItems(oldWrapGroupDimension.items[i], this.items[arrayStartIndex + i])) {\n itemsChanged = true;\n break;\n }\n }\n\n if (!itemsChanged) {\n ++this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths += oldWrapGroupDimension.childWidth || 0;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights += oldWrapGroupDimension.childHeight || 0;\n this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex] = oldWrapGroupDimension;\n }\n }\n }\n\n protected calculateDimensions(): IDimensions {\n let scrollElement = this.getScrollElement();\n\n const maxCalculatedScrollBarSize: number = 25; // Note: Formula to auto-calculate doesn't work for ParentScroll, so we default to this if not set by consuming application\n this.calculatedScrollbarHeight = Math.max(Math.min(scrollElement.offsetHeight - scrollElement.clientHeight, maxCalculatedScrollBarSize), this.calculatedScrollbarHeight);\n this.calculatedScrollbarWidth = Math.max(Math.min(scrollElement.offsetWidth - scrollElement.clientWidth, maxCalculatedScrollBarSize), this.calculatedScrollbarWidth);\n\n let viewportWidth = scrollElement.offsetWidth - (this.scrollbarWidth || this.calculatedScrollbarWidth || (this.horizontal ? 0 : maxCalculatedScrollBarSize));\n let viewportHeight = scrollElement.offsetHeight - (this.scrollbarHeight || this.calculatedScrollbarHeight || (this.horizontal ? maxCalculatedScrollBarSize : 0));\n\n let content = (this.containerElementRef && this.containerElementRef.nativeElement) || this.contentElementRef.nativeElement;\n\n let itemsPerWrapGroup = this.countItemsPerWrapGroup();\n let wrapGroupsPerPage;\n\n let defaultChildWidth;\n let defaultChildHeight;\n\n if (this.isAngularUniversalSSR) {\n viewportWidth = this.ssrViewportWidth;\n viewportHeight = this.ssrViewportHeight;\n defaultChildWidth = this.ssrChildWidth;\n defaultChildHeight = this.ssrChildHeight;\n let itemsPerRow = Math.max(Math.ceil(viewportWidth / defaultChildWidth), 1);\n let itemsPerCol = Math.max(Math.ceil(viewportHeight / defaultChildHeight), 1);\n wrapGroupsPerPage = this.horizontal ? itemsPerRow : itemsPerCol;\n } else if (!this.enableUnequalChildrenSizes) {\n if (content.children.length > 0) {\n if (!this.childWidth || !this.childHeight) {\n if (!this.minMeasuredChildWidth && viewportWidth > 0) {\n this.minMeasuredChildWidth = viewportWidth;\n }\n if (!this.minMeasuredChildHeight && viewportHeight > 0) {\n this.minMeasuredChildHeight = viewportHeight;\n }\n }\n\n let child = content.children[0];\n let clientRect = this.getElementSize(child);\n this.minMeasuredChildWidth = Math.min(this.minMeasuredChildWidth, clientRect.width);\n this.minMeasuredChildHeight = Math.min(this.minMeasuredChildHeight, clientRect.height);\n }\n\n defaultChildWidth = this.childWidth || this.minMeasuredChildWidth || viewportWidth;\n defaultChildHeight = this.childHeight || this.minMeasuredChildHeight || viewportHeight;\n let itemsPerRow = Math.max(Math.ceil(viewportWidth / defaultChildWidth), 1);\n let itemsPerCol = Math.max(Math.ceil(viewportHeight / defaultChildHeight), 1);\n wrapGroupsPerPage = this.horizontal ? itemsPerRow : itemsPerCol;\n } else {\n let scrollOffset = scrollElement[this._scrollType] - (this.previousViewPort ? this.previousViewPort.padding : 0);\n\n let arrayStartIndex = this.previousViewPort.startIndexWithBuffer || 0;\n let wrapGroupIndex = Math.ceil(arrayStartIndex / itemsPerWrapGroup);\n\n let maxWidthForWrapGroup = 0;\n let maxHeightForWrapGroup = 0;\n let sumOfVisibleMaxWidths = 0;\n let sumOfVisibleMaxHeights = 0;\n wrapGroupsPerPage = 0;\n\n for (let i = 0; i < content.children.length; ++i) {\n ++arrayStartIndex;\n let child = content.children[i];\n let clientRect = this.getElementSize(child);\n\n maxWidthForWrapGroup = Math.max(maxWidthForWrapGroup, clientRect.width);\n maxHeightForWrapGroup = Math.max(maxHeightForWrapGroup, clientRect.height);\n\n if (arrayStartIndex % itemsPerWrapGroup === 0) {\n let oldValue = this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex];\n if (oldValue) {\n --this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths -= oldValue.childWidth || 0;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights -= oldValue.childHeight || 0;\n }\n\n ++this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n const items = this.items.slice(arrayStartIndex - itemsPerWrapGroup, arrayStartIndex);\n this.wrapGroupDimensions.maxChildSizePerWrapGroup[wrapGroupIndex] = {\n childWidth: maxWidthForWrapGroup,\n childHeight: maxHeightForWrapGroup,\n items: items\n };\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths += maxWidthForWrapGroup;\n this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights += maxHeightForWrapGroup;\n\n if (this.horizontal) {\n let maxVisibleWidthForWrapGroup = Math.min(maxWidthForWrapGroup, Math.max(viewportWidth - sumOfVisibleMaxWidths, 0));\n if (scrollOffset > 0) {\n let scrollOffsetToRemove = Math.min(scrollOffset, maxVisibleWidthForWrapGroup);\n maxVisibleWidthForWrapGroup -= scrollOffsetToRemove;\n scrollOffset -= scrollOffsetToRemove;\n }\n\n sumOfVisibleMaxWidths += maxVisibleWidthForWrapGroup;\n if (maxVisibleWidthForWrapGroup > 0 && viewportWidth >= sumOfVisibleMaxWidths) {\n ++wrapGroupsPerPage;\n }\n } else {\n let maxVisibleHeightForWrapGroup = Math.min(maxHeightForWrapGroup, Math.max(viewportHeight - sumOfVisibleMaxHeights, 0));\n if (scrollOffset > 0) {\n let scrollOffsetToRemove = Math.min(scrollOffset, maxVisibleHeightForWrapGroup);\n maxVisibleHeightForWrapGroup -= scrollOffsetToRemove;\n scrollOffset -= scrollOffsetToRemove;\n }\n\n sumOfVisibleMaxHeights += maxVisibleHeightForWrapGroup;\n if (maxVisibleHeightForWrapGroup > 0 && viewportHeight >= sumOfVisibleMaxHeights) {\n ++wrapGroupsPerPage;\n }\n }\n\n ++wrapGroupIndex;\n\n maxWidthForWrapGroup = 0;\n maxHeightForWrapGroup = 0;\n }\n }\n\n let averageChildWidth = this.wrapGroupDimensions.sumOfKnownWrapGroupChildWidths / this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n let averageChildHeight = this.wrapGroupDimensions.sumOfKnownWrapGroupChildHeights / this.wrapGroupDimensions.numberOfKnownWrapGroupChildSizes;\n defaultChildWidth = this.childWidth || averageChildWidth || viewportWidth;\n defaultChildHeight = this.childHeight || averageChildHeight || viewportHeight;\n\n if (this.horizontal) {\n if (viewportWidth > sumOfVisibleMaxWidths) {\n wrapGroupsPerPage += Math.ceil((viewportWidth - sumOfVisibleMaxWidths) / defaultChildWidth);\n }\n } else {\n if (viewportHeight > sumOfVisibleMaxHeights) {\n wrapGroupsPerPage += Math.ceil((viewportHeight - sumOfVisibleMaxHeights) / defaultChildHeight);\n }\n }\n }\n\n let itemCount = this.items.length;\n let itemsPerPage = itemsPerWrapGroup * wrapGroupsPerPage;\n let pageCount_fractional = itemCount / itemsPerPage;\n let numberOfWrapGroups = Math.ceil(itemCount / itemsPerWrapGroup);\n\n let scrollLength = 0;\n\n let defaultScrollLengthPerWrapGroup = this.horizontal ? defaultChildWidth : defaultChildHeight;\n if (this.enableUnequalChildrenSizes) {\n let numUnknownChildSizes = 0;\n for (let i = 0; i < numberOfWrapGroups; ++i) {\n let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];\n if (childSize) {\n scrollLength += childSize;\n } else {\n ++numUnknownChildSizes;\n }\n }\n\n scrollLength += Math.round(numUnknownChildSizes * defaultScrollLengthPerWrapGroup);\n } else {\n scrollLength = numberOfWrapGroups * defaultScrollLengthPerWrapGroup;\n }\n\n let viewportLength = this.horizontal ? viewportWidth : viewportHeight;\n let maxScrollPosition = Math.max(scrollLength - viewportLength, 0);\n\n return {\n childHeight: defaultChildHeight,\n childWidth: defaultChildWidth,\n itemCount: itemCount,\n itemsPerPage: itemsPerPage,\n itemsPerWrapGroup: itemsPerWrapGroup,\n maxScrollPosition: maxScrollPosition,\n pageCount_fractional: pageCount_fractional,\n scrollLength: scrollLength,\n viewportLength: viewportLength,\n wrapGroupsPerPage: wrapGroupsPerPage,\n };\n }\n\n protected cachedPageSize: number = 0;\n protected previousScrollNumberElements: number = 0;\n\n protected calculatePadding(arrayStartIndexWithBuffer: number, dimensions: IDimensions): number {\n if (dimensions.itemCount === 0) {\n return 0;\n }\n\n let defaultScrollLengthPerWrapGroup = dimensions[this._childScrollDim];\n let startingWrapGroupIndex = Math.floor(arrayStartIndexWithBuffer / dimensions.itemsPerWrapGroup) || 0;\n\n if (!this.enableUnequalChildrenSizes) {\n return defaultScrollLengthPerWrapGroup * startingWrapGroupIndex;\n }\n\n let numUnknownChildSizes = 0;\n let result = 0;\n for (let i = 0; i < startingWrapGroupIndex; ++i) {\n let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];\n if (childSize) {\n result += childSize;\n } else {\n ++numUnknownChildSizes;\n }\n }\n result += Math.round(numUnknownChildSizes * defaultScrollLengthPerWrapGroup);\n\n return result;\n }\n\n protected calculatePageInfo(scrollPosition: number, dimensions: IDimensions): IPageInfo {\n let scrollPercentage = 0;\n let scrollBottomPercentage = 1;\n let arrayStartIndex = 0;\n let arrayEndIndex = 0;\n if (this.enableUnequalChildrenSizes) {\n const numberOfWrapGroups = Math.ceil(dimensions.itemCount / dimensions.itemsPerWrapGroup);\n let totalScrolledLength = 0;\n let defaultScrollLengthPerWrapGroup = dimensions[this._childScrollDim];\n let i = 0;\n for (; i < numberOfWrapGroups; ++i) {\n let childSize = this.wrapGroupDimensions.maxChildSizePerWrapGroup[i] && this.wrapGroupDimensions.maxChildSizePerWrapGroup[i][this._childScrollDim];\n if (childSize) {\n totalScrolledLength += childSize;\n } else {\n totalScrolledLength += defaultScrollLengthPerWrapGroup;\n }\n\n if (scrollPosition < totalScrolledLength) {\n scrollPercentage = i / numberOfWrapGroups;\n break;\n }\n }\n let j = i + 1;\n for (; j < numberOfWrapGroups; ++j) {\n let childSize = this.wrapGroup