UNPKG

igniteui-angular

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

1,318 lines • 214 kB
/** * @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