ng-cdk-table-virtual-scroll
Version:
Virtual scroll for for Angular Cdk Table
325 lines (317 loc) • 12.3 kB
JavaScript
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