Created
June 3, 2019 05:03
-
-
Save sarfarazansari/566bb86bd31566e4bf830c03c0d32546 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Component, | |
ContentChild, | |
ElementRef, | |
EventEmitter, | |
Inject, | |
Optional, | |
Input, | |
NgModule, | |
NgZone, | |
OnChanges, | |
OnDestroy, | |
OnInit, | |
Output, | |
Renderer2, | |
ViewChild, | |
ChangeDetectorRef | |
} from '@angular/core'; | |
import { PLATFORM_ID } from '@angular/core'; | |
import { isPlatformServer } from '@angular/common'; | |
import { CommonModule } from '@angular/common'; | |
import * as tween from '@tweenjs/tween.js'; | |
export interface VirtualScrollerDefaultOptions { | |
scrollThrottlingTime: number; | |
scrollDebounceTime: number; | |
scrollAnimationTime: number; | |
scrollbarWidth?: number; | |
scrollbarHeight?: number; | |
checkResizeInterval: number; | |
resizeBypassRefreshThreshold: number; | |
modifyOverflowStyleOfParentScroll: boolean; | |
stripedTable: boolean; | |
} | |
export function VIRTUAL_SCROLLER_DEFAULT_OPTIONS_FACTORY(): VirtualScrollerDefaultOptions { | |
return { | |
scrollThrottlingTime: 0, | |
scrollDebounceTime: 0, | |
scrollAnimationTime: 750, | |
checkResizeInterval: 1000, | |
resizeBypassRefreshThreshold: 5, | |
modifyOverflowStyleOfParentScroll: true, | |
stripedTable: false | |
}; | |
} | |
export interface WrapGroupDimensions { | |
numberOfKnownWrapGroupChildSizes: number; | |
sumOfKnownWrapGroupChildWidths: number; | |
sumOfKnownWrapGroupChildHeights: number; | |
maxChildSizePerWrapGroup: WrapGroupDimension[]; | |
} | |
export interface WrapGroupDimension { | |
childWidth: number; | |
childHeight: number; | |
items: any[]; | |
} | |
export interface IDimensions { | |
itemCount: number; | |
itemsPerWrapGroup: number; | |
wrapGroupsPerPage: number; | |
itemsPerPage: number; | |
pageCount_fractional: number; | |
childWidth: number; | |
childHeight: number; | |
scrollLength: number; | |
viewportLength: number; | |
maxScrollPosition: number; | |
} | |
export interface IPageInfo { | |
startIndex: number; | |
endIndex: number; | |
scrollStartPosition: number; | |
scrollEndPosition: number; | |
startIndexWithBuffer: number; | |
endIndexWithBuffer: number; | |
maxScrollPosition: number; | |
} | |
export interface IViewport extends IPageInfo { | |
padding: number; | |
scrollLength: number; | |
} | |
@Component({ | |
selector: 'kiss-virtual-scroll', | |
template: ` | |
<div class="total-padding" #invisiblePadding></div> | |
<div class="scrollable-content" #content> | |
<ng-content></ng-content> | |
</div> | |
`, | |
// tslint:disable-next-line: use-host-property-decorator | |
host: { | |
'[class.horizontal]': 'horizontal', | |
'[class.vertical]': '!horizontal', | |
'[class.selfScroll]': '!parentScroll' | |
}, | |
styles: [` | |
:host { | |
position: relative; | |
display: block; | |
-webkit-overflow-scrolling: touch; | |
} | |
:host.horizontal.selfScroll { | |
overflow-y: visible; | |
overflow-x: auto; | |
} | |
: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; | |
} | |
.total-padding { | |
width: 1px; | |
opacity: 0; | |
} | |
:host.horizontal .total-padding { | |
height: 100%; | |
} | |
`] | |
}) | |
export class KissVirtualScrollerComponent implements OnInit, OnChanges, OnDestroy { | |
public get viewPortInfo(): IPageInfo { | |
let pageInfo: IViewport = this.previousViewPort || <any>{}; | |
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 | |
}; | |
} | |
@Input() | |
public get enableUnequalChildrenSizes(): boolean { | |
return this._enableUnequalChildrenSizes; | |
} | |
public set enableUnequalChildrenSizes(value: boolean) { | |
if (this._enableUnequalChildrenSizes === value) { | |
return; | |
} | |
this._enableUnequalChildrenSizes = value; | |
this.minMeasuredChildWidth = undefined; | |
this.minMeasuredChildHeight = undefined; | |
} | |
@Input() | |
public get bufferAmount(): number { | |
if (typeof (this._bufferAmount) === 'number' && this._bufferAmount >= 0) { | |
return this._bufferAmount; | |
} else { | |
return this.enableUnequalChildrenSizes ? 5 : 0; | |
} | |
} | |
public set bufferAmount(value: number) { | |
this._bufferAmount = value; | |
} | |
@Input() | |
public get scrollThrottlingTime(): number { | |
return this._scrollThrottlingTime; | |
} | |
public set scrollThrottlingTime(value: number) { | |
this._scrollThrottlingTime = value; | |
this.updateOnScrollFunction(); | |
} | |
@Input() | |
public get scrollDebounceTime(): number { | |
return this._scrollDebounceTime; | |
} | |
public set scrollDebounceTime(value: number) { | |
this._scrollDebounceTime = value; | |
this.updateOnScrollFunction(); | |
} | |
@Input() | |
public get checkResizeInterval(): number { | |
return this._checkResizeInterval; | |
} | |
public set checkResizeInterval(value: number) { | |
if (this._checkResizeInterval === value) { | |
return; | |
} | |
this._checkResizeInterval = value; | |
this.addScrollEventHandlers(); | |
} | |
@Input() | |
public get items(): any[] { | |
return this._items; | |
} | |
public set items(value: any[]) { | |
if (value === this._items) { | |
return; | |
} | |
this._items = value || []; | |
this.refresh_internal(true); | |
} | |
@Input() | |
public get horizontal(): boolean { | |
return this._horizontal; | |
} | |
public set horizontal(value: boolean) { | |
this._horizontal = value; | |
this.updateDirection(); | |
} | |
@Input() | |
public get parentScroll(): Element | Window { | |
return this._parentScroll; | |
} | |
public set parentScroll(value: Element | Window) { | |
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'; | |
} | |
} | |
constructor(protected readonly element: ElementRef, | |
protected readonly renderer: Renderer2, | |
protected readonly zone: NgZone, | |
protected changeDetectorRef: ChangeDetectorRef, | |
@Inject(PLATFORM_ID) platformId: Object, | |
@Optional() @Inject('virtual-scroller-default-options') | |
options: VirtualScrollerDefaultOptions) { | |
this.isAngularUniversalSSR = isPlatformServer(platformId); | |
this.scrollThrottlingTime = options.scrollThrottlingTime; | |
this.scrollDebounceTime = options.scrollDebounceTime; | |
this.scrollAnimationTime = options.scrollAnimationTime; | |
this.scrollbarWidth = options.scrollbarWidth; | |
this.scrollbarHeight = options.scrollbarHeight; | |
this.checkResizeInterval = options.checkResizeInterval; | |
this.resizeBypassRefreshThreshold = options.resizeBypassRefreshThreshold; | |
this.modifyOverflowStyleOfParentScroll = options.modifyOverflowStyleOfParentScroll; | |
this.stripedTable = options.stripedTable; | |
this.horizontal = false; | |
this.resetWrapGroupDimensions(); | |
} | |
public viewPortItems: any[]; | |
public window = window; | |
@Input() | |
public executeRefreshOutsideAngularZone: boolean = false; | |
protected _enableUnequalChildrenSizes: boolean = false; | |
@Input() | |
public useMarginInsteadOfTranslate: boolean = false; | |
@Input() | |
public modifyOverflowStyleOfParentScroll: boolean; | |
@Input() | |
public stripedTable: boolean; | |
@Input() | |
public scrollbarWidth: number; | |
@Input() | |
public scrollbarHeight: number; | |
@Input() | |
public childWidth: number; | |
@Input() | |
public childHeight: number; | |
@Input() | |
public ssrChildWidth: number; | |
@Input() | |
public ssrChildHeight: number; | |
@Input() | |
public ssrViewportWidth: number = 1920; | |
@Input() | |
public ssrViewportHeight: number = 1080; | |
protected _bufferAmount: number = 0; | |
@Input() | |
public scrollAnimationTime: number; | |
@Input() | |
public resizeBypassRefreshThreshold: number; | |
protected _scrollThrottlingTime: number; | |
protected _scrollDebounceTime: number; | |
protected onScroll: () => void; | |
// tslint:disable-next-line: member-ordering | |
protected checkScrollElementResizedTimer: number; | |
// tslint:disable-next-line: member-ordering | |
protected _checkResizeInterval: number; | |
// tslint:disable-next-line: member-ordering | |
protected _items: any[] = []; | |
// tslint:disable-next-line: member-ordering | |
protected _horizontal: boolean; | |
// tslint:disable-next-line: member-ordering | |
protected oldParentScrollOverflow: { x: string, y: string }; | |
// tslint:disable-next-line: member-ordering | |
protected _parentScroll: Element | Window; | |
// tslint:disable-next-line: member-ordering | |
@Output() | |
public vsUpdate: EventEmitter<any[]> = new EventEmitter<any[]>(); | |
// tslint:disable-next-line: member-ordering | |
@Output() | |
public vsChange: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>(); | |
// tslint:disable-next-line: member-ordering | |
@Output() | |
public vsStart: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>(); | |
// tslint:disable-next-line: member-ordering | |
@Output() | |
public vsEnd: EventEmitter<IPageInfo> = new EventEmitter<IPageInfo>(); | |
// tslint:disable-next-line: member-ordering | |
@ViewChild('content', { read: ElementRef }) | |
protected contentElementRef: ElementRef; | |
// tslint:disable-next-line: member-ordering | |
@ViewChild('invisiblePadding', { read: ElementRef }) | |
protected invisiblePaddingElementRef: ElementRef; | |
// tslint:disable-next-line: member-ordering | |
@ContentChild('header', { read: ElementRef }) | |
protected headerElementRef: ElementRef; | |
// tslint:disable-next-line: member-ordering | |
@ContentChild('container', { read: ElementRef }) | |
protected containerElementRef: ElementRef; | |
protected isAngularUniversalSSR: boolean; | |
protected previousScrollBoundingRect: ClientRect; | |
protected _invisiblePaddingProperty; | |
protected _offsetType; | |
protected _scrollType; | |
protected _pageOffsetType; | |
protected _childScrollDim; | |
protected _translateDir; | |
protected _marginDir; | |
protected calculatedScrollbarWidth: number = 0; | |
protected calculatedScrollbarHeight: number = 0; | |
protected padding: number = 0; | |
protected previousViewPort: IViewport = <any>{}; | |
protected currentTween: tween.Tween; | |
protected cachedItemsLength: number; | |
protected disposeScrollHandler: () => void | undefined; | |
protected disposeResizeHandler: () => void | undefined; | |
protected minMeasuredChildWidth: number; | |
protected minMeasuredChildHeight: number; | |
protected wrapGroupDimensions: WrapGroupDimensions; | |
// tslint:disable-next-line: member-ordering | |
protected cachedPageSize: number = 0; | |
protected previousScrollNumberElements: number = 0; | |
protected updateOnScrollFunction(): void { | |
if (this.scrollDebounceTime) { | |
this.onScroll = <any>this.debounce(() => { | |
this.refresh_internal(false); | |
}, this.scrollDebounceTime); | |
} else if (this.scrollThrottlingTime) { | |
this.onScroll = <any>this.throttleTrailing(() => { | |
this.refresh_internal(false); | |
}, this.scrollThrottlingTime); | |
} else { | |
this.onScroll = () => { | |
this.refresh_internal(false); | |
}; | |
} | |
} | |
@Input() | |
public compareItems: (item1: any, item2: any) => boolean = (item1: any, item2: any) => item1 === item2 | |
protected revertParentOverscroll(): void { | |
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; | |
} | |
public ngOnInit(): void { | |
this.addScrollEventHandlers(); | |
} | |
public ngOnDestroy(): void { | |
this.removeScrollEventHandlers(); | |
this.revertParentOverscroll(); | |
} | |
public ngOnChanges(changes: any): void { | |
let indexLengthChanged = this.cachedItemsLength !== this.items.length; | |
this.cachedItemsLength = this.items.length; | |
const firstRun: boolean = !changes.items || !changes.items.previousValue || changes.items.previousValue.length === 0; | |
this.refresh_internal(indexLengthChanged || firstRun); | |
} | |
// tslint:disable-next-line: use-life-cycle-interface | |
public ngDoCheck(): void { | |
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); | |
} | |
} | |
} | |
public refresh(): void { | |
this.refresh_internal(true); | |
} | |
public invalidateAllCachedMeasurements(): void { | |
this.wrapGroupDimensions = { | |
maxChildSizePerWrapGroup: [], | |
numberOfKnownWrapGroupChildSizes: 0, | |
sumOfKnownWrapGroupChildWidths: 0, | |
sumOfKnownWrapGroupChildHeights: 0 | |
}; | |
this.minMeasuredChildWidth = undefined; | |
this.minMeasuredChildHeight = undefined; | |
this.refresh_internal(false); | |
} | |
public invalidateCachedMeasurementForItem(item: any): void { | |
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); | |
} | |
public invalidateCachedMeasurementAtIndex(index: number): void { | |
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); | |
} | |
// tslint:disable-next-line: no-unnecessary-initializer | |
public scrollInto(item: any, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void { | |
let index: number = this.items.indexOf(item); | |
if (index === -1) { | |
return; | |
} | |
this.scrollToIndex(index, alignToBeginning, additionalOffset, animationMilliseconds, animationCompletedCallback); | |
} | |
// tslint:disable-next-line: no-unnecessary-initializer | |
public scrollToIndex(index: number, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void { | |
let maxRetries: number = 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); | |
} | |
// tslint:disable-next-line: no-unnecessary-initializer | |
protected scrollToIndex_internal(index: number, alignToBeginning: boolean = true, additionalOffset: number = 0, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void { | |
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); | |
} | |
// tslint:disable-next-line: no-unnecessary-initializer | |
public scrollToPosition(scrollPosition: number, animationMilliseconds: number = undefined, animationCompletedCallback: () => void = undefined): void { | |
scrollPosition += this.getElementsOffset(); | |
animationMilliseconds = animationMilliseconds === undefined ? this.scrollAnimationTime : animationMilliseconds; | |
let scrollElement = this.getScrollElement(); | |
let animationRequest: number; | |
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?: number) => { | |
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; | |
} | |
protected getElementSize(element: HTMLElement): ClientRect { | |
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 | |
}; | |
} | |
protected checkScrollElementResized(): void { | |
let boundingRect = this.getElementSize(this.getScrollElement()); | |
let sizeChanged: boolean; | |
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); | |
} | |
} | |
} | |
protected updateDirection(): void { | |
if (this.horizontal) { | |
this._invisiblePaddingProperty = 'width'; | |
this._offsetType = 'offsetLeft'; | |
this._pageOffsetType = 'pageXOffset'; | |
this._childScrollDim = 'childWidth'; | |
this._marginDir = 'margin-left'; | |
this._translateDir = 'translateX'; | |
this._scrollType = 'scrollLeft'; | |
} else { | |
this._invisiblePaddingProperty = 'height'; | |
this._offsetType = 'offsetTop'; | |
this._pageOffsetType = 'pageYOffset'; | |
this._childScrollDim = 'childHeight'; | |
this._marginDir = 'margin-top'; | |
this._translateDir = 'translateY'; | |
this._scrollType = 'scrollTop'; | |
} | |
} | |
protected debounce(func: Function, wait: number): Function { | |
const throttled = this.throttleTrailing(func, wait); | |
const result = function () { | |
throttled['cancel'](); | |
throttled.apply(this, arguments); | |
}; | |
result['cancel'] = function () { | |
throttled['cancel'](); | |
}; | |
return result; | |
} | |
protected throttleTrailing(func: Function, wait: number): Function { | |
let timeout; | |
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; | |
} | |
// tslint:disable-next-line: no-unnecessary-initializer | |
protected refresh_internal(itemsArrayModified: boolean, refreshCompletedCallback: () => void = undefined, maxRunTimes: number = 2): void { | |
if (itemsArrayModified) { | |
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 scrollLengthChanged = viewport.scrollLength !== this.previousViewPort.scrollLength; | |
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 (scrollLengthChanged) { | |
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: IPageInfo = (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 && (scrollLengthChanged || paddingChanged)) { | |
this.refresh_internal(false, refreshCompletedCallback, maxRunTimes - 1); | |
return; | |
} | |
if (refreshCompletedCallback) { | |
refreshCompletedCallback(); | |
} | |
} | |
}); | |
}); | |
} | |
protected getScrollElement(): HTMLElement { | |
return this.parentScroll instanceof Window ? document.scrollingElement || document.documentElement || document.body : this.parentScroll || this.element.nativeElement; | |
} | |
protected addScrollEventHandlers(): void { | |
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 = <any>setInterval(() => { this.checkScrollElementResized(); }, this._checkResizeInterval); | |
} | |
} | |
}); | |
} | |
protected removeScrollEventHandlers(): void { | |
if (this.checkScrollElementResizedTimer) { | |
clearInterval(this.checkScrollElementResizedTimer); | |
} | |
if (this.disposeScrollHandler) { | |
this.disposeScrollHandler(); | |
this.disposeScrollHandler = undefined; | |
} | |
if (this.disposeResizeHandler) { | |
this.disposeResizeHandler(); | |
this.disposeResizeHandler = undefined; | |
} | |
} | |
protected getElementsOffset(): number { | |
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; | |
} | |
protected countItemsPerWrapGroup(): number { | |
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; | |
} | |
protected getScrollStartPosition(): number { | |
let windowScrollValue; | |
if (this.parentScroll instanceof Window) { | |
windowScrollValue = window[this._pageOffsetType]; | |
} | |
return windowScrollValue || this.getScrollElement()[this._scrollType] || 0; | |
} | |
protected resetWrapGroupDimensions(): void { | |
const oldWrapGroupDimensions = this.wrapGroupDimensions; | |
this.invalidateAllCachedMeasurements(); | |
if (!this.enableUnequalChildrenSizes || !oldWrapGroupDimensions || oldWrapGroupDimensions.numberOfKnownWrapGroupChildSizes === 0) { | |
return; | |
} | |
const itemsPerWrapGroup: number = this.countItemsPerWrapGroup(); | |
for (let wrapGroupIndex = 0; wrapGroupIndex < oldWrapGroupDimensions.maxChildSizePerWrapGroup.length; ++wrapGroupIndex) { | |
const oldWrapGroupDimension: WrapGroupDimension = 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; | |
} | |
} | |
} | |
protected calculateDimensions(): IDimensions { | |
let scrollElement = this.getScrollElement(); | |
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 | |
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; | |
// tslint:disable-next-line: variable-name | |
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 { | |
itemCount: itemCount, | |
itemsPerWrapGroup: itemsPerWrapGroup, | |
wrapGroupsPerPage: wrapGroupsPerPage, | |
itemsPerPage: itemsPerPage, | |
pageCount_fractional: pageCount_fractional, | |
childWidth: defaultChildWidth, | |
childHeight: defaultChildHeight, | |
scrollLength: scrollLength, | |
viewportLength: viewportLength, | |
maxScrollPosition: maxScrollPosition | |
}; | |
} | |
protected calculatePadding(arrayStartIndexWithBuffer: number, dimensions: IDimensions): number { | |
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; | |
} | |
protected calculatePageInfo(scrollPosition: number, dimensions: IDimensions): IPageInfo { | |
let scrollPercentage = 0; | |
if (this.enableUnequalChildrenSizes) { | |
const numberOfWrapGroups = Math.ceil(dimensions.itemCount / dimensions.itemsPerWrapGroup); | |
let totalScrolledLength = 0; | |
let defaultScrollLengthPerWrapGroup = dimensions[this._childScrollDim]; | |
for (let i = 0; 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; | |
} | |
} | |
} else { | |
scrollPercentage = scrollPosition / dimensions.scrollLength; | |
} | |
// tslint:disable-next-line: variable-name | |
let startingArrayIndex_fractional = Math.min(Math.max(scrollPercentage * dimensions.pageCount_fractional, 0), dimensions.pageCount_fractional) * dimensions.itemsPerPage; | |
let maxStart = dimensions.itemCount - dimensions.itemsPerPage - 1; | |
let arrayStartIndex = Math.min(Math.floor(startingArrayIndex_fractional), maxStart); | |
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 arrayEndIndex = Math.ceil(startingArrayIndex_fractional) + dimensions.itemsPerPage - 1; | |
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 | |
}; | |
} | |
protected calculateViewport(): IViewport { | |
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 = dimensions.scrollLength; | |
return { | |
startIndex: pageInfo.startIndex, | |
endIndex: pageInfo.endIndex, | |
startIndexWithBuffer: pageInfo.startIndexWithBuffer, | |
endIndexWithBuffer: pageInfo.endIndexWithBuffer, | |
padding: Math.round(newPadding), | |
scrollLength: Math.round(newScrollLength), | |
scrollStartPosition: pageInfo.scrollStartPosition, | |
scrollEndPosition: pageInfo.scrollEndPosition, | |
maxScrollPosition: pageInfo.maxScrollPosition | |
}; | |
} | |
} | |
@NgModule({ | |
exports: [KissVirtualScrollerComponent], | |
declarations: [KissVirtualScrollerComponent], | |
imports: [CommonModule], | |
providers: [ | |
{ | |
provide: 'virtual-scroller-default-options', | |
useFactory: VIRTUAL_SCROLLER_DEFAULT_OPTIONS_FACTORY | |
} | |
] | |
}) | |
export class KissVirtualScrollerModule { } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment