@sparser/au2-data-grid
Version:
A data grid for Aurelia 2
246 lines (221 loc) • 8.36 kB
text/typescript
import {
onResolve,
onResolveAll,
resolve,
} from '@aurelia/kernel';
import {
BindingContext,
Scope,
} from '@aurelia/runtime';
import {
bindable,
ICustomAttributeController,
ICustomAttributeViewModel,
IHydratedController,
IHydratedParentController,
IRenderLocation,
ISyntheticView,
templateController,
} from '@aurelia/runtime-html';
import {
ChangeType,
GridStateChangeSubscriber,
GridStateModel,
OrderChangeData,
OrderChangeDropLocation,
} from './grid-state.js';
import {
SortOption,
} from './sorting-options.js';
/**
* Template controller to render the headers.
* @internal
*/
export class GridHeaders implements ICustomAttributeViewModel, GridStateChangeSubscriber {
private readonly location: IRenderLocation = resolve(IRenderLocation);
public readonly $controller!: ICustomAttributeController<this>; // This is set by the controller after this instance is constructed
public state!: GridStateModel;
private headers!: ISyntheticView[];
private promise: Promise<void> | void = void 0;
/**
* Key: original column-index; Value: view-index
*/
private _indexMap: Map<number, number> = new Map<number, number>();
public get indexMap(): Map<number, number> { return this._indexMap; }
public attaching(
initiator: IHydratedController,
parent: IHydratedParentController,
): void | Promise<void> {
const indexMap = this._indexMap;
indexMap.clear();
const location = this.location;
const state = this.state;
const columns = state.columns;
let len = 0;
const headers = this.headers = columns.reduce((acc, column, i) => {
if (column.hidden) return acc;
acc.push(column.headerViewFactory!.create(initiator).setLocation(location));
indexMap.set(i, len++);
return acc;
}, [] as ISyntheticView[]);
const activationPromises = new Array<void | Promise<void>>(len);
for (let i = 0; i < len; i++) {
const header = headers[i];
header.nodes.link(headers[i + 1]?.nodes ?? location);
activationPromises[i] = header.activate(initiator, parent, Scope.create(new BindingContext('state', columns[i])));
}
this.queue(() => onResolveAll(...activationPromises));
state.addSubscriber(this);
return this.promise;
}
public detaching(initiator: IHydratedController, parent: IHydratedParentController): void | Promise<void> {
this.state.removeSubscriber(this);
this.queue(() => onResolveAll(...this.headers.map((header) => header.deactivate(initiator, parent))));
return this.promise;
}
public dispose(): void {
const headers = this.headers;
const len = headers.length;
for (let i = 0; i < len; i++) {
headers[i].dispose();
}
this.headers.length = 0;
}
public handleGridStateChange(type: ChangeType.Width): void;
public handleGridStateChange(type: ChangeType.Order, value: OrderChangeData): void;
public handleGridStateChange(type: ChangeType.Sort, newValue: SortOption<Record<string, unknown>>, oldValue: SortOption<Record<string, unknown>> | null): void;
public handleGridStateChange(type: ChangeType, value?: SortOption<Record<string, unknown>> | OrderChangeData, _oldValue?: SortOption<Record<string, unknown>> | null): void {
if (type !== ChangeType.Order) return;
handleReordering(this.headers, value as OrderChangeData, this.location, this._indexMap);
}
private queue(action: () => void | Promise<void>): void {
const previousPromise = this.promise;
let promise: void | Promise<void> = void 0;
promise = this.promise = onResolve(
onResolve(previousPromise, action),
() => {
if (this.promise === promise) {
this.promise = void 0;
}
}
);
}
}
/**
* Template controller to render the content cells.
* @internal
*/
export class GridContent implements ICustomAttributeViewModel {
private readonly location: IRenderLocation = resolve(IRenderLocation);
public item: unknown;
public state!: GridStateModel;
public readonly $controller!: ICustomAttributeController<this>; // This is set by the controller after this instance is constructed
private cells!: ISyntheticView[];
private promise: Promise<void> | void = void 0;
/**
* Key: original column-index; Value: view-index
*/
private _indexMap: Map<number, number> = new Map<number, number>();
public attaching(
initiator: IHydratedController,
parent: IHydratedParentController,
): void | Promise<void> {
const headersTc: GridHeaders = this.$controller
.parent // synthetic
?.parent // repeater
?.parent // grid
?.children
?.find(c => c.viewModel instanceof GridHeaders)
?.viewModel as GridHeaders;
if (headersTc == null) throw new Error('The grid-headers is not found.');
const indexMap = this._indexMap = headersTc.indexMap;
const item = this.item;
const location = this.location;
const state = this.state;
const columns = state.columns;
const len = indexMap.size;
const cells = this.cells = new Array<ISyntheticView>(len);
const activationPromises = new Array<void | Promise<void>>(len);
let i = 0;
for (const [key,] of indexMap) {
const cell = cells[i++] = columns[key].contentViewFactory!.create(initiator).setLocation(location);
activationPromises[i] = cell.activate(initiator, parent, Scope.create(new BindingContext('item', item)));
}
this.queue(() => onResolveAll(...activationPromises));
state.addSubscriber(this);
return this.promise;
}
public detaching(initiator: IHydratedController, parent: IHydratedParentController): void | Promise<void> {
this.state.removeSubscriber(this);
this.queue(() => onResolveAll(...this.cells.map((cell) => cell.deactivate(initiator, parent))));
return this.promise;
}
public dispose(): void {
const cells = this.cells;
const len = cells.length;
for (let i = 0; i < len; i++) {
cells[i].dispose();
}
this.cells.length = 0;
}
private queue(action: () => void | Promise<void>): void {
const previousPromise = this.promise;
let promise: void | Promise<void> = void 0;
promise = this.promise = onResolve(
onResolve(previousPromise, action),
() => {
if (this.promise === promise) {
this.promise = void 0;
}
}
);
}
public handleGridStateChange(type: ChangeType.Width): void;
public handleGridStateChange(type: ChangeType.Order, value: OrderChangeData): void;
public handleGridStateChange(type: ChangeType.Sort, newValue: SortOption<Record<string, unknown>>, oldValue: SortOption<Record<string, unknown>> | null): void;
public handleGridStateChange(type: ChangeType, value?: SortOption<Record<string, unknown>> | OrderChangeData, _oldValue?: SortOption<Record<string, unknown>> | null): void {
if (type !== ChangeType.Order) return;
handleReordering(this.cells, value as OrderChangeData, this.location, this._indexMap);
}
}
function handleReordering(
views: ISyntheticView[],
changeData: OrderChangeData,
location: IRenderLocation,
indexMap: Map<number, number>,
): void {
const fromIdx = changeData.fromIndex;
let toIdx = changeData.toIndex;
const dropLocation = changeData.location;
const fromNodes = views[indexMap.get(fromIdx)!].nodes;
const toNodes = views[indexMap.get(toIdx)!].nodes;
// link the next node with the previous node
views[fromIdx - 1]?.nodes.link(views[fromIdx + 1]?.nodes ?? location);
switch (dropLocation) {
case OrderChangeDropLocation.Before:
views[toIdx - 1]?.nodes.link(fromNodes);
fromNodes.link(toNodes);
if (fromIdx < toIdx) {
toIdx--;
}
break;
case OrderChangeDropLocation.After:
fromNodes.link(views[toIdx + 1]?.nodes ?? location);
toNodes.link(fromNodes);
if (fromIdx > toIdx) {
toIdx++;
}
break;
}
fromNodes.addToLinked();
views.splice(
toIdx,
0,
views.splice(fromIdx, 1)[0]
);
}