From 69ed58a46fdb0df728ba4d102187aa6240102f7d Mon Sep 17 00:00:00 2001 From: maks Date: Fri, 23 Aug 2024 14:25:53 +0100 Subject: [PATCH] minimal filter refactoring --- docs | 2 +- src/components.d.ts | 10 +- src/components/data/cell-renderer.tsx | 5 +- src/components/data/column.service.ts | 14 +- src/components/revoGrid/revo-grid.tsx | 3 +- .../{filter.service.ts => filter.indexed.ts} | 43 ++-- src/plugins/filter/filter.panel.tsx | 166 +++++++++------ src/plugins/filter/filter.plugin.tsx | 192 ++++++++++-------- src/plugins/filter/filter.types.ts | 126 +++++++++++- src/types/interfaces.ts | 6 + src/utils/column.utils.ts | 13 ++ 11 files changed, 386 insertions(+), 194 deletions(-) rename src/plugins/filter/{filter.service.ts => filter.indexed.ts} (93%) diff --git a/docs b/docs index c7c5a7ca..d90b52f6 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit c7c5a7ca5a1c8d7bc4bac8cc7034a5f4612ce507 +Subproject commit d90b52f62204e80ecccff2808586f77215b93d16 diff --git a/src/components.d.ts b/src/components.d.ts index 139e4517..4b859d04 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -8,7 +8,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusAfterRenderEvent, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; import { GridPlugin } from "./plugins/base.plugin"; import { AutoSizeColumnConfig } from "./plugins/column.auto-size.plugin"; -import { ColumnFilterConfig, FilterCaptions, FilterCollection } from "./plugins/filter/filter.plugin"; +import { ColumnFilterConfig, FilterCaptions, FilterCollection, LogicFunction, MultiFilterItem, ShowData } from "./plugins/filter/filter.types"; import { GroupingOptions } from "./plugins/groupingRow/grouping.row.types"; import { VNode } from "@stencil/core"; import { FocusedData } from "./components/revoGrid/viewport.service"; @@ -16,15 +16,13 @@ import { ColumnCollection } from "./utils/column.utils"; import { DataInput } from "./plugins/export/types"; import { Observable } from "./utils/store.utils"; import { DSourceState, Groups } from "./store/index"; -import { MultiFilterItem, ShowData } from "./plugins/filter/filter.panel"; -import { LogicFunction } from "./plugins/filter/filter.types"; import { ResizeProps } from "./components/header/resizable.directive"; import { Cell as Cell1, ColumnRegular as ColumnRegular1, DataType as DataType1, DimensionCols as DimensionCols1, DimensionRows as DimensionRows1, DimensionSettingsState as DimensionSettingsState1, Observable as Observable1, SelectionStoreState as SelectionStoreState1 } from "./components"; import { EventData } from "./components/overlay/selection.utils"; export { AfterEditEvent, AllDimensionType, ApplyFocusEvent, BeforeCellRenderEvent, BeforeEdit, BeforeRangeSaveDataDetails, BeforeRowRenderEvent, BeforeSaveDataDetails, Cell, ChangedRange, ColumnDataSchemaModel, ColumnGrouping, ColumnProp, ColumnRegular, ColumnType, DataFormat, DataType, DimensionCols, DimensionRows, DimensionSettingsState, DimensionType, DimensionTypeCol, DragStartEvent, EditCell, EditorCtr, Editors, ElementScroll, FocusAfterRenderEvent, FocusRenderEvent, FocusTemplateFunc, InitialHeaderClick, MultiDimensionType, Nullable, PluginBaseComponent, PositionItem, RangeArea, RangeClipboardCopyEventProps, RangeClipboardPasteEvent, RowDefinition, RowHeaders, SaveDataDetails, SelectionStoreState, TempRange, Theme, ViewportData, ViewPortResizeEvent, ViewPortScrollEvent, ViewportState, ViewSettingSizeProp } from "./types/index"; export { GridPlugin } from "./plugins/base.plugin"; export { AutoSizeColumnConfig } from "./plugins/column.auto-size.plugin"; -export { ColumnFilterConfig, FilterCaptions, FilterCollection } from "./plugins/filter/filter.plugin"; +export { ColumnFilterConfig, FilterCaptions, FilterCollection, LogicFunction, MultiFilterItem, ShowData } from "./plugins/filter/filter.types"; export { GroupingOptions } from "./plugins/groupingRow/grouping.row.types"; export { VNode } from "@stencil/core"; export { FocusedData } from "./components/revoGrid/viewport.service"; @@ -32,8 +30,6 @@ export { ColumnCollection } from "./utils/column.utils"; export { DataInput } from "./plugins/export/types"; export { Observable } from "./utils/store.utils"; export { DSourceState, Groups } from "./store/index"; -export { MultiFilterItem, ShowData } from "./plugins/filter/filter.panel"; -export { LogicFunction } from "./plugins/filter/filter.types"; export { ResizeProps } from "./components/header/resizable.directive"; export { Cell as Cell1, ColumnRegular as ColumnRegular1, DataType as DataType1, DimensionCols as DimensionCols1, DimensionRows as DimensionRows1, DimensionSettingsState as DimensionSettingsState1, Observable as Observable1, SelectionStoreState as SelectionStoreState1 } from "./components"; export { EventData } from "./components/overlay/selection.utils"; @@ -404,7 +400,6 @@ export namespace Components { "filterEntities": Record; "filterItems": MultiFilterItem; "filterNames": Record; - "filterTypes": Record; "getChanges": () => Promise; "show": (newEntity?: ShowData) => Promise; } @@ -1712,7 +1707,6 @@ declare namespace LocalJSX { "filterEntities"?: Record; "filterItems"?: MultiFilterItem; "filterNames"?: Record; - "filterTypes"?: Record; "onFilterChange"?: (event: RevogrFilterPanelCustomEvent) => void; } /** diff --git a/src/components/data/cell-renderer.tsx b/src/components/data/cell-renderer.tsx index 8c28c44c..e45d2cd4 100644 --- a/src/components/data/cell-renderer.tsx +++ b/src/components/data/cell-renderer.tsx @@ -7,11 +7,12 @@ import { } from '@type'; import { + getCellData, DRAGGABLE_CLASS, DRAG_ICON_CLASS, -} from '../../utils/consts'; +} from '../../utils'; -import { getCellData, isRowDragService } from './column.service'; +import { isRowDragService } from './column.service'; interface RenderProps { model: ColumnDataSchemaModel; diff --git a/src/components/data/column.service.ts b/src/components/data/column.service.ts index 23a07ff0..86064e88 100644 --- a/src/components/data/column.service.ts +++ b/src/components/data/column.service.ts @@ -1,5 +1,5 @@ import { DSourceState, getSourceItem, getVisibleSourceItem } from '@store'; -import { CELL_CLASS, DISABLED_CLASS } from '../../utils/consts'; +import { getCellData, Observable, CELL_CLASS, DISABLED_CLASS } from '../../utils'; import { getRange } from '@store'; import { isGroupingColumn } from '../../plugins/groupingRow/grouping.service'; @@ -23,7 +23,6 @@ import { EditorCtr, Editors, } from '@type'; -import { Observable } from '../../utils/store.utils'; export type ColumnStores = { [T in DimensionCols]: Observable>; @@ -108,10 +107,10 @@ export default class ColumnService { colIndex: number, val?: string, ): BeforeSaveDataDetails { + const data = this.rowDataModel(rowIndex, colIndex); if (typeof val === 'undefined') { - val = this.getCellData(rowIndex, colIndex); + val = getCellData(data.model[data.prop as number]); } - const data = this.rowDataModel(rowIndex, colIndex); return { prop: data.prop, rowIndex, @@ -386,13 +385,6 @@ export default class ColumnService { } } -export function getCellData(val?: any) { - if (typeof val === 'undefined' || val === null) { - return ''; - } - return val; -} - /** * Checks if the given rowDrag is a service for dragging rows. */ diff --git a/src/components/revoGrid/revo-grid.tsx b/src/components/revoGrid/revo-grid.tsx index f906048a..cad60aea 100644 --- a/src/components/revoGrid/revo-grid.tsx +++ b/src/components/revoGrid/revo-grid.tsx @@ -57,8 +57,6 @@ import AutoSize, { import { FilterPlugin, - ColumnFilterConfig, - FilterCollection, } from '../../plugins/filter/filter.plugin'; import SortingPlugin from '../../plugins/sorting/sorting.plugin'; import ExportFilePlugin from '../../plugins/export/export.plugin'; @@ -81,6 +79,7 @@ import type { Observable } from '../../utils/store.utils'; import type { GridPlugin } from '../../plugins/base.plugin'; import { ColumnCollection, getColumnByProp, getColumns } from '../../utils/column.utils'; import { WCAGPlugin } from '../../plugins/wcag'; +import { ColumnFilterConfig, FilterCollection } from '../../plugins/filter/filter.types'; /** diff --git a/src/plugins/filter/filter.service.ts b/src/plugins/filter/filter.indexed.ts similarity index 93% rename from src/plugins/filter/filter.service.ts rename to src/plugins/filter/filter.indexed.ts index 40296327..7e8e8db9 100644 --- a/src/plugins/filter/filter.service.ts +++ b/src/plugins/filter/filter.indexed.ts @@ -8,28 +8,8 @@ import beginsWith from './conditions/string/beginswith'; import contains, { notContains } from './conditions/string/contains'; import { LogicFunction } from './filter.types'; -export const filterNames = { - none: 'None', - empty: 'Not set', - notEmpty: 'Set', - eq: 'Equal', - notEq: 'Not equal', - begins: 'Begins with', - contains: 'Contains', - notContains: 'Does not contain', - - eqN: '=', - neqN: '!=', - gt: '>', - gte: '>=', - lt: '<', - lte: '<=', -}; - -export type FilterType = keyof typeof filterNames; - -export const filterEntities: Record = { +export const filterCoreFunctionsIndexedByType: Record = { none: () => true, empty: notSet, notEmpty: set, @@ -51,3 +31,24 @@ export const filterTypes: Record = { string: ['notEmpty', 'empty', 'eq', 'notEq', 'begins', 'contains', 'notContains'], number: ['notEmpty', 'empty', 'eqN', 'neqN', 'gt', 'gte', 'lt', 'lte'], }; + +export const filterNames = { + none: 'None', + empty: 'Not set', + notEmpty: 'Set', + + eq: 'Equal', + notEq: 'Not equal', + begins: 'Begins with', + contains: 'Contains', + notContains: 'Does not contain', + + eqN: '=', + neqN: '!=', + gt: '>', + gte: '>=', + lt: '<', + lte: '<=', +}; + +export type FilterType = keyof typeof filterNames; diff --git a/src/plugins/filter/filter.panel.tsx b/src/plugins/filter/filter.panel.tsx index 06cf2e76..7c4e5745 100644 --- a/src/plugins/filter/filter.panel.tsx +++ b/src/plugins/filter/filter.panel.tsx @@ -1,40 +1,24 @@ -import { Component, Event, EventEmitter, h, Host, Listen, Method, Prop, State, VNode, Element } from '@stencil/core'; -import { FilterType } from './filter.service'; +import { + h, + Component, + Event, + EventEmitter, + Host, + Listen, + Method, + Prop, + State, + VNode, + Element, +} from '@stencil/core'; +import debounce from 'lodash/debounce'; + import { AndOrButton, isFilterBtn, TrashButton } from './filter.button'; import '../../utils/closest.polifill'; -import { LogicFunction } from './filter.types'; -import { FilterCaptions } from './filter.plugin'; -import debounce from 'lodash/debounce'; +import { FilterCaptions, LogicFunction, MultiFilterItem, ShowData } from './filter.types'; import { ColumnProp } from '@type'; +import { FilterType } from './filter.indexed'; -export type FilterItem = { - // column id - prop?: ColumnProp; - // filter type definition - type?: FilterType; - // value for additional filtering, text value or some id - value?: any; -}; - -export type FilterData = { - id: number; - type: FilterType; - value?: any; - relation: 'and' | 'or'; -}; - -export type MultiFilterItem = { - [prop: string]: FilterData[]; -}; - -export type ShowData = { - x: number; - y: number; - /** - * Auto correct position if it is out of document bounds - */ - autoCorrect?: boolean; -} & FilterItem; const defaultType: FilterType = 'none'; @@ -72,7 +56,6 @@ export class FilterPanel { @State() currentFilterType: FilterType = defaultType; @State() changes: ShowData | undefined; @Prop() filterItems: MultiFilterItem = {}; - @Prop() filterTypes: Record = {}; @Prop() filterNames: Record = {}; @Prop() filterEntities: Record = {}; @Prop() filterCaptions: FilterCaptions | undefined; @@ -100,7 +83,11 @@ export class FilterPanel { const isOutside = !path.includes(this.element); - if (e.target instanceof HTMLElement && isOutside && !isFilterBtn(e.target)) { + if ( + e.target instanceof HTMLElement && + isOutside && + !isFilterBtn(e.target) + ) { this.changes = undefined; } } @@ -128,22 +115,33 @@ export class FilterPanel { } renderSelectOptions(type: FilterType, isDefaultTypeRemoved = false) { + if (!this.changes) { + return; + } const options: VNode[] = []; - const prop = this.changes?.prop; + const prop = this.changes.prop; if (!isDefaultTypeRemoved) { - const capts = Object.assign(this.filterCaptionsInternal, this.filterCaptions); + const capts = Object.assign( + this.filterCaptionsInternal, + this.filterCaptions, + ); options.push( - , ); } - for (let gIndex in this.filterTypes) { + for (let gIndex in this.changes.filterTypes) { options.push( - ...this.filterTypes[gIndex].map(k => ( + ...this.changes.filterTypes[gIndex].map(k => ( @@ -159,9 +157,13 @@ export class FilterPanel { if (!currentFilter) return ''; - if (this.filterEntities[currentFilter[index].type].extra !== 'input') return ''; + if (this.filterEntities[currentFilter[index].type].extra !== 'input') + return ''; - const capts = Object.assign(this.filterCaptionsInternal, this.filterCaptions); + const capts = Object.assign( + this.filterCaptionsInternal, + this.filterCaptions, + ); return ( {propFilters.map((d, index) => { @@ -190,7 +195,9 @@ export class FilterPanel { if (index !== this.filterItems[prop].length - 1) { andOrButton = (
this.toggleFilterAndOr(d.id)}> - +
); } @@ -198,8 +205,14 @@ export class FilterPanel { return (
- this.onFilterTypeChange(e, prop, index)} + > + {this.renderSelectOptions( + this.filterItems[prop][index].type, + true, + )}
{andOrButton}
this.onRemoveFilter(d.id)}> @@ -211,7 +224,7 @@ export class FilterPanel { ); })} - {propFilters.length > 0 ?
: ''} + {propFilters.length > 0 ?
: ''}
); } @@ -227,7 +240,7 @@ export class FilterPanel { el.style.left = `${maxLeft - (el.parentElement?.getBoundingClientRect().left ?? 0)}px`; } } - + render() { if (!this.changes) { return ; @@ -238,28 +251,55 @@ export class FilterPanel { top: `${this.changes.y}px`, }; - const capts = Object.assign(this.filterCaptionsInternal, this.filterCaptions); + const capts = Object.assign( + this.filterCaptionsInternal, + this.filterCaptions, + ); return ( - { this.changes?.autoCorrect && this.autoCorrect(el) }}> + { + this.changes?.autoCorrect && this.autoCorrect(el); + }} + >
{this.getFilterItemsList()}
- this.onAddNewFilter(e)} + > {this.renderSelectOptions(this.currentFilterType)}
- {this.disableDynamicFiltering && - - } - -
@@ -278,7 +318,9 @@ export class FilterPanel { // adding setTimeout will wait for the next tick DOM update then focus on input setTimeout(() => { - const input = document.getElementById('filter-input-' + this.filterItems[prop][index].id); + const input = document.getElementById( + 'filter-input-' + this.filterItems[prop][index].id, + ); if (input instanceof HTMLInputElement) { input.focus(); } @@ -328,14 +370,18 @@ export class FilterPanel { // adding setTimeout will wait for the next tick DOM update then focus on input setTimeout(() => { - const input = document.getElementById('filter-input-' + this.currentFilterId) as HTMLInputElement; + const input = document.getElementById( + 'filter-input-' + this.currentFilterId, + ) as HTMLInputElement; if (input) input.focus(); }, 0); } private onUserInput(index: number, prop: ColumnProp, event: Event) { // update the value of the filter item - this.filterItems[prop][index].value = (event.target as HTMLInputElement).value; + this.filterItems[prop][index].value = ( + event.target as HTMLInputElement + ).value; if (!this.disableDynamicFiltering) this.debouncedApplyFilter(); } diff --git a/src/plugins/filter/filter.plugin.tsx b/src/plugins/filter/filter.plugin.tsx index f2d6dcbe..67707b56 100644 --- a/src/plugins/filter/filter.plugin.tsx +++ b/src/plugins/filter/filter.plugin.tsx @@ -3,38 +3,20 @@ import reduce from 'lodash/reduce'; import { BasePlugin } from '../base.plugin'; import { FILTER_PROP, isFilterBtn } from './filter.button'; -import { MultiFilterItem } from './filter.panel'; import { - filterEntities, + filterCoreFunctionsIndexedByType, filterNames, FilterType, filterTypes, -} from './filter.service'; -import { LogicFunction } from './filter.types'; -import { ColumnProp, ColumnRegular, DataType } from '@type'; -import { PluginProviders } from '@type'; - -type CustomFilter = { - columnFilterType: string; // property defined in column filter: string/number/abstract/enum...etc - name: string; - func: LogicFunction; -}; - -export type FilterCaptions = { - title: string; - save: string; - reset: string; - cancel: string; - add: string, - placeholder: string, - and: string, - or: string, -}; - -export type FilterLocalization = { - captions: FilterCaptions; - filterNames: Record; -}; +} from './filter.indexed'; +import { + ColumnFilterConfig, + FilterCollection, + LogicFunction, + MultiFilterItem, +} from './filter.types'; +import { ColumnRegular, DataType, PluginProviders } from '@type'; +import { getCellDataParsed } from 'src/utils'; /** * @typedef ColumnFilterConfig @@ -50,23 +32,6 @@ export type FilterLocalization = { /** * @internal */ -export type ColumnFilterConfig = { - collection?: FilterCollection; - include?: string[]; - customFilters?: Record; - filterProp?: string; - localization?: FilterLocalization; - multiFilterItems?: MultiFilterItem; - disableDynamicFiltering?: boolean; -}; -type HeaderEvent = CustomEvent; -type FilterCollectionItem = { - filter: LogicFunction; - type: FilterType; - value?: any; -}; - -export type FilterCollection = Record; export const FILTER_TRIMMED_TYPE = 'filter'; export const FILTER_CONFIG_CHANGED_EVENT = 'filterconfigchanged'; @@ -75,11 +40,16 @@ export class FilterPlugin extends BasePlugin { pop?: HTMLRevogrFilterPanelElement; filterCollection: FilterCollection = {}; multiFilterItems: MultiFilterItem = {}; - possibleFilters: Record = { ...filterTypes }; - possibleFilterNames: Record = { ...filterNames }; - possibleFilterEntities: Record = { - ...filterEntities, + + filterByType: Record<'string' | 'number' | string, (FilterType | string)[]> = + { ...filterTypes }; + filterNameIndexByType: Record = { + ...filterNames, + }; + filterFunctionsIndexedByType: Record = { + ...filterCoreFunctionsIndexedByType, }; + filterProp = FILTER_PROP; constructor( @@ -92,8 +62,6 @@ export class FilterPlugin extends BasePlugin { this.initConfig(config); } - const headerclick = (e: HeaderEvent) => this.headerclick(e); - const aftersourceset = async () => { const filterCollectionProps = Object.keys(this.filterCollection); if (filterCollectionProps.length > 0) { @@ -113,7 +81,11 @@ export class FilterPlugin extends BasePlugin { } await this.runFiltering(); }; - this.addEventListener('headerclick', headerclick); + this.addEventListener( + 'headerclick', + (e: CustomEvent) => + this.headerclick(e), + ); this.addEventListener( FILTER_CONFIG_CHANGED_EVENT, ({ detail }: CustomEvent) => { @@ -132,13 +104,15 @@ export class FilterPlugin extends BasePlugin { this.onFilterChange(detail), ); - const existingNodes = this.revogrid.registerVNode.filter((n) => n.$tag$ !== 'revogr-filter-panel'); + const existingNodes = this.revogrid.registerVNode.filter( + n => n.$tag$ !== 'revogr-filter-panel', + ); this.revogrid.registerVNode = [ ...existingNodes, this.onFilterChange(e.detail)} disableDynamicFiltering={config?.disableDynamicFiltering} @@ -151,18 +125,20 @@ export class FilterPlugin extends BasePlugin { if (config.multiFilterItems) { this.multiFilterItems = { ...config.multiFilterItems }; } + // Add custom filters if (config.customFilters) { - for (let cType in config.customFilters) { - const cFilter = config.customFilters[cType]; - if (!this.possibleFilters[cFilter.columnFilterType]) { - this.possibleFilters[cFilter.columnFilterType] = []; + for (let customFilterType in config.customFilters) { + const cFilter = config.customFilters[customFilterType]; + if (!this.filterByType[cFilter.columnFilterType]) { + this.filterByType[cFilter.columnFilterType] = []; } - this.possibleFilters[cFilter.columnFilterType].push(cType); - this.possibleFilterEntities[cType] = cFilter.func; - this.possibleFilterNames[cType] = cFilter.name; + this.filterByType[cFilter.columnFilterType].push(customFilterType); + this.filterFunctionsIndexedByType[customFilterType] = cFilter.func; + this.filterNameIndexByType[customFilterType] = cFilter.name; } } + // Add filterProp if provided in config if (config.filterProp) { this.filterProp = config.filterProp; } @@ -175,9 +151,9 @@ export class FilterPlugin extends BasePlugin { if (cfgInlcude) { const filters: Record = {}; - for (let t in this.possibleFilters) { + for (let t in this.filterByType) { // validate filters, if appropriate function present - const newTypes = this.possibleFilters[t].filter( + const newTypes = this.filterByType[t].filter( f => cfgInlcude.indexOf(f) > -1, ); if (newTypes.length) { @@ -186,14 +162,15 @@ export class FilterPlugin extends BasePlugin { } // if any valid filters provided show them if (Object.keys(filters).length > 0) { - this.possibleFilters = filters; + this.filterByType = filters; } } + if (config.collection) { this.filterCollection = reduce( config.collection, (result: FilterCollection, item, prop) => { - if (this.possibleFilterEntities[item.type]) { + if (this.filterFunctionsIndexedByType[item.type]) { result[prop] = item; } else { console.warn(`${item.type} type is not found.`); @@ -207,15 +184,15 @@ export class FilterPlugin extends BasePlugin { if (config.localization) { if (config.localization.filterNames) { Object.entries(config.localization.filterNames).forEach(([k, v]) => { - if (this.possibleFilterNames[k] != void 0) { - this.possibleFilterNames[k] = v; + if (this.filterNameIndexByType[k] != void 0) { + this.filterNameIndexByType[k] = v; } }); } } } - async headerclick(e: HeaderEvent) { + async headerclick(e: CustomEvent) { const el = e.detail.originalEvent?.target as HTMLElement; if (!isFilterBtn(el)) { return; @@ -237,13 +214,14 @@ export class FilterPlugin extends BasePlugin { const gridPos = this.revogrid.getBoundingClientRect(); const buttonPos = el.getBoundingClientRect(); const prop = e.detail.prop; - this.pop.filterTypes = this.getColumnFilter(e.detail.filter); + this.pop.show({ ...this.filterCollection[prop], x: buttonPos.x - gridPos.x, y: buttonPos.y - gridPos.y + buttonPos.height, autoCorrect: true, prop, + filterTypes: this.getColumnFilter(e.detail.filter), }); } @@ -252,7 +230,7 @@ export class FilterPlugin extends BasePlugin { ): Record { let filterType = 'string'; if (!type) { - return { [filterType]: this.possibleFilters[filterType] }; + return { [filterType]: this.filterByType[filterType] }; } // if custom column filter @@ -263,21 +241,26 @@ export class FilterPlugin extends BasePlugin { } else if (typeof type === 'object' && type.length) { return type.reduce((r: Record, multiType) => { if (this.isValidType(multiType)) { - r[multiType] = this.possibleFilters[multiType]; + r[multiType] = this.filterByType[multiType]; } return r; }, {}); } - return { [filterType]: this.possibleFilters[filterType] }; + return { [filterType]: this.filterByType[filterType] }; } isValidType(type: any): type is string { - return !!(typeof type === 'string' && this.possibleFilters[type]); + return !!(typeof type === 'string' && this.filterByType[type]); } - // called on internal component change + /** + * Called on internal component change + */ async onFilterChange(filterItems: MultiFilterItem) { + // store the filter items this.multiFilterItems = filterItems; + + // run the filtering when the items change this.runFiltering(); } @@ -286,30 +269,44 @@ export class FilterPlugin extends BasePlugin { */ async doFiltering( collection: FilterCollection, - items: DataType[], + source: DataType[], columns: ColumnRegular[], filterItems: MultiFilterItem, ) { const columnsToUpdate: ColumnRegular[] = []; + /** + * Loop through the columns and update the columns that need to be updated with the `hasFilter` property. + */ + const columnByProp: Record = {}; columns.forEach(rgCol => { const column = { ...rgCol }; const hasFilter = filterItems[column.prop]; + columnByProp[column.prop] = column; + + /** + * If the column has a filter and it's not already marked as filtered, update the column. + */ if (column[this.filterProp] && !hasFilter) { delete column[this.filterProp]; columnsToUpdate.push(column); } + + /** + * If the column does not have a filter and it's marked as filtered, update the column. + */ + if (!column[this.filterProp] && hasFilter) { columnsToUpdate.push(column); column[this.filterProp] = true; } }); - const itemsToFilter = this.getRowFilter(items, filterItems); + const itemsToTrim = this.getRowFilter(source, filterItems, columnByProp); // check is filter event prevented const { defaultPrevented, detail } = this.emit('beforefiltertrimmed', { collection, - itemsToFilter, - source: items, + itemsToFilter: itemsToTrim, + source, filterItems, }); if (defaultPrevented) { @@ -326,7 +323,7 @@ export class FilterPlugin extends BasePlugin { } // applies the hasFilter to the columns to show filter icon - await this.revogrid.updateColumns(columnsToUpdate); + this.providers.column.updateColumns(columnsToUpdate); this.emit('afterfilterapply'); } @@ -346,7 +343,7 @@ export class FilterPlugin extends BasePlugin { if (this.multiFilterItems[prop].length > 0) { const firstFilterItem = this.multiFilterItems[prop][0]; collection[prop] = { - filter: filterEntities[firstFilterItem.type], + filter: this.filterFunctionsIndexedByType[firstFilterItem.type], type: firstFilterItem.type, value: firstFilterItem.value, }; @@ -380,7 +377,14 @@ export class FilterPlugin extends BasePlugin { }; } - getRowFilter(rows: DataType[], filterItems: MultiFilterItem) { + /** + * Get trimmed rows based on filter + */ + getRowFilter( + rows: DataType[], + filterItems: MultiFilterItem, + columnByProp: Record + ): Record { const propKeys = Object.keys(filterItems); const trimmed: Record = {}; @@ -389,29 +393,40 @@ export class FilterPlugin extends BasePlugin { // each rows rows.forEach((model, rowIndex) => { - // working on all props + // check filter by column properties for (const prop of propKeys) { const propFilters = filterItems[prop]; + // reset the count of satisfied filters propFilterSatisfiedCount = 0; + // reset the array of last filter results lastFilterResults = []; // testing each filter for a prop for (const [filterIndex, filterData] of propFilters.entries()) { // the filter LogicFunction based on the type - const filter = this.possibleFilterEntities[filterData.type]; + const filterFunc = this.filterFunctionsIndexedByType[filterData.type]; // THE MAGIC OF FILTERING IS HERE + const column = columnByProp[prop]; + // If there is no column but user wants to filter by a property + const value = column ? getCellDataParsed(model, columnByProp[prop]) : model[prop]; + // OR relation if (filterData.relation === 'or') { + // reset the array of last filter results lastFilterResults = []; - if (filter(model[prop], filterData.value)) { + // if the filter is satisfied, continue to the next filter + if (filterFunc(value, filterData.value)) { continue; } + // if the filter is not satisfied, count it propFilterSatisfiedCount++; + + // AND relation } else { // 'and' relation will need to know the next filter // so we save this current filter to include it in the next filter - lastFilterResults.push(!filter(model[prop], filterData.value)); + lastFilterResults.push(!filterFunc(value, filterData.value)); // check first if we have a filter on the next index to pair it with this current filter const nextFilterData = propFilters[filterIndex + 1]; @@ -419,20 +434,23 @@ export class FilterPlugin extends BasePlugin { if (!nextFilterData || nextFilterData.relation !== 'and') { // let's just continue since for sure propFilterSatisfiedCount cannot be satisfied if (lastFilterResults.indexOf(true) === -1) { + // reset the array of last filter results lastFilterResults = []; continue; } // we need to add all of the lastFilterResults since we need to satisfy all propFilterSatisfiedCount += lastFilterResults.length; + // reset the array of last filter results lastFilterResults = []; } } } // end of propFilters forEach // add to the list of removed/trimmed rows of filter condition is satisfied - if (propFilterSatisfiedCount === propFilters.length) + if (propFilterSatisfiedCount === propFilters.length) { trimmed[rowIndex] = true; + } } // end of for-of propKeys }); return trimmed; diff --git a/src/plugins/filter/filter.types.ts b/src/plugins/filter/filter.types.ts index ab82277d..a406363a 100644 --- a/src/plugins/filter/filter.types.ts +++ b/src/plugins/filter/filter.types.ts @@ -1,10 +1,132 @@ -export type DateEnum = 'today' | 'yesterday' | 'tomorrow' | 'thisweek' | 'lastweek' | 'nextweek' | 'thismonth' | 'lastmonth' | 'nextmonth' | 'thisyear' | 'lastyear' | 'nextyear'; +import { ColumnProp } from '@type'; +import { FilterType } from './filter.indexed'; + +export type DateEnum = + | 'today' + | 'yesterday' + | 'tomorrow' + | 'thisweek' + | 'lastweek' + | 'nextweek' + | 'thismonth' + | 'lastmonth' + | 'nextmonth' + | 'thisyear' + | 'lastyear' + | 'nextyear'; export type ExtraField = 'input' | 'select' | 'multi' | 'datepicker'; export type LogicFunctionParam = any; -export type LogicFunctionExtraParam = 'select' | 'input' | 'multi' | 'datepicker' | undefined | string | number | Date | DateEnum | null | undefined | string[] | number[]; +export type LogicFunctionExtraParam = + | 'select' + | 'input' + | 'multi' + | 'datepicker' + | undefined + | string + | number + | Date + | DateEnum + | null + | undefined + | string[] + | number[]; export type LogicFunction = { (value: LogicFunctionParam, extra?: LogicFunctionExtraParam): boolean; extra?: ExtraField; }; + +type CustomFilter = { + columnFilterType: string; // property defined in column filter: string/number/abstract/enum...etc + name: string; + func: LogicFunction; +}; + +export type FilterCaptions = { + title: string; + save: string; + reset: string; + cancel: string; + add: string; + placeholder: string; + and: string; + or: string; +}; + +export type FilterLocalization = { + captions: FilterCaptions; + filterNames: Record; +}; +/** + * Filter configuration for a column. This is the type of the `filter` property on a column. + */ +export type ColumnFilterConfig = { + /** + * The collection of filters to be applied to the column. + */ + collection?: FilterCollection; + /** + * The names of the filters to be included in the filter dropdown. + */ + include?: string[]; + /** + * A mapping of custom filter names to custom filter functions. + */ + customFilters?: Record; + /** + * The property on the column idintifying which has the filter is applied. + */ + filterProp?: string; + /** + * The localization for the filter dropdown. + */ + localization?: FilterLocalization; + /** + * Information about the multi-filter items. + */ + multiFilterItems?: MultiFilterItem; + /** + * Whether or not to disable dynamic filtering. If set to true, the filter will only be applied + * when the user clicks on the filter button. + */ + disableDynamicFiltering?: boolean; +}; +type FilterCollectionItem = { + filter: LogicFunction; + type: FilterType; + value?: any; +}; + +export type FilterCollection = Record; + + +export type FilterItem = { + // column id + prop?: ColumnProp; + // filter type definition + type?: FilterType; + // value for additional filtering, text value or some id + value?: any; +}; + +export type FilterData = { + id: number; + type: FilterType; + value?: any; + relation: 'and' | 'or'; +}; + +export type MultiFilterItem = { + [prop: string]: FilterData[]; +}; + +export type ShowData = { + x: number; + y: number; + /** + * Auto correct position if it is out of document bounds + */ + autoCorrect?: boolean; + filterTypes?: Record; +} & FilterItem; diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 03e1be0c..1ea2a5f7 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -181,6 +181,12 @@ export interface ColumnType extends ColumnProperties { * Represents the cell compare function for custom sorting. */ cellCompare?: CellCompareFunc; + + /** + * Represents the cell value parse function for custom parsing. + * Currently only used for filtering. + */ + cellParser?: (model: DataType, column: ColumnRegular) => any; } export type Order = 'asc' | 'desc' | undefined; /** diff --git a/src/utils/column.utils.ts b/src/utils/column.utils.ts index ad18a687..20ced98c 100644 --- a/src/utils/column.utils.ts +++ b/src/utils/column.utils.ts @@ -7,6 +7,7 @@ import { ColumnProp, ColumnRegular, ColumnTypes, + DataType, DimensionCols, ViewSettingSizeProp, } from '@type'; @@ -18,6 +19,18 @@ export interface ColumnGroup extends StoreGroup { } export type ColumnGroupingCollection = Record; +export function getCellData(val?: any) { + if (typeof val === 'undefined' || val === null) { + return ''; + } + return val; +} + +export function getCellDataParsed(model: DataType, column: ColumnRegular) { + const val = column.cellParser ? column.cellParser(model, column) : model[column.prop]; + return getCellData(val); +} + /** * Column collection definition. * Used to access indexed data for columns.