xenos
Version:
Xenos is a data grid built upon angular2 and bootstrap.
295 lines (240 loc) • 9.9 kB
text/typescript
import { DataGridColumnConfig } from "./data-grid-column-config";
import { DataGrid } from "./data-grid.component";
import { FilterCandidate } from "./filter-candidate";
import { FilterDescriptor } from "./filter-descriptor";
import { ColumnFilterDescriptor } from "./column-filter-descriptor";
import { ColumnSortDescriptor } from "./column-sort-descriptor";
import { SortDirection } from "./sort-direction";
import { SortDescriptor } from "./sort-descriptor";
import { IdGenerator } from "./id-generator";
import { CandidateMatcher } from "./candidate-matcher";
import { DefaultCandidateMatcher } from "./default-candidate-matcher";
import { Subject } from "rxjs/Subject";
import { Observable } from "rxjs/Observable";
import { async } from "rxjs/scheduler/async";
import "rxjs/add/Observable/timer";
import "rxjs/add/operator/debounce";
import "rxjs/add/operator/map";
export class DataGridColumn {
private threshold: number = 30;
private config: DataGridColumnConfig;
private debounceThreshold: number = 200;
private candidateQueryInternal: string = "";
private candidates: FilterCandidate[] = [];
private candidateFinder: Subject<string> = new Subject<string>();
private sortDirectionCycler: Map<SortDirection, SortDirection> =
new Map<SortDirection, SortDirection>();
constructor(config?: DataGridColumnConfig) {
if (!config) {
config = this.getDefaultOptions();
}
if (!config.id) {
config.id = IdGenerator.getNextColumnId();
}
if (!config.sortDirection) {
config.sortDirection = undefined;
}
this.config = config;
this.sortDirectionCycler.set(null, SortDirection.ascending);
this.sortDirectionCycler.set(undefined, SortDirection.ascending);
this.sortDirectionCycler.set(SortDirection.ascending, SortDirection.descending);
this.sortDirectionCycler.set(SortDirection.descending, SortDirection.ascending);
this.matchers = [new DefaultCandidateMatcher()];
this.candidateFinder.asObservable()
.debounce(x => Observable.timer(this.debounceThreshold))
.observeOn(async)
.map(x => this.findCandidates(x))
.subscribe(x => {
this.candidates.splice(0);
x.forEach(x => this.candidates.push(x));
});
}
public dataGrid: DataGrid;
public matchers: CandidateMatcher[];
public get id(): any {
return this.config.id;
}
public get initialSortDirection(): SortDirection {
return this.config.sortDirection;
}
public get sortDirection(): SortDirection {
this.ensureAttachment();
let id = IdGenerator.synthesizeColumnSortId(this);
let desc = this.dataGrid.sortDescriptors.find(x => x.id == id);
return desc ? desc.direction : undefined;
}
public set sortDirection(direction: SortDirection) {
this.ensureAttachment();
let id = IdGenerator.synthesizeColumnSortId(this);
let index = this.dataGrid.sortDescriptors.findIndex(x => x.id === id);
if (~index) {
if (direction) {
let descriptor = new ColumnSortDescriptor(this, direction);
this.dataGrid.sortDescriptors.splice(index, 1, descriptor);
} else {
this.dataGrid.sortDescriptors.splice(index, 1);
}
} else {
if (direction) {
let descriptor = new ColumnSortDescriptor(this, direction);
this.dataGrid.sortDescriptors.push(descriptor);
}
}
}
public get hidden(): boolean {
return this.config.hidden;
}
public set hidden(value: boolean) {
this.config.hidden = value;
}
public get disableSorting(): boolean {
return this.config.disableSorting;
}
public set disableSorting(value: boolean) {
this.config.disableSorting = value;
}
public get disableFiltering(): boolean {
return this.config.disableFiltering;
}
public set disableFiltering(value: boolean) {
this.config.disableFiltering = value;
}
public get valueAccessor(): (x: any) => any {
return this.config.valueAccessor;
}
public get cellRenderer(): (x: any, c?: HTMLTableCellElement, r?: HTMLTableRowElement) => string | HTMLElement {
return this.config.cellRenderer;
}
public get headerRenderer(): (c?: HTMLTableHeaderCellElement, r?: HTMLTableRowElement) => string | HTMLElement {
return this.config.headerRenderer;
}
public get candidateRenderer(): (x: any) => string | HTMLElement {
return this.config.candidateRenderer;
}
private ensureAttachment(): void {
if (!this.dataGrid) {
throw new Error("The column is not attached to a grid.");
}
}
private get activeFilters(): ColumnFilterDescriptor[] {
this.ensureAttachment();
return this.dataGrid.filterDescriptors
.map(x => <any>x)
.filter(x => x.column)
.map(x => <ColumnFilterDescriptor>x)
.filter(x => x.column.id === this.id);
}
public set candidateQuery(phrase: string) {
this.candidateQueryInternal = phrase;
this.candidateFinder.next(phrase);
}
public get candidateQuery(): string {
return this.candidateQueryInternal;
}
public reset(): void {
this.candidateQuery = "";
this.candidates.splice(0, this.candidates.length);
}
public cycleSortDirections(): void {
this.sortDirection = this.sortDirectionCycler.get(this.sortDirection);
}
private removeAllColumnFilters(event: Event): void {
event.preventDefault();
event.stopPropagation();
this.ensureAttachment();
let filters = this.dataGrid.filterDescriptors.filter(x => x.groupId !== this.config.id);
this.dataGrid.filterDescriptors.splice(0);
filters.forEach(x => this.dataGrid.filterDescriptors.push(x));
}
private removeFilter(event: Event, descriptor: FilterDescriptor): void {
event.preventDefault();
event.stopPropagation();
this.ensureAttachment();
let index = this.dataGrid.filterDescriptors.findIndex(x => x.id === descriptor.id);
if (~index) {
this.dataGrid.filterDescriptors.splice(index, 1);
}
}
private onCandidateClicked(event: Event, candidate: FilterCandidate): void {
event.preventDefault();
event.stopPropagation();
this.ensureAttachment();
var desc = new ColumnFilterDescriptor(this, candidate.value, x => {
let value = this.valueAccessor(x);
return value === candidate.value;
});
if (this.dataGrid.filterDescriptors.find(x => x.id === desc.id)) {
return;
}
this.dataGrid.filterDescriptors.push(desc);
}
private findCandidates(phrase: string): FilterCandidate[] {
this.ensureAttachment();
if (!this.dataGrid.lastSnapShot) {
return [];
}
let distinctDates = new Map<number, Date>();
let distinctValues = new Map<any, any>();
// We need to restrict the candidate selection for all columns if filters are set to only
// allow possible combinations except for the active one in case we want to change the current selection.
let source = this.activeFilters.length > 0
? this.dataGrid.lastSnapShot.items
: this.dataGrid.lastSnapShot.processedItems;
source.forEach(x => {
let value = this.config.valueAccessor(x);
// Dates require special treatment for they are compared by reference and not by value,
// which means the Map will not recognize equal dates coming from different instances.
if (value instanceof Date) {
let ms = value.getTime();
if (!distinctDates.has(ms)) {
distinctDates.set(ms, value);
}
return;
}
if (!distinctValues.has(value)) {
distinctValues.set(value, value);
}
});
// Merge date and non-date items
distinctDates.forEach(x => {
distinctValues.set(x, x);
});
// If we don't have a specific query and there are only a few items in the source, we simply return all.
let empty = (phrase == null || phrase == "");
if (empty && distinctValues.size < this.threshold) {
let values = [];
distinctValues.forEach(x => {
values.push(new FilterCandidate(x));
});
return values;
}
if (empty) {
return source;
}
let values = [];
distinctValues.forEach(x => {
let match = false;
this.matchers.forEach(y => {
if (match) {
return;
}
match = y.match(phrase, x);
});
if (match) {
values.push(new FilterCandidate(x));
}
});
return values;
}
private getDefaultOptions(): DataGridColumnConfig {
return {
id: undefined,
headerRenderer: () => undefined,
disableFiltering: false,
disableSorting: false,
hidden: false,
cellRenderer: x => x,
valueAccessor: x => x
}
}
}