thingsboard/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts
2023-11-21 11:27:37 +02:00

261 lines
8.2 KiB
TypeScript

///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DataSource, ListRange } from '@angular/cdk/collections';
import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { BreakpointObserver } from '@angular/cdk/layout';
import { resolveBreakpoint } from '@shared/models/constants';
export type GridEntitiesFetchFunction<T, F> = (pageSize: number, page: number, filter: F) => Observable<PageData<T>>;
export type GridCellType = 'emptyCell' | 'loadingCell';
export interface ScrollGridColumns {
columns: number;
breakpoints?: {[breakpoint: string]: number};
}
export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]> {
public initialDataLoading = true;
private _data: T[] = [];
private _rows: (T | GridCellType)[][] = Array.from<T[]>({length: 100000});
private _hasNext = true;
private _columns: number;
private _viewport: CdkVirtualScrollViewport;
private _pendingRange: ListRange = null;
private _fetchingData = false;
private _fetchSubscription: Subscription;
private _totalElements = 0;
private _dataStream: BehaviorSubject<(T | GridCellType)[][]>;
private _subscription: Subscription;
constructor(private breakpointObserver: BreakpointObserver,
private columns: ScrollGridColumns | number,
private fetchFunction: GridEntitiesFetchFunction<T, F>,
private filter: F) {
super();
}
connect(collectionViewer: CdkVirtualForOf<(T | GridCellType)[]>): Observable<(T | GridCellType)[][]> {
this._viewport = (collectionViewer as any)._viewport;
this._init();
if (typeof this.columns === 'object' && this.columns.breakpoints) {
const breakpoints = Object.keys(this.columns.breakpoints);
this._subscription.add(this.breakpointObserver.observe(breakpoints.map(breakpoint => resolveBreakpoint(breakpoint))).subscribe(
() => {
this._columnsChanged(this._detectColumns());
}
));
}
this._subscription.add(
collectionViewer.viewChange.subscribe(range => this._fetchDataFromRange(range))
);
return this._dataStream;
}
disconnect(): void {
this._reset();
this._subscription.unsubscribe();
}
get isEmpty(): boolean {
return !this._data.length;
}
get active(): boolean {
return !!this._subscription && !this._subscription.closed;
}
public updateFilter(filter: F) {
this.filter = filter;
this.update();
}
public update() {
if (this.active) {
const prevLength = this._rows.length;
this._reset();
const dataLengthChanged = prevLength !== this._rows.length;
const range = this._viewport.getRenderedRange();
if (dataLengthChanged) {
// Force recalculate new range
if (range.start === 0) {
range.start = 1;
}
this._viewport.appendOnly = false;
}
const scrollOffset = this._viewport.measureScrollOffset();
if (scrollOffset > 0) {
this._viewport.scrollToOffset(0);
}
this._dataUpdated();
this._viewport.appendOnly = true;
if (!dataLengthChanged) {
this._fetchDataFromRange(range);
}
}
}
private _detectColumns(): number {
if (typeof this.columns !== 'object') {
return this.columns;
} else {
let columns = this.columns.columns;
if (this.columns.breakpoints) {
for (const breakpoint of Object.keys(this.columns.breakpoints)) {
const breakpointValue = resolveBreakpoint(breakpoint);
if (this.breakpointObserver.isMatched(breakpointValue)) {
columns = this.columns.breakpoints[breakpoint];
break;
}
}
}
return columns;
}
}
private _init() {
this._subscription = new Subscription();
this._columns = this._detectColumns();
if (this._dataStream) {
this._dataStream.complete();
}
this._dataStream = new BehaviorSubject(this._rows);
}
private _reset() {
this._data = [];
this._totalElements = 0;
this.initialDataLoading = true;
this._rows = Array.from<T[]>({length: 100000});
this._hasNext = true;
this._pendingRange = null;
this._fetchingData = false;
if (this._fetchSubscription) {
this._fetchSubscription.unsubscribe();
}
}
private _columnsChanged(columns: number) {
if (this._columns !== columns) {
const fetchData = columns > this._columns;
this._columns = columns;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
if (fetchData && this._hasNext) {
this._fetchDataFromRange(this._viewport.getRenderedRange());
}
}
}
private _fetchDataFromRange(range: ListRange) {
if (this._hasNext) {
if (this._fetchingData) {
this._pendingRange = range;
} else {
const endIndex = (range.end + 1) * this._columns;
if (endIndex > this._data.length) {
const startIndex = this._data.length;
const minPageSize = endIndex - startIndex;
const maxPageSize = minPageSize * 2;
let pageSize = minPageSize;
let page = Math.floor(startIndex / pageSize);
while (startIndex % pageSize !== 0 && pageSize <= maxPageSize) {
if (((page + 1) * pageSize) > endIndex) {
break;
}
pageSize++;
page = Math.floor(startIndex / pageSize);
}
const offset = startIndex % pageSize;
this._fetchData(offset, pageSize, page);
}
}
}
}
private _fetchData(offset: number, pageSize: number, page: number) {
this._fetchingData = true;
this._fetchSubscription = this.fetchFunction(pageSize, page, this.filter).pipe(
catchError(() => of(emptyPageData<T>()))
).subscribe(
(data) => {
this._hasNext = data.hasNext;
if (data.data.length > offset) {
for (let i = offset; i < data.data.length; i++) {
this._data.push(data.data[i]);
}
}
this._totalElements = data.totalElements;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
this.initialDataLoading = false;
this._fetchingData = false;
if (this._pendingRange) {
const range = this._pendingRange;
this._pendingRange = null;
this._fetchDataFromRange(range);
}
}
);
}
private _dataUpdated() {
for (let index = 0; index < this._data.length; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = this._data[index];
}
this._fillGridCells();
this._dataStream.next(this._rows);
}
private _fillGridCells() {
if (this._totalElements) {
const startIndex = this._data.length;
const endIndex = this._rows.length * this._columns;
for (let index = startIndex; index < endIndex; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
const cellType: GridCellType = index < this._totalElements ? 'loadingCell' : 'emptyCell';
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = cellType;
}
}
}
}