ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
545 lines (460 loc) • 20.2 kB
text/typescript
import {ExternalPromise, Promise, Utils as _} from "../utils";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {PopupService} from "../widgets/popupService";
import {ValueService} from "../valueService/valueService";
import {ColumnController} from "../columnController/columnController";
import {ColumnApi} from "../columnController/columnApi";
import {RowNode} from "../entities/rowNode";
import {Column} from "../entities/column";
import {Autowired, Bean, Context, PostConstruct, PreDestroy} from "../context/context";
import {IRowModel} from "../interfaces/iRowModel";
import {EventService} from "../eventService";
import {ColumnEventType, Events, FilterChangedEvent, FilterModifiedEvent} from "../events";
import {IDoesFilterPassParams, IFilterComp, IFilterParams} from "../interfaces/iFilter";
import {ColDef, GetQuickFilterTextParams} from "../entities/colDef";
import {GridApi} from "../gridApi";
import {ComponentResolver} from "../components/framework/componentResolver";
export class FilterManager {
private $compile: any;
private $scope: any;
private gridOptionsWrapper: GridOptionsWrapper;
private gridCore: any;
private popupService: PopupService;
private valueService: ValueService;
private columnController: ColumnController;
private rowModel: IRowModel;
private eventService: EventService;
private enterprise: boolean;
private context: Context;
private columnApi: ColumnApi;
private gridApi: GridApi;
private componentResolver: ComponentResolver;
public static QUICK_FILTER_SEPARATOR = '\n';
private allFilters: {[p: string]: FilterWrapper} = {};
private quickFilter: string = null;
private quickFilterParts: string[] = null;
private advancedFilterPresent: boolean;
private externalFilterPresent: boolean;
public init(): void {
this.eventService.addEventListener(Events.EVENT_ROW_DATA_CHANGED, this.onNewRowsLoaded.bind(this));
this.eventService.addEventListener(Events.EVENT_NEW_COLUMNS_LOADED, this.onNewColumnsLoaded.bind(this));
this.quickFilter = this.parseQuickFilter(this.gridOptionsWrapper.getQuickFilterText());
this.setQuickFilterParts();
// check this here, in case there is a filter from the start
this.checkExternalFilter();
}
private setQuickFilterParts(): void {
if (this.quickFilter) {
this.quickFilterParts = this.quickFilter.split(' ');
} else {
this.quickFilterParts = null;
}
}
public setFilterModel(model: any) {
let allPromises: Promise<IFilterComp> [] = [];
if (model) {
// mark the filters as we set them, so any active filters left over we stop
let modelKeys = Object.keys(model);
_.iterateObject(this.allFilters, (colId: string, filterWrapper: FilterWrapper) => {
_.removeFromArray(modelKeys, colId);
let newModel = model[colId];
this.setModelOnFilterWrapper(filterWrapper.filterPromise, newModel);
allPromises.push(filterWrapper.filterPromise);
});
// at this point, processedFields contains data for which we don't have a filter working yet
_.iterateArray(modelKeys, (colId) => {
let column = this.columnController.getPrimaryColumn(colId);
if (!column) {
console.warn('Warning ag-grid setFilterModel - no column found for colId ' + colId);
return;
}
let filterWrapper = this.getOrCreateFilterWrapper(column);
this.setModelOnFilterWrapper(filterWrapper.filterPromise, model[colId]);
allPromises.push(filterWrapper.filterPromise);
});
} else {
_.iterateObject(this.allFilters, (key, filterWrapper: FilterWrapper) => {
this.setModelOnFilterWrapper(filterWrapper.filterPromise, null);
allPromises.push(filterWrapper.filterPromise);
});
}
Promise.all(allPromises).then(whatever=> {
this.onFilterChanged();
});
}
private setModelOnFilterWrapper(filterPromise: Promise<IFilterComp>, newModel: any) {
filterPromise.then(filter=> {
if (typeof filter.setModel !== 'function') {
console.warn('Warning ag-grid - filter missing setModel method, which is needed for setFilterModel');
return;
}
filter.setModel(newModel);
});
}
public getFilterModel(): any {
let result = <any>{};
_.iterateObject(this.allFilters, function(key: any, filterWrapper: FilterWrapper) {
// because user can provide filters, we provide useful error checking and messages
let filterPromise: Promise<IFilterComp> = filterWrapper.filterPromise;
let filter = filterPromise.resolveNow(null, filter=>filter);
if (filter == null) { return null; }
if (typeof filter.getModel !== 'function') {
console.warn('Warning ag-grid - filter API missing getModel method, which is needed for getFilterModel');
return;
}
let model = filter.getModel();
if (_.exists(model)) {
result[key] = model;
}
});
return result;
}
// returns true if any advanced filter (ie not quick filter) active
public isAdvancedFilterPresent() {
return this.advancedFilterPresent;
}
private setAdvancedFilterPresent() {
let atLeastOneActive = false;
_.iterateObject(this.allFilters, function(key, filterWrapper: FilterWrapper) {
if (filterWrapper.filterPromise.resolveNow(false, filter=>filter.isFilterActive())) {
atLeastOneActive = true;
}
});
this.advancedFilterPresent = atLeastOneActive;
}
private updateFilterFlagInColumns(source: ColumnEventType): void {
_.iterateObject(this.allFilters, function(key, filterWrapper: FilterWrapper) {
let filterActive = filterWrapper.filterPromise.resolveNow(false, filter=>filter.isFilterActive());
filterWrapper.column.setFilterActive(filterActive, source);
});
}
// returns true if quickFilter or advancedFilter
public isAnyFilterPresent(): boolean {
return this.isQuickFilterPresent() || this.advancedFilterPresent || this.externalFilterPresent;
}
private doesFilterPass(node: RowNode, filterToSkip?: any) {
let data = node.data;
let colKeys = Object.keys(this.allFilters);
for (let i = 0, l = colKeys.length; i < l; i++) { // critical code, don't use functional programming
let colId = colKeys[i];
let filterWrapper: FilterWrapper = this.allFilters[colId];
// if no filter, always pass
if (filterWrapper === undefined) {
continue;
}
let filter: IFilterComp = filterWrapper.filterPromise.resolveNow(undefined, filter=>filter);
// if filter not yet there, continue
if (filter === undefined) {
continue;
}
if (filter === filterToSkip) {
continue;
}
// don't bother with filters that are not active
if (!filter.isFilterActive()) {
continue;
}
if (!filter.doesFilterPass) { // because users can do custom filters, give nice error message
console.error('Filter is missing method doesFilterPass');
}
let params: IDoesFilterPassParams = {
node: node,
data: data
};
if (!filter.doesFilterPass(params)) {
return false;
}
}
// all filters passed
return true;
}
private parseQuickFilter(newFilter: string): string {
if (_.missing(newFilter) || newFilter === "") {
return null;
}
if (this.gridOptionsWrapper.isRowModelInfinite()) {
console.warn('ag-grid: cannot do quick filtering when doing virtual paging');
return null;
}
return newFilter.toUpperCase();
}
// returns true if it has changed (not just same value again)
public setQuickFilter(newFilter: any): void {
let parsedFilter = this.parseQuickFilter(newFilter);
if (this.quickFilter !== parsedFilter) {
this.quickFilter = parsedFilter;
this.setQuickFilterParts();
this.onFilterChanged();
}
}
private checkExternalFilter(): void {
this.externalFilterPresent = this.gridOptionsWrapper.isExternalFilterPresent();
}
public onFilterChanged(): void {
this.setAdvancedFilterPresent();
this.updateFilterFlagInColumns("filterChanged");
this.checkExternalFilter();
_.iterateObject(this.allFilters, function(key, filterWrapper: FilterWrapper) {
filterWrapper.filterPromise.then(filter=> {
if (filter.onAnyFilterChanged) {
filter.onAnyFilterChanged();
}
});
});
let event: FilterChangedEvent = {
type: Events.EVENT_FILTER_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
public isQuickFilterPresent(): boolean {
return this.quickFilter !== null;
}
public doesRowPassOtherFilters(filterToSkip: any, node: any): boolean {
return this.doesRowPassFilter(node, filterToSkip);
}
private doesRowPassQuickFilterNoCache(node: RowNode, filterPart: string): boolean {
let columns = this.columnController.getAllColumnsForQuickFilter();
let filterPasses = false;
columns.forEach( column => {
if (filterPasses) { return; }
let part = this.getQuickFilterTextForColumn(column, node);
if (_.exists(part)) {
if (part.indexOf(filterPart)>=0) {
filterPasses = true;
}
}
});
return filterPasses;
}
private doesRowPassQuickFilterCache(node: any, filterPart: string): boolean {
if (!node.quickFilterAggregateText) {
this.aggregateRowForQuickFilter(node);
}
let filterPasses = node.quickFilterAggregateText.indexOf(filterPart) >= 0;
return filterPasses;
}
private doesRowPassQuickFilter(node: any): boolean {
let filterPasses = true;
let usingCache = this.gridOptionsWrapper.isCacheQuickFilter();
this.quickFilterParts.forEach( filterPart => {
let partPasses = usingCache ?
this.doesRowPassQuickFilterCache(node, filterPart) : this.doesRowPassQuickFilterNoCache(node, filterPart);
// each part must pass, if any fails, then the whole filter fails
if (!partPasses) {
filterPasses = false;
}
});
return filterPasses;
}
public doesRowPassFilter(node: any, filterToSkip?: any): boolean {
// the row must pass ALL of the filters, so if any of them fail,
// we return true. that means if a row passes the quick filter,
// but fails the column filter, it fails overall
// first up, check quick filter
if (this.isQuickFilterPresent()) {
if (!this.doesRowPassQuickFilter(node)) {
return false;
}
}
// secondly, give the client a chance to reject this row
if (this.externalFilterPresent) {
if (!this.gridOptionsWrapper.doesExternalFilterPass(node)) {
return false;
}
}
// lastly, check our internal advanced filter
if (this.advancedFilterPresent) {
if (!this.doesFilterPass(node, filterToSkip)) {
return false;
}
}
// got this far, all filters pass
return true;
}
private getQuickFilterTextForColumn(column: Column, rowNode: RowNode): string {
let value = this.valueService.getValue(column, rowNode, true);
let valueAfterCallback: any;
let colDef = column.getColDef();
if (column.getColDef().getQuickFilterText) {
let params: GetQuickFilterTextParams = {
value: value,
node: rowNode,
data: rowNode.data,
column: column,
colDef: colDef
};
valueAfterCallback = column.getColDef().getQuickFilterText(params);
} else {
valueAfterCallback = value;
}
if (valueAfterCallback && valueAfterCallback !== '') {
return valueAfterCallback.toString().toUpperCase();
} else {
return null;
}
}
private aggregateRowForQuickFilter(node: RowNode) {
let stringParts: string[] = [];
let columns = this.columnController.getAllColumnsForQuickFilter();
columns.forEach( column => {
let part = this.getQuickFilterTextForColumn(column, node);
if (_.exists(part)) {
stringParts.push(part);
}
});
node.quickFilterAggregateText = stringParts.join(FilterManager.QUICK_FILTER_SEPARATOR);
}
private onNewRowsLoaded(source: ColumnEventType) {
_.iterateObject(this.allFilters, function(key, filterWrapper: FilterWrapper) {
filterWrapper.filterPromise.then(filter=> {
if (filter.onNewRowsLoaded) {
filter.onNewRowsLoaded();
}
});
});
this.updateFilterFlagInColumns(source);
this.setAdvancedFilterPresent();
}
private createValueGetter(column: Column) {
return (node: RowNode) => {
return this.valueService.getValue(column, node, true);
};
}
public getFilterComponent(column: Column): Promise<IFilterComp> {
let filterWrapper = this.getOrCreateFilterWrapper(column);
return filterWrapper.filterPromise;
}
public getOrCreateFilterWrapper(column: Column): FilterWrapper {
let filterWrapper: FilterWrapper = this.cachedFilter(column);
if (!filterWrapper) {
filterWrapper = this.createFilterWrapper(column);
this.allFilters[column.getColId()] = filterWrapper;
}
return filterWrapper;
}
public cachedFilter(column: Column): FilterWrapper {
return this.allFilters[column.getColId()];
}
private createFilterInstance(column: Column, $scope: any): Promise<IFilterComp> {
let defaultFilter: string = 'agTextColumnFilter';
if (this.gridOptionsWrapper.isEnterprise()) {
defaultFilter = 'agSetColumnFilter';
}
let sanitisedColDef: ColDef = _.cloneObject(column.getColDef());
let event: FilterModifiedEvent = {
type: Events.EVENT_FILTER_MODIFIED,
api: this.gridApi,
columnApi: this.columnApi
};
let filterChangedCallback = this.onFilterChanged.bind(this);
let filterModifiedCallback = () => this.eventService.dispatchEvent(event);
let params: IFilterParams = {
column: column,
colDef: sanitisedColDef,
rowModel: this.rowModel,
filterChangedCallback: filterChangedCallback,
filterModifiedCallback: filterModifiedCallback,
valueGetter: this.createValueGetter(column),
context: this.gridOptionsWrapper.getContext(),
doesRowPassOtherFilter: null,
$scope: $scope
};
return this.componentResolver.createAgGridComponent<IFilterComp>(
sanitisedColDef,
params,
'filter',
{
api: this.gridApi,
columnApi: this.columnApi,
column: column,
colDef: sanitisedColDef
},
defaultFilter,
true,
(params, filter)=>_.assign(params, {
doesRowPassOtherFilter: this.doesRowPassOtherFilters.bind(this, filter),
})
);
}
private createFilterWrapper(column: Column): FilterWrapper {
let filterWrapper: FilterWrapper = {
column: column,
filterPromise: null,
scope: <any> null,
compiledElement: null,
guiPromise: Promise.external<HTMLElement>()
};
filterWrapper.scope = this.gridOptionsWrapper.isAngularCompileFilters() ? this.$scope.$new() : null;
filterWrapper.filterPromise = this.createFilterInstance(column, filterWrapper.scope);
this.putIntoGui(filterWrapper);
return filterWrapper;
}
private putIntoGui(filterWrapper: FilterWrapper): void {
let eFilterGui = document.createElement('div');
eFilterGui.className = 'ag-filter';
filterWrapper.filterPromise.then(filter=> {
let guiFromFilter = filter.getGui();
if (_.missing(guiFromFilter)) {
console.warn(`getGui method from filter returned ${guiFromFilter}, it should be a DOM element or an HTML template string.`);
}
// for backwards compatibility with Angular 1 - we
// used to allow providing back HTML from getGui().
// once we move away from supporting Angular 1
// directly, we can change this.
if (typeof guiFromFilter === 'string') {
guiFromFilter = _.loadTemplate(<string>guiFromFilter);
}
eFilterGui.appendChild(guiFromFilter);
if (filterWrapper.scope) {
const compiledElement = this.$compile(eFilterGui)(filterWrapper.scope);
filterWrapper.compiledElement = compiledElement;
setTimeout( () => filterWrapper.scope.$apply(), 0);
}
filterWrapper.guiPromise.resolve(eFilterGui);
});
}
private onNewColumnsLoaded(): void {
this.destroy();
}
// destroys the filter, so it not longer takes part
public destroyFilter(column: Column, source: ColumnEventType = "api"): void {
let filterWrapper: FilterWrapper = this.allFilters[column.getColId()];
if (filterWrapper) {
this.disposeFilterWrapper(filterWrapper, source);
this.onFilterChanged();
}
}
private disposeFilterWrapper(filterWrapper: FilterWrapper, source: ColumnEventType): void {
filterWrapper.filterPromise.then(filter=> {
filter.setModel(null);
if (filter.destroy) {
filter.destroy();
}
filterWrapper.column.setFilterActive(false, source);
if (filterWrapper.scope) {
if (filterWrapper.compiledElement) {
filterWrapper.compiledElement.remove();
}
filterWrapper.scope.$destroy();
}
delete this.allFilters[filterWrapper.column.getColId()];
});
}
public destroy() {
_.iterateObject(this.allFilters, (key: string, filterWrapper: any) => {
this.disposeFilterWrapper(filterWrapper, "filterDestroyed");
});
}
}
export interface FilterWrapper {
compiledElement: any;
column: Column;
filterPromise: Promise<IFilterComp>;
scope: any;
guiPromise: ExternalPromise<HTMLElement>;
}