@toolbox-web/grid 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +126 -41
  2. package/all.js +1045 -935
  3. package/all.js.map +1 -1
  4. package/index.js +39 -33
  5. package/index.js.map +1 -1
  6. package/lib/core/grid.d.ts +12 -2
  7. package/lib/core/grid.d.ts.map +1 -1
  8. package/lib/core/internal/header.d.ts.map +1 -1
  9. package/lib/core/internal/keyboard.d.ts.map +1 -1
  10. package/lib/core/types.d.ts +34 -1
  11. package/lib/core/types.d.ts.map +1 -1
  12. package/lib/plugins/clipboard/index.js.map +1 -1
  13. package/lib/plugins/column-virtualization/index.js.map +1 -1
  14. package/lib/plugins/context-menu/index.js.map +1 -1
  15. package/lib/plugins/editing/EditingPlugin.d.ts.map +1 -1
  16. package/lib/plugins/editing/index.js +155 -145
  17. package/lib/plugins/editing/index.js.map +1 -1
  18. package/lib/plugins/export/index.js.map +1 -1
  19. package/lib/plugins/filtering/FilteringPlugin.d.ts +31 -0
  20. package/lib/plugins/filtering/FilteringPlugin.d.ts.map +1 -1
  21. package/lib/plugins/filtering/filter-model.d.ts +30 -3
  22. package/lib/plugins/filtering/filter-model.d.ts.map +1 -1
  23. package/lib/plugins/filtering/index.d.ts +1 -0
  24. package/lib/plugins/filtering/index.d.ts.map +1 -1
  25. package/lib/plugins/filtering/index.js +471 -361
  26. package/lib/plugins/filtering/index.js.map +1 -1
  27. package/lib/plugins/filtering/types.d.ts +32 -0
  28. package/lib/plugins/filtering/types.d.ts.map +1 -1
  29. package/lib/plugins/grouping-columns/index.js.map +1 -1
  30. package/lib/plugins/grouping-rows/index.js.map +1 -1
  31. package/lib/plugins/master-detail/index.js.map +1 -1
  32. package/lib/plugins/multi-sort/MultiSortPlugin.d.ts +4 -0
  33. package/lib/plugins/multi-sort/MultiSortPlugin.d.ts.map +1 -1
  34. package/lib/plugins/multi-sort/index.js +49 -39
  35. package/lib/plugins/multi-sort/index.js.map +1 -1
  36. package/lib/plugins/pinned-columns/index.js.map +1 -1
  37. package/lib/plugins/pinned-rows/index.js.map +1 -1
  38. package/lib/plugins/pivot/index.js.map +1 -1
  39. package/lib/plugins/print/index.js.map +1 -1
  40. package/lib/plugins/reorder/index.js +81 -78
  41. package/lib/plugins/reorder/index.js.map +1 -1
  42. package/lib/plugins/responsive/index.js +58 -55
  43. package/lib/plugins/responsive/index.js.map +1 -1
  44. package/lib/plugins/row-reorder/index.js +5 -2
  45. package/lib/plugins/row-reorder/index.js.map +1 -1
  46. package/lib/plugins/selection/index.js.map +1 -1
  47. package/lib/plugins/server-side/index.js.map +1 -1
  48. package/lib/plugins/tree/index.js.map +1 -1
  49. package/lib/plugins/undo-redo/index.js.map +1 -1
  50. package/lib/plugins/visibility/index.js.map +1 -1
  51. package/package.json +1 -1
  52. package/umd/grid.all.umd.js +29 -29
  53. package/umd/grid.all.umd.js.map +1 -1
  54. package/umd/grid.umd.js +2 -2
  55. package/umd/grid.umd.js.map +1 -1
  56. package/umd/plugins/editing.umd.js +1 -1
  57. package/umd/plugins/editing.umd.js.map +1 -1
  58. package/umd/plugins/filtering.umd.js +1 -1
  59. package/umd/plugins/filtering.umd.js.map +1 -1
  60. package/umd/plugins/multi-sort.umd.js +1 -1
  61. package/umd/plugins/multi-sort.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"filtering.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Convert a value to a comparable number.\n * Handles Date objects, numeric values, and date/ISO strings.\n */\nfunction toNumeric(value: unknown): number {\n if (value instanceof Date) return value.getTime();\n const n = Number(value);\n if (!isNaN(n)) return n;\n // Try parsing as a date string (ISO 8601, etc.)\n const d = new Date(value as string);\n return d.getTime(); // NaN if unparseable\n}\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(row: Record<string, unknown>, filter: FilterModel, caseSensitive = false): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // Set operators handle null explicitly: null is never \"in\" a set,\n // and null is never excluded by \"notIn\" (it's not a listed value).\n if (filter.operator === 'notIn') {\n if (rawValue == null) return true;\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n }\n if (filter.operator === 'in') {\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const filterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(filterValue);\n\n case 'notContains':\n return !compareValue.includes(filterValue);\n\n case 'equals':\n return compareValue === filterValue;\n\n case 'notEquals':\n return compareValue !== filterValue;\n\n case 'startsWith':\n return compareValue.startsWith(filterValue);\n\n case 'endsWith':\n return compareValue.endsWith(filterValue);\n\n // Number/Date operators (use toNumeric for Date objects and date strings)\n case 'lessThan':\n return toNumeric(rawValue) < toNumeric(filter.value);\n\n case 'lessThanOrEqual':\n return toNumeric(rawValue) <= toNumeric(filter.value);\n\n case 'greaterThan':\n return toNumeric(rawValue) > toNumeric(filter.value);\n\n case 'greaterThanOrEqual':\n return toNumeric(rawValue) >= toNumeric(filter.value);\n\n case 'between':\n return toNumeric(rawValue) >= toNumeric(filter.value) && toNumeric(rawValue) <= toNumeric(filter.valueTo);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false,\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) => filters.every((f) => matchesFilter(row, f, caseSensitive)));\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n })),\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(rows: T[], field: string): unknown[] {\n const values = new Set<unknown>();\n for (const row of rows) {\n const value = row[field];\n if (value != null) {\n values.add(value);\n }\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport { isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport { computeFilterCacheKey, filterRows, getUniqueValues } from './filter-model';\nimport styles from './filtering.css?inline';\nimport filterPanelStyles from './FilteringPlugin.css?inline';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * Adds column header filters with text search, dropdown options, and custom filter panels.\n * Supports both **local filtering** for small datasets and **async handlers** for server-side\n * filtering on large datasets.\n *\n * ## Installation\n *\n * ```ts\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `debounceMs` | `number` | `300` | Debounce delay for filter input |\n * | `caseSensitive` | `boolean` | `false` | Case-sensitive string matching |\n * | `trimInput` | `boolean` | `true` | Trim whitespace from filter input |\n * | `useWorker` | `boolean` | `true` | Use Web Worker for datasets >1000 rows |\n * | `filterPanelRenderer` | `FilterPanelRenderer` | - | Custom filter panel renderer |\n * | `valuesHandler` | `FilterValuesHandler` | - | Async handler to fetch unique filter values |\n * | `filterHandler` | `FilterHandler<TRow>` | - | Async handler to apply filters remotely |\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `filterable` | `boolean` | Enable filtering for this column |\n * | `filterType` | `'text' \\| 'select' \\| 'number' \\| 'date'` | Filter UI type |\n * | `filterOptions` | `unknown[]` | Predefined options for select filters |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setFilter` | `(field, value) => void` | Set filter value for a column |\n * | `getFilters` | `() => FilterModel[]` | Get all current filters |\n * | `clearFilters` | `() => void` | Clear all filters |\n * | `clearFilter` | `(field) => void` | Clear filter for a specific column |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-filter-panel-bg` | `var(--tbw-color-panel-bg)` | Panel background |\n * | `--tbw-filter-panel-fg` | `var(--tbw-color-fg)` | Panel text color |\n * | `--tbw-filter-panel-border` | `var(--tbw-color-border)` | Panel border |\n * | `--tbw-filter-active-color` | `var(--tbw-color-accent)` | Active filter indicator |\n * | `--tbw-filter-input-bg` | `var(--tbw-color-bg)` | Input background |\n * | `--tbw-filter-input-focus` | `var(--tbw-color-accent)` | Input focus border |\n *\n * @example Basic Usage with Filterable Columns\n * ```ts\n * import '@toolbox-web/grid';\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', filterable: true },\n * { field: 'status', header: 'Status', filterable: true, filterType: 'select' },\n * { field: 'email', header: 'Email', filterable: true },\n * ],\n * plugins: [new FilteringPlugin({ debounceMs: 300 })],\n * };\n * grid.rows = data;\n * ```\n *\n * @example Server-Side Filtering with Async Handlers\n * ```ts\n * new FilteringPlugin({\n * // Fetch unique values from server for filter dropdown\n * valuesHandler: async (field, column) => {\n * const response = await fetch(`/api/distinct-values?field=${field}`);\n * return response.json();\n * },\n * // Apply filters on the server\n * filterHandler: async (filters, currentRows) => {\n * const response = await fetch('/api/data', {\n * method: 'POST',\n * body: JSON.stringify({ filters }),\n * });\n * return response.json();\n * },\n * });\n * ```\n *\n * @see {@link FilterConfig} for all configuration options\n * @see {@link FilterModel} for filter data structure\n * @see {@link FilterPanelParams} for custom panel renderer parameters\n *\n * @internal Extends BaseGridPlugin\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n /**\n * Plugin manifest - declares events emitted by this plugin.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n events: [\n {\n type: 'filter-applied',\n description: 'Emitted when filter criteria change. Subscribers can react to row visibility changes.',\n },\n ],\n queries: [\n {\n type: 'getContextMenuItems',\n description: 'Contributes filter-related items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'filtering';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // #region Helpers\n\n /**\n * Check if filtering is enabled at the grid level.\n * Grid-wide `filterable: false` disables filtering for all columns.\n */\n private isFilteringEnabled(): boolean {\n return this.grid.effectiveConfig?.filterable !== false;\n }\n\n /**\n * Check if a specific column is filterable, respecting both grid-level and column-level settings.\n */\n private isColumnFilterable(col: { filterable?: boolean; field?: string }): boolean {\n if (!this.isFilteringEnabled()) return false;\n return col.filterable !== false;\n }\n\n // #endregion\n\n // #region Internal State\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n /** Spot-check of input rows for cache invalidation when upstream plugins (e.g. sort) change row order */\n private cachedInputSpot: { len: number; first: unknown; mid: unknown; last: unknown } | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private panelAnchorElement: HTMLElement | null = null; // For CSS anchor positioning cleanup\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private panelAbortController: AbortController | null = null; // For panel-scoped listeners\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly DEFAULT_LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n /**\n * Get the item height from CSS variable or fallback to default.\n * Reads --tbw-filter-item-height from the panel element.\n */\n private getListItemHeight(): number {\n if (this.panelElement) {\n const cssValue = getComputedStyle(this.panelElement).getPropertyValue('--tbw-filter-item-height');\n if (cssValue && cssValue.trim()) {\n const parsed = parseFloat(cssValue);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n }\n return FilteringPlugin.DEFAULT_LIST_ITEM_HEIGHT;\n }\n\n /**\n * Sync excludedValues map from a filter model (for set filters).\n */\n private syncExcludedValues(field: string, filter: FilterModel | null): void {\n if (!filter) {\n this.excludedValues.delete(field);\n } else if (filter.type === 'set' && filter.operator === 'notIn' && Array.isArray(filter.value)) {\n this.excludedValues.set(field, new Set(filter.value));\n } else if (filter.type === 'set') {\n // Other set operators may have different semantics; clear for safety\n this.excludedValues.delete(field);\n }\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n /** @internal */\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n // Abort panel-scoped listeners (document click handler, etc.)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n // #endregion\n\n // #region Query Handlers\n\n /**\n * Handle inter-plugin queries.\n * Contributes filter-related items to the header context menu.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Only contribute items if filtering is enabled for this column\n if (!this.isFilteringEnabled()) return undefined;\n if (!this.isColumnFilterable(column)) return undefined;\n\n const items: HeaderContextMenuItem[] = [];\n const fieldFiltered = this.isFieldFiltered(column.field);\n const hasAnyFilter = this.filters.size > 0;\n\n if (fieldFiltered) {\n items.push({\n id: 'filtering/clear-column-filter',\n label: `Clear Filter`,\n icon: '✕',\n order: 20,\n action: () => this.clearFieldFilter(column.field),\n });\n }\n\n if (hasAnyFilter) {\n items.push({\n id: 'filtering/clear-all-filters',\n label: 'Clear All Filters',\n icon: '✕',\n order: 21,\n disabled: !hasAnyFilter,\n action: () => this.clearAllFilters(),\n });\n }\n\n return items.length > 0 ? items : undefined;\n }\n return undefined;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // If using async filterHandler, processRows becomes a passthrough\n // Actual filtering happens in applyFiltersAsync and rows are set directly on grid\n if (this.config.filterHandler) {\n // Return cached result if available (set by async handler)\n if (this.cachedResult) return this.cachedResult;\n // Otherwise return rows as-is (filtering happens async)\n return [...rows];\n }\n\n // Check cache — also verify input rows haven't changed (e.g. due to sort)\n const newCacheKey = computeFilterCacheKey(filterList);\n const inputSpot = {\n len: rows.length,\n first: rows[0],\n mid: rows[Math.floor(rows.length / 2)],\n last: rows[rows.length - 1],\n };\n const inputUnchanged =\n this.cachedInputSpot != null &&\n inputSpot.len === this.cachedInputSpot.len &&\n inputSpot.first === this.cachedInputSpot.first &&\n inputSpot.mid === this.cachedInputSpot.mid &&\n inputSpot.last === this.cachedInputSpot.last;\n\n if (this.cacheKey === newCacheKey && this.cachedResult && inputUnchanged) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows([...rows] as Record<string, unknown>[], filterList, this.config.caseSensitive);\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n this.cachedInputSpot = inputSpot;\n\n return result;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = gridEl.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n // Use visibleColumns since data-col is the index within _visibleColumns\n const col = this.visibleColumns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || !this.isColumnFilterable(col)) return;\n\n // Skip utility columns (expander, selection checkbox, etc.)\n if (isUtilityColumn(col)) return;\n\n const field = col.field;\n if (!field) return;\n\n const hasFilter = this.filters.has(field);\n\n // Check if button already exists\n let filterBtn = cell.querySelector('.tbw-filter-btn') as HTMLElement | null;\n\n if (filterBtn) {\n // Update active state and icon of existing button\n const wasActive = filterBtn.classList.contains('active');\n filterBtn.classList.toggle('active', hasFilter);\n (cell as HTMLElement).classList.toggle('filtered', hasFilter);\n // Update icon if active state changed\n if (wasActive !== hasFilter) {\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n }\n return;\n }\n\n // Create filter button\n filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n // Use grid icons configuration\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n\n // Mark button as active if filter exists\n if (hasFilter) {\n filterBtn.classList.add('active');\n (cell as HTMLElement).classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn!);\n });\n\n // Insert before resize handle to maintain order: [label, sort-indicator, filter-btn, resize-handle]\n const resizeHandle = cell.querySelector('.resize-handle');\n if (resizeHandle) {\n cell.insertBefore(filterBtn, resizeHandle);\n } else {\n cell.appendChild(filterBtn);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n this.syncExcludedValues(field, null);\n } else {\n const fullFilter = { ...filter, field };\n this.filters.set(field, fullFilter);\n this.syncExcludedValues(field, fullFilter);\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n this.excludedValues.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n this.syncExcludedValues(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n\n this.applyFiltersInternal();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.applyFiltersInternal();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n */\n getUniqueValues(field: string): unknown[] {\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Copy CSS classes and data attributes from grid to filter panel.\n * This ensures theme classes (e.g., .eds-theme) cascade to the panel.\n */\n private copyGridThemeContext(panel: HTMLElement): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Copy all CSS classes from grid to panel (except internal ones)\n for (const className of gridEl.classList) {\n // Skip internal classes that shouldn't be copied\n if (className.startsWith('tbw-') || className === 'selecting') continue;\n panel.classList.add(className);\n }\n\n // Copy data-theme attribute if present\n const theme = gridEl.dataset.theme;\n if (theme) {\n panel.dataset.theme = theme;\n }\n }\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n // Only inject if we have valid CSS text (Vite's ?inline import)\n // When importing from source without Vite, the import is a module object, not a string\n if (typeof filterPanelStyles !== 'string' || !filterPanelStyles) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n // Copy theme classes from grid to panel for proper theming\n this.copyGridThemeContext(panel);\n // Add animation class if animations are enabled\n if (this.isAnimationEnabled) {\n panel.classList.add('tbw-filter-panel-animated');\n }\n this.panelElement = panel;\n this.openPanelField = field;\n\n // If using async valuesHandler, show loading state and fetch values\n if (this.config.valuesHandler) {\n panel.innerHTML = '<div class=\"tbw-filter-loading\">Loading...</div>';\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n this.setupPanelCloseHandler(panel, buttonEl);\n\n this.config.valuesHandler(field, column).then((values) => {\n // Check if panel is still open for this field\n if (this.openPanelField !== field || !this.panelElement) return;\n panel.innerHTML = '';\n this.renderPanelContent(field, column, panel, values);\n });\n return;\n }\n\n // Sync path: get unique values from local rows\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n\n // Position and append to body BEFORE rendering content\n // so getListItemHeight() can read CSS variables from computed styles\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n this.renderPanelContent(field, column, panel, uniqueValues);\n this.setupPanelCloseHandler(panel, buttonEl);\n }\n\n /**\n * Render filter panel content with given values\n */\n private renderPanelContent(field: string, column: ColumnConfig, panel: HTMLElement, uniqueValues: unknown[]): void {\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n // Resolution order: plugin config → typeDefaults → built-in\n let usedCustomRenderer = false;\n\n // 1. Check plugin-level filterPanelRenderer\n if (this.config.filterPanelRenderer) {\n this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n\n // 2. Check typeDefaults for this column's type\n if (!usedCustomRenderer && column.type) {\n const typeDefault = this.grid.effectiveConfig.typeDefaults?.[column.type];\n if (typeDefault?.filterPanelRenderer) {\n typeDefault.filterPanelRenderer(panel, params);\n usedCustomRenderer = panel.children.length > 0;\n }\n }\n\n // 3. Fall back to built-in type-specific panel renderers\n if (!usedCustomRenderer) {\n const columnType = column.type;\n if (columnType === 'number') {\n this.renderNumberFilterPanel(panel, params, uniqueValues);\n } else if (columnType === 'date') {\n this.renderDateFilterPanel(panel, params, uniqueValues);\n } else {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n }\n }\n\n /**\n * Setup click-outside handler to close the panel\n */\n private setupPanelCloseHandler(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Create abort controller for panel-scoped listeners\n // This allows cleanup when panel closes OR when grid disconnects\n this.panelAbortController = new AbortController();\n\n // Add global click handler to close on outside click\n // Defer to next tick to avoid immediate close from the click that opened the panel\n setTimeout(() => {\n document.addEventListener(\n 'click',\n (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n },\n { signal: this.panelAbortController?.signal },\n );\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n const panel = this.panelElement;\n if (panel) {\n panel.remove();\n this.panelElement = null;\n }\n // Clean up anchor name from header cell\n if (this.panelAnchorElement) {\n (this.panelAnchorElement.style as any).anchorName = '';\n this.panelAnchorElement = null;\n }\n this.openPanelField = null;\n // Abort panel-scoped listeners (document click handler)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n /** Cache for CSS anchor positioning support check */\n private static supportsAnchorPositioning: boolean | null = null;\n\n /**\n * Check if browser supports CSS Anchor Positioning\n */\n private static checkAnchorPositioningSupport(): boolean {\n if (FilteringPlugin.supportsAnchorPositioning === null) {\n FilteringPlugin.supportsAnchorPositioning = CSS.supports('anchor-name', '--test');\n }\n return FilteringPlugin.supportsAnchorPositioning;\n }\n\n /**\n * Position the panel below the header cell\n * Uses CSS Anchor Positioning if supported, falls back to JS positioning\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Find the parent header cell\n const headerCell = buttonEl.closest('.cell') as HTMLElement | null;\n const anchorEl = headerCell ?? buttonEl;\n\n // Set anchor name on the header cell for CSS anchor positioning\n (anchorEl.style as any).anchorName = '--tbw-filter-anchor';\n this.panelAnchorElement = anchorEl; // Store for cleanup\n\n // If CSS Anchor Positioning is supported, CSS handles positioning\n // but we need to detect if it flipped above to adjust animation\n if (FilteringPlugin.checkAnchorPositioningSupport()) {\n // Check position after CSS anchor positioning takes effect\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n const anchorRect = anchorEl.getBoundingClientRect();\n // If panel top is above anchor top, it flipped to above\n if (panelRect.top < anchorRect.top) {\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n return;\n }\n\n // Fallback: JS-based positioning for older browsers\n const rect = anchorEl.getBoundingClientRect();\n\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows viewport edges\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n\n // Check horizontal overflow - align right edge to header cell right edge\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${rect.right - panelRect.width}px`;\n }\n\n // Check vertical overflow - flip to above header cell\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>,\n ): void {\n const { field } = params;\n // Get item height from CSS variable or use default\n const itemHeight = this.getListItemHeight();\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const strValue = value == null ? '(Blank)' : String(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `calc(var(--tbw-filter-item-height, 28px) * ${index})`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = strValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * itemHeight}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: itemHeight,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const caseSensitive = this.config.caseSensitive ?? false;\n const compareFilter = caseSensitive ? filterText : filterText.toLowerCase();\n\n // Filter the unique values\n filteredValues = uniqueValues.filter((value) => {\n const strValue = value == null ? '(Blank)' : String(value);\n const compareValue = caseSensitive ? strValue : strValue.toLowerCase();\n return !filterText || compareValue.includes(compareFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true },\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a number range filter panel with min/max inputs and slider\n */\n private renderNumberFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: number; max?: number; step?: number } | undefined;\n\n // Helper to convert to number\n const toNumber = (val: unknown, fallback: number): number => {\n if (typeof val === 'number') return val;\n if (typeof val === 'string') {\n const num = parseFloat(val);\n return isNaN(num) ? fallback : num;\n }\n return fallback;\n };\n\n // Compute min/max from data if not specified\n const numericValues = uniqueValues.filter((v) => typeof v === 'number' && !isNaN(v)) as number[];\n const dataMin = numericValues.length > 0 ? Math.min(...numericValues) : 0;\n const dataMax = numericValues.length > 0 ? Math.max(...numericValues) : 100;\n\n const min = toNumber(filterParams?.min ?? editorParams?.min, dataMin);\n const max = toNumber(filterParams?.max ?? editorParams?.max, dataMax);\n const step = filterParams?.step ?? editorParams?.step ?? 1;\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentMin = min;\n let currentMax = max;\n if (currentFilter?.operator === 'between') {\n currentMin = toNumber(currentFilter.value, min);\n currentMax = toNumber(currentFilter.valueTo, max);\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentMin = toNumber(currentFilter.value, min);\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentMax = toNumber(currentFilter.value, max);\n }\n\n // Range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-range-inputs';\n\n // Min input\n const minGroup = document.createElement('div');\n minGroup.className = 'tbw-filter-range-group';\n\n const minLabel = document.createElement('label');\n minLabel.textContent = 'Min';\n minLabel.className = 'tbw-filter-range-label';\n\n const minInput = document.createElement('input');\n minInput.type = 'number';\n minInput.className = 'tbw-filter-range-input';\n minInput.min = String(min);\n minInput.max = String(max);\n minInput.step = String(step);\n minInput.value = String(currentMin);\n\n minGroup.appendChild(minLabel);\n minGroup.appendChild(minInput);\n rangeContainer.appendChild(minGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // Max input\n const maxGroup = document.createElement('div');\n maxGroup.className = 'tbw-filter-range-group';\n\n const maxLabel = document.createElement('label');\n maxLabel.textContent = 'Max';\n maxLabel.className = 'tbw-filter-range-label';\n\n const maxInput = document.createElement('input');\n maxInput.type = 'number';\n maxInput.className = 'tbw-filter-range-input';\n maxInput.min = String(min);\n maxInput.max = String(max);\n maxInput.step = String(step);\n maxInput.value = String(currentMax);\n\n maxGroup.appendChild(maxLabel);\n maxGroup.appendChild(maxInput);\n rangeContainer.appendChild(maxGroup);\n\n panel.appendChild(rangeContainer);\n\n // Range slider (dual thumb using two range inputs)\n const sliderContainer = document.createElement('div');\n sliderContainer.className = 'tbw-filter-range-slider';\n\n const sliderTrack = document.createElement('div');\n sliderTrack.className = 'tbw-filter-range-track';\n\n const sliderFill = document.createElement('div');\n sliderFill.className = 'tbw-filter-range-fill';\n\n const minSlider = document.createElement('input');\n minSlider.type = 'range';\n minSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-min';\n minSlider.min = String(min);\n minSlider.max = String(max);\n minSlider.step = String(step);\n minSlider.value = String(currentMin);\n\n const maxSlider = document.createElement('input');\n maxSlider.type = 'range';\n maxSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-max';\n maxSlider.min = String(min);\n maxSlider.max = String(max);\n maxSlider.step = String(step);\n maxSlider.value = String(currentMax);\n\n sliderContainer.appendChild(sliderTrack);\n sliderContainer.appendChild(sliderFill);\n sliderContainer.appendChild(minSlider);\n sliderContainer.appendChild(maxSlider);\n panel.appendChild(sliderContainer);\n\n // Update fill position\n const updateFill = () => {\n const minVal = parseFloat(minSlider.value);\n const maxVal = parseFloat(maxSlider.value);\n const range = max - min;\n const leftPercent = ((minVal - min) / range) * 100;\n const rightPercent = ((maxVal - min) / range) * 100;\n sliderFill.style.left = `${leftPercent}%`;\n sliderFill.style.width = `${rightPercent - leftPercent}%`;\n };\n\n // Sync inputs with sliders\n minSlider.addEventListener('input', () => {\n const val = Math.min(parseFloat(minSlider.value), parseFloat(maxSlider.value));\n minSlider.value = String(val);\n minInput.value = String(val);\n updateFill();\n });\n\n maxSlider.addEventListener('input', () => {\n const val = Math.max(parseFloat(maxSlider.value), parseFloat(minSlider.value));\n maxSlider.value = String(val);\n maxInput.value = String(val);\n updateFill();\n });\n\n // Sync sliders with inputs\n minInput.addEventListener('input', () => {\n let val = parseFloat(minInput.value) || min;\n val = Math.max(min, Math.min(val, parseFloat(maxInput.value)));\n minSlider.value = String(val);\n updateFill();\n });\n\n maxInput.addEventListener('input', () => {\n let val = parseFloat(maxInput.value) || max;\n val = Math.min(max, Math.max(val, parseFloat(minInput.value)));\n maxSlider.value = String(val);\n updateFill();\n });\n\n // Initialize fill\n updateFill();\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n const minVal = parseFloat(minInput.value);\n const maxVal = parseFloat(maxInput.value);\n params.applyTextFilter('between', minVal, maxVal);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a date range filter panel with from/to date inputs\n */\n private renderDateFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: string; max?: string } | undefined;\n\n // Compute min/max from data if not specified\n const dateValues = uniqueValues\n .filter((v) => v instanceof Date || (typeof v === 'string' && !isNaN(Date.parse(v))))\n .map((v) => (v instanceof Date ? v : new Date(v as string)))\n .filter((d) => !isNaN(d.getTime()));\n\n const dataMin = dateValues.length > 0 ? new Date(Math.min(...dateValues.map((d) => d.getTime()))) : null;\n const dataMax = dateValues.length > 0 ? new Date(Math.max(...dateValues.map((d) => d.getTime()))) : null;\n\n // Format date for input[type=\"date\"] (YYYY-MM-DD)\n const formatDateForInput = (date: Date | null): string => {\n if (!date) return '';\n return date.toISOString().split('T')[0];\n };\n\n const parseFilterParam = (value: unknown): string => {\n if (!value) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatDateForInput(new Date(value));\n return '';\n };\n\n const minDate =\n parseFilterParam(filterParams?.min) || parseFilterParam(editorParams?.min) || formatDateForInput(dataMin);\n const maxDate =\n parseFilterParam(filterParams?.max) || parseFilterParam(editorParams?.max) || formatDateForInput(dataMax);\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentFrom = '';\n let currentTo = '';\n const isBlankFilter = currentFilter?.operator === 'blank';\n if (currentFilter?.operator === 'between') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n currentTo = parseFilterParam(currentFilter.valueTo) || '';\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentTo = parseFilterParam(currentFilter.value) || '';\n }\n\n // Date range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-date-range';\n\n // From input\n const fromGroup = document.createElement('div');\n fromGroup.className = 'tbw-filter-date-group';\n\n const fromLabel = document.createElement('label');\n fromLabel.textContent = 'From';\n fromLabel.className = 'tbw-filter-range-label';\n\n const fromInput = document.createElement('input');\n fromInput.type = 'date';\n fromInput.className = 'tbw-filter-date-input';\n if (minDate) fromInput.min = minDate;\n if (maxDate) fromInput.max = maxDate;\n fromInput.value = currentFrom;\n\n fromGroup.appendChild(fromLabel);\n fromGroup.appendChild(fromInput);\n rangeContainer.appendChild(fromGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // To input\n const toGroup = document.createElement('div');\n toGroup.className = 'tbw-filter-date-group';\n\n const toLabel = document.createElement('label');\n toLabel.textContent = 'To';\n toLabel.className = 'tbw-filter-range-label';\n\n const toInput = document.createElement('input');\n toInput.type = 'date';\n toInput.className = 'tbw-filter-date-input';\n if (minDate) toInput.min = minDate;\n if (maxDate) toInput.max = maxDate;\n toInput.value = currentTo;\n\n toGroup.appendChild(toLabel);\n toGroup.appendChild(toInput);\n rangeContainer.appendChild(toGroup);\n\n panel.appendChild(rangeContainer);\n\n // \"Show only blank\" checkbox\n const blankRow = document.createElement('label');\n blankRow.className = 'tbw-filter-blank-option';\n\n const blankCheckbox = document.createElement('input');\n blankCheckbox.type = 'checkbox';\n blankCheckbox.className = 'tbw-filter-blank-checkbox';\n blankCheckbox.checked = isBlankFilter;\n\n const blankLabel = document.createTextNode('Show only blank');\n blankRow.appendChild(blankCheckbox);\n blankRow.appendChild(blankLabel);\n\n // Toggle date inputs disabled state when blank is checked\n const toggleDateInputs = (disabled: boolean): void => {\n fromInput.disabled = disabled;\n toInput.disabled = disabled;\n rangeContainer.classList.toggle('tbw-filter-disabled', disabled);\n };\n toggleDateInputs(isBlankFilter);\n\n blankCheckbox.addEventListener('change', () => {\n toggleDateInputs(blankCheckbox.checked);\n });\n\n panel.appendChild(blankRow);\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n if (blankCheckbox.checked) {\n params.applyTextFilter('blank', '');\n return;\n }\n\n const from = fromInput.value;\n const to = toInput.value;\n\n if (from && to) {\n params.applyTextFilter('between', from, to);\n } else if (from) {\n params.applyTextFilter('greaterThanOrEqual', from);\n } else if (to) {\n params.applyTextFilter('lessThanOrEqual', to);\n } else {\n params.clearFilter();\n }\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.applyFiltersInternal();\n }\n\n /**\n * Apply a text/number/date filter\n */\n private applyTextFilter(\n field: string,\n operator: FilterModel['operator'],\n value: string | number,\n valueTo?: string | number,\n ): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.applyFiltersInternal();\n }\n\n /**\n * Internal method to apply filters (sync or async based on config)\n */\n private applyFiltersInternal(): void {\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n const filterList = [...this.filters.values()];\n\n // If using async filterHandler, delegate to server\n if (this.config.filterHandler) {\n const gridEl = this.grid as unknown as Element;\n gridEl.setAttribute('aria-busy', 'true');\n\n const result = this.config.filterHandler(filterList, this.sourceRows as unknown[]);\n\n // Handle async or sync result\n const handleResult = (rows: unknown[]) => {\n gridEl.removeAttribute('aria-busy');\n this.cachedResult = rows;\n\n // Update grid rows directly for async filtering\n (this.grid as unknown as { rows: unknown[] }).rows = rows;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: rows.length,\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n\n // Trigger afterRender to update filter button active state\n this.requestRender();\n };\n\n if (result && typeof (result as Promise<unknown[]>).then === 'function') {\n (result as Promise<unknown[]>).then(handleResult);\n } else {\n handleResult(result as unknown[]);\n }\n return;\n }\n\n // Sync path: emit event and re-render (processRows will handle filtering)\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: 0,\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n this.requestRender();\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return filter state for a column if it has an active filter.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n }\n // #endregion\n}\n"],"names":["toNumeric","value","n","matchesFilter","row","filter","caseSensitive","rawValue","stringValue","compareValue","filterValue","filterRows","rows","filters","f","computeFilterCacheKey","getUniqueValues","field","values","a","b","FilteringPlugin","BaseGridPlugin","styles","col","cssValue","parsed","grid","query","params","column","items","fieldFiltered","hasAnyFilter","filterList","newCacheKey","inputSpot","inputUnchanged","result","gridEl","cell","colIndex","isUtilityColumn","hasFilter","filterBtn","wasActive","iconName","e","resizeHandle","fullFilter","panel","className","theme","style","filterPanelStyles","buttonEl","uniqueValues","excludedSet","currentSearchText","excluded","operator","valueTo","usedCustomRenderer","typeDefault","columnType","anchorEl","panelRect","anchorRect","rect","excludedValues","itemHeight","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","v","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","strValue","item","checkbox","label","totalItems","viewportHeight","scrollTop","shouldBypassVirtualization","idx","window","computeVirtualWindow","i","renderValues","filterText","compareFilter","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterParams","editorParams","toNumber","val","fallback","num","numericValues","dataMin","dataMax","min","max","step","currentFilter","currentMin","currentMax","rangeContainer","minGroup","minLabel","minInput","separator","maxGroup","maxLabel","maxInput","sliderContainer","sliderTrack","sliderFill","minSlider","maxSlider","updateFill","minVal","maxVal","range","leftPercent","rightPercent","dateValues","formatDateForInput","date","parseFilterParam","minDate","maxDate","currentFrom","currentTo","isBlankFilter","fromGroup","fromLabel","fromInput","toGroup","toLabel","toInput","blankRow","blankCheckbox","blankLabel","toggleDateInputs","disabled","from","to","handleResult","filterModel","state"],"mappings":"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"}
1
+ {"version":3,"file":"filtering.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Sentinel value used in set-filter unique values to represent rows with\n * no value (null, undefined, empty array via filterValue extractor).\n * Exported so server-side implementations can use the same constant.\n */\nexport const BLANK_FILTER_VALUE = '(Blank)';\n\n/**\n * Convert a value to a comparable number.\n * Handles Date objects, numeric values, and date/ISO strings.\n */\nfunction toNumeric(value: unknown): number {\n if (value instanceof Date) return value.getTime();\n const n = Number(value);\n if (!isNaN(n)) return n;\n // Try parsing as a date string (ISO 8601, etc.)\n const d = new Date(value as string);\n return d.getTime(); // NaN if unparseable\n}\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValue - Optional extractor for complex cell values (arrays, objects)\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(\n row: Record<string, unknown>,\n filter: FilterModel,\n caseSensitive = false,\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[],\n): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // When a filterValue extractor is present, use array-aware matching for set operators.\n // Each extracted value is checked individually against the filter set.\n if (filterValue && (filter.operator === 'notIn' || filter.operator === 'in')) {\n const extracted = filterValue(rawValue, row);\n const values = Array.isArray(extracted) ? extracted : extracted != null ? [extracted] : [];\n\n if (filter.operator === 'notIn') {\n // Row is hidden if ANY extracted value is in the excluded set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const excluded = filter.value;\n if (!Array.isArray(excluded)) return true;\n if (values.length === 0) return !excluded.includes(BLANK_FILTER_VALUE);\n return !values.some((v) => excluded.includes(v));\n }\n if (filter.operator === 'in') {\n // Row passes if ANY extracted value is in the included set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const included = filter.value;\n if (!Array.isArray(included)) return false;\n if (values.length === 0) return included.includes(BLANK_FILTER_VALUE);\n return values.some((v) => included.includes(v));\n }\n }\n\n // Set operators handle null explicitly: null is never \"in\" a set,\n // and null is never excluded by \"notIn\" (it's not a listed value).\n if (filter.operator === 'notIn') {\n if (rawValue == null) return true;\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n }\n if (filter.operator === 'in') {\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const compareFilterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(compareFilterValue);\n\n case 'notContains':\n return !compareValue.includes(compareFilterValue);\n\n case 'equals':\n return compareValue === compareFilterValue;\n\n case 'notEquals':\n return compareValue !== compareFilterValue;\n\n case 'startsWith':\n return compareValue.startsWith(compareFilterValue);\n\n case 'endsWith':\n return compareValue.endsWith(compareFilterValue);\n\n // Number/Date operators (use toNumeric for Date objects and date strings)\n case 'lessThan':\n return toNumeric(rawValue) < toNumeric(filter.value);\n\n case 'lessThanOrEqual':\n return toNumeric(rawValue) <= toNumeric(filter.value);\n\n case 'greaterThan':\n return toNumeric(rawValue) > toNumeric(filter.value);\n\n case 'greaterThanOrEqual':\n return toNumeric(rawValue) >= toNumeric(filter.value);\n\n case 'between':\n return toNumeric(rawValue) >= toNumeric(filter.value) && toNumeric(rawValue) <= toNumeric(filter.valueTo);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValues - Optional map of field → value extractor for complex columns\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false,\n filterValues?: Map<string, (value: unknown, row: T) => unknown | unknown[]>,\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) =>\n filters.every((f) =>\n matchesFilter(\n row,\n f,\n caseSensitive,\n filterValues?.get(f.field) as\n | ((value: unknown, row: Record<string, unknown>) => unknown | unknown[])\n | undefined,\n ),\n ),\n );\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n })),\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * When `filterValue` is provided, the extractor is called for each row's cell value.\n * If it returns an array, each element is added individually (flattened).\n * This enables complex-valued cells (e.g., arrays of objects) to expose\n * their individual filterable values.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @param filterValue - Optional extractor for complex cell values\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(\n rows: T[],\n field: string,\n filterValue?: (value: unknown, row: T) => unknown | unknown[],\n): unknown[] {\n const values = new Set<unknown>();\n let hasBlank = false;\n for (const row of rows) {\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) {\n hasBlank = true;\n }\n for (const v of extracted) {\n if (v != null) values.add(v);\n }\n } else if (extracted != null) {\n values.add(extracted);\n } else {\n hasBlank = true;\n }\n } else {\n if (cellValue != null) {\n values.add(cellValue);\n }\n }\n }\n // When a filterValue extractor is present and some rows have no values,\n // include a \"(Blank)\" sentinel so users can explicitly filter empty rows.\n if (filterValue && hasBlank) {\n values.add(BLANK_FILTER_VALUE);\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n\n/**\n * Extract unique values for multiple fields in a single pass through the rows.\n * This is more efficient than calling `getUniqueValues` N times when\n * computing derived state for several set filters at once.\n *\n * @param rows - The rows to extract values from\n * @param fields - Array of { field, filterValue? } descriptors\n * @returns Map of field → sorted unique values (same contract as `getUniqueValues`)\n */\nexport function getUniqueValuesBatch<T extends Record<string, unknown>>(\n rows: T[],\n fields: { field: string; filterValue?: (value: unknown, row: T) => unknown | unknown[] }[],\n): Map<string, unknown[]> {\n // Per-field accumulators\n const acc = new Map<string, { values: Set<unknown>; hasBlank: boolean; hasExtractor: boolean }>();\n for (const { field, filterValue } of fields) {\n acc.set(field, { values: new Set(), hasBlank: false, hasExtractor: !!filterValue });\n }\n\n // Single pass through all rows\n for (const row of rows) {\n for (const { field, filterValue } of fields) {\n const entry = acc.get(field)!;\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) entry.hasBlank = true;\n for (const v of extracted) {\n if (v != null) entry.values.add(v);\n }\n } else if (extracted != null) {\n entry.values.add(extracted);\n } else {\n entry.hasBlank = true;\n }\n } else {\n if (cellValue != null) entry.values.add(cellValue);\n }\n }\n }\n\n // Build sorted output\n const result = new Map<string, unknown[]>();\n for (const [field, { values, hasBlank, hasExtractor }] of acc) {\n if (hasExtractor && hasBlank) values.add(BLANK_FILTER_VALUE);\n result.set(\n field,\n [...values].sort((a, b) => {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n return String(a).localeCompare(String(b));\n }),\n );\n }\n return result;\n}\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport { isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport { computeFilterCacheKey, filterRows, getUniqueValues, getUniqueValuesBatch } from './filter-model';\nimport styles from './filtering.css?inline';\nimport filterPanelStyles from './FilteringPlugin.css?inline';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * Adds column header filters with text search, dropdown options, and custom filter panels.\n * Supports both **local filtering** for small datasets and **async handlers** for server-side\n * filtering on large datasets.\n *\n * ## Installation\n *\n * ```ts\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `debounceMs` | `number` | `300` | Debounce delay for filter input |\n * | `caseSensitive` | `boolean` | `false` | Case-sensitive string matching |\n * | `trimInput` | `boolean` | `true` | Trim whitespace from filter input |\n * | `useWorker` | `boolean` | `true` | Use Web Worker for datasets >1000 rows |\n * | `filterPanelRenderer` | `FilterPanelRenderer` | - | Custom filter panel renderer |\n * | `valuesHandler` | `FilterValuesHandler` | - | Async handler to fetch unique filter values |\n * | `filterHandler` | `FilterHandler<TRow>` | - | Async handler to apply filters remotely |\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `filterable` | `boolean` | Enable filtering for this column |\n * | `filterType` | `'text' \\| 'select' \\| 'number' \\| 'date'` | Filter UI type |\n * | `filterOptions` | `unknown[]` | Predefined options for select filters |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setFilter` | `(field, value) => void` | Set filter value for a column |\n * | `getFilters` | `() => FilterModel[]` | Get all current filters |\n * | `clearFilters` | `() => void` | Clear all filters |\n * | `clearFilter` | `(field) => void` | Clear filter for a specific column |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-filter-panel-bg` | `var(--tbw-color-panel-bg)` | Panel background |\n * | `--tbw-filter-panel-fg` | `var(--tbw-color-fg)` | Panel text color |\n * | `--tbw-filter-panel-border` | `var(--tbw-color-border)` | Panel border |\n * | `--tbw-filter-active-color` | `var(--tbw-color-accent)` | Active filter indicator |\n * | `--tbw-filter-input-bg` | `var(--tbw-color-bg)` | Input background |\n * | `--tbw-filter-input-focus` | `var(--tbw-color-accent)` | Input focus border |\n *\n * @example Basic Usage with Filterable Columns\n * ```ts\n * import '@toolbox-web/grid';\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', filterable: true },\n * { field: 'status', header: 'Status', filterable: true, filterType: 'select' },\n * { field: 'email', header: 'Email', filterable: true },\n * ],\n * plugins: [new FilteringPlugin({ debounceMs: 300 })],\n * };\n * grid.rows = data;\n * ```\n *\n * @example Column Formatters in Filter Panel\n * When a column defines a `format` function, the built-in set filter panel\n * displays formatted labels instead of raw values. Search within the panel\n * also matches against the formatted text.\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * {\n * field: 'price',\n * filterable: true,\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * // Filter checkboxes show \"$9.99\" instead of \"9.99\"\n * },\n * ],\n * plugins: [new FilteringPlugin()],\n * };\n * ```\n *\n * @example Server-Side Filtering with Async Handlers\n * ```ts\n * new FilteringPlugin({\n * // Fetch unique values from server for filter dropdown\n * valuesHandler: async (field, column) => {\n * const response = await fetch(`/api/distinct-values?field=${field}`);\n * return response.json();\n * },\n * // Apply filters on the server\n * filterHandler: async (filters, currentRows) => {\n * const response = await fetch('/api/data', {\n * method: 'POST',\n * body: JSON.stringify({ filters }),\n * });\n * return response.json();\n * },\n * });\n * ```\n *\n * @see {@link FilterConfig} for all configuration options\n * @see {@link FilterModel} for filter data structure\n * @see {@link FilterPanelParams} for custom panel renderer parameters\n *\n * @internal Extends BaseGridPlugin\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n /**\n * Plugin manifest - declares events emitted by this plugin.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n events: [\n {\n type: 'filter-applied',\n description: 'Emitted when filter criteria change. Subscribers can react to row visibility changes.',\n },\n ],\n queries: [\n {\n type: 'getContextMenuItems',\n description: 'Contributes filter-related items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'filtering';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // #region Helpers\n\n /**\n * Check if filtering is enabled at the grid level.\n * Grid-wide `filterable: false` disables filtering for all columns.\n */\n private isFilteringEnabled(): boolean {\n return this.grid.effectiveConfig?.filterable !== false;\n }\n\n /**\n * Check if a specific column is filterable, respecting both grid-level and column-level settings.\n */\n private isColumnFilterable(col: { filterable?: boolean; field?: string }): boolean {\n if (!this.isFilteringEnabled()) return false;\n return col.filterable !== false;\n }\n\n /**\n * Build a map of field → filterValue extractor for columns that have one.\n * Used to pass array-aware value extraction to the pure filter functions.\n */\n private getFilterValues():\n | Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]>\n | undefined {\n const columns = this.grid.effectiveConfig?.columns;\n if (!columns) return undefined;\n\n let map: Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]> | undefined;\n for (const col of columns) {\n if (col.field && col.filterValue) {\n if (!map) map = new Map();\n map.set(col.field, col.filterValue);\n }\n }\n return map;\n }\n\n // #endregion\n\n // #region Internal State\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n /** Spot-check of input rows for cache invalidation when upstream plugins (e.g. sort) change row order */\n private cachedInputSpot: { len: number; first: unknown; mid: unknown; last: unknown } | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private panelAnchorElement: HTMLElement | null = null; // For CSS anchor positioning cleanup\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private panelAbortController: AbortController | null = null; // For panel-scoped listeners\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly DEFAULT_LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n /**\n * Get the item height from CSS variable or fallback to default.\n * Reads --tbw-filter-item-height from the panel element.\n */\n private getListItemHeight(): number {\n if (this.panelElement) {\n const cssValue = getComputedStyle(this.panelElement).getPropertyValue('--tbw-filter-item-height');\n if (cssValue && cssValue.trim()) {\n const parsed = parseFloat(cssValue);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n }\n return FilteringPlugin.DEFAULT_LIST_ITEM_HEIGHT;\n }\n\n /**\n * Compute the inclusion (selected) map from the current filters and excluded values.\n * For set filters this is: uniqueValues \\ excludedValues.\n * Only includes entries for fields that have a set filter.\n * Uses a single-pass batch extraction to avoid iterating sourceRows per field.\n */\n private computeSelected(): Record<string, unknown[]> {\n // Collect the fields that need unique values\n const setFields: {\n field: string;\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[];\n }[] = [];\n for (const [field, filter] of this.filters) {\n if (filter.type !== 'set' || filter.operator !== 'notIn') continue;\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n setFields.push({ field, filterValue: col?.filterValue });\n }\n if (setFields.length === 0) return {};\n\n // Single pass through sourceRows for all fields\n const uniqueMap = getUniqueValuesBatch(this.sourceRows as Record<string, unknown>[], setFields);\n\n const selected: Record<string, unknown[]> = {};\n for (const { field } of setFields) {\n const excluded = this.excludedValues.get(field);\n const unique = uniqueMap.get(field) ?? [];\n selected[field] = excluded ? unique.filter((v) => !excluded.has(v)) : unique;\n }\n return selected;\n }\n\n /**\n * Sync excludedValues map from a filter model (for set filters).\n */\n private syncExcludedValues(field: string, filter: FilterModel | null): void {\n if (!filter) {\n this.excludedValues.delete(field);\n } else if (filter.type === 'set' && filter.operator === 'notIn' && Array.isArray(filter.value)) {\n this.excludedValues.set(field, new Set(filter.value));\n } else if (filter.type === 'set') {\n // Other set operators may have different semantics; clear for safety\n this.excludedValues.delete(field);\n }\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n /** @internal */\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n // Abort panel-scoped listeners (document click handler, etc.)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n // #endregion\n\n // #region Query Handlers\n\n /**\n * Handle inter-plugin queries.\n * Contributes filter-related items to the header context menu.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Only contribute items if filtering is enabled for this column\n if (!this.isFilteringEnabled()) return undefined;\n if (!this.isColumnFilterable(column)) return undefined;\n\n const items: HeaderContextMenuItem[] = [];\n const fieldFiltered = this.isFieldFiltered(column.field);\n const hasAnyFilter = this.filters.size > 0;\n\n if (fieldFiltered) {\n items.push({\n id: 'filtering/clear-column-filter',\n label: `Clear Filter`,\n icon: '✕',\n order: 20,\n action: () => this.clearFieldFilter(column.field),\n });\n }\n\n if (hasAnyFilter) {\n items.push({\n id: 'filtering/clear-all-filters',\n label: 'Clear All Filters',\n icon: '✕',\n order: 21,\n disabled: !hasAnyFilter,\n action: () => this.clearAllFilters(),\n });\n }\n\n return items.length > 0 ? items : undefined;\n }\n return undefined;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // If using async filterHandler, processRows becomes a passthrough\n // Actual filtering happens in applyFiltersAsync and rows are set directly on grid\n if (this.config.filterHandler) {\n // Return cached result if available (set by async handler)\n if (this.cachedResult) return this.cachedResult;\n // Otherwise return rows as-is (filtering happens async)\n return [...rows];\n }\n\n // Check cache — also verify input rows haven't changed (e.g. due to sort)\n const newCacheKey = computeFilterCacheKey(filterList);\n const inputSpot = {\n len: rows.length,\n first: rows[0],\n mid: rows[Math.floor(rows.length / 2)],\n last: rows[rows.length - 1],\n };\n const inputUnchanged =\n this.cachedInputSpot != null &&\n inputSpot.len === this.cachedInputSpot.len &&\n inputSpot.first === this.cachedInputSpot.first &&\n inputSpot.mid === this.cachedInputSpot.mid &&\n inputSpot.last === this.cachedInputSpot.last;\n\n if (this.cacheKey === newCacheKey && this.cachedResult && inputUnchanged) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows(\n [...rows] as Record<string, unknown>[],\n filterList,\n this.config.caseSensitive,\n this.getFilterValues(),\n );\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n this.cachedInputSpot = inputSpot;\n\n return result;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = gridEl.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n // Use visibleColumns since data-col is the index within _visibleColumns\n const col = this.visibleColumns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || !this.isColumnFilterable(col)) return;\n\n // Skip utility columns (expander, selection checkbox, etc.)\n if (isUtilityColumn(col)) return;\n\n const field = col.field;\n if (!field) return;\n\n const hasFilter = this.filters.has(field);\n\n // Check if button already exists\n let filterBtn = cell.querySelector('.tbw-filter-btn') as HTMLElement | null;\n\n if (filterBtn) {\n // Update active state and icon of existing button\n const wasActive = filterBtn.classList.contains('active');\n filterBtn.classList.toggle('active', hasFilter);\n (cell as HTMLElement).classList.toggle('filtered', hasFilter);\n // Update icon if active state changed\n if (wasActive !== hasFilter) {\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n }\n return;\n }\n\n // Create filter button\n filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n // Use grid icons configuration\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n\n // Mark button as active if filter exists\n if (hasFilter) {\n filterBtn.classList.add('active');\n (cell as HTMLElement).classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn!);\n });\n\n // Insert before resize handle to maintain order: [label, sort-indicator, filter-btn, resize-handle]\n const resizeHandle = cell.querySelector('.resize-handle');\n if (resizeHandle) {\n cell.insertBefore(filterBtn, resizeHandle);\n } else {\n cell.appendChild(filterBtn);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n this.syncExcludedValues(field, null);\n } else {\n const fullFilter = { ...filter, field };\n this.filters.set(field, fullFilter);\n this.syncExcludedValues(field, fullFilter);\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n this.excludedValues.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n this.syncExcludedValues(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n\n this.applyFiltersInternal();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.applyFiltersInternal();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n * When a column has `filterValue`, individual extracted values are returned.\n */\n getUniqueValues(field: string): unknown[] {\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n const getter = col?.filterValue;\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field, getter);\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Copy CSS classes and data attributes from grid to filter panel.\n * This ensures theme classes (e.g., .eds-theme) cascade to the panel.\n */\n private copyGridThemeContext(panel: HTMLElement): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Copy all CSS classes from grid to panel (except internal ones)\n for (const className of gridEl.classList) {\n // Skip internal classes that shouldn't be copied\n if (className.startsWith('tbw-') || className === 'selecting') continue;\n panel.classList.add(className);\n }\n\n // Copy data-theme attribute if present\n const theme = gridEl.dataset.theme;\n if (theme) {\n panel.dataset.theme = theme;\n }\n }\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n // Only inject if we have valid CSS text (Vite's ?inline import)\n // When importing from source without Vite, the import is a module object, not a string\n if (typeof filterPanelStyles !== 'string' || !filterPanelStyles) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n // Copy theme classes from grid to panel for proper theming\n this.copyGridThemeContext(panel);\n // Add animation class if animations are enabled\n if (this.isAnimationEnabled) {\n panel.classList.add('tbw-filter-panel-animated');\n }\n this.panelElement = panel;\n this.openPanelField = field;\n\n // If using async valuesHandler, show loading state and fetch values\n if (this.config.valuesHandler) {\n panel.innerHTML = '<div class=\"tbw-filter-loading\">Loading...</div>';\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n this.setupPanelCloseHandler(panel, buttonEl);\n\n this.config.valuesHandler(field, column).then((values) => {\n // Check if panel is still open for this field\n if (this.openPanelField !== field || !this.panelElement) return;\n panel.innerHTML = '';\n this.renderPanelContent(field, column, panel, values);\n });\n return;\n }\n\n // Sync path: get unique values from local rows\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field, column.filterValue);\n\n // Position and append to body BEFORE rendering content\n // so getListItemHeight() can read CSS variables from computed styles\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n this.renderPanelContent(field, column, panel, uniqueValues);\n this.setupPanelCloseHandler(panel, buttonEl);\n }\n\n /**\n * Render filter panel content with given values\n */\n private renderPanelContent(field: string, column: ColumnConfig, panel: HTMLElement, uniqueValues: unknown[]): void {\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n // Resolution order: plugin config → typeDefaults → built-in\n let usedCustomRenderer = false;\n\n // 1. Check plugin-level filterPanelRenderer\n if (this.config.filterPanelRenderer) {\n this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n\n // 2. Check typeDefaults for this column's type\n if (!usedCustomRenderer && column.type) {\n const typeDefault = this.grid.effectiveConfig.typeDefaults?.[column.type];\n if (typeDefault?.filterPanelRenderer) {\n typeDefault.filterPanelRenderer(panel, params);\n usedCustomRenderer = panel.children.length > 0;\n }\n }\n\n // 3. Fall back to built-in type-specific panel renderers\n if (!usedCustomRenderer) {\n const columnType = column.type;\n if (columnType === 'number') {\n this.renderNumberFilterPanel(panel, params, uniqueValues);\n } else if (columnType === 'date') {\n this.renderDateFilterPanel(panel, params, uniqueValues);\n } else {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n }\n }\n\n /**\n * Setup click-outside handler to close the panel\n */\n private setupPanelCloseHandler(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Create abort controller for panel-scoped listeners\n // This allows cleanup when panel closes OR when grid disconnects\n this.panelAbortController = new AbortController();\n\n // Add global click handler to close on outside click\n // Defer to next tick to avoid immediate close from the click that opened the panel\n setTimeout(() => {\n document.addEventListener(\n 'click',\n (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n },\n { signal: this.panelAbortController?.signal },\n );\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n const panel = this.panelElement;\n if (panel) {\n panel.remove();\n this.panelElement = null;\n }\n // Clean up anchor name from header cell\n if (this.panelAnchorElement) {\n (this.panelAnchorElement.style as any).anchorName = '';\n this.panelAnchorElement = null;\n }\n this.openPanelField = null;\n // Abort panel-scoped listeners (document click handler)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n /** Cache for CSS anchor positioning support check */\n private static supportsAnchorPositioning: boolean | null = null;\n\n /**\n * Check if browser supports CSS Anchor Positioning\n */\n private static checkAnchorPositioningSupport(): boolean {\n if (FilteringPlugin.supportsAnchorPositioning === null) {\n FilteringPlugin.supportsAnchorPositioning = CSS.supports('anchor-name', '--test');\n }\n return FilteringPlugin.supportsAnchorPositioning;\n }\n\n /**\n * Position the panel below the header cell\n * Uses CSS Anchor Positioning if supported, falls back to JS positioning\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Find the parent header cell\n const headerCell = buttonEl.closest('.cell') as HTMLElement | null;\n const anchorEl = headerCell ?? buttonEl;\n\n // Set anchor name on the header cell for CSS anchor positioning\n (anchorEl.style as any).anchorName = '--tbw-filter-anchor';\n this.panelAnchorElement = anchorEl; // Store for cleanup\n\n // If CSS Anchor Positioning is supported, CSS handles positioning\n // but we need to detect if it flipped above to adjust animation\n if (FilteringPlugin.checkAnchorPositioningSupport()) {\n // Check position after CSS anchor positioning takes effect\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n const anchorRect = anchorEl.getBoundingClientRect();\n // If panel top is above anchor top, it flipped to above\n if (panelRect.top < anchorRect.top) {\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n return;\n }\n\n // Fallback: JS-based positioning for older browsers\n const rect = anchorEl.getBoundingClientRect();\n\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows viewport edges\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n\n // Check horizontal overflow - align right edge to header cell right edge\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${rect.right - panelRect.width}px`;\n }\n\n // Check vertical overflow - flip to above header cell\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>,\n ): void {\n const { field, column } = params;\n // Get item height from CSS variable or use default\n const itemHeight = this.getListItemHeight();\n\n // Helper: format a value using the column's format function (for ID-to-name translation, etc.)\n // When filterValue is set, unique values are already extracted primitives — skip format.\n const formatValue = (value: unknown): string => {\n if (value == null) return '(Blank)';\n if (column.format && !column.filterValue) {\n const formatted = column.format(value, undefined as never);\n if (formatted) return formatted;\n }\n return String(value);\n };\n\n // Sort unique values by formatted display name\n uniqueValues = uniqueValues.slice().sort((a, b) => formatValue(a).localeCompare(formatValue(b)));\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const displayValue = formatValue(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `calc(var(--tbw-filter-item-height, 28px) * ${index})`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = displayValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * itemHeight}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: itemHeight,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const caseSensitive = this.config.caseSensitive ?? false;\n const compareFilter = caseSensitive ? filterText : filterText.toLowerCase();\n\n // Filter the unique values - search against formatted display name\n filteredValues = uniqueValues.filter((value) => {\n const displayStr = formatValue(value);\n const compareValue = caseSensitive ? displayStr : displayStr.toLowerCase();\n return !filterText || compareValue.includes(compareFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true },\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a number range filter panel with min/max inputs and slider\n */\n private renderNumberFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: number; max?: number; step?: number } | undefined;\n\n // Helper to convert to number\n const toNumber = (val: unknown, fallback: number): number => {\n if (typeof val === 'number') return val;\n if (typeof val === 'string') {\n const num = parseFloat(val);\n return isNaN(num) ? fallback : num;\n }\n return fallback;\n };\n\n // Compute min/max from data if not specified\n const numericValues = uniqueValues.filter((v) => typeof v === 'number' && !isNaN(v)) as number[];\n const dataMin = numericValues.length > 0 ? Math.min(...numericValues) : 0;\n const dataMax = numericValues.length > 0 ? Math.max(...numericValues) : 100;\n\n const min = toNumber(filterParams?.min ?? editorParams?.min, dataMin);\n const max = toNumber(filterParams?.max ?? editorParams?.max, dataMax);\n const step = filterParams?.step ?? editorParams?.step ?? 1;\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentMin = min;\n let currentMax = max;\n if (currentFilter?.operator === 'between') {\n currentMin = toNumber(currentFilter.value, min);\n currentMax = toNumber(currentFilter.valueTo, max);\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentMin = toNumber(currentFilter.value, min);\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentMax = toNumber(currentFilter.value, max);\n }\n\n // Range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-range-inputs';\n\n // Min input\n const minGroup = document.createElement('div');\n minGroup.className = 'tbw-filter-range-group';\n\n const minLabel = document.createElement('label');\n minLabel.textContent = 'Min';\n minLabel.className = 'tbw-filter-range-label';\n\n const minInput = document.createElement('input');\n minInput.type = 'number';\n minInput.className = 'tbw-filter-range-input';\n minInput.min = String(min);\n minInput.max = String(max);\n minInput.step = String(step);\n minInput.value = String(currentMin);\n\n minGroup.appendChild(minLabel);\n minGroup.appendChild(minInput);\n rangeContainer.appendChild(minGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // Max input\n const maxGroup = document.createElement('div');\n maxGroup.className = 'tbw-filter-range-group';\n\n const maxLabel = document.createElement('label');\n maxLabel.textContent = 'Max';\n maxLabel.className = 'tbw-filter-range-label';\n\n const maxInput = document.createElement('input');\n maxInput.type = 'number';\n maxInput.className = 'tbw-filter-range-input';\n maxInput.min = String(min);\n maxInput.max = String(max);\n maxInput.step = String(step);\n maxInput.value = String(currentMax);\n\n maxGroup.appendChild(maxLabel);\n maxGroup.appendChild(maxInput);\n rangeContainer.appendChild(maxGroup);\n\n panel.appendChild(rangeContainer);\n\n // Range slider (dual thumb using two range inputs)\n const sliderContainer = document.createElement('div');\n sliderContainer.className = 'tbw-filter-range-slider';\n\n const sliderTrack = document.createElement('div');\n sliderTrack.className = 'tbw-filter-range-track';\n\n const sliderFill = document.createElement('div');\n sliderFill.className = 'tbw-filter-range-fill';\n\n const minSlider = document.createElement('input');\n minSlider.type = 'range';\n minSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-min';\n minSlider.min = String(min);\n minSlider.max = String(max);\n minSlider.step = String(step);\n minSlider.value = String(currentMin);\n\n const maxSlider = document.createElement('input');\n maxSlider.type = 'range';\n maxSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-max';\n maxSlider.min = String(min);\n maxSlider.max = String(max);\n maxSlider.step = String(step);\n maxSlider.value = String(currentMax);\n\n sliderContainer.appendChild(sliderTrack);\n sliderContainer.appendChild(sliderFill);\n sliderContainer.appendChild(minSlider);\n sliderContainer.appendChild(maxSlider);\n panel.appendChild(sliderContainer);\n\n // Update fill position\n const updateFill = () => {\n const minVal = parseFloat(minSlider.value);\n const maxVal = parseFloat(maxSlider.value);\n const range = max - min;\n const leftPercent = ((minVal - min) / range) * 100;\n const rightPercent = ((maxVal - min) / range) * 100;\n sliderFill.style.left = `${leftPercent}%`;\n sliderFill.style.width = `${rightPercent - leftPercent}%`;\n };\n\n // Sync inputs with sliders\n minSlider.addEventListener('input', () => {\n const val = Math.min(parseFloat(minSlider.value), parseFloat(maxSlider.value));\n minSlider.value = String(val);\n minInput.value = String(val);\n updateFill();\n });\n\n maxSlider.addEventListener('input', () => {\n const val = Math.max(parseFloat(maxSlider.value), parseFloat(minSlider.value));\n maxSlider.value = String(val);\n maxInput.value = String(val);\n updateFill();\n });\n\n // Sync sliders with inputs\n minInput.addEventListener('input', () => {\n let val = parseFloat(minInput.value) || min;\n val = Math.max(min, Math.min(val, parseFloat(maxInput.value)));\n minSlider.value = String(val);\n updateFill();\n });\n\n maxInput.addEventListener('input', () => {\n let val = parseFloat(maxInput.value) || max;\n val = Math.min(max, Math.max(val, parseFloat(minInput.value)));\n maxSlider.value = String(val);\n updateFill();\n });\n\n // Initialize fill\n updateFill();\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n const minVal = parseFloat(minInput.value);\n const maxVal = parseFloat(maxInput.value);\n params.applyTextFilter('between', minVal, maxVal);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a date range filter panel with from/to date inputs\n */\n private renderDateFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: string; max?: string } | undefined;\n\n // Compute min/max from data if not specified\n const dateValues = uniqueValues\n .filter((v) => v instanceof Date || (typeof v === 'string' && !isNaN(Date.parse(v))))\n .map((v) => (v instanceof Date ? v : new Date(v as string)))\n .filter((d) => !isNaN(d.getTime()));\n\n const dataMin = dateValues.length > 0 ? new Date(Math.min(...dateValues.map((d) => d.getTime()))) : null;\n const dataMax = dateValues.length > 0 ? new Date(Math.max(...dateValues.map((d) => d.getTime()))) : null;\n\n // Format date for input[type=\"date\"] (YYYY-MM-DD)\n const formatDateForInput = (date: Date | null): string => {\n if (!date) return '';\n return date.toISOString().split('T')[0];\n };\n\n const parseFilterParam = (value: unknown): string => {\n if (!value) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatDateForInput(new Date(value));\n return '';\n };\n\n const minDate =\n parseFilterParam(filterParams?.min) || parseFilterParam(editorParams?.min) || formatDateForInput(dataMin);\n const maxDate =\n parseFilterParam(filterParams?.max) || parseFilterParam(editorParams?.max) || formatDateForInput(dataMax);\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentFrom = '';\n let currentTo = '';\n const isBlankFilter = currentFilter?.operator === 'blank';\n if (currentFilter?.operator === 'between') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n currentTo = parseFilterParam(currentFilter.valueTo) || '';\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentTo = parseFilterParam(currentFilter.value) || '';\n }\n\n // Date range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-date-range';\n\n // From input\n const fromGroup = document.createElement('div');\n fromGroup.className = 'tbw-filter-date-group';\n\n const fromLabel = document.createElement('label');\n fromLabel.textContent = 'From';\n fromLabel.className = 'tbw-filter-range-label';\n\n const fromInput = document.createElement('input');\n fromInput.type = 'date';\n fromInput.className = 'tbw-filter-date-input';\n if (minDate) fromInput.min = minDate;\n if (maxDate) fromInput.max = maxDate;\n fromInput.value = currentFrom;\n\n fromGroup.appendChild(fromLabel);\n fromGroup.appendChild(fromInput);\n rangeContainer.appendChild(fromGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // To input\n const toGroup = document.createElement('div');\n toGroup.className = 'tbw-filter-date-group';\n\n const toLabel = document.createElement('label');\n toLabel.textContent = 'To';\n toLabel.className = 'tbw-filter-range-label';\n\n const toInput = document.createElement('input');\n toInput.type = 'date';\n toInput.className = 'tbw-filter-date-input';\n if (minDate) toInput.min = minDate;\n if (maxDate) toInput.max = maxDate;\n toInput.value = currentTo;\n\n toGroup.appendChild(toLabel);\n toGroup.appendChild(toInput);\n rangeContainer.appendChild(toGroup);\n\n panel.appendChild(rangeContainer);\n\n // \"Show only blank\" checkbox\n const blankRow = document.createElement('label');\n blankRow.className = 'tbw-filter-blank-option';\n\n const blankCheckbox = document.createElement('input');\n blankCheckbox.type = 'checkbox';\n blankCheckbox.className = 'tbw-filter-blank-checkbox';\n blankCheckbox.checked = isBlankFilter;\n\n const blankLabel = document.createTextNode('Show only blank');\n blankRow.appendChild(blankCheckbox);\n blankRow.appendChild(blankLabel);\n\n // Toggle date inputs disabled state when blank is checked\n const toggleDateInputs = (disabled: boolean): void => {\n fromInput.disabled = disabled;\n toInput.disabled = disabled;\n rangeContainer.classList.toggle('tbw-filter-disabled', disabled);\n };\n toggleDateInputs(isBlankFilter);\n\n blankCheckbox.addEventListener('change', () => {\n toggleDateInputs(blankCheckbox.checked);\n });\n\n panel.appendChild(blankRow);\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n if (blankCheckbox.checked) {\n params.applyTextFilter('blank', '');\n return;\n }\n\n const from = fromInput.value;\n const to = toInput.value;\n\n if (from && to) {\n params.applyTextFilter('between', from, to);\n } else if (from) {\n params.applyTextFilter('greaterThanOrEqual', from);\n } else if (to) {\n params.applyTextFilter('lessThanOrEqual', to);\n } else {\n params.clearFilter();\n }\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.applyFiltersInternal();\n }\n\n /**\n * Apply a text/number/date filter\n */\n private applyTextFilter(\n field: string,\n operator: FilterModel['operator'],\n value: string | number,\n valueTo?: string | number,\n ): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.applyFiltersInternal();\n }\n\n /**\n * Internal method to apply filters (sync or async based on config)\n */\n private applyFiltersInternal(): void {\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n const filterList = [...this.filters.values()];\n\n // If using async filterHandler, delegate to server\n if (this.config.filterHandler) {\n const gridEl = this.grid as unknown as Element;\n gridEl.setAttribute('aria-busy', 'true');\n\n const result = this.config.filterHandler(filterList, this.sourceRows as unknown[]);\n\n // Handle async or sync result\n const handleResult = (rows: unknown[]) => {\n gridEl.removeAttribute('aria-busy');\n this.cachedResult = rows;\n\n // Update grid rows directly for async filtering\n (this.grid as unknown as { rows: unknown[] }).rows = rows;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: rows.length,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n\n // Trigger afterRender to update filter button active state\n this.requestRender();\n };\n\n if (result && typeof (result as Promise<unknown[]>).then === 'function') {\n (result as Promise<unknown[]>).then(handleResult);\n } else {\n handleResult(result as unknown[]);\n }\n return;\n }\n\n // Sync path: emit event and re-render (processRows will handle filtering)\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n this.requestRender();\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return filter state for a column if it has an active filter.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n }\n // #endregion\n}\n"],"names":["BLANK_FILTER_VALUE","toNumeric","value","n","matchesFilter","row","filter","caseSensitive","filterValue","rawValue","extracted","values","excluded","v","included","stringValue","compareValue","compareFilterValue","filterRows","rows","filters","filterValues","f","computeFilterCacheKey","getUniqueValues","field","hasBlank","cellValue","a","b","getUniqueValuesBatch","fields","acc","entry","result","hasExtractor","FilteringPlugin","BaseGridPlugin","styles","col","columns","map","cssValue","parsed","setFields","c","uniqueMap","selected","unique","grid","query","params","column","items","fieldFiltered","hasAnyFilter","filterList","newCacheKey","inputSpot","inputUnchanged","gridEl","cell","colIndex","isUtilityColumn","hasFilter","filterBtn","wasActive","iconName","e","resizeHandle","fullFilter","getter","panel","className","theme","style","filterPanelStyles","buttonEl","uniqueValues","excludedSet","currentSearchText","operator","valueTo","usedCustomRenderer","typeDefault","columnType","anchorEl","panelRect","anchorRect","rect","excludedValues","itemHeight","formatValue","formatted","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","displayValue","item","checkbox","label","totalItems","viewportHeight","scrollTop","shouldBypassVirtualization","idx","window","computeVirtualWindow","i","renderValues","filterText","compareFilter","displayStr","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterParams","editorParams","toNumber","val","fallback","num","numericValues","dataMin","dataMax","min","max","step","currentFilter","currentMin","currentMax","rangeContainer","minGroup","minLabel","minInput","separator","maxGroup","maxLabel","maxInput","sliderContainer","sliderTrack","sliderFill","minSlider","maxSlider","updateFill","minVal","maxVal","range","leftPercent","rightPercent","dateValues","d","formatDateForInput","date","parseFilterParam","minDate","maxDate","currentFrom","currentTo","isBlankFilter","fromGroup","fromLabel","fromInput","toGroup","toLabel","toInput","blankRow","blankCheckbox","blankLabel","toggleDateInputs","disabled","from","to","handleResult","filterModel","state"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- (function(d,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],l):(d=typeof globalThis<"u"?globalThis:d||self,l(d.TbwGridPlugin_multiSort={},d.TbwGrid))})(this,(function(d,l){"use strict";function S(r,t,i){return t.length?[...r].sort((n,e)=>{for(const o of t){const a=i.find(g=>g.field===o.field)?.sortComparator??b,p=n[o.field],u=e[o.field],s=a(p,u,n,e);if(s!==0)return o.direction==="asc"?s:-s}return 0}):[...r]}function b(r,t){return r==null&&t==null?0:r==null?1:t==null?-1:typeof r=="number"&&typeof t=="number"?r-t:r instanceof Date&&t instanceof Date?r.getTime()-t.getTime():typeof r=="boolean"&&typeof t=="boolean"?r===t?0:r?-1:1:String(r).localeCompare(String(t))}function y(r,t,i,n){const e=r.find(o=>o.field===t);return i?e?e.direction==="asc"?r.map(o=>o.field===t?{...o,direction:"desc"}:o):r.filter(o=>o.field!==t):r.length<n?[...r,{field:t,direction:"asc"}]:r:e?.direction==="asc"?[{field:t,direction:"desc"}]:e?.direction==="desc"?[]:[{field:t,direction:"asc"}]}function h(r,t){const i=r.findIndex(n=>n.field===t);return i>=0?i+1:void 0}function m(r,t){return r.find(i=>i.field===t)?.direction}const M='@layer tbw-plugins{.header-cell[data-sort=asc]:after{content:"↑";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-indicator{margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-index{font-size:var(--tbw-font-size-2xs, .7em);background:var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));color:var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));border-radius:50%;width:var(--tbw-multi-sort-badge-size, 1em);height:var(--tbw-multi-sort-badge-size, 1em);display:inline-flex;align-items:center;justify-content:center;margin-left:var(--tbw-spacing-xs, .125em);font-weight:600}}';class w extends l.BaseGridPlugin{name="multiSort";styles=M;get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];detach(){this.sortModel=[]}processRows(t){return this.sortModel.length===0?[...t]:S([...t],this.sortModel,[...this.columns])}onHeaderClick(t){if(!this.columns.find(o=>o.field===t.field)?.sortable)return!1;const n=t.originalEvent.shiftKey,e=this.config.maxSortColumns??3;return this.sortModel=y(this.sortModel,t.field,n,e),this.emit("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),!0}afterRender(){const t=this.gridElement;if(!t)return;const i=this.config.showSortIndex!==!1;t.querySelectorAll(".header-row .cell[data-field]").forEach(e=>{const o=e.getAttribute("data-field");if(!o)return;const f=h(this.sortModel,o),a=m(this.sortModel,o);if(e.querySelector(".sort-index")?.remove(),a){e.querySelector('[part~="sort-indicator"], .sort-indicator')?.remove(),e.setAttribute("data-sort",a);const s=document.createElement("span");s.className="sort-indicator",this.setIcon(s,this.resolveIcon(a==="asc"?"sortAsc":"sortDesc"));const g=e.querySelector(".tbw-filter-btn"),v=e.querySelector(".resize-handle"),x=g??v;if(x?e.insertBefore(s,x):e.appendChild(s),i&&this.sortModel.length>1&&f!==void 0){const c=document.createElement("span");c.className="sort-index",c.textContent=String(f),s.nextSibling?e.insertBefore(c,s.nextSibling):e.appendChild(c)}}else e.removeAttribute("data-sort"),e.querySelector('[part~="sort-indicator"], .sort-indicator')?.remove()})}getSortModel(){return[...this.sortModel]}setSortModel(t){this.sortModel=[...t],this.emit("sort-change",{sortModel:[...t]}),this.requestRender()}clearSort(){this.sortModel=[],this.emit("sort-change",{sortModel:[]}),this.requestRender()}getSortIndex(t){return h(this.sortModel,t)}getSortDirection(t){return m(this.sortModel,t)}getColumnState(t){const i=this.sortModel.findIndex(e=>e.field===t);return i===-1?void 0:{sort:{direction:this.sortModel[i].direction,priority:i}}}applyColumnState(t,i){if(!i.sort){this.sortModel=this.sortModel.filter(o=>o.field!==t);return}const n=this.sortModel.findIndex(o=>o.field===t),e={field:t,direction:i.sort.direction};n!==-1?this.sortModel[n]=e:this.sortModel.splice(i.sort.priority,0,e)}}d.MultiSortPlugin=w,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
1
+ (function(d,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],l):(d=typeof globalThis<"u"?globalThis:d||self,l(d.TbwGridPlugin_multiSort={},d.TbwGrid))})(this,(function(d,l){"use strict";function x(r,t,i){return t.length?[...r].sort((n,e)=>{for(const o of t){const c=i.find(h=>h.field===o.field)?.sortComparator??b,p=n[o.field],f=e[o.field],s=c(p,f,n,e);if(s!==0)return o.direction==="asc"?s:-s}return 0}):[...r]}function b(r,t){return r==null&&t==null?0:r==null?1:t==null?-1:typeof r=="number"&&typeof t=="number"?r-t:r instanceof Date&&t instanceof Date?r.getTime()-t.getTime():typeof r=="boolean"&&typeof t=="boolean"?r===t?0:r?-1:1:String(r).localeCompare(String(t))}function y(r,t,i,n){const e=r.find(o=>o.field===t);return i?e?e.direction==="asc"?r.map(o=>o.field===t?{...o,direction:"desc"}:o):r.filter(o=>o.field!==t):r.length<n?[...r,{field:t,direction:"asc"}]:r:e?.direction==="asc"?[{field:t,direction:"desc"}]:e?.direction==="desc"?[]:[{field:t,direction:"asc"}]}function g(r,t){const i=r.findIndex(n=>n.field===t);return i>=0?i+1:void 0}function m(r,t){return r.find(i=>i.field===t)?.direction}const M='@layer tbw-plugins{.header-cell[data-sort=asc]:after{content:"↑";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-indicator{margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-index{font-size:var(--tbw-font-size-2xs, .7em);background:var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));color:var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));border-radius:50%;width:var(--tbw-multi-sort-badge-size, 1em);height:var(--tbw-multi-sort-badge-size, 1em);display:inline-flex;align-items:center;justify-content:center;margin-left:var(--tbw-spacing-xs, .125em);font-weight:600}}';class w extends l.BaseGridPlugin{name="multiSort";styles=M;get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];cachedSortResult=null;detach(){this.sortModel=[],this.cachedSortResult=null}processRows(t){if(this.sortModel.length===0)return this.cachedSortResult=null,[...t];const i=this.gridElement;if(i&&!i._isGridEditMode&&typeof i._activeEditRows=="number"&&i._activeEditRows!==-1&&this.cachedSortResult&&this.cachedSortResult.length===t.length)return[...this.cachedSortResult];const n=x([...t],this.sortModel,[...this.columns]);return this.cachedSortResult=n,n}onHeaderClick(t){if(!this.columns.find(o=>o.field===t.field)?.sortable)return!1;const n=t.originalEvent.shiftKey,e=this.config.maxSortColumns??3;return this.sortModel=y(this.sortModel,t.field,n,e),this.emit("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),this.grid?.requestStateChange?.(),!0}afterRender(){const t=this.gridElement;if(!t)return;const i=this.config.showSortIndex!==!1;t.querySelectorAll(".header-row .cell[data-field]").forEach(e=>{const o=e.getAttribute("data-field");if(!o)return;const u=g(this.sortModel,o),c=m(this.sortModel,o);if(e.querySelector(".sort-index")?.remove(),c){e.querySelector('[part~="sort-indicator"], .sort-indicator')?.remove(),e.setAttribute("data-sort",c);const s=document.createElement("span");s.className="sort-indicator",this.setIcon(s,this.resolveIcon(c==="asc"?"sortAsc":"sortDesc"));const h=e.querySelector(".tbw-filter-btn"),v=e.querySelector(".resize-handle"),S=h??v;if(S?e.insertBefore(s,S):e.appendChild(s),i&&this.sortModel.length>1&&u!==void 0){const a=document.createElement("span");a.className="sort-index",a.textContent=String(u),s.nextSibling?e.insertBefore(a,s.nextSibling):e.appendChild(a)}}else e.removeAttribute("data-sort"),e.querySelector('[part~="sort-indicator"], .sort-indicator')?.remove()})}getSortModel(){return[...this.sortModel]}setSortModel(t){this.sortModel=[...t],this.emit("sort-change",{sortModel:[...t]}),this.requestRender(),this.grid?.requestStateChange?.()}clearSort(){this.sortModel=[],this.emit("sort-change",{sortModel:[]}),this.requestRender(),this.grid?.requestStateChange?.()}getSortIndex(t){return g(this.sortModel,t)}getSortDirection(t){return m(this.sortModel,t)}getColumnState(t){const i=this.sortModel.findIndex(e=>e.field===t);return i===-1?void 0:{sort:{direction:this.sortModel[i].direction,priority:i}}}applyColumnState(t,i){if(!i.sort){this.sortModel=this.sortModel.filter(o=>o.field!==t);return}const n=this.sortModel.findIndex(o=>o.field===t),e={field:t,direction:i.sort.direction};n!==-1?this.sortModel[n]=e:this.sortModel.splice(i.sort.priority,0,e)}}d.MultiSortPlugin=w,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
2
2
  //# sourceMappingURL=multi-sort.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\n });\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback)\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `maxSortColumns` | `number` | `3` | Maximum columns to sort by |\n * | `showSortIndex` | `boolean` | `true` | Show sort priority badges |\n * | `initialSort` | `SortModel[]` | - | Pre-configured sort order on load |\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setSort` | `(sortModel: SortModel[]) => void` | Set sort programmatically |\n * | `getSortModel` | `() => SortModel[]` | Get current sort model |\n * | `clearSort` | `() => void` | Clear all sorting |\n * | `addSort` | `(field, direction) => void` | Add a column to sort |\n * | `removeSort` | `(field) => void` | Remove a column from sort |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.addEventListener('sort-change', (e) => {\n * console.log('Active sorts:', e.detail.sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n return [...rows];\n }\n return applySorts([...rows], this.sortModel, [...this.columns]);\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n // Update all sortable header cells with sort indicators\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // Remove any stale sort indicators left by a previous afterRender cycle\n // Base indicators use part=\"sort-indicator\", plugin indicators use class=\"sort-indicator\"\n const staleIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n staleIndicator?.remove();\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.emit('sort-change', { sortModel: [...model] });\n this.requestRender();\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n\n // Re-sort the model by priority to ensure correct order\n // This is handled after all columns are processed, but we maintain order here\n }\n // #endregion\n}\n"],"names":["applySorts","rows","sorts","columns","a","b","sort","comparator","c","defaultComparator","aVal","bVal","result","toggleSort","current","field","shiftKey","maxColumns","existing","s","getSortIndex","sortModel","index","getSortDirection","MultiSortPlugin","BaseGridPlugin","styles","event","gridEl","showIndex","cell","sortIndex","sortDir","indicator","filterBtn","resizeHandle","insertBefore","badge","model","state","existingIndex","newEntry"],"mappings":"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"}
1
+ {"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\n });\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback)\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `maxSortColumns` | `number` | `3` | Maximum columns to sort by |\n * | `showSortIndex` | `boolean` | `true` | Show sort priority badges |\n * | `initialSort` | `SortModel[]` | - | Pre-configured sort order on load |\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setSort` | `(sortModel: SortModel[]) => void` | Set sort programmatically |\n * | `getSortModel` | `() => SortModel[]` | Get current sort model |\n * | `clearSort` | `() => void` | Clear all sorting |\n * | `addSort` | `(field, direction) => void` | Add a column to sort |\n * | `removeSort` | `(field) => void` | Remove a column from sort |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.addEventListener('sort-change', (e) => {\n * console.log('Active sorts:', e.detail.sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n /** Cached sort result — returned as-is while a row edit is active to prevent\n * the edited row from jumping to a new sorted position mid-edit. Row data\n * mutations are still visible because the array holds shared object refs. */\n private cachedSortResult: unknown[] | null = null;\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n this.cachedSortResult = null;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return [...rows];\n }\n\n // Freeze sort order while a row is actively being edited (row mode only).\n // Re-sorting mid-edit would move the edited row to a new index while the\n // editors remain at the old position, causing data/UI mismatch.\n // In grid mode (_isGridEditMode) sorting is safe — afterCellRender\n // re-injects editors into the re-sorted cells.\n // We return the cached previous sort result (same object references, so\n // in-place value mutations are already visible) instead of unsorted input.\n const el = this.gridElement as unknown as Record<string, unknown> | undefined;\n if (el && !el._isGridEditMode && typeof el._activeEditRows === 'number' && el._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n const sorted = applySorts([...rows], this.sortModel, [...this.columns]);\n this.cachedSortResult = sorted;\n return sorted;\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n // Update all sortable header cells with sort indicators\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // Remove any stale sort indicators left by a previous afterRender cycle\n // Base indicators use part=\"sort-indicator\", plugin indicators use class=\"sort-indicator\"\n const staleIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n staleIndicator?.remove();\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.emit('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n\n // Re-sort the model by priority to ensure correct order\n // This is handled after all columns are processed, but we maintain order here\n }\n // #endregion\n}\n"],"names":["applySorts","rows","sorts","columns","a","b","sort","comparator","c","defaultComparator","aVal","bVal","result","toggleSort","current","field","shiftKey","maxColumns","existing","s","getSortIndex","sortModel","index","getSortDirection","MultiSortPlugin","BaseGridPlugin","styles","el","sorted","event","gridEl","showIndex","cell","sortIndex","sortDir","indicator","filterBtn","resizeHandle","insertBefore","badge","model","state","existingIndex","newEntry"],"mappings":"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"}