@toolbox-web/grid 2.0.0-rc.1 → 2.0.0-rc.2
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.
- package/README.md +23 -6
- package/all.js +2 -2
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/internal/sorting.d.ts +4 -0
- package/lib/core/plugin/base-plugin.d.ts +31 -0
- package/lib/core/plugin/plugin-manager.d.ts +4 -1
- package/lib/core/plugin/types.d.ts +2 -0
- package/lib/core/types.d.ts +20 -1
- package/lib/plugins/clipboard/ClipboardPlugin.d.ts +8 -1
- package/lib/plugins/clipboard/index.js +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/export/ExportPlugin.d.ts +8 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +1 -1
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/GroupingRowsPlugin.d.ts +33 -1
- package/lib/plugins/grouping-rows/grouping-rows.d.ts +15 -1
- package/lib/plugins/grouping-rows/index.js +2 -2
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/multi-sort/index.js +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js +1 -1
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/PivotPlugin.d.ts +10 -0
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/print/index.js +1 -1
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/reorder-rows/RowReorderPlugin.d.ts +3 -0
- package/lib/plugins/reorder-rows/index.js +1 -1
- package/lib/plugins/reorder-rows/index.js.map +1 -1
- package/lib/plugins/responsive/index.js +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/TreePlugin.d.ts +16 -0
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/tree/types.d.ts +6 -0
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/index.js +1 -1
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/clipboard.umd.js +1 -1
- package/umd/plugins/clipboard.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/export.umd.js +1 -1
- package/umd/plugins/export.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pivot.umd.js +1 -1
- package/umd/plugins/pivot.umd.js.map +1 -1
- package/umd/plugins/reorder-rows.umd.js +1 -1
- package/umd/plugins/reorder-rows.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/tooltip.umd.js +1 -1
- package/umd/plugins/tooltip.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grouping-rows.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/GroupingRowsPlugin.ts"],"sourcesContent":["/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { DefaultExpandedValue, GroupDefinition, GroupingRowsConfig, GroupRowModelItem, RenderRow } from './types';\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({ rows, config, expanded, initialExpanded }: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n node.children.forEach((c) => visit(c));\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n node.children.forEach((c) => visit(c));\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { BaseGridPlugin, CellClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n incompatibleWith: [\n {\n name: 'tree',\n reason:\n 'Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while ' +\n 'GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid.',\n },\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'grouping-state-change',\n description: 'Emitted when groups are expanded/collapsed. Subscribers can react to row visibility changes.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded. Use to lazily load group row data.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /** Pre-defined group definitions (server-side mode) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Lazily loaded row data keyed by group key */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Whether an async groups fetch is currently in progress */\n private groupsFetchInFlight = false;\n /** Group keys with an in-flight rows fetch */\n private rowsFetchInFlight = new Set<string>();\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupsFetchInFlight = false;\n this.rowsFetchInFlight.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups) ||\n typeof config?.groups === 'function'\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || this.config.groups != null) {\n // If groups is an async callback and hasn't been resolved yet, trigger fetch\n if (typeof this.config.groups === 'function' && this.preDefinedGroups.length === 0 && !this.groupsFetchInFlight) {\n this.fetchGroupsAsync(this.config.groups);\n // Return empty while loading\n this.isActive = true;\n this.flattenedRows = [];\n return [];\n }\n if (this.preDefinedGroups.length > 0) {\n return this.processPreDefinedGroups();\n }\n // Static array path\n if (Array.isArray(this.config.groups) && this.config.groups.length > 0) {\n return this.processPreDefinedGroups();\n }\n // Groups callback in flight or empty result — return empty\n this.isActive = typeof this.config.groups === 'function';\n this.flattenedRows = [];\n return [];\n }\n\n const config = this.config;\n\n // Check if grouping is configured\n if (typeof config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const initialBuild = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: new Set(), // Empty to get all root groups\n });\n\n // If no grouping produced, return original rows\n if (initialBuild.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // Resolve defaultExpanded on first render only\n let initialExpanded: Set<string> | undefined;\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(config.defaultExpanded ?? false, allKeys);\n\n // Mark as applied and populate expandedKeys for subsequent toggles\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n // Build with proper expanded state\n const grouped = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: this.expandedKeys,\n initialExpanded,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n // The grid will need to handle group rows specially\n return grouped.map((item) => {\n if (item.kind === 'group') {\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: getGroupRowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n rowEl.innerHTML = result;\n } else {\n rowEl.innerHTML = '';\n rowEl.appendChild(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const style = this.animationStyle;\n if (style === false || this.keysToAnimate.size === 0) return;\n\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const item = this.flattenedRows[idx];\n const key = item?.kind === 'data' ? `data-${idx}` : undefined;\n\n if (key && this.keysToAnimate.has(key)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the row model from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n private processPreDefinedGroups(): any[] {\n const groups =\n this.preDefinedGroups.length > 0\n ? this.preDefinedGroups\n : Array.isArray(this.config.groups)\n ? this.config.groups\n : [];\n\n if (groups.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n\n // Track visible data rows for animation\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n return grouped.map((item) => {\n if (item.kind === 'group') {\n // Look up the pre-defined group to get rowCount from server\n const groupDef = this.findGroupDefinition(groups, item.key);\n const rowCount = groupDef?.rowCount ?? item.rows.length;\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount,\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /**\n * Fetch group definitions from an async callback.\n * Sets `preDefinedGroups` when resolved and triggers re-render.\n */\n private fetchGroupsAsync(fn: () => Promise<GroupDefinition[]>): void {\n this.groupsFetchInFlight = true;\n fn().then(\n (groups) => {\n this.groupsFetchInFlight = false;\n this.preDefinedGroups = groups;\n this.requestRender();\n },\n () => {\n // On error, clear the in-flight flag so a retry can happen\n this.groupsFetchInFlight = false;\n },\n );\n }\n\n /**\n * Fetch rows for a group using the `rows` callback and update state.\n * Manages loading indicator automatically. Guards against duplicate in-flight fetches.\n */\n private fetchGroupRowsAsync(groupKey: string, groupDef: GroupDefinition): void {\n const rowsFn = this.config.rows;\n if (!rowsFn || this.rowsFetchInFlight.has(groupKey)) return;\n\n this.rowsFetchInFlight.add(groupKey);\n this.loadingGroups.add(groupKey);\n this.requestRender();\n\n rowsFn(groupDef).then(\n (rows) => {\n this.rowsFetchInFlight.delete(groupKey);\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n },\n () => {\n this.rowsFetchInFlight.delete(groupKey);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n },\n );\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n */\n expandAll(): void {\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.emitPluginEvent('grouping-state-change', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n */\n collapseAll(): void {\n this.expandedKeys = collapseAllGroups();\n this.emitPluginEvent('grouping-state-change', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n\n // In accordion mode, collapse sibling groups when expanding\n if (config.accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n | GroupRowModelItem\n | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.emit<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Auto-fetch rows via `rows` callback if configured and not already cached\n if (config.rows && !this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.fetchGroupRowsAsync(key, groupDef);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n // Notify other plugins that grouping state changed (row visibility changed)\n this.emitPluginEvent('grouping-state-change', {\n expandedKeys: [...this.expandedKeys],\n });\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n * @param fn - The groupOn function or undefined to disable\n */\n setGroupOn(fn: ((row: any) => any[] | any | null | false) | undefined): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.rowsFetchInFlight.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups} or the resolved `groups` config.\n * Returns an empty array when using `groupOn`-based grouping or while\n * an async `groups` callback is still in flight.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * If the {@link GroupingRowsConfig.rows | rows} callback is configured,\n * this is called automatically — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n this.rowsFetchInFlight.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n this.rowsFetchInFlight.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"names":["buildGroupedRowModel","rows","config","expanded","initialExpanded","groupOn","root","key","value","depth","children","Map","forEach","r","path","Array","isArray","parent","rawVal","depthIdx","seg","String","composite","node","get","set","push","size","has","length","rowIndexMap","i","effectiveExpanded","Set","flat","visit","c","isExpanded","kind","row","rowIndex","resolveDefaultExpanded","allGroupKeys","buildPreDefinedGroupModel","groups","groupRows","loadingGroups","parentPath","group","currentPath","isLoading","childRows","__loading","__groupKey","idx","getGroupPath","targetKey","found","GroupingRowsPlugin","BaseGridPlugin","static","incompatibleWith","name","reason","events","type","description","queries","configRules","id","severity","message","check","accordion","defaultExpanded","styles","defaultConfig","showRowCount","indentWidth","aggregators","animation","expandedKeys","flattenedRows","isActive","previousVisibleKeys","keysToAnimate","hasAppliedDefaultExpanded","preDefinedGroups","groupRowsMap","groupsFetchInFlight","rowsFetchInFlight","animationStyle","this","isAnimationEnabled","detach","clear","getRowHeight","_index","groupRowHeight","__isGroupRow","handleQuery","query","context","detect","enableRowGrouping","processRows","processPreDefinedGroups","fetchGroupsAsync","initialBuild","allKeys","filter","map","getGroupKeys","grouped","currentVisibleKeys","item","add","__groupValue","__groupDepth","__groupRows","__groupExpanded","__groupRowCount","groupRow","__rowCacheKey","onCellClick","event","target","originalEvent","closest","GridClasses","GROUP_TOGGLE","toggle","onKeyDown","focusRow","grid","_focusRow","preventDefault","requestRenderWithFocus","renderRow","rowEl","_rowIndex","className","__isCustomRow","innerHTML","cell","document","createElement","style","gridColumn","setAttribute","textContent","appendChild","setRowLoadingState","groupRowRenderer","toggleExpand","result","handleToggle","setProperty","height","fullWidth","renderFullWidthGroupRow","renderPerColumnGroupRow","afterRender","body","gridElement","querySelector","animClass","querySelectorAll","parseInt","getAttribute","classList","addEventListener","remove","once","collectGroupKeys","groupDef","findGroupDefinition","rowCount","fn","then","requestRender","fetchGroupRowsAsync","groupKey","rowsFn","delete","keys","g","getActiveGroups","createToggleButton","btn","EXPANDED","setIcon","e","stopPropagation","getGroupLabelText","formatLabel","label","GROUP_LABEL","count","GROUP_COUNT","aggregatorEntries","Object","entries","aggregatesContainer","field","aggRef","col","columns","find","aggregatorRegistry","run","aggSpan","colHeader","header","bodyEl","gridTemplate","gridTemplateColumns","display","toggleRendered","colIdx","isExpanderColumn","firstColAgg","aggResult","expandAll","expandAllGroups","emitPluginEvent","collapseAll","isExpanding","newKeys","existingKey","startsWith","existingGroup","newSet","toggleGroupExpansion","emit","activeGroups","groupPath","groupName","announce","getA11yMessage","expand","collapse","getGroupState","expandedCount","totalGroups","getRowCount","refreshGroups","getExpandedGroups","getFlattenedRows","isGroupingActive","setGroupOn","setGroups","getGroups","setGroupRows","setGroupLoading","loading","clearGroupRows"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"grouping-rows.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/GroupingRowsPlugin.ts"],"sourcesContent":["/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { DefaultExpandedValue, GroupDefinition, GroupingRowsConfig, GroupRowModelItem, RenderRow } from './types';\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n /** Sort direction per group depth level. 1 = ascending, -1 = descending.\n * When omitted, groups at all levels sort ascending. */\n groupSortDirections?: Map<number, 1 | -1>;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({ rows, config, expanded, initialExpanded, groupSortDirections }: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Sort sibling groups by their group value so that group header order is\n // deterministic and respects the active sort direction for each depth level.\n // Default: ascending. When a user sorts a grouped column, the corresponding\n // depth level's direction flips, keeping groups in predictable order.\n const sortedChildren = (node: GroupNode): GroupNode[] => {\n const children = [...node.children.values()];\n // Determine direction for this depth level (children are one level deeper than the node)\n const childDepth = node === root ? 0 : node.depth + 1;\n const dir = groupSortDirections?.get(childDepth) ?? 1;\n children.sort((a, b) => {\n const av = a.value;\n const bv = b.value;\n if (av == null && bv == null) return 0;\n if (av == null) return dir;\n if (bv == null) return -dir;\n return av > bv ? dir : av < bv ? -dir : 0;\n });\n return children;\n };\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n for (const c of sortedChildren(node)) visit(c);\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n for (const c of sortedChildren(node)) visit(c);\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Discover which column field produces the group value at each depth level.\n *\n * Samples the first row's `groupOn` output to get the group path, then checks\n * which column fields produce matching values for that row. This mapping allows\n * the plugin to apply user-invoked column sort directions to the correct group\n * depth levels.\n *\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: (row: any) => any[] | any | null | false,\n columnFields: string[],\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n if (rows.length === 0) return depthToField;\n\n const sampleRow = rows[0];\n let path: any = groupOn(sampleRow);\n if (path == null || path === false) return depthToField;\n if (!Array.isArray(path)) path = [path];\n\n for (let depth = 0; depth < path.length; depth++) {\n const groupValue = path[depth];\n for (const field of columnFields) {\n if (sampleRow[field] === groupValue) {\n depthToField.set(depth, field);\n break;\n }\n }\n }\n return depthToField;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n modifiesRowStructure: true,\n hookPriority: {\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\n {\n name: 'tree',\n reason:\n 'Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while ' +\n 'GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid.',\n },\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'grouping-state-change',\n description: 'Emitted when groups are expanded/collapsed. Subscribers can react to row visibility changes.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded. Use to lazily load group row data.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n {\n type: 'grouping:get-grouped-fields',\n description: 'Returns the column field names that match group depth levels (string[])',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /**\n * Optional dependency on MultiSort for coordinated sort management.\n * When MultiSort is loaded, GroupingRows queries its sort model to determine\n * group header ordering. Without it, falls back to core sort state.\n */\n static override readonly dependencies = [\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /** Pre-defined group definitions (server-side mode) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Lazily loaded row data keyed by group key */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Whether an async groups fetch is currently in progress */\n private groupsFetchInFlight = false;\n /** Column fields that produce group values (depth 0, 1, ...). Cached for\n * the `grouping:get-grouped-fields` query so MultiSort can filter them out. */\n private groupedFields: string[] = [];\n /** User-specified sort directions per group depth level. Toggled via\n * header clicks on grouped columns. */\n private userGroupSortDirections = new Map<number, 1 | -1>();\n /** Group keys with an in-flight rows fetch */\n private rowsFetchInFlight = new Set<string>();\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupsFetchInFlight = false;\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n this.rowsFetchInFlight.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n if (query.type === 'grouping:get-grouped-fields') {\n return [...this.groupedFields];\n }\n return undefined;\n }\n\n /**\n * Intercept header clicks on grouped columns to toggle group sort direction.\n * Returns `true` for grouped columns to prevent MultiSort from handling them.\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n const fieldIndex = this.groupedFields.indexOf(event.field);\n if (fieldIndex === -1) return; // Not a grouped column — let other plugins handle it\n\n if (!event.column.sortable) return;\n\n // The fieldIndex in groupedFields corresponds to the group depth level\n // (resolveGroupFields populates them in depth order: 0, 1, 2, ...)\n const targetDepth = fieldIndex;\n\n // Toggle direction: asc → desc → asc\n const current = this.userGroupSortDirections.get(targetDepth) ?? 1;\n this.userGroupSortDirections.set(targetDepth, current === 1 ? -1 : 1);\n\n this.requestRender();\n return true; // Prevent MultiSort from handling this column\n }\n // #endregion\n\n // #region Sort State Resolution\n\n /**\n * Build a sort-direction map for each group depth level by cross-referencing\n * the active sort state with the column fields that produce group values.\n *\n * Supports three direction sources (in priority order):\n * 1. User-set directions via header clicks on grouped columns\n * 2. MultiSort plugin model (for backwards compatibility / state restore)\n * 3. Core single-column sort state\n */\n private resolveGroupSortDirections(rows: readonly any[]): Map<number, 1 | -1> | undefined {\n const config = this.config;\n if (typeof config.groupOn !== 'function' || rows.length === 0) {\n this.groupedFields = [];\n return undefined;\n }\n\n // Discover depth → field mapping by sampling rows\n const columnFields = this.columns.map((c) => c.field);\n const depthToField = resolveGroupFields([...rows], config.groupOn, columnFields);\n\n // Cache grouped field names for the grouping:get-grouped-fields query\n this.groupedFields = [...depthToField.values()];\n\n if (depthToField.size === 0) return undefined;\n\n // Start with user-set directions (from header clicks on grouped columns)\n const directions = new Map<number, 1 | -1>(this.userGroupSortDirections);\n\n // Fill missing depths from MultiSort model or core sort state\n if (directions.size < depthToField.size) {\n const activeSorts = new Map<string, 1 | -1>();\n\n const multiSortResults = this.grid?.query?.('sort:get-model', null);\n if (Array.isArray(multiSortResults) && multiSortResults.length > 0) {\n const sortModel = multiSortResults[0] as Array<{ field: string; direction: 'asc' | 'desc' }>;\n if (Array.isArray(sortModel)) {\n for (const entry of sortModel) {\n activeSorts.set(entry.field, entry.direction === 'desc' ? -1 : 1);\n }\n }\n }\n\n if (activeSorts.size === 0) {\n const gridHost = this.grid as unknown as { _sortState?: { field: string; direction: 1 | -1 } | null };\n if (gridHost._sortState) {\n activeSorts.set(gridHost._sortState.field, gridHost._sortState.direction);\n }\n }\n\n for (const [depth, field] of depthToField) {\n if (!directions.has(depth)) {\n const dir = activeSorts.get(field);\n if (dir !== undefined) {\n directions.set(depth, dir);\n }\n }\n }\n }\n\n return directions.size > 0 ? directions : undefined;\n }\n\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups) ||\n typeof config?.groups === 'function'\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || this.config.groups != null) {\n // If groups is an async callback and hasn't been resolved yet, trigger fetch\n if (typeof this.config.groups === 'function' && this.preDefinedGroups.length === 0 && !this.groupsFetchInFlight) {\n this.fetchGroupsAsync(this.config.groups);\n // Return empty while loading\n this.isActive = true;\n this.flattenedRows = [];\n return [];\n }\n if (this.preDefinedGroups.length > 0) {\n return this.processPreDefinedGroups();\n }\n // Static array path\n if (Array.isArray(this.config.groups) && this.config.groups.length > 0) {\n return this.processPreDefinedGroups();\n }\n // Groups callback in flight or empty result — return empty\n this.isActive = typeof this.config.groups === 'function';\n this.flattenedRows = [];\n return [];\n }\n\n const config = this.config;\n\n // Check if grouping is configured\n if (typeof config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const groupSortDirections = this.resolveGroupSortDirections(rows);\n const initialBuild = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: new Set(), // Empty to get all root groups\n groupSortDirections,\n });\n\n // If no grouping produced, return original rows\n if (initialBuild.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // Resolve defaultExpanded on first render only\n let initialExpanded: Set<string> | undefined;\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(config.defaultExpanded ?? false, allKeys);\n\n // Mark as applied and populate expandedKeys for subsequent toggles\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n // Build with proper expanded state\n const grouped = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: this.expandedKeys,\n initialExpanded,\n groupSortDirections,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n // The grid will need to handle group rows specially\n return grouped.map((item) => {\n if (item.kind === 'group') {\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: getGroupRowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n rowEl.innerHTML = result;\n } else {\n rowEl.innerHTML = '';\n rowEl.appendChild(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const style = this.animationStyle;\n if (style === false || this.keysToAnimate.size === 0) return;\n\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const item = this.flattenedRows[idx];\n const key = item?.kind === 'data' ? `data-${idx}` : undefined;\n\n if (key && this.keysToAnimate.has(key)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the row model from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n private processPreDefinedGroups(): any[] {\n const groups =\n this.preDefinedGroups.length > 0\n ? this.preDefinedGroups\n : Array.isArray(this.config.groups)\n ? this.config.groups\n : [];\n\n if (groups.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n\n // Track visible data rows for animation\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n return grouped.map((item) => {\n if (item.kind === 'group') {\n // Look up the pre-defined group to get rowCount from server\n const groupDef = this.findGroupDefinition(groups, item.key);\n const rowCount = groupDef?.rowCount ?? item.rows.length;\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount,\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /**\n * Fetch group definitions from an async callback.\n * Sets `preDefinedGroups` when resolved and triggers re-render.\n */\n private fetchGroupsAsync(fn: () => Promise<GroupDefinition[]>): void {\n this.groupsFetchInFlight = true;\n fn().then(\n (groups) => {\n this.groupsFetchInFlight = false;\n this.preDefinedGroups = groups;\n this.requestRender();\n },\n () => {\n // On error, clear the in-flight flag so a retry can happen\n this.groupsFetchInFlight = false;\n },\n );\n }\n\n /**\n * Fetch rows for a group using the `rows` callback and update state.\n * Manages loading indicator automatically. Guards against duplicate in-flight fetches.\n */\n private fetchGroupRowsAsync(groupKey: string, groupDef: GroupDefinition): void {\n const rowsFn = this.config.rows;\n if (!rowsFn || this.rowsFetchInFlight.has(groupKey)) return;\n\n this.rowsFetchInFlight.add(groupKey);\n this.loadingGroups.add(groupKey);\n this.requestRender();\n\n rowsFn(groupDef).then(\n (rows) => {\n this.rowsFetchInFlight.delete(groupKey);\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n },\n () => {\n this.rowsFetchInFlight.delete(groupKey);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n },\n );\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n */\n expandAll(): void {\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.emitPluginEvent('grouping-state-change', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n */\n collapseAll(): void {\n this.expandedKeys = collapseAllGroups();\n this.emitPluginEvent('grouping-state-change', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n\n // In accordion mode, collapse sibling groups when expanding\n if (config.accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n | GroupRowModelItem\n | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.emit<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Auto-fetch rows via `rows` callback if configured and not already cached\n if (config.rows && !this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.fetchGroupRowsAsync(key, groupDef);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n // Notify other plugins that grouping state changed (row visibility changed)\n this.emitPluginEvent('grouping-state-change', {\n expandedKeys: [...this.expandedKeys],\n });\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n * @param fn - The groupOn function or undefined to disable\n */\n setGroupOn(fn: ((row: any) => any[] | any | null | false) | undefined): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.rowsFetchInFlight.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups} or the resolved `groups` config.\n * Returns an empty array when using `groupOn`-based grouping or while\n * an async `groups` callback is still in flight.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * If the {@link GroupingRowsConfig.rows | rows} callback is configured,\n * this is called automatically — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n this.rowsFetchInFlight.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n this.rowsFetchInFlight.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"names":["buildGroupedRowModel","rows","config","expanded","initialExpanded","groupSortDirections","groupOn","root","key","value","depth","children","Map","forEach","r","path","Array","isArray","parent","rawVal","depthIdx","seg","String","composite","node","get","set","push","size","has","length","rowIndexMap","i","effectiveExpanded","Set","sortedChildren","values","childDepth","dir","sort","a","b","av","bv","flat","visit","c","isExpanded","kind","row","rowIndex","resolveDefaultExpanded","allGroupKeys","buildPreDefinedGroupModel","groups","groupRows","loadingGroups","parentPath","group","currentPath","isLoading","childRows","__loading","__groupKey","idx","getGroupPath","targetKey","found","GroupingRowsPlugin","BaseGridPlugin","static","modifiesRowStructure","hookPriority","onHeaderClick","incompatibleWith","name","reason","events","type","description","queries","configRules","id","severity","message","check","accordion","defaultExpanded","required","styles","defaultConfig","showRowCount","indentWidth","aggregators","animation","expandedKeys","flattenedRows","isActive","previousVisibleKeys","keysToAnimate","hasAppliedDefaultExpanded","preDefinedGroups","groupRowsMap","groupsFetchInFlight","groupedFields","userGroupSortDirections","rowsFetchInFlight","animationStyle","this","isAnimationEnabled","detach","clear","getRowHeight","_index","groupRowHeight","__isGroupRow","handleQuery","query","context","event","fieldIndex","indexOf","field","column","sortable","targetDepth","current","requestRender","resolveGroupSortDirections","columnFields","columns","map","depthToField","sampleRow","groupValue","resolveGroupFields","directions","activeSorts","multiSortResults","grid","sortModel","entry","direction","gridHost","_sortState","detect","enableRowGrouping","processRows","processPreDefinedGroups","fetchGroupsAsync","initialBuild","allKeys","filter","getGroupKeys","grouped","currentVisibleKeys","item","add","__groupValue","__groupDepth","__groupRows","__groupExpanded","__groupRowCount","groupRow","__rowCacheKey","onCellClick","target","originalEvent","closest","GridClasses","GROUP_TOGGLE","toggle","onKeyDown","focusRow","_focusRow","preventDefault","requestRenderWithFocus","renderRow","rowEl","_rowIndex","className","__isCustomRow","innerHTML","cell","document","createElement","style","gridColumn","setAttribute","textContent","appendChild","setRowLoadingState","groupRowRenderer","toggleExpand","result","handleToggle","setProperty","height","fullWidth","renderFullWidthGroupRow","renderPerColumnGroupRow","afterRender","body","gridElement","querySelector","animClass","querySelectorAll","parseInt","getAttribute","classList","addEventListener","remove","once","collectGroupKeys","groupDef","findGroupDefinition","rowCount","fn","then","fetchGroupRowsAsync","groupKey","rowsFn","delete","keys","g","getActiveGroups","createToggleButton","btn","EXPANDED","setIcon","e","stopPropagation","getGroupLabelText","formatLabel","label","GROUP_LABEL","count","GROUP_COUNT","aggregatorEntries","Object","entries","aggregatesContainer","aggRef","col","find","aggregatorRegistry","run","aggSpan","colHeader","header","bodyEl","gridTemplate","gridTemplateColumns","display","toggleRendered","colIdx","isExpanderColumn","firstColAgg","aggResult","expandAll","expandAllGroups","emitPluginEvent","collapseAll","isExpanding","newKeys","existingKey","startsWith","existingGroup","newSet","toggleGroupExpansion","emit","activeGroups","groupPath","groupName","announce","getA11yMessage","expand","collapse","getGroupState","expandedCount","totalGroups","getRowCount","refreshGroups","getExpandedGroups","getFlattenedRows","isGroupingActive","setGroupOn","setGroups","getGroups","setGroupRows","setGroupLoading","loading","clearGroupRows"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/internal/aria"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/aria","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_multiSort={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,r){"use strict";function i(t,e){return null==t&&null==e?0:null==t?1:null==e?-1:"number"==typeof t&&"number"==typeof e?t-e:t instanceof Date&&e instanceof Date?t.getTime()-e.getTime():"boolean"==typeof t&&"boolean"==typeof e?t===e?0:t?-1:1:String(t).localeCompare(String(e))}function o(t,e){const r=t.findIndex(t=>t.field===e);return r>=0?r+1:void 0}function s(t,e){return t.find(t=>t.field===e)?.direction}class n extends r.BaseGridPlugin{static manifest={queries:[{type:"sort:get-model",description:"Returns the current multi-sort model as SortModel[]"},{type:"sort:set-model",description:"Sets the multi-sort model from context (SortModel[])"}]};name="multiSort";styles='@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}}';get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];cachedSortResult=null;get#t(){return this.grid}clearCoreSortState(){this.#t._sortState=null}detach(){this.sortModel=[],this.cachedSortResult=null}handleQuery(t){switch(t.type){case"sort:get-model":return[...this.sortModel];case"sort:set-model":{const e=t.context;return!!Array.isArray(e)&&(this.sortModel=[...e],this.clearCoreSortState(),this.
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/internal/aria"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/aria","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_multiSort={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,r){"use strict";function i(t,e){return null==t&&null==e?0:null==t?1:null==e?-1:"number"==typeof t&&"number"==typeof e?t-e:t instanceof Date&&e instanceof Date?t.getTime()-e.getTime():"boolean"==typeof t&&"boolean"==typeof e?t===e?0:t?-1:1:String(t).localeCompare(String(e))}function o(t,e){const r=t.findIndex(t=>t.field===e);return r>=0?r+1:void 0}function s(t,e){return t.find(t=>t.field===e)?.direction}class n extends r.BaseGridPlugin{static manifest={queries:[{type:"sort:get-model",description:"Returns the current multi-sort model as SortModel[]"},{type:"sort:set-model",description:"Sets the multi-sort model from context (SortModel[])"}]};name="multiSort";styles='@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}}';get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];cachedSortResult=null;get#t(){return this.grid}clearCoreSortState(){this.#t._sortState=null}#e(t){const e=this.grid?.query?.("grouping:get-grouped-fields",null);if(!Array.isArray(e)||0===e.length)return t;const r=e[0];if(!Array.isArray(r)||0===r.length)return t;const i=new Set(r),o=t.filter(t=>!i.has(t.field));return o.length===t.length?t:o}detach(){this.sortModel=[],this.cachedSortResult=null}handleQuery(t){switch(t.type){case"sort:get-model":return[...this.sortModel];case"sort:set-model":{const e=t.context;return!!Array.isArray(e)&&(this.sortModel=[...e],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),!0)}default:return}}processRows(t){if(0===this.sortModel.length)return this.cachedSortResult=null,[...t];const e=this.#t;if(!e._isGridEditMode&&"number"==typeof e._activeEditRows&&-1!==e._activeEditRows&&this.cachedSortResult&&this.cachedSortResult.length===t.length)return[...this.cachedSortResult];const r=this.#e(this.sortModel),o=t;return r.length>0&&function(t,e,r){if(!e.length)return;const o=e.map(t=>{const e=r.find(e=>e.field===t.field);return{field:t.field,asc:"asc"===t.direction,comparator:e?.sortComparator??i}});if(1===o.length){const{field:e,asc:r,comparator:i}=o[0];t.sort((t,o)=>{const s=i(t[e],o[e],t,o);return r?s:-s})}else t.sort((t,e)=>{for(let r=0;r<o.length;r++){const{field:i,asc:s,comparator:n}=o[r],l=n(t[i],e[i],t,e);if(0!==l)return s?l:-l}return 0})}(o,r,this.columns),this.cachedSortResult=o,o}onHeaderClick(t){const r=this.columns.find(e=>e.field===t.field);if(!r?.sortable)return!1;const i=t.originalEvent.shiftKey,o=this.config.maxSortColumns??3;if(this.sortModel=function(t,e,r,i){const o=t.find(t=>t.field===e);return r?o?"asc"===o.direction?t.map(t=>t.field===e?{...t,direction:"desc"}:t):t.filter(t=>t.field!==e):t.length<i?[...t,{field:e,direction:"asc"}]:t:"asc"===o?.direction?[{field:e,direction:"desc"}]:"desc"===o?.direction?[]:[{field:e,direction:"asc"}]}(this.sortModel,t.field,i,o),this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),this.grid?.requestStateChange?.(),this.sortModel.length>0){const t=this.sortModel.map(t=>{const e=this.columns.find(e=>e.field===t.field);return`${e?.header??t.field} ${"asc"===t.direction?"ascending":"descending"}`});e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortApplied",t.join(", then "),""))}else e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortCleared"));return!0}afterRender(){const t=this.gridElement;if(!t)return;const e=!1!==this.config.showSortIndex;t.querySelectorAll(".header-row .cell[data-field]").forEach(t=>{const r=t.getAttribute("data-field");if(!r)return;const i=o(this.sortModel,r),n=s(this.sortModel,r);if(t.querySelector(".sort-index")?.remove(),n){const r=this.updateSortIndicator(t,n);if(e&&this.sortModel.length>1&&void 0!==i){const e=document.createElement("span");e.className="sort-index",e.textContent=String(i),r.nextSibling?t.insertBefore(e,r.nextSibling):t.appendChild(e)}}else t.classList.contains("sortable")&&this.updateSortIndicator(t,null)})}getSortModel(){return[...this.sortModel]}setSortModel(t){if(this.sortModel=[...t],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...t]}),this.requestRender(),this.grid?.requestStateChange?.(),t.length>0){const r=t.map(t=>{const e=this.columns.find(e=>e.field===t.field);return`${e?.header??t.field} ${"asc"===t.direction?"ascending":"descending"}`});e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortApplied",r.join(", then "),""))}}clearSort(){this.sortModel=[],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[]}),this.requestRender(),this.grid?.requestStateChange?.(),e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortCleared"))}getSortIndex(t){return o(this.sortModel,t)}getSortDirection(t){return s(this.sortModel,t)}getColumnState(t){const e=this.sortModel.findIndex(e=>e.field===t);if(-1===e)return;return{sort:{direction:this.sortModel[e].direction,priority:e}}}applyColumnState(t,e){if(!e.sort)return void(this.sortModel=this.sortModel.filter(e=>e.field!==t));const r=this.sortModel.findIndex(e=>e.field===t),i={field:t,direction:e.sort.direction};-1!==r?this.sortModel[r]=i:this.sortModel.splice(e.sort.priority,0,i),this.clearCoreSortState()}}t.MultiSortPlugin=n,Object.defineProperty(t,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 const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n return {\n field: sort.field,\n asc: sort.direction === 'asc',\n comparator: col?.sortComparator ?? defaultComparator,\n };\n });\n\n if (chain.length === 1) {\n // Single-sort fast path — avoid loop overhead\n const { field, asc, comparator } = chain[0];\n rows.sort((a: any, b: any) => {\n const result = comparator(a[field], b[field], a, b);\n return asc ? result : -result;\n });\n } else {\n rows.sort((a: any, b: any) => {\n for (let i = 0; i < chain.length; i++) {\n const { field, asc, comparator } = chain[i];\n const result = comparator(a[field], b[field], a, b);\n if (result !== 0) return asc ? result : -result;\n }\n return 0;\n });\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 { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, 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 * ## 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 * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('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.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', 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 /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\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\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\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 Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\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 grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort in-place — the input array is already a mutable copy from plugin-manager.\n // Pre-resolved comparator chain avoids column lookup on every pair comparison.\n const mutableRows = rows as unknown[];\n sortRowsInPlace(mutableRows, this.sortModel, this.columns);\n this.cachedSortResult = mutableRows;\n return mutableRows;\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 this.clearCoreSortState();\n\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\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 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 cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\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 if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\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.clearCoreSortState();\n this.emit('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\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 // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n }\n // #endregion\n}\n"],"names":["defaultComparator","a","b","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","static","queries","type","description","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","detach","handleQuery","query","model","context","Array","isArray","emit","requestRender","processRows","rows","length","_isGridEditMode","_activeEditRows","mutableRows","sorts","columns","chain","map","sort","col","c","asc","comparator","sortComparator","result","i","sortRowsInPlace","onHeaderClick","event","column","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","filter","toggleSort","requestStateChange","labels","header","announce","gridElement","getA11yMessage","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","querySelector","remove","indicator","updateSortIndicator","badge","document","createElement","className","textContent","nextSibling","insertBefore","appendChild","classList","contains","getSortModel","setSortModel","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"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 const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n return {\n field: sort.field,\n asc: sort.direction === 'asc',\n comparator: col?.sortComparator ?? defaultComparator,\n };\n });\n\n if (chain.length === 1) {\n // Single-sort fast path — avoid loop overhead\n const { field, asc, comparator } = chain[0];\n rows.sort((a: any, b: any) => {\n const result = comparator(a[field], b[field], a, b);\n return asc ? result : -result;\n });\n } else {\n rows.sort((a: any, b: any) => {\n for (let i = 0; i < chain.length; i++) {\n const { field, asc, comparator } = chain[i];\n const result = comparator(a[field], b[field], a, b);\n if (result !== 0) return asc ? result : -result;\n }\n return 0;\n });\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 { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, 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 * ## 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 * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('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.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', 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 /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\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\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\n\n /**\n * Remove sorts on fields that are owned by the grouping plugin.\n * GroupingRowsPlugin handles group header ordering independently, so\n * multi-sort should only sort by non-grouped data columns.\n */\n #filterGroupedFields(model: SortModel[]): SortModel[] {\n const results = this.grid?.query?.('grouping:get-grouped-fields', null);\n if (!Array.isArray(results) || results.length === 0) return model;\n\n const groupedFields = results[0] as string[];\n if (!Array.isArray(groupedFields) || groupedFields.length === 0) return model;\n\n const groupedSet = new Set(groupedFields);\n const filtered = model.filter((s) => !groupedSet.has(s.field));\n return filtered.length === model.length ? model : filtered;\n }\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 Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\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 grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort in-place — the input array is already a mutable copy from plugin-manager.\n // Pre-resolved comparator chain avoids column lookup on every pair comparison.\n // Exclude fields owned by the grouping plugin — group header order is handled\n // by GroupingRowsPlugin, so multi-sort should only affect within-group data order.\n const effectiveModel = this.#filterGroupedFields(this.sortModel);\n\n const mutableRows = rows as unknown[];\n if (effectiveModel.length > 0) {\n sortRowsInPlace(mutableRows, effectiveModel, this.columns);\n }\n this.cachedSortResult = mutableRows;\n return mutableRows;\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 this.clearCoreSortState();\n\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\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 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 cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\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 if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\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.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\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 // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n }\n // #endregion\n}\n"],"names":["defaultComparator","a","b","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","static","queries","type","description","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","filterGroupedFields","model","results","query","Array","isArray","length","groupedFields","groupedSet","Set","filtered","filter","has","detach","handleQuery","context","broadcast","requestRender","processRows","rows","_isGridEditMode","_activeEditRows","effectiveModel","mutableRows","sorts","columns","chain","map","sort","col","c","asc","comparator","sortComparator","result","i","sortRowsInPlace","onHeaderClick","event","column","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","toggleSort","requestStateChange","labels","header","announce","gridElement","getA11yMessage","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","querySelector","remove","indicator","updateSortIndicator","badge","document","createElement","className","textContent","nextSibling","insertBefore","appendChild","classList","contains","getSortModel","setSortModel","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"mappings":"8ZAwEO,SAASA,EAAkBC,EAAYC,GAE5C,OAAS,MAALD,GAAkB,MAALC,EAAkB,EAC1B,MAALD,EAAkB,EACb,MAALC,GAAkB,EAGL,iBAAND,GAA+B,iBAANC,EAC3BD,EAAIC,EAGTD,aAAaE,MAAQD,aAAaC,KAC7BF,EAAEG,UAAYF,EAAEE,UAIR,kBAANH,GAAgC,kBAANC,EAC5BD,IAAMC,EAAI,EAAID,GAAI,EAAK,EAIzBI,OAAOJ,GAAGK,cAAcD,OAAOH,GACxC,CAmDO,SAASK,EAAaC,EAAwBC,GACnD,MAAMC,EAAQF,EAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GACrD,OAAOC,GAAS,EAAIA,EAAQ,OAAI,CAClC,CASO,SAASG,EAAiBL,EAAwBC,GACvD,OAAOD,EAAUM,KAAMF,GAAMA,EAAEH,QAAUA,IAAQM,SACnD,CCnFO,MAAMC,UAAwBC,EAAAA,eAKnCC,gBAAoD,CAClDC,QAAS,CACP,CAAEC,KAAM,iBAAkBC,YAAa,uDACvC,CAAED,KAAM,iBAAkBC,YAAa,0DAKlCC,KAAO,YAEEC,isBAGlB,iBAAuBC,GACrB,MAAO,CACLC,eAAgB,EAChBC,eAAe,EAEnB,CAGQlB,UAAyB,GAIzBmB,iBAAqC,KAG7C,KAAIC,GACF,OAAOC,KAAKC,IACd,CASQ,kBAAAC,GACNF,MAAKD,EAAcI,WAAa,IAClC,CAOA,EAAAC,CAAqBC,GACnB,MAAMC,EAAUN,KAAKC,MAAMM,QAAQ,8BAA+B,MAClE,IAAKC,MAAMC,QAAQH,IAA+B,IAAnBA,EAAQI,OAAc,OAAOL,EAE5D,MAAMM,EAAgBL,EAAQ,GAC9B,IAAKE,MAAMC,QAAQE,IAA2C,IAAzBA,EAAcD,OAAc,OAAOL,EAExE,MAAMO,EAAa,IAAIC,IAAIF,GACrBG,EAAWT,EAAMU,OAAQhC,IAAO6B,EAAWI,IAAIjC,EAAEH,QACvD,OAAOkC,EAASJ,SAAWL,EAAMK,OAASL,EAAQS,CACpD,CAMS,MAAAG,GACPjB,KAAKrB,UAAY,GACjBqB,KAAKF,iBAAmB,IAC1B,CAMS,WAAAoB,CAAYX,GACnB,OAAQA,EAAMhB,MACZ,IAAK,iBACH,MAAO,IAAIS,KAAKrB,WAClB,IAAK,iBAAkB,CACrB,MAAM0B,EAAQE,EAAMY,QACpB,QAAKX,MAAMC,QAAQJ,KACnBL,KAAKrB,UAAY,IAAI0B,GACrBL,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAIqB,KAAKrB,aACpDqB,KAAKqB,iBACE,EACT,CACA,QACE,OAEN,CAOS,WAAAC,CAAYC,GACnB,GAA8B,IAA1BvB,KAAKrB,UAAU+B,OAEjB,OADAV,KAAKF,iBAAmB,KACjB,IAAIyB,GAUb,MAAMtB,EAAOD,MAAKD,EAClB,IAAKE,EAAKuB,iBAAmD,iBAAzBvB,EAAKwB,kBAAyD,IAAzBxB,EAAKwB,iBACxEzB,KAAKF,kBAAoBE,KAAKF,iBAAiBY,SAAWa,EAAKb,OACjE,MAAO,IAAIV,KAAKF,kBAQpB,MAAM4B,EAAiB1B,MAAKI,EAAqBJ,KAAKrB,WAEhDgD,EAAcJ,EAKpB,OAJIG,EAAehB,OAAS,GD5KzB,SAAyCa,EAAcK,EAAoBC,GAChF,IAAKD,EAAMlB,OAAQ,OAGnB,MAAMoB,EAAQF,EAAMG,IAAKC,IACvB,MAAMC,EAAMJ,EAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUoD,EAAKpD,OACjD,MAAO,CACLA,MAAOoD,EAAKpD,MACZuD,IAAwB,QAAnBH,EAAK9C,UACVkD,WAAYH,GAAKI,gBAAkBlE,KAIvC,GAAqB,IAAjB2D,EAAMpB,OAAc,CAEtB,MAAM9B,MAAEA,EAAAuD,IAAOA,EAAAC,WAAKA,GAAeN,EAAM,GACzCP,EAAKS,KAAK,CAAC5D,EAAQC,KACjB,MAAMiE,EAASF,EAAWhE,EAAEQ,GAAQP,EAAEO,GAAQR,EAAGC,GACjD,OAAO8D,EAAMG,GAAUA,GAE3B,MACEf,EAAKS,KAAK,CAAC5D,EAAQC,KACjB,IAAA,IAASkE,EAAI,EAAGA,EAAIT,EAAMpB,OAAQ6B,IAAK,CACrC,MAAM3D,MAAEA,EAAAuD,IAAOA,EAAAC,WAAKA,GAAeN,EAAMS,GACnCD,EAASF,EAAWhE,EAAEQ,GAAQP,EAAEO,GAAQR,EAAGC,GACjD,GAAe,IAAXiE,EAAc,OAAOH,EAAMG,GAAUA,CAC3C,CACA,OAAO,GAGb,CC+IME,CAAgBb,EAAaD,EAAgB1B,KAAK6B,SAEpD7B,KAAKF,iBAAmB6B,EACjBA,CACT,CAGS,aAAAc,CAAcC,GACrB,MAAMC,EAAS3C,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAU8D,EAAM9D,OAC1D,IAAK+D,GAAQC,SAAU,OAAO,EAE9B,MAAMC,EAAWH,EAAMI,cAAcD,SAC/BE,EAAa/C,KAAKgD,OAAOpD,gBAAkB,EAUjD,GARAI,KAAKrB,UDhHF,SAAoBsE,EAAsBrE,EAAeiE,EAAmBE,GACjF,MAAMG,EAAWD,EAAQhE,KAAMF,GAAMA,EAAEH,QAAUA,GAEjD,OAAIiE,EAEEK,EACyB,QAAvBA,EAAShE,UAEJ+D,EAAQlB,IAAKhD,GAAOA,EAAEH,QAAUA,EAAQ,IAAKG,EAAGG,UAAW,QAAoBH,GAG/EkE,EAAQlC,OAAQhC,GAAMA,EAAEH,QAAUA,GAElCqE,EAAQvC,OAASqC,EAEnB,IAAIE,EAAS,CAAErE,QAAOM,UAAW,QAGnC+D,EAGqB,QAAxBC,GAAUhE,UACL,CAAC,CAAEN,QAAOM,UAAW,SACK,SAAxBgE,GAAUhE,UACZ,GAEF,CAAC,CAAEN,QAAOM,UAAW,OAEhC,CCoFqBiE,CAAWnD,KAAKrB,UAAW+D,EAAM9D,MAAOiE,EAAUE,GACnE/C,KAAKE,qBAELF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAIqB,KAAKrB,aACpDqB,KAAKqB,gBACLrB,KAAKC,MAAMmD,uBAGPpD,KAAKrB,UAAU+B,OAAS,EAAG,CAC7B,MAAM2C,EAASrD,KAAKrB,UAAUoD,IAAKhD,IACjC,MAAMkD,EAAMjC,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUG,EAAEH,OACnD,MAAO,GAAGqD,GAAKqB,QAAUvE,EAAEH,SAAyB,QAAhBG,EAAEG,UAAsB,YAAc,iBAE5EqE,EAAAA,SAASvD,KAAKwD,YAAcC,EAAAA,eAAezD,KAAKwD,YAAc,cAAeH,EAAOK,KAAK,WAAY,IACvG,MACEH,EAAAA,SAASvD,KAAKwD,YAAcC,EAAAA,eAAezD,KAAKwD,YAAc,gBAGhE,OAAO,CACT,CAGS,WAAAG,GACP,MAAMC,EAAS5D,KAAKwD,YACpB,IAAKI,EAAQ,OAEb,MAAMC,GAA0C,IAA9B7D,KAAKgD,OAAOnD,cAEV+D,EAAOE,iBAAiB,iCAChCC,QAASC,IACnB,MAAMpF,EAAQoF,EAAKC,aAAa,cAChC,IAAKrF,EAAO,OAEZ,MAAMsF,EAAYxF,EAAasB,KAAKrB,UAAWC,GACzCuF,EAAUnF,EAAiBgB,KAAKrB,UAAWC,GAKjD,GAFAoF,EAAKI,cAAc,gBAAgBC,SAE/BF,EAAS,CACX,MAAMG,EAAYtE,KAAKuE,oBAAoBP,EAAMG,GAGjD,GAAIN,GAAa7D,KAAKrB,UAAU+B,OAAS,QAAmB,IAAdwD,EAAyB,CACrE,MAAMM,EAAQC,SAASC,cAAc,QACrCF,EAAMG,UAAY,aAClBH,EAAMI,YAAcpG,OAAO0F,GACvBI,EAAUO,YACZb,EAAKc,aAAaN,EAAOF,EAAUO,aAEnCb,EAAKe,YAAYP,EAErB,CACF,MAAWR,EAAKgB,UAAUC,SAAS,aACjCjF,KAAKuE,oBAAoBP,EAAM,OAGrC,CASA,YAAAkB,GACE,MAAO,IAAIlF,KAAKrB,UAClB,CAMA,YAAAwG,CAAa9E,GAMX,GALAL,KAAKrB,UAAY,IAAI0B,GACrBL,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAI0B,KAC/CL,KAAKqB,gBACLrB,KAAKC,MAAMmD,uBACP/C,EAAMK,OAAS,EAAG,CACpB,MAAM2C,EAAShD,EAAM0B,IAAKhD,IACxB,MAAMkD,EAAMjC,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUG,EAAEH,OACnD,MAAO,GAAGqD,GAAKqB,QAAUvE,EAAEH,SAAyB,QAAhBG,EAAEG,UAAsB,YAAc,iBAE5EqE,EAAAA,SAASvD,KAAKwD,YAAcC,EAAAA,eAAezD,KAAKwD,YAAc,cAAeH,EAAOK,KAAK,WAAY,IACvG,CACF,CAKA,SAAA0B,GACEpF,KAAKrB,UAAY,GACjBqB,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,KAC3CqB,KAAKqB,gBACLrB,KAAKC,MAAMmD,uBACXG,EAAAA,SAASvD,KAAKwD,YAAcC,EAAAA,eAAezD,KAAKwD,YAAc,eAChE,CAOA,YAAA9E,CAAaE,GACX,OAAOF,EAAasB,KAAKrB,UAAWC,EACtC,CAOA,gBAAAI,CAAiBJ,GACf,OAAOI,EAAiBgB,KAAKrB,UAAWC,EAC1C,CASS,cAAAyG,CAAezG,GACtB,MAAMC,EAAQmB,KAAKrB,UAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GAC1D,QAAIC,EAAc,OAGlB,MAAO,CACLmD,KAAM,CACJ9C,UAHcc,KAAKrB,UAAUE,GAGRK,UACrBoG,SAAUzG,GAGhB,CAOS,gBAAA0G,CAAiB3G,EAAe4G,GAEvC,IAAKA,EAAMxD,KAGT,YADAhC,KAAKrB,UAAYqB,KAAKrB,UAAUoC,OAAQhC,GAAMA,EAAEH,QAAUA,IAK5D,MAAM6G,EAAgBzF,KAAKrB,UAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GAC5D8G,EAAsB,CAC1B9G,QACAM,UAAWsG,EAAMxD,KAAK9C,YAGF,IAAlBuG,EAEFzF,KAAKrB,UAAU8G,GAAiBC,EAGhC1F,KAAKrB,UAAUgH,OAAOH,EAAMxD,KAAKsD,SAAU,EAAGI,GAMhD1F,KAAKE,oBACP"}
|