UNPKG

@sparser/au2-data-grid

Version:
333 lines (306 loc) 11.8 kB
import { DI, IContainer, } from '@aurelia/kernel'; import { CustomElementDefinition, ViewFactory, } from '@aurelia/runtime-html'; import { SortDirection, SortOption, } from './sorting-options.js'; export interface ExportableGridState { columns: ExportableColumnState[]; } export interface IGridState { export(): ExportableGridState; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface IGridStateModel extends GridStateModel { } // eslint-disable-next-line @typescript-eslint/naming-convention export const IGridStateModel = DI.createInterface<IGridStateModel>('IGridStateModel'); /** * This aggregates the structural metadata for the grid. * This is meant for internal use. * @internal */ export class GridStateModel implements IGridState { private _activeSortOptions: SortOption<Record<string, unknown>> | null = null; /** * @internal */ private subscribers: GridStateChangeSubscriber[] = []; /** @internal */ private viewFactoriesCreated: boolean = false; public readonly columns: Column[] = []; public get activeSortOptions(): SortOption<Record<string, unknown>> | null { return this._activeSortOptions; } /** * Exports the grid state. */ public export(): ExportableGridState { return { columns: this.columns.map((c) => c.export()) }; } /** * Applies a previously exported state. * @param {ExportableGridState} state The state to apply. */ public applyState(state: ExportableGridState): void { const columns = this.columns; const stateColumns = state.columns; const len = stateColumns.length; for (let i = 0; i < len; i++) { const stateColumn = stateColumns[i]; const colIndex = columns.findIndex(c => c.id === stateColumn.id); if (colIndex === -1) continue; const column = columns[colIndex]; if (!column.tryApplyState(stateColumn) || colIndex === i) continue; // move column columns.splice(colIndex, 1); columns.splice(i, 0, column); } } /** * Marks the hidden columns as per the given column ids. * Note that in absence of a `id` attribute in `grid-column`, the `property` is used as the `id`. * @param {string[]} columnIds The collection of ids of the columns to hide. */ public hideColumns(columnIds: string[]): void { const len = columnIds?.length ?? 0; if (len === 0) return; const columns = this.columns; for (let i = 0; i < len; i++) { const id = columnIds[i]; const col = columns.find(c => c.id === id); if (col === undefined) continue; col.hidden = true; } } /** * Creates the view factories for every column using the given `container`. */ public createViewFactories(container: IContainer): void { if (this.viewFactoriesCreated) return; const columns = this.columns; const len = columns.length; for (let i = 0; i < len; i++) { columns[i].createViewFactories(container); } this.viewFactoriesCreated = true; } /** * Initializes the sort options. * To this end the first column with non-null direction is used . */ public initializeActiveSortOptions(): SortOption<Record<string, unknown>> | null { const column = this.columns.find(c => c.direction !== null); if (column == null) return null; return this._activeSortOptions = { property: column.property!, direction: column.direction!, }; } /** * Adds the given `subscriber` to the collection of subscribers. * The subscribers will be notified for state changes. */ public addSubscriber(subscriber: GridStateChangeSubscriber): void { this.subscribers.push(subscriber); } /** * Removes the given `subscriber` from the collection of subscribers. * The subscriber won't be notified for any further state changes. */ public removeSubscriber(subscriber: GridStateChangeSubscriber): void { const subscribers = this.subscribers; const idx = subscribers.findIndex(s => s === subscriber); if (idx === -1) return; subscribers.splice(idx, 1); } private notifySubscribers( type: ChangeType.Width, ): void; private notifySubscribers( type: ChangeType.Sort, newValue: SortOption<Record<string, unknown>>, oldValue: SortOption<Record<string, unknown>> | null, ): void; private notifySubscribers( type: ChangeType.Order, newValue: OrderChangeData, oldValue: null, ): void; private notifySubscribers( type: ChangeType, newValue?: SortOption<Record<string, unknown>> | OrderChangeData, oldValue?: SortOption<Record<string, unknown>> | null, ): void { const subscribers = this.subscribers; const len = subscribers.length; for (let i = 0; i < len; i++) { subscribers[i].handleGridStateChange(type, newValue, oldValue); } } /** @internal */ public handleChange(type: ChangeType.Width): void; public handleChange(type: ChangeType.Sort, column: Column): void; public handleChange(type: ChangeType.Order, sourceId: string, destination: Column, location: OrderChangeDropLocation): void; public handleChange(type: ChangeType, columnOrId?: string | Column, destination?: Column, location?: OrderChangeDropLocation): void { switch (type) { case ChangeType.Sort: { const oldSortOptions = this._activeSortOptions; const oldProperty = oldSortOptions?.property; const newProperty = (columnOrId as Column).property!; if (oldProperty !== newProperty) { // this is needed so that change to the old sort column can be propagated to the view. this.columns .find(c => c.property === oldProperty) ?.setDirection(null, false); } const newSortOptions = this._activeSortOptions = { property: newProperty, direction: (columnOrId as Column).direction! }; this.notifySubscribers(type, newSortOptions, oldSortOptions); return; } case ChangeType.Order: { const columns = this.columns; const sourceIndex = columns.findIndex(c => c.id === columnOrId); const destinationIndex = columns.findIndex(c => c === destination); const diff = destinationIndex - sourceIndex; if (diff === 1 && location === OrderChangeDropLocation.Before || diff === -1 && location === OrderChangeDropLocation.After ) return; let $destinationIndex = destinationIndex; if (sourceIndex < destinationIndex && location === OrderChangeDropLocation.Before) { $destinationIndex--; } else if(sourceIndex > destinationIndex && location === OrderChangeDropLocation.After) { $destinationIndex++; } columns.splice($destinationIndex, 0, columns.splice(sourceIndex, 1)[0]); this.notifySubscribers(type, { fromIndex: sourceIndex, toIndex: destinationIndex, location } as OrderChangeData, null); return; } case ChangeType.Width: this.notifySubscribers(type); return; default: throw new Error(`Unsupported change type: ${String(type)}.`); } } } export interface ExportableColumnState { readonly id: string; readonly property: string | null; readonly isResizable: boolean; widthPx: string | null; direction: SortDirection | null; } export interface ColumnState extends ExportableColumnState { setDirection(direction: SortDirection | null, notifyParent: boolean): void; export(): ExportableColumnState; } /** * This describes the structural metadata of a column. * This is meant for internal use. * @internal */ export class Column implements ColumnState { private static id = 0; public static generateId(): string { return `unnamed-column-${++this.id}`; } /** @internal */ private readonly _sortable: boolean = false; /** @internal */ private _direction: SortDirection | null = null; /** @internal */ private _headerViewFactory: ViewFactory | null = null; /** @internal */ private _contentViewFactory: ViewFactory | null = null; /** * This is registered from inside the grid-header CE during `binding`. * @internal */ public headerElement?: HTMLElement; public hidden: boolean = false; public constructor( public readonly parent: GridStateModel, public readonly id: string, public readonly property: string | null, private readonly exportable: boolean, direction: SortDirection | null, public readonly isResizable: boolean, public widthPx: string | null, private readonly header: CustomElementDefinition, private readonly content: CustomElementDefinition, ) { if (!id) throw new Error('Cannot instantiate ColumnState; expected non-null, non-undefined, non-empty string for id.'); if (property !== null) { if (property.length === 0) throw new Error('Cannot instantiate ColumnState; expected non-empty property.'); this._sortable = true; } else { direction = null; } this._direction = direction; parent.columns.push(this); } public get direction(): SortDirection | null { return this._direction; } public get sortable(): boolean { return this._sortable; } public get headerViewFactory(): ViewFactory | null { return this._headerViewFactory; } public get contentViewFactory(): ViewFactory | null { return this._contentViewFactory; } /** @internal */ public setDirection(direction: SortDirection | null, notifyParent: boolean): void { if (!this._sortable) throw new Error(`The column '${this.id}' is not sortable.`); this._direction = direction; if (notifyParent) { this.parent.handleChange(ChangeType.Sort, this); } } public export(): ExportableColumnState { if (!this.exportable) throw new Error(`The column '${this.id}' is not exportable.`); return { id: this.id, property: this.property, direction: this._direction, isResizable: this.isResizable, widthPx: this.widthPx, }; } /** @internal */ public tryApplyState(state: ExportableColumnState): boolean { if (this.id !== state.id || this.property !== state.property) return false; this._direction = state.direction; this.widthPx = state.widthPx; return true; } public createViewFactories(container: IContainer): void { // invocation is expected once during pre-binding stage if (this._headerViewFactory !== null && this._contentViewFactory !== null) return; this._headerViewFactory = new ViewFactory(container, this.header); this._contentViewFactory = new ViewFactory(container, this.content); } } export const enum ChangeType { /** Content sorting is changed. */ Sort = 1, /** Column is reordered. */ Order = 2, /** Width of a column is changed. */ Width = 3, } export const enum OrderChangeDropLocation { Before = 1, After = 2, } export interface OrderChangeData { fromIndex: number; toIndex: number; location: OrderChangeDropLocation; } export type GridStateChangeSubscriber = { handleGridStateChange(type: ChangeType.Width): void; handleGridStateChange(type: ChangeType.Order, value: OrderChangeData): void; handleGridStateChange(type: ChangeType.Sort, newValue: SortOption<Record<string, unknown>>, oldValue: SortOption<Record<string, unknown>> | null): void; handleGridStateChange(type: ChangeType, newValue?: SortOption<Record<string, unknown>> | OrderChangeData, oldValue?: SortOption<Record<string, unknown>> | null): void; };