@toolbox-web/grid 3.9.2 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all.js +1 -1
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/types.d.ts +6 -3
- package/lib/features/clipboard.js.map +1 -1
- package/lib/features/column-virtualization.js.map +1 -1
- package/lib/features/context-menu.js.map +1 -1
- package/lib/features/editing.js.map +1 -1
- package/lib/features/export.js.map +1 -1
- package/lib/features/filtering.js.map +1 -1
- package/lib/features/grouping-columns.js.map +1 -1
- package/lib/features/grouping-rows.js.map +1 -1
- package/lib/features/master-detail.js.map +1 -1
- package/lib/features/multi-sort.js.map +1 -1
- package/lib/features/pinned-columns.js.map +1 -1
- package/lib/features/pinned-rows.js.map +1 -1
- package/lib/features/pivot.js.map +1 -1
- package/lib/features/print.js.map +1 -1
- package/lib/features/registry.js +1 -1
- package/lib/features/registry.js.map +1 -1
- package/lib/features/reorder-columns.js.map +1 -1
- package/lib/features/responsive.js.map +1 -1
- package/lib/features/row-drag-drop.js.map +1 -1
- package/lib/features/selection.js.map +1 -1
- package/lib/features/server-side.js.map +1 -1
- package/lib/features/shell.js.map +1 -1
- package/lib/features/sticky-rows.js.map +1 -1
- package/lib/features/tooltip.js.map +1 -1
- package/lib/features/tree.js.map +1 -1
- package/lib/features/undo-redo.js.map +1 -1
- package/lib/features/visibility.js.map +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/editors.d.ts +15 -0
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/types.d.ts +5 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/filter-model.d.ts +20 -1
- package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
- package/lib/plugins/filtering/index.d.ts +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/filtering/types.d.ts +58 -3
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/hierarchy/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.map +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
- package/lib/plugins/pivot/types.d.ts +2 -0
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/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.map +1 -1
- package/lib/plugins/shared/form-field-target.d.ts +10 -0
- package/lib/plugins/shared/format-value.d.ts +12 -0
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/undo-redo/index.js.map +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.map +1 -1
- package/umd/plugins/column-virtualization.umd.js.map +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/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.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/hierarchy.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pinned-rows.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/print.umd.js.map +1 -1
- package/umd/plugins/reorder-columns.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js.map +1 -1
- package/umd/plugins/row-drag-drop.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.map +1 -1
- package/umd/plugins/shell.umd.js.map +1 -1
- package/umd/plugins/sticky-rows.umd.js.map +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
- package/umd/plugins/undo-redo.umd.js.map +1 -1
- package/umd/plugins/visibility.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grouping-rows.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/core/constants.ts","../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../libs/grid/src/lib/core/internal/row-teardown.ts","../../../../../libs/grid/src/lib/plugins/shared/loading-placeholder.ts","../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy.ts","../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy-plugin.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { FrameworkAdapter, GridConfig, LoadingContext, RowElementInternal } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/** Remove a cell's owned loading overlay and release adapter-managed content. */\nexport function removeCellLoadingOverlay(cellEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!cellEl.firstElementChild) return;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Move a cell's owned loading overlay in front of the cell while its content is rewritten,\n * so the still-active loader survives adapter release and content replacement. The caller\n * appends it back. The overlay stays connected because React prunes detached portal containers.\n */\nexport function parkCellLoadingOverlay(cellEl: HTMLElement): HTMLElement | null {\n if (!cellEl.firstElementChild) return null;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return null;\n const parent = cellEl.parentElement;\n if (parent) parent.insertBefore(overlay, cellEl);\n else overlay.remove();\n return overlay;\n}\n\n/** Remove a row's owned loading overlay and release adapter-managed content. */\nexport function removeRowLoadingOverlay(rowEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!rowEl.firstElementChild) return;\n const overlay = rowEl.querySelector<HTMLElement>(':scope > .tbw-row-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * in place of the default spinner. Only invoked when the overlay is created.\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setRowLoadingState(\n rowEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present.\n // Direct children only — a nested grid's row overlay must not be mistaken for this row's.\n if (!rowEl.querySelector(':scope > .tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n if (renderer) {\n overlay.appendChild(createLoadingContent('small', renderer));\n } else {\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n }\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n removeRowLoadingOverlay(rowEl, adapter);\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setCellLoadingState(\n cellEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n\n // Breadcrumb so the render hot path can skip the per-cell scan on the\n // overwhelming majority of rows. Conservative: only ever set here, so it can\n // read stale-true after the last loading cell clears (costs one extra scan).\n const rowEl = cellEl.parentElement as RowElementInternal | null;\n if (rowEl) rowEl.__hasCellLoading = true;\n\n if (renderer) {\n cellEl.classList.add('tbw-cell-loading-custom');\n if (!cellEl.querySelector(':scope > .tbw-cell-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-cell-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n overlay.appendChild(createLoadingContent('small', renderer));\n cellEl.appendChild(overlay);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading-custom');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.classList.remove('tbw-cell-loading-custom');\n cellEl.removeAttribute('aria-busy');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n}\n","/**\n * Row teardown shared by the core render path and plugin-rendered rows\n * (loading placeholders), so framework-adapter cleanup has one implementation.\n */\nimport type { GridHost } from '../types';\nimport { removeCellLoadingOverlay, removeRowLoadingOverlay } from './loading';\n\n/**\n * Release a pooled row's adapter-owned content — its row loader, cell loaders, then\n * cell views — before the row is discarded. Callers own the `beginBatch`/`endBatch`\n * bracket so a multi-row teardown collapses into one deferred commit (#330).\n */\nexport function releasePooledRow(adapter: GridHost['__frameworkAdapter'], rowEl: HTMLElement): void {\n if (!adapter?.releaseCell && !adapter?.unmount) return;\n removeRowLoadingOverlay(rowEl, adapter);\n const cells = rowEl.children;\n for (let c = 0; c < cells.length; c++) {\n const cell = cells[c] as HTMLElement;\n removeCellLoadingOverlay(cell, adapter);\n if (cell.firstElementChild) adapter.releaseCell?.(cell);\n }\n}\n\n/**\n * Wipe a pooled row's cells ahead of a full rebuild.\n *\n * Framework views are released BEFORE the DOM is wiped — without this,\n * Angular EmbeddedViewRefs / React roots / Vue apps created by renderers and\n * editor factories stay alive in the adapter's tracking arrays after their DOM\n * is destroyed, leaking memory on every recycle. Wrapped in `beginBatch`/`endBatch`\n * because `innerHTML = ''` detaches every cell as a group, letting adapters defer\n * per-cell sync commits to a single render once detachment completes (#330).\n */\nexport function clearRowForRebuild(grid: Pick<GridHost, '__frameworkAdapter'> & HTMLElement, rowEl: HTMLElement): void {\n // Clear loading state before rebuild — grid re-applies it after render for\n // actually-loading rows. Prevents a stale tbw-row-loading class from\n // persisting when pool elements are recycled.\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n const adapter = grid.__frameworkAdapter;\n if (!adapter?.releaseCell && !adapter?.unmount) {\n rowEl.innerHTML = '';\n return;\n }\n adapter.beginBatch?.(grid);\n try {\n removeRowLoadingOverlay(rowEl, adapter);\n const children = rowEl.children;\n for (let i = children.length - 1; i >= 0; i--) {\n const cell = children[i] as HTMLElement;\n removeCellLoadingOverlay(cell, adapter);\n adapter.releaseCell?.(cell);\n }\n rowEl.innerHTML = '';\n } finally {\n adapter.endBatch?.(grid);\n }\n}\n","/**\n * Loading placeholder rows shared by plugins that synthesize them (ServerSide\n * blocks in flight, GroupingRows groups being fetched). Inlined into each\n * plugin bundle that imports it — core never calls it.\n */\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { clearRowForRebuild } from '../../core/internal/row-teardown';\nimport type { GridHost, RowElementInternal } from '../../core/types';\n\n/**\n * Marker class on placeholder rows. Lets layout plugins (Responsive card mode)\n * tell a placeholder apart from a data row — `tbw-row-loading` is not enough,\n * because `grid.setRowLoading()` puts that on real rows too.\n */\nexport const PLACEHOLDER_ROW_CLASS = 'placeholder-row';\n\n/**\n * Render a placeholder as one full-width cell in the core row-loading state, so\n * no column renderer runs for a row that holds no data.\n *\n * Call from a plugin `renderRow` hook and return `true`. The previous cells are\n * released through the framework adapter first (a pooled element may have held\n * React portals / Vue teleports / Angular views for a loaded row). The row is\n * flagged `__isCustomRow`, which forces a full rebuild once real data replaces\n * the placeholder; that rebuild also clears the loading state.\n *\n * Height: the row occupies exactly what virtualization reserves for it — the\n * height a plugin reports through `getRowHeight()` (e.g. a Responsive card), else\n * the default row height. Any inline height a recycled element carried (a\n * Responsive card height, a per-row `--tbw-row-height`) is dropped.\n */\nexport function renderLoadingPlaceholderRow(grid: GridHost, rowEl: HTMLElement, row: unknown, rowIndex: number): void {\n const el = rowEl as RowElementInternal;\n syncPlaceholderHeight(grid, el, row, rowIndex);\n // Same placeholder, still intact: nothing else to do (renders run every scroll frame).\n if (el.__rowDataRef === row && el.__isCustomRow && el.classList.contains('tbw-row-loading')) return;\n\n clearRowForRebuild(grid, el);\n el.className = `data-grid-row ${PLACEHOLDER_ROW_CLASS}`;\n el.__isCustomRow = true;\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'; // keeps the row at its normal height\n el.appendChild(cell);\n setRowLoadingState(el, true, grid.effectiveConfig?.loadingRenderer);\n}\n\n/** Match the row's DOM height to the slot virtualization reserves for it. Writes only on change. */\nfunction syncPlaceholderHeight(grid: GridHost, el: HTMLElement, row: unknown, rowIndex: number): void {\n const { style } = el;\n if (style.getPropertyValue('--tbw-row-height')) style.removeProperty('--tbw-row-height');\n const pluginHeight = grid._getPluginRowHeight?.(row, rowIndex);\n const height = pluginHeight !== undefined && pluginHeight > 0 ? `${pluginHeight}px` : '';\n if (style.height !== height) style.height = height;\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Hierarchy Engine\n *\n * Pure row-model builder shared by the HierarchyPlugin and its contributors.\n */\n\nimport type { HierarchyContributor, HierarchyNode, HierarchyRowMeta } from './types';\n\n/**\n * Build the flat row list for `rows` from every contributor's sibling-list\n * transforms, recording each emitted row's ARIA position in `meta`.\n *\n * Contributors whose `beginHierarchy` returns `false` sit out the rebuild.\n * With no active contributor the rows pass through unchanged. When every\n * active contributor's `endHierarchy` returns `false`, no position is kept in `meta`.\n *\n * This is what `HierarchyPlugin.process()` runs for all attached contributors.\n * Call it directly to run a contributor without a grid — e.g. as the\n * `processRows` fallback when no HierarchyPlugin is attached, or in unit tests.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * const hierarchy = this.grid?.getPluginByName('hierarchy');\n * return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n * }\n * ```\n *\n * @param rows - The root rows.\n * @param contributors - Contributors to run, in `processRows` hook order.\n * @param meta - Receives each emitted row's position. Pass a fresh map per rebuild.\n * @returns The flattened rows, in render order.\n * @since 3.9.0\n */\nexport function buildHierarchy(\n rows: readonly unknown[],\n contributors: readonly HierarchyContributor[],\n meta: WeakMap<object, HierarchyRowMeta> = new WeakMap(),\n): unknown[] {\n const active: HierarchyContributor[] = [];\n for (const stage of ['structure', 'transform'] as const) {\n for (const c of contributors) {\n if (c.hierarchyStage === stage && c.beginHierarchy(rows)) active.push(c);\n }\n }\n if (active.length === 0) return [...rows];\n\n const out: unknown[] = [];\n const process = (nodes: HierarchyNode[], parent: HierarchyNode | null, depth: number): HierarchyNode[] => {\n const context = { parent: parent ? parent.row : null, parentKey: parent?.key ?? null, depth };\n for (const c of active) nodes = c.processSiblings(nodes, context);\n return nodes;\n };\n const emit = (nodes: HierarchyNode[], depth: number): void => {\n const setSize = nodes.length;\n for (let i = 0; i < setSize; i++) {\n const node = nodes[i];\n out.push(node.row);\n if (typeof node.row === 'object' && node.row !== null) {\n meta.set(node.row, { level: depth + 1, setSize, posInSet: i + 1 });\n }\n const children = node.children;\n if (node.expanded && children) {\n emit(node.shaped ? children : process(children, node, depth + 1), depth + 1);\n }\n }\n };\n\n const roots: HierarchyNode[] = new Array(rows.length);\n for (let i = 0; i < rows.length; i++) roots[i] = { row: rows[i] };\n emit(process(roots, null, 0), 0);\n\n const positionOf = (row: unknown) => (typeof row === 'object' && row !== null ? meta.get(row) : undefined);\n let shaped = false;\n for (const c of active) if (c.endHierarchy(out, positionOf)) shaped = true;\n if (!shaped) {\n for (const row of out) if (typeof row === 'object' && row !== null) meta.delete(row);\n }\n return out;\n}\n","/**\n * Hierarchy Plugin\n *\n * Owns the hierarchical row model shared by structure-contributing plugins.\n */\n\nimport type { PluginManifest } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { RowElementInternal } from '../../core/types';\nimport { buildHierarchy } from './hierarchy';\nimport type { HierarchyContributor, HierarchyRowMeta } from './types';\n\n/** Attribute write that skips the DOM mutation when the value is unchanged (per-frame hot path). */\nfunction setAttrIfChanged(el: Element, name: string, value: number): void {\n const next = String(value);\n if (el.getAttribute(name) !== next) el.setAttribute(name, next);\n}\n\nfunction isContributor(plugin: unknown): plugin is HierarchyContributor & BaseGridPlugin {\n return typeof (plugin as Partial<HierarchyContributor>).processSiblings === 'function';\n}\n\nfunction processRowsPriority(plugin: BaseGridPlugin): number {\n return (plugin.constructor as typeof BaseGridPlugin).manifest?.hookPriority?.processRows ?? 0;\n}\n\n/**\n * Hierarchy Plugin for tbw-grid\n *\n * Builds one hierarchical row model from every attached structure contributor\n * — {@link https://toolboxjs.com/grid/plugins/tree/ | TreePlugin} discovers\n * child rows, {@link https://toolboxjs.com/grid/plugins/grouping-rows/ | GroupingRowsPlugin}\n * groups sibling lists — and announces each row's position with the WAI-ARIA\n * treegrid pattern (`aria-level` / `aria-setsize` / `aria-posinset`).\n *\n * Contributors know nothing about each other, so they compose: a grid with\n * both Tree and GroupingRows groups the children of every tree level.\n *\n * You never need to add this plugin yourself — TreePlugin and\n * GroupingRowsPlugin declare it as a dependency and the grid attaches it\n * automatically. It has no configuration.\n *\n * @example Group the children of every tree node\n * ```ts\n * import { TreePlugin } from '@toolbox-web/grid/plugins/tree';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * grid.gridConfig = {\n * plugins: [\n * new TreePlugin({ childrenField: 'children' }),\n * new GroupingRowsPlugin({\n * // Leave the root level ungrouped, group every child level by kind\n * groupOn: (row, { depth }) => (depth > 0 ? row.kind : null),\n * }),\n * ],\n * };\n * ```\n *\n * @since 3.9.0\n */\nexport class HierarchyPlugin extends BaseGridPlugin {\n /** @internal */\n static override readonly manifest: PluginManifest = {\n modifiesRowStructure: true,\n };\n\n /** @internal */\n readonly name = 'hierarchy';\n\n #meta = new WeakMap<object, HierarchyRowMeta>();\n\n /** @internal */\n override detach(): void {\n // Restore the default `role=\"grid\"` (template default in `core/internal/dom-builder.ts`).\n this.gridElement?.querySelector('.rows-body')?.setAttribute('role', 'grid');\n this.#meta = new WeakMap();\n }\n\n /**\n * Build the row model for a contributor's `processRows` hook.\n *\n * Every contributor forwards its hook here; only the one that runs last in\n * the `processRows` pipeline builds the model (for all contributors at\n * once) — the earlier ones pass their rows through untouched.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * return this.grid.getPluginByName('hierarchy')?.process(rows, this) ?? [...rows];\n * }\n * ```\n *\n * @since 3.9.0\n */\n process(rows: readonly unknown[], caller: HierarchyContributor): unknown[] {\n const contributors = this.#contributors();\n if (!contributors.includes(caller as HierarchyContributor & BaseGridPlugin)) {\n this.#meta = new WeakMap();\n return buildHierarchy(rows, [caller], this.#meta);\n }\n if (contributors[contributors.length - 1] !== caller) return rows as unknown[];\n this.#meta = new WeakMap();\n return buildHierarchy(rows, contributors, this.#meta);\n }\n\n /**\n * Hierarchy position of a row in the current row model, or `undefined` when\n * the row is not rendered (e.g. collapsed under a parent).\n *\n * @example\n * ```ts\n * const meta = grid.getPluginByName('hierarchy')?.getRowMeta(grid.rows[0]);\n * console.log(meta?.level, meta?.posInSet, meta?.setSize);\n * ```\n *\n * @since 3.9.0\n */\n getRowMeta(row: unknown): HierarchyRowMeta | undefined {\n return typeof row === 'object' && row !== null ? this.#meta.get(row) : undefined;\n }\n\n /** @internal */\n override afterRender(): void {\n this.#applyAria();\n }\n\n /** @internal */\n override onScrollRender(): void {\n this.#applyAria();\n }\n\n /** Attached contributors, in `processRows` hook order. */\n #contributors(): Array<HierarchyContributor & BaseGridPlugin> {\n const plugins = (this.grid?._pluginManager?.getPlugins?.() ?? []) as readonly BaseGridPlugin[];\n return plugins.filter(isContributor).sort((a, b) => processRowsPriority(a) - processRowsPriority(b));\n }\n\n #applyAria(): void {\n if (this.#contributors().length === 0) return;\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n let hierarchical = false;\n for (const rowEl of body.querySelectorAll('.data-grid-row')) {\n const meta = this.getRowMeta((rowEl as RowElementInternal).__rowDataRef);\n if (!meta) {\n // Pooled row elements may carry a position from a previous row model.\n rowEl.removeAttribute('aria-level');\n rowEl.removeAttribute('aria-setsize');\n rowEl.removeAttribute('aria-posinset');\n continue;\n }\n hierarchical = true;\n setAttrIfChanged(rowEl, 'aria-level', meta.level);\n setAttrIfChanged(rowEl, 'aria-setsize', meta.setSize);\n setAttrIfChanged(rowEl, 'aria-posinset', meta.posInSet);\n }\n\n // `treegrid` only while the model has hierarchy, so per-row level/setsize/posinset are valid in context.\n const role = hierarchical ? 'treegrid' : 'grid';\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== role) rowsBody.setAttribute('role', role);\n }\n}\n","/**\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 { HierarchyNode } from '../hierarchy';\nimport type {\n DefaultExpandedValue,\n GroupDefinition,\n GroupingRowsConfig,\n GroupOnContext,\n GroupRowModelItem,\n RenderRow,\n} from './types';\n\n/** Context for the root row list (no parent). */\nconst ROOT_CONTEXT: GroupOnContext = { parent: null, depth: 0 };\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 /** Passed to `groupOn` as its second argument (default: the root list). */\n context?: GroupOnContext;\n /** Prepended to every top-level group key so nested lists get unique keys. */\n keyPrefix?: 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({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n context = ROOT_CONTEXT,\n keyPrefix = '',\n}: 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, context);\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__' ? keyPrefix + 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 grouped 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 * @param context - Passed to `groupOn` (default: the root list).\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: NonNullable<GroupingRowsConfig['groupOn']>,\n columnFields: string[],\n context: GroupOnContext = ROOT_CONTEXT,\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n let sampleRow: any;\n let path: any = null;\n for (const row of rows) {\n path = groupOn(row, context);\n if (path != null && path !== false) {\n sampleRow = row;\n break;\n }\n }\n if (sampleRow === undefined) 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 * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\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({\n kind: 'data',\n row: { __loading: true, __groupKey: group.key, __rowCacheKey: `__loading:group:${group.key}` },\n rowIndex: -1,\n });\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\n/** Every group key of a pre-defined group tree, depth-first. */\nexport function collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) keys.push(...collectGroupKeys(g.children));\n }\n return keys;\n}\n\n/** Find a group definition by key anywhere in a pre-defined group tree. */\nexport function findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n const found = g.children?.length ? findGroupDefinition(g.children, key) : undefined;\n if (found) return found;\n }\n return undefined;\n}\n// #endregion\n\n// #region ServerSide Root Nodes (#538)\n\n/** A ServerSide loading placeholder standing in for an unloaded root node (mirrors `isLoadingPlaceholder`). */\nexport function isRootPlaceholder(row: unknown): row is { __loading: true; __index: number } {\n const r = row as { __loading?: unknown; __index?: unknown } | null;\n return r != null && typeof r === 'object' && r.__loading === true && typeof r.__index === 'number';\n}\n\n/**\n * The definitions before the first gap or placeholder — the contiguous run from node 0.\n * A numeric `defaultExpanded` (a depth-first key index) resolves against the leading LOADED\n * definitions (`#nodeGroups`, never the filtered render slots), so blocks landing out of order\n * cannot shift it onto a later group.\n */\nexport function leadingGroups(slots: readonly unknown[]): GroupDefinition[] {\n const leading: GroupDefinition[] = [];\n for (const slot of slots) {\n if (slot == null || isRootPlaceholder(slot)) break;\n leading.push(slot as GroupDefinition);\n }\n return leading;\n}\n\n/**\n * Node-indexed root slots. ServerSide's root list (`input`) is authoritative: a placeholder sits\n * at its `__index`, and a definition renders only while ServerSide still lists that object — at\n * its node index in `nodeGroups` (sparse, by node index). So definitions whose block ServerSide\n * dropped (`purgeCache()`, an infinite-scroll estimate that shrank) or that a local filter\n * removed do not render. An empty `input` means no node count yet (a refetch in flight, or no\n * ServerSide input) → the loaded definitions. `groups` = the rendered definitions, in node order.\n */\nexport function buildRootSlots(nodeGroups: readonly (GroupDefinition | undefined)[], input: readonly HierarchyNode[]) {\n let slots: unknown[] = nodeGroups.slice();\n if (input.length > 0) {\n const nodeIndexOf = new Map<unknown, number>();\n nodeGroups.forEach((group, i) => nodeIndexOf.set(group, i));\n slots = [];\n for (const { row } of input) {\n const index = isRootPlaceholder(row) ? row.__index : nodeIndexOf.get(row);\n if (index !== undefined) slots[index] = row;\n }\n }\n const groups = slots.filter((slot): slot is GroupDefinition => slot != null && !isRootPlaceholder(slot));\n return { slots, groups };\n}\n\n/**\n * Root list in node order: `groupNodes[k]` (the node of the k-th definition in `slots`) or a\n * placeholder node. Records each root's node index (slot index) in `rootIndexOf`.\n */\nexport function interleaveRootNodes(\n slots: readonly unknown[],\n groupNodes: readonly HierarchyNode[],\n rootIndexOf: WeakMap<object, number>,\n): HierarchyNode[] {\n const roots: HierarchyNode[] = [];\n let next = 0;\n for (let i = 0; i < slots.length; i++) {\n const slot = slots[i];\n if (slot == null) continue;\n const node = isRootPlaceholder(slot) ? { row: slot } : groupNodes[next++];\n rootIndexOf.set(node.row as object, i);\n roots.push(node);\n }\n return roots;\n}\n\n/** Index-aligned with `rows`: node index of the root each rendered row belongs to, plus the root count. */\nexport function computeRootOwners(rows: readonly unknown[], rootIndexOf: WeakMap<object, number>) {\n const owners: number[] = new Array(rows.length);\n let owner = 0;\n let count = 0;\n for (let i = 0; i < rows.length; i++) {\n const rootIndex = rootIndexOf.get(rows[i] as object);\n if (rootIndex !== undefined) {\n owner = rootIndex;\n count++;\n }\n owners[i] = owner;\n }\n return { owners, count };\n}\n\n/** Translate a rendered row range to the root nodes that own it (see {@link computeRootOwners}). */\nexport function mapViewportToNodes(\n { owners, count }: { owners: readonly number[]; count: number },\n viewportStart: number,\n viewportEnd: number,\n): { startNode: number; endNode: number; totalLoadedNodes: number } {\n const last = owners.length - 1;\n const ownerAt = (index: number) => owners[Math.max(0, Math.min(index, last))] ?? 0;\n return { startNode: ownerAt(viewportStart), endNode: ownerAt(viewportEnd) + 1, totalLoadedNodes: count };\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\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 { renderLoadingPlaceholderRow } from '../shared/loading-placeholder';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\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 type { HierarchyContributor, HierarchyNode, HierarchySiblingContext } from '../hierarchy';\nimport { buildHierarchy, HierarchyPlugin } from '../hierarchy';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n DataSourceErrorDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n buildRootSlots,\n collapseAllGroups,\n collectGroupKeys,\n computeRootOwners,\n expandAllGroups,\n findGroupDefinition,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n interleaveRootNodes,\n leadingGroups,\n mapViewportToNodes,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupLoadErrorDetail,\n GroupOnContext,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\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 * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> implements HierarchyContributor {\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 processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\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: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n {\n type: 'group-load-error',\n description:\n \"Emitted when a pre-defined group's ServerSide getChildRows() fails. The group leaves its loading state.\",\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 type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\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 {\n name: 'hierarchy',\n required: true,\n reason: 'Builds the hierarchical row model',\n provide: () => new HierarchyPlugin(),\n },\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n readonly hierarchyStage = 'transform';\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 /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /**\n * ServerSide-fed definitions, sparse and indexed by node (`startNode + i` of each\n * `datasource:data` block). `preDefinedGroups` is its compacted form. Merged per\n * block within one ServerSide cache generation, reset when the generation changes (#538).\n */\n #nodeGroups: (GroupDefinition | undefined)[] = [];\n /** Definitions come from generation-stamped ServerSide blocks — unloaded root nodes render as its placeholders. */\n #serverFed = false;\n /** `DataSourceDataDetail.generation` of the claimed blocks (`undefined` = none, or a legacy emitter). */\n #serverGeneration: number | undefined;\n /**\n * Every group key a ServerSide block delivered, across generations — `defaultExpanded: true`\n * opens only keys not in here, so a group the user collapsed stays collapsed after a refetch.\n */\n #seenServerKeys = new Set<string>();\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /**\n * In-flight `datasource:fetch-children` requests (ServerSide active only),\n * keyed by group key. Each controller's signal is passed as\n * `FetchChildrenQuery.signal`, so aborting it cancels `getChildRows()` and\n * drops its result. Aborted when the group definitions are replaced (`setGroups()`,\n * or a new ServerSide cache generation — sort/filter, `refresh()`, `purgeCache()` —\n * which then re-requests every still-expanded group whose rows were loaded or\n * loading; an appended ServerSide block keeps them), on `setGroupRows`/`clearGroupRows`,\n * and on detach. Collapsing keeps the\n * load: its rows are cached for the next expand, and re-expanding while it is\n * in flight does not re-request.\n */\n #groupLoads = new Map<string, AbortController>();\n /**\n * Group keys already requested in the current definitions generation (any\n * request path). An expanded group whose rows were never requested —\n * `defaultExpanded`, `expandAll()`, `expand()` — gets ONE automatic request\n * after the render that shows it (#534); a failing group is therefore not\n * re-requested on every render. A manual collapsed → expanded transition\n * (`toggle()`, `expand()`, `expandAll()`) is a fresh attempt, so collapse +\n * re-expand still retries. Cleared when the definitions are replaced (`setGroups()`,\n * a new ServerSide cache generation, detach) — not when a later ServerSide block is appended.\n */\n #requestedKeys = new Set<string>();\n #groupRequestsQueued = 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\n // Per-rebuild state, set in `beginHierarchy`.\n #mode: 'groupOn' | 'predefined' = 'groupOn';\n #pending: DefaultExpandedValue | undefined;\n #pendingApplied = false;\n #groupSortDirections: Map<number, 1 | -1> | undefined;\n /** Set once a sibling list yielded the grouped column fields (nested-only grouping skips the root list). */\n #groupFieldsResolved = false;\n /** Render-model item of every row emitted by this rebuild, keyed by row object. */\n #renderItems = new WeakMap<object, RenderRow>();\n /** Node index of every root row (top-level group or ServerSide placeholder) emitted by this rebuild. */\n #rootIndexOf = new WeakMap<object, number>();\n /** Pre-defined mode: node index of the root each `flattenedRows` entry belongs to, and the root count. */\n #rootOwners: { owners: number[]; count: number } = { owners: [], count: 0 };\n #anyGroup = false;\n /** Top-level keys of nested (tree child) lists already seen, for `defaultExpanded: true`. */\n #seenNestedKeys = 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.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.#resetServerGroups();\n this.#rootOwners = { owners: [], count: 0 };\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n this.#seenNestedKeys.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 if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level node indices for ServerSide pagination.\n // Unloaded nodes are placeholder roots, so the mapping reaches past the loaded groups (#538).\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0 || this.#mode !== 'predefined') return undefined;\n return mapViewportToNodes(this.#rootOwners, viewportStart, viewportEnd) satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail<GroupDefinition>;\n if (d.claimed) return;\n d.claimed = true;\n const groups = d.rows;\n // Another block of the current dataset: merge it at its node offset and keep\n // loaded rows, in-flight loads and expansion (#538).\n if (this.#serverFed && d.generation !== undefined && d.generation === this.#serverGeneration) {\n this.#mergeServerGroups(groups, d.startNode);\n this.requestRender();\n return;\n }\n // A new dataset (first load, refresh, purge, server sort/filter): expanded groups\n // whose rows were loaded or loading are invalidated, so re-request them below.\n const reload = [...this.expandedKeys].filter((k) => this.groupRowsMap.has(k) || this.#groupLoads.has(k));\n this.#nodeGroups = [];\n // Only generation-stamped blocks are node-indexed; a legacy emitter's block is a compact replacement.\n this.#serverFed = d.generation !== undefined;\n this.#serverGeneration = d.generation;\n // The default resolves once: an applied default (or retained expansion) survives a refetch,\n // so collapsed groups stay collapsed; never-seen groups still open via `#seenServerKeys`.\n this.hasAppliedDefaultExpanded ||= this.expandedKeys.size > 0;\n this.#mergeServerGroups(groups, this.#serverFed ? d.startNode : 0);\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.clear();\n for (const key of reload) {\n const groupDef = findGroupDefinition(this.preDefinedGroups, key);\n if (groupDef) this.#requestGroupRows(key, groupDef, getGroupPath(this.preDefinedGroups, key));\n }\n this.requestRender();\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.#groupLoads.delete(groupKey);\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n\n // Listen for datasource:error — release the group's loading state;\n // collapsing and re-expanding retries the fetch.\n this.on('datasource:error', (detail: unknown) => {\n const d = detail as DataSourceErrorDetail;\n if (d.context?.source !== 'grouping-rows') return;\n const groupKey = d.context.groupKey as string | undefined;\n if (!groupKey || !this.#groupLoads.delete(groupKey)) return;\n this.loadingGroups.delete(groupKey);\n this.emit<GroupLoadErrorDetail>('group-load-error', {\n groupKey,\n groupPath: (d.context.groupPath as string[] | undefined) ?? [groupKey],\n error: d.error,\n });\n this.requestRender();\n });\n }\n\n /**\n * Abort a group's in-flight fetch (or every fetch when `groupKey` is omitted)\n * and release its loading state — an aborted request never settles.\n */\n #abortGroupLoads(groupKey?: string): void {\n for (const [key, controller] of this.#groupLoads) {\n if (groupKey != null && key !== groupKey) continue;\n controller.abort();\n this.#groupLoads.delete(key);\n this.loadingGroups.delete(key);\n }\n }\n\n /** Forget ServerSide-fed definitions (`setGroups()`, detach). */\n #resetServerGroups(): void {\n this.#nodeGroups = [];\n this.#serverFed = false;\n this.#serverGeneration = undefined;\n this.#seenServerKeys.clear();\n }\n\n /**\n * Place a block's definitions at their node indices and refresh the compacted `preDefinedGroups`.\n * Once the default has been applied (the render-time resolution runs once), `defaultExpanded: true`\n * opens the block's never-seen groups like the first block's.\n */\n #mergeServerGroups(groups: GroupDefinition[], startNode: number): void {\n const start = Number.isInteger(startNode) && startNode > 0 ? startNode : 0;\n for (let i = 0; i < groups.length; i++) this.#nodeGroups[start + i] = groups[i];\n this.preDefinedGroups = this.#nodeGroups.filter((group): group is GroupDefinition => group != null);\n const openNew = this.config.defaultExpanded === true && this.hasAppliedDefaultExpanded;\n for (const key of collectGroupKeys(groups)) {\n if (openNew && !this.#seenServerKeys.has(key)) this.expandedKeys.add(key);\n this.#seenServerKeys.add(key);\n }\n }\n\n /**\n * Show a group's loading state and request its rows via `datasource:fetch-children`,\n * unless a request for it is already in flight. The request is tracked (and made\n * cancellable) only when a ServerSide data source will answer it.\n */\n #requestGroupRows(key: string, group: GroupDefinition, groupPath: string[]): void {\n this.#requestedKeys.add(key);\n this.loadingGroups.add(key);\n if (this.#groupLoads.has(key)) return;\n const controller = this.queryBoolean('datasource:is-active') ? new AbortController() : undefined;\n if (controller) this.#groupLoads.set(key, controller);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group, groupPath },\n signal: controller?.signal,\n } satisfies FetchChildrenQuery);\n }\n\n /** An expanded pre-defined group whose rows were never loaded, loading, or requested this generation. */\n #needsGroupRows(item: RenderRow): item is GroupRowModelItem {\n return (\n item.kind === 'group' &&\n item.expanded &&\n !this.groupRowsMap.has(item.key) &&\n !this.loadingGroups.has(item.key) &&\n !this.#requestedKeys.has(item.key)\n );\n }\n\n /** Flush automatic group-row requests in a microtask — side effects stay out of the render pipeline (#534). */\n #queueGroupRowRequests(): void {\n if (this.#groupRequestsQueued) return;\n this.#groupRequestsQueued = true;\n queueMicrotask(() => {\n this.#groupRequestsQueued = false;\n this.#requestExpandedGroupRows();\n });\n }\n\n /**\n * Request the rows of every rendered, expanded leaf group that has none, however it\n * was expanded (`defaultExpanded`, `expandAll()`, `expand()`). ServerSide only —\n * imperative mode leaves loading to the consumer (`group-expand` / `setGroupRows`).\n * Expansion is rechecked against `expandedKeys`: a group (or ancestor) collapsed\n * after the render that queued this flush is skipped, and stays eligible.\n */\n #requestExpandedGroupRows(): void {\n if (!this.isActive || this.#mode !== 'predefined') return;\n const serverSideActive = this.queryBoolean('datasource:is-active');\n const groups = this.getActiveGroups();\n let requested = false;\n for (const item of this.flattenedRows) {\n if (!this.#needsGroupRows(item)) continue;\n const groupPath = getGroupPath(groups, item.key);\n if (!groupPath.every((key) => this.expandedKeys.has(key))) continue;\n const group = findGroupDefinition(groups, item.key);\n // A parent group renders its child groups, never rows.\n if (!serverSideActive || !group || group.children?.length) {\n this.#requestedKeys.add(item.key);\n continue;\n }\n this.#requestGroupRows(item.key, group, groupPath);\n requested = true;\n }\n if (requested) this.requestRender();\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[], context: GroupOnContext): 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, context);\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 sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, 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 );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n const hierarchy = this.grid?.getPluginByName?.('hierarchy');\n return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n }\n\n /** @internal Hierarchy contributor hook */\n beginHierarchy(_rows: readonly unknown[]): boolean {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n this.#pending = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.#pendingApplied = false;\n this.#anyGroup = false;\n this.#renderItems = new WeakMap();\n this.#rootIndexOf = new WeakMap();\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n this.#mode = 'predefined';\n return true;\n }\n\n if (typeof this.config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n this.groupedFields = [];\n return false;\n }\n this.#mode = 'groupOn';\n this.groupedFields = [];\n this.#groupSortDirections = undefined;\n this.#groupFieldsResolved = false;\n return true;\n }\n\n /** @internal Hierarchy contributor hook */\n processSiblings(nodes: HierarchyNode[], context: HierarchySiblingContext): HierarchyNode[] {\n const isRoot = context.parentKey === null && context.parent === null;\n if (this.#mode === 'predefined') return isRoot ? this.#preDefinedNodes(context.depth, nodes) : nodes;\n\n const config = this.config;\n const rows = nodes.map((node) => node.row);\n const groupOnContext = { parent: context.parent, depth: context.depth };\n if (!this.#groupFieldsResolved) {\n this.#groupSortDirections = this.resolveGroupSortDirections(rows, groupOnContext);\n this.#groupFieldsResolved = this.groupedFields.length > 0;\n }\n const build = (expanded: Set<string>, initialExpanded?: Set<string>) =>\n buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections: this.#groupSortDirections,\n context: groupOnContext,\n keyPrefix: isRoot ? '' : `${context.parentKey ?? ''}::`,\n });\n\n const grouped = isRoot ? this.#buildRootGroups(build) : this.#buildNestedGroups(build);\n if (grouped.length === 0) return nodes;\n\n const byRow = new Map<unknown, HierarchyNode>();\n for (const node of nodes) byRow.set(node.row, node);\n return this.#nest(grouped, context.depth, (row) => byRow.get(row) ?? { row }, getGroupRowCount);\n }\n\n /** @internal Hierarchy contributor hook */\n endHierarchy(rows: readonly unknown[]): boolean {\n if (!this.#anyGroup) {\n this.isActive = false;\n this.flattenedRows = [];\n return false;\n }\n this.isActive = true;\n this.flattenedRows = rows.map((row) => this.#renderItems.get(row as object) ?? { kind: 'data', row, rowIndex: -1 });\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (this.#pendingApplied) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n const predefined = this.#mode === 'predefined';\n if (predefined) this.#rootOwners = computeRootOwners(rows, this.#rootIndexOf);\n let needsGroupRows = false;\n this.flattenedRows.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 } else if (predefined && !needsGroupRows) {\n needsGroupRows = this.#needsGroupRows(item);\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n if (needsGroupRows) this.#queueGroupRowRequests();\n return true;\n }\n\n /** Group the root rows, resolving `defaultExpanded` / a pending expansion against the fresh group keys. */\n #buildRootGroups(build: (expanded: Set<string>, initialExpanded?: Set<string>) => RenderRow[]): RenderRow[] {\n const config = this.config;\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const initialBuild = build(new Set());\n if (initialBuild.length === 0) return initialBuild;\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n const pendingExpansion = this.#pending;\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n this.#pendingApplied = isPendingApply;\n\n // Build with proper expanded state\n return build(this.expandedKeys, initialExpanded);\n }\n\n /**\n * Group a tree node's children. A pending expansion (`setGroupOn(fn, expanded)`, deferred\n * `expandAll`/`collapseAll`) resolves against each list's own groups; otherwise\n * `defaultExpanded: true` opens each list's new top-level groups.\n */\n #buildNestedGroups(build: (expanded: Set<string>) => RenderRow[]): RenderRow[] {\n const grouped = build(this.expandedKeys);\n const pending = this.#pending;\n if (pending === undefined && this.config.defaultExpanded !== true) return grouped;\n const listKeys: string[] = [];\n for (const item of grouped) if (item.kind === 'group' && item.depth === 0) listKeys.push(item.key);\n const open =\n pending !== undefined\n ? resolveDefaultExpanded(pending, listKeys)\n : new Set(listKeys.filter((key) => !this.#seenNestedKeys.has(key)));\n if (pending !== undefined) this.#pendingApplied = true;\n let added = false;\n for (const key of listKeys) {\n this.#seenNestedKeys.add(key);\n if (open.has(key) && !this.expandedKeys.has(key)) {\n this.expandedKeys.add(key);\n added = true;\n }\n }\n return added ? build(this.expandedKeys) : grouped;\n }\n\n /**\n * Re-nest a flat group model (visible rows only) into hierarchy nodes,\n * creating the synthetic group-header row objects.\n */\n #nest(\n flat: RenderRow[],\n baseDepth: number,\n dataNode: (row: unknown) => HierarchyNode,\n rowCount: (item: GroupRowModelItem) => number,\n ): HierarchyNode[] {\n this.#anyGroup = true;\n const roots: HierarchyNode[] = [];\n const open: HierarchyNode[] = [];\n for (const item of flat) {\n if (item.kind === 'data') {\n const node = dataNode(item.row);\n (open.length > 0 ? (open[open.length - 1].children as HierarchyNode[]) : roots).push(node);\n if (typeof item.row === 'object' && item.row !== null) this.#renderItems.set(item.row, item);\n continue;\n }\n const groupItem: GroupRowModelItem = baseDepth === 0 ? item : { ...item, depth: baseDepth + item.depth };\n const row = {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: groupItem.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n this.#renderItems.set(row, groupItem);\n const node: HierarchyNode = { row, key: item.key, expanded: item.expanded, shaped: true, children: [] };\n open.length = item.depth;\n (item.depth > 0 ? (open[item.depth - 1].children as HierarchyNode[]) : roots).push(node);\n open.push(node);\n }\n return roots;\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 renderLoadingPlaceholderRow(this.grid, rowEl, row, rowIndex);\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 setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(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 // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!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 body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n const style = this.animationStyle;\n const animate = style !== false && this.keysToAnimate.size > 0;\n if (!animate) return;\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n\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 if (this.flattenedRows[idx]?.kind === 'data' && this.keysToAnimate.has(`data-${idx}`)) {\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 root nodes from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n *\n * ServerSide-fed (#538): the root list is node-indexed — a loaded definition, or the\n * ServerSide placeholder `input` holds for an unloaded node, so unloaded groups take up\n * scroll space and scrolling to them maps (`datasource:viewport-mapping`) onto their\n * block. `input` is authoritative: a definition ServerSide no longer lists (its block was\n * purged) does not render — see `buildRootSlots`.\n */\n #preDefinedNodes(baseDepth: number, input: readonly HierarchyNode[]): HierarchyNode[] {\n const active = this.getActiveGroups();\n const { slots, groups } = this.#serverFed\n ? buildRootSlots(this.#nodeGroups, input)\n : { slots: active, groups: active };\n if (slots.length === 0) return [];\n\n // Resolve defaultExpanded on first render only. A numeric index waits until every\n // definition before it has loaded (blocks may land out of order, #538). Server-fed, the\n // default resolves against the LOADED definitions, never the rendered `slots`/`groups`: a\n // local filter hides a definition without unloading it, and must not stall or shift a\n // numeric index, nor leave a hidden group out of `true` (the default resolves only once).\n const defaultExpanded = this.config.defaultExpanded ?? false;\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && defaultExpanded !== false) {\n // Not server-fed: `slots` === `groups`, unfiltered.\n const loaded = !this.#serverFed\n ? groups\n : typeof defaultExpanded === 'number'\n ? leadingGroups(this.#nodeGroups)\n : this.preDefinedGroups;\n const allKeys = collectGroupKeys(loaded);\n const initialExpanded = resolveDefaultExpanded(defaultExpanded, 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 // Prefer the server-reported row count over the loaded rows.\n const groupNodes = this.#nest(\n grouped,\n baseDepth,\n (row) => ({ row }),\n (item) => findGroupDefinition(groups, item.key)?.rowCount ?? item.rows.length,\n );\n return interleaveRootNodes(slots, groupNodes, this.#rootIndexOf);\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 // #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 * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n *\n * With `ServerSidePlugin` active, every newly expanded pre-defined group\n * requests its rows via `getChildRows()` after the next render (#534).\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n const expanded = expandAllGroups(this.flattenedRows);\n for (const key of expanded) if (!this.expandedKeys.has(key)) this.#requestedKeys.delete(key);\n this.expandedKeys = expanded;\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { 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 const accordion =\n typeof config.accordion === 'function' ? !!group && config.accordion(group.depth) : config.accordion;\n\n // In accordion mode, collapse sibling groups when expanding\n if (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 | 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.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\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 // Request child rows via unified DataSource if not already cached or in flight\n if (!this.groupRowsMap.has(key)) {\n const groupDef = findGroupDefinition(activeGroups, key);\n if (groupDef) this.#requestGroupRows(key, groupDef, groupPath);\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 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 * With `ServerSidePlugin` active, an expanded pre-defined group without rows requests\n * them via `getChildRows()` after the next render, like {@link toggle}.\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.#requestedKeys.delete(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 *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: GroupingRowsConfig['groupOn'], expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\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.#resetServerGroups();\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.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}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\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 * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — 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.#abortGroupLoads(groupKey);\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. Also cancels any in-flight ServerSide fetch for the\n * cleared groups, so a stale response cannot repopulate them.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n this.#abortGroupLoads(groupKey);\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"grouping-rows.umd.js","names":["DEFAULT_A11Y_MESSAGES","BaseGridPlugin","BaseGridPlugin","styles","aggregatorRegistry","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_groupingRows","TbwGrid","this","_toolbox_web_grid","Object","defineProperty","Symbol","toStringTag","value","GridClasses","announce","gridEl","message","effectiveConfig","a11y","announcements","el","querySelector","textContent","requestAnimationFrame","getA11yMessage","key","args","customFn","messages","DEFAULT_A11Y_MESSAGES","_F","split","map","s","reverse","join","DANGEROUS_TAGS","RegExp","slice","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","sanitizeToFragment","html","template","document","createElement","innerHTML","root","toRemove","elements","querySelectorAll","tagName","toLowerCase","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","forEach","removeAttribute","remove","sanitizeNode","content","setSanitizedHTML","indexOf","replaceChildren","createLoadingContent","size","renderer","result","wrapper","spinner","className","setAttribute","createDefaultSpinner","removeCellLoadingOverlay","cellEl","adapter","firstElementChild","overlay","unmount","removeRowLoadingOverlay","rowEl","renderLoadingPlaceholderRow","grid","row","rowIndex","style","getPropertyValue","removeProperty","pluginHeight","_getPluginRowHeight","height","syncPlaceholderHeight","__rowDataRef","__isCustomRow","classList","contains","__frameworkAdapter","releaseCell","beginBatch","children","i","length","cell","endBatch","clearRowForRebuild","gridColumn","appendChild","loading","add","setRowLoadingState","loadingRenderer","buildHierarchy","rows","contributors","meta","WeakMap","active","stage","c","hierarchyStage","beginHierarchy","out","process","nodes","parent","depth","context","parentKey","processSiblings","emit","setSize","node","set","level","posInSet","expanded","shaped","roots","positionOf","get","endHierarchy","delete","setAttrIfChanged","next","String","getAttribute","isContributor","plugin","processRowsPriority","constructor","manifest","hookPriority","processRows","HierarchyPlugin","BaseGridPlugin","static","modifiesRowStructure","detach","gridElement","caller","includes","getRowMeta","afterRender","applyAria","onScrollRender","_pluginManager","getPlugins","filter","sort","a","b","body","hierarchical","role","rowsBody","ROOT_CONTEXT","resolveDefaultExpanded","allGroupKeys","isArray","getGroupRowCount","groupRow","kind","buildPreDefinedGroupModel","groups","groupRows","loadingGroups","parentPath","flat","group","currentPath","isExpanded","isLoading","childRows","__loading","__groupKey","__rowCacheKey","idx","getGroupPath","targetKey","found","collectGroupKeys","keys","g","findGroupDefinition","isRootPlaceholder","r","__index","GroupingRowsPlugin","onHeaderClick","incompatibleWith","reason","events","type","description","queries","configRules","id","severity","check","config","accordion","defaultExpanded","required","provide","styles","defaultConfig","showRowCount","indentWidth","aggregators","animation","expandedKeys","flattenedRows","isActive","previousVisibleKeys","keysToAnimate","hasAppliedDefaultExpanded","pendingExpansion","groupConfigDirty","preDefinedGroups","nodeGroups","serverFed","serverGeneration","seenServerKeys","groupRowsMap","Map","groupLoads","requestedKeys","groupRequestsQueued","groupedFields","userGroupSortDirections","mode","pending","pendingApplied","groupSortDirections","groupFieldsResolved","renderItems","rootIndexOf","rootOwners","owners","count","anyGroup","seenNestedKeys","animationStyle","isAnimationEnabled","clear","resetServerGroups","abortGroupLoads","getRowHeight","_index","groupRowHeight","__isGroupRow","handleQuery","query","viewportStart","viewportEnd","last","ownerAt","index","Math","max","min","startNode","endNode","totalLoadedNodes","mapViewportToNodes","attach","super","on","detail","d","claimed","generation","mergeServerGroups","requestRender","reload","k","groupDef","requestGroupRows","source","groupKey","groupPath","error","controller","abort","start","Number","isInteger","openNew","queryBoolean","AbortController","signal","needsGroupRows","item","queueGroupRowRequests","queueMicrotask","requestExpandedGroupRows","serverSideActive","getActiveGroups","requested","every","event","fieldIndex","field","column","sortable","targetDepth","current","resolveGroupSortDirections","groupOn","columnFields","columns","depthToField","sampleRow","path","groupValue","resolveGroupFields","values","directions","activeSorts","multiSortResults","sortModel","entry","direction","sortState","_sortState","dir","detect","enableRowGrouping","hierarchy","getPluginByName","_rows","isRoot","preDefinedNodes","groupOnContext","build","initialExpanded","keyPrefix","rawVal","depthIdx","seg","composite","rowIndexMap","effectiveExpanded","sortedChildren","childDepth","av","bv","visit","buildGroupedRowModel","grouped","buildRootGroups","buildNestedGroups","byRow","nest","broadcast","currentVisibleKeys","predefined","owner","rootIndex","computeRootOwners","initialBuild","expansionConfig","isPendingApply","listKeys","open","added","baseDepth","dataNode","rowCount","groupItem","__groupValue","__groupDepth","__groupRows","__groupExpanded","__groupRowCount","onCellClick","originalEvent","target","closest","toggle","onKeyDown","composedPath","HTMLElement","tag","isContentEditable","isFormFieldKeyTarget","focusRow","_focusRow","preventDefault","requestRenderWithFocus","renderRow","groupRowRenderer","toggleExpand","handleToggle","setProperty","fullWidth","renderFullWidthGroupRow","renderPerColumnGroupRow","animClass","parseInt","addEventListener","once","input","slots","nodeIndexOf","slot","buildRootSlots","loaded","leading","leadingGroups","groupNodes","interleaveRootNodes","createToggleButton","btn","setIcon","e","stopPropagation","getGroupLabelText","formatLabel","label","aggregatorEntries","entries","aggregatesContainer","aggRef","col","find","aggregatorRegistry","run","aggSpan","header","gridTemplate","gridTemplateColumns","display","toggleRendered","colIdx","firstColAgg","aggResult","expandAll","expandAllGroups","collapseAll","isExpanding","newKeys","existingKey","startsWith","existingGroup","newSet","toggleGroupExpansion","activeGroups","groupName","expand","collapse","getGroupState","expandedCount","totalGroups","getRowCount","refreshGroups","getExpandedGroups","getFlattenedRows","isGroupingActive","setGroupOn","fn","setGroups","getGroups","setGroupRows","setGroupLoading","clearGroupRows"],"sources":["../../../../../libs/grid/src/lib/core/constants.ts","../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../libs/grid/src/lib/plugins/shared/form-field-target.ts","../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../libs/grid/src/lib/core/internal/row-teardown.ts","../../../../../libs/grid/src/lib/plugins/shared/loading-placeholder.ts","../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy.ts","../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy-plugin.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","/**\n * True when a keyboard event originates in a text-entry control — `<input>`, `<select>`,\n * `<textarea>` or a contenteditable element. Plugin key handlers (e.g. Space to toggle a tree\n * row) must defer to the control so the keystroke is typed rather than swallowed.\n *\n * Reads `composedPath()[0]` so controls inside an editor's open shadow root are recognised;\n * `event.target` is retargeted to the shadow host by the time the grid sees the event.\n * @internal\n */\nexport function isFormFieldKeyTarget(event: Event): boolean {\n const path = event.composedPath?.() ?? [];\n const target = path.length ? path[0] : event.target;\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || target.isContentEditable;\n}\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { FrameworkAdapter, GridConfig, LoadingContext, RowElementInternal } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/** Remove a cell's owned loading overlay and release adapter-managed content. */\nexport function removeCellLoadingOverlay(cellEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!cellEl.firstElementChild) return;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Move a cell's owned loading overlay in front of the cell while its content is rewritten,\n * so the still-active loader survives adapter release and content replacement. The caller\n * appends it back. The overlay stays connected because React prunes detached portal containers.\n */\nexport function parkCellLoadingOverlay(cellEl: HTMLElement): HTMLElement | null {\n if (!cellEl.firstElementChild) return null;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return null;\n const parent = cellEl.parentElement;\n if (parent) parent.insertBefore(overlay, cellEl);\n else overlay.remove();\n return overlay;\n}\n\n/** Remove a row's owned loading overlay and release adapter-managed content. */\nexport function removeRowLoadingOverlay(rowEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!rowEl.firstElementChild) return;\n const overlay = rowEl.querySelector<HTMLElement>(':scope > .tbw-row-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * in place of the default spinner. Only invoked when the overlay is created.\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setRowLoadingState(\n rowEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present.\n // Direct children only — a nested grid's row overlay must not be mistaken for this row's.\n if (!rowEl.querySelector(':scope > .tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n if (renderer) {\n overlay.appendChild(createLoadingContent('small', renderer));\n } else {\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n }\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n removeRowLoadingOverlay(rowEl, adapter);\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setCellLoadingState(\n cellEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n\n // Breadcrumb so the render hot path can skip the per-cell scan on the\n // overwhelming majority of rows. Conservative: only ever set here, so it can\n // read stale-true after the last loading cell clears (costs one extra scan).\n const rowEl = cellEl.parentElement as RowElementInternal | null;\n if (rowEl) rowEl.__hasCellLoading = true;\n\n if (renderer) {\n cellEl.classList.add('tbw-cell-loading-custom');\n if (!cellEl.querySelector(':scope > .tbw-cell-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-cell-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n overlay.appendChild(createLoadingContent('small', renderer));\n cellEl.appendChild(overlay);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading-custom');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.classList.remove('tbw-cell-loading-custom');\n cellEl.removeAttribute('aria-busy');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n}\n","/**\n * Row teardown shared by the core render path and plugin-rendered rows\n * (loading placeholders), so framework-adapter cleanup has one implementation.\n */\nimport type { GridHost } from '../types';\nimport { removeCellLoadingOverlay, removeRowLoadingOverlay } from './loading';\n\n/**\n * Release a pooled row's adapter-owned content — its row loader, cell loaders, then\n * cell views — before the row is discarded. Callers own the `beginBatch`/`endBatch`\n * bracket so a multi-row teardown collapses into one deferred commit (#330).\n */\nexport function releasePooledRow(adapter: GridHost['__frameworkAdapter'], rowEl: HTMLElement): void {\n if (!adapter?.releaseCell && !adapter?.unmount) return;\n removeRowLoadingOverlay(rowEl, adapter);\n const cells = rowEl.children;\n for (let c = 0; c < cells.length; c++) {\n const cell = cells[c] as HTMLElement;\n removeCellLoadingOverlay(cell, adapter);\n if (cell.firstElementChild) adapter.releaseCell?.(cell);\n }\n}\n\n/**\n * Wipe a pooled row's cells ahead of a full rebuild.\n *\n * Framework views are released BEFORE the DOM is wiped — without this,\n * Angular EmbeddedViewRefs / React roots / Vue apps created by renderers and\n * editor factories stay alive in the adapter's tracking arrays after their DOM\n * is destroyed, leaking memory on every recycle. Wrapped in `beginBatch`/`endBatch`\n * because `innerHTML = ''` detaches every cell as a group, letting adapters defer\n * per-cell sync commits to a single render once detachment completes (#330).\n */\nexport function clearRowForRebuild(grid: Pick<GridHost, '__frameworkAdapter'> & HTMLElement, rowEl: HTMLElement): void {\n // Clear loading state before rebuild — grid re-applies it after render for\n // actually-loading rows. Prevents a stale tbw-row-loading class from\n // persisting when pool elements are recycled.\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n const adapter = grid.__frameworkAdapter;\n if (!adapter?.releaseCell && !adapter?.unmount) {\n rowEl.innerHTML = '';\n return;\n }\n adapter.beginBatch?.(grid);\n try {\n removeRowLoadingOverlay(rowEl, adapter);\n const children = rowEl.children;\n for (let i = children.length - 1; i >= 0; i--) {\n const cell = children[i] as HTMLElement;\n removeCellLoadingOverlay(cell, adapter);\n adapter.releaseCell?.(cell);\n }\n rowEl.innerHTML = '';\n } finally {\n adapter.endBatch?.(grid);\n }\n}\n","/**\n * Loading placeholder rows shared by plugins that synthesize them (ServerSide\n * blocks in flight, GroupingRows groups being fetched). Inlined into each\n * plugin bundle that imports it — core never calls it.\n */\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { clearRowForRebuild } from '../../core/internal/row-teardown';\nimport type { GridHost, RowElementInternal } from '../../core/types';\n\n/**\n * Marker class on placeholder rows. Lets layout plugins (Responsive card mode)\n * tell a placeholder apart from a data row — `tbw-row-loading` is not enough,\n * because `grid.setRowLoading()` puts that on real rows too.\n */\nexport const PLACEHOLDER_ROW_CLASS = 'placeholder-row';\n\n/**\n * Render a placeholder as one full-width cell in the core row-loading state, so\n * no column renderer runs for a row that holds no data.\n *\n * Call from a plugin `renderRow` hook and return `true`. The previous cells are\n * released through the framework adapter first (a pooled element may have held\n * React portals / Vue teleports / Angular views for a loaded row). The row is\n * flagged `__isCustomRow`, which forces a full rebuild once real data replaces\n * the placeholder; that rebuild also clears the loading state.\n *\n * Height: the row occupies exactly what virtualization reserves for it — the\n * height a plugin reports through `getRowHeight()` (e.g. a Responsive card), else\n * the default row height. Any inline height a recycled element carried (a\n * Responsive card height, a per-row `--tbw-row-height`) is dropped.\n */\nexport function renderLoadingPlaceholderRow(grid: GridHost, rowEl: HTMLElement, row: unknown, rowIndex: number): void {\n const el = rowEl as RowElementInternal;\n syncPlaceholderHeight(grid, el, row, rowIndex);\n // Same placeholder, still intact: nothing else to do (renders run every scroll frame).\n if (el.__rowDataRef === row && el.__isCustomRow && el.classList.contains('tbw-row-loading')) return;\n\n clearRowForRebuild(grid, el);\n el.className = `data-grid-row ${PLACEHOLDER_ROW_CLASS}`;\n el.__isCustomRow = true;\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'; // keeps the row at its normal height\n el.appendChild(cell);\n setRowLoadingState(el, true, grid.effectiveConfig?.loadingRenderer);\n}\n\n/** Match the row's DOM height to the slot virtualization reserves for it. Writes only on change. */\nfunction syncPlaceholderHeight(grid: GridHost, el: HTMLElement, row: unknown, rowIndex: number): void {\n const { style } = el;\n if (style.getPropertyValue('--tbw-row-height')) style.removeProperty('--tbw-row-height');\n const pluginHeight = grid._getPluginRowHeight?.(row, rowIndex);\n const height = pluginHeight !== undefined && pluginHeight > 0 ? `${pluginHeight}px` : '';\n if (style.height !== height) style.height = height;\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Hierarchy Engine\n *\n * Pure row-model builder shared by the HierarchyPlugin and its contributors.\n */\n\nimport type { HierarchyContributor, HierarchyNode, HierarchyRowMeta } from './types';\n\n/**\n * Build the flat row list for `rows` from every contributor's sibling-list\n * transforms, recording each emitted row's ARIA position in `meta`.\n *\n * Contributors whose `beginHierarchy` returns `false` sit out the rebuild.\n * With no active contributor the rows pass through unchanged. When every\n * active contributor's `endHierarchy` returns `false`, no position is kept in `meta`.\n *\n * This is what `HierarchyPlugin.process()` runs for all attached contributors.\n * Call it directly to run a contributor without a grid — e.g. as the\n * `processRows` fallback when no HierarchyPlugin is attached, or in unit tests.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * const hierarchy = this.grid?.getPluginByName('hierarchy');\n * return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n * }\n * ```\n *\n * @param rows - The root rows.\n * @param contributors - Contributors to run, in `processRows` hook order.\n * @param meta - Receives each emitted row's position. Pass a fresh map per rebuild.\n * @returns The flattened rows, in render order.\n * @since 3.9.0\n */\nexport function buildHierarchy(\n rows: readonly unknown[],\n contributors: readonly HierarchyContributor[],\n meta: WeakMap<object, HierarchyRowMeta> = new WeakMap(),\n): unknown[] {\n const active: HierarchyContributor[] = [];\n for (const stage of ['structure', 'transform'] as const) {\n for (const c of contributors) {\n if (c.hierarchyStage === stage && c.beginHierarchy(rows)) active.push(c);\n }\n }\n if (active.length === 0) return [...rows];\n\n const out: unknown[] = [];\n const process = (nodes: HierarchyNode[], parent: HierarchyNode | null, depth: number): HierarchyNode[] => {\n const context = { parent: parent ? parent.row : null, parentKey: parent?.key ?? null, depth };\n for (const c of active) nodes = c.processSiblings(nodes, context);\n return nodes;\n };\n const emit = (nodes: HierarchyNode[], depth: number): void => {\n const setSize = nodes.length;\n for (let i = 0; i < setSize; i++) {\n const node = nodes[i];\n out.push(node.row);\n if (typeof node.row === 'object' && node.row !== null) {\n meta.set(node.row, { level: depth + 1, setSize, posInSet: i + 1 });\n }\n const children = node.children;\n if (node.expanded && children) {\n emit(node.shaped ? children : process(children, node, depth + 1), depth + 1);\n }\n }\n };\n\n const roots: HierarchyNode[] = new Array(rows.length);\n for (let i = 0; i < rows.length; i++) roots[i] = { row: rows[i] };\n emit(process(roots, null, 0), 0);\n\n const positionOf = (row: unknown) => (typeof row === 'object' && row !== null ? meta.get(row) : undefined);\n let shaped = false;\n for (const c of active) if (c.endHierarchy(out, positionOf)) shaped = true;\n if (!shaped) {\n for (const row of out) if (typeof row === 'object' && row !== null) meta.delete(row);\n }\n return out;\n}\n","/**\n * Hierarchy Plugin\n *\n * Owns the hierarchical row model shared by structure-contributing plugins.\n */\n\nimport type { PluginManifest } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { RowElementInternal } from '../../core/types';\nimport { buildHierarchy } from './hierarchy';\nimport type { HierarchyContributor, HierarchyRowMeta } from './types';\n\n/** Attribute write that skips the DOM mutation when the value is unchanged (per-frame hot path). */\nfunction setAttrIfChanged(el: Element, name: string, value: number): void {\n const next = String(value);\n if (el.getAttribute(name) !== next) el.setAttribute(name, next);\n}\n\nfunction isContributor(plugin: unknown): plugin is HierarchyContributor & BaseGridPlugin {\n return typeof (plugin as Partial<HierarchyContributor>).processSiblings === 'function';\n}\n\nfunction processRowsPriority(plugin: BaseGridPlugin): number {\n return (plugin.constructor as typeof BaseGridPlugin).manifest?.hookPriority?.processRows ?? 0;\n}\n\n/**\n * Hierarchy Plugin for tbw-grid\n *\n * Builds one hierarchical row model from every attached structure contributor\n * — {@link https://toolboxjs.com/grid/plugins/tree/ | TreePlugin} discovers\n * child rows, {@link https://toolboxjs.com/grid/plugins/grouping-rows/ | GroupingRowsPlugin}\n * groups sibling lists — and announces each row's position with the WAI-ARIA\n * treegrid pattern (`aria-level` / `aria-setsize` / `aria-posinset`).\n *\n * Contributors know nothing about each other, so they compose: a grid with\n * both Tree and GroupingRows groups the children of every tree level.\n *\n * You never need to add this plugin yourself — TreePlugin and\n * GroupingRowsPlugin declare it as a dependency and the grid attaches it\n * automatically. It has no configuration.\n *\n * @example Group the children of every tree node\n * ```ts\n * import { TreePlugin } from '@toolbox-web/grid/plugins/tree';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * grid.gridConfig = {\n * plugins: [\n * new TreePlugin({ childrenField: 'children' }),\n * new GroupingRowsPlugin({\n * // Leave the root level ungrouped, group every child level by kind\n * groupOn: (row, { depth }) => (depth > 0 ? row.kind : null),\n * }),\n * ],\n * };\n * ```\n *\n * @since 3.9.0\n */\nexport class HierarchyPlugin extends BaseGridPlugin {\n /** @internal */\n static override readonly manifest: PluginManifest = {\n modifiesRowStructure: true,\n };\n\n /** @internal */\n readonly name = 'hierarchy';\n\n #meta = new WeakMap<object, HierarchyRowMeta>();\n\n /** @internal */\n override detach(): void {\n // Restore the default `role=\"grid\"` (template default in `core/internal/dom-builder.ts`).\n this.gridElement?.querySelector('.rows-body')?.setAttribute('role', 'grid');\n this.#meta = new WeakMap();\n }\n\n /**\n * Build the row model for a contributor's `processRows` hook.\n *\n * Every contributor forwards its hook here; only the one that runs last in\n * the `processRows` pipeline builds the model (for all contributors at\n * once) — the earlier ones pass their rows through untouched.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * return this.grid.getPluginByName('hierarchy')?.process(rows, this) ?? [...rows];\n * }\n * ```\n *\n * @since 3.9.0\n */\n process(rows: readonly unknown[], caller: HierarchyContributor): unknown[] {\n const contributors = this.#contributors();\n if (!contributors.includes(caller as HierarchyContributor & BaseGridPlugin)) {\n this.#meta = new WeakMap();\n return buildHierarchy(rows, [caller], this.#meta);\n }\n if (contributors[contributors.length - 1] !== caller) return rows as unknown[];\n this.#meta = new WeakMap();\n return buildHierarchy(rows, contributors, this.#meta);\n }\n\n /**\n * Hierarchy position of a row in the current row model, or `undefined` when\n * the row is not rendered (e.g. collapsed under a parent).\n *\n * @example\n * ```ts\n * const meta = grid.getPluginByName('hierarchy')?.getRowMeta(grid.rows[0]);\n * console.log(meta?.level, meta?.posInSet, meta?.setSize);\n * ```\n *\n * @since 3.9.0\n */\n getRowMeta(row: unknown): HierarchyRowMeta | undefined {\n return typeof row === 'object' && row !== null ? this.#meta.get(row) : undefined;\n }\n\n /** @internal */\n override afterRender(): void {\n this.#applyAria();\n }\n\n /** @internal */\n override onScrollRender(): void {\n this.#applyAria();\n }\n\n /** Attached contributors, in `processRows` hook order. */\n #contributors(): Array<HierarchyContributor & BaseGridPlugin> {\n const plugins = (this.grid?._pluginManager?.getPlugins?.() ?? []) as readonly BaseGridPlugin[];\n return plugins.filter(isContributor).sort((a, b) => processRowsPriority(a) - processRowsPriority(b));\n }\n\n #applyAria(): void {\n if (this.#contributors().length === 0) return;\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n let hierarchical = false;\n for (const rowEl of body.querySelectorAll('.data-grid-row')) {\n const meta = this.getRowMeta((rowEl as RowElementInternal).__rowDataRef);\n if (!meta) {\n // Pooled row elements may carry a position from a previous row model.\n rowEl.removeAttribute('aria-level');\n rowEl.removeAttribute('aria-setsize');\n rowEl.removeAttribute('aria-posinset');\n continue;\n }\n hierarchical = true;\n setAttrIfChanged(rowEl, 'aria-level', meta.level);\n setAttrIfChanged(rowEl, 'aria-setsize', meta.setSize);\n setAttrIfChanged(rowEl, 'aria-posinset', meta.posInSet);\n }\n\n // `treegrid` only while the model has hierarchy, so per-row level/setsize/posinset are valid in context.\n const role = hierarchical ? 'treegrid' : 'grid';\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== role) rowsBody.setAttribute('role', role);\n }\n}\n","/**\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 { HierarchyNode } from '../hierarchy';\nimport type {\n DefaultExpandedValue,\n GroupDefinition,\n GroupingRowsConfig,\n GroupOnContext,\n GroupRowModelItem,\n RenderRow,\n} from './types';\n\n/** Context for the root row list (no parent). */\nconst ROOT_CONTEXT: GroupOnContext = { parent: null, depth: 0 };\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 /** Passed to `groupOn` as its second argument (default: the root list). */\n context?: GroupOnContext;\n /** Prepended to every top-level group key so nested lists get unique keys. */\n keyPrefix?: 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({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n context = ROOT_CONTEXT,\n keyPrefix = '',\n}: 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, context);\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__' ? keyPrefix + 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 grouped 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 * @param context - Passed to `groupOn` (default: the root list).\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: NonNullable<GroupingRowsConfig['groupOn']>,\n columnFields: string[],\n context: GroupOnContext = ROOT_CONTEXT,\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n let sampleRow: any;\n let path: any = null;\n for (const row of rows) {\n path = groupOn(row, context);\n if (path != null && path !== false) {\n sampleRow = row;\n break;\n }\n }\n if (sampleRow === undefined) 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 * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\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({\n kind: 'data',\n row: { __loading: true, __groupKey: group.key, __rowCacheKey: `__loading:group:${group.key}` },\n rowIndex: -1,\n });\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\n/** Every group key of a pre-defined group tree, depth-first. */\nexport function collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) keys.push(...collectGroupKeys(g.children));\n }\n return keys;\n}\n\n/** Find a group definition by key anywhere in a pre-defined group tree. */\nexport function findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n const found = g.children?.length ? findGroupDefinition(g.children, key) : undefined;\n if (found) return found;\n }\n return undefined;\n}\n// #endregion\n\n// #region ServerSide Root Nodes (#538)\n\n/** A ServerSide loading placeholder standing in for an unloaded root node (mirrors `isLoadingPlaceholder`). */\nexport function isRootPlaceholder(row: unknown): row is { __loading: true; __index: number } {\n const r = row as { __loading?: unknown; __index?: unknown } | null;\n return r != null && typeof r === 'object' && r.__loading === true && typeof r.__index === 'number';\n}\n\n/**\n * The definitions before the first gap or placeholder — the contiguous run from node 0.\n * A numeric `defaultExpanded` (a depth-first key index) resolves against the leading LOADED\n * definitions (`#nodeGroups`, never the filtered render slots), so blocks landing out of order\n * cannot shift it onto a later group.\n */\nexport function leadingGroups(slots: readonly unknown[]): GroupDefinition[] {\n const leading: GroupDefinition[] = [];\n for (const slot of slots) {\n if (slot == null || isRootPlaceholder(slot)) break;\n leading.push(slot as GroupDefinition);\n }\n return leading;\n}\n\n/**\n * Node-indexed root slots. ServerSide's root list (`input`) is authoritative: a placeholder sits\n * at its `__index`, and a definition renders only while ServerSide still lists that object — at\n * its node index in `nodeGroups` (sparse, by node index). So definitions whose block ServerSide\n * dropped (`purgeCache()`, an infinite-scroll estimate that shrank) or that a local filter\n * removed do not render. An empty `input` means no node count yet (a refetch in flight, or no\n * ServerSide input) → the loaded definitions. `groups` = the rendered definitions, in node order.\n */\nexport function buildRootSlots(nodeGroups: readonly (GroupDefinition | undefined)[], input: readonly HierarchyNode[]) {\n let slots: unknown[] = nodeGroups.slice();\n if (input.length > 0) {\n const nodeIndexOf = new Map<unknown, number>();\n nodeGroups.forEach((group, i) => nodeIndexOf.set(group, i));\n slots = [];\n for (const { row } of input) {\n const index = isRootPlaceholder(row) ? row.__index : nodeIndexOf.get(row);\n if (index !== undefined) slots[index] = row;\n }\n }\n const groups = slots.filter((slot): slot is GroupDefinition => slot != null && !isRootPlaceholder(slot));\n return { slots, groups };\n}\n\n/**\n * Root list in node order: `groupNodes[k]` (the node of the k-th definition in `slots`) or a\n * placeholder node. Records each root's node index (slot index) in `rootIndexOf`.\n */\nexport function interleaveRootNodes(\n slots: readonly unknown[],\n groupNodes: readonly HierarchyNode[],\n rootIndexOf: WeakMap<object, number>,\n): HierarchyNode[] {\n const roots: HierarchyNode[] = [];\n let next = 0;\n for (let i = 0; i < slots.length; i++) {\n const slot = slots[i];\n if (slot == null) continue;\n const node = isRootPlaceholder(slot) ? { row: slot } : groupNodes[next++];\n rootIndexOf.set(node.row as object, i);\n roots.push(node);\n }\n return roots;\n}\n\n/** Index-aligned with `rows`: node index of the root each rendered row belongs to, plus the root count. */\nexport function computeRootOwners(rows: readonly unknown[], rootIndexOf: WeakMap<object, number>) {\n const owners: number[] = new Array(rows.length);\n let owner = 0;\n let count = 0;\n for (let i = 0; i < rows.length; i++) {\n const rootIndex = rootIndexOf.get(rows[i] as object);\n if (rootIndex !== undefined) {\n owner = rootIndex;\n count++;\n }\n owners[i] = owner;\n }\n return { owners, count };\n}\n\n/** Translate a rendered row range to the root nodes that own it (see {@link computeRootOwners}). */\nexport function mapViewportToNodes(\n { owners, count }: { owners: readonly number[]; count: number },\n viewportStart: number,\n viewportEnd: number,\n): { startNode: number; endNode: number; totalLoadedNodes: number } {\n const last = owners.length - 1;\n const ownerAt = (index: number) => owners[Math.max(0, Math.min(index, last))] ?? 0;\n return { startNode: ownerAt(viewportStart), endNode: ownerAt(viewportEnd) + 1, totalLoadedNodes: count };\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\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 { isFormFieldKeyTarget } from '../shared/form-field-target';\nimport { renderLoadingPlaceholderRow } from '../shared/loading-placeholder';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\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 type { HierarchyContributor, HierarchyNode, HierarchySiblingContext } from '../hierarchy';\nimport { buildHierarchy, HierarchyPlugin } from '../hierarchy';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n DataSourceErrorDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n buildRootSlots,\n collapseAllGroups,\n collectGroupKeys,\n computeRootOwners,\n expandAllGroups,\n findGroupDefinition,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n interleaveRootNodes,\n leadingGroups,\n mapViewportToNodes,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupLoadErrorDetail,\n GroupOnContext,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\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 * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> implements HierarchyContributor {\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 processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\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: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n {\n type: 'group-load-error',\n description:\n \"Emitted when a pre-defined group's ServerSide getChildRows() fails. The group leaves its loading state.\",\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 type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\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 {\n name: 'hierarchy',\n required: true,\n reason: 'Builds the hierarchical row model',\n provide: () => new HierarchyPlugin(),\n },\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n readonly hierarchyStage = 'transform';\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 /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /**\n * ServerSide-fed definitions, sparse and indexed by node (`startNode + i` of each\n * `datasource:data` block). `preDefinedGroups` is its compacted form. Merged per\n * block within one ServerSide cache generation, reset when the generation changes (#538).\n */\n #nodeGroups: (GroupDefinition | undefined)[] = [];\n /** Definitions come from generation-stamped ServerSide blocks — unloaded root nodes render as its placeholders. */\n #serverFed = false;\n /** `DataSourceDataDetail.generation` of the claimed blocks (`undefined` = none, or a legacy emitter). */\n #serverGeneration: number | undefined;\n /**\n * Every group key a ServerSide block delivered, across generations — `defaultExpanded: true`\n * opens only keys not in here, so a group the user collapsed stays collapsed after a refetch.\n */\n #seenServerKeys = new Set<string>();\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /**\n * In-flight `datasource:fetch-children` requests (ServerSide active only),\n * keyed by group key. Each controller's signal is passed as\n * `FetchChildrenQuery.signal`, so aborting it cancels `getChildRows()` and\n * drops its result. Aborted when the group definitions are replaced (`setGroups()`,\n * or a new ServerSide cache generation — sort/filter, `refresh()`, `purgeCache()` —\n * which then re-requests every still-expanded group whose rows were loaded or\n * loading; an appended ServerSide block keeps them), on `setGroupRows`/`clearGroupRows`,\n * and on detach. Collapsing keeps the\n * load: its rows are cached for the next expand, and re-expanding while it is\n * in flight does not re-request.\n */\n #groupLoads = new Map<string, AbortController>();\n /**\n * Group keys already requested in the current definitions generation (any\n * request path). An expanded group whose rows were never requested —\n * `defaultExpanded`, `expandAll()`, `expand()` — gets ONE automatic request\n * after the render that shows it (#534); a failing group is therefore not\n * re-requested on every render. A manual collapsed → expanded transition\n * (`toggle()`, `expand()`, `expandAll()`) is a fresh attempt, so collapse +\n * re-expand still retries. Cleared when the definitions are replaced (`setGroups()`,\n * a new ServerSide cache generation, detach) — not when a later ServerSide block is appended.\n */\n #requestedKeys = new Set<string>();\n #groupRequestsQueued = 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\n // Per-rebuild state, set in `beginHierarchy`.\n #mode: 'groupOn' | 'predefined' = 'groupOn';\n #pending: DefaultExpandedValue | undefined;\n #pendingApplied = false;\n #groupSortDirections: Map<number, 1 | -1> | undefined;\n /** Set once a sibling list yielded the grouped column fields (nested-only grouping skips the root list). */\n #groupFieldsResolved = false;\n /** Render-model item of every row emitted by this rebuild, keyed by row object. */\n #renderItems = new WeakMap<object, RenderRow>();\n /** Node index of every root row (top-level group or ServerSide placeholder) emitted by this rebuild. */\n #rootIndexOf = new WeakMap<object, number>();\n /** Pre-defined mode: node index of the root each `flattenedRows` entry belongs to, and the root count. */\n #rootOwners: { owners: number[]; count: number } = { owners: [], count: 0 };\n #anyGroup = false;\n /** Top-level keys of nested (tree child) lists already seen, for `defaultExpanded: true`. */\n #seenNestedKeys = 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.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.#resetServerGroups();\n this.#rootOwners = { owners: [], count: 0 };\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n this.#seenNestedKeys.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 if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level node indices for ServerSide pagination.\n // Unloaded nodes are placeholder roots, so the mapping reaches past the loaded groups (#538).\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0 || this.#mode !== 'predefined') return undefined;\n return mapViewportToNodes(this.#rootOwners, viewportStart, viewportEnd) satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail<GroupDefinition>;\n if (d.claimed) return;\n d.claimed = true;\n const groups = d.rows;\n // Another block of the current dataset: merge it at its node offset and keep\n // loaded rows, in-flight loads and expansion (#538).\n if (this.#serverFed && d.generation !== undefined && d.generation === this.#serverGeneration) {\n this.#mergeServerGroups(groups, d.startNode);\n this.requestRender();\n return;\n }\n // A new dataset (first load, refresh, purge, server sort/filter): expanded groups\n // whose rows were loaded or loading are invalidated, so re-request them below.\n const reload = [...this.expandedKeys].filter((k) => this.groupRowsMap.has(k) || this.#groupLoads.has(k));\n this.#nodeGroups = [];\n // Only generation-stamped blocks are node-indexed; a legacy emitter's block is a compact replacement.\n this.#serverFed = d.generation !== undefined;\n this.#serverGeneration = d.generation;\n // The default resolves once: an applied default (or retained expansion) survives a refetch,\n // so collapsed groups stay collapsed; never-seen groups still open via `#seenServerKeys`.\n this.hasAppliedDefaultExpanded ||= this.expandedKeys.size > 0;\n this.#mergeServerGroups(groups, this.#serverFed ? d.startNode : 0);\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.clear();\n for (const key of reload) {\n const groupDef = findGroupDefinition(this.preDefinedGroups, key);\n if (groupDef) this.#requestGroupRows(key, groupDef, getGroupPath(this.preDefinedGroups, key));\n }\n this.requestRender();\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.#groupLoads.delete(groupKey);\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n\n // Listen for datasource:error — release the group's loading state;\n // collapsing and re-expanding retries the fetch.\n this.on('datasource:error', (detail: unknown) => {\n const d = detail as DataSourceErrorDetail;\n if (d.context?.source !== 'grouping-rows') return;\n const groupKey = d.context.groupKey as string | undefined;\n if (!groupKey || !this.#groupLoads.delete(groupKey)) return;\n this.loadingGroups.delete(groupKey);\n this.emit<GroupLoadErrorDetail>('group-load-error', {\n groupKey,\n groupPath: (d.context.groupPath as string[] | undefined) ?? [groupKey],\n error: d.error,\n });\n this.requestRender();\n });\n }\n\n /**\n * Abort a group's in-flight fetch (or every fetch when `groupKey` is omitted)\n * and release its loading state — an aborted request never settles.\n */\n #abortGroupLoads(groupKey?: string): void {\n for (const [key, controller] of this.#groupLoads) {\n if (groupKey != null && key !== groupKey) continue;\n controller.abort();\n this.#groupLoads.delete(key);\n this.loadingGroups.delete(key);\n }\n }\n\n /** Forget ServerSide-fed definitions (`setGroups()`, detach). */\n #resetServerGroups(): void {\n this.#nodeGroups = [];\n this.#serverFed = false;\n this.#serverGeneration = undefined;\n this.#seenServerKeys.clear();\n }\n\n /**\n * Place a block's definitions at their node indices and refresh the compacted `preDefinedGroups`.\n * Once the default has been applied (the render-time resolution runs once), `defaultExpanded: true`\n * opens the block's never-seen groups like the first block's.\n */\n #mergeServerGroups(groups: GroupDefinition[], startNode: number): void {\n const start = Number.isInteger(startNode) && startNode > 0 ? startNode : 0;\n for (let i = 0; i < groups.length; i++) this.#nodeGroups[start + i] = groups[i];\n this.preDefinedGroups = this.#nodeGroups.filter((group): group is GroupDefinition => group != null);\n const openNew = this.config.defaultExpanded === true && this.hasAppliedDefaultExpanded;\n for (const key of collectGroupKeys(groups)) {\n if (openNew && !this.#seenServerKeys.has(key)) this.expandedKeys.add(key);\n this.#seenServerKeys.add(key);\n }\n }\n\n /**\n * Show a group's loading state and request its rows via `datasource:fetch-children`,\n * unless a request for it is already in flight. The request is tracked (and made\n * cancellable) only when a ServerSide data source will answer it.\n */\n #requestGroupRows(key: string, group: GroupDefinition, groupPath: string[]): void {\n this.#requestedKeys.add(key);\n this.loadingGroups.add(key);\n if (this.#groupLoads.has(key)) return;\n const controller = this.queryBoolean('datasource:is-active') ? new AbortController() : undefined;\n if (controller) this.#groupLoads.set(key, controller);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group, groupPath },\n signal: controller?.signal,\n } satisfies FetchChildrenQuery);\n }\n\n /** An expanded pre-defined group whose rows were never loaded, loading, or requested this generation. */\n #needsGroupRows(item: RenderRow): item is GroupRowModelItem {\n return (\n item.kind === 'group' &&\n item.expanded &&\n !this.groupRowsMap.has(item.key) &&\n !this.loadingGroups.has(item.key) &&\n !this.#requestedKeys.has(item.key)\n );\n }\n\n /** Flush automatic group-row requests in a microtask — side effects stay out of the render pipeline (#534). */\n #queueGroupRowRequests(): void {\n if (this.#groupRequestsQueued) return;\n this.#groupRequestsQueued = true;\n queueMicrotask(() => {\n this.#groupRequestsQueued = false;\n this.#requestExpandedGroupRows();\n });\n }\n\n /**\n * Request the rows of every rendered, expanded leaf group that has none, however it\n * was expanded (`defaultExpanded`, `expandAll()`, `expand()`). ServerSide only —\n * imperative mode leaves loading to the consumer (`group-expand` / `setGroupRows`).\n * Expansion is rechecked against `expandedKeys`: a group (or ancestor) collapsed\n * after the render that queued this flush is skipped, and stays eligible.\n */\n #requestExpandedGroupRows(): void {\n if (!this.isActive || this.#mode !== 'predefined') return;\n const serverSideActive = this.queryBoolean('datasource:is-active');\n const groups = this.getActiveGroups();\n let requested = false;\n for (const item of this.flattenedRows) {\n if (!this.#needsGroupRows(item)) continue;\n const groupPath = getGroupPath(groups, item.key);\n if (!groupPath.every((key) => this.expandedKeys.has(key))) continue;\n const group = findGroupDefinition(groups, item.key);\n // A parent group renders its child groups, never rows.\n if (!serverSideActive || !group || group.children?.length) {\n this.#requestedKeys.add(item.key);\n continue;\n }\n this.#requestGroupRows(item.key, group, groupPath);\n requested = true;\n }\n if (requested) this.requestRender();\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[], context: GroupOnContext): 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, context);\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 sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, 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 );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n const hierarchy = this.grid?.getPluginByName?.('hierarchy');\n return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n }\n\n /** @internal Hierarchy contributor hook */\n beginHierarchy(_rows: readonly unknown[]): boolean {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n this.#pending = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.#pendingApplied = false;\n this.#anyGroup = false;\n this.#renderItems = new WeakMap();\n this.#rootIndexOf = new WeakMap();\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n this.#mode = 'predefined';\n return true;\n }\n\n if (typeof this.config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n this.groupedFields = [];\n return false;\n }\n this.#mode = 'groupOn';\n this.groupedFields = [];\n this.#groupSortDirections = undefined;\n this.#groupFieldsResolved = false;\n return true;\n }\n\n /** @internal Hierarchy contributor hook */\n processSiblings(nodes: HierarchyNode[], context: HierarchySiblingContext): HierarchyNode[] {\n const isRoot = context.parentKey === null && context.parent === null;\n if (this.#mode === 'predefined') return isRoot ? this.#preDefinedNodes(context.depth, nodes) : nodes;\n\n const config = this.config;\n const rows = nodes.map((node) => node.row);\n const groupOnContext = { parent: context.parent, depth: context.depth };\n if (!this.#groupFieldsResolved) {\n this.#groupSortDirections = this.resolveGroupSortDirections(rows, groupOnContext);\n this.#groupFieldsResolved = this.groupedFields.length > 0;\n }\n const build = (expanded: Set<string>, initialExpanded?: Set<string>) =>\n buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections: this.#groupSortDirections,\n context: groupOnContext,\n keyPrefix: isRoot ? '' : `${context.parentKey ?? ''}::`,\n });\n\n const grouped = isRoot ? this.#buildRootGroups(build) : this.#buildNestedGroups(build);\n if (grouped.length === 0) return nodes;\n\n const byRow = new Map<unknown, HierarchyNode>();\n for (const node of nodes) byRow.set(node.row, node);\n return this.#nest(grouped, context.depth, (row) => byRow.get(row) ?? { row }, getGroupRowCount);\n }\n\n /** @internal Hierarchy contributor hook */\n endHierarchy(rows: readonly unknown[]): boolean {\n if (!this.#anyGroup) {\n this.isActive = false;\n this.flattenedRows = [];\n return false;\n }\n this.isActive = true;\n this.flattenedRows = rows.map((row) => this.#renderItems.get(row as object) ?? { kind: 'data', row, rowIndex: -1 });\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (this.#pendingApplied) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n const predefined = this.#mode === 'predefined';\n if (predefined) this.#rootOwners = computeRootOwners(rows, this.#rootIndexOf);\n let needsGroupRows = false;\n this.flattenedRows.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 } else if (predefined && !needsGroupRows) {\n needsGroupRows = this.#needsGroupRows(item);\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n if (needsGroupRows) this.#queueGroupRowRequests();\n return true;\n }\n\n /** Group the root rows, resolving `defaultExpanded` / a pending expansion against the fresh group keys. */\n #buildRootGroups(build: (expanded: Set<string>, initialExpanded?: Set<string>) => RenderRow[]): RenderRow[] {\n const config = this.config;\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const initialBuild = build(new Set());\n if (initialBuild.length === 0) return initialBuild;\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n const pendingExpansion = this.#pending;\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n this.#pendingApplied = isPendingApply;\n\n // Build with proper expanded state\n return build(this.expandedKeys, initialExpanded);\n }\n\n /**\n * Group a tree node's children. A pending expansion (`setGroupOn(fn, expanded)`, deferred\n * `expandAll`/`collapseAll`) resolves against each list's own groups; otherwise\n * `defaultExpanded: true` opens each list's new top-level groups.\n */\n #buildNestedGroups(build: (expanded: Set<string>) => RenderRow[]): RenderRow[] {\n const grouped = build(this.expandedKeys);\n const pending = this.#pending;\n if (pending === undefined && this.config.defaultExpanded !== true) return grouped;\n const listKeys: string[] = [];\n for (const item of grouped) if (item.kind === 'group' && item.depth === 0) listKeys.push(item.key);\n const open =\n pending !== undefined\n ? resolveDefaultExpanded(pending, listKeys)\n : new Set(listKeys.filter((key) => !this.#seenNestedKeys.has(key)));\n if (pending !== undefined) this.#pendingApplied = true;\n let added = false;\n for (const key of listKeys) {\n this.#seenNestedKeys.add(key);\n if (open.has(key) && !this.expandedKeys.has(key)) {\n this.expandedKeys.add(key);\n added = true;\n }\n }\n return added ? build(this.expandedKeys) : grouped;\n }\n\n /**\n * Re-nest a flat group model (visible rows only) into hierarchy nodes,\n * creating the synthetic group-header row objects.\n */\n #nest(\n flat: RenderRow[],\n baseDepth: number,\n dataNode: (row: unknown) => HierarchyNode,\n rowCount: (item: GroupRowModelItem) => number,\n ): HierarchyNode[] {\n this.#anyGroup = true;\n const roots: HierarchyNode[] = [];\n const open: HierarchyNode[] = [];\n for (const item of flat) {\n if (item.kind === 'data') {\n const node = dataNode(item.row);\n (open.length > 0 ? (open[open.length - 1].children as HierarchyNode[]) : roots).push(node);\n if (typeof item.row === 'object' && item.row !== null) this.#renderItems.set(item.row, item);\n continue;\n }\n const groupItem: GroupRowModelItem = baseDepth === 0 ? item : { ...item, depth: baseDepth + item.depth };\n const row = {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: groupItem.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n this.#renderItems.set(row, groupItem);\n const node: HierarchyNode = { row, key: item.key, expanded: item.expanded, shaped: true, children: [] };\n open.length = item.depth;\n (item.depth > 0 ? (open[item.depth - 1].children as HierarchyNode[]) : roots).push(node);\n open.push(node);\n }\n return roots;\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 // Inputs (e.g. in a custom group renderer) own Space (#546).\n if (isFormFieldKeyTarget(event)) 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 renderLoadingPlaceholderRow(this.grid, rowEl, row, rowIndex);\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 setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(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 // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!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 body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n const style = this.animationStyle;\n const animate = style !== false && this.keysToAnimate.size > 0;\n if (!animate) return;\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n\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 if (this.flattenedRows[idx]?.kind === 'data' && this.keysToAnimate.has(`data-${idx}`)) {\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 root nodes from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n *\n * ServerSide-fed (#538): the root list is node-indexed — a loaded definition, or the\n * ServerSide placeholder `input` holds for an unloaded node, so unloaded groups take up\n * scroll space and scrolling to them maps (`datasource:viewport-mapping`) onto their\n * block. `input` is authoritative: a definition ServerSide no longer lists (its block was\n * purged) does not render — see `buildRootSlots`.\n */\n #preDefinedNodes(baseDepth: number, input: readonly HierarchyNode[]): HierarchyNode[] {\n const active = this.getActiveGroups();\n const { slots, groups } = this.#serverFed\n ? buildRootSlots(this.#nodeGroups, input)\n : { slots: active, groups: active };\n if (slots.length === 0) return [];\n\n // Resolve defaultExpanded on first render only. A numeric index waits until every\n // definition before it has loaded (blocks may land out of order, #538). Server-fed, the\n // default resolves against the LOADED definitions, never the rendered `slots`/`groups`: a\n // local filter hides a definition without unloading it, and must not stall or shift a\n // numeric index, nor leave a hidden group out of `true` (the default resolves only once).\n const defaultExpanded = this.config.defaultExpanded ?? false;\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && defaultExpanded !== false) {\n // Not server-fed: `slots` === `groups`, unfiltered.\n const loaded = !this.#serverFed\n ? groups\n : typeof defaultExpanded === 'number'\n ? leadingGroups(this.#nodeGroups)\n : this.preDefinedGroups;\n const allKeys = collectGroupKeys(loaded);\n const initialExpanded = resolveDefaultExpanded(defaultExpanded, 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 // Prefer the server-reported row count over the loaded rows.\n const groupNodes = this.#nest(\n grouped,\n baseDepth,\n (row) => ({ row }),\n (item) => findGroupDefinition(groups, item.key)?.rowCount ?? item.rows.length,\n );\n return interleaveRootNodes(slots, groupNodes, this.#rootIndexOf);\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 // #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 * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n *\n * With `ServerSidePlugin` active, every newly expanded pre-defined group\n * requests its rows via `getChildRows()` after the next render (#534).\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n const expanded = expandAllGroups(this.flattenedRows);\n for (const key of expanded) if (!this.expandedKeys.has(key)) this.#requestedKeys.delete(key);\n this.expandedKeys = expanded;\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { 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 const accordion =\n typeof config.accordion === 'function' ? !!group && config.accordion(group.depth) : config.accordion;\n\n // In accordion mode, collapse sibling groups when expanding\n if (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 | 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.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\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 // Request child rows via unified DataSource if not already cached or in flight\n if (!this.groupRowsMap.has(key)) {\n const groupDef = findGroupDefinition(activeGroups, key);\n if (groupDef) this.#requestGroupRows(key, groupDef, groupPath);\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 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 * With `ServerSidePlugin` active, an expanded pre-defined group without rows requests\n * them via `getChildRows()` after the next render, like {@link toggle}.\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.#requestedKeys.delete(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 *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: GroupingRowsConfig['groupOn'], expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\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.#resetServerGroups();\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.#abortGroupLoads();\n this.#requestedKeys.clear();\n this.loadingGroups.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}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\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 * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — 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.#abortGroupLoads(groupKey);\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. Also cancels any in-flight ServerSide fetch for the\n * cleared groups, so a stale response cannot repopulate them.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n this.#abortGroupLoads(groupKey);\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
|