UNPKG

@transunion-ui/tablejs

Version:
1,063 lines 178 kB
import { Directive, ContentChild, EventEmitter, Inject, Input, Output } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { TablejsForOfContext } from './../../directives/virtual-for/virtual-for.directive'; import { ScrollPrevSpacerComponent } from '../../components/scroll-prev-spacer/scroll-prev-spacer.component'; import { take } from 'rxjs'; import * as i0 from "@angular/core"; import * as i1 from "./../../services/grid/grid.service"; import * as i2 from "./../../services/directive-registration/directive-registration.service"; import * as i3 from "./../../services/scroll-dispatcher/scroll-dispatcher.service"; import * as i4 from "./../../services/operating-system/operating-system.service"; export class ScrollViewportDirective { constructor(elementRef, gridService, document, directiveRegistrationService, scrollDispatcherService, operatingSystem, cdr, rendererFactory) { this.elementRef = elementRef; this.gridService = gridService; this.document = document; this.directiveRegistrationService = directiveRegistrationService; this.scrollDispatcherService = scrollDispatcherService; this.operatingSystem = operatingSystem; this.cdr = cdr; this.rendererFactory = rendererFactory; this.templateRef = null; this.templateID = ''; this.generateCloneMethod = null; this._arrowUpSpeed = 1; this._arrowDownSpeed = 1; this._preItemOverflow = 1; this._postItemOverflow = 1; this._itemLoadLimit = Infinity; this.items = null; // Custom Elements Inputs this.templateid = null; this.preitemoverflow = 1; this.postitemoverflow = 1; this.arrowupspeed = 1; this.arrowdownspeed = 1; this.itemloadlimit = Infinity; this.itemAdded = new EventEmitter(); this.itemRemoved = new EventEmitter(); this.itemUpdated = new EventEmitter(); this.rangeUpdated = new EventEmitter(); this.viewportScrolled = new EventEmitter(); this.viewportReady = new EventEmitter(); this.viewportInitialized = new EventEmitter(); this.containerHeight = null; this.heightLookup = {}; this.itemVisibilityLookup = {}; this.listElm = null; this.listContent = null; this.prevSpacer = null; this.postSpacer = null; this.gridDirective = null; this.pauseViewportRenderUpdates = false; this.range = { startIndex: 0, endIndex: 1, extendedStartIndex: 0, extendedEndIndex: 1 }; this.lastRange = { startIndex: this.range.startIndex, endIndex: this.range.endIndex, extendedStartIndex: this.range.extendedStartIndex, extendedEndIndex: this.range.extendedEndIndex }; this.lastScrollTop = 0; this.currentScrollTop = 0; this.currentScrollChange = 0; this.template = null; this.estimatedFullContentHeight = 0; this.estimatedPreListHeight = 0; this.estimatedPostListHeight = 0; this.totalItemsCounted = 0; this.totalHeightCount = 0; this.itemName = ''; this.overflowHeightCount = 0; this.scrollChangeByFirstIndexedItem = 0; this.lastVisibleItemHeight = Infinity; this.adjustedStartIndex = null; this.forcedEndIndex = undefined; this.placeholderObject = {}; this.postItemOverflowCount = -1; this.preItemOverflowCount = -1; this.lastVisibleItemOverflow = 0; this.preOverflowHeight = 0; this.mouseIsOverViewport = false; this.lastHeight = 0; this.observer = null; this.handleMouseOver = null; this.handleMouseOut = null; this.handleKeyDown = null; this.cloneFromTemplateRef = false; this.viewportHasScrolled = false; this.templateContext = null; this.virtualNexus = null; this._cloneMethod = null; this.onTransitionEnd = (e) => { }; this.onTransitionRun = (e) => { }; this.onTransitionStart = (e) => { }; this.onTransitionCancel = (e) => { }; this.renderer = this.rendererFactory.createRenderer(null, null); this.elementRef.nativeElement.scrollViewportDirective = this; } get arrowUpSpeed() { return Number(this._arrowUpSpeed); } set arrowUpSpeed(value) { this._arrowUpSpeed = Number(value); } get arrowDownSpeed() { return Number(this._arrowDownSpeed); } set arrowDownSpeed(value) { this._arrowDownSpeed = Number(value); } get preItemOverflow() { return Number(this._preItemOverflow); } set preItemOverflow(value) { this._preItemOverflow = Number(value); } get postItemOverflow() { return Number(this._postItemOverflow); } set postItemOverflow(value) { this._postItemOverflow = Number(value); } get itemLoadLimit() { return Number(this._itemLoadLimit); } set itemLoadLimit(value) { this._itemLoadLimit = Number(value); } handleScroll(e) { e.preventDefault(); this.currentScrollTop = this.listContent.scrollTop; this.currentScrollChange = this.currentScrollTop - this.lastScrollTop; this.scrollChangeByFirstIndexedItem += this.currentScrollChange; this.lastVisibleItemOverflow -= this.currentScrollChange; const newRange = this.getRangeChange(this.scrollChangeByFirstIndexedItem); this.updateScrollFromRange(newRange); this.scrollDispatcherService.dispatchViewportScrolledEvents(this.viewportScrolled, this.lastScrollTop, this.scrollChangeByFirstIndexedItem, this, this.elementRef.nativeElement); } registerViewportToElement() { this.elementRef.nativeElement.scrollViewport = this; } attachMutationObserver() { const ths = this; this.observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { ths.updateMutations(mutation); }); }); this.observer.observe(this.listContent, { // configure it to listen to attribute changes attributes: true, subtree: true, childList: true }); } updateMutations(mutation) { if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(node => { this.directiveRegistrationService.registerNodeAttributes(node); this.getChildNodes(node); }); } } getChildNodes(node) { node.childNodes.forEach(childNode => { this.directiveRegistrationService.registerNodeAttributes(childNode); if (childNode.childNodes) { this.getChildNodes(childNode); } }); } registerCustomElementsInputs(viewport) { this.templateID = viewport.getAttribute('templateID'); this.preItemOverflow = Number(viewport.getAttribute('preItemOverflow')); this.postItemOverflow = Number(viewport.getAttribute('postItemOverflow')); this.itemLoadLimit = Number(viewport.getAttribute('itemLoadLimit')); this.arrowUpSpeed = Number(viewport.getAttribute('arrowUpSpeed')); this.arrowDownSpeed = Number(viewport.getAttribute('arrowDownSpeed')); this.fillViewportScrolling = viewport.getAttribute('fillViewportScrolling'); } convertCustomElementsVariables() { if (this.templateid) { this.templateID = this.templateid; } if (this.preitemoverflow) { this.preItemOverflow = Number(this.preitemoverflow); } if (this.postitemoverflow) { this.postItemOverflow = Number(this.postitemoverflow); } if (this.arrowdownspeed) { this.arrowDownSpeed = Number(this.arrowdownspeed); } if (this.arrowupspeed) { this.arrowUpSpeed = Number(this.arrowupspeed); } if (this.itemloadlimit !== null) { this.itemLoadLimit = Number(this.itemloadlimit); } } createTBodies() { this.listElm = this.elementRef.nativeElement; let body = this.listElm.getElementsByTagName('tbody')[0]; if (body) { body = body.getAttribute('tablejsViewport') !== null ? body : null; } this.listContent = body ? body : document.createElement('tbody'); this.listContent.setAttribute('tablejsListContent', ''); this.listContent.setAttribute('tablejsViewport', ''); this.listContent.style.display = 'block'; this.listContent.style.position = 'relative'; this.listContent.style.height = '350px'; this.listContent.style.overflowY = 'auto'; this.listElm.appendChild(this.listContent); if (this.fillViewportScrolling !== undefined && this.fillViewportScrolling !== null) { const coverBody = document.createElement('tbody'); coverBody.style.display = 'block'; coverBody.style.position = 'absolute'; coverBody.style.width = '100%'; coverBody.style.height = '100%'; coverBody.style.overflow = 'auto'; coverBody.style.pointerEvents = 'none'; coverBody.style.visibility = 'false'; this.listElm.appendChild(coverBody); } this.directiveRegistrationService.registerViewportOnGridDirective(this.listContent); const componentRef = this.virtualNexus.virtualForDirective._viewContainer.createComponent(ScrollPrevSpacerComponent); this.virtualNexus.virtualForDirective._viewContainer.detach(0); const ref = this.virtualNexus.virtualForDirective._viewContainer.createEmbeddedView(componentRef.instance.template, undefined, 0); componentRef.destroy(); this.prevSpacer = ref.rootNodes[0]; this.postSpacer = document.createElement('tr'); this.postSpacer.setAttribute('tablejsPostSpacer', ''); this.postSpacer.style.display = 'block'; this.postSpacer.style.position = 'relative'; this.listContent.appendChild(this.postSpacer); } addScrollHandler() { this.listContent.addEventListener('scroll', this.handleListContentScroll = (e) => { this.handleScroll(e); }); } rerenderRowAt(index, updateScrollPosition = false) { if (!this.viewportHasScrolled) { return; } const ind = index - this.adjustedStartIndex; const itemName = 'item' + index; if (ind > this.items.length - 1 || this.itemVisibilityLookup[this.itemName] !== true) { return; } const indexMap = {}; for (let i = 1; i < this.virtualNexus.virtualForDirective._viewContainer.length; i++) { indexMap[this.virtualNexus.virtualForDirective._viewContainer.get(i).rootNodes[0].index] = i; } ; const detachedRef = this.virtualNexus.virtualForDirective._viewContainer.detach(indexMap[index]); const child = detachedRef.rootNodes[0]; detachedRef.destroy(); this.templateContext = new TablejsForOfContext(this.items[index], this.virtualNexus.virtualForDirective._tablejsForOf, index, this.items.length); const ref = this.virtualNexus.virtualForDirective._viewContainer.createEmbeddedView(this.virtualNexus.virtualForDirective._template, this.templateContext, indexMap[index]); this.virtualNexus.virtualForDirective._viewContainer.move(ref, indexMap[index]); let clone = ref.rootNodes[0]; clone.index = index; this.cdr.detectChanges(); this.scrollDispatcherService.dispatchRemoveItemEvents(this.itemRemoved, child, index, this, this.elementRef.nativeElement); const lookupHeight = clone.offsetHeight; const oldHeight = this.heightLookup[itemName]; this.heightLookup[itemName] = lookupHeight; clone.lastHeight = lookupHeight; this.addResizeSensor(clone, index); if (oldHeight) { this.updateEstimatedHeightFromResize(oldHeight, lookupHeight); } else { this.updateEstimatedHeight(lookupHeight); } if (updateScrollPosition) { this.refreshViewport(); } this.scrollDispatcherService.dispatchUpdateItemEvents(this.itemUpdated, clone, index, this, this.elementRef.nativeElement); this.scrollDispatcherService.dispatchAddItemEvents(this.itemAdded, clone, index, this, this.elementRef.nativeElement); } viewportRendered() { this.virtualNexus = this.directiveRegistrationService.getVirtualNexusFromViewport(this); if (this.virtualNexus && this.virtualNexus.virtualForDirective) { this.items = this.virtualNexus.virtualForDirective._tablejsForOf; this.virtualForChangesSubscription$ = this.virtualNexus.virtualForDirective.changes.subscribe(item => { const isTheSameArray = this.items === item.tablejsForOf; this.items = item.tablejsForOf; const scrollToOptions = { index: 0, scrollAfterIndexedItem: 0 }; if (isTheSameArray) { scrollToOptions.index = this.range.startIndex; scrollToOptions.scrollAfterIndexedItem = this.scrollChangeByFirstIndexedItem; // array has changed...rerender current elements const listChildren = Array.from(this.listContent.childNodes); } else { this.updateItems(item.tablejsForOf, scrollToOptions); } }); } this.createTBodies(); this.addScrollHandler(); if (this.items && (this.generateCloneMethod || this.virtualNexus.virtualForDirective._template)) { this.initScroll({ items: this.items, generateCloneMethod: this._cloneMethod }); } this.scrollDispatcherService.dispatchViewportReadyEvents(this.viewportReady, this, this.elementRef.nativeElement); } scrollToBottom() { this.range.startIndex = this.items.length; this.scrollToExact(this.range.startIndex, 0); } scrollToTop() { this.scrollToExact(0, 0); } pageUp() { let heightCount = this.scrollChangeByFirstIndexedItem; if (this.range.startIndex === 0) { this.scrollToExact(0, 0); return; } for (let i = this.range.startIndex - 1; i >= 0; i--) { const lookupHeight = this.heightLookup['item' + i] ? this.heightLookup['item' + i] : this.avgItemHeight; heightCount += lookupHeight; if (heightCount >= this.containerHeight || i === 0) { const overflowDifference = heightCount >= this.containerHeight ? heightCount - this.containerHeight : 0; this.scrollToExact(i, overflowDifference); break; } } } pageDown() { this.range.startIndex = this.range.endIndex - 1; const overflowDifference = this.heightLookup['item' + (this.range.endIndex - 1).toString()] - this.lastVisibleItemOverflow; this.scrollToExact(this.range.startIndex, overflowDifference); } addArrowListeners() { this.elementRef.nativeElement.addEventListener('mouseenter', this.handleMouseOver = (e) => { this.mouseIsOverViewport = true; }); this.elementRef.nativeElement.addEventListener('mouseleave', this.handleMouseOut = (e) => { this.mouseIsOverViewport = false; }); document.addEventListener('keydown', this.handleKeyDown = (e) => { if (this.mouseIsOverViewport) { const isMac = this.operatingSystem.isMac(); switch (e.code) { case 'ArrowDown': if (isMac && e.metaKey) { e.preventDefault(); this.scrollToBottom(); } else { e.preventDefault(); this.range.startIndex += Number(this.arrowDownSpeed); this.scrollToExact(this.range.startIndex, 0); } break; case 'ArrowUp': if (isMac && e.metaKey) { e.preventDefault(); this.scrollToTop(); } else { if (this.scrollChangeByFirstIndexedItem === 0) { e.preventDefault(); this.range.startIndex -= Number(this.arrowUpSpeed); this.scrollToExact(this.range.startIndex, 0); } else { e.preventDefault(); this.scrollChangeByFirstIndexedItem = 0; this.scrollToExact(this.range.startIndex, 0); } } break; case 'PageDown': e.preventDefault(); this.pageDown(); break; case 'PageUp': e.preventDefault(); this.pageUp(); break; case 'End': e.preventDefault(); this.scrollToBottom(); break; case 'Home': e.preventDefault(); this.scrollToTop(); break; } } }); } ngAfterViewInit() { this.gridDirective = this.gridService.getParentTablejsGridDirective(this.elementRef.nativeElement)['gridDirective']; this.gridDirective.scrollViewportDirective = this; this.preGridInitializeSubscription$ = this.gridDirective.preGridInitialize.pipe(take(1)).subscribe(res => { this.cdr.detectChanges(); this.refreshContainerHeight(); this.refreshViewport(); // placeholder object is used only to initialize first grid render if (this.items[0] === this.placeholderObject) { this.items.shift(); } }); this.viewportRendered(); this.addArrowListeners(); } ngOnInit() { this.registerViewportToElement(); this._cloneMethod = this.generateCloneMethod; } ngOnDestroy() { this.listElm = null; if (this.virtualNexus && this.virtualNexus.virtualForDirective) { this.virtualNexus.virtualForDirective._viewContainer.detach(0); this.virtualNexus.virtualForDirective._viewContainer.clear(); } this.items = []; this.elementRef.nativeElement.scrollViewport = null; this.templateRef = null; this._cloneMethod = null; this.generateCloneMethod = null; if (this.virtualNexus) { this.directiveRegistrationService.clearVirtualNexus(this.virtualNexus); this.virtualNexus.virtualForDirective = null; this.virtualNexus.scrollViewportDirective = null; this.virtualNexus = null; } clearTimeout(this.timeoutID); this.elementRef.nativeElement.removeEventListener('mouseenter', this.handleMouseOver); this.elementRef.nativeElement.removeEventListener('mouseleave', this.handleMouseOut); if (this.listContent) { this.listContent.removeEventListener('scroll', this.handleListContentScroll); } this.handleListContentScroll = null; document.removeEventListener('keydown', this.handleKeyDown); if (this.virtualForChangesSubscription$) { this.virtualForChangesSubscription$.unsubscribe(); } if (this.preGridInitializeSubscription$) { this.preGridInitializeSubscription$.unsubscribe(); } this.elementRef.nativeElement.scrollViewportDirective = null; } setScrollSpacers() { const numItemsAfterShownList = this.items.length - this.range.extendedEndIndex; const numItemsBeforeShownList = this.adjustedStartIndex; const totalUnshownItems = numItemsBeforeShownList + numItemsAfterShownList; const beforeItemHeightPercent = totalUnshownItems !== 0 ? numItemsBeforeShownList / totalUnshownItems : 0; const afterItemHeightPercent = totalUnshownItems !== 0 ? numItemsAfterShownList / totalUnshownItems : 0; const remainingHeight = this.estimatedFullContentHeight - this.lastHeight; this.estimatedPreListHeight = Math.round(beforeItemHeightPercent * remainingHeight); this.estimatedPostListHeight = Math.round(afterItemHeightPercent * remainingHeight); // account for rounding both up this.estimatedPostListHeight = this.estimatedPostListHeight - (afterItemHeightPercent * remainingHeight) === 0.5 ? this.estimatedPostListHeight - 1 : this.estimatedPostListHeight; if (this.forcedEndIndex) { this.estimatedPreListHeight = 0; this.estimatedPostListHeight = 0; } this.prevSpacer.style.height = this.estimatedPreListHeight.toString() + 'px'; this.postSpacer.style.height = this.estimatedPostListHeight.toString() + 'px'; } setHeightByListHeightDifference(liHeight, listHeight) { return liHeight - listHeight; } removePreScrollItems(lastIndex, index) { if (lastIndex < index) { for (let i = lastIndex; i < index; i++) { const firstRef = this.virtualNexus.virtualForDirective._viewContainer.get(1); if (firstRef) { const firstChild = firstRef.rootNodes[0]; const itemName = 'item' + i; this.itemVisibilityLookup[itemName] = false; const detachedRef = this.virtualNexus.virtualForDirective._viewContainer.detach(1); detachedRef.destroy(); this.cdr.detectChanges(); this.removeResizeSensor(firstChild, i); this.lastHeight -= this.heightLookup[itemName]; this.scrollDispatcherService.dispatchRemoveItemEvents(this.itemRemoved, firstChild, i, this, this.elementRef.nativeElement); } } } } removePostScrollItems(lastEndIndex, endIndex) { if (lastEndIndex >= this.items.length) { lastEndIndex = this.items.length - 1; } for (let i = lastEndIndex; i >= endIndex; i--) { const lastChild = this.getPreviousSibling(this.listContent.lastElementChild); if (lastChild) { const itemName = 'item' + i; this.itemVisibilityLookup[itemName] = false; const detachedRef = this.virtualNexus.virtualForDirective._viewContainer.detach(this.virtualNexus.virtualForDirective._viewContainer.length - 1); detachedRef.destroy(); this.cdr.detectChanges(); this.removeResizeSensor(lastChild, i); this.lastHeight -= this.heightLookup[itemName]; this.scrollDispatcherService.dispatchRemoveItemEvents(this.itemRemoved, detachedRef.rootNodes[0], i, this, this.elementRef.nativeElement); } } } updateItems(items, scrollToOptions = { index: -1, scrollAfterIndexedItem: 0 }) { if (this.pauseViewportRenderUpdates) { return; } for (let i = this.virtualNexus.virtualForDirective._viewContainer.length - 1; i > 0; i--) { const detachedRef = this.virtualNexus.virtualForDirective._viewContainer.detach(i); detachedRef.destroy(); } this.cdr.detectChanges(); this.resetToInitialValues(); this.items = items; if (this.virtualNexus) { this.virtualNexus.virtualForDirective._tablejsForOf = items; } if (scrollToOptions.index !== -1) { this.scrollToExact(scrollToOptions.index, scrollToOptions.scrollAfterIndexedItem); } } resetToInitialValues() { this.lastScrollTop = 0; this.currentScrollTop = 0; this.currentScrollChange = 0; this.estimatedFullContentHeight = 0; this.estimatedPreListHeight = 0; this.estimatedPostListHeight = 0; this.totalItemsCounted = 0; this.totalHeightCount = 0; this.avgItemHeight = undefined; this.heightLookup = {}; this.itemVisibilityLookup = {}; this.overflowHeightCount = 0; this.scrollChangeByFirstIndexedItem = 0; this.lastVisibleItemHeight = Infinity; this.preOverflowHeight = 0; this.lastHeight = 0; this.range.startIndex = 0; this.range.endIndex = 0; this.range.extendedStartIndex = 0; this.range.extendedEndIndex = 0; this.lastRange.startIndex = this.range.startIndex; this.lastRange.endIndex = this.range.endIndex; this.lastRange.extendedStartIndex = this.range.extendedStartIndex; this.lastRange.extendedEndIndex = this.range.extendedEndIndex; this.forcedEndIndex = undefined; } recalculateRowHeight(index) { const itemName = 'item' + index; const indexMap = {}; for (let i = 1; i < this.virtualNexus.virtualForDirective._viewContainer.length; i++) { indexMap[this.virtualNexus.virtualForDirective._viewContainer.get(i).rootNodes[0].index] = i; } ; const rowRef = this.virtualNexus.virtualForDirective._viewContainer.get(indexMap[index]); const rowEl = rowRef.rootNodes[0]; const lookupHeight = rowEl.offsetHeight; const heightDifference = lookupHeight - this.heightLookup[itemName]; this.updateEstimatedHeightFromResize(this.heightLookup[itemName], lookupHeight); this.heightLookup[itemName] = lookupHeight; rowEl.lastHeight = lookupHeight; this.lastHeight += heightDifference; } updateEstimatedHeightFromResize(oldHeight, newHeight) { this.totalHeightCount += (newHeight - oldHeight); this.avgItemHeight = (this.totalHeightCount / this.totalItemsCounted); this.estimatedFullContentHeight = this.avgItemHeight * this.items.length; } updateEstimatedHeight(height) { this.totalHeightCount += height; this.totalItemsCounted++; this.avgItemHeight = (this.totalHeightCount / this.totalItemsCounted); this.estimatedFullContentHeight = this.avgItemHeight * this.items.length; } getPreviousSibling(el) { if (!el) { return null; } let prev = el.previousSibling; while (prev !== null && prev !== undefined && prev.nodeType !== 1) { prev = prev.previousSibling; } return prev; } getNextSibling(el) { if (!el) { return null; } let next = el.nextSibling; while (next !== null && next !== undefined && next.nodeType !== 1) { next = next.nextSibling; } return next; } getEstimatedChildInsertions(remainingHeight) { return Math.ceil(remainingHeight / this.avgItemHeight); } setLastRangeToCurrentRange() { this.lastRange.startIndex = this.range.startIndex; this.lastRange.endIndex = this.range.endIndex; this.lastRange.extendedStartIndex = this.range.extendedStartIndex; this.lastRange.extendedEndIndex = this.range.extendedEndIndex; } resetLastHeight() { if (!this.lastHeight) { this.lastHeight = 0; } } maintainIndexInBounds(index) { if (index > this.items.length - 1) { index = this.items.length - 1; } else if (index < 0) { index = 0; } return index; } maintainEndIndexInBounds(index) { if (index > this.items.length) { index = this.items.length; } else if (index < 0) { index = 0; } return index; } showRange(startIndex, endIndex, overflow = 0) { this.updateItems(this.items, { index: startIndex, scrollAfterIndexedItem: endIndex }); startIndex = this.maintainIndexInBounds(startIndex); endIndex = this.maintainEndIndexInBounds(endIndex); if (endIndex <= startIndex) { endIndex = startIndex + 1; } const oldContainerHeight = this.containerHeight; const oldPreItemOverflow = Number(this.preItemOverflow); const oldPostItemOverflow = Number(this.postItemOverflow); this.preItemOverflow = 0; this.postItemOverflow = 0; this.containerHeight = 100000; this.forcedEndIndex = endIndex; this.scrollToExact(startIndex, overflow); const rangeToKeep = { ...this.range }; const lastRangeToKeep = { ...this.lastRange }; this.preItemOverflow = oldPreItemOverflow; this.postItemOverflow = oldPostItemOverflow; this.containerHeight = oldContainerHeight; this.forcedEndIndex = undefined; this.range = rangeToKeep; this.lastRange = lastRangeToKeep; } getDisplayedContentsHeight() { return this.lastHeight; } refreshContainerHeight() { this.containerHeight = this.listContent.clientHeight; } allItemsFitViewport(recalculateContainerHeight = false, refreshViewport = false) { if (recalculateContainerHeight) { this.cdr.detectChanges(); this.refreshContainerHeight(); } if (refreshViewport) { this.refreshViewport(true); } return this.range.startIndex === this.range.extendedStartIndex && this.range.endIndex === this.range.extendedEndIndex && this.lastHeight <= this.containerHeight; } getCurrentScrollPosition() { return { index: this.range.startIndex, overflow: this.scrollChangeByFirstIndexedItem, lastItemOverflow: this.lastVisibleItemOverflow > 0 ? 0 : this.lastVisibleItemOverflow }; } setHeightsForOverflowCalculations(itemIndex, scrollToIndex, itemHeight) { this.lastHeight += itemHeight; if (itemIndex < scrollToIndex) { this.preOverflowHeight += itemHeight; } if (itemIndex >= scrollToIndex) { this.overflowHeightCount += itemHeight; if (this.overflowHeightCount >= this.containerHeight) { this.postItemOverflowCount++; if (this.postItemOverflowCount === 0) { this.lastVisibleItemHeight = this.heightLookup['item' + itemIndex]; } } } } addResizeSensor(el, index) { } removeResizeSensor(el, index) { } getCloneFromTemplateRef(index) { let clone; this.templateContext = new TablejsForOfContext(this.items[index], this.virtualNexus.virtualForDirective._tablejsForOf, index, this.items.length); const viewRef = this.virtualNexus.virtualForDirective._template.createEmbeddedView(this.templateContext); viewRef.detectChanges(); clone = viewRef.rootNodes[0]; return clone; } addScrollItems(index, overflow) { const scrollingUp = index < this.lastRange.startIndex; this.range.extendedStartIndex = this.adjustedStartIndex; this.range.startIndex = index; this.overflowHeightCount = -overflow; this.preOverflowHeight = 0; const firstEl = this.getNextSibling(this.listContent.firstElementChild); this.lastHeight = 0; let batchSize = this.avgItemHeight !== undefined && isNaN(this.avgItemHeight) === false ? this.getEstimatedChildInsertions(this.containerHeight - this.lastHeight) + Number(this.preItemOverflow) + Number(this.postItemOverflow) : 1; let itemsToBatch = []; let itemBefore; let indexBefore; const firstRef = this.virtualNexus.virtualForDirective._viewContainer.get(1); const appendToEnd = firstRef === null; for (let i = this.adjustedStartIndex; i < this.adjustedStartIndex + Number(this.itemLoadLimit); i++) { if (i < 0) { continue; } if (i > this.items.length - 1) { break; } this.itemName = 'item' + i; // only insert item if it is not already visible const itemIsInvisible = this.itemVisibilityLookup[this.itemName] !== true; if (itemIsInvisible) { itemBefore = !scrollingUp ? this.postSpacer : firstEl; indexBefore = !scrollingUp || appendToEnd ? this.virtualNexus.virtualForDirective._viewContainer.length : this.virtualNexus.virtualForDirective._viewContainer.indexOf(firstRef); this.itemVisibilityLookup[this.itemName] = true; this.templateContext = new TablejsForOfContext(this.items[i], this.virtualNexus.virtualForDirective._tablejsForOf, i, this.items.length); const ref = this.virtualNexus.virtualForDirective._viewContainer.createEmbeddedView(this.virtualNexus.virtualForDirective._template, this.templateContext, indexBefore); this.virtualNexus.virtualForDirective._viewContainer.move(ref, indexBefore); const prev = ref.rootNodes[0]; prev.index = i; itemsToBatch.push({ index: i, name: this.itemName, item: prev, before: itemBefore }); this.scrollDispatcherService.dispatchAddItemEvents(this.itemAdded, prev, i, this, this.elementRef.nativeElement); } else { itemsToBatch.push({ index: i, name: this.itemName, item: null, before: null }); this.setHeightsForOverflowCalculations(i, index, this.heightLookup[this.itemName]); } if (itemsToBatch.length === batchSize || i === this.items.length - 1 || this.postItemOverflowCount >= Number(this.postItemOverflow)) { for (let j = 0; j < itemsToBatch.length; j++) { const batchObj = itemsToBatch[j]; const name = batchObj.name; const ind = batchObj.index; const oldHeight = this.heightLookup[name]; if (batchObj.item === null) { continue; } this.cdr.detectChanges(); const lookupHeight = batchObj.item.offsetHeight; this.heightLookup[name] = lookupHeight; batchObj.item.lastHeight = lookupHeight; this.addResizeSensor(batchObj.item, batchObj.index); if (oldHeight) { this.updateEstimatedHeightFromResize(oldHeight, lookupHeight); } else { this.updateEstimatedHeight(lookupHeight); } this.setHeightsForOverflowCalculations(ind, index, lookupHeight); } batchSize = this.getEstimatedChildInsertions(this.containerHeight - this.lastHeight) + Number(this.preItemOverflow) + Number(this.postItemOverflow); if (batchSize <= 0) { batchSize = Number(this.postItemOverflow); } itemsToBatch = []; } if (this.postItemOverflowCount <= 0) { this.range.endIndex = i + 1; } this.range.extendedEndIndex = i + 1; // if item height is lower than the bottom of the container area, stop adding items if (this.forcedEndIndex === undefined) { if (this.postItemOverflowCount >= Number(this.postItemOverflow)) { break; } } else { if (i === this.forcedEndIndex - 1) { break; } } } let itemName; let endIndexFound = false; let heightCount = -overflow; for (let i = this.range.startIndex; i < this.range.extendedEndIndex; i++) { itemName = 'item' + i; heightCount += this.heightLookup[itemName]; if (this.forcedEndIndex !== undefined) { if (i === this.forcedEndIndex - 1) { this.range.endIndex = i + 1; this.lastVisibleItemOverflow = heightCount - this.containerHeight; endIndexFound = true; break; } } else { if (heightCount >= this.containerHeight && !endIndexFound) { this.range.endIndex = i + 1; this.lastVisibleItemOverflow = heightCount - this.containerHeight; endIndexFound = true; break; } } } } addMissingPostScrollItemsAndUpdateOverflow(index, overflow) { let firstEl; let itemsToBatch = []; let batchSize; if (this.overflowHeightCount <= this.containerHeight) { batchSize = this.getEstimatedChildInsertions(this.containerHeight) + Number(this.preItemOverflow); this.preItemOverflowCount = -1; this.preOverflowHeight = 0; firstEl = this.getNextSibling(this.listContent.firstElementChild); let heightCount = 0; let count = 0; for (let i = this.range.endIndex - 1; i >= 0; i--) { this.itemName = 'item' + i; count++; if (i <= this.range.extendedStartIndex && this.itemVisibilityLookup[this.itemName] !== true) { this.itemVisibilityLookup[this.itemName] = true; this.templateContext = new TablejsForOfContext(this.items[i], this.virtualNexus.virtualForDirective._tablejsForOf, i, this.items.length); const ref = this.virtualNexus.virtualForDirective._viewContainer.createEmbeddedView(this.virtualNexus.virtualForDirective._template, this.templateContext, 1); this.virtualNexus.virtualForDirective._viewContainer.move(ref, 1); const prev = ref.rootNodes[0]; prev.index = i; this.cdr.detectChanges(); itemsToBatch.push({ index: i, name: this.itemName, item: prev, before: firstEl }); this.scrollDispatcherService.dispatchAddItemEvents(this.itemAdded, prev, i, this, this.elementRef.nativeElement); firstEl = prev; this.range.extendedStartIndex = i; this.adjustedStartIndex = i; } else { itemsToBatch.push({ index: i, name: this.itemName, item: null, before: null }); heightCount += this.heightLookup[this.itemName]; if (heightCount > this.containerHeight) { this.preItemOverflowCount++; if (this.preItemOverflowCount === 0) { overflow = heightCount - this.containerHeight; this.range.startIndex = i; index = i; } else { this.preOverflowHeight += this.heightLookup[this.itemName]; } this.range.extendedStartIndex = i; this.adjustedStartIndex = i; } } if (itemsToBatch.length === batchSize || i === 0) { for (let j = 0; j < itemsToBatch.length; j++) { const batchObj = itemsToBatch[j]; if (batchObj.item === null) { continue; } const name = batchObj.name; const ind = batchObj.index; const lookupHeight = batchObj.item.offsetHeight; const oldHeight = this.heightLookup[name]; this.heightLookup[name] = lookupHeight; batchObj.item.lastHeight = lookupHeight; this.addResizeSensor(batchObj.item, batchObj.index); if (oldHeight) { this.updateEstimatedHeightFromResize(oldHeight, lookupHeight); } else { this.updateEstimatedHeight(lookupHeight); } heightCount += lookupHeight; if (heightCount > this.containerHeight) { this.preItemOverflowCount++; if (this.preItemOverflowCount === 0) { overflow = heightCount - this.containerHeight; this.range.startIndex = batchObj.index; index = batchObj.index; } else { this.preOverflowHeight += lookupHeight; } this.range.extendedStartIndex = batchObj.index; this.adjustedStartIndex = batchObj.index; } } batchSize = this.getEstimatedChildInsertions(this.containerHeight - this.lastHeight) + Number(this.preItemOverflow); if (batchSize <= 0) { batchSize = Number(this.preItemOverflow); } itemsToBatch = []; } if (this.preItemOverflowCount >= Number(this.preItemOverflow)) { break; } } } return overflow; } scrollToExact(index, overflow = 0) { if (!this.items || this.items.length === 0) { return; } this.resetLastHeight(); index = this.maintainIndexInBounds(index); overflow = index === 0 && overflow < 0 ? 0 : overflow; this.adjustedStartIndex = index - Number(this.preItemOverflow) <= 0 ? 0 : index - Number(this.preItemOverflow); this.preItemOverflowCount = -1; this.postItemOverflowCount = -1; this.lastVisibleItemOverflow = 0; this.range.endIndex = 0; this.range.extendedEndIndex = 0; this.removePreScrollItems(this.lastRange.extendedStartIndex, Math.min(this.adjustedStartIndex, this.lastRange.extendedEndIndex)); this.addScrollItems(index, overflow); this.removePostScrollItems(this.lastRange.extendedEndIndex - 1, Math.max(this.lastRange.extendedStartIndex, this.range.extendedEndIndex)); if (!this.forcedEndIndex) { overflow = this.addMissingPostScrollItemsAndUpdateOverflow(index, overflow); } this.setLastRangeToCurrentRange(); this.setScrollSpacers(); this.lastScrollTop = this.preOverflowHeight + overflow + this.estimatedPreListHeight; this.listContent.scrollTop = this.lastScrollTop; this.currentScrollTop = this.lastScrollTop; this.scrollChangeByFirstIndexedItem = overflow; this.scrollDispatcherService.dispatchRangeUpdateEvents(this.rangeUpdated, this.range, this, this.elementRef.nativeElement); this.viewportHasScrolled = true; } getRangeChange(scrollChange) { let heightCount = 0; let rangeStartCount = 0; let overflow = 0; const newRange = { startIndex: null, endIndex: null, extendedStartIndex: null, extendedEndIndex: null }; let itemName; if (scrollChange > 0) { for (let i = this.range.startIndex; i <= this.range.endIndex + Number(this.itemLoadLimit); i++) { overflow = scrollChange - heightCount; itemName = 'item' + i; if (this.heightLookup[itemName]) { heightCount += this.heightLookup[itemName]; } else { heightCount += this.avgItemHeight; } if (heightCount >= scrollChange) { break; } rangeStartCount++; } newRange.startIndex = this.range.startIndex + rangeStartCount; newRange.endIndex = rangeStartCount < this.range.endIndex - this.range.startIndex ? this.range.endIndex : newRange.startIndex + 1; } if (scrollChange < 0) { rangeStartCount = -1; overflow = scrollChange; for (let i = this.range.startIndex - 1; i >= 0; i--) { itemName = 'item' + i; if (this.heightLookup[itemName]) { overflow += this.heightLookup[itemName]; heightCount += this.heightLookup[itemName]; } else { overflow += this.avgItemHeight; heightCount += this.avgItemHeight; } if (overflow >= 0) { break; } rangeStartCount--; } newRange.startIndex = this.range.startIndex + rangeStartCount >= 0 ? this.range.startIndex + rangeStartCount : 0; newRange.endIndex = rangeStartCount < this.range.endIndex - this.range.startIndex ? this.range.endIndex : newRange.startIndex + 1; } this.scrollChangeByFirstIndexedItem = overflow; return newRange; } refreshViewport(recalculateRows = false) { if (recalculateRows) { for (let i = this.range.extendedStartIndex; i < this.range.extendedEndIndex; i++) { this.recalculateRowHeight(i); } } this.scrollToExact(this.range.startIndex, this.scrollChangeByFirstIndexedItem); } updateScrollFromRange(newRange) { if (newRange.startIndex !== null) { if (this.range.startIndex !== newRange.startIndex || this.lastVisibleItemOverflow < 0) { this.range.startIndex = newRange.startIndex; this.range.endIndex = newRange.endIndex; this.refreshViewport(); } else { this.lastScrollTop = this.currentScrollTop; } } this.lastScrollTop = this.currentScrollTop; } initScroll(options) { this.items = options.items; this._cloneMethod = options.generateCloneMethod; const itemsAreEmpty = this.items.length === 0; let index = options.initialIndex ? options.initialIndex : 0; if (this.virtualNexus && this.virtualNexus.virtualForDirective._template) { clearTimeout(this.timeoutID); this.timeoutID = setTimeout(() => { this.cloneFromTemplateRef = true; this.verifyViewportIsReady(); this.initFirstScroll(index); }); } else { this.template = document.getElementById(this.templateID); this.verifyViewportIsReady(); this.initFirstScroll(index); } } verifyViewportIsReady() { if (this.templateID === '' && !this.templateIsSet()) { throw Error('Scroll viewport template ID is not set.'); } if (!this.itemsAreSet()) { throw new Error('Scroll viewport requires an array of items. Please supply an items array.'); } if (!this.cloneMethodIsSet() && !this.templateIsSet()) { throw new Error('Scroll viewport requires a cloning method or a template. Please supply a method as follows:\n\n (template: HTMLElement, items: any[], index: number) => Node\n\n or supply a tablejsVirtualFor'); } } initFirstScroll(index) { const itemsAreEmpty = this.items.length === 0; this.refreshContainerHeight(); if (itemsAreEmpty) { this.items.push(this.placeholderObject); this.scrollToExact(index, 0); const node = this.virtualNexus.virtualForDirective._viewContainer.get(1).rootNodes[0]; this.renderer.setStyle(node, 'height', '0px'); this.renderer.setStyle(node, 'minHeight', '0px'); this.renderer.setStyle(node, 'overflow', 'hidden'); } else { this.scrollToExact(index, 0); } this.scrollDispatcherService.dispatchViewportInitializedEvents(this.viewportInitialized, this, this.elementRef.nativeElement); } itemsAreSet() { return !!this.items; } cloneMethodIsSet() { return !!this._cloneMethod; } templateIsSet() { return this.virtualNexus.virtualForDirective._template !== undefined && this.virtualNexus.virtualForDirective._template !== null; } } ScrollViewportDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: ScrollViewportDirective, deps: [{ token: i0.ElementRef }, { token: i1.GridService }, { token: DOCUMENT }, { token: i2.DirectiveRegistrationService }, { token: i3.ScrollDispatcherService }, { token: i4.OperatingSystemService }, { token: i0.ChangeDetectorRef }, { token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Directive }); ScrollViewportDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.0.3", type: Sc