@toolbox-web/grid 3.9.1 → 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.
Files changed (148) hide show
  1. package/all.js +1 -1
  2. package/all.js.map +1 -1
  3. package/custom-elements.json +2 -2
  4. package/index.js +1 -1
  5. package/index.js.map +1 -1
  6. package/lib/core/grid.d.ts +8 -4
  7. package/lib/core/plugin/base-plugin.d.ts +7 -0
  8. package/lib/core/types.d.ts +8 -3
  9. package/lib/features/clipboard.js.map +1 -1
  10. package/lib/features/column-virtualization.js.map +1 -1
  11. package/lib/features/context-menu.js.map +1 -1
  12. package/lib/features/editing.js.map +1 -1
  13. package/lib/features/export.js.map +1 -1
  14. package/lib/features/filtering.js.map +1 -1
  15. package/lib/features/grouping-columns.js.map +1 -1
  16. package/lib/features/grouping-rows.js.map +1 -1
  17. package/lib/features/master-detail.js.map +1 -1
  18. package/lib/features/multi-sort.js.map +1 -1
  19. package/lib/features/pinned-columns.js.map +1 -1
  20. package/lib/features/pinned-rows.js.map +1 -1
  21. package/lib/features/pivot.js.map +1 -1
  22. package/lib/features/print.js.map +1 -1
  23. package/lib/features/registry.js +1 -1
  24. package/lib/features/registry.js.map +1 -1
  25. package/lib/features/reorder-columns.js.map +1 -1
  26. package/lib/features/responsive.js.map +1 -1
  27. package/lib/features/row-drag-drop.js.map +1 -1
  28. package/lib/features/selection.js.map +1 -1
  29. package/lib/features/server-side.js.map +1 -1
  30. package/lib/features/shell.js.map +1 -1
  31. package/lib/features/sticky-rows.js.map +1 -1
  32. package/lib/features/tooltip.js.map +1 -1
  33. package/lib/features/tree.js.map +1 -1
  34. package/lib/features/undo-redo.js.map +1 -1
  35. package/lib/features/visibility.js.map +1 -1
  36. package/lib/plugins/clipboard/index.js.map +1 -1
  37. package/lib/plugins/column-virtualization/index.js.map +1 -1
  38. package/lib/plugins/context-menu/context-menu-plugin.d.ts +9 -4
  39. package/lib/plugins/context-menu/index.js +1 -1
  40. package/lib/plugins/context-menu/index.js.map +1 -1
  41. package/lib/plugins/context-menu/types.d.ts +8 -4
  42. package/lib/plugins/editing/editors.d.ts +15 -0
  43. package/lib/plugins/editing/index.js +1 -1
  44. package/lib/plugins/editing/index.js.map +1 -1
  45. package/lib/plugins/editing/internal/dirty-tracking-manager.d.ts +2 -1
  46. package/lib/plugins/editing/types.d.ts +5 -1
  47. package/lib/plugins/export/index.js +1 -1
  48. package/lib/plugins/export/index.js.map +1 -1
  49. package/lib/plugins/filtering/filter-model.d.ts +22 -2
  50. package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
  51. package/lib/plugins/filtering/filtering-plugin.d.ts +5 -2
  52. package/lib/plugins/filtering/index.d.ts +1 -1
  53. package/lib/plugins/filtering/index.js +1 -1
  54. package/lib/plugins/filtering/index.js.map +1 -1
  55. package/lib/plugins/filtering/types.d.ts +58 -3
  56. package/lib/plugins/grouping-columns/index.js.map +1 -1
  57. package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +7 -14
  58. package/lib/plugins/grouping-rows/grouping-rows.d.ts +48 -0
  59. package/lib/plugins/grouping-rows/index.d.ts +1 -1
  60. package/lib/plugins/grouping-rows/index.js +1 -1
  61. package/lib/plugins/grouping-rows/index.js.map +1 -1
  62. package/lib/plugins/grouping-rows/types.d.ts +21 -0
  63. package/lib/plugins/hierarchy/index.js.map +1 -1
  64. package/lib/plugins/master-detail/index.d.ts +1 -1
  65. package/lib/plugins/master-detail/index.js +1 -1
  66. package/lib/plugins/master-detail/index.js.map +1 -1
  67. package/lib/plugins/master-detail/master-detail-plugin.d.ts +28 -6
  68. package/lib/plugins/master-detail/master-detail.d.ts +0 -8
  69. package/lib/plugins/master-detail/types.d.ts +25 -1
  70. package/lib/plugins/multi-sort/index.js +1 -1
  71. package/lib/plugins/multi-sort/index.js.map +1 -1
  72. package/lib/plugins/pinned-columns/index.js.map +1 -1
  73. package/lib/plugins/pinned-rows/index.js.map +1 -1
  74. package/lib/plugins/pivot/index.js +1 -1
  75. package/lib/plugins/pivot/index.js.map +1 -1
  76. package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
  77. package/lib/plugins/pivot/types.d.ts +2 -0
  78. package/lib/plugins/print/index.js.map +1 -1
  79. package/lib/plugins/reorder-columns/index.js.map +1 -1
  80. package/lib/plugins/responsive/index.js.map +1 -1
  81. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  82. package/lib/plugins/selection/index.js +1 -1
  83. package/lib/plugins/selection/index.js.map +1 -1
  84. package/lib/plugins/selection/selection-plugin.d.ts +8 -1
  85. package/lib/plugins/server-side/datasource-types.d.ts +28 -0
  86. package/lib/plugins/server-side/datasource.d.ts +7 -0
  87. package/lib/plugins/server-side/index.js +1 -1
  88. package/lib/plugins/server-side/index.js.map +1 -1
  89. package/lib/plugins/server-side/server-side-plugin.d.ts +61 -8
  90. package/lib/plugins/server-side/types.d.ts +9 -1
  91. package/lib/plugins/shared/form-field-target.d.ts +10 -0
  92. package/lib/plugins/shared/format-value.d.ts +12 -0
  93. package/lib/plugins/shell/index.js.map +1 -1
  94. package/lib/plugins/sticky-rows/index.js.map +1 -1
  95. package/lib/plugins/tooltip/index.js.map +1 -1
  96. package/lib/plugins/tree/index.js +1 -1
  97. package/lib/plugins/tree/index.js.map +1 -1
  98. package/lib/plugins/tree/tree-plugin.d.ts +5 -11
  99. package/lib/plugins/tree/types.d.ts +22 -6
  100. package/lib/plugins/undo-redo/history.d.ts +2 -1
  101. package/lib/plugins/undo-redo/index.js +1 -1
  102. package/lib/plugins/undo-redo/index.js.map +1 -1
  103. package/lib/plugins/undo-redo/types.d.ts +13 -1
  104. package/lib/plugins/undo-redo/undo-redo-plugin.d.ts +15 -2
  105. package/lib/plugins/visibility/index.js.map +1 -1
  106. package/package.json +1 -1
  107. package/umd/grid.all.umd.js +1 -1
  108. package/umd/grid.all.umd.js.map +1 -1
  109. package/umd/grid.umd.js +1 -1
  110. package/umd/grid.umd.js.map +1 -1
  111. package/umd/plugins/clipboard.umd.js.map +1 -1
  112. package/umd/plugins/column-virtualization.umd.js.map +1 -1
  113. package/umd/plugins/context-menu.umd.js +1 -1
  114. package/umd/plugins/context-menu.umd.js.map +1 -1
  115. package/umd/plugins/editing.umd.js +1 -1
  116. package/umd/plugins/editing.umd.js.map +1 -1
  117. package/umd/plugins/export.umd.js +1 -1
  118. package/umd/plugins/export.umd.js.map +1 -1
  119. package/umd/plugins/filtering.umd.js +1 -1
  120. package/umd/plugins/filtering.umd.js.map +1 -1
  121. package/umd/plugins/grouping-columns.umd.js.map +1 -1
  122. package/umd/plugins/grouping-rows.umd.js +1 -1
  123. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  124. package/umd/plugins/hierarchy.umd.js.map +1 -1
  125. package/umd/plugins/master-detail.umd.js +1 -1
  126. package/umd/plugins/master-detail.umd.js.map +1 -1
  127. package/umd/plugins/multi-sort.umd.js +1 -1
  128. package/umd/plugins/multi-sort.umd.js.map +1 -1
  129. package/umd/plugins/pinned-columns.umd.js.map +1 -1
  130. package/umd/plugins/pinned-rows.umd.js.map +1 -1
  131. package/umd/plugins/pivot.umd.js +1 -1
  132. package/umd/plugins/pivot.umd.js.map +1 -1
  133. package/umd/plugins/print.umd.js.map +1 -1
  134. package/umd/plugins/reorder-columns.umd.js.map +1 -1
  135. package/umd/plugins/responsive.umd.js.map +1 -1
  136. package/umd/plugins/row-drag-drop.umd.js.map +1 -1
  137. package/umd/plugins/selection.umd.js +1 -1
  138. package/umd/plugins/selection.umd.js.map +1 -1
  139. package/umd/plugins/server-side.umd.js +1 -1
  140. package/umd/plugins/server-side.umd.js.map +1 -1
  141. package/umd/plugins/shell.umd.js.map +1 -1
  142. package/umd/plugins/sticky-rows.umd.js.map +1 -1
  143. package/umd/plugins/tooltip.umd.js.map +1 -1
  144. package/umd/plugins/tree.umd.js +1 -1
  145. package/umd/plugins/tree.umd.js.map +1 -1
  146. package/umd/plugins/undo-redo.umd.js +1 -1
  147. package/umd/plugins/undo-redo.umd.js.map +1 -1
  148. package/umd/plugins/visibility.umd.js.map +1 -1
@@ -419,7 +419,9 @@ export interface FilterPanelParams {
419
419
  * these are the extracted/flattened values (not the raw cell values).
420
420
  *
421
421
  * When a `valuesHandler` is provided in the plugin config, this array
422
- * contains the values returned by the handler instead of locally-extracted ones.
422
+ * contains the values returned by the handler instead of locally-extracted ones
423
+ * (unwrapped from any `{ value, label }` options). Blank handler values are
424
+ * normalized to `BLANK_FILTER_VALUE`, as local blanks are.
423
425
  *
424
426
  * Typical use: render checkboxes, radio buttons, or a searchable list.
425
427
  */
@@ -536,6 +538,30 @@ export interface FilterPanelParams {
536
538
  * @since 0.1.1
537
539
  */
538
540
  export type FilterPanelRenderer = (container: HTMLElement, params: FilterPanelParams) => void | undefined;
541
+ /**
542
+ * A filter value paired with the label the set filter panel should show for it.
543
+ *
544
+ * Return these from a {@link FilterValuesHandler} when the server knows the
545
+ * display text. Server-fetched values have no rows, so the panel can't call
546
+ * `column.format(value, row)` with a real row for them. Same `{ value, label }`
547
+ * shape as {@link ColumnConfig.options}.
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * valuesHandler: async (field) => {
552
+ * const res = await fetch(`/api/distinct/${field}`);
553
+ * const rows: { id: string; name: string }[] = await res.json();
554
+ * return rows.map((d) => ({ value: d.id, label: d.name }));
555
+ * }
556
+ * ```
557
+ * @since 3.10.0
558
+ */
559
+ export interface FilterValueOption {
560
+ /** The value the filter matches against (what ends up in the filter model). */
561
+ value: unknown;
562
+ /** Text shown next to the checkbox and matched by the panel's search box. */
563
+ label: string;
564
+ }
539
565
  /**
540
566
  * Async handler for fetching unique filter values from a server.
541
567
  *
@@ -543,9 +569,24 @@ export type FilterPanelRenderer = (container: HTMLElement, params: FilterPanelPa
543
569
  * this handler is called when the filter panel opens to fetch all
544
570
  * possible values for the column.
545
571
  *
572
+ * Each entry is either a plain value or a {@link FilterValueOption}
573
+ * (`{ value, label }`); the two may be mixed. Without a label the panel uses
574
+ * `column.format(value, row)` — but there is no row for server-fetched values,
575
+ * so `row` is `undefined`, and a `format` that throws (e.g. by reading the row)
576
+ * falls back to the raw value with a TBW062 warning. Return labels when the
577
+ * column's `format` needs the row.
578
+ *
579
+ * Blank values — `null`, `undefined`, and `''` (unless the column has a
580
+ * `filterValue` or `valueAccessor`, where `''` is a real value) — become the
581
+ * panel's single blank entry (`BLANK_FILTER_VALUE`), so unchecking it hides
582
+ * blank rows just like the locally built "(Blank)" entry. A label on a blank
583
+ * option (`{ value: null, label: 'Unassigned' }`) names that entry. The filter
584
+ * model — and therefore a `filterHandler` — receives `BLANK_FILTER_VALUE` for
585
+ * that entry, never the raw `null` / `''`.
586
+ *
546
587
  * @param field - The field/column name
547
588
  * @param column - The column configuration
548
- * @returns Promise resolving to array of unique values
589
+ * @returns Promise resolving to an array of unique values and/or `{ value, label }` options
549
590
  *
550
591
  * @example
551
592
  * ```ts
@@ -556,7 +597,7 @@ export type FilterPanelRenderer = (container: HTMLElement, params: FilterPanelPa
556
597
  * ```
557
598
  * @since 0.2.7
558
599
  */
559
- export type FilterValuesHandler = (field: string, column: ColumnConfig) => Promise<unknown[]>;
600
+ export type FilterValuesHandler = (field: string, column: ColumnConfig) => Promise<unknown[] | FilterValueOption[]>;
560
601
  /**
561
602
  * Async handler for applying filters on a server.
562
603
  *
@@ -753,6 +794,8 @@ export interface FilterConfig<TRow = unknown> {
753
794
  *
754
795
  * The returned values populate `FilterPanelParams.uniqueValues` and appear
755
796
  * in the checkbox list (or are passed to your custom `filterPanelRenderer`).
797
+ * Entries may be `{ value, label }` options ({@link FilterValueOption}) to
798
+ * supply the checkbox text; `uniqueValues` then holds just the values.
756
799
  *
757
800
  * @example
758
801
  * ```typescript
@@ -766,6 +809,18 @@ export interface FilterConfig<TRow = unknown> {
766
809
  *
767
810
  * @example
768
811
  * ```typescript
812
+ * // Server-provided labels (e.g. IDs shown as names)
813
+ * new FilteringPlugin({
814
+ * valuesHandler: async (field) => {
815
+ * const res = await fetch(`/api/data/distinct/${field}`);
816
+ * const items: { id: string; name: string }[] = await res.json();
817
+ * return items.map((i) => ({ value: i.id, label: i.name }));
818
+ * },
819
+ * })
820
+ * ```
821
+ *
822
+ * @example
823
+ * ```typescript
769
824
  * // Combine with filterHandler for full server-side filtering
770
825
  * new FilteringPlugin<Employee>({
771
826
  * valuesHandler: async (field) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns.ts","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns.css?inline","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns-plugin.ts"],"sourcesContent":["/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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 * Column Groups Core Logic\n *\n * Pure functions for computing and managing column header groups.\n */\n\n// Import types to enable module augmentation\nimport { COLUMN_GROUP_NO_ID, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport type { ColumnConfig } from '../../core/types';\nimport './types';\nimport type {\n ColumnGroup,\n ColumnGroupDefinition,\n ColumnGroupInternal,\n GroupHeaderRenderParams,\n GroupingColumnsConfig,\n} from './types';\n\n/**\n * Generate a stable slug from a header string for use as auto-generated group id.\n * E.g. `'Personal Info'` → `'personal-info'`\n */\nexport function slugifyHeader(header: string): string {\n return header\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/**\n * Validate column group definitions and resolve auto-generated ids.\n * Throws if a group has neither `id` nor `header`.\n *\n * @returns A new array with `id` resolved on every definition.\n */\nexport function resolveColumnGroupDefs(defs: ColumnGroupDefinition[]): (ColumnGroupDefinition & { id: string })[] {\n return defs.map((def) => {\n if (def.id) return def as ColumnGroupDefinition & { id: string };\n if (!def.header) {\n throwDiagnostic(\n COLUMN_GROUP_NO_ID,\n 'ColumnGroupDefinition requires either an \"id\" or a \"header\" to generate an id from.',\n );\n }\n return { ...def, id: slugifyHeader(def.header) };\n });\n}\n\n/**\n * Compute column groups from column configuration.\n * Handles explicit groups (via column.group) and creates implicit groups for ungrouped columns.\n *\n * @param columns - Array of column configurations\n * @returns Array of column groups, or empty if no meaningful groups\n */\nexport function computeColumnGroups<T>(columns: ColumnConfig<T>[]): ColumnGroup<T>[] {\n if (!columns.length) return [];\n\n const groupsOrdered: ColumnGroupInternal<T>[] = [];\n /** Track first-seen label for each explicit group id so fragments share the label. */\n const labelMap = new Map<string, string | undefined>();\n\n // Helper to push unnamed implicit group for a run of ungrouped columns\n const pushImplicit = (startIdx: number, cols: ColumnConfig<T>[]) => {\n if (!cols.length) return;\n // Merge with previous implicit group if adjacent to reduce noise\n const prev = groupsOrdered[groupsOrdered.length - 1];\n if (prev && prev.implicit && prev.firstIndex + prev.columns.length === startIdx) {\n prev.columns.push(...cols);\n return;\n }\n groupsOrdered.push({\n id: '__implicit__' + startIdx,\n label: undefined,\n columns: cols,\n firstIndex: startIdx,\n implicit: true,\n });\n };\n\n let run: ColumnConfig<T>[] = [];\n let runStart = 0;\n\n columns.forEach((col, idx) => {\n const g = col.group;\n if (!g) {\n if (run.length === 0) runStart = idx;\n run.push(col);\n return;\n }\n // Close any pending implicit run\n if (run.length) {\n pushImplicit(runStart, run.slice());\n run = [];\n }\n const id = typeof g === 'string' ? g : g.id;\n const rawLabel = typeof g === 'string' ? undefined : g.label;\n\n // Track labels: first column to define a label for this group id wins\n if (rawLabel && !labelMap.has(id)) {\n labelMap.set(id, rawLabel);\n }\n const label = labelMap.get(id) ?? rawLabel;\n\n // Extend the last group if it has the same id (contiguous run)\n const last = groupsOrdered[groupsOrdered.length - 1];\n if (last && !last.implicit && last.id === id) {\n last.columns.push(col);\n } else {\n // New group or new fragment (same id but non-contiguous)\n groupsOrdered.push({\n id,\n label,\n columns: [col],\n firstIndex: idx,\n });\n }\n });\n\n // Trailing implicit run\n if (run.length) pushImplicit(runStart, run);\n\n // If we only have a single implicit group covering all columns, treat as no groups\n if (groupsOrdered.length === 1 && groupsOrdered[0].implicit && groupsOrdered[0].columns.length === columns.length) {\n return [];\n }\n\n return groupsOrdered as ColumnGroup<T>[];\n}\n\n/**\n * Merge column groups into their final form for rendering:\n * identifies embedded implicit groups and collapses adjacent same-ID fragments.\n *\n * Call once per render and pass the result to both `buildGroupHeaderRow` and\n * `applyGroupedHeaderCellClasses` to avoid redundant computation.\n */\nexport function mergeGroups<T>(groups: ColumnGroup<T>[]): { merged: ColumnGroup<T>[]; embedded: Set<string> } {\n const embedded = findEmbeddedImplicitGroups(groups);\n const merged = mergeAdjacentSameIdGroups(groups, embedded);\n return { merged, embedded };\n}\n\n/**\n * Apply CSS classes to header cells based on their group membership.\n *\n * @param headerRowEl - The header row element\n * @param groups - The computed column groups (raw, before merging)\n * @param columns - The column configurations\n * @param precomputed - Optional pre-computed merged groups (avoids redundant merge)\n */\nexport function applyGroupedHeaderCellClasses(\n headerRowEl: HTMLElement | null,\n groups: ColumnGroup[],\n columns: ColumnConfig[],\n precomputed?: { merged: ColumnGroup[]; embedded: Set<string> },\n): void {\n if (!groups.length || !headerRowEl) return;\n\n const { merged: mergedGroups, embedded } = precomputed ?? mergeGroups(groups);\n\n const fieldToGroup = new Map<string, string>();\n for (const g of mergedGroups) {\n if (String(g.id).startsWith('__implicit__')) continue;\n for (const c of g.columns) {\n if (c.field) {\n fieldToGroup.set(c.field, g.id);\n }\n }\n }\n\n // Also map embedded implicit columns to their enclosing explicit group\n for (let i = 0; i < groups.length; i++) {\n const g = groups[i];\n if (!String(g.id).startsWith('__implicit__') || !embedded.has(String(g.id))) continue;\n // Find nearest explicit group before this implicit — that's the enclosing group\n for (let b = i - 1; b >= 0; b--) {\n if (!String(groups[b].id).startsWith('__implicit__')) {\n for (const c of g.columns) {\n if (c.field) fieldToGroup.set(c.field, groups[b].id);\n }\n break;\n }\n }\n }\n\n const headerCells = Array.from(headerRowEl.querySelectorAll('.cell[data-field]')) as HTMLElement[];\n headerCells.forEach((cell) => {\n const f = cell.getAttribute('data-field') || '';\n const gid = fieldToGroup.get(f);\n if (gid) {\n cell.classList.add('grouped');\n if (!cell.getAttribute('data-group')) {\n cell.setAttribute('data-group', gid);\n }\n }\n });\n\n // Mark group end cells for styling using merged groups (including implicit groups).\n // CSS :last-child rules suppress the border on the very last cell, so we mark all groups.\n for (const g of mergedGroups) {\n const last = g.columns[g.columns.length - 1];\n const cell = headerCells.find((c) => c.getAttribute('data-field') === last.field);\n if (cell) cell.classList.add('group-end');\n }\n}\n\n/**\n * Compute the grid range [start, end] for a group based on its first and last\n * column positions in the final columns array.\n */\nfunction computeGroupGridRange(group: ColumnGroup, columns: ColumnConfig[]): [number, number] | null {\n const first = group.columns[0];\n const last = group.columns[group.columns.length - 1];\n const start = first ? columns.findIndex((c) => c.field === first.field) : -1;\n const end = last ? columns.findIndex((c) => c.field === last.field) : -1;\n return start !== -1 && end !== -1 ? [start, end] : null;\n}\n\n/**\n * Find implicit groups that are sandwiched between two fragments of the same\n * explicit group (e.g. a utility column inserted between members of the same group).\n *\n * Only implicit groups whose columns are **all** internal/utility fields (prefixed\n * with `__tbw_`) qualify. When a regular data column sits between two fragments\n * of the same group it represents a genuine break introduced by column reordering\n * and must NOT be absorbed.\n *\n * @returns Set of implicit group IDs that are visually embedded.\n */\nexport function findEmbeddedImplicitGroups(groups: ColumnGroup[]): Set<string> {\n const embedded = new Set<string>();\n\n for (let i = 0; i < groups.length; i++) {\n if (!String(groups[i].id).startsWith('__implicit__')) continue;\n\n // Only absorb implicit groups that contain exclusively utility columns.\n // Data columns (user-visible fields) must always get their own header space.\n const allUtility = groups[i].columns.every((c) => c.field?.startsWith('__tbw_'));\n if (!allUtility) continue;\n\n // Find nearest explicit group before this implicit group\n let beforeId: string | null = null;\n for (let b = i - 1; b >= 0; b--) {\n if (!String(groups[b].id).startsWith('__implicit__')) {\n beforeId = groups[b].id;\n break;\n }\n }\n\n // Find nearest explicit group after this implicit group\n let afterId: string | null = null;\n for (let a = i + 1; a < groups.length; a++) {\n if (!String(groups[a].id).startsWith('__implicit__')) {\n afterId = groups[a].id;\n break;\n }\n }\n\n // Embedded if sandwiched between two fragments of the same group\n if (beforeId && afterId && beforeId === afterId) {\n embedded.add(String(groups[i].id));\n }\n }\n\n return embedded;\n}\n\n/**\n * Merge adjacent same-ID group fragments after removing embedded implicit groups.\n * This produces the final group list for header rendering, where utility columns\n * (checkbox, expander) are absorbed into the surrounding group's span.\n *\n * @param groups - The computed column groups (potentially fragmented)\n * @param embedded - Set of embedded implicit group IDs to skip\n * @returns Merged group list where same-ID fragments separated by embedded implicits are combined\n */\nexport function mergeAdjacentSameIdGroups<T>(groups: ColumnGroup<T>[], embedded: Set<string>): ColumnGroup<T>[] {\n const result: ColumnGroup<T>[] = [];\n for (const g of groups) {\n // Skip embedded implicit groups — they get absorbed into the surrounding fragment\n if (String(g.id).startsWith('__implicit__') && embedded.has(String(g.id))) continue;\n\n const prev = result[result.length - 1];\n if (prev && !String(g.id).startsWith('__implicit__') && prev.id === g.id) {\n // Merge with previous same-ID group\n result[result.length - 1] = {\n ...prev,\n columns: [...prev.columns, ...g.columns],\n };\n } else {\n result.push({ ...g, columns: [...g.columns] });\n }\n }\n return result;\n}\n\n/**\n * Build the group header row element.\n *\n * @param groups - The computed column groups\n * @param columns - The column configurations (final array including any plugin-added columns)\n * @param renderer - Optional custom group header renderer\n * @param precomputed - Optional pre-computed merged groups (avoids redundant merge)\n * @returns The group header row element, or null if no groups\n */\nexport function buildGroupHeaderRow(\n groups: ColumnGroup[],\n columns: ColumnConfig[],\n renderer?: GroupingColumnsConfig['groupHeaderRenderer'],\n precomputed?: { merged: ColumnGroup[]; embedded: Set<string> },\n): HTMLElement | null {\n if (groups.length === 0) return null;\n\n const groupRow = document.createElement('div');\n groupRow.className = 'header-group-row';\n groupRow.setAttribute('role', 'row');\n\n const { merged: mergedGroups } = precomputed ?? mergeGroups(groups);\n\n for (const g of mergedGroups) {\n const gid = String(g.id);\n const isImplicit = gid.startsWith('__implicit__');\n\n // Compute actual range from first to last member in the final columns array.\n // This correctly spans over any interleaved utility columns (e.g. checkbox).\n const range = computeGroupGridRange(g, columns);\n if (!range) continue;\n const [startIndex, endIndex] = range;\n const span = endIndex - startIndex + 1;\n\n const label = isImplicit ? '' : g.label || g.id;\n\n const cell = document.createElement('div');\n cell.className = 'cell header-group-cell';\n if (isImplicit) cell.classList.add('implicit-group');\n // `role=\"row\"` only permits cell-ish owned children (WCAG 1.3.1 / axe\n // `aria-required-children`). Real groups are column headers; the implicit\n // spacers carry no label, so they are removed from the a11y tree instead.\n cell.setAttribute('role', isImplicit ? 'presentation' : 'columnheader');\n cell.setAttribute('data-group', gid);\n cell.style.gridColumn = `${startIndex + 1} / span ${span}`;\n\n // Apply per-group renderer → fallback renderer → plain text label\n const activeRenderer = (!isImplicit && (g.renderer || renderer)) || undefined;\n if (activeRenderer && !isImplicit) {\n const params: GroupHeaderRenderParams = {\n id: gid,\n label: String(label),\n columns: g.columns as ColumnConfig[],\n firstIndex: startIndex,\n isImplicit: false,\n };\n const result = activeRenderer(params);\n if (result instanceof HTMLElement) {\n cell.appendChild(result);\n } else if (typeof result === 'string') {\n // Sanitize renderer-returned HTML to prevent XSS, mirroring the cell\n // renderer pipeline in core/internal/rows.ts.\n setSanitizedHTML(cell, result);\n } else {\n cell.textContent = label;\n }\n } else {\n // Always use textContent for non-rendered labels to prevent HTML injection\n cell.textContent = label;\n }\n\n groupRow.appendChild(cell);\n }\n\n return groupRow;\n}\n\n/**\n * Check if any columns have group configuration.\n *\n * @param columns - The column configurations\n * @returns True if at least one column has a group\n */\nexport function hasColumnGroups(columns: ColumnConfig[]): boolean {\n return columns.some((col) => col.group != null);\n}\n\n/**\n * Get group ID for a specific column.\n *\n * @param column - The column configuration\n * @returns The group ID, or undefined if not grouped\n */\nexport function getColumnGroupId(column: ColumnConfig): string | undefined {\n const g = column.group;\n if (!g) return undefined;\n return typeof g === 'string' ? g : g.id;\n}\n","@layer tbw-plugins {\n .header-group-row {\n display: grid;\n grid-auto-flow: column;\n background: var(--tbw-grouping-columns-header-bg, var(--tbw-color-header-bg));\n border-bottom: 1px solid var(--tbw-grouping-columns-border, var(--tbw-color-border));\n }\n .header-group-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--tbw-button-padding-sm, 0.25rem 0.5rem);\n font-weight: 600;\n font-size: var(--tbw-font-size-sm, 0.9em);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n .header-group-cell:last-child {\n border-right: none;\n }\n .header-row .cell.grouped {\n border-top: none;\n }\n .header-row .cell.group-end {\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n /* Last cell shouldn't have right border - avoids double border with grid edge */\n .header-row .cell.group-end:last-child {\n border-right: none;\n }\n\n /* Data cells in group-end columns - extends the strong border through all rows */\n .rows .cell.group-end {\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n /* Last cell in row shouldn't have right border */\n .rows .cell.group-end:last-child {\n border-right: none;\n }\n\n /* Hide borders when showGroupBorders is false */\n .header-group-row.no-borders {\n border-bottom: none;\n }\n .header-group-row.no-borders .header-group-cell {\n border-right: none;\n }\n .header-row.no-group-borders .cell.group-end {\n border-right: 1px solid var(--tbw-color-border);\n }\n} /* End @layer tbw-plugins */\n","/**\n * Column Groups Plugin (Class-based)\n *\n * Enables multi-level column header grouping.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { COLUMN_GROUPS_CONFLICT } from '../../core/internal/diagnostics';\nimport type { AfterCellRenderContext, PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport { type CollectHeaderRowsContext, type HeaderRowCell, type HeaderRowContribution } from '../../core/plugin/types';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ColumnGroupInfo } from '../visibility/types';\nimport {\n applyGroupedHeaderCellClasses,\n buildGroupHeaderRow,\n computeColumnGroups,\n hasColumnGroups,\n mergeGroups,\n resolveColumnGroupDefs,\n} from './grouping-columns';\nimport styles from './grouping-columns.css?inline';\nimport type { ColumnGroup, ColumnGroupDefinition, GroupingColumnsConfig } from './types';\n\n/**\n * Fold a column's inline `group` property (string shorthand or object) into an\n * id-keyed accumulator, appending the field to an existing entry when present.\n * Columns without a `group` are ignored.\n */\nfunction mergeInlineGroup(groupMap: Map<string, ColumnGroupInfo>, col: ColumnConfig): void {\n if (!col.group) return;\n const id = typeof col.group === 'string' ? col.group : col.group.id;\n const label = typeof col.group === 'string' ? col.group : (col.group.label ?? col.group.id);\n const existing = groupMap.get(id);\n if (!existing) {\n groupMap.set(id, { id, label, fields: [col.field] });\n } else if (!existing.fields.includes(col.field)) {\n existing.fields.push(col.field);\n }\n}\n\n/**\n * Column Grouping Plugin for tbw-grid\n *\n * Enables visual grouping of columns under shared headers. Supports two approaches:\n * declarative `columnGroups` at the grid level, or inline `group` property on columns.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingColumnsPlugin } from '@toolbox-web/grid/plugins/grouping-columns';\n * ```\n *\n * ## Grid Config: `columnGroups`\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `id` | `string` | Unique group identifier |\n * | `header` | `string` | Display label for the group header |\n * | `children` | `string[]` | Array of column field names in this group |\n *\n * ## Column Config: `group`\n *\n * | Type | Description |\n * |------|-------------|\n * | `string` | Simple group ID (used as both id and label) |\n * | `{ id: string; label?: string }` | Group object with explicit id and optional label |\n *\n * @example Declarative columnGroups (Recommended)\n * ```ts\n * import '@toolbox-web/grid';\n * import { GroupingColumnsPlugin } from '@toolbox-web/grid/plugins/grouping-columns';\n *\n * grid.gridConfig = {\n * columnGroups: [\n * { id: 'personal', header: 'Personal Info', children: ['firstName', 'lastName', 'email'] },\n * { id: 'work', header: 'Work Info', children: ['department', 'title', 'salary'] },\n * ],\n * columns: [\n * { field: 'firstName', header: 'First Name' },\n * { field: 'lastName', header: 'Last Name' },\n * // ...\n * ],\n * plugins: [new GroupingColumnsPlugin()],\n * };\n * ```\n *\n * @example Inline group Property\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } },\n * { field: 'lastName', header: 'Last Name', group: 'personal' }, // string shorthand\n * ],\n * plugins: [new GroupingColumnsPlugin()],\n * };\n * ```\n *\n * @see {@link GroupingColumnsConfig} for all configuration options\n * @see {@link ColumnGroup} for the group structure\n * @see ReorderPlugin for drag-to-reorder within groups\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingColumnsPlugin extends BaseGridPlugin<GroupingColumnsConfig> {\n /**\n * Plugin manifest - declares owned properties for configuration validation.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'group',\n level: 'column',\n description: 'the \"group\" column property',\n },\n {\n property: 'columnGroups',\n level: 'config',\n description: 'the \"columnGroups\" config property',\n isUsed: (v) => Array.isArray(v) && v.length > 0,\n },\n ],\n queries: [\n { type: 'getColumnGrouping', description: 'Returns column group metadata for the visibility panel' },\n {\n type: 'collectHeaderRows',\n description: 'Contributes a group-header row above the leaf header for exports / printers',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'groupingColumns';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingColumnsConfig> {\n return {\n showGroupBorders: true,\n lockGroupOrder: false,\n };\n }\n\n // #region Internal State\n private groups: ColumnGroup[] = [];\n private isActive = false;\n /** Fields that are the last column in a group (for group-end border class). */\n #groupEndFields = new Set<string>();\n /** Resolved column group definitions (with auto-generated ids). */\n #resolvedGroupDefs: (ColumnGroupDefinition & { id: string })[] = [];\n /** Map of group id → per-group renderer from ColumnGroupDefinition.renderer */\n #groupRenderers = new Map<string, NonNullable<ColumnGroupDefinition['renderer']>>();\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: import('../../core/plugin/base-plugin').GridElement): void {\n super.attach(grid);\n\n // Listen for cancelable column-move events to enforce group contiguity\n this.gridElement.addEventListener('column-move', this.#onColumnMove, {\n signal: this.disconnectSignal,\n });\n }\n\n /** @internal */\n override detach(): void {\n this.groups = [];\n this.isActive = false;\n this.#groupEndFields.clear();\n this.#resolvedGroupDefs = [];\n this.#groupRenderers.clear();\n }\n\n // #region Column Move Guard\n\n /**\n * Handle the cancelable column-move event.\n * - When lockGroupOrder is enabled, prevents moves that would break group contiguity.\n * - Always refreshes #groupEndFields after a successful move so that afterCellRender\n * applies group-end borders to the correct (reordered) last column.\n */\n #onColumnMove = (e: Event): void => {\n if (!this.isActive) return;\n\n const event = e as CustomEvent<{ field: string; columnOrder: string[] }>;\n const { field, columnOrder } = event.detail;\n\n if (this.config.lockGroupOrder) {\n // Aggregate all columns per group ID (across fragments) for full contiguity check\n const fullGroups = new Map<string, ColumnConfig[]>();\n for (const group of this.groups) {\n if (group.id.startsWith('__implicit__')) continue;\n const existing = fullGroups.get(group.id);\n if (existing) {\n existing.push(...group.columns);\n } else {\n fullGroups.set(group.id, [...group.columns]);\n }\n }\n for (const [, columns] of fullGroups) {\n if (!this.#isGroupContiguous(columns, columnOrder)) {\n event.preventDefault();\n this.#flashHeaderCell(field);\n return;\n }\n }\n }\n\n // Recompute group-end fields based on proposed column order.\n // setColumnOrder runs synchronously after this handler returns,\n // but afterCellRender (which reads #groupEndFields) fires during\n // the subsequent refreshVirtualWindow. Precompute using the\n // proposed columnOrder so the borders are correct immediately.\n this.#recomputeGroupEndFields(columnOrder);\n };\n\n /**\n * Recompute which fields are group-end based on a column order.\n * Marks every boundary where adjacent columns belong to different groups,\n * which naturally handles fragmented groups (same group ID split across\n * non-contiguous positions).\n */\n #recomputeGroupEndFields(columnOrder: string[]): void {\n this.#groupEndFields.clear();\n\n // Build field → group ID map from all groups\n const fieldToGroup = new Map<string, string>();\n for (const group of this.groups) {\n for (const col of group.columns) {\n fieldToGroup.set(col.field, group.id);\n }\n }\n\n // Mark every position where the group changes (skip the very last column)\n for (let i = 0; i < columnOrder.length - 1; i++) {\n const currentGroup = fieldToGroup.get(columnOrder[i]);\n const nextGroup = fieldToGroup.get(columnOrder[i + 1]);\n if (currentGroup !== nextGroup) {\n this.#groupEndFields.add(columnOrder[i]);\n }\n }\n }\n\n /**\n * Check if all columns are contiguous in the proposed column order.\n */\n #isGroupContiguous(columns: ColumnConfig[], columnOrder: string[]): boolean {\n const indices = columns\n .map((c) => columnOrder.indexOf(c.field))\n .filter((i) => i !== -1)\n .sort((a, b) => a - b);\n if (indices.length <= 1) return true;\n return indices.length === indices[indices.length - 1] - indices[0] + 1;\n }\n\n /**\n * Flash the header cell with an error color to indicate a blocked move.\n */\n #flashHeaderCell(field: string): void {\n const headerCell = this.gridElement?.querySelector(\n `.header-row [part~=\"header-cell\"][data-field=\"${field}\"]`,\n ) as HTMLElement;\n if (!headerCell) return;\n\n headerCell.style.setProperty('--_flash-color', 'var(--tbw-color-error)');\n headerCell.animate(\n [{ backgroundColor: 'rgba(from var(--_flash-color) r g b / 30%)' }, { backgroundColor: 'transparent' }],\n { duration: 400, easing: 'ease-out' },\n );\n }\n // #endregion\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getColumnGrouping') {\n return this.#getStableColumnGrouping();\n }\n if (query.type === 'collectHeaderRows') {\n return this.#buildHeaderRowContribution((query as PluginQuery<CollectHeaderRowsContext>).context);\n }\n return undefined;\n }\n\n /**\n * Build a {@link HeaderRowContribution} aligned to the caller-supplied\n * leaf-column array. Reuses the same fragment-aware compute/merge pipeline\n * the visual header row uses, so reordered / hidden columns and embedded\n * utility columns produce identical spans to what the user sees.\n *\n * Returns `undefined` (rather than an empty row) when no meaningful groups\n * exist, so the export/print consumer can keep its current single-row\n * layout unchanged.\n */\n #buildHeaderRowContribution(context: CollectHeaderRowsContext): HeaderRowContribution | undefined {\n const columns = context?.columns;\n if (!Array.isArray(columns) || columns.length === 0) return undefined;\n\n const groups = computeColumnGroups(columns);\n if (groups.length === 0) return undefined;\n\n const { merged } = mergeGroups(groups);\n\n const cells: HeaderRowCell[] = [];\n for (const g of merged) {\n const gid = String(g.id);\n const isImplicit = gid.startsWith('__implicit__');\n\n // Span = distance from the group's first column to its last in the\n // resolved columns array. This naturally absorbs any utility columns\n // that sit between fragments of the same group (matches the visual\n // header's `gridColumn: 'start / span N'` calculation).\n const first = g.columns[0];\n const last = g.columns[g.columns.length - 1];\n if (!first || !last) continue;\n const start = columns.findIndex((c) => c.field === first.field);\n const end = columns.findIndex((c) => c.field === last.field);\n if (start < 0 || end < 0) continue;\n const span = end - start + 1;\n\n cells.push({\n label: isImplicit ? '' : (g.label ?? g.id),\n span,\n source: this.name,\n });\n }\n\n if (cells.length === 0) return undefined;\n\n // INVARIANT: cell spans sum to columns.length. If they don't, there are\n // gaps (e.g. columns between merged groups that no fragment claims) —\n // fill them with blank spacers so consumers can rely on the invariant.\n const totalSpan = cells.reduce((s, c) => s + c.span, 0);\n if (totalSpan !== columns.length) {\n // Conservative fallback: collapse to a single blank row of correct span.\n // This shouldn't trip under normal grouping flows but guards against\n // unexpected column-set drift between the live grid and the snapshot\n // passed by the consumer.\n return { cells: [{ label: '', span: columns.length, source: this.name }] };\n }\n\n return { cells };\n }\n\n /**\n * Get stable column grouping info that includes ALL columns (visible and hidden).\n * Used by the visibility panel to maintain group structure regardless of visibility state.\n * Fields within each group are sorted by current display order.\n */\n #getStableColumnGrouping(): ColumnGroupInfo[] {\n // Precedence: resolved declarative defs → active computed groups → inline scan.\n const result = this.#groupsFromResolvedDefs() ?? this.#groupsFromActiveGroups() ?? this.#groupsFromInlineColumns();\n this.#sortGroupFieldsByDisplayOrder(result);\n return result;\n }\n\n /** Source 1 — resolved declarative `columnGroups` (from plugin config or gridConfig). */\n #groupsFromResolvedDefs(): ColumnGroupInfo[] | undefined {\n if (this.#resolvedGroupDefs.length === 0) return undefined;\n return this.#resolvedGroupDefs\n .filter((g) => g.children.length > 0)\n .map((g) => ({ id: g.id, label: g.header, fields: [...g.children] }));\n }\n\n /**\n * Source 2 — groups already computed by `processColumns`. Fragments (same group\n * id at non-contiguous positions) are aggregated into one entry, and hidden\n * columns carrying inline group properties are folded in afterwards.\n */\n #groupsFromActiveGroups(): ColumnGroupInfo[] | undefined {\n if (!this.isActive || this.groups.length === 0) return undefined;\n\n const groupMap = new Map<string, ColumnGroupInfo>();\n for (const g of this.groups) {\n if (g.id.startsWith('__implicit__')) continue;\n const existing = groupMap.get(g.id);\n if (existing) {\n for (const c of g.columns) {\n if (!existing.fields.includes(c.field)) existing.fields.push(c.field);\n }\n } else {\n groupMap.set(g.id, { id: g.id, label: g.label ?? g.id, fields: g.columns.map((c) => c.field) });\n }\n }\n\n // Hidden columns have no computed group, so pick up their inline `group` property\n for (const col of this.columns as ColumnConfig[]) {\n if ((col as { hidden?: boolean }).hidden) mergeInlineGroup(groupMap, col);\n }\n\n return Array.from(groupMap.values());\n }\n\n /** Source 3 — fall back to scanning ALL columns (including hidden) for inline `group`. */\n #groupsFromInlineColumns(): ColumnGroupInfo[] {\n const groupMap = new Map<string, ColumnGroupInfo>();\n for (const col of this.columns as ColumnConfig[]) mergeInlineGroup(groupMap, col);\n return Array.from(groupMap.values());\n }\n\n /**\n * Sort fields within each group by current display order so consumers\n * (e.g. the visibility panel) render columns in their reordered positions.\n */\n #sortGroupFieldsByDisplayOrder(groups: ColumnGroupInfo[]): void {\n const displayOrder = this.grid?.getColumnOrder();\n if (!displayOrder || displayOrder.length === 0) return;\n const orderIndex = new Map(displayOrder.map((f, i) => [f, i]));\n for (const group of groups) {\n group.fields.sort((a, b) => (orderIndex.get(a) ?? Infinity) - (orderIndex.get(b) ?? Infinity));\n }\n }\n // #endregion\n\n // #region Static Detection\n\n /**\n * Auto-detect column groups from column configuration.\n * Detects both inline `column.group` properties and declarative `columnGroups` config.\n */\n static detect(rows: readonly any[], config: any): boolean {\n // Check for declarative columnGroups in plugin feature config\n const featureConfig = config?.features?.groupingColumns;\n if (\n featureConfig &&\n typeof featureConfig === 'object' &&\n Array.isArray(featureConfig.columnGroups) &&\n featureConfig.columnGroups.length > 0\n ) {\n return true;\n }\n // Check for declarative columnGroups in gridConfig\n if (config?.columnGroups && Array.isArray(config.columnGroups) && config.columnGroups.length > 0) {\n return true;\n }\n // Check for inline group properties on columns\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasColumnGroups(columns);\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n // Resolve columnGroups source — plugin config takes precedence over gridConfig\n const pluginColumnGroups = this.config?.columnGroups;\n const gridColumnGroups = this.grid?.gridConfig?.columnGroups;\n let columnGroupDefs: ColumnGroupDefinition[] | undefined;\n\n if (pluginColumnGroups && Array.isArray(pluginColumnGroups) && pluginColumnGroups.length > 0) {\n // Warn if both sources are defined\n if (gridColumnGroups && Array.isArray(gridColumnGroups) && gridColumnGroups.length > 0) {\n this.warn(\n COLUMN_GROUPS_CONFLICT,\n 'columnGroups defined in both gridConfig and groupingColumns feature config. ' +\n 'Using feature config (higher precedence).',\n );\n }\n columnGroupDefs = pluginColumnGroups;\n } else if (gridColumnGroups && Array.isArray(gridColumnGroups) && gridColumnGroups.length > 0) {\n columnGroupDefs = gridColumnGroups;\n }\n\n let processedColumns: ColumnConfig[];\n\n if (columnGroupDefs && columnGroupDefs.length > 0) {\n // Resolve ids (auto-generate from header when missing) and validate\n const resolved = resolveColumnGroupDefs(columnGroupDefs);\n this.#resolvedGroupDefs = resolved;\n\n // Collect per-group renderers\n this.#groupRenderers.clear();\n for (const def of resolved) {\n if (def.renderer) {\n this.#groupRenderers.set(def.id, def.renderer);\n }\n }\n\n // Build a map of field → group info from the declarative config\n const fieldToGroup = new Map<string, { id: string; label: string }>();\n for (const group of resolved) {\n for (const field of group.children) {\n fieldToGroup.set(field, { id: group.id, label: group.header });\n }\n }\n\n // Apply group property to columns that don't already have one\n processedColumns = columns.map((col) => {\n const groupInfo = fieldToGroup.get(col.field);\n if (groupInfo && !col.group) {\n return { ...col, group: groupInfo };\n }\n return col;\n });\n } else {\n this.#resolvedGroupDefs = [];\n this.#groupRenderers.clear();\n processedColumns = [...columns];\n }\n\n // Compute groups from column definitions (now including applied groups)\n const groups = computeColumnGroups(processedColumns);\n\n if (groups.length === 0) {\n this.isActive = false;\n this.groups = [];\n return processedColumns;\n }\n\n // Attach per-group renderers to computed groups\n if (this.#groupRenderers.size > 0) {\n for (const g of groups) {\n const r = this.#groupRenderers.get(g.id);\n if (r) g.renderer = r;\n }\n }\n\n this.isActive = true;\n this.groups = groups;\n\n // Pre-compute group-end fields for the afterCellRender hook\n this.#groupEndFields.clear();\n for (const g of groups) {\n const lastCol = g.columns[g.columns.length - 1];\n if (lastCol?.field) {\n this.#groupEndFields.add(lastCol.field);\n }\n }\n\n // Return columns with group info applied\n return processedColumns;\n }\n\n /** @internal */\n override afterRender(): void {\n if (!this.isActive) {\n // Remove any existing group header\n const header = this.gridElement?.querySelector('.header');\n const existingGroupRow = header?.querySelector('.header-group-row');\n if (existingGroupRow) existingGroupRow.remove();\n return;\n }\n\n const header = this.gridElement?.querySelector('.header');\n if (!header) return;\n\n // Remove existing group row if present\n const existingGroupRow = header.querySelector('.header-group-row');\n if (existingGroupRow) existingGroupRow.remove();\n\n // Recompute groups from visible columns only (hidden columns have no CSS grid track).\n // This also picks up any plugin-added columns (e.g. expander) that weren't present\n // during processColumns.\n const finalColumns = this.visibleColumns as ColumnConfig[];\n const groups = computeColumnGroups(finalColumns);\n if (groups.length === 0) return;\n\n // Attach per-group renderers from resolved definitions\n if (this.#groupRenderers.size > 0) {\n for (const g of groups) {\n const r = this.#groupRenderers.get(g.id);\n if (r) g.renderer = r;\n }\n }\n\n // Keep #groupEndFields in sync for afterCellRender (covers scheduler-driven renders)\n this.#groupEndFields.clear();\n const precomputed = mergeGroups(groups);\n for (let gi = 0; gi < precomputed.merged.length; gi++) {\n const g = precomputed.merged[gi];\n const lastCol = g.columns[g.columns.length - 1];\n // Don't mark the last group — no adjacent group follows it\n if (lastCol?.field && gi < precomputed.merged.length - 1) {\n this.#groupEndFields.add(lastCol.field);\n }\n }\n\n // Build and insert group header row\n const groupRow = buildGroupHeaderRow(groups, finalColumns, this.config.groupHeaderRenderer, precomputed);\n if (groupRow) {\n // Toggle border visibility class\n groupRow.classList.toggle('no-borders', !this.config.showGroupBorders);\n\n const headerRow = header.querySelector('.header-row');\n if (headerRow) {\n header.insertBefore(groupRow, headerRow);\n } else {\n header.appendChild(groupRow);\n }\n }\n\n // Apply classes to header cells\n const headerRow = header.querySelector('.header-row') as HTMLElement;\n if (headerRow) {\n // Toggle border visibility on header cells\n headerRow.classList.toggle('no-group-borders', !this.config.showGroupBorders);\n applyGroupedHeaderCellClasses(headerRow, groups, finalColumns, precomputed);\n }\n }\n\n /**\n * Apply group-end class to individual cells during render and scroll.\n * This is more efficient than querySelectorAll in afterRender and ensures\n * cells recycled during scroll also get the class applied.\n * @internal\n */\n override afterCellRender(context: AfterCellRenderContext): void {\n if (!this.isActive || !this.config.showGroupBorders) return;\n context.cellElement.classList.toggle('group-end', this.#groupEndFields.has(context.column.field));\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Check if column groups are active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Get the computed column groups.\n * @returns Array of column groups\n */\n getGroups(): ColumnGroup[] {\n return this.groups;\n }\n\n /**\n * Get columns in a specific group (aggregated across all fragments).\n * @param groupId - The group ID to find\n * @returns Array of columns in the group\n */\n getGroupColumns(groupId: string): ColumnConfig[] {\n return this.groups.filter((g) => g.id === groupId).flatMap((g) => g.columns);\n }\n\n /**\n * Refresh column groups (recompute from current columns).\n */\n refresh(): void {\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["styles","BaseGridPlugin","_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","el","tagName","toLowerCase","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","value","forEach","removeAttribute","remove","sanitizeNode","content","setSanitizedHTML","indexOf","replaceChildren","textContent","resolveColumnGroupDefs","defs","def","id","header","code","message","gridId","pluginName","Error","gridPrefix","docsUrl","formatDiagnostic","throwDiagnostic","trim","replace","computeColumnGroups","columns","length","groupsOrdered","labelMap","Map","pushImplicit","startIdx","cols","prev","implicit","firstIndex","label","run","runStart","col","idx","g","group","rawLabel","set","get","last","mergeGroups","groups","embedded","i","String","startsWith","every","c","field","beforeId","b","afterId","a","add","findEmbeddedImplicitGroups","merged","mergeAdjacentSameIdGroups","computeGroupGridRange","first","start","findIndex","end","result","mergeInlineGroup","groupMap","existing","fields","includes","GroupingColumnsPlugin","static","ownedProperties","property","level","description","isUsed","v","isArray","queries","type","styles","defaultConfig","showGroupBorders","lockGroupOrder","isActive","groupEndFields","resolvedGroupDefs","groupRenderers","attach","grid","super","this","gridElement","addEventListener","onColumnMove","signal","disconnectSignal","detach","clear","e","event","columnOrder","detail","config","fullGroups","isGroupContiguous","preventDefault","flashHeaderCell","recomputeGroupEndFields","fieldToGroup","indices","filter","sort","headerCell","querySelector","style","setProperty","animate","backgroundColor","duration","easing","handleQuery","query","getStableColumnGrouping","buildHeaderRowContribution","context","cells","isImplicit","span","source","reduce","groupsFromResolvedDefs","groupsFromActiveGroups","groupsFromInlineColumns","sortGroupFieldsByDisplayOrder","children","hidden","values","displayOrder","getColumnOrder","orderIndex","f","Infinity","detect","rows","featureConfig","features","groupingColumns","columnGroups","hasColumnGroups","processColumns","pluginColumnGroups","gridColumnGroups","gridConfig","columnGroupDefs","processedColumns","warn","resolved","renderer","groupInfo","size","r","lastCol","afterRender","existingGroupRow","finalColumns","visibleColumns","precomputed","gi","groupRow","className","setAttribute","mergedGroups","gid","range","startIndex","endIndex","cell","classList","gridColumn","activeRenderer","HTMLElement","appendChild","buildGroupHeaderRow","groupHeaderRenderer","toggle","headerRow","insertBefore","headerRowEl","headerCells","getAttribute","find","applyGroupedHeaderCellClasses","afterCellRender","cellElement","column","isGroupingActive","getGroups","getGroupColumns","groupId","flatMap","refresh","requestRender"],"sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns.ts","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns.css?inline","../../../../../../libs/grid/src/lib/plugins/grouping-columns/grouping-columns-plugin.ts"],"sourcesContent":["/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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 * Column Groups Core Logic\n *\n * Pure functions for computing and managing column header groups.\n */\n\n// Import types to enable module augmentation\nimport { COLUMN_GROUP_NO_ID, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport type { ColumnConfig } from '../../core/types';\nimport './types';\nimport type {\n ColumnGroup,\n ColumnGroupDefinition,\n ColumnGroupInternal,\n GroupHeaderRenderParams,\n GroupingColumnsConfig,\n} from './types';\n\n/**\n * Generate a stable slug from a header string for use as auto-generated group id.\n * E.g. `'Personal Info'` → `'personal-info'`\n */\nexport function slugifyHeader(header: string): string {\n return header\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/**\n * Validate column group definitions and resolve auto-generated ids.\n * Throws if a group has neither `id` nor `header`.\n *\n * @returns A new array with `id` resolved on every definition.\n */\nexport function resolveColumnGroupDefs(defs: ColumnGroupDefinition[]): (ColumnGroupDefinition & { id: string })[] {\n return defs.map((def) => {\n if (def.id) return def as ColumnGroupDefinition & { id: string };\n if (!def.header) {\n throwDiagnostic(\n COLUMN_GROUP_NO_ID,\n 'ColumnGroupDefinition requires either an \"id\" or a \"header\" to generate an id from.',\n );\n }\n return { ...def, id: slugifyHeader(def.header) };\n });\n}\n\n/**\n * Compute column groups from column configuration.\n * Handles explicit groups (via column.group) and creates implicit groups for ungrouped columns.\n *\n * @param columns - Array of column configurations\n * @returns Array of column groups, or empty if no meaningful groups\n */\nexport function computeColumnGroups<T>(columns: ColumnConfig<T>[]): ColumnGroup<T>[] {\n if (!columns.length) return [];\n\n const groupsOrdered: ColumnGroupInternal<T>[] = [];\n /** Track first-seen label for each explicit group id so fragments share the label. */\n const labelMap = new Map<string, string | undefined>();\n\n // Helper to push unnamed implicit group for a run of ungrouped columns\n const pushImplicit = (startIdx: number, cols: ColumnConfig<T>[]) => {\n if (!cols.length) return;\n // Merge with previous implicit group if adjacent to reduce noise\n const prev = groupsOrdered[groupsOrdered.length - 1];\n if (prev && prev.implicit && prev.firstIndex + prev.columns.length === startIdx) {\n prev.columns.push(...cols);\n return;\n }\n groupsOrdered.push({\n id: '__implicit__' + startIdx,\n label: undefined,\n columns: cols,\n firstIndex: startIdx,\n implicit: true,\n });\n };\n\n let run: ColumnConfig<T>[] = [];\n let runStart = 0;\n\n columns.forEach((col, idx) => {\n const g = col.group;\n if (!g) {\n if (run.length === 0) runStart = idx;\n run.push(col);\n return;\n }\n // Close any pending implicit run\n if (run.length) {\n pushImplicit(runStart, run.slice());\n run = [];\n }\n const id = typeof g === 'string' ? g : g.id;\n const rawLabel = typeof g === 'string' ? undefined : g.label;\n\n // Track labels: first column to define a label for this group id wins\n if (rawLabel && !labelMap.has(id)) {\n labelMap.set(id, rawLabel);\n }\n const label = labelMap.get(id) ?? rawLabel;\n\n // Extend the last group if it has the same id (contiguous run)\n const last = groupsOrdered[groupsOrdered.length - 1];\n if (last && !last.implicit && last.id === id) {\n last.columns.push(col);\n } else {\n // New group or new fragment (same id but non-contiguous)\n groupsOrdered.push({\n id,\n label,\n columns: [col],\n firstIndex: idx,\n });\n }\n });\n\n // Trailing implicit run\n if (run.length) pushImplicit(runStart, run);\n\n // If we only have a single implicit group covering all columns, treat as no groups\n if (groupsOrdered.length === 1 && groupsOrdered[0].implicit && groupsOrdered[0].columns.length === columns.length) {\n return [];\n }\n\n return groupsOrdered as ColumnGroup<T>[];\n}\n\n/**\n * Merge column groups into their final form for rendering:\n * identifies embedded implicit groups and collapses adjacent same-ID fragments.\n *\n * Call once per render and pass the result to both `buildGroupHeaderRow` and\n * `applyGroupedHeaderCellClasses` to avoid redundant computation.\n */\nexport function mergeGroups<T>(groups: ColumnGroup<T>[]): { merged: ColumnGroup<T>[]; embedded: Set<string> } {\n const embedded = findEmbeddedImplicitGroups(groups);\n const merged = mergeAdjacentSameIdGroups(groups, embedded);\n return { merged, embedded };\n}\n\n/**\n * Apply CSS classes to header cells based on their group membership.\n *\n * @param headerRowEl - The header row element\n * @param groups - The computed column groups (raw, before merging)\n * @param columns - The column configurations\n * @param precomputed - Optional pre-computed merged groups (avoids redundant merge)\n */\nexport function applyGroupedHeaderCellClasses(\n headerRowEl: HTMLElement | null,\n groups: ColumnGroup[],\n columns: ColumnConfig[],\n precomputed?: { merged: ColumnGroup[]; embedded: Set<string> },\n): void {\n if (!groups.length || !headerRowEl) return;\n\n const { merged: mergedGroups, embedded } = precomputed ?? mergeGroups(groups);\n\n const fieldToGroup = new Map<string, string>();\n for (const g of mergedGroups) {\n if (String(g.id).startsWith('__implicit__')) continue;\n for (const c of g.columns) {\n if (c.field) {\n fieldToGroup.set(c.field, g.id);\n }\n }\n }\n\n // Also map embedded implicit columns to their enclosing explicit group\n for (let i = 0; i < groups.length; i++) {\n const g = groups[i];\n if (!String(g.id).startsWith('__implicit__') || !embedded.has(String(g.id))) continue;\n // Find nearest explicit group before this implicit — that's the enclosing group\n for (let b = i - 1; b >= 0; b--) {\n if (!String(groups[b].id).startsWith('__implicit__')) {\n for (const c of g.columns) {\n if (c.field) fieldToGroup.set(c.field, groups[b].id);\n }\n break;\n }\n }\n }\n\n const headerCells = Array.from(headerRowEl.querySelectorAll('.cell[data-field]')) as HTMLElement[];\n headerCells.forEach((cell) => {\n const f = cell.getAttribute('data-field') || '';\n const gid = fieldToGroup.get(f);\n if (gid) {\n cell.classList.add('grouped');\n if (!cell.getAttribute('data-group')) {\n cell.setAttribute('data-group', gid);\n }\n }\n });\n\n // Mark group end cells for styling using merged groups (including implicit groups).\n // CSS :last-child rules suppress the border on the very last cell, so we mark all groups.\n for (const g of mergedGroups) {\n const last = g.columns[g.columns.length - 1];\n const cell = headerCells.find((c) => c.getAttribute('data-field') === last.field);\n if (cell) cell.classList.add('group-end');\n }\n}\n\n/**\n * Compute the grid range [start, end] for a group based on its first and last\n * column positions in the final columns array.\n */\nfunction computeGroupGridRange(group: ColumnGroup, columns: ColumnConfig[]): [number, number] | null {\n const first = group.columns[0];\n const last = group.columns[group.columns.length - 1];\n const start = first ? columns.findIndex((c) => c.field === first.field) : -1;\n const end = last ? columns.findIndex((c) => c.field === last.field) : -1;\n return start !== -1 && end !== -1 ? [start, end] : null;\n}\n\n/**\n * Find implicit groups that are sandwiched between two fragments of the same\n * explicit group (e.g. a utility column inserted between members of the same group).\n *\n * Only implicit groups whose columns are **all** internal/utility fields (prefixed\n * with `__tbw_`) qualify. When a regular data column sits between two fragments\n * of the same group it represents a genuine break introduced by column reordering\n * and must NOT be absorbed.\n *\n * @returns Set of implicit group IDs that are visually embedded.\n */\nexport function findEmbeddedImplicitGroups(groups: ColumnGroup[]): Set<string> {\n const embedded = new Set<string>();\n\n for (let i = 0; i < groups.length; i++) {\n if (!String(groups[i].id).startsWith('__implicit__')) continue;\n\n // Only absorb implicit groups that contain exclusively utility columns.\n // Data columns (user-visible fields) must always get their own header space.\n const allUtility = groups[i].columns.every((c) => c.field?.startsWith('__tbw_'));\n if (!allUtility) continue;\n\n // Find nearest explicit group before this implicit group\n let beforeId: string | null = null;\n for (let b = i - 1; b >= 0; b--) {\n if (!String(groups[b].id).startsWith('__implicit__')) {\n beforeId = groups[b].id;\n break;\n }\n }\n\n // Find nearest explicit group after this implicit group\n let afterId: string | null = null;\n for (let a = i + 1; a < groups.length; a++) {\n if (!String(groups[a].id).startsWith('__implicit__')) {\n afterId = groups[a].id;\n break;\n }\n }\n\n // Embedded if sandwiched between two fragments of the same group\n if (beforeId && afterId && beforeId === afterId) {\n embedded.add(String(groups[i].id));\n }\n }\n\n return embedded;\n}\n\n/**\n * Merge adjacent same-ID group fragments after removing embedded implicit groups.\n * This produces the final group list for header rendering, where utility columns\n * (checkbox, expander) are absorbed into the surrounding group's span.\n *\n * @param groups - The computed column groups (potentially fragmented)\n * @param embedded - Set of embedded implicit group IDs to skip\n * @returns Merged group list where same-ID fragments separated by embedded implicits are combined\n */\nexport function mergeAdjacentSameIdGroups<T>(groups: ColumnGroup<T>[], embedded: Set<string>): ColumnGroup<T>[] {\n const result: ColumnGroup<T>[] = [];\n for (const g of groups) {\n // Skip embedded implicit groups — they get absorbed into the surrounding fragment\n if (String(g.id).startsWith('__implicit__') && embedded.has(String(g.id))) continue;\n\n const prev = result[result.length - 1];\n if (prev && !String(g.id).startsWith('__implicit__') && prev.id === g.id) {\n // Merge with previous same-ID group\n result[result.length - 1] = {\n ...prev,\n columns: [...prev.columns, ...g.columns],\n };\n } else {\n result.push({ ...g, columns: [...g.columns] });\n }\n }\n return result;\n}\n\n/**\n * Build the group header row element.\n *\n * @param groups - The computed column groups\n * @param columns - The column configurations (final array including any plugin-added columns)\n * @param renderer - Optional custom group header renderer\n * @param precomputed - Optional pre-computed merged groups (avoids redundant merge)\n * @returns The group header row element, or null if no groups\n */\nexport function buildGroupHeaderRow(\n groups: ColumnGroup[],\n columns: ColumnConfig[],\n renderer?: GroupingColumnsConfig['groupHeaderRenderer'],\n precomputed?: { merged: ColumnGroup[]; embedded: Set<string> },\n): HTMLElement | null {\n if (groups.length === 0) return null;\n\n const groupRow = document.createElement('div');\n groupRow.className = 'header-group-row';\n groupRow.setAttribute('role', 'row');\n\n const { merged: mergedGroups } = precomputed ?? mergeGroups(groups);\n\n for (const g of mergedGroups) {\n const gid = String(g.id);\n const isImplicit = gid.startsWith('__implicit__');\n\n // Compute actual range from first to last member in the final columns array.\n // This correctly spans over any interleaved utility columns (e.g. checkbox).\n const range = computeGroupGridRange(g, columns);\n if (!range) continue;\n const [startIndex, endIndex] = range;\n const span = endIndex - startIndex + 1;\n\n const label = isImplicit ? '' : g.label || g.id;\n\n const cell = document.createElement('div');\n cell.className = 'cell header-group-cell';\n if (isImplicit) cell.classList.add('implicit-group');\n // `role=\"row\"` only permits cell-ish owned children (WCAG 1.3.1 / axe\n // `aria-required-children`). Real groups are column headers; the implicit\n // spacers carry no label, so they are removed from the a11y tree instead.\n cell.setAttribute('role', isImplicit ? 'presentation' : 'columnheader');\n cell.setAttribute('data-group', gid);\n cell.style.gridColumn = `${startIndex + 1} / span ${span}`;\n\n // Apply per-group renderer → fallback renderer → plain text label\n const activeRenderer = (!isImplicit && (g.renderer || renderer)) || undefined;\n if (activeRenderer && !isImplicit) {\n const params: GroupHeaderRenderParams = {\n id: gid,\n label: String(label),\n columns: g.columns as ColumnConfig[],\n firstIndex: startIndex,\n isImplicit: false,\n };\n const result = activeRenderer(params);\n if (result instanceof HTMLElement) {\n cell.appendChild(result);\n } else if (typeof result === 'string') {\n // Sanitize renderer-returned HTML to prevent XSS, mirroring the cell\n // renderer pipeline in core/internal/rows.ts.\n setSanitizedHTML(cell, result);\n } else {\n cell.textContent = label;\n }\n } else {\n // Always use textContent for non-rendered labels to prevent HTML injection\n cell.textContent = label;\n }\n\n groupRow.appendChild(cell);\n }\n\n return groupRow;\n}\n\n/**\n * Check if any columns have group configuration.\n *\n * @param columns - The column configurations\n * @returns True if at least one column has a group\n */\nexport function hasColumnGroups(columns: ColumnConfig[]): boolean {\n return columns.some((col) => col.group != null);\n}\n\n/**\n * Get group ID for a specific column.\n *\n * @param column - The column configuration\n * @returns The group ID, or undefined if not grouped\n */\nexport function getColumnGroupId(column: ColumnConfig): string | undefined {\n const g = column.group;\n if (!g) return undefined;\n return typeof g === 'string' ? g : g.id;\n}\n","@layer tbw-plugins {\n .header-group-row {\n display: grid;\n grid-auto-flow: column;\n background: var(--tbw-grouping-columns-header-bg, var(--tbw-color-header-bg));\n border-bottom: 1px solid var(--tbw-grouping-columns-border, var(--tbw-color-border));\n }\n .header-group-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--tbw-button-padding-sm, 0.25rem 0.5rem);\n font-weight: 600;\n font-size: var(--tbw-font-size-sm, 0.9em);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n .header-group-cell:last-child {\n border-right: none;\n }\n .header-row .cell.grouped {\n border-top: none;\n }\n .header-row .cell.group-end {\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n /* Last cell shouldn't have right border - avoids double border with grid edge */\n .header-row .cell.group-end:last-child {\n border-right: none;\n }\n\n /* Data cells in group-end columns - extends the strong border through all rows */\n .rows .cell.group-end {\n border-right: 2px solid\n var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)));\n }\n /* Last cell in row shouldn't have right border */\n .rows .cell.group-end:last-child {\n border-right: none;\n }\n\n /* Hide borders when showGroupBorders is false */\n .header-group-row.no-borders {\n border-bottom: none;\n }\n .header-group-row.no-borders .header-group-cell {\n border-right: none;\n }\n .header-row.no-group-borders .cell.group-end {\n border-right: 1px solid var(--tbw-color-border);\n }\n} /* End @layer tbw-plugins */\n","/**\n * Column Groups Plugin (Class-based)\n *\n * Enables multi-level column header grouping.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { COLUMN_GROUPS_CONFLICT } from '../../core/internal/diagnostics';\nimport type { AfterCellRenderContext, PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport { type CollectHeaderRowsContext, type HeaderRowCell, type HeaderRowContribution } from '../../core/plugin/types';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ColumnGroupInfo } from '../visibility/types';\nimport {\n applyGroupedHeaderCellClasses,\n buildGroupHeaderRow,\n computeColumnGroups,\n hasColumnGroups,\n mergeGroups,\n resolveColumnGroupDefs,\n} from './grouping-columns';\nimport styles from './grouping-columns.css?inline';\nimport type { ColumnGroup, ColumnGroupDefinition, GroupingColumnsConfig } from './types';\n\n/**\n * Fold a column's inline `group` property (string shorthand or object) into an\n * id-keyed accumulator, appending the field to an existing entry when present.\n * Columns without a `group` are ignored.\n */\nfunction mergeInlineGroup(groupMap: Map<string, ColumnGroupInfo>, col: ColumnConfig): void {\n if (!col.group) return;\n const id = typeof col.group === 'string' ? col.group : col.group.id;\n const label = typeof col.group === 'string' ? col.group : (col.group.label ?? col.group.id);\n const existing = groupMap.get(id);\n if (!existing) {\n groupMap.set(id, { id, label, fields: [col.field] });\n } else if (!existing.fields.includes(col.field)) {\n existing.fields.push(col.field);\n }\n}\n\n/**\n * Column Grouping Plugin for tbw-grid\n *\n * Enables visual grouping of columns under shared headers. Supports two approaches:\n * declarative `columnGroups` at the grid level, or inline `group` property on columns.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingColumnsPlugin } from '@toolbox-web/grid/plugins/grouping-columns';\n * ```\n *\n * ## Grid Config: `columnGroups`\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `id` | `string` | Unique group identifier |\n * | `header` | `string` | Display label for the group header |\n * | `children` | `string[]` | Array of column field names in this group |\n *\n * ## Column Config: `group`\n *\n * | Type | Description |\n * |------|-------------|\n * | `string` | Simple group ID (used as both id and label) |\n * | `{ id: string; label?: string }` | Group object with explicit id and optional label |\n *\n * @example Declarative columnGroups (Recommended)\n * ```ts\n * import '@toolbox-web/grid';\n * import { GroupingColumnsPlugin } from '@toolbox-web/grid/plugins/grouping-columns';\n *\n * grid.gridConfig = {\n * columnGroups: [\n * { id: 'personal', header: 'Personal Info', children: ['firstName', 'lastName', 'email'] },\n * { id: 'work', header: 'Work Info', children: ['department', 'title', 'salary'] },\n * ],\n * columns: [\n * { field: 'firstName', header: 'First Name' },\n * { field: 'lastName', header: 'Last Name' },\n * // ...\n * ],\n * plugins: [new GroupingColumnsPlugin()],\n * };\n * ```\n *\n * @example Inline group Property\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'firstName', header: 'First Name', group: { id: 'personal', label: 'Personal Info' } },\n * { field: 'lastName', header: 'Last Name', group: 'personal' }, // string shorthand\n * ],\n * plugins: [new GroupingColumnsPlugin()],\n * };\n * ```\n *\n * @see {@link GroupingColumnsConfig} for all configuration options\n * @see {@link ColumnGroup} for the group structure\n * @see ReorderPlugin for drag-to-reorder within groups\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingColumnsPlugin extends BaseGridPlugin<GroupingColumnsConfig> {\n /**\n * Plugin manifest - declares owned properties for configuration validation.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'group',\n level: 'column',\n description: 'the \"group\" column property',\n },\n {\n property: 'columnGroups',\n level: 'config',\n description: 'the \"columnGroups\" config property',\n isUsed: (v) => Array.isArray(v) && v.length > 0,\n },\n ],\n queries: [\n { type: 'getColumnGrouping', description: 'Returns column group metadata for the visibility panel' },\n {\n type: 'collectHeaderRows',\n description: 'Contributes a group-header row above the leaf header for exports / printers',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'groupingColumns';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingColumnsConfig> {\n return {\n showGroupBorders: true,\n lockGroupOrder: false,\n };\n }\n\n // #region Internal State\n private groups: ColumnGroup[] = [];\n private isActive = false;\n /** Fields that are the last column in a group (for group-end border class). */\n #groupEndFields = new Set<string>();\n /** Resolved column group definitions (with auto-generated ids). */\n #resolvedGroupDefs: (ColumnGroupDefinition & { id: string })[] = [];\n /** Map of group id → per-group renderer from ColumnGroupDefinition.renderer */\n #groupRenderers = new Map<string, NonNullable<ColumnGroupDefinition['renderer']>>();\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: import('../../core/plugin/base-plugin').GridElement): void {\n super.attach(grid);\n\n // Listen for cancelable column-move events to enforce group contiguity\n this.gridElement.addEventListener('column-move', this.#onColumnMove, {\n signal: this.disconnectSignal,\n });\n }\n\n /** @internal */\n override detach(): void {\n this.groups = [];\n this.isActive = false;\n this.#groupEndFields.clear();\n this.#resolvedGroupDefs = [];\n this.#groupRenderers.clear();\n }\n\n // #region Column Move Guard\n\n /**\n * Handle the cancelable column-move event.\n * - When lockGroupOrder is enabled, prevents moves that would break group contiguity.\n * - Always refreshes #groupEndFields after a successful move so that afterCellRender\n * applies group-end borders to the correct (reordered) last column.\n */\n #onColumnMove = (e: Event): void => {\n if (!this.isActive) return;\n\n const event = e as CustomEvent<{ field: string; columnOrder: string[] }>;\n const { field, columnOrder } = event.detail;\n\n if (this.config.lockGroupOrder) {\n // Aggregate all columns per group ID (across fragments) for full contiguity check\n const fullGroups = new Map<string, ColumnConfig[]>();\n for (const group of this.groups) {\n if (group.id.startsWith('__implicit__')) continue;\n const existing = fullGroups.get(group.id);\n if (existing) {\n existing.push(...group.columns);\n } else {\n fullGroups.set(group.id, [...group.columns]);\n }\n }\n for (const [, columns] of fullGroups) {\n if (!this.#isGroupContiguous(columns, columnOrder)) {\n event.preventDefault();\n this.#flashHeaderCell(field);\n return;\n }\n }\n }\n\n // Recompute group-end fields based on proposed column order.\n // setColumnOrder runs synchronously after this handler returns,\n // but afterCellRender (which reads #groupEndFields) fires during\n // the subsequent refreshVirtualWindow. Precompute using the\n // proposed columnOrder so the borders are correct immediately.\n this.#recomputeGroupEndFields(columnOrder);\n };\n\n /**\n * Recompute which fields are group-end based on a column order.\n * Marks every boundary where adjacent columns belong to different groups,\n * which naturally handles fragmented groups (same group ID split across\n * non-contiguous positions).\n */\n #recomputeGroupEndFields(columnOrder: string[]): void {\n this.#groupEndFields.clear();\n\n // Build field → group ID map from all groups\n const fieldToGroup = new Map<string, string>();\n for (const group of this.groups) {\n for (const col of group.columns) {\n fieldToGroup.set(col.field, group.id);\n }\n }\n\n // Mark every position where the group changes (skip the very last column)\n for (let i = 0; i < columnOrder.length - 1; i++) {\n const currentGroup = fieldToGroup.get(columnOrder[i]);\n const nextGroup = fieldToGroup.get(columnOrder[i + 1]);\n if (currentGroup !== nextGroup) {\n this.#groupEndFields.add(columnOrder[i]);\n }\n }\n }\n\n /**\n * Check if all columns are contiguous in the proposed column order.\n */\n #isGroupContiguous(columns: ColumnConfig[], columnOrder: string[]): boolean {\n const indices = columns\n .map((c) => columnOrder.indexOf(c.field))\n .filter((i) => i !== -1)\n .sort((a, b) => a - b);\n if (indices.length <= 1) return true;\n return indices.length === indices[indices.length - 1] - indices[0] + 1;\n }\n\n /**\n * Flash the header cell with an error color to indicate a blocked move.\n */\n #flashHeaderCell(field: string): void {\n const headerCell = this.gridElement?.querySelector(\n `.header-row [part~=\"header-cell\"][data-field=\"${field}\"]`,\n ) as HTMLElement;\n if (!headerCell) return;\n\n headerCell.style.setProperty('--_flash-color', 'var(--tbw-color-error)');\n headerCell.animate(\n [{ backgroundColor: 'rgba(from var(--_flash-color) r g b / 30%)' }, { backgroundColor: 'transparent' }],\n { duration: 400, easing: 'ease-out' },\n );\n }\n // #endregion\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getColumnGrouping') {\n return this.#getStableColumnGrouping();\n }\n if (query.type === 'collectHeaderRows') {\n return this.#buildHeaderRowContribution((query as PluginQuery<CollectHeaderRowsContext>).context);\n }\n return undefined;\n }\n\n /**\n * Build a {@link HeaderRowContribution} aligned to the caller-supplied\n * leaf-column array. Reuses the same fragment-aware compute/merge pipeline\n * the visual header row uses, so reordered / hidden columns and embedded\n * utility columns produce identical spans to what the user sees.\n *\n * Returns `undefined` (rather than an empty row) when no meaningful groups\n * exist, so the export/print consumer can keep its current single-row\n * layout unchanged.\n */\n #buildHeaderRowContribution(context: CollectHeaderRowsContext): HeaderRowContribution | undefined {\n const columns = context?.columns;\n if (!Array.isArray(columns) || columns.length === 0) return undefined;\n\n const groups = computeColumnGroups(columns);\n if (groups.length === 0) return undefined;\n\n const { merged } = mergeGroups(groups);\n\n const cells: HeaderRowCell[] = [];\n for (const g of merged) {\n const gid = String(g.id);\n const isImplicit = gid.startsWith('__implicit__');\n\n // Span = distance from the group's first column to its last in the\n // resolved columns array. This naturally absorbs any utility columns\n // that sit between fragments of the same group (matches the visual\n // header's `gridColumn: 'start / span N'` calculation).\n const first = g.columns[0];\n const last = g.columns[g.columns.length - 1];\n if (!first || !last) continue;\n const start = columns.findIndex((c) => c.field === first.field);\n const end = columns.findIndex((c) => c.field === last.field);\n if (start < 0 || end < 0) continue;\n const span = end - start + 1;\n\n cells.push({\n label: isImplicit ? '' : (g.label ?? g.id),\n span,\n source: this.name,\n });\n }\n\n if (cells.length === 0) return undefined;\n\n // INVARIANT: cell spans sum to columns.length. If they don't, there are\n // gaps (e.g. columns between merged groups that no fragment claims) —\n // fill them with blank spacers so consumers can rely on the invariant.\n const totalSpan = cells.reduce((s, c) => s + c.span, 0);\n if (totalSpan !== columns.length) {\n // Conservative fallback: collapse to a single blank row of correct span.\n // This shouldn't trip under normal grouping flows but guards against\n // unexpected column-set drift between the live grid and the snapshot\n // passed by the consumer.\n return { cells: [{ label: '', span: columns.length, source: this.name }] };\n }\n\n return { cells };\n }\n\n /**\n * Get stable column grouping info that includes ALL columns (visible and hidden).\n * Used by the visibility panel to maintain group structure regardless of visibility state.\n * Fields within each group are sorted by current display order.\n */\n #getStableColumnGrouping(): ColumnGroupInfo[] {\n // Precedence: resolved declarative defs → active computed groups → inline scan.\n const result = this.#groupsFromResolvedDefs() ?? this.#groupsFromActiveGroups() ?? this.#groupsFromInlineColumns();\n this.#sortGroupFieldsByDisplayOrder(result);\n return result;\n }\n\n /** Source 1 — resolved declarative `columnGroups` (from plugin config or gridConfig). */\n #groupsFromResolvedDefs(): ColumnGroupInfo[] | undefined {\n if (this.#resolvedGroupDefs.length === 0) return undefined;\n return this.#resolvedGroupDefs\n .filter((g) => g.children.length > 0)\n .map((g) => ({ id: g.id, label: g.header, fields: [...g.children] }));\n }\n\n /**\n * Source 2 — groups already computed by `processColumns`. Fragments (same group\n * id at non-contiguous positions) are aggregated into one entry, and hidden\n * columns carrying inline group properties are folded in afterwards.\n */\n #groupsFromActiveGroups(): ColumnGroupInfo[] | undefined {\n if (!this.isActive || this.groups.length === 0) return undefined;\n\n const groupMap = new Map<string, ColumnGroupInfo>();\n for (const g of this.groups) {\n if (g.id.startsWith('__implicit__')) continue;\n const existing = groupMap.get(g.id);\n if (existing) {\n for (const c of g.columns) {\n if (!existing.fields.includes(c.field)) existing.fields.push(c.field);\n }\n } else {\n groupMap.set(g.id, { id: g.id, label: g.label ?? g.id, fields: g.columns.map((c) => c.field) });\n }\n }\n\n // Hidden columns have no computed group, so pick up their inline `group` property\n for (const col of this.columns as ColumnConfig[]) {\n if ((col as { hidden?: boolean }).hidden) mergeInlineGroup(groupMap, col);\n }\n\n return Array.from(groupMap.values());\n }\n\n /** Source 3 — fall back to scanning ALL columns (including hidden) for inline `group`. */\n #groupsFromInlineColumns(): ColumnGroupInfo[] {\n const groupMap = new Map<string, ColumnGroupInfo>();\n for (const col of this.columns as ColumnConfig[]) mergeInlineGroup(groupMap, col);\n return Array.from(groupMap.values());\n }\n\n /**\n * Sort fields within each group by current display order so consumers\n * (e.g. the visibility panel) render columns in their reordered positions.\n */\n #sortGroupFieldsByDisplayOrder(groups: ColumnGroupInfo[]): void {\n const displayOrder = this.grid?.getColumnOrder();\n if (!displayOrder || displayOrder.length === 0) return;\n const orderIndex = new Map(displayOrder.map((f, i) => [f, i]));\n for (const group of groups) {\n group.fields.sort((a, b) => (orderIndex.get(a) ?? Infinity) - (orderIndex.get(b) ?? Infinity));\n }\n }\n // #endregion\n\n // #region Static Detection\n\n /**\n * Auto-detect column groups from column configuration.\n * Detects both inline `column.group` properties and declarative `columnGroups` config.\n */\n static detect(rows: readonly any[], config: any): boolean {\n // Check for declarative columnGroups in plugin feature config\n const featureConfig = config?.features?.groupingColumns;\n if (\n featureConfig &&\n typeof featureConfig === 'object' &&\n Array.isArray(featureConfig.columnGroups) &&\n featureConfig.columnGroups.length > 0\n ) {\n return true;\n }\n // Check for declarative columnGroups in gridConfig\n if (config?.columnGroups && Array.isArray(config.columnGroups) && config.columnGroups.length > 0) {\n return true;\n }\n // Check for inline group properties on columns\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasColumnGroups(columns);\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n // Resolve columnGroups source — plugin config takes precedence over gridConfig\n const pluginColumnGroups = this.config?.columnGroups;\n const gridColumnGroups = this.grid?.gridConfig?.columnGroups;\n let columnGroupDefs: ColumnGroupDefinition[] | undefined;\n\n if (pluginColumnGroups && Array.isArray(pluginColumnGroups) && pluginColumnGroups.length > 0) {\n // Warn if both sources are defined\n if (gridColumnGroups && Array.isArray(gridColumnGroups) && gridColumnGroups.length > 0) {\n this.warn(\n COLUMN_GROUPS_CONFLICT,\n 'columnGroups defined in both gridConfig and groupingColumns feature config. ' +\n 'Using feature config (higher precedence).',\n );\n }\n columnGroupDefs = pluginColumnGroups;\n } else if (gridColumnGroups && Array.isArray(gridColumnGroups) && gridColumnGroups.length > 0) {\n columnGroupDefs = gridColumnGroups;\n }\n\n let processedColumns: ColumnConfig[];\n\n if (columnGroupDefs && columnGroupDefs.length > 0) {\n // Resolve ids (auto-generate from header when missing) and validate\n const resolved = resolveColumnGroupDefs(columnGroupDefs);\n this.#resolvedGroupDefs = resolved;\n\n // Collect per-group renderers\n this.#groupRenderers.clear();\n for (const def of resolved) {\n if (def.renderer) {\n this.#groupRenderers.set(def.id, def.renderer);\n }\n }\n\n // Build a map of field → group info from the declarative config\n const fieldToGroup = new Map<string, { id: string; label: string }>();\n for (const group of resolved) {\n for (const field of group.children) {\n fieldToGroup.set(field, { id: group.id, label: group.header });\n }\n }\n\n // Apply group property to columns that don't already have one\n processedColumns = columns.map((col) => {\n const groupInfo = fieldToGroup.get(col.field);\n if (groupInfo && !col.group) {\n return { ...col, group: groupInfo };\n }\n return col;\n });\n } else {\n this.#resolvedGroupDefs = [];\n this.#groupRenderers.clear();\n processedColumns = [...columns];\n }\n\n // Compute groups from column definitions (now including applied groups)\n const groups = computeColumnGroups(processedColumns);\n\n if (groups.length === 0) {\n this.isActive = false;\n this.groups = [];\n return processedColumns;\n }\n\n // Attach per-group renderers to computed groups\n if (this.#groupRenderers.size > 0) {\n for (const g of groups) {\n const r = this.#groupRenderers.get(g.id);\n if (r) g.renderer = r;\n }\n }\n\n this.isActive = true;\n this.groups = groups;\n\n // Pre-compute group-end fields for the afterCellRender hook\n this.#groupEndFields.clear();\n for (const g of groups) {\n const lastCol = g.columns[g.columns.length - 1];\n if (lastCol?.field) {\n this.#groupEndFields.add(lastCol.field);\n }\n }\n\n // Return columns with group info applied\n return processedColumns;\n }\n\n /** @internal */\n override afterRender(): void {\n if (!this.isActive) {\n // Remove any existing group header\n const header = this.gridElement?.querySelector('.header');\n const existingGroupRow = header?.querySelector('.header-group-row');\n if (existingGroupRow) existingGroupRow.remove();\n return;\n }\n\n const header = this.gridElement?.querySelector('.header');\n if (!header) return;\n\n // Remove existing group row if present\n const existingGroupRow = header.querySelector('.header-group-row');\n if (existingGroupRow) existingGroupRow.remove();\n\n // Recompute groups from visible columns only (hidden columns have no CSS grid track).\n // This also picks up any plugin-added columns (e.g. expander) that weren't present\n // during processColumns.\n const finalColumns = this.visibleColumns as ColumnConfig[];\n const groups = computeColumnGroups(finalColumns);\n if (groups.length === 0) return;\n\n // Attach per-group renderers from resolved definitions\n if (this.#groupRenderers.size > 0) {\n for (const g of groups) {\n const r = this.#groupRenderers.get(g.id);\n if (r) g.renderer = r;\n }\n }\n\n // Keep #groupEndFields in sync for afterCellRender (covers scheduler-driven renders)\n this.#groupEndFields.clear();\n const precomputed = mergeGroups(groups);\n for (let gi = 0; gi < precomputed.merged.length; gi++) {\n const g = precomputed.merged[gi];\n const lastCol = g.columns[g.columns.length - 1];\n // Don't mark the last group — no adjacent group follows it\n if (lastCol?.field && gi < precomputed.merged.length - 1) {\n this.#groupEndFields.add(lastCol.field);\n }\n }\n\n // Build and insert group header row\n const groupRow = buildGroupHeaderRow(groups, finalColumns, this.config.groupHeaderRenderer, precomputed);\n if (groupRow) {\n // Toggle border visibility class\n groupRow.classList.toggle('no-borders', !this.config.showGroupBorders);\n\n const headerRow = header.querySelector('.header-row');\n if (headerRow) {\n header.insertBefore(groupRow, headerRow);\n } else {\n header.appendChild(groupRow);\n }\n }\n\n // Apply classes to header cells\n const headerRow = header.querySelector('.header-row') as HTMLElement;\n if (headerRow) {\n // Toggle border visibility on header cells\n headerRow.classList.toggle('no-group-borders', !this.config.showGroupBorders);\n applyGroupedHeaderCellClasses(headerRow, groups, finalColumns, precomputed);\n }\n }\n\n /**\n * Apply group-end class to individual cells during render and scroll.\n * This is more efficient than querySelectorAll in afterRender and ensures\n * cells recycled during scroll also get the class applied.\n * @internal\n */\n override afterCellRender(context: AfterCellRenderContext): void {\n if (!this.isActive || !this.config.showGroupBorders) return;\n context.cellElement.classList.toggle('group-end', this.#groupEndFields.has(context.column.field));\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Check if column groups are active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Get the computed column groups.\n * @returns Array of column groups\n */\n getGroups(): ColumnGroup[] {\n return this.groups;\n }\n\n /**\n * Get columns in a specific group (aggregated across all fragments).\n * @param groupId - The group ID to find\n * @returns Array of columns in the group\n */\n getGroupColumns(groupId: string): ColumnConfig[] {\n return this.groups.filter((g) => g.id === groupId).flatMap((g) => g.columns);\n }\n\n /**\n * Refresh column groups (recompute from current columns).\n */\n refresh(): void {\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
@@ -164,11 +164,6 @@ export declare class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfi
164
164
  * @internal
165
165
  */
166
166
  handleQuery(query: PluginQuery): unknown;
167
- /**
168
- * Translate a flat row index to the top-level group index it belongs to.
169
- * Walks flattenedRows up to the given index and counts depth-0 groups encountered.
170
- */
171
- private getTopLevelGroupIndex;
172
167
  /** @internal */
173
168
  attach(grid: GridElement): void;
174
169
  /**
@@ -211,18 +206,10 @@ export declare class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfi
211
206
  renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean;
212
207
  /** @internal */
213
208
  afterRender(): void;
214
- /**
215
- * Collect all group keys from a pre-defined group tree.
216
- */
217
- private collectGroupKeys;
218
209
  /**
219
210
  * Get the active group definitions (pre-defined or empty).
220
211
  */
221
212
  private getActiveGroups;
222
- /**
223
- * Find a group definition by key in the pre-defined group tree.
224
- */
225
- private findGroupDefinition;
226
213
  /**
227
214
  * Create a toggle button for expanding/collapsing a group.
228
215
  */
@@ -241,6 +228,9 @@ export declare class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfi
241
228
  * call defers until the next render rebuilds the group model against the
242
229
  * new `groupOn` (issue #335). The `group-toggle` event is emitted once
243
230
  * the new keys are known.
231
+ *
232
+ * With `ServerSidePlugin` active, every newly expanded pre-defined group
233
+ * requests its rows via `getChildRows()` after the next render (#534).
244
234
  */
245
235
  expandAll(): void;
246
236
  /**
@@ -265,6 +255,8 @@ export declare class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfi
265
255
  isExpanded(key: string): boolean;
266
256
  /**
267
257
  * Expand a specific group.
258
+ * With `ServerSidePlugin` active, an expanded pre-defined group without rows requests
259
+ * them via `getChildRows()` after the next render, like {@link toggle}.
268
260
  * @param key - The group key to expand
269
261
  */
270
262
  expand(key: string): void;
@@ -381,7 +373,8 @@ export declare class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfi
381
373
  * Clear cached row data for one or all groups.
382
374
  *
383
375
  * Use when the server data has changed and groups need to be re-fetched
384
- * on next expand.
376
+ * on next expand. Also cancels any in-flight ServerSide fetch for the
377
+ * cleared groups, so a stale response cannot repopulate them.
385
378
  *
386
379
  * @param groupKey - Specific group key to clear, or omit to clear all
387
380
  */