UNPKG

ag-grid-enterprise

Version:

ag-Grid Enterprise Features

477 lines (406 loc) 17.2 kB
import { ClientSideRowModel, ColDef, Column, Constants, ExternalPromise, IRowModel, ISetFilterParams, Promise, SetFilterValuesFunc, SetFilterValuesFuncParams, TextFilter, TextFormatter, Utils, ValueFormatterService } from "ag-grid-community"; // we cannot have 'null' as a key in a JavaScript map, // it needs to be a string. so we use this string for // storing null values. const NULL_VALUE = '___NULL___'; export enum SetFilterModelValuesType { PROVIDED_LIST, PROVIDED_CB, NOT_PROVIDED } export class SetFilterModel { private colDef: ColDef; private filterParams: ISetFilterParams; private clientSideRowModel: ClientSideRowModel; private valueGetter: any; private allUniqueValues: string[]; // all values in the table private availableUniqueValues: string[]; // all values not filtered by other rows private displayedValues: any[]; // all values we are rendering on screen (ie after mini filter) private miniFilter: string | null; private selectedValuesCount: number; private selectedValuesMap: { [value: string]: any }; private suppressSorting: boolean; private formatter: TextFormatter; // to make code more readable, we work these out once, and // then refer to each time. both are derived from the filterParams private showingAvailableOnly: boolean; private valuesType: SetFilterModelValuesType; private doesRowPassOtherFilters: any; private modelUpdatedFunc: (values: string[] | null, selected?: string[] | null) => void; private isLoadingFunc: (loading: boolean) => void; private filterValuesExternalPromise: ExternalPromise<string[]>; private filterValuesPromise: Promise<string[]>; private valueFormatterService: ValueFormatterService; private column: Column; constructor(colDef: ColDef, rowModel: IRowModel, valueGetter: any, doesRowPassOtherFilters: any, suppressSorting: boolean, modelUpdatedFunc: (values: string[], selected?: string[]) => void, isLoadingFunc: (loading: boolean) => void, valueFormatterService: ValueFormatterService, column: Column) { this.suppressSorting = suppressSorting; this.colDef = colDef; this.valueGetter = valueGetter; this.doesRowPassOtherFilters = doesRowPassOtherFilters; this.modelUpdatedFunc = modelUpdatedFunc; this.isLoadingFunc = isLoadingFunc; this.valueFormatterService = valueFormatterService; this.column = column; if (rowModel.getType() === Constants.ROW_MODEL_TYPE_CLIENT_SIDE) { this.clientSideRowModel = <ClientSideRowModel> rowModel; } this.filterParams = this.colDef.filterParams ? <ISetFilterParams> this.colDef.filterParams : <ISetFilterParams>{}; if (Utils.exists(this.filterParams) && Utils.exists(this.filterParams.values)) { this.valuesType = Array.isArray(this.filterParams.values) ? SetFilterModelValuesType.PROVIDED_LIST : SetFilterModelValuesType.PROVIDED_CB; this.showingAvailableOnly = this.filterParams.suppressRemoveEntries !== true; } else { this.valuesType = SetFilterModelValuesType.NOT_PROVIDED; this.showingAvailableOnly = true; } this.createAllUniqueValues(); this.createAvailableUniqueValues(); // by default, no filter, so we display everything this.displayedValues = this.availableUniqueValues; this.miniFilter = null; // we use a map rather than an array for the selected values as the lookup // for a map is much faster than the lookup for an array, especially when // the length of the array is thousands of records long this.selectedValuesMap = {}; this.selectEverything(); this.formatter = this.filterParams.textFormatter ? this.filterParams.textFormatter : TextFilter.DEFAULT_FORMATTER; } // if keepSelection not set will always select all filters // if keepSelection set will keep current state of selected filters // unless selectAll chosen in which case will select all public refreshAfterNewRowsLoaded(keepSelection: any, isSelectAll: boolean) { this.createAllUniqueValues(); this.refreshSelection(keepSelection, isSelectAll); } // if keepSelection not set will always select all filters // if keepSelection set will keep current state of selected filters // unless selectAll chosen in which case will select all public refreshValues(valuesToUse: string[], keepSelection: any, isSelectAll: boolean) { this.setValues(valuesToUse); this.refreshSelection(keepSelection, isSelectAll); } private refreshSelection(keepSelection: any, isSelectAll: boolean) { this.createAvailableUniqueValues(); let oldModel = Object.keys(this.selectedValuesMap); this.selectedValuesMap = {}; this.processMiniFilter(); if (keepSelection) { this.setModel(oldModel, isSelectAll); } else { this.selectEverything(); } } public refreshAfterAnyFilterChanged() { if (this.showingAvailableOnly) { this.createAvailableUniqueValues(); this.processMiniFilter(); } } private createAllUniqueValues() { if (this.areValuesSync()) { let valuesToUse: string[] = this.extractSyncValuesToUse(); this.setValues(valuesToUse); this.filterValuesPromise = Promise.resolve([]); } else { this.filterValuesExternalPromise = Promise.external<string[]>(); this.filterValuesPromise = this.filterValuesExternalPromise.promise; this.isLoadingFunc(true); this.setValues([]); let callback = <SetFilterValuesFunc> this.filterParams.values; let params: SetFilterValuesFuncParams = { success: this.onAsyncValuesLoaded.bind(this), colDef: this.colDef }; callback(params); } } private onAsyncValuesLoaded(values: string[]): void { this.modelUpdatedFunc(values); this.isLoadingFunc(false); this.filterValuesExternalPromise.resolve(values); } private areValuesSync() { return this.valuesType == SetFilterModelValuesType.PROVIDED_LIST || this.valuesType == SetFilterModelValuesType.NOT_PROVIDED; } public setValuesType(value: SetFilterModelValuesType) { this.valuesType = value; } private setValues(valuesToUse: string[]) { this.allUniqueValues = valuesToUse; if (!this.suppressSorting) { this.sortValues(this.allUniqueValues); } } private extractSyncValuesToUse() { let valuesToUse: string[]; if (this.valuesType == SetFilterModelValuesType.PROVIDED_LIST) { if (Array.isArray(this.filterParams.values)) { valuesToUse = Utils.toStrings(<string[]>this.filterParams.values); } else { // In this case the values are async but have already been resolved, so we can reuse them valuesToUse = this.allUniqueValues; } } else if (this.valuesType == SetFilterModelValuesType.PROVIDED_CB) { throw Error(`ag-grid: Error extracting values to use. We should not extract the values synchronously when using a callback for the filterParams.values`); } else { let uniqueValuesAsAnyObjects = this.getUniqueValues(false); valuesToUse = Utils.toStrings(uniqueValuesAsAnyObjects); } return valuesToUse; } private createAvailableUniqueValues() { let dontCheckAvailableValues = !this.showingAvailableOnly || this.valuesType == SetFilterModelValuesType.PROVIDED_LIST || this.valuesType == SetFilterModelValuesType.PROVIDED_CB; if (dontCheckAvailableValues) { this.availableUniqueValues = this.allUniqueValues; return; } let uniqueValuesAsAnyObjects = this.getUniqueValues(true); this.availableUniqueValues = Utils.toStrings(uniqueValuesAsAnyObjects); this.sortValues(this.availableUniqueValues); } private sortValues(values: any[]): void { if (this.filterParams && this.filterParams.comparator) { values.sort(this.filterParams.comparator); } else if (this.colDef.comparator) { values.sort(this.colDef.comparator); } else { values.sort(Utils.defaultComparator); } } private getUniqueValues(filterOutNotAvailable: boolean): any[] { let uniqueCheck = <any>{}; let result = <any>[]; if (!this.clientSideRowModel) { console.error('ag-Grid: Set Filter cannot initialise because you are using a row model that does not contain all rows in the browser. Either use a different filter type, or configure Set Filter such that you provide it with values'); return []; } this.clientSideRowModel.forEachLeafNode((node: any) => { // only pull values from rows that have data. this means we skip filler group nodes. if (!node.data) { return; } let value = this.valueGetter(node); if (this.colDef.keyCreator) { value = this.colDef.keyCreator({value: value}); } if (value === "" || value === undefined) { value = null; } if (filterOutNotAvailable) { if (!this.doesRowPassOtherFilters(node)) { return; } } if (value != null && Array.isArray(value)) { for (let j = 0; j < value.length; j++) { addUniqueValueIfMissing(value[j]); } } else { addUniqueValueIfMissing(value); } }); function addUniqueValueIfMissing(value: any) { if (!uniqueCheck.hasOwnProperty(value)) { result.push(value); uniqueCheck[value] = 1; } } return result; } //sets mini filter. returns true if it changed from last value, otherwise false public setMiniFilter(newMiniFilter: string): boolean { newMiniFilter = Utils.makeNull(newMiniFilter); if (this.miniFilter === newMiniFilter) { //do nothing if filter has not changed return false; } this.miniFilter = newMiniFilter; this.processMiniFilter(); return true; } public getMiniFilter() { return this.miniFilter; } private processMiniFilter() { // if no filter, just use the unique values if (this.miniFilter === null) { this.displayedValues = this.availableUniqueValues; return; } // if filter present, we filter down the list this.displayedValues = []; const miniFilter = this.formatter(this.miniFilter); // make upper case to have search case insensitive const miniFilterUpperCase = miniFilter.toUpperCase(); //This function encapsulates the logic to check if a string matches the mini filter let matchesFn: any = (valueToCheck: string): boolean => { if (valueToCheck == null) { return false; } // allow for case insensitive searches, make both filter and value uppercase const valueUpperCase = valueToCheck.toUpperCase(); return valueUpperCase.indexOf(miniFilterUpperCase) >= 0; }; for (let i = 0, l = this.availableUniqueValues.length; i < l; i++) { let value = this.availableUniqueValues[i]; if (value) { const displayedValue = this.formatter(value.toString()); let formattedValue: string = this.valueFormatterService.formatValue(this.column, null, null, displayedValue); if (matchesFn(displayedValue) || matchesFn(formattedValue)) { this.displayedValues.push(value); } } } } public getDisplayedValueCount() { return this.displayedValues.length; } public getDisplayedValue(index: any) { return this.displayedValues[index]; } public selectEverything() { if (!this.filterParams.selectAllOnMiniFilter || !this.miniFilter) { this.selectOn(this.allUniqueValues); } else { this.selectOn(this.displayedValues); } } private selectOn(toSelectOn: any[]) { let count = toSelectOn.length; for (let i = 0; i < count; i++) { let key = toSelectOn[i]; let safeKey = this.valueToKey(key); this.selectedValuesMap[safeKey] = null; } this.selectedValuesCount = count; } private valueToKey(key: string): string { if (key === null) { return NULL_VALUE; } else { return key; } } private keyToValue(value: string): string | null { if (value === NULL_VALUE) { return null; } else { return value; } } public isFilterActive(): boolean { return this.allUniqueValues.length !== this.selectedValuesCount; } public selectNothing(): void { if (!this.filterParams.selectAllOnMiniFilter || !this.miniFilter) { this.selectedValuesMap = {}; this.selectedValuesCount = 0; } else { this.displayedValues.forEach(it => this.unselectValue(it)); } } public getUniqueValueCount(): number { return this.allUniqueValues.length; } public getUniqueValue(index: any): string { return this.allUniqueValues[index]; } public unselectValue(value: any) { let safeKey = this.valueToKey(value); if (this.selectedValuesMap[safeKey] !== undefined) { delete this.selectedValuesMap[safeKey]; this.selectedValuesCount--; } } public selectValue(value: any) { let safeKey = this.valueToKey(value); if (this.selectedValuesMap[safeKey] === undefined) { this.selectedValuesMap[safeKey] = null; this.selectedValuesCount++; } } public isValueSelected(value: any) { let safeKey = this.valueToKey(value); return this.selectedValuesMap[safeKey] !== undefined; } public isEverythingSelected(): boolean { if (!this.filterParams.selectAllOnMiniFilter || !this.miniFilter) { return this.allUniqueValues.length === this.selectedValuesCount; } else { return this.displayedValues.filter(it => this.isValueSelected(it)).length === this.displayedValues.length; } } public isNothingSelected() { if (!this.filterParams.selectAllOnMiniFilter || !this.miniFilter) { return this.selectedValuesCount === 0; } else { return this.displayedValues.filter(it => this.isValueSelected(it)).length === 0; } } public getModel(): string[] | null { if (!this.isFilterActive()) { return null; } let selectedValues: any[] = []; Utils.iterateObject(this.selectedValuesMap, (key: string) => { let value = this.keyToValue(key); selectedValues.push(value); }); return selectedValues; } public setModel(model: string[] | null, isSelectAll = false): void { if (this.areValuesSync()) { this.setSyncModel(model, isSelectAll); } else { this.filterValuesExternalPromise.promise.then(values => { this.setSyncModel(model, isSelectAll); this.modelUpdatedFunc(values, model); }); } } private setSyncModel(model: string[] | null, isSelectAll = false): void { if (model && !isSelectAll) { this.selectNothing(); for (let i = 0; i < model.length; i++) { let rawValue = model[i]; let value = this.keyToValue(rawValue); if (value && this.allUniqueValues.indexOf(value) >= 0) { this.selectValue(value); } } } else { this.selectEverything(); } } public onFilterValuesReady(callback: () => void): void { //This guarantees that if the user is racing to set values async into the set filter, only the first instance //will be used // ie Values are async and the user manually wants to override them before the retrieval of values is triggered // (set filter values in the following example) // http://plnkr.co/edit/eFka7ynvPj68tL3VJFWf?p=preview this.filterValuesPromise.firstOneOnly(callback); } }