UNPKG

ng-cdk-table-virtual-scroll

Version:
325 lines (317 loc) 12.3 kB
import { __decorate } from 'tslib'; import { Injectable, NgZone, Input, ContentChild, Directive, forwardRef, NgModule } from '@angular/core'; import { distinctUntilChanged, filter, tap, takeWhile, takeUntil, switchMap, map } from 'rxjs/operators'; import { BehaviorSubject, Subscription, ReplaySubject, Subject } from 'rxjs'; import { VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling'; import { DataSource, CdkTable } from '@angular/cdk/table'; class TableVirtualScrollDataSource extends DataSource { constructor(initialData = []) { super(); /** Stream emitting render data to the table (depends on ordered data changes). */ this._renderData = new BehaviorSubject([]); /** * Subscription to the changes that should trigger an update to the table's rendered rows, such * as filtering, sorting, pagination, or base data changes. */ this._renderChangesSubscription = null; this._data = new BehaviorSubject(initialData); this.updateChangeSubscription(); } /** Array of data that should be rendered by the table, where each object represents one row. */ get data() { return this._data.value; } set data(data) { this._data.next(data); } updateChangeSubscription() { this.initStreams(); this._renderChangesSubscription = new Subscription(); this._renderChangesSubscription.add(this._data.subscribe((data) => this.dataToRender$.next(data))); this._renderChangesSubscription.add(this.dataOfRange$.subscribe((data) => this._renderData.next(data))); } initStreams() { if (!this.streamsReady) { this.dataToRender$ = new ReplaySubject(1); this.dataOfRange$ = new ReplaySubject(1); this.streamsReady = true; } } /** * Used by the MatTable. Called when it connects to the data source. * @private */ connect() { if (!this._renderChangesSubscription) { this.updateChangeSubscription(); } return this._renderData; } /** * Used by the MatTable. Called when it disconnects from the data source. * @private */ disconnect() { if (this._renderChangesSubscription) { this._renderChangesSubscription.unsubscribe(); } this._renderChangesSubscription = null; } } let FixedSizeTableVirtualScrollStrategy = class FixedSizeTableVirtualScrollStrategy { constructor() { this.indexChange = new Subject(); this.stickyChange = new Subject(); this.renderedRangeStream = new BehaviorSubject({ start: 0, end: 0 }); this.scrolledIndexChange = this.indexChange.pipe(distinctUntilChanged()); this._dataLength = 0; } get dataLength() { return this._dataLength; } set dataLength(value) { this._dataLength = value; this.onDataLengthChanged(); } attach(viewport) { this.viewport = viewport; this.viewport.renderedRangeStream.subscribe(this.renderedRangeStream); this.onDataLengthChanged(); } detach() { this.indexChange.complete(); this.stickyChange.complete(); this.renderedRangeStream.complete(); } onContentScrolled() { this.updateContent(); } onDataLengthChanged() { if (this.viewport) { this.viewport.setTotalContentSize(this.dataLength * this.rowHeight + this.headerHeight + this.footerHeight); } this.updateContent(); } onContentRendered() { // no-op } onRenderedOffsetChanged() { // no-op } scrollToIndex(index, behavior) { if (!this.viewport || !this.rowHeight) { return; } this.viewport.scrollToOffset((index - 1) * this.rowHeight + this.headerHeight); } setConfig(configs) { const { rowHeight, headerHeight, footerHeight, bufferMultiplier } = configs; if (this.rowHeight === rowHeight && this.headerHeight === headerHeight && this.footerHeight === footerHeight && this.bufferMultiplier === bufferMultiplier) { return; } this.rowHeight = rowHeight; this.headerHeight = headerHeight; this.footerHeight = footerHeight; this.bufferMultiplier = bufferMultiplier; this.onDataLengthChanged(); } updateContent() { if (!this.viewport || !this.rowHeight) { return; } const scrollOffset = this.viewport.measureScrollOffset(); const amount = Math.ceil(this.viewport.getViewportSize() / this.rowHeight); const offset = Math.max(scrollOffset - this.headerHeight, 0); const buffer = Math.ceil(amount * this.bufferMultiplier); const skip = Math.round(offset / this.rowHeight); const index = Math.max(0, skip); const start = Math.max(0, index - buffer); const end = Math.min(this.dataLength, index + amount + buffer); const renderedOffset = start * this.rowHeight; this.viewport.setRenderedContentOffset(renderedOffset); this.viewport.setRenderedRange({ start, end }); this.indexChange.next(index); this.stickyChange.next(renderedOffset); } }; FixedSizeTableVirtualScrollStrategy = __decorate([ Injectable() ], FixedSizeTableVirtualScrollStrategy); var TableItemSizeDirective_1; function _tableVirtualScrollDirectiveStrategyFactory(tableDir) { return tableDir.scrollStrategy; } const stickyHeaderSelector = '.cdk-header-row .cdk-table-sticky'; const stickyFooterSelector = '.cdk-footer-row .cdk-table-sticky'; const defaults = { rowHeight: 48, headerHeight: 56, headerEnabled: true, footerHeight: 48, footerEnabled: false, bufferMultiplier: 0.7, }; let TableItemSizeDirective = TableItemSizeDirective_1 = class TableItemSizeDirective { constructor(zone) { this.zone = zone; this.alive = true; // tslint:disable-next-line:no-input-rename this.rowHeight = defaults.rowHeight; this.headerEnabled = defaults.headerEnabled; this.headerHeight = defaults.headerHeight; this.footerEnabled = defaults.footerEnabled; this.footerHeight = defaults.footerHeight; this.bufferMultiplier = defaults.bufferMultiplier; this.scrollStrategy = new FixedSizeTableVirtualScrollStrategy(); this.dataSourceChanges = new Subject(); } ngOnDestroy() { this.alive = false; this.dataSourceChanges.complete(); } isAlive() { return () => this.alive; } isStickyEnabled() { return (!!this.scrollStrategy.viewport && this.table['_headerRowDefs'] .map((def) => def.sticky) .reduce((prevState, state) => prevState && state, true)); } ngAfterContentInit() { const switchDataSourceOrigin = this.table['_switchDataSource']; this.table['_switchDataSource'] = (dataSource) => { switchDataSourceOrigin.call(this.table, dataSource); this.connectDataSource(dataSource); }; this.connectDataSource(this.table.dataSource); this.scrollStrategy.stickyChange .pipe(filter(() => this.isStickyEnabled()), tap(() => { if (!this.stickyPositions) { this.initStickyPositions(); } }), takeWhile(this.isAlive())) .subscribe((stickyOffset) => { this.setSticky(stickyOffset); }); } connectDataSource(dataSource) { this.dataSourceChanges.next(); if (dataSource instanceof TableVirtualScrollDataSource) { dataSource.dataToRender$ .pipe(distinctUntilChanged(), takeUntil(this.dataSourceChanges), takeWhile(this.isAlive()), tap((data) => (this.scrollStrategy.dataLength = data.length)), switchMap((data) => this.scrollStrategy.renderedRangeStream.pipe(map(({ start, end }) => typeof start !== 'number' || typeof end !== 'number' ? data : data.slice(start, end))))) .subscribe((data) => { this.zone.run(() => { dataSource.dataOfRange$.next(data); }); }); } else { throw new Error('[tvsItemSize] requires TableVirtualScrollDataSource be set as [dataSource] of [cdk-table]'); } } ngOnChanges() { const config = { rowHeight: +this.rowHeight || defaults.rowHeight, headerHeight: this.headerEnabled ? +this.headerHeight || defaults.headerHeight : 0, footerHeight: this.footerEnabled ? +this.footerHeight || defaults.footerHeight : 0, bufferMultiplier: +this.bufferMultiplier || defaults.bufferMultiplier, }; this.scrollStrategy.setConfig(config); } setSticky(offset) { this.scrollStrategy.viewport.elementRef.nativeElement .querySelectorAll(stickyHeaderSelector) .forEach((el) => { const parent = el.parentElement; let baseOffset = 0; if (this.stickyPositions.has(parent)) { baseOffset = this.stickyPositions.get(parent); } el.style.top = `${baseOffset - offset}px`; }); this.scrollStrategy.viewport.elementRef.nativeElement .querySelectorAll(stickyFooterSelector) .forEach((el) => { const parent = el.parentElement; let baseOffset = 0; if (this.stickyPositions.has(parent)) { baseOffset = this.stickyPositions.get(parent); } el.style.bottom = `${-baseOffset + offset}px`; }); } initStickyPositions() { this.stickyPositions = new Map(); this.scrollStrategy.viewport.elementRef.nativeElement .querySelectorAll(stickyHeaderSelector) .forEach((el) => { const parent = el.parentElement; if (!this.stickyPositions.has(parent)) { this.stickyPositions.set(parent, parent.offsetTop); } }); } }; TableItemSizeDirective.ctorParameters = () => [ { type: NgZone } ]; __decorate([ Input('tvsItemSize') ], TableItemSizeDirective.prototype, "rowHeight", void 0); __decorate([ Input() ], TableItemSizeDirective.prototype, "headerEnabled", void 0); __decorate([ Input() ], TableItemSizeDirective.prototype, "headerHeight", void 0); __decorate([ Input() ], TableItemSizeDirective.prototype, "footerEnabled", void 0); __decorate([ Input() ], TableItemSizeDirective.prototype, "footerHeight", void 0); __decorate([ Input() ], TableItemSizeDirective.prototype, "bufferMultiplier", void 0); __decorate([ ContentChild(CdkTable, { static: false }) ], TableItemSizeDirective.prototype, "table", void 0); TableItemSizeDirective = TableItemSizeDirective_1 = __decorate([ Directive({ selector: 'cdk-virtual-scroll-viewport[tvsItemSize]', providers: [ { provide: VIRTUAL_SCROLL_STRATEGY, useFactory: _tableVirtualScrollDirectiveStrategyFactory, deps: [forwardRef(() => TableItemSizeDirective_1)], }, ], }) ], TableItemSizeDirective); let TableVirtualScrollModule = class TableVirtualScrollModule { }; TableVirtualScrollModule = __decorate([ NgModule({ declarations: [TableItemSizeDirective], exports: [TableItemSizeDirective], imports: [], }) ], TableVirtualScrollModule); /* * Public API Surface of ng-fixed-size-table-virtual-scroll */ /** * Generated bundle index. Do not edit. */ export { FixedSizeTableVirtualScrollStrategy, TableItemSizeDirective, TableVirtualScrollDataSource, TableVirtualScrollModule, _tableVirtualScrollDirectiveStrategyFactory }; //# sourceMappingURL=ng-cdk-table-virtual-scroll.js.map