@sparser/au2-data-grid
Version:
A data grid for Aurelia 2
301 lines (269 loc) • 9.79 kB
text/typescript
import {
ILogger,
noop,
} from '@aurelia/kernel';
import {
observable,
} from '@aurelia/runtime';
import {
SortOption,
} from './sorting-options.js';
import {
Writable
} from './util.js';
const defaultPageSize = 50;
/**
* Handles the data part of the grid.
* This has very little to do with the presentation of the data.
*/
export class ContentModel<T> {
public readonly isAnySelected: boolean = false;
public readonly isOneSelected: boolean = false;
public readonly selectionCount: number = 0;
public allItems: T[] | null;
public readonly selectedItems: T[] = [];
public readonly selectionMode: ItemSelectionMode;
private readonly onSelectionChange: SelectionChangeHandler<T>;
private readonly pageSize: number | null;
private readonly fetchPage: FetchPage<T> | null;
private readonly fetchCount: FetchCount<T> | null;
private readonly logger: ILogger;
private _currentPage!: T[];
private _currentPageNumber: number = 0;
private pagePromise: Promise<void> | null = null;
private countPromise: Promise<void> | null = null;
private _totalCount: number = undefined!;
private _sortOptions: SortOption<T>[] = [];
private _pageCount: number = undefined!;
private initialized: boolean = false;
public constructor(
allItems: T[] | null,
pagingOptions: Partial<PagingOptions<T>> | null,
selectionOptions: Partial<SelectionOptions<T>> | null,
public readonly onSorting: ApplySorting<T> | null,
logger: ILogger,
) {
this.logger = logger.scopeTo('GridModel');
this.allItems = allItems;
const fetchPage = this.fetchPage = pagingOptions?.fetchPage ?? null;
this.fetchCount = pagingOptions?.fetchCount ?? null;
const hasAllItems = allItems !== null;
if (!hasAllItems && fetchPage === null) throw new Error('Either allItems or pagingOptions is required.');
const pageSize = pagingOptions?.pageSize;
const isPagingDisabled = pagingOptions === null || pageSize === null;
this.pageSize = isPagingDisabled ? null : (pageSize ?? defaultPageSize);
if (isPagingDisabled && hasAllItems) {
this._currentPage = allItems!;
this._totalCount = allItems.length;
}
this.selectionMode = selectionOptions?.mode ?? ItemSelectionMode.None;
this.onSelectionChange = selectionOptions?.onSelectionChange ?? noop as SelectionChangeHandler<T>;
this.initialized = true;
}
public get currentPage(): T[] { return this._currentPage; }
public get totalCount(): number { return this._totalCount; }
public get pageCount(): number { return this._pageCount; }
public get currentPageNumber(): number { return this._currentPageNumber; }
public selectItem(item: T): void {
const selectedItems = this.selectedItems;
switch (this.selectionMode) {
case ItemSelectionMode.None:
return;
case ItemSelectionMode.Single:
if (selectedItems[0] != item && this._currentPage.includes(item)) {
selectedItems[0] = item;
this.handleSelectionChange();
}
break;
case ItemSelectionMode.Multiple: {
if (!selectedItems.includes(item) && this._currentPage.includes(item)) {
selectedItems.push(item);
this.handleSelectionChange();
}
break;
}
}
}
public selectRange(startIndex: number, endIndex: number): void {
if (this.selectionMode !== ItemSelectionMode.Multiple) return;
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
const items = this._currentPage;
const selectedItems = this.selectedItems;
let hasChange = false;
for (let i = start; i <= end; i++) {
const item = items[i];
if (!selectedItems.includes(item)) {
selectedItems.push(item);
hasChange = true;
}
}
if (hasChange) {
this.handleSelectionChange();
}
}
public toggleSelection(item: T): void {
if (this.selectionMode !== ItemSelectionMode.Multiple) return;
const selectedItems = this.selectedItems;
const idx = selectedItems.findIndex(x => item === x);
if (idx === -1) {
selectedItems.push(item);
} else {
selectedItems.splice(idx, 1);
}
this.handleSelectionChange();
}
private handleSelectionChange(): void {
// cloned to avoid unintentional mutation
const selectedItems = this.selectedItems.slice();
const len = (this as Writable<ContentModel<T>>).selectionCount = selectedItems.length;
const isAnySelected = (this as Writable<ContentModel<T>>).isAnySelected = len > 0;
const isOneSelected = (this as Writable<ContentModel<T>>).isOneSelected = len === 1;
this.onSelectionChange(selectedItems, isOneSelected, isAnySelected);
}
public clearSelections(): void {
(this as Writable<ContentModel<T>>).isAnySelected = (this as Writable<ContentModel<T>>).isOneSelected = false;
(this as Writable<ContentModel<T>>).selectionCount = 0;
this.selectedItems.length = 0;
}
public isSelected(item: T): boolean {
return this.selectedItems.includes(item);
}
public applySorting(...sortOptions: SortOption<T>[]): void {
const oldValue = this._sortOptions;
const newValue = this._sortOptions = sortOptions;
this.onSorting?.(newValue, oldValue, this.allItems, this);
this.goToPage(1, true);
}
public goToPage(pageNumber: number, force: boolean = false): void {
if (!this.initialized) return;
const oldNumber = this._currentPageNumber;
if (oldNumber === pageNumber
&& this.pagePromise !== null
&& this.countPromise !== null
) {
return;
}
this._currentPageNumber = pageNumber;
if (oldNumber !== pageNumber || force) {
this.setPage();
this.setTotalCount();
}
}
public goToPreviousPage(): void {
const pageNumber = this._currentPageNumber;
if (pageNumber === 1) {
this.logger.warn('Cannot go to previous page; already on the first page.');
return;
}
this.goToPage(pageNumber - 1);
}
public goToNextPage(): void {
const pageNumber = this._currentPageNumber;
if (pageNumber === this._pageCount) {
this.logger.warn('Cannot go to next page; already on the last page.');
return;
}
this.goToPage(pageNumber + 1);
}
/** @internal */
public setTotalCount(): void {
const pageSize = this.pageSize;
const allItems = this.allItems;
if (allItems !== null) {
const totalCount = this._totalCount = allItems.length;
if (pageSize !== null) {
this._pageCount = Math.ceil(totalCount / pageSize);
}
this.countPromise = null;
return;
}
const fetchCount = this.fetchCount;
if (fetchCount === null) {
this.logger.warn('fetchCount is not set.');
return;
}
const countPromise = fetchCount(this);
if (countPromise instanceof Promise) {
const promise = this.countPromise = countPromise.then((count) => {
this._totalCount = count;
if (pageSize !== null) {
this._pageCount = Math.ceil(count / pageSize);
}
if (this.countPromise === promise) {
this.countPromise = null;
}
});
return;
}
this._totalCount = countPromise;
if (pageSize !== null) {
this._pageCount = Math.ceil(countPromise / pageSize);
}
this.countPromise = null;
}
/** @internal */
public setPage(): void {
const allItems = this.allItems;
const pageSize = this.pageSize;
const pageNumber = this._currentPageNumber;
this.clearSelections();
if (allItems !== null) {
this._currentPage = pageSize !== null
? allItems.slice(pageSize * (pageNumber - 1), pageSize * pageNumber)
: allItems;
this.pagePromise = null;
return;
}
// one of fetchPage or allItems should always be there.
const fetchPage = this.fetchPage!;
const pagePromise = fetchPage(pageNumber, pageSize!, this);
if (pagePromise instanceof Promise) {
const promise = this.pagePromise = pagePromise
.then((data) => {
this._currentPage = data;
if (this.pagePromise === promise) {
this.pagePromise = null;
}
});
return;
}
this._currentPage = pagePromise;
this.pagePromise = null;
}
public async wait(rethrowError: boolean = false): Promise<void> {
try {
await Promise.all([this.pagePromise, this.countPromise]);
} catch (e) {
if (rethrowError) {
throw e;
}
}
}
public async refresh(rethrowError: boolean = false): Promise<void> {
this.goToPage(1, true);
return this.wait(rethrowError);
}
private allItemsChanged(): void {
this.goToPage(1, true);
}
}
export enum ItemSelectionMode {
None = 0,
Single = 1,
Multiple = 2,
}
type SelectionChangeHandler<T> = (selectedItems: T[], isOneSelected: boolean, isAnySelected: boolean) => void;
export interface SelectionOptions<T> {
mode: ItemSelectionMode;
onSelectionChange: SelectionChangeHandler<T>;
}
export type FetchCount<T> = (model: ContentModel<T>) => number | Promise<number>;
export type FetchPage<T> = (currentPage: number, pageSize: number, model: ContentModel<T>) => T[] | Promise<T[]>;
export interface PagingOptions<T> {
pageSize: number | null;
fetchPage: FetchPage<T>;
fetchCount: FetchCount<T>;
}
export type ApplySorting<T> = (newValue: SortOption<T>[], oldValue: SortOption<T>[], allItems: T[] | null, model: ContentModel<T>) => void;