igniteui-angular
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
1,318 lines • 214 kB
JavaScript
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, ComponentFactoryResolver, Directive, EventEmitter, Input, IterableDiffers, NgModule, NgZone, Output, TemplateRef, ViewContainerRef } from '@angular/core';
import { DisplayContainerComponent } from './display.container';
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
import { VirtualHelperComponent } from './virtual.helper.component';
import { IgxScrollInertiaModule } from './../scroll-inertia/scroll_inertia.directive';
import { IgxForOfSyncService } from './for_of.sync.service';
/**
* @template T
*/
export class IgxForOfDirective {
/**
* @param {?} _viewContainer
* @param {?} _template
* @param {?} _differs
* @param {?} resolver
* @param {?} cdr
* @param {?} _zone
*/
constructor(_viewContainer, _template, _differs, resolver, cdr, _zone) {
this._viewContainer = _viewContainer;
this._template = _template;
this._differs = _differs;
this.resolver = resolver;
this.cdr = cdr;
this._zone = _zone;
/**
* The current state of the directive. It contains `startIndex` and `chunkSize`.
* state.startIndex - The index of the item at which the current visible chunk begins.
* state.chunkSize - The number of items the current visible chunk holds.
* These options can be used when implementing remote virtualization as they provide the necessary state information.
* ```typescript
* const gridState = this.parentVirtDir.state;
* ```
*/
this.state = {
startIndex: 0,
chunkSize: 0
};
/**
* The total count of the virtual data items, when using remote service.
* ```typescript
* this.parentVirtDir.totalItemCount = data.Count;
* ```
*/
this.totalItemCount = null;
/**
* An event that is emitted after a new chunk has been loaded.
* ```html
* <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (onChunkLoad)="chunkLoad($event)"></ng-template>
* ```
* ```typescript
* chunkLoad(e){
* alert("chunk loaded!");
* }
* ```
*/
this.onChunkLoad = new EventEmitter();
/**
* An event that is emitted after data has been changed.
* ```html
* <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (onDataChanged)="dataChanged($event)"></ng-template>
* ```
* ```typescript
* dataChanged(e){
* alert("data changed!");
* }
* ```
*/
this.onDataChanged = new EventEmitter();
this.onBeforeViewDestroyed = new EventEmitter();
/**
* An event that is emitted on chunk loading to emit the current state information - startIndex, endIndex, totalCount.
* Can be used for implementing remote load on demand for the igxFor data.
* ```html
* <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (onChunkPreload)="chunkPreload($event)"></ng-template>
* ```
* ```typescript
* chunkPreload(e){
* alert("chunk is loading!");
* }
* ```
*/
this.onChunkPreload = new EventEmitter();
this._sizesCache = [];
this._differ = null;
this.heightCache = [];
this.MAX_PERF_SCROLL_DIFF = 4;
/**
* Height that is being virtualized.
*/
this._virtHeight = 0;
/**
* Ratio for height that's being virtualizaed and the one visible
* If _virtHeightRatio = 1, the visible height and the virtualized are the same, also _maxHeight > _virtHeight.
*/
this._virtHeightRatio = 1;
/**
* Internal track for scroll top that is being virtualized
*/
this._virtScrollTop = 0;
/**
* If the next onScroll event is triggered due to internal setting of scrollTop
*/
this._bScrollInternal = false;
// End properties related to virtual height handling
this._embeddedViews = [];
}
/**
* @protected
* @return {?}
*/
get sizesCache() {
return this._sizesCache;
}
/**
* @protected
* @param {?} value
* @return {?}
*/
set sizesCache(value) {
this._sizesCache = value;
}
/**
* @private
* @return {?}
*/
get _isScrolledToBottom() {
if (!this.getVerticalScroll()) {
return true;
}
/** @type {?} */
const scrollTop = this.getVerticalScroll().scrollTop;
/** @type {?} */
const scrollHeight = this.getVerticalScroll().scrollHeight;
// Use === and not >= because `scrollTop + container size` can't be bigger than `scrollHeight`, unless something isn't updated.
// Also use Math.round because Chrome has some inconsistencies and `scrollTop + container` can be float when zooming the page.
return Math.round(scrollTop + this.igxForContainerSize) === scrollHeight;
}
/**
* @private
* @return {?}
*/
get _isAtBottomIndex() {
return this.igxForOf && this.state.startIndex + this.state.chunkSize > this.igxForOf.length;
}
/**
* @hidden
* @protected
* @return {?}
*/
get isRemote() {
return this.totalItemCount !== null;
}
/**
* @hidden
* @protected
* @return {?}
*/
removeScrollEventListeners() {
if (this.igxForScrollOrientation === 'horizontal') {
this._zone.runOutsideAngular(() => this.getHorizontalScroll().removeEventListener('scroll', this.func));
}
else {
/** @type {?} */
const vertical = this.getVerticalScroll();
if (vertical) {
this._zone.runOutsideAngular(() => vertical.removeEventListener('scroll', this.verticalScrollHandler));
}
}
}
/**
* @param {?} event
* @return {?}
*/
verticalScrollHandler(event) {
this.onScroll(event);
}
/**
* @return {?}
*/
isScrollable() {
return this.vh.instance.height > parseInt(this.igxForContainerSize, 10);
}
/**
* @hidden
* @return {?}
*/
ngOnInit() {
/** @type {?} */
let totalSize = 0;
/** @type {?} */
const vc = this.igxForScrollContainer ? this.igxForScrollContainer._viewContainer : this._viewContainer;
this.igxForSizePropName = this.igxForSizePropName || 'width';
/** @type {?} */
const dcFactory = this.resolver.resolveComponentFactory(DisplayContainerComponent);
this.dc = this._viewContainer.createComponent(dcFactory, 0);
this.dc.instance.scrollDirection = this.igxForScrollOrientation;
if (typeof MSGesture === 'function') {
// On Edge and IE when scrolling on touch the page scroll instead of the grid.
this.dc.instance._viewContainer.element.nativeElement.style.touchAction = 'none';
}
if (this.igxForOf && this.igxForOf.length) {
this.dc.instance.notVirtual = !(this.igxForContainerSize && this.state.chunkSize < this.igxForOf.length);
totalSize = this.initSizesCache(this.igxForOf);
this.hScroll = this.getElement(vc, 'igx-horizontal-virtual-helper');
if (this.hScroll) {
this.state.startIndex = this.getIndexAt(this.hScroll.scrollLeft, this.sizesCache, 0);
}
this.state.chunkSize = this._calculateChunkSize();
for (let i = 0; i < this.state.chunkSize && this.igxForOf[i] !== undefined; i++) {
/** @type {?} */
const input = this.igxForOf[i];
/** @type {?} */
const embeddedView = this.dc.instance._vcr.createEmbeddedView(this._template, { $implicit: input, index: this.igxForOf.indexOf(input) });
this._embeddedViews.push(embeddedView);
}
}
if (this.igxForScrollOrientation === 'vertical') {
this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
/** @type {?} */
const factory = this.resolver.resolveComponentFactory(VirtualHelperComponent);
this.vh = vc.createComponent(factory);
this._maxHeight = this._calcMaxBrowserHeight();
this.vh.instance.height = this.igxForOf ? this._calcHeight() : 0;
this._zone.runOutsideAngular(() => {
this.verticalScrollHandler = this.verticalScrollHandler.bind(this);
this.vh.instance.elementRef.nativeElement.addEventListener('scroll', this.verticalScrollHandler);
this.dc.instance.scrollContainer = this.vh.instance.elementRef.nativeElement;
});
}
if (this.igxForScrollOrientation === 'horizontal') {
this.func = (evt) => { this.onHScroll(evt); };
this.hScroll = this.getElement(vc, 'igx-horizontal-virtual-helper');
if (!this.hScroll) {
/** @type {?} */
const hvFactory = this.resolver.resolveComponentFactory(HVirtualHelperComponent);
this.hvh = vc.createComponent(hvFactory);
this.hvh.instance.width = totalSize;
this.hScroll = this.hvh.instance.elementRef.nativeElement;
this._zone.runOutsideAngular(() => {
this.hvh.instance.elementRef.nativeElement.addEventListener('scroll', this.func);
this.dc.instance.scrollContainer = this.hScroll;
});
}
else {
this._zone.runOutsideAngular(() => {
this.hScroll.addEventListener('scroll', this.func);
this.dc.instance.scrollContainer = this.hScroll;
});
}
/** @type {?} */
const scrollOffset = this.hScroll.scrollLeft -
(this.sizesCache && this.sizesCache.length ? this.sizesCache[this.state.startIndex] : 0);
this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
}
}
/**
* @hidden
* @return {?}
*/
ngOnDestroy() {
this.removeScrollEventListeners();
}
/**
* @hidden
* @param {?} changes
* @return {?}
*/
ngOnChanges(changes) {
/** @type {?} */
const forOf = 'igxForOf';
if (forOf in changes) {
/** @type {?} */
const value = changes[forOf].currentValue;
if (!this._differ && value) {
try {
this._differ = this._differs.find(value).create(this.igxForTrackBy);
}
catch (e) {
throw new Error(`Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
NgFor only supports binding to Iterables such as Arrays.`);
}
}
}
/** @type {?} */
const defaultItemSize = 'igxForItemSize';
if (defaultItemSize in changes && !changes[defaultItemSize].firstChange && this.igxForScrollOrientation === 'vertical') {
// handle default item size changed.
this.initSizesCache(this.igxForOf);
}
/** @type {?} */
const containerSize = 'igxForContainerSize';
if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
this._recalcOnContainerChange(changes);
}
}
/**
* @hidden
* @return {?}
*/
ngDoCheck() {
if (this._differ) {
/** @type {?} */
const changes = this._differ.diff(this.igxForOf);
if (changes) {
// re-init cache.
if (!this.igxForOf) {
return;
}
this._updateSizeCache();
this._zone.run(() => {
this._applyChanges();
this.cdr.markForCheck();
this._updateScrollOffset();
this.onDataChanged.emit();
});
}
}
}
/**
* Shifts the scroll thumb position.
* ```typescript
* this.parentVirtDir.addScrollTop(5);
* ```
* @param {?} addTop negative value to scroll up and positive to scroll down;
* @return {?}
*/
addScrollTop(addTop) {
if (addTop === 0 && this.igxForScrollOrientation === 'horizontal') {
return false;
}
/** @type {?} */
const originalVirtScrollTop = this._virtScrollTop;
/** @type {?} */
const containerSize = parseInt(this.igxForContainerSize, 10);
/** @type {?} */
const maxVirtScrollTop = this._virtHeight - containerSize;
this._bScrollInternal = true;
this._virtScrollTop += addTop;
this._virtScrollTop = this._virtScrollTop > 0 ?
(this._virtScrollTop < maxVirtScrollTop ? this._virtScrollTop : maxVirtScrollTop) :
0;
this.vh.instance.elementRef.nativeElement.scrollTop += addTop / this._virtHeightRatio;
if (Math.abs(addTop / this._virtHeightRatio) < 1) {
// Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
/** @type {?} */
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollTop);
// scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
}
/** @type {?} */
const curScrollTop = this.vh.instance.elementRef.nativeElement.scrollTop;
/** @type {?} */
const maxRealScrollTop = this.vh.instance.elementRef.nativeElement.scrollHeight - containerSize;
if ((this._virtScrollTop > 0 && curScrollTop === 0) ||
(this._virtScrollTop < maxVirtScrollTop && curScrollTop === maxRealScrollTop)) {
// Actual scroll position is at the top or bottom, but virtual one is not at the top or bottom (there's more to scroll)
// Recalculate actual scroll position based on the virtual scroll.
this.vh.instance.elementRef.nativeElement.scrollTop = this._virtScrollTop / this._virtHeightRatio;
}
else if (this._virtScrollTop === 0 && curScrollTop > 0) {
// Actual scroll position is not at the top, but virtual scroll is. Just update the actual scroll
this.vh.instance.elementRef.nativeElement.scrollTop = 0;
}
else if (this._virtScrollTop === maxVirtScrollTop && curScrollTop < maxRealScrollTop) {
// Actual scroll position is not at the bottom, but virtual scroll is. Just update the acual scroll
this.vh.instance.elementRef.nativeElement.scrollTop = maxRealScrollTop;
}
return this._virtScrollTop !== originalVirtScrollTop;
}
/**
* Scrolls to the specified index.
* ```typescript
* this.parentVirtDir.scrollTo(5);
* ```
* @param {?} index
* @return {?}
*/
scrollTo(index) {
if (index < 0 || index > (this.isRemote ? this.totalItemCount : this.igxForOf.length) - 1) {
return;
}
/** @type {?} */
const containerSize = parseInt(this.igxForContainerSize, 10);
/** @type {?} */
const scr = this.igxForScrollOrientation === 'horizontal' ?
this.hScroll.scrollLeft : this.vh.instance.elementRef.nativeElement.scrollTop;
/** @type {?} */
const isPrevItem = index < this.state.startIndex || scr > this.sizesCache[index];
/** @type {?} */
let nextScroll = isPrevItem ? this.sizesCache[index] : this.sizesCache[index + 1] - containerSize;
if (nextScroll < 0) {
return;
}
if (this.igxForScrollOrientation === 'horizontal') {
this.hScroll.scrollLeft = nextScroll;
}
else {
/** @type {?} */
const maxVirtScrollTop = this._virtHeight - containerSize;
if (nextScroll > maxVirtScrollTop) {
nextScroll = maxVirtScrollTop;
}
this._bScrollInternal = true;
this._virtScrollTop = nextScroll;
this.vh.instance.elementRef.nativeElement.scrollTop = this._virtScrollTop / this._virtHeightRatio;
this._adjustToIndex = !isPrevItem ? index : null;
}
}
/**
* Scrolls by one item into the appropriate next direction.
* For "horizontal" orientation that will be the right column and for "vertical" that is the lower row.
* ```typescript
* this.parentVirtDir.scrollNext();
* ```
* @return {?}
*/
scrollNext() {
/** @type {?} */
const scr = Math.ceil(this.igxForScrollOrientation === 'horizontal' ?
this.hScroll.scrollLeft :
this.vh.instance.elementRef.nativeElement.scrollTop);
/** @type {?} */
const endIndex = this.getIndexAt(scr + parseInt(this.igxForContainerSize, 10), this.sizesCache, 0);
this.scrollTo(endIndex);
}
/**
* Scrolls by one item into the appropriate previous direction.
* For "horizontal" orientation that will be the left column and for "vertical" that is the upper row.
* ```typescript
* this.parentVirtDir.scrollPrev();
* ```
* @return {?}
*/
scrollPrev() {
this.scrollTo(this.state.startIndex - 1);
}
/**
* Scrolls by one page into the appropriate next direction.
* For "horizontal" orientation that will be one view to the right and for "vertical" that is one view to the bottom.
* ```typescript
* this.parentVirtDir.scrollNextPage();
* ```
* @return {?}
*/
scrollNextPage() {
if (this.igxForScrollOrientation === 'horizontal') {
this.hvh.instance.elementRef.nativeElement.scrollLeft += parseInt(this.igxForContainerSize, 10);
}
else {
this.addScrollTop(parseInt(this.igxForContainerSize, 10));
}
}
/**
* Scrolls by one page into the appropriate previous direction.
* For "horizontal" orientation that will be one view to the left and for "vertical" that is one view to the top.
* ```typescript
* this.parentVirtDir.scrollPrevPage();
* ```
* @return {?}
*/
scrollPrevPage() {
if (this.igxForScrollOrientation === 'horizontal') {
this.hvh.instance.elementRef.nativeElement.scrollLeft -= parseInt(this.igxForContainerSize, 10);
}
else {
/** @type {?} */
const containerSize = (parseInt(this.igxForContainerSize, 10));
this.addScrollTop(-containerSize);
}
}
/**
* @hidden
* @param {?} colIndex
* @return {?}
*/
getColumnScrollLeft(colIndex) {
return this.sizesCache[colIndex];
}
/**
* Returns a reference to the vertical scrollbar DOM element.
* ```typescript
* this.parentVirtDir.getVerticalScroll();
* ```
* @return {?}
*/
getVerticalScroll() {
if (this.vh) {
return this.vh.instance.elementRef.nativeElement;
}
return null;
}
/**
* Returns the total number of items that are fully visible.
* ```typescript
* this.parentVirtDir.getItemCountInView();
* ```
* @return {?}
*/
getItemCountInView() {
/** @type {?} */
const position = this.igxForScrollOrientation === 'horizontal' ?
this.hScroll.scrollLeft :
this.vh.instance.elementRef.nativeElement.scrollTop;
/** @type {?} */
let startIndex = this.getIndexAt(position, this.sizesCache, 0);
if (position - this.sizesCache[startIndex] > 0) {
// fisrt item is not fully in view
startIndex++;
}
/** @type {?} */
const endIndex = this.getIndexAt(position + parseInt(this.igxForContainerSize, 10), this.sizesCache, 0);
return endIndex - startIndex;
}
/**
* Returns a reference to the horizontal scrollbar DOM element.
* ```typescript
* this.parentVirtDir.getHorizontalScroll();
* ```
* @return {?}
*/
getHorizontalScroll() {
return this.getElement(this._viewContainer, 'igx-horizontal-virtual-helper') || this.hScroll;
}
/**
* Returns the size of the element at the specified index.
* ```typescript
* this.parentVirtDir.getSizeAt(1);
* ```
* @param {?} index
* @return {?}
*/
getSizeAt(index) {
return this.sizesCache[index + 1] - this.sizesCache[index];
}
/**
* Returns the scroll offset of the element at the specified index.
* ```typescript
* this.parentVirtDir.getScrollForIndex(1);
* ```
* @param {?} index
* @param {?=} bottom
* @return {?}
*/
getScrollForIndex(index, bottom) {
/** @type {?} */
const containerSize = parseInt(this.igxForContainerSize, 10);
/** @type {?} */
const scroll = bottom ? this.sizesCache[index + 1] - containerSize : this.sizesCache[index];
return scroll;
}
/**
* @hidden
* Function that is called when scrolling vertically
* @protected
* @param {?} event
* @return {?}
*/
onScroll(event) {
/* in certain situations this may be called when no scrollbar is visible */
if (!parseInt(this.vh.instance.elementRef.nativeElement.style.height, 10)) {
return;
}
/** @type {?} */
const containerSize = parseInt(this.igxForContainerSize, 10);
/** @type {?} */
const maxRealScrollTop = event.target.children[0].scrollHeight - containerSize;
/** @type {?} */
const realPercentScrolled = event.target.scrollTop / maxRealScrollTop;
if (!this._bScrollInternal) {
/** @type {?} */
const maxVirtScrollTop = this._virtHeight - containerSize;
this._virtScrollTop = realPercentScrolled * maxVirtScrollTop;
}
else {
this._bScrollInternal = false;
}
/** @type {?} */
const prevStartIndex = this.state.startIndex;
/** @type {?} */
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollTop);
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
requestAnimationFrame(() => {
// check if height/width has changes in views.
this.recalcUpdateSizes();
});
this.dc.changeDetectorRef.detectChanges();
if (prevStartIndex !== this.state.startIndex) {
this.onChunkLoad.emit(this.state);
}
}
/**
* @hidden
* Function that recaculates and updates cache sizes.
* @return {?}
*/
recalcUpdateSizes() {
/** @type {?} */
const dimension = this.igxForScrollOrientation === 'horizontal' ?
this.igxForSizePropName : 'height';
/** @type {?} */
const diffs = [];
/** @type {?} */
let totalDiff = 0;
for (let i = 0; i < this._embeddedViews.length; i++) {
/** @type {?} */
const view = this._embeddedViews[i];
/** @type {?} */
const rNode = view.rootNodes.find((node) => node.nodeType === Node.ELEMENT_NODE);
if (rNode) {
/** @type {?} */
const h = Math.max(rNode.offsetHeight, parseInt(this.igxForItemSize, 10));
/** @type {?} */
const index = this.state.startIndex + i;
if (!this.isRemote && !this.igxForOf[index]) {
continue;
}
/** @type {?} */
const oldVal = dimension === 'height' ? this.heightCache[index] : this.igxForOf[index][dimension];
/** @type {?} */
const newVal = dimension === 'height' ? h : rNode.clientWidth;
if (dimension === 'height') {
this.heightCache[index] = newVal;
}
else {
this.igxForOf[index][dimension] = newVal;
}
/** @type {?} */
const currDiff = newVal - oldVal;
diffs.push(currDiff);
totalDiff += currDiff;
this.sizesCache[index + 1] += totalDiff;
}
}
// update cache
if (Math.abs(totalDiff) > 0) {
for (let j = this.state.startIndex + this.state.chunkSize + 1; j < this.sizesCache.length; j++) {
this.sizesCache[j] += totalDiff;
}
// update scrBar heights/widths
if (this.igxForScrollOrientation === 'horizontal') {
/** @type {?} */
const totalWidth = parseInt(this.hScroll.children[0].style.width, 10) + totalDiff;
this.hScroll.children[0].style.width = totalWidth + 'px';
}
/** @type {?} */
const reducer = (acc, val) => acc + val;
if (this.igxForScrollOrientation === 'vertical') {
/** @type {?} */
const scrToBottom = this._isScrolledToBottom && !this.dc.instance.notVirtual;
/** @type {?} */
const hSum = this.heightCache.reduce(reducer);
if (hSum > this._maxHeight) {
this._virtHeightRatio = hSum / this._maxHeight;
}
this.vh.instance.height = Math.min(this.vh.instance.height + totalDiff, this._maxHeight);
this._virtHeight = hSum;
if (!this.vh.instance.destroyed) {
this.vh.instance.cdr.detectChanges();
}
if (scrToBottom && !this._isAtBottomIndex) {
/** @type {?} */
const containerSize = parseInt(this.igxForContainerSize, 10);
/** @type {?} */
const scrollOffset = this.fixedUpdateAllElements(this._virtHeight - containerSize);
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
return;
}
if (this._adjustToIndex) {
// in case scrolled to specific index where after scroll heights are changed
// need to adjust the offsets so that item is last in view.
/** @type {?} */
const updatesToIndex = this._adjustToIndex - this.state.startIndex + 1;
/** @type {?} */
const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer);
/** @type {?} */
const currOffset = parseInt(this.dc.instance._viewContainer.element.nativeElement.style.top, 10);
this.dc.instance._viewContainer.element.nativeElement.style.top = (currOffset - sumDiffs) + 'px';
this._adjustToIndex = null;
}
}
}
}
/**
* @hidden
* @protected
* @param {?} inScrollTop
* @return {?}
*/
fixedUpdateAllElements(inScrollTop) {
/** @type {?} */
const count = this.isRemote ? this.totalItemCount : this.igxForOf.length;
/** @type {?} */
let newStart = this.getIndexAt(inScrollTop, this.sizesCache, 0);
if (newStart + this.state.chunkSize > count) {
newStart = count - this.state.chunkSize;
}
/** @type {?} */
const prevStart = this.state.startIndex;
/** @type {?} */
const diff = newStart - this.state.startIndex;
this.state.startIndex = newStart;
if (diff) {
this.onChunkPreload.emit(this.state);
if (!this.isRemote) {
/*recalculate and apply page size.*/
if (diff > 0 && diff <= this.MAX_PERF_SCROLL_DIFF) {
this.moveApplyScrollNext(prevStart);
}
else if (diff < 0 && Math.abs(diff) <= this.MAX_PERF_SCROLL_DIFF) {
this.moveApplyScrollPrev(prevStart);
}
else {
this.fixedApplyScroll();
}
}
}
return inScrollTop - this.sizesCache[this.state.startIndex];
}
/**
* @hidden
* The function applies an optimized state change for scrolling down/right employing context change with view rearrangement
* @protected
* @param {?} prevIndex
* @return {?}
*/
moveApplyScrollNext(prevIndex) {
/** @type {?} */
const start = prevIndex + this.state.chunkSize;
for (let i = start; i < start + this.state.startIndex - prevIndex && this.igxForOf[i] !== undefined; i++) {
/** @type {?} */
const input = this.igxForOf[i];
/** @type {?} */
const embView = this._embeddedViews.shift();
/** @type {?} */
const cntx = embView.context;
cntx.$implicit = input;
cntx.index = this.getContextIndex(input);
/** @type {?} */
const view = this.dc.instance._vcr.detach(0);
this.dc.instance._vcr.insert(view);
this._embeddedViews.push(embView);
}
}
/**
* @hidden
* The function applies an optimized state change for scrolling up/left employing context change with view rearrangement
* @protected
* @param {?} prevIndex
* @return {?}
*/
moveApplyScrollPrev(prevIndex) {
for (let i = prevIndex - 1; i >= this.state.startIndex && this.igxForOf[i] !== undefined; i--) {
/** @type {?} */
const input = this.igxForOf[i];
/** @type {?} */
const embView = this._embeddedViews.pop();
/** @type {?} */
const cntx = embView.context;
cntx.$implicit = input;
cntx.index = this.getContextIndex(input);
/** @type {?} */
const view = this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1);
this.dc.instance._vcr.insert(view, 0);
this._embeddedViews.unshift(embView);
}
}
/**
* @hidden
* @protected
* @param {?} input
* @return {?}
*/
getContextIndex(input) {
return this.isRemote ? this.state.startIndex + this.igxForOf.indexOf(input) : this.igxForOf.indexOf(input);
}
/**
* @hidden
* The function applies an optimized state change through context change for each view
* @protected
* @return {?}
*/
fixedApplyScroll() {
/** @type {?} */
let j = 0;
/** @type {?} */
const endIndex = this.state.startIndex + this.state.chunkSize;
for (let i = this.state.startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
/** @type {?} */
const input = this.igxForOf[i];
/** @type {?} */
const embView = this._embeddedViews[j++];
/** @type {?} */
const cntx = ((/** @type {?} */ (embView))).context;
cntx.$implicit = input;
cntx.index = this.getContextIndex(input);
}
}
/**
* @hidden
* Function that is called when scrolling horizontally
* @protected
* @param {?} event
* @return {?}
*/
onHScroll(event) {
/* in certain situations this may be called when no scrollbar is visible */
if (!parseInt(this.hScroll.children[0].style.width, 10)) {
return;
}
/** @type {?} */
const curScrollLeft = event.target.scrollLeft;
/** @type {?} */
const prevStartIndex = this.state.startIndex;
// Updating horizontal chunks
/** @type {?} */
const scrollOffset = this.fixedUpdateAllElements(curScrollLeft);
this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
this.dc.changeDetectorRef.detectChanges();
if (prevStartIndex !== this.state.startIndex) {
this.onChunkLoad.emit(this.state);
}
}
/**
* Gets the function used to track changes in the items collection.
* By default the object references are compared. However this can be optimized if you have unique identifier
* value that can be used for the comparison instead of the object ref or if you have some other property values
* in the item object that should be tracked for changes.
* This option is similar to ngForTrackBy.
* ```typescript
* const trackFunc = this.parentVirtDir.igxForTrackBy;
* ```
* @return {?}
*/
get igxForTrackBy() { return this._trackByFn; }
/**
* Sets the function used to track changes in the items collection.
* This function can be set in scenarios where you want to optimize or
* customize the tracking of changes for the items in the collection.
* The igxForTrackBy function takes the index and the current item as arguments and needs to return the unique identifier for this item.
* ```typescript
* this.parentVirtDir.igxForTrackBy = (index, item) => {
* return item.id + item.width;
* };
* ```
* @param {?} fn
* @return {?}
*/
set igxForTrackBy(fn) { this._trackByFn = fn; }
/**
* @hidden
* @protected
* @return {?}
*/
_applyChanges() {
/** @type {?} */
const prevChunkSize = this.state.chunkSize;
this.applyChunkSizeChange();
this._recalcScrollBarSize();
if (this.igxForOf && this.igxForOf.length && this.dc) {
/** @type {?} */
const embeddedViewCopy = Object.assign([], this._embeddedViews);
/** @type {?} */
let startIndex = this.state.startIndex;
/** @type {?} */
let endIndex = this.state.chunkSize + this.state.startIndex;
if (this.isRemote) {
startIndex = 0;
endIndex = this.igxForOf.length;
}
for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
/** @type {?} */
const input = this.igxForOf[i];
/** @type {?} */
const embView = embeddedViewCopy.shift();
/** @type {?} */
const cntx = ((/** @type {?} */ (embView))).context;
cntx.$implicit = input;
cntx.index = this.getContextIndex(input);
}
this.dc.changeDetectorRef.detectChanges();
if (prevChunkSize !== this.state.chunkSize) {
this.onChunkLoad.emit(this.state);
}
if (this.igxForScrollOrientation === 'vertical') {
this.recalcUpdateSizes();
}
}
}
/**
* @hidden
* @protected
* @return {?}
*/
_calcMaxBrowserHeight() {
/** @type {?} */
const div = document.createElement('div');
/** @type {?} */
const style = div.style;
style.position = 'absolute';
style.top = '9999999999999999px';
document.body.appendChild(div);
/** @type {?} */
const size = Math.abs(div.getBoundingClientRect()['top']);
document.body.removeChild(div);
return size;
}
/**
* @hidden
* Recalculates the chunkSize based on current startIndex and returns the new size.
* This should be called after this.state.startIndex is updated, not before.
* @protected
* @return {?}
*/
_calculateChunkSize() {
/** @type {?} */
let chunkSize = 0;
if (this.igxForContainerSize !== null && this.igxForContainerSize !== undefined) {
if (!this.sizesCache) {
this.initSizesCache(this.igxForOf);
}
chunkSize = this._calcMaxChunkSize();
if (this.igxForOf && chunkSize > this.igxForOf.length) {
chunkSize = this.igxForOf.length;
}
}
else {
if (this.igxForOf) {
chunkSize = this.igxForOf.length;
}
}
return chunkSize;
}
/**
* @hidden
* @protected
* @param {?} viewref
* @param {?} nodeName
* @return {?}
*/
getElement(viewref, nodeName) {
/** @type {?} */
const elem = viewref.element.nativeElement.parentNode.getElementsByTagName(nodeName);
return elem.length > 0 ? elem[0] : null;
}
/**
* @hidden
* @protected
* @param {?} items
* @return {?}
*/
initSizesCache(items) {
/** @type {?} */
let totalSize = 0;
/** @type {?} */
let size = 0;
/** @type {?} */
const dimension = this.igxForScrollOrientation === 'horizontal' ?
this.igxForSizePropName : 'height';
/** @type {?} */
let i = 0;
this.sizesCache = [];
this.heightCache = [];
this.sizesCache.push(0);
/** @type {?} */
const count = this.isRemote ? this.totalItemCount : items.length;
for (i; i < count; i++) {
if (dimension === 'height') {
// cols[i][dimension] = parseInt(this.igxForItemSize, 10) || 0;
size = parseInt(this.igxForItemSize, 10) || 0;
this.heightCache.push(size);
}
else {
size = this._getItemSize(items[i], dimension);
}
totalSize += size;
this.sizesCache.push(totalSize);
}
return totalSize;
}
/**
* @protected
* @return {?}
*/
_updateSizeCache() {
if (this.igxForScrollOrientation === 'horizontal') {
this.initSizesCache(this.igxForOf);
return;
}
/** @type {?} */
const scr = this.vh.instance.elementRef.nativeElement;
/** @type {?} */
const oldHeight = this.heightCache.length > 0 ? this.heightCache.reduce((acc, val) => acc + val) : 0;
/** @type {?} */
const newHeight = this.initSizesCache(this.igxForOf);
/** @type {?} */
const diff = oldHeight - newHeight;
// if data has been changed while container is scrolled
// should update scroll top/left according to change so that same startIndex is in view
if (Math.abs(diff) > 0 && scr.scrollTop > 0) {
this.recalcUpdateSizes();
/** @type {?} */
const offset = parseInt(this.dc.instance._viewContainer.element.nativeElement.style.top, 10);
scr.scrollTop = this.sizesCache[this.state.startIndex] - offset;
}
}
/**
* @hidden
* @protected
* @return {?}
*/
_calcMaxChunkSize() {
/** @type {?} */
let i = 0;
/** @type {?} */
let length = 0;
/** @type {?} */
let maxLength = 0;
/** @type {?} */
const arr = [];
/** @type {?} */
let sum = 0;
/** @type {?} */
const availableSize = parseInt(this.igxForContainerSize, 10);
if (!availableSize) {
return 0;
}
/** @type {?} */
const dimension = this.igxForScrollOrientation === 'horizontal' ?
this.igxForSizePropName : 'height';
/** @type {?} */
const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
for (i; i < this.igxForOf.length; i++) {
/** @type {?} */
let item = this.igxForOf[i];
if (dimension === 'height') {
item = { value: this.igxForOf[i], height: this.heightCache[i] };
}
/** @type {?} */
const size = dimension === 'height' ?
this.heightCache[i] :
this._getItemSize(item, dimension);
sum = arr.reduce(reducer, size);
if (sum < availableSize) {
arr.push(item);
length = arr.length;
if (i === this.igxForOf.length - 1) {
// reached end without exceeding
// include prev items until size is filled or first item is reached.
/** @type {?} */
let curItem = dimension === 'height' ? arr[0].value : arr[0];
/** @type {?} */
let prevIndex = this.igxForOf.indexOf(curItem) - 1;
while (prevIndex >= 0 && sum <= availableSize) {
curItem = dimension === 'height' ? arr[0].value : arr[0];
prevIndex = this.igxForOf.indexOf(curItem) - 1;
/** @type {?} */
const prevItem = this.igxForOf[prevIndex];
/** @type {?} */
const prevSize = dimension === 'height' ?
this.heightCache[prevIndex] :
parseInt(prevItem[dimension], 10);
sum = arr.reduce(reducer, prevSize);
arr.unshift(prevItem);
length = arr.length;
}
}
}
else {
arr.push(item);
length = arr.length + 1;
arr.shift();
}
if (length > maxLength) {
maxLength = length;
}
}
return maxLength;
}
/**
* @hidden
* @protected
* @param {?} left
* @param {?} set
* @param {?} index
* @return {?}
*/
getIndexAt(left, set, index) {
/** @type {?} */
let start = 0;
/** @type {?} */
let end = set.length - 1;
if (left === 0) {
return 0;
}
while (start <= end) {
/** @type {?} */
const midIdx = Math.floor((start + end) / 2);
/** @type {?} */
const midLeft = set[midIdx];
/** @type {?} */
const cmp = left - midLeft;
if (cmp > 0) {
start = midIdx + 1;
}
else if (cmp < 0) {
end = midIdx - 1;
}
else {
return midIdx;
}
}
return end;
}
/**
* @protected
* @return {?}
*/
_recalcScrollBarSize() {
/** @type {?} */
const count = this.isRemote ? this.totalItemCount : (this.igxForOf ? this.igxForOf.length : 0);
this.dc.instance.notVirtual = !(this.igxForContainerSize && this.dc && this.state.chunkSize < count);
if (this.igxForScrollOrientation === 'horizontal') {
/** @type {?} */
const totalWidth = this.igxForContainerSize ? this.initSizesCache(this.igxForOf) : 0;
this.hScroll.style.width = this.igxForContainerSize + 'px';
this.hScroll.children[0].style.width = totalWidth + 'px';
}
if (this.igxForScrollOrientation === 'vertical') {
this.vh.instance.elementRef.nativeElement.style.height = parseInt(this.igxForContainerSize, 10) + 'px';
this.vh.instance.height = this._calcHeight();
}
}
/**
* @protected
* @return {?}
*/
_calcHeight() {
/** @type {?} */
let height;
if (this.heightCache) {
height = this.heightCache.reduce((acc, val) => acc + val, 0);
}
else {
height = this.initSizesCache(this.igxForOf);
}
this._virtHeight = height;
if (height > this._maxHeight) {
this._virtHeightRatio = height / this._maxHeight;
height = this._maxHeight;
}
return height;
}
/**
* @protected
* @param {?} changes
* @return {?}
*/
_recalcOnContainerChange(changes) {
this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
this.dc.instance._viewContainer.element.nativeElement.style.left = '0px';
/** @type {?} */
const prevChunkSize = this.state.chunkSize;
this.applyChunkSizeChange();
this._recalcScrollBarSize();
if (prevChunkSize !== this.state.chunkSize) {
this.onChunkLoad.emit(this.state);
}
if (this.sizesCache && this.hScroll && this.hScroll.scrollLeft !== 0) {
// Updating horizontal chunks and offsets based on the new scrollLeft
/** @type {?} */
const scrollOffset = this.fixedUpdateAllElements(this.hScroll.scrollLeft);
this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
}
}
/**
* @hidden
* Removes an elemenet from the embedded views and updates chunkSize.
* @protected
* @return {?}
*/
removeLastElem() {
/** @type {?} */
const oldElem = this._embeddedViews.pop();
this.onBeforeViewDestroyed.emit(oldElem);
oldElem.destroy();
this.state.chunkSize--;
}
/**
* @hidden
* If there exists an element that we can create embedded view for creates it, appends it and updates chunkSize
* @protected
* @return {?}
*/
addLastElem() {
/** @type {?} */
let elemIndex = this.state.startIndex + this.state.chunkSize;
if (!this.isRemote && !this.igxForOf) {
return;
}
if (elemIndex >= this.igxForOf.length) {
elemIndex = this.igxForOf.length - this.state.chunkSize;
}
/** @type {?} */
const input = this.igxForOf[elemIndex];
/** @type {?} */
const embeddedView = this.dc.instance._vcr.createEmbeddedView(this._template, { $implicit: input, index: elemIndex });
this._embeddedViews.push(embeddedView);
this.state.chunkSize++;
this._zone.run(() => {
this.cdr.markForCheck();
});
}
/**
* Recalculates chunkSize and adds/removes elements if need due to the change.
* this.state.chunkSize is updated in \@addLastElem() or \@removeLastElem()
* @protected
* @return {?}
*/
applyChunkSizeChange() {
/** @type {?} */
const chunkSize = this.isRemote ? (this.igxForOf ? this.igxForOf.length : 0) : this._calculateChunkSize();
if (chunkSize > this.state.chunkSize) {
/** @type {?} */
const diff = chunkSize - this.state.chunkSize;
for (let i = 0; i < diff; i++) {
this.addLastElem();
}
}
else if (chunkSize < this.state.chunkSize) {
/** @type {?} */
const diff = this.state.chunkSize - chunkSize;
for (let i = 0; i < diff; i++) {
this.removeLastElem();
}
}
}
/**
* @protected
* @return {?}
*/
_updateScrollOffset() {
if (this.igxForScrollOrientation === 'horizontal') {
this._updateHScrollOffset();
}
else {
this._updateVScrollOffset();
}
}
/**
* @private
* @return {?}
*/
_updateVScrollOffset() {
/** @type {?} */
let scrollOffset = 0;
/** @type {?} */
const vScroll = this.vh.instance.elementRef.nativeElement;
scrollOffset = vScroll && parseInt(vScroll.style.height, 10) ?
vScroll.scrollTop - this.sizesCache[this.state.startIndex] : 0;
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
}
/**
* @private
* @return {?}
*/
_updateHScrollOffset() {
/** @type {?} */
let scrollOffset = 0;
scrollOffset = this.hScroll && parseInt(this.hScroll.children[0].style.width, 10) ?
this.hScroll.scrollLeft - this.sizesCache[this.state.startIndex] : 0;
this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
}
/**
* @private
* @param {?} item
* @param {?} dimension
* @return {?}
*/
_getItemSize(item, dimension) {
/** @type {?} */
const hasDimension = (item[dimension] !== null && item[dimension] !== undefined);
return hasDimension ? parseInt(item[dimension], 10) : this.igxForItemSize;
}
}
IgxForOfDirective.decorators = [
{ type: Directive, args: [{ selector: '[igxFor][igxForOf]' },] }
];
/** @nocollapse */
IgxForOfDirective.ctorParameters = () => [
{ type: ViewContainerRef },
{ type: TemplateRef },
{ type: IterableDiffers },
{ type: ComponentFactoryResolver },
{ type: ChangeDetectorRef },
{ type: NgZone }
];
IgxForOfDirective.propDecorators = {
igxForOf: [{ type: Input }],
igxForSizePropName: [{ type: Input }],
igxForScrollOrientation: [{ type: Input }],
igxForScrollContainer: [{ type: Input }],
igxForContainerSize: [{ type: Input }],
igxForItemSize: [{ type: Input }],
onChunkLoad: [{ type: Output }],
onDataChanged: [{ type: Output }],
onBeforeViewDestroyed: [{ type: Output }],
onChunkPreload: [{ type: Output }],
igxForTrackBy: [{ type: Input }]
};
if (false) {
/**
* An \@Input property that sets the data to