@toolbox-web/grid 3.8.2 → 3.9.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 (100) hide show
  1. package/all.d.ts +1 -0
  2. package/all.js +1 -1
  3. package/all.js.map +1 -1
  4. package/index.js +1 -1
  5. package/index.js.map +1 -1
  6. package/lib/core/defaults.d.ts +1 -1
  7. package/lib/core/internal/aria.d.ts +1 -1
  8. package/lib/core/internal/config-manager.d.ts +0 -4
  9. package/lib/core/plugin/base-plugin.d.ts +14 -0
  10. package/lib/core/plugin/plugin-manager.d.ts +5 -1
  11. package/lib/core/plugin/types.d.ts +2 -0
  12. package/lib/core/types.d.ts +6 -0
  13. package/lib/features/registry.js +1 -1
  14. package/lib/plugins/clipboard/clipboard-plugin.d.ts +2 -3
  15. package/lib/plugins/clipboard/index.js +1 -1
  16. package/lib/plugins/clipboard/index.js.map +1 -1
  17. package/lib/plugins/column-virtualization/index.js +1 -1
  18. package/lib/plugins/column-virtualization/index.js.map +1 -1
  19. package/lib/plugins/context-menu/index.js +1 -1
  20. package/lib/plugins/context-menu/index.js.map +1 -1
  21. package/lib/plugins/editing/index.js +1 -1
  22. package/lib/plugins/editing/index.js.map +1 -1
  23. package/lib/plugins/export/index.js +1 -1
  24. package/lib/plugins/export/index.js.map +1 -1
  25. package/lib/plugins/filtering/index.js +1 -1
  26. package/lib/plugins/filtering/index.js.map +1 -1
  27. package/lib/plugins/grouping-columns/index.js +1 -1
  28. package/lib/plugins/grouping-columns/index.js.map +1 -1
  29. package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +20 -24
  30. package/lib/plugins/grouping-rows/grouping-rows.d.ts +9 -4
  31. package/lib/plugins/grouping-rows/index.d.ts +1 -1
  32. package/lib/plugins/grouping-rows/index.js +1 -1
  33. package/lib/plugins/grouping-rows/index.js.map +1 -1
  34. package/lib/plugins/grouping-rows/types.d.ts +32 -4
  35. package/lib/plugins/hierarchy/hierarchy-plugin.d.ts +79 -0
  36. package/lib/plugins/hierarchy/hierarchy.d.ts +28 -0
  37. package/lib/plugins/hierarchy/index.d.ts +9 -0
  38. package/lib/plugins/hierarchy/index.js +2 -0
  39. package/lib/plugins/hierarchy/index.js.map +1 -0
  40. package/lib/plugins/hierarchy/types.d.ts +92 -0
  41. package/lib/plugins/master-detail/index.js +1 -1
  42. package/lib/plugins/master-detail/index.js.map +1 -1
  43. package/lib/plugins/multi-sort/index.js +1 -1
  44. package/lib/plugins/multi-sort/index.js.map +1 -1
  45. package/lib/plugins/pinned-columns/index.js +1 -1
  46. package/lib/plugins/pinned-columns/index.js.map +1 -1
  47. package/lib/plugins/pinned-rows/index.js +1 -1
  48. package/lib/plugins/pinned-rows/index.js.map +1 -1
  49. package/lib/plugins/pivot/index.js +1 -1
  50. package/lib/plugins/pivot/index.js.map +1 -1
  51. package/lib/plugins/print/index.js +1 -1
  52. package/lib/plugins/print/index.js.map +1 -1
  53. package/lib/plugins/reorder-columns/index.js +1 -1
  54. package/lib/plugins/reorder-columns/index.js.map +1 -1
  55. package/lib/plugins/responsive/index.js +1 -1
  56. package/lib/plugins/responsive/index.js.map +1 -1
  57. package/lib/plugins/row-drag-drop/index.js +1 -1
  58. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  59. package/lib/plugins/row-drag-drop/row-drag-drop-plugin.d.ts +1 -0
  60. package/lib/plugins/row-drag-drop/types.d.ts +3 -2
  61. package/lib/plugins/selection/index.js +1 -1
  62. package/lib/plugins/selection/index.js.map +1 -1
  63. package/lib/plugins/selection/selection-plugin.d.ts +2 -2
  64. package/lib/plugins/server-side/index.js +1 -1
  65. package/lib/plugins/server-side/index.js.map +1 -1
  66. package/lib/plugins/shell/index.js +1 -1
  67. package/lib/plugins/shell/index.js.map +1 -1
  68. package/lib/plugins/sticky-rows/index.js +1 -1
  69. package/lib/plugins/sticky-rows/index.js.map +1 -1
  70. package/lib/plugins/tooltip/index.js +1 -1
  71. package/lib/plugins/tooltip/index.js.map +1 -1
  72. package/lib/plugins/tree/index.js +1 -1
  73. package/lib/plugins/tree/index.js.map +1 -1
  74. package/lib/plugins/tree/tree-datasource.d.ts +7 -10
  75. package/lib/plugins/tree/tree-plugin.d.ts +23 -3
  76. package/lib/plugins/undo-redo/index.js +1 -1
  77. package/lib/plugins/undo-redo/index.js.map +1 -1
  78. package/lib/plugins/visibility/index.js +1 -1
  79. package/lib/plugins/visibility/index.js.map +1 -1
  80. package/package.json +1 -1
  81. package/umd/grid.all.umd.js +1 -1
  82. package/umd/grid.all.umd.js.map +1 -1
  83. package/umd/grid.umd.js +1 -1
  84. package/umd/grid.umd.js.map +1 -1
  85. package/umd/plugins/clipboard.umd.js +1 -1
  86. package/umd/plugins/clipboard.umd.js.map +1 -1
  87. package/umd/plugins/editing.umd.js +1 -1
  88. package/umd/plugins/editing.umd.js.map +1 -1
  89. package/umd/plugins/grouping-rows.umd.js +1 -1
  90. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  91. package/umd/plugins/hierarchy.umd.js +2 -0
  92. package/umd/plugins/hierarchy.umd.js.map +1 -0
  93. package/umd/plugins/row-drag-drop.umd.js +1 -1
  94. package/umd/plugins/row-drag-drop.umd.js.map +1 -1
  95. package/umd/plugins/selection.umd.js +1 -1
  96. package/umd/plugins/selection.umd.js.map +1 -1
  97. package/umd/plugins/tooltip.umd.js +1 -1
  98. package/umd/plugins/tooltip.umd.js.map +1 -1
  99. package/umd/plugins/tree.umd.js +1 -1
  100. package/umd/plugins/tree.umd.js.map +1 -1
@@ -1,2 +1,2 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("../../core/constants"),require("../../core/internal/aggregators"),require("../../core/internal/aria"),require("../../core/internal/loading"),require("../../core/internal/sanitize"),require("../../core/plugin/base-plugin"),require("../../core/plugin/expander-column")):"function"==typeof define&&define.amd?define(["exports","../../core/constants","../../core/internal/aggregators","../../core/internal/aria","../../core/internal/loading","../../core/internal/sanitize","../../core/plugin/base-plugin","../../core/plugin/expander-column"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_groupingRows={},e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid)}(this,function(e,t,r,o,n,i,s,a){function d({rows:e,config:t,expanded:r,initialExpanded:o,groupSortDirections:n}){const i=t.groupOn;if("function"!=typeof i)return[];const s={key:"__root__",value:null,depth:-1,rows:[],children:new Map};if(e.forEach(e=>{let t=i(e);null==t||!1===t?t=["__ungrouped__"]:Array.isArray(t)||(t=[t]);let r=s;t.forEach((t,o)=>{const n=null==t?"∅":String(t),i="__root__"===r.key?n:r.key+"||"+n;let s=r.children.get(n);s||(s={key:i,value:t,depth:o,rows:[],children:new Map,parent:r},r.children.set(n,s)),s.rows.push(e),r=s})}),1===s.children.size&&s.children.has("__ungrouped__")&&s.children.get("__ungrouped__").rows.length===e.length)return[];const a=new Map;for(let g=0;g<e.length;g++)a.set(e[g],g);const d=new Set([...r,...o??[]]),p=e=>{const t=[...e.children.values()],r=e===s?0:e.depth+1,o=n?.get(r)??1;return t.sort((e,t)=>{const r=e.value,n=t.value;return null==r&&null==n?0:null==r?o:null==n?-o:r>n?o:r<n?-o:0}),t},u=[],l=e=>{if(e===s){for(const t of p(e))l(t);return}const t=d.has(e.key);if(u.push({kind:"group",key:e.key,value:e.value,depth:e.depth,rows:e.rows,expanded:t}),t)if(e.children.size)for(const r of p(e))l(r);else e.rows.forEach(e=>u.push({kind:"data",row:e,rowIndex:a.get(e)??-1}))};return l(s),u}function p(e,t){if(!0===e)return new Set(t);if(!1===e||null==e)return new Set;if("number"==typeof e){const r=t[e];return r?new Set([r]):new Set}return"string"==typeof e?new Set([e]):Array.isArray(e)?new Set(e):new Set}function u({groups:e,expanded:t,groupRows:r,loadingGroups:o,parentPath:n=[]}){const i=[],s=n.length;for(const a of e){const e=[...n,a.key],d=t.has(a.key),p=r.get(a.key)??[],l=o.has(a.key);if(i.push({kind:"group",key:a.key,value:a.value,depth:s,rows:p,expanded:d}),d)if(a.children?.length){const n=u({groups:a.children,expanded:t,groupRows:r,loadingGroups:o,parentPath:e});i.push(...n)}else l?i.push({kind:"data",row:{__loading:!0,__groupKey:a.key},rowIndex:-1}):p.forEach((e,t)=>{i.push({kind:"data",row:e,rowIndex:t})})}return i}function l(e,t,r=[]){for(const o of e){const e=[...r,o.key];if(o.key===t)return e;if(o.children?.length){const r=l(o.children,t,e);if(r.length>0)return r}}return[]}Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var g=class extends s.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:10,onHeaderClick:-1},incompatibleWith:[{name:"tree",reason:"Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid."},{name:"pivot",reason:"PivotPlugin creates its own aggregated row and column structure. Row grouping cannot be applied on top of pivot-generated rows."}],events:[{type:"group-toggle",description:"Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus."},{type:"group-expand",description:"Emitted when a pre-defined group is expanded."},{type:"group-collapse",description:"Emitted when a pre-defined group is collapsed."}],queries:[{type:"canMoveRow",description:"Returns false for group header rows (cannot be reordered)"},{type:"grouping:get-grouped-fields",description:"Returns the column field names that match group depth levels (string[])"},{type:"datasource:viewport-mapping",description:"Translates flat viewport row indices to top-level group indices for ServerSide pagination."}],configRules:[{id:"groupingRows/accordion-defaultExpanded",severity:"warn",message:'"accordion: true" and "defaultExpanded" (non-false) are used together.\n → In accordion mode, only one group can be open at a time.\n → Using defaultExpanded with multiple groups will collapse to one on first toggle.\n → Consider using "defaultExpanded: false" or a single group key/index with accordion mode.',check:e=>!0===e.accordion&&!1!==e.defaultExpanded&&void 0!==e.defaultExpanded&&!("number"==typeof e.defaultExpanded)&&!("string"==typeof e.defaultExpanded)&&(!0===e.defaultExpanded||Array.isArray(e.defaultExpanded)&&e.defaultExpanded.length>1)}]};static dependencies=[{name:"multiSort",required:!1,reason:"Queries sort model for coordinated group sorting"},{name:"serverSide",required:!1,reason:"Consumes datasource events for lazy-loaded grouped data"}];name="groupingRows";styles='@layer tbw-plugins{.group-row{grid-template-columns:var(--tbw-column-template);background:var(--tbw-grouping-rows-bg,var(--tbw-color-panel-bg));border-bottom:var(--tbw-row-divider);min-height:var(--tbw-row-height);font-weight:500;display:grid}.group-row .cell{padding:var(--tbw-cell-padding,.125rem .5rem);align-items:center;display:flex}@media (hover:hover){.group-row:hover{background:var(--tbw-grouping-rows-bg-hover,var(--tbw-color-row-hover))}}.group-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:var(--tbw-toggle-size,1.25rem);height:var(--tbw-toggle-size,1.25rem);font:inherit;background:0 0;border:0;justify-content:center;align-items:center;margin-right:.25rem;display:inline-flex}.group-toggle:hover{background:var(--tbw-grouping-rows-toggle-hover,var(--tbw-color-row-hover));border-radius:var(--tbw-border-radius,.125rem)}.group-toggle{position:relative}.group-toggle:after{content:"";inline-size:var(--tbw-touch-target-min,24px);block-size:var(--tbw-touch-target-min,24px);position:absolute;inset-block-start:50%;inset-inline-start:50%;translate:-50% -50%}@media (pointer:coarse){.group-toggle{min-width:var(--tbw-touch-target-min,24px);min-height:var(--tbw-touch-target-min,24px)}}.group-label{align-items:center;gap:var(--tbw-panel-gap,var(--tbw-spacing-md,.5rem));display:inline-flex}.group-count{color:var(--tbw-grouping-rows-count-color,var(--tbw-color-fg-muted));font-size:var(--tbw-font-size-xs,.85em);font-weight:400}.group-aggregates{align-items:center;gap:var(--tbw-spacing-lg,1rem);margin-left:var(--tbw-spacing-lg,1rem);font-weight:400;font-size:var(--tbw-font-size-sm,.875em);color:var(--tbw-grouping-rows-aggregate-color,var(--tbw-color-fg-muted));display:inline-flex}.group-aggregate{white-space:nowrap}.group-row{padding-left:calc(var(--tbw-group-depth,0) * var(--tbw-group-indent-width,1.25em))}.data-grid-row.tbw-group-slide-in{animation:tbw-group-slide-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-group-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}.data-grid-row.tbw-group-fade-in{animation:tbw-group-fade-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-group-fade-in{0%{opacity:0}to{opacity:1}}}';get defaultConfig(){return{defaultExpanded:!1,showRowCount:!0,indentWidth:20,aggregators:{},animation:"slide",accordion:!1}}expandedKeys=new Set;flattenedRows=[];isActive=!1;previousVisibleKeys=new Set;keysToAnimate=new Set;hasAppliedDefaultExpanded=!1;pendingExpansion;groupConfigDirty=!1;preDefinedGroups=[];groupRowsMap=new Map;loadingGroups=new Set;groupedFields=[];userGroupSortDirections=new Map;flatMeta=[];get animationStyle(){return!!this.isAnimationEnabled&&(this.config.animation??"slide")}detach(){(this.gridElement?.querySelector(".rows-body"))?.setAttribute("role","grid"),this.expandedKeys.clear(),this.flattenedRows=[],this.flatMeta=[],this.isActive=!1,this.previousVisibleKeys.clear(),this.keysToAnimate.clear(),this.hasAppliedDefaultExpanded=!1,this.pendingExpansion=void 0,this.groupConfigDirty=!1,this.preDefinedGroups=[],this.groupRowsMap.clear(),this.loadingGroups.clear(),this.groupedFields=[],this.userGroupSortDirections.clear()}getRowHeight(e,t){if(null!=this.config.groupRowHeight)return!0===e.__isGroupRow?this.config.groupRowHeight:void 0}handleQuery(e){if("canMoveRow"===e.type&&!0===e.context?.__isGroupRow)return!1;if("grouping:get-grouped-fields"===e.type)return[...this.groupedFields];if("datasource:viewport-mapping"===e.type){const{viewportStart:t,viewportEnd:r}=e.context;if(0===this.flattenedRows.length)return;if(0===this.preDefinedGroups.length&&!Array.isArray(this.config.groups))return;const o=this.getActiveGroups();return{startNode:this.getTopLevelGroupIndex(t),endNode:this.getTopLevelGroupIndex(r)+1,totalLoadedNodes:o.length}}}getTopLevelGroupIndex(e){let t=-1;const r=Math.min(e,this.flattenedRows.length-1);for(let o=0;o<=r;o++){const e=this.flattenedRows[o];"group"===e.kind&&0===e.depth&&t++}return Math.max(0,t)}attach(e){super.attach(e),this.on("datasource:data",e=>{const t=e;t.claimed||(t.claimed=!0,this.preDefinedGroups=t.rows,this.groupRowsMap.clear(),this.loadingGroups.clear(),this.hasAppliedDefaultExpanded=!1,this.requestRender())}),this.on("datasource:children",e=>{const t=e;if("grouping-rows"!==t.context?.source)return;t.claimed=!0;const r=t.context.groupKey;r&&(this.groupRowsMap.set(r,t.rows),this.loadingGroups.delete(r),this.requestRender())})}onHeaderClick(e){const t=this.groupedFields.indexOf(e.field);if(-1===t)return;if(!e.column.sortable)return;const r=t,o=this.userGroupSortDirections.get(r)??1;return this.userGroupSortDirections.set(r,1===o?-1:1),this.requestRender(),!0}resolveGroupSortDirections(e){const t=this.config;if("function"!=typeof t.groupOn||0===e.length)return void(this.groupedFields=[]);const r=this.columns.map(e=>e.field),o=function(e,t,r){const o=new Map;if(0===e.length)return o;const n=e[0];let i=t(n);if(null==i||!1===i)return o;Array.isArray(i)||(i=[i]);for(let s=0;s<i.length;s++){const e=i[s];for(const t of r)if(n[t]===e){o.set(s,t);break}}return o}([...e],t.groupOn,r);if(this.groupedFields=[...o.values()],0===o.size)return;const n=new Map(this.userGroupSortDirections);if(n.size<o.size){const e=new Map,t=this.grid?.query?.("sort:get-model",null);if(Array.isArray(t)&&t.length>0){const r=t[0];if(Array.isArray(r))for(const t of r)e.set(t.field,"desc"===t.direction?-1:1)}if(0===e.size){const t=this.grid._sortState;t&&e.set(t.field,t.direction)}for(const[r,i]of o)if(!n.has(r)){const t=e.get(i);void 0!==t&&n.set(r,t)}}return n.size>0?n:void 0}static detect(e,t){return"function"==typeof t?.groupOn||"boolean"==typeof t?.enableRowGrouping||Array.isArray(t?.groups)}processRows(e){const t=this.pendingExpansion;if(this.pendingExpansion=void 0,this.groupConfigDirty=!1,this.preDefinedGroups.length>0||Array.isArray(this.config.groups))return this.preDefinedGroups.length>0||Array.isArray(this.config.groups)&&this.config.groups.length>0?this.processPreDefinedGroups():(this.isActive=!1,this.flattenedRows=[],[]);const r=this.config;if("function"!=typeof r.groupOn)return this.isActive=!1,this.flattenedRows=[],[...e];const o=this.resolveGroupSortDirections(e),n=d({rows:[...e],config:r,expanded:new Set,groupSortDirections:o});if(0===n.length)return this.isActive=!1,this.flattenedRows=[],[...e];let i;const s=void 0!==t;let a;if(s?i=t:this.hasAppliedDefaultExpanded||0!==this.expandedKeys.size||!1===r.defaultExpanded||(i=r.defaultExpanded??!1),void 0!==i){const e=function(e){return e.filter(e=>"group"===e.kind).map(e=>e.key)}(n);a=p(i,e),(s||a.size>0)&&(this.expandedKeys=new Set(a),this.hasAppliedDefaultExpanded=!0)}const u=d({rows:[...e],config:r,expanded:this.expandedKeys,initialExpanded:a,groupSortDirections:o});this.isActive=!0,this.flattenedRows=u,this.computeFlatMeta(),s&&this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.keysToAnimate.clear();const l=new Set;return u.forEach((e,t)=>{if("data"===e.kind){const e=`data-${t}`;l.add(e),this.previousVisibleKeys.has(e)||this.keysToAnimate.add(e)}}),this.previousVisibleKeys=l,u.map(e=>{return"group"===e.kind?{__isGroupRow:!0,__groupKey:e.key,__groupValue:e.value,__groupDepth:e.depth,__groupRows:e.rows,__groupExpanded:e.expanded,__groupRowCount:(t=e,"group"!==t.kind?0:t.rows.length),__rowCacheKey:`group:${e.key}`}:e.row;var t})}onCellClick(e){const r=e.row;if(r?.__isGroupRow&&e.originalEvent.target?.closest(`.${t.GridClasses.GROUP_TOGGLE}`))return this.toggle(r.__groupKey),!0}onKeyDown(e){if(" "!==e.key)return;const t=this.grid._focusRow,r=this.rows[t];return r?.__isGroupRow?(e.preventDefault(),this.toggle(r.__groupKey),this.requestRenderWithFocus(),!0):void 0}renderRow(e,t,r){if(!0===e?.__loading&&e?.__groupKey){t.className="data-grid-row",t.__isCustomRow=!0,t.innerHTML="";const e=document.createElement("div");return e.className="cell",e.style.gridColumn="1 / -1",e.setAttribute("role","gridcell"),e.textContent=" ",t.appendChild(e),(0,n.setRowLoadingState)(t,!0),!0}if(!e?.__isGroupRow)return!1;const o=this.config;if(o.groupRowRenderer){const r=()=>{this.toggle(e.__groupKey)},n=o.groupRowRenderer({key:e.__groupKey,value:e.__groupValue,depth:e.__groupDepth,rows:e.__groupRows,expanded:e.__groupExpanded,toggleExpand:r});if(n)return t.className="data-grid-row group-row",t.__isCustomRow=!0,t.setAttribute("data-group-depth",String(e.__groupDepth)),"string"==typeof n?(0,i.setSanitizedHTML)(t,n):t.replaceChildren(n),!0}const s=()=>{this.toggle(e.__groupKey)};t.className="data-grid-row group-row",t.__isCustomRow=!0,t.setAttribute("data-group-depth",String(e.__groupDepth)),t.setAttribute("role","row"),t.setAttribute("aria-expanded",String(e.__groupExpanded)),t.classList.toggle("tbw-row-expanded",!!e.__groupExpanded);const a=this.flatMeta[r];return a&&(t.setAttribute("aria-level",String(a.level)),t.setAttribute("aria-setsize",String(a.setSize)),t.setAttribute("aria-posinset",String(a.posInSet))),t.style.setProperty("--tbw-group-depth",String(e.__groupDepth||0)),void 0!==o.indentWidth&&t.style.setProperty("--tbw-group-indent-width",`${o.indentWidth}px`),t.style.height="",t.innerHTML="",!1!==o.fullWidth?this.renderFullWidthGroupRow(e,t,s):this.renderPerColumnGroupRow(e,t,s),!0}afterRender(){const e=this.gridElement?.querySelector(".rows");if(!e)return;const t=this.gridElement?.querySelector(".rows-body");t&&"treegrid"!==t.getAttribute("role")&&t.setAttribute("role","treegrid");for(const n of e.querySelectorAll(".data-grid-row:not(.group-row)")){const e=n.querySelector(".cell[data-row]"),t=e?parseInt(e.getAttribute("data-row")??"-1",10):-1,r=this.flatMeta[t];r&&(n.setAttribute("aria-level",String(r.level)),n.setAttribute("aria-setsize",String(r.setSize)),n.setAttribute("aria-posinset",String(r.posInSet)))}const r=this.animationStyle;if(!1===r||0===this.keysToAnimate.size)return;const o="fade"===r?"tbw-group-fade-in":"tbw-group-slide-in";for(const n of e.querySelectorAll(".data-grid-row:not(.group-row)")){const e=n.querySelector(".cell[data-row]"),t=e?parseInt(e.getAttribute("data-row")??"-1",10):-1,r="data"===this.flattenedRows[t]?.kind?`data-${t}`:void 0;r&&this.keysToAnimate.has(r)&&(n.classList.add(o),n.addEventListener("animationend",()=>n.classList.remove(o),{once:!0}))}this.keysToAnimate.clear()}computeFlatMeta(){const e=this.flattenedRows,t=new Array(e.length),r=new Map,o=new Map;let n=null,i=0;for(const p of e)if("group"===p.kind){n&&o.set(n.key,i),n=p,i=0;const e=`${p.key.split("||").slice(0,-1).join("||")}@${p.depth}`;r.set(e,(r.get(e)??0)+1)}else i++;n&&o.set(n.key,i);const s=new Map;let a=null,d=0;for(let p=0;p<e.length;p++){const n=e[p];if("group"===n.kind){const e=`${n.key.split("||").slice(0,-1).join("||")}@${n.depth}`,o=(s.get(e)??0)+1;s.set(e,o),t[p]={level:n.depth+1,setSize:r.get(e)??1,posInSet:o},a=n,d=0}else{d++;const r=a?.depth??-1,n=a?o.get(a.key)??1:e.length;t[p]={level:r+2,setSize:n,posInSet:d}}}this.flatMeta=t}processPreDefinedGroups(){const e=this.preDefinedGroups.length>0?this.preDefinedGroups:Array.isArray(this.config.groups)?this.config.groups:[];if(0===e.length)return this.isActive=!1,this.flattenedRows=[],[];if(!this.hasAppliedDefaultExpanded&&0===this.expandedKeys.size&&!1!==this.config.defaultExpanded){const t=this.collectGroupKeys(e),r=p(this.config.defaultExpanded??!1,t);r.size>0&&(this.expandedKeys=new Set(r),this.hasAppliedDefaultExpanded=!0)}const t=u({groups:e,expanded:this.expandedKeys,groupRows:this.groupRowsMap,loadingGroups:this.loadingGroups});this.isActive=!0,this.flattenedRows=t,this.computeFlatMeta(),this.keysToAnimate.clear();const r=new Set;return t.forEach((e,t)=>{if("data"===e.kind){const e=`data-${t}`;r.add(e),this.previousVisibleKeys.has(e)||this.keysToAnimate.add(e)}}),this.previousVisibleKeys=r,t.map(t=>{if("group"===t.kind){const r=this.findGroupDefinition(e,t.key)?.rowCount??t.rows.length;return{__isGroupRow:!0,__groupKey:t.key,__groupValue:t.value,__groupDepth:t.depth,__groupRows:t.rows,__groupExpanded:t.expanded,__groupRowCount:r,__rowCacheKey:`group:${t.key}`}}return t.row})}collectGroupKeys(e){const t=[];for(const r of e)t.push(r.key),r.children?.length&&t.push(...this.collectGroupKeys(r.children));return t}getActiveGroups(){return this.preDefinedGroups.length>0?this.preDefinedGroups:Array.isArray(this.config.groups)?this.config.groups:[]}findGroupDefinition(e,t){for(const r of e){if(r.key===t)return r;if(r.children?.length){const e=this.findGroupDefinition(r.children,t);if(e)return e}}}createToggleButton(e,r){const o=document.createElement("button");return o.type="button",o.className=`${t.GridClasses.GROUP_TOGGLE}${e?` ${t.GridClasses.EXPANDED}`:""}`,o.setAttribute("aria-label",e?"Collapse group":"Expand group"),this.setIcon(o,e?"collapse":"expand"),o.addEventListener("click",e=>{e.stopPropagation(),r()}),o}getGroupLabelText(e,t,r){const o=this.config;return o.formatLabel?o.formatLabel(e,t,r):String(e)}renderFullWidthGroupRow(e,o,n){const i=this.config,s=i.aggregators??{},a=e.__groupRows??[],d=document.createElement("div");d.className="cell group-full",d.style.gridColumn="1 / -1",d.setAttribute("role","gridcell"),d.setAttribute("data-col","0"),d.appendChild(this.createToggleButton(e.__groupExpanded,n));const p=document.createElement("span");if(p.className=t.GridClasses.GROUP_LABEL,p.textContent=this.getGroupLabelText(e.__groupValue,e.__groupDepth||0,e.__groupKey),d.appendChild(p),!1!==i.showRowCount){const r=document.createElement("span");r.className=t.GridClasses.GROUP_COUNT,r.textContent=`(${e.__groupRowCount??e.__groupRows?.length??0})`,d.appendChild(r)}const u=Object.entries(s);if(u.length>0){const e=document.createElement("span");e.className="group-aggregates";for(const[t,o]of u){const n=this.columns.find(e=>e.field===t),i=r.aggregatorRegistry.run(o,a,t,n);if(null!=i){const r=document.createElement("span");r.className="group-aggregate",r.setAttribute("data-field",t),r.textContent=`${n?.header??t}: ${i}`,e.appendChild(r)}}e.children.length>0&&d.appendChild(e)}o.appendChild(d)}renderPerColumnGroupRow(e,o,n){const i=this.config,s=i.aggregators??{},d=this.columns,p=e.__groupRows??[],u=(this.gridElement?.querySelector(".body"))?.style.gridTemplateColumns||"";u&&(o.style.display="grid",o.style.gridTemplateColumns=u);let l=!1;d.forEach((d,u)=>{const g=document.createElement("div");if(g.className="cell group-cell",g.setAttribute("data-col",String(u)),g.setAttribute("role","gridcell"),(0,a.isExpanderColumn)(d))return g.setAttribute("data-field",d.field),void o.appendChild(g);if(l){const e=s[d.field];if(e){const t=r.aggregatorRegistry.run(e,p,d.field,d);g.textContent=null!=t?String(t):""}else g.textContent=""}else{l=!0,g.appendChild(this.createToggleButton(e.__groupExpanded,n));const o=document.createElement("span"),a=s[d.field];if(a){const t=r.aggregatorRegistry.run(a,p,d.field,d);o.textContent=String(null!=t?t:e.__groupValue)}else o.textContent=this.getGroupLabelText(e.__groupValue,e.__groupDepth||0,e.__groupKey);if(g.appendChild(o),!1!==i.showRowCount){const e=document.createElement("span");e.className=t.GridClasses.GROUP_COUNT,e.textContent=` (${p.length})`,g.appendChild(e)}}o.appendChild(g)})}expandAll(){if(this.groupConfigDirty)return this.pendingExpansion=!0,this.expandedKeys=new Set,void this.requestRender();this.expandedKeys=function(e){const t=new Set;for(const r of e)"group"===r.kind&&t.add(r.key);return t}(this.flattenedRows),this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}collapseAll(){if(this.groupConfigDirty)return this.pendingExpansion=!1,this.expandedKeys=new Set,void this.requestRender();this.expandedKeys=new Set,this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}toggle(e){const t=!this.expandedKeys.has(e),r=this.config,n=this.flattenedRows.find(t=>"group"===t.kind&&t.key===e);if(r.accordion&&t&&n){const t=new Set;for(const r of this.expandedKeys)if(e.startsWith(r+"||")||r.startsWith(e+"||"))e.startsWith(r+"||")&&t.add(r);else{const e=this.flattenedRows.find(e=>"group"===e.kind&&e.key===r);e&&e.depth!==n.depth&&t.add(r)}t.add(e),this.expandedKeys=t}else this.expandedKeys=function(e,t){const r=new Set(e);return r.has(t)?r.delete(t):r.add(t),r}(this.expandedKeys,e);this.broadcast("group-toggle",{key:e,expanded:this.expandedKeys.has(e),value:n?.value,depth:n?.depth??0,expandedKeys:[...this.expandedKeys]});const i=this.getActiveGroups();if(i.length>0){const r=l(i,e);if(t){if(this.emit("group-expand",{groupKey:e,groupPath:r}),!this.groupRowsMap.has(e)){const t=this.findGroupDefinition(i,e);t&&(this.loadingGroups.add(e),this.grid?.query?.("datasource:fetch-children",{context:{source:"grouping-rows",groupKey:e,group:t,groupPath:r}}))}}else this.emit("group-collapse",{groupKey:e,groupPath:r})}const s=this.expandedKeys.has(e),a=null!=n?.value?String(n.value):e;if(s){const e=n?.rows?.length??0;(0,o.announce)(this.gridElement,(0,o.getA11yMessage)(this.gridElement,"groupExpanded",a,e))}else(0,o.announce)(this.gridElement,(0,o.getA11yMessage)(this.gridElement,"groupCollapsed",a));this.requestRender()}isExpanded(e){return this.expandedKeys.has(e)}expand(e){this.expandedKeys.has(e)||(this.expandedKeys=new Set([...this.expandedKeys,e]),this.requestRender())}collapse(e){if(this.expandedKeys.has(e)){const t=new Set(this.expandedKeys);t.delete(e),this.expandedKeys=t,this.requestRender()}}getGroupState(){const e=this.flattenedRows.filter(e=>"group"===e.kind);return{isActive:this.isActive,expandedCount:this.expandedKeys.size,totalGroups:e.length,expandedKeys:[...this.expandedKeys]}}getRowCount(){return this.flattenedRows.length}refreshGroups(){this.requestRender()}getExpandedGroups(){return[...this.expandedKeys]}getFlattenedRows(){return this.flattenedRows}isGroupingActive(){return this.isActive}setGroupOn(e,t){this.config.groupOn=e,this.groupConfigDirty=!0,void 0!==t&&(this.pendingExpansion=t,this.expandedKeys=new Set,this.hasAppliedDefaultExpanded=!1),this.requestRender()}setGroups(e){this.preDefinedGroups=e,this.groupRowsMap.clear(),this.loadingGroups.clear(),this.expandedKeys.clear(),this.hasAppliedDefaultExpanded=!1,this.requestRender()}getGroups(){return this.preDefinedGroups.length>0?[...this.preDefinedGroups]:Array.isArray(this.config.groups)?[...this.config.groups]:[]}setGroupRows(e,t){this.groupRowsMap.set(e,t),this.loadingGroups.delete(e),this.requestRender()}setGroupLoading(e,t){t?this.loadingGroups.add(e):this.loadingGroups.delete(e),this.requestRender()}clearGroupRows(e){null!=e?this.groupRowsMap.delete(e):this.groupRowsMap.clear(),this.requestRender()}};e.GroupingRowsPlugin=g});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("../../core/constants"),require("../../core/internal/aggregators"),require("../../core/internal/aria"),require("../../core/internal/loading"),require("../../core/internal/sanitize"),require("../../core/plugin/base-plugin"),require("../../core/plugin/expander-column")):"function"==typeof define&&define.amd?define(["exports","../../core/constants","../../core/internal/aggregators","../../core/internal/aria","../../core/internal/loading","../../core/internal/sanitize","../../core/plugin/base-plugin","../../core/plugin/expander-column"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_groupingRows={},e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid)}(this,function(e,t,r,o,n,i,s,a){function d(e,t,r=new WeakMap){const o=[];for(const u of["structure","transform"])for(const r of t)r.hierarchyStage===u&&r.beginHierarchy(e)&&o.push(r);if(0===o.length)return[...e];const n=[],i=(e,t,r)=>{const n={parent:t?t.row:null,parentKey:t?.key??null,depth:r};for(const i of o)e=i.processSiblings(e,n);return e},s=(e,t)=>{const o=e.length;for(let a=0;a<o;a++){const d=e[a];n.push(d.row),"object"==typeof d.row&&null!==d.row&&r.set(d.row,{level:t+1,setSize:o,posInSet:a+1});const p=d.children;d.expanded&&p&&s(d.shaped?p:i(p,d,t+1),t+1)}},a=new Array(e.length);for(let u=0;u<e.length;u++)a[u]={row:e[u]};s(i(a,null,0),0);const d=e=>"object"==typeof e&&null!==e?r.get(e):void 0;let p=!1;for(const u of o)u.endHierarchy(n,d)&&(p=!0);if(!p)for(const u of n)"object"==typeof u&&null!==u&&r.delete(u);return n}function p(e,t,r){const o=String(r);e.getAttribute(t)!==o&&e.setAttribute(t,o)}function u(e){return"function"==typeof e.processSiblings}function l(e){return e.constructor.manifest?.hookPriority?.processRows??0}Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var g=class extends s.BaseGridPlugin{static manifest={modifiesRowStructure:!0};name="hierarchy";#e=new WeakMap;detach(){this.gridElement?.querySelector(".rows-body")?.setAttribute("role","grid"),this.#e=new WeakMap}process(e,t){const r=this.#t();return r.includes(t)?r[r.length-1]!==t?e:(this.#e=new WeakMap,d(e,r,this.#e)):(this.#e=new WeakMap,d(e,[t],this.#e))}getRowMeta(e){return"object"==typeof e&&null!==e?this.#e.get(e):void 0}afterRender(){this.#r()}onScrollRender(){this.#r()}#t(){return(this.grid?._pluginManager?.getPlugins?.()??[]).filter(u).sort((e,t)=>l(e)-l(t))}#r(){if(0===this.#t().length)return;const e=this.gridElement?.querySelector(".rows");if(!e)return;let t=!1;for(const n of e.querySelectorAll(".data-grid-row")){const e=this.getRowMeta(n.__rowDataRef);e?(t=!0,p(n,"aria-level",e.level),p(n,"aria-setsize",e.setSize),p(n,"aria-posinset",e.posInSet)):(n.removeAttribute("aria-level"),n.removeAttribute("aria-setsize"),n.removeAttribute("aria-posinset"))}const r=t?"treegrid":"grid",o=this.gridElement?.querySelector(".rows-body");o&&o.getAttribute("role")!==r&&o.setAttribute("role",r)}},h={parent:null,depth:0};function c(e,t){if(!0===e)return new Set(t);if(!1===e||null==e)return new Set;if("number"==typeof e){const r=t[e];return r?new Set([r]):new Set}return"string"==typeof e?new Set([e]):Array.isArray(e)?new Set(e):new Set}function f(e){return"group"!==e.kind?0:e.rows.length}function w({groups:e,expanded:t,groupRows:r,loadingGroups:o,parentPath:n=[]}){const i=[],s=n.length;for(const a of e){const e=[...n,a.key],d=t.has(a.key),p=r.get(a.key)??[],u=o.has(a.key);if(i.push({kind:"group",key:a.key,value:a.value,depth:s,rows:p,expanded:d}),d)if(a.children?.length){const n=w({groups:a.children,expanded:t,groupRows:r,loadingGroups:o,parentPath:e});i.push(...n)}else u?i.push({kind:"data",row:{__loading:!0,__groupKey:a.key},rowIndex:-1}):p.forEach((e,t)=>{i.push({kind:"data",row:e,rowIndex:t})})}return i}function y(e,t,r=[]){for(const o of e){const e=[...r,o.key];if(o.key===t)return e;if(o.children?.length){const r=y(o.children,t,e);if(r.length>0)return r}}return[]}var m=class extends s.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:10,onHeaderClick:-1},incompatibleWith:[{name:"pivot",reason:"PivotPlugin creates its own aggregated row and column structure. Row grouping cannot be applied on top of pivot-generated rows."}],events:[{type:"group-toggle",description:"Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus."},{type:"group-expand",description:"Emitted when a pre-defined group is expanded."},{type:"group-collapse",description:"Emitted when a pre-defined group is collapsed."}],queries:[{type:"canMoveRow",description:"Returns false for group header rows (cannot be reordered)"},{type:"grouping:get-grouped-fields",description:"Returns the column field names that match group depth levels (string[])"},{type:"datasource:viewport-mapping",description:"Translates flat viewport row indices to top-level group indices for ServerSide pagination."}],configRules:[{id:"groupingRows/accordion-defaultExpanded",severity:"warn",message:'"accordion: true" and "defaultExpanded" (non-false) are used together.\n → In accordion mode, only one group can be open at a time.\n → Using defaultExpanded with multiple groups will collapse to one on first toggle.\n → Consider using "defaultExpanded: false" or a single group key/index with accordion mode.',check:e=>!0===e.accordion&&!1!==e.defaultExpanded&&void 0!==e.defaultExpanded&&!("number"==typeof e.defaultExpanded)&&!("string"==typeof e.defaultExpanded)&&(!0===e.defaultExpanded||Array.isArray(e.defaultExpanded)&&e.defaultExpanded.length>1)}]};static dependencies=[{name:"hierarchy",required:!0,reason:"Builds the hierarchical row model",provide:()=>new g},{name:"multiSort",required:!1,reason:"Queries sort model for coordinated group sorting"},{name:"serverSide",required:!1,reason:"Consumes datasource events for lazy-loaded grouped data"}];name="groupingRows";hierarchyStage="transform";styles='@layer tbw-plugins{.group-row{grid-template-columns:var(--tbw-column-template);background:var(--tbw-grouping-rows-bg,var(--tbw-color-panel-bg));border-bottom:var(--tbw-row-divider);min-height:var(--tbw-row-height);font-weight:500;display:grid}.group-row .cell{padding:var(--tbw-cell-padding,.125rem .5rem);align-items:center;display:flex}@media (hover:hover){.group-row:hover{background:var(--tbw-grouping-rows-bg-hover,var(--tbw-color-row-hover))}}.group-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:var(--tbw-toggle-size,1.25rem);height:var(--tbw-toggle-size,1.25rem);font:inherit;background:0 0;border:0;justify-content:center;align-items:center;margin-right:.25rem;display:inline-flex}.group-toggle:hover{background:var(--tbw-grouping-rows-toggle-hover,var(--tbw-color-row-hover));border-radius:var(--tbw-border-radius,.125rem)}.group-toggle{position:relative}.group-toggle:after{content:"";inline-size:var(--tbw-touch-target-min,24px);block-size:var(--tbw-touch-target-min,24px);position:absolute;inset-block-start:50%;inset-inline-start:50%;translate:-50% -50%}@media (pointer:coarse){.group-toggle{min-width:var(--tbw-touch-target-min,24px);min-height:var(--tbw-touch-target-min,24px)}}.group-label{align-items:center;gap:var(--tbw-panel-gap,var(--tbw-spacing-md,.5rem));display:inline-flex}.group-count{color:var(--tbw-grouping-rows-count-color,var(--tbw-color-fg-muted));font-size:var(--tbw-font-size-xs,.85em);font-weight:400}.group-aggregates{align-items:center;gap:var(--tbw-spacing-lg,1rem);margin-left:var(--tbw-spacing-lg,1rem);font-weight:400;font-size:var(--tbw-font-size-sm,.875em);color:var(--tbw-grouping-rows-aggregate-color,var(--tbw-color-fg-muted));display:inline-flex}.group-aggregate{white-space:nowrap}.group-row{padding-left:calc(var(--tbw-group-depth,0) * var(--tbw-group-indent-width,1.25em))}.data-grid-row.tbw-group-slide-in{animation:tbw-group-slide-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-group-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}.data-grid-row.tbw-group-fade-in{animation:tbw-group-fade-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-group-fade-in{0%{opacity:0}to{opacity:1}}}';get defaultConfig(){return{defaultExpanded:!1,showRowCount:!0,indentWidth:20,aggregators:{},animation:"slide",accordion:!1}}expandedKeys=new Set;flattenedRows=[];isActive=!1;previousVisibleKeys=new Set;keysToAnimate=new Set;hasAppliedDefaultExpanded=!1;pendingExpansion;groupConfigDirty=!1;preDefinedGroups=[];groupRowsMap=new Map;loadingGroups=new Set;groupedFields=[];userGroupSortDirections=new Map;#o="groupOn";#n;#i=!1;#s;#a=!1;#d=new WeakMap;#p=!1;#u=new Set;get animationStyle(){return!!this.isAnimationEnabled&&(this.config.animation??"slide")}detach(){this.expandedKeys.clear(),this.flattenedRows=[],this.isActive=!1,this.previousVisibleKeys.clear(),this.keysToAnimate.clear(),this.hasAppliedDefaultExpanded=!1,this.pendingExpansion=void 0,this.groupConfigDirty=!1,this.preDefinedGroups=[],this.groupRowsMap.clear(),this.loadingGroups.clear(),this.groupedFields=[],this.userGroupSortDirections.clear(),this.#u.clear()}getRowHeight(e,t){if(null!=this.config.groupRowHeight)return!0===e.__isGroupRow?this.config.groupRowHeight:void 0}handleQuery(e){if("canMoveRow"===e.type&&!0===e.context?.__isGroupRow)return!1;if("grouping:get-grouped-fields"===e.type)return[...this.groupedFields];if("datasource:viewport-mapping"===e.type){const{viewportStart:t,viewportEnd:r}=e.context;if(0===this.flattenedRows.length)return;if(0===this.preDefinedGroups.length&&!Array.isArray(this.config.groups))return;const o=this.getActiveGroups();return{startNode:this.getTopLevelGroupIndex(t),endNode:this.getTopLevelGroupIndex(r)+1,totalLoadedNodes:o.length}}}getTopLevelGroupIndex(e){let t=-1;const r=Math.min(e,this.flattenedRows.length-1);for(let o=0;o<=r;o++){const e=this.flattenedRows[o];"group"===e.kind&&0===e.depth&&t++}return Math.max(0,t)}attach(e){super.attach(e),this.on("datasource:data",e=>{const t=e;t.claimed||(t.claimed=!0,this.preDefinedGroups=t.rows,this.groupRowsMap.clear(),this.loadingGroups.clear(),this.hasAppliedDefaultExpanded=!1,this.requestRender())}),this.on("datasource:children",e=>{const t=e;if("grouping-rows"!==t.context?.source)return;t.claimed=!0;const r=t.context.groupKey;r&&(this.groupRowsMap.set(r,t.rows),this.loadingGroups.delete(r),this.requestRender())})}onHeaderClick(e){const t=this.groupedFields.indexOf(e.field);if(-1===t)return;if(!e.column.sortable)return;const r=t,o=this.userGroupSortDirections.get(r)??1;return this.userGroupSortDirections.set(r,1===o?-1:1),this.requestRender(),!0}resolveGroupSortDirections(e,t){const r=this.config;if("function"!=typeof r.groupOn||0===e.length)return void(this.groupedFields=[]);const o=this.columns.map(e=>e.field),n=function(e,t,r,o=h){const n=new Map;let i,s=null;for(const a of e)if(s=t(a,o),null!=s&&!1!==s){i=a;break}if(void 0===i)return n;Array.isArray(s)||(s=[s]);for(let a=0;a<s.length;a++){const e=s[a];for(const t of r)if(i[t]===e){n.set(a,t);break}}return n}([...e],r.groupOn,o,t);if(this.groupedFields=[...n.values()],0===n.size)return;const i=new Map(this.userGroupSortDirections);if(i.size<n.size){const e=new Map,t=this.grid?.query?.("sort:get-model",null);if(Array.isArray(t)&&t.length>0){const r=t[0];if(Array.isArray(r))for(const t of r)e.set(t.field,"desc"===t.direction?-1:1)}if(0===e.size){const t=this.grid._sortState;t&&e.set(t.field,t.direction)}for(const[r,o]of n)if(!i.has(r)){const t=e.get(o);void 0!==t&&i.set(r,t)}}return i.size>0?i:void 0}static detect(e,t){return"function"==typeof t?.groupOn||"boolean"==typeof t?.enableRowGrouping||Array.isArray(t?.groups)}processRows(e){const t=this.grid?.getPluginByName?.("hierarchy");return t?t.process(e,this):d(e,[this])}beginHierarchy(e){return this.#n=this.pendingExpansion,this.pendingExpansion=void 0,this.groupConfigDirty=!1,this.#i=!1,this.#p=!1,this.#d=new WeakMap,this.preDefinedGroups.length>0||Array.isArray(this.config.groups)?(this.#o="predefined",!0):"function"!=typeof this.config.groupOn?(this.isActive=!1,this.flattenedRows=[],this.groupedFields=[],!1):(this.#o="groupOn",this.groupedFields=[],this.#s=void 0,this.#a=!1,!0)}processSiblings(e,t){const r=null===t.parentKey&&null===t.parent;if("predefined"===this.#o)return r?this.#l(t.depth):e;const o=this.config,n=e.map(e=>e.row),i={parent:t.parent,depth:t.depth};this.#a||(this.#s=this.resolveGroupSortDirections(n,i),this.#a=this.groupedFields.length>0);const s=(e,s)=>function({rows:e,config:t,expanded:r,initialExpanded:o,groupSortDirections:n,context:i=h,keyPrefix:s=""}){const a=t.groupOn;if("function"!=typeof a)return[];const d={key:"__root__",value:null,depth:-1,rows:[],children:new Map};if(e.forEach(e=>{let t=a(e,i);null==t||!1===t?t=["__ungrouped__"]:Array.isArray(t)||(t=[t]);let r=d;t.forEach((t,o)=>{const n=null==t?"∅":String(t),i="__root__"===r.key?s+n:r.key+"||"+n;let a=r.children.get(n);a||(a={key:i,value:t,depth:o,rows:[],children:new Map,parent:r},r.children.set(n,a)),a.rows.push(e),r=a})}),1===d.children.size&&d.children.has("__ungrouped__")&&d.children.get("__ungrouped__").rows.length===e.length)return[];const p=new Map;for(let h=0;h<e.length;h++)p.set(e[h],h);const u=new Set([...r,...o??[]]),l=e=>{const t=[...e.children.values()],r=e===d?0:e.depth+1,o=n?.get(r)??1;return t.sort((e,t)=>{const r=e.value,n=t.value;return null==r&&null==n?0:null==r?o:null==n?-o:r>n?o:r<n?-o:0}),t},g=[],c=e=>{if(e===d){for(const t of l(e))c(t);return}const t=u.has(e.key);if(g.push({kind:"group",key:e.key,value:e.value,depth:e.depth,rows:e.rows,expanded:t}),t)if(e.children.size)for(const r of l(e))c(r);else e.rows.forEach(e=>g.push({kind:"data",row:e,rowIndex:p.get(e)??-1}))};return c(d),g}({rows:n,config:o,expanded:e,initialExpanded:s,groupSortDirections:this.#s,context:i,keyPrefix:r?"":`${t.parentKey??""}::`}),a=r?this.#g(s):this.#h(s);if(0===a.length)return e;const d=new Map;for(const p of e)d.set(p.row,p);return this.#c(a,t.depth,e=>d.get(e)??{row:e},f)}endHierarchy(e){if(!this.#p)return this.isActive=!1,this.flattenedRows=[],!1;this.isActive=!0,this.flattenedRows=e.map(e=>this.#d.get(e)??{kind:"data",row:e,rowIndex:-1}),this.#i&&this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.keysToAnimate.clear();const t=new Set;return this.flattenedRows.forEach((e,r)=>{if("data"===e.kind){const e=`data-${r}`;t.add(e),this.previousVisibleKeys.has(e)||this.keysToAnimate.add(e)}}),this.previousVisibleKeys=t,!0}#g(e){const t=this.config,r=e(new Set);if(0===r.length)return r;const o=this.#n;let n;const i=void 0!==o;let s;if(i?n=o:this.hasAppliedDefaultExpanded||0!==this.expandedKeys.size||!1===t.defaultExpanded||(n=t.defaultExpanded??!1),void 0!==n){s=c(n,r.filter(e=>"group"===e.kind).map(e=>e.key)),(i||s.size>0)&&(this.expandedKeys=new Set(s),this.hasAppliedDefaultExpanded=!0)}return this.#i=i,e(this.expandedKeys,s)}#h(e){const t=e(this.expandedKeys),r=this.#n;if(void 0===r&&!0!==this.config.defaultExpanded)return t;const o=[];for(const s of t)"group"===s.kind&&0===s.depth&&o.push(s.key);const n=void 0!==r?c(r,o):new Set(o.filter(e=>!this.#u.has(e)));void 0!==r&&(this.#i=!0);let i=!1;for(const s of o)this.#u.add(s),n.has(s)&&!this.expandedKeys.has(s)&&(this.expandedKeys.add(s),i=!0);return i?e(this.expandedKeys):t}#c(e,t,r,o){this.#p=!0;const n=[],i=[];for(const s of e){if("data"===s.kind){const e=r(s.row);(i.length>0?i[i.length-1].children:n).push(e),"object"==typeof s.row&&null!==s.row&&this.#d.set(s.row,s);continue}const e=0===t?s:{...s,depth:t+s.depth},a={__isGroupRow:!0,__groupKey:s.key,__groupValue:s.value,__groupDepth:e.depth,__groupRows:s.rows,__groupExpanded:s.expanded,__groupRowCount:o(s),__rowCacheKey:`group:${s.key}`};this.#d.set(a,e);const d={row:a,key:s.key,expanded:s.expanded,shaped:!0,children:[]};i.length=s.depth,(s.depth>0?i[s.depth-1].children:n).push(d),i.push(d)}return n}onCellClick(e){const r=e.row;if(r?.__isGroupRow&&e.originalEvent.target?.closest(`.${t.GridClasses.GROUP_TOGGLE}`))return this.toggle(r.__groupKey),!0}onKeyDown(e){if(" "!==e.key)return;const t=this.grid._focusRow,r=this.rows[t];return r?.__isGroupRow?(e.preventDefault(),this.toggle(r.__groupKey),this.requestRenderWithFocus(),!0):void 0}renderRow(e,t,r){if(!0===e?.__loading&&e?.__groupKey){t.className="data-grid-row",t.__isCustomRow=!0,t.innerHTML="";const e=document.createElement("div");return e.className="cell",e.style.gridColumn="1 / -1",e.setAttribute("role","gridcell"),e.textContent=" ",t.appendChild(e),(0,n.setRowLoadingState)(t,!0),!0}if(!e?.__isGroupRow)return!1;const o=this.config;if(o.groupRowRenderer){const r=()=>{this.toggle(e.__groupKey)},n=o.groupRowRenderer({key:e.__groupKey,value:e.__groupValue,depth:e.__groupDepth,rows:e.__groupRows,expanded:e.__groupExpanded,toggleExpand:r});if(n)return t.className="data-grid-row group-row",t.__isCustomRow=!0,t.setAttribute("data-group-depth",String(e.__groupDepth)),"string"==typeof n?(0,i.setSanitizedHTML)(t,n):t.replaceChildren(n),!0}const s=()=>{this.toggle(e.__groupKey)};return t.className="data-grid-row group-row",t.__isCustomRow=!0,t.setAttribute("data-group-depth",String(e.__groupDepth)),t.setAttribute("role","row"),t.setAttribute("aria-expanded",String(e.__groupExpanded)),t.classList.toggle("tbw-row-expanded",!!e.__groupExpanded),t.style.setProperty("--tbw-group-depth",String(e.__groupDepth||0)),void 0!==o.indentWidth&&t.style.setProperty("--tbw-group-indent-width",`${o.indentWidth}px`),t.style.height="",t.innerHTML="",!1!==o.fullWidth?this.renderFullWidthGroupRow(e,t,s):this.renderPerColumnGroupRow(e,t,s),!0}afterRender(){const e=this.gridElement?.querySelector(".rows");if(!e)return;const t=this.animationStyle;if(!(!1!==t&&this.keysToAnimate.size>0))return;const r="fade"===t?"tbw-group-fade-in":"tbw-group-slide-in";for(const o of e.querySelectorAll(".data-grid-row:not(.group-row)")){const e=o.querySelector(".cell[data-row]"),t=e?parseInt(e.getAttribute("data-row")??"-1",10):-1;"data"===this.flattenedRows[t]?.kind&&this.keysToAnimate.has(`data-${t}`)&&(o.classList.add(r),o.addEventListener("animationend",()=>o.classList.remove(r),{once:!0}))}this.keysToAnimate.clear()}#l(e){const t=this.getActiveGroups();if(0===t.length)return[];if(!this.hasAppliedDefaultExpanded&&0===this.expandedKeys.size&&!1!==this.config.defaultExpanded){const e=this.collectGroupKeys(t),r=c(this.config.defaultExpanded??!1,e);r.size>0&&(this.expandedKeys=new Set(r),this.hasAppliedDefaultExpanded=!0)}const r=w({groups:t,expanded:this.expandedKeys,groupRows:this.groupRowsMap,loadingGroups:this.loadingGroups});return this.#c(r,e,e=>({row:e}),e=>this.findGroupDefinition(t,e.key)?.rowCount??e.rows.length)}collectGroupKeys(e){const t=[];for(const r of e)t.push(r.key),r.children?.length&&t.push(...this.collectGroupKeys(r.children));return t}getActiveGroups(){return this.preDefinedGroups.length>0?this.preDefinedGroups:Array.isArray(this.config.groups)?this.config.groups:[]}findGroupDefinition(e,t){for(const r of e){if(r.key===t)return r;if(r.children?.length){const e=this.findGroupDefinition(r.children,t);if(e)return e}}}createToggleButton(e,r){const o=document.createElement("button");return o.type="button",o.className=`${t.GridClasses.GROUP_TOGGLE}${e?` ${t.GridClasses.EXPANDED}`:""}`,o.setAttribute("aria-label",e?"Collapse group":"Expand group"),this.setIcon(o,e?"collapse":"expand"),o.addEventListener("click",e=>{e.stopPropagation(),r()}),o}getGroupLabelText(e,t,r){const o=this.config;return o.formatLabel?o.formatLabel(e,t,r):String(e)}renderFullWidthGroupRow(e,o,n){const i=this.config,s=i.aggregators??{},a=e.__groupRows??[],d=document.createElement("div");d.className="cell group-full",d.style.gridColumn="1 / -1",d.setAttribute("role","gridcell"),d.setAttribute("data-col","0"),d.appendChild(this.createToggleButton(e.__groupExpanded,n));const p=document.createElement("span");if(p.className=t.GridClasses.GROUP_LABEL,p.textContent=this.getGroupLabelText(e.__groupValue,e.__groupDepth||0,e.__groupKey),d.appendChild(p),!1!==i.showRowCount){const r=document.createElement("span");r.className=t.GridClasses.GROUP_COUNT,r.textContent=`(${e.__groupRowCount??e.__groupRows?.length??0})`,d.appendChild(r)}const u=Object.entries(s);if(u.length>0){const e=document.createElement("span");e.className="group-aggregates";for(const[t,o]of u){const n=this.columns.find(e=>e.field===t),i=r.aggregatorRegistry.run(o,a,t,n);if(null!=i){const r=document.createElement("span");r.className="group-aggregate",r.setAttribute("data-field",t),r.textContent=`${n?.header??t}: ${i}`,e.appendChild(r)}}e.children.length>0&&d.appendChild(e)}o.appendChild(d)}renderPerColumnGroupRow(e,o,n){const i=this.config,s=i.aggregators??{},d=this.columns,p=e.__groupRows??[],u=(this.gridElement?.querySelector(".body"))?.style.gridTemplateColumns||"";u&&(o.style.display="grid",o.style.gridTemplateColumns=u);let l=!1;d.forEach((d,u)=>{const g=document.createElement("div");if(g.className="cell group-cell",g.setAttribute("data-col",String(u)),g.setAttribute("role","gridcell"),(0,a.isExpanderColumn)(d))return g.setAttribute("data-field",d.field),void o.appendChild(g);if(l){const e=s[d.field];if(e){const t=r.aggregatorRegistry.run(e,p,d.field,d);g.textContent=null!=t?String(t):""}else g.textContent=""}else{l=!0,g.appendChild(this.createToggleButton(e.__groupExpanded,n));const o=document.createElement("span"),a=s[d.field];if(a){const t=r.aggregatorRegistry.run(a,p,d.field,d);o.textContent=String(null!=t?t:e.__groupValue)}else o.textContent=this.getGroupLabelText(e.__groupValue,e.__groupDepth||0,e.__groupKey);if(g.appendChild(o),!1!==i.showRowCount){const e=document.createElement("span");e.className=t.GridClasses.GROUP_COUNT,e.textContent=` (${p.length})`,g.appendChild(e)}}o.appendChild(g)})}expandAll(){if(this.groupConfigDirty)return this.pendingExpansion=!0,this.expandedKeys=new Set,void this.requestRender();this.expandedKeys=function(e){const t=new Set;for(const r of e)"group"===r.kind&&t.add(r.key);return t}(this.flattenedRows),this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}collapseAll(){if(this.groupConfigDirty)return this.pendingExpansion=!1,this.expandedKeys=new Set,void this.requestRender();this.expandedKeys=new Set,this.broadcast("group-toggle",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}toggle(e){const t=!this.expandedKeys.has(e),r=this.config,n=this.flattenedRows.find(t=>"group"===t.kind&&t.key===e);if(("function"==typeof r.accordion?n&&r.accordion(n.depth):r.accordion)&&t&&n){const t=new Set;for(const r of this.expandedKeys)if(e.startsWith(r+"||")||r.startsWith(e+"||"))e.startsWith(r+"||")&&t.add(r);else{const e=this.flattenedRows.find(e=>"group"===e.kind&&e.key===r);e&&e.depth!==n.depth&&t.add(r)}t.add(e),this.expandedKeys=t}else this.expandedKeys=function(e,t){const r=new Set(e);return r.has(t)?r.delete(t):r.add(t),r}(this.expandedKeys,e);this.broadcast("group-toggle",{key:e,expanded:this.expandedKeys.has(e),value:n?.value,depth:n?.depth??0,expandedKeys:[...this.expandedKeys]});const i=this.getActiveGroups();if(i.length>0){const r=y(i,e);if(t){if(this.emit("group-expand",{groupKey:e,groupPath:r}),!this.groupRowsMap.has(e)){const t=this.findGroupDefinition(i,e);t&&(this.loadingGroups.add(e),this.grid?.query?.("datasource:fetch-children",{context:{source:"grouping-rows",groupKey:e,group:t,groupPath:r}}))}}else this.emit("group-collapse",{groupKey:e,groupPath:r})}const s=this.expandedKeys.has(e),a=null!=n?.value?String(n.value):e;if(s){const e=n?.rows?.length??0;(0,o.announce)(this.gridElement,(0,o.getA11yMessage)(this.gridElement,"groupExpanded",a,e))}else(0,o.announce)(this.gridElement,(0,o.getA11yMessage)(this.gridElement,"groupCollapsed",a));this.requestRender()}isExpanded(e){return this.expandedKeys.has(e)}expand(e){this.expandedKeys.has(e)||(this.expandedKeys=new Set([...this.expandedKeys,e]),this.requestRender())}collapse(e){if(this.expandedKeys.has(e)){const t=new Set(this.expandedKeys);t.delete(e),this.expandedKeys=t,this.requestRender()}}getGroupState(){const e=this.flattenedRows.filter(e=>"group"===e.kind);return{isActive:this.isActive,expandedCount:this.expandedKeys.size,totalGroups:e.length,expandedKeys:[...this.expandedKeys]}}getRowCount(){return this.flattenedRows.length}refreshGroups(){this.requestRender()}getExpandedGroups(){return[...this.expandedKeys]}getFlattenedRows(){return this.flattenedRows}isGroupingActive(){return this.isActive}setGroupOn(e,t){this.config.groupOn=e,this.groupConfigDirty=!0,void 0!==t&&(this.pendingExpansion=t,this.expandedKeys=new Set,this.hasAppliedDefaultExpanded=!1),this.requestRender()}setGroups(e){this.preDefinedGroups=e,this.groupRowsMap.clear(),this.loadingGroups.clear(),this.expandedKeys.clear(),this.hasAppliedDefaultExpanded=!1,this.requestRender()}getGroups(){return this.preDefinedGroups.length>0?[...this.preDefinedGroups]:Array.isArray(this.config.groups)?[...this.config.groups]:[]}setGroupRows(e,t){this.groupRowsMap.set(e,t),this.loadingGroups.delete(e),this.requestRender()}setGroupLoading(e,t){t?this.loadingGroups.add(e):this.loadingGroups.delete(e),this.requestRender()}clearGroupRows(e){null!=e?this.groupRowsMap.delete(e):this.groupRowsMap.clear(),this.requestRender()}};e.GroupingRowsPlugin=m});
2
2
  //# sourceMappingURL=grouping-rows.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grouping-rows.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { DefaultExpandedValue, GroupDefinition, GroupingRowsConfig, GroupRowModelItem, RenderRow } from './types';\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n /** Sort direction per group depth level. 1 = ascending, -1 = descending.\n * When omitted, groups at all levels sort ascending. */\n groupSortDirections?: Map<number, 1 | -1>;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n}: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Sort sibling groups by their group value so that group header order is\n // deterministic and respects the active sort direction for each depth level.\n // Default: ascending. When a user sorts a grouped column, the corresponding\n // depth level's direction flips, keeping groups in predictable order.\n const sortedChildren = (node: GroupNode): GroupNode[] => {\n const children = [...node.children.values()];\n // Determine direction for this depth level (children are one level deeper than the node)\n const childDepth = node === root ? 0 : node.depth + 1;\n const dir = groupSortDirections?.get(childDepth) ?? 1;\n children.sort((a, b) => {\n const av = a.value;\n const bv = b.value;\n if (av == null && bv == null) return 0;\n if (av == null) return dir;\n if (bv == null) return -dir;\n return av > bv ? dir : av < bv ? -dir : 0;\n });\n return children;\n };\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n for (const c of sortedChildren(node)) visit(c);\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n for (const c of sortedChildren(node)) visit(c);\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Discover which column field produces the group value at each depth level.\n *\n * Samples the first row's `groupOn` output to get the group path, then checks\n * which column fields produce matching values for that row. This mapping allows\n * the plugin to apply user-invoked column sort directions to the correct group\n * depth levels.\n *\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: (row: any) => any[] | any | null | false,\n columnFields: string[],\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n if (rows.length === 0) return depthToField;\n\n const sampleRow = rows[0];\n let path: any = groupOn(sampleRow);\n if (path == null || path === false) return depthToField;\n if (!Array.isArray(path)) path = [path];\n\n for (let depth = 0; depth < path.length; depth++) {\n const groupValue = path[depth];\n for (const field of columnFields) {\n if (sampleRow[field] === groupValue) {\n depthToField.set(depth, field);\n break;\n }\n }\n }\n return depthToField;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n modifiesRowStructure: true,\n hookPriority: {\n processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\n {\n name: 'tree',\n reason:\n 'Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while ' +\n 'GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid.',\n },\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n {\n type: 'grouping:get-grouped-fields',\n description: 'Returns the column field names that match group depth levels (string[])',\n },\n {\n type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /**\n * Optional dependency on MultiSort for coordinated sort management.\n * When MultiSort is loaded, GroupingRows queries its sort model to determine\n * group header ordering. Without it, falls back to core sort state.\n */\n static override readonly dependencies = [\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Column fields that produce group values (depth 0, 1, ...). Cached for\n * the `grouping:get-grouped-fields` query so MultiSort can filter them out. */\n private groupedFields: string[] = [];\n /** User-specified sort directions per group depth level. Toggled via\n * header clicks on grouped columns. */\n private userGroupSortDirections = new Map<number, 1 | -1>();\n /**\n * Per-flatten-index ARIA position metadata for `aria-level` /\n * `aria-setsize` / `aria-posinset` (WAI-ARIA Treegrid pattern). Computed\n * once per `flattenedRows` rebuild by {@link computeFlatMeta}; consumed\n * by {@link afterRender} on every visible row.\n */\n private flatMeta: Array<{ level: number; setSize: number; posInSet: number }> = [];\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n // Restore default `role=\"grid\"` on the rows-body so the grid stays\n // ARIA-valid after the plugin is removed (template default lives in\n // `core/internal/dom-builder.ts`). See WAI-ARIA Treegrid pattern.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n rowsBody?.setAttribute('role', 'grid');\n\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.flatMeta = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n if (query.type === 'grouping:get-grouped-fields') {\n return [...this.groupedFields];\n }\n if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level group indices for ServerSide pagination\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0) return undefined;\n // Only respond in pre-defined groups mode (when datasource data is being consumed)\n if (this.preDefinedGroups.length === 0 && !Array.isArray(this.config.groups)) return undefined;\n\n const activeGroups = this.getActiveGroups();\n const startNode = this.getTopLevelGroupIndex(viewportStart);\n const endNode = this.getTopLevelGroupIndex(viewportEnd) + 1; // exclusive\n const totalLoadedNodes = activeGroups.length;\n\n return { startNode, endNode, totalLoadedNodes } satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n /**\n * Translate a flat row index to the top-level group index it belongs to.\n * Walks flattenedRows up to the given index and counts depth-0 groups encountered.\n */\n private getTopLevelGroupIndex(flatIndex: number): number {\n let groupIndex = -1;\n const clampedIndex = Math.min(flatIndex, this.flattenedRows.length - 1);\n for (let i = 0; i <= clampedIndex; i++) {\n const row = this.flattenedRows[i];\n if (row.kind === 'group' && row.depth === 0) {\n groupIndex++;\n }\n }\n return Math.max(0, groupIndex);\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail;\n if (!d.claimed) {\n d.claimed = true;\n // Interpret incoming rows as group definitions\n this.preDefinedGroups = d.rows as unknown as GroupDefinition[];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n }\n\n /**\n * Intercept header clicks on grouped columns to toggle group sort direction.\n * Returns `true` for grouped columns to prevent MultiSort from handling them.\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n const fieldIndex = this.groupedFields.indexOf(event.field);\n if (fieldIndex === -1) return; // Not a grouped column — let other plugins handle it\n\n if (!event.column.sortable) return;\n\n // The fieldIndex in groupedFields corresponds to the group depth level\n // (resolveGroupFields populates them in depth order: 0, 1, 2, ...)\n const targetDepth = fieldIndex;\n\n // Toggle direction: asc → desc → asc\n const current = this.userGroupSortDirections.get(targetDepth) ?? 1;\n this.userGroupSortDirections.set(targetDepth, current === 1 ? -1 : 1);\n\n this.requestRender();\n return true; // Prevent MultiSort from handling this column\n }\n // #endregion\n\n // #region Sort State Resolution\n\n /**\n * Build a sort-direction map for each group depth level by cross-referencing\n * the active sort state with the column fields that produce group values.\n *\n * Supports three direction sources (in priority order):\n * 1. User-set directions via header clicks on grouped columns\n * 2. MultiSort plugin model (for backwards compatibility / state restore)\n * 3. Core single-column sort state\n */\n private resolveGroupSortDirections(rows: readonly any[]): Map<number, 1 | -1> | undefined {\n const config = this.config;\n if (typeof config.groupOn !== 'function' || rows.length === 0) {\n this.groupedFields = [];\n return undefined;\n }\n\n // Discover depth → field mapping by sampling rows\n const columnFields = this.columns.map((c) => c.field);\n const depthToField = resolveGroupFields([...rows], config.groupOn, columnFields);\n\n // Cache grouped field names for the grouping:get-grouped-fields query\n this.groupedFields = [...depthToField.values()];\n\n if (depthToField.size === 0) return undefined;\n\n // Start with user-set directions (from header clicks on grouped columns)\n const directions = new Map<number, 1 | -1>(this.userGroupSortDirections);\n\n // Fill missing depths from MultiSort model or core sort state\n if (directions.size < depthToField.size) {\n const activeSorts = new Map<string, 1 | -1>();\n\n const multiSortResults = this.grid?.query?.('sort:get-model', null);\n if (Array.isArray(multiSortResults) && multiSortResults.length > 0) {\n const sortModel = multiSortResults[0] as Array<{ field: string; direction: 'asc' | 'desc' }>;\n if (Array.isArray(sortModel)) {\n for (const entry of sortModel) {\n activeSorts.set(entry.field, entry.direction === 'desc' ? -1 : 1);\n }\n }\n }\n\n if (activeSorts.size === 0) {\n const sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, sortState.direction);\n }\n }\n\n for (const [depth, field] of depthToField) {\n if (!directions.has(depth)) {\n const dir = activeSorts.get(field);\n if (dir !== undefined) {\n directions.set(depth, dir);\n }\n }\n }\n }\n\n return directions.size > 0 ? directions : undefined;\n }\n\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups)\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n const pendingExpansion = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n if (this.preDefinedGroups.length > 0 || (Array.isArray(this.config.groups) && this.config.groups.length > 0)) {\n return this.processPreDefinedGroups();\n }\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n const config = this.config;\n\n // Check if grouping is configured\n if (typeof config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const groupSortDirections = this.resolveGroupSortDirections(rows);\n const initialBuild = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: new Set(), // Empty to get all root groups\n groupSortDirections,\n });\n\n // If no grouping produced, return original rows\n if (initialBuild.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n // Build with proper expanded state\n const grouped = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: this.expandedKeys,\n initialExpanded,\n groupSortDirections,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (isPendingApply) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n // The grid will need to handle group rows specially\n return grouped.map((item) => {\n if (item.kind === 'group') {\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: getGroupRowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // &nbsp; for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!row.__groupExpanded);\n // WAI-ARIA Treegrid: group rows announce their hierarchical position.\n const groupMeta = this.flatMeta[_rowIndex];\n if (groupMeta) {\n rowEl.setAttribute('aria-level', String(groupMeta.level));\n rowEl.setAttribute('aria-setsize', String(groupMeta.setSize));\n rowEl.setAttribute('aria-posinset', String(groupMeta.posInSet));\n }\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n // Hierarchy is now in play \\u2192 swap the rows-body role from `grid` to\n // `treegrid` per WAI-ARIA so per-row level/setsize/posinset are valid\n // in context. Idempotent on the hot path.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== 'treegrid') {\n rowsBody.setAttribute('role', 'treegrid');\n }\n\n // Apply ARIA position metadata to every visible DATA row (group rows are\n // populated by `renderRow` directly since they bypass the default cell\n // template that exposes `data-row`).\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const meta = this.flatMeta[idx];\n if (!meta) continue;\n rowEl.setAttribute('aria-level', String(meta.level));\n rowEl.setAttribute('aria-setsize', String(meta.setSize));\n rowEl.setAttribute('aria-posinset', String(meta.posInSet));\n }\n\n const style = this.animationStyle;\n if (style === false || this.keysToAnimate.size === 0) return;\n\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const item = this.flattenedRows[idx];\n const key = item?.kind === 'data' ? `data-${idx}` : undefined;\n\n if (key && this.keysToAnimate.has(key)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n\n /**\n * Compute per-row ARIA hierarchy metadata (`aria-level` / `aria-setsize` /\n * `aria-posinset`) from `flattenedRows`. Two-pass over the flat list:\n * first pass tallies group siblings per (parent-prefix, depth) and counts\n * the data rows that follow each group header; second pass assigns per-row\n * meta. Called once per `flattenedRows` rebuild (not per render).\n */\n private computeFlatMeta(): void {\n const flat = this.flattenedRows;\n const meta: Array<{ level: number; setSize: number; posInSet: number }> = new Array(flat.length);\n\n // Pass 1: count group siblings per (parent-prefix, depth) and direct\n // data-row children per group (used as `setSize` for data rows).\n const groupSiblingCount = new Map<string, number>();\n const groupDataCount = new Map<string, number>();\n let activeGroup: GroupRowModelItem | null = null;\n let activeDataCount = 0;\n for (const item of flat) {\n if (item.kind === 'group') {\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n activeGroup = item;\n activeDataCount = 0;\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n groupSiblingCount.set(k, (groupSiblingCount.get(k) ?? 0) + 1);\n } else {\n activeDataCount++;\n }\n }\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n\n // Pass 2: assign per-row level/setsize/posinset.\n const groupSiblingPos = new Map<string, number>();\n let currentGroup: GroupRowModelItem | null = null;\n let dataPos = 0;\n for (let i = 0; i < flat.length; i++) {\n const item = flat[i];\n if (item.kind === 'group') {\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n const pos = (groupSiblingPos.get(k) ?? 0) + 1;\n groupSiblingPos.set(k, pos);\n meta[i] = { level: item.depth + 1, setSize: groupSiblingCount.get(k) ?? 1, posInSet: pos };\n currentGroup = item;\n dataPos = 0;\n } else {\n dataPos++;\n const baseDepth = currentGroup?.depth ?? -1;\n const setSize = currentGroup ? (groupDataCount.get(currentGroup.key) ?? 1) : flat.length;\n meta[i] = { level: baseDepth + 2, setSize, posInSet: dataPos };\n }\n }\n\n this.flatMeta = meta;\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the row model from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n private processPreDefinedGroups(): any[] {\n const groups =\n this.preDefinedGroups.length > 0\n ? this.preDefinedGroups\n : Array.isArray(this.config.groups)\n ? this.config.groups\n : [];\n\n if (groups.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Track visible data rows for animation\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n return grouped.map((item) => {\n if (item.kind === 'group') {\n // Look up the pre-defined group to get rowCount from server\n const groupDef = this.findGroupDefinition(groups, item.key);\n const rowCount = groupDef?.rowCount ?? item.rows.length;\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount,\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n\n // In accordion mode, collapse sibling groups when expanding\n if (config.accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n GroupRowModelItem | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Request child rows via unified DataSource if not already cached\n if (!this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.loadingGroups.add(key);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group: groupDef, groupPath },\n } satisfies FetchChildrenQuery);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: ((row: any) => any[] | any | null | false) | undefined, expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"grouping-rows.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy.ts","../../../../../libs/grid/src/lib/plugins/hierarchy/hierarchy-plugin.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Hierarchy Engine\n *\n * Pure row-model builder shared by the HierarchyPlugin and its contributors.\n */\n\nimport type { HierarchyContributor, HierarchyNode, HierarchyRowMeta } from './types';\n\n/**\n * Build the flat row list for `rows` from every contributor's sibling-list\n * transforms, recording each emitted row's ARIA position in `meta`.\n *\n * Contributors whose `beginHierarchy` returns `false` sit out the rebuild.\n * With no active contributor the rows pass through unchanged. When every\n * active contributor's `endHierarchy` returns `false`, no position is kept in `meta`.\n *\n * This is what `HierarchyPlugin.process()` runs for all attached contributors.\n * Call it directly to run a contributor without a grid — e.g. as the\n * `processRows` fallback when no HierarchyPlugin is attached, or in unit tests.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * const hierarchy = this.grid?.getPluginByName('hierarchy');\n * return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n * }\n * ```\n *\n * @param rows - The root rows.\n * @param contributors - Contributors to run, in `processRows` hook order.\n * @param meta - Receives each emitted row's position. Pass a fresh map per rebuild.\n * @returns The flattened rows, in render order.\n * @since 3.9.0\n */\nexport function buildHierarchy(\n rows: readonly unknown[],\n contributors: readonly HierarchyContributor[],\n meta: WeakMap<object, HierarchyRowMeta> = new WeakMap(),\n): unknown[] {\n const active: HierarchyContributor[] = [];\n for (const stage of ['structure', 'transform'] as const) {\n for (const c of contributors) {\n if (c.hierarchyStage === stage && c.beginHierarchy(rows)) active.push(c);\n }\n }\n if (active.length === 0) return [...rows];\n\n const out: unknown[] = [];\n const process = (nodes: HierarchyNode[], parent: HierarchyNode | null, depth: number): HierarchyNode[] => {\n const context = { parent: parent ? parent.row : null, parentKey: parent?.key ?? null, depth };\n for (const c of active) nodes = c.processSiblings(nodes, context);\n return nodes;\n };\n const emit = (nodes: HierarchyNode[], depth: number): void => {\n const setSize = nodes.length;\n for (let i = 0; i < setSize; i++) {\n const node = nodes[i];\n out.push(node.row);\n if (typeof node.row === 'object' && node.row !== null) {\n meta.set(node.row, { level: depth + 1, setSize, posInSet: i + 1 });\n }\n const children = node.children;\n if (node.expanded && children) {\n emit(node.shaped ? children : process(children, node, depth + 1), depth + 1);\n }\n }\n };\n\n const roots: HierarchyNode[] = new Array(rows.length);\n for (let i = 0; i < rows.length; i++) roots[i] = { row: rows[i] };\n emit(process(roots, null, 0), 0);\n\n const positionOf = (row: unknown) => (typeof row === 'object' && row !== null ? meta.get(row) : undefined);\n let shaped = false;\n for (const c of active) if (c.endHierarchy(out, positionOf)) shaped = true;\n if (!shaped) {\n for (const row of out) if (typeof row === 'object' && row !== null) meta.delete(row);\n }\n return out;\n}\n","/**\n * Hierarchy Plugin\n *\n * Owns the hierarchical row model shared by structure-contributing plugins.\n */\n\nimport type { PluginManifest } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { RowElementInternal } from '../../core/types';\nimport { buildHierarchy } from './hierarchy';\nimport type { HierarchyContributor, HierarchyRowMeta } from './types';\n\n/** Attribute write that skips the DOM mutation when the value is unchanged (per-frame hot path). */\nfunction setAttrIfChanged(el: Element, name: string, value: number): void {\n const next = String(value);\n if (el.getAttribute(name) !== next) el.setAttribute(name, next);\n}\n\nfunction isContributor(plugin: unknown): plugin is HierarchyContributor & BaseGridPlugin {\n return typeof (plugin as Partial<HierarchyContributor>).processSiblings === 'function';\n}\n\nfunction processRowsPriority(plugin: BaseGridPlugin): number {\n return (plugin.constructor as typeof BaseGridPlugin).manifest?.hookPriority?.processRows ?? 0;\n}\n\n/**\n * Hierarchy Plugin for tbw-grid\n *\n * Builds one hierarchical row model from every attached structure contributor\n * — {@link https://toolboxjs.com/grid/plugins/tree/ | TreePlugin} discovers\n * child rows, {@link https://toolboxjs.com/grid/plugins/grouping-rows/ | GroupingRowsPlugin}\n * groups sibling lists — and announces each row's position with the WAI-ARIA\n * treegrid pattern (`aria-level` / `aria-setsize` / `aria-posinset`).\n *\n * Contributors know nothing about each other, so they compose: a grid with\n * both Tree and GroupingRows groups the children of every tree level.\n *\n * You never need to add this plugin yourself — TreePlugin and\n * GroupingRowsPlugin declare it as a dependency and the grid attaches it\n * automatically. It has no configuration.\n *\n * @example Group the children of every tree node\n * ```ts\n * import { TreePlugin } from '@toolbox-web/grid/plugins/tree';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * grid.gridConfig = {\n * plugins: [\n * new TreePlugin({ childrenField: 'children' }),\n * new GroupingRowsPlugin({\n * // Leave the root level ungrouped, group every child level by kind\n * groupOn: (row, { depth }) => (depth > 0 ? row.kind : null),\n * }),\n * ],\n * };\n * ```\n *\n * @since 3.9.0\n */\nexport class HierarchyPlugin extends BaseGridPlugin {\n /** @internal */\n static override readonly manifest: PluginManifest = {\n modifiesRowStructure: true,\n };\n\n /** @internal */\n readonly name = 'hierarchy';\n\n #meta = new WeakMap<object, HierarchyRowMeta>();\n\n /** @internal */\n override detach(): void {\n // Restore the default `role=\"grid\"` (template default in `core/internal/dom-builder.ts`).\n this.gridElement?.querySelector('.rows-body')?.setAttribute('role', 'grid');\n this.#meta = new WeakMap();\n }\n\n /**\n * Build the row model for a contributor's `processRows` hook.\n *\n * Every contributor forwards its hook here; only the one that runs last in\n * the `processRows` pipeline builds the model (for all contributors at\n * once) — the earlier ones pass their rows through untouched.\n *\n * @example\n * ```ts\n * override processRows(rows: readonly unknown[]): unknown[] {\n * return this.grid.getPluginByName('hierarchy')?.process(rows, this) ?? [...rows];\n * }\n * ```\n *\n * @since 3.9.0\n */\n process(rows: readonly unknown[], caller: HierarchyContributor): unknown[] {\n const contributors = this.#contributors();\n if (!contributors.includes(caller as HierarchyContributor & BaseGridPlugin)) {\n this.#meta = new WeakMap();\n return buildHierarchy(rows, [caller], this.#meta);\n }\n if (contributors[contributors.length - 1] !== caller) return rows as unknown[];\n this.#meta = new WeakMap();\n return buildHierarchy(rows, contributors, this.#meta);\n }\n\n /**\n * Hierarchy position of a row in the current row model, or `undefined` when\n * the row is not rendered (e.g. collapsed under a parent).\n *\n * @example\n * ```ts\n * const meta = grid.getPluginByName('hierarchy')?.getRowMeta(grid.rows[0]);\n * console.log(meta?.level, meta?.posInSet, meta?.setSize);\n * ```\n *\n * @since 3.9.0\n */\n getRowMeta(row: unknown): HierarchyRowMeta | undefined {\n return typeof row === 'object' && row !== null ? this.#meta.get(row) : undefined;\n }\n\n /** @internal */\n override afterRender(): void {\n this.#applyAria();\n }\n\n /** @internal */\n override onScrollRender(): void {\n this.#applyAria();\n }\n\n /** Attached contributors, in `processRows` hook order. */\n #contributors(): Array<HierarchyContributor & BaseGridPlugin> {\n const plugins = (this.grid?._pluginManager?.getPlugins?.() ?? []) as readonly BaseGridPlugin[];\n return plugins.filter(isContributor).sort((a, b) => processRowsPriority(a) - processRowsPriority(b));\n }\n\n #applyAria(): void {\n if (this.#contributors().length === 0) return;\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n let hierarchical = false;\n for (const rowEl of body.querySelectorAll('.data-grid-row')) {\n const meta = this.getRowMeta((rowEl as RowElementInternal).__rowDataRef);\n if (!meta) {\n // Pooled row elements may carry a position from a previous row model.\n rowEl.removeAttribute('aria-level');\n rowEl.removeAttribute('aria-setsize');\n rowEl.removeAttribute('aria-posinset');\n continue;\n }\n hierarchical = true;\n setAttrIfChanged(rowEl, 'aria-level', meta.level);\n setAttrIfChanged(rowEl, 'aria-setsize', meta.setSize);\n setAttrIfChanged(rowEl, 'aria-posinset', meta.posInSet);\n }\n\n // `treegrid` only while the model has hierarchy, so per-row level/setsize/posinset are valid in context.\n const role = hierarchical ? 'treegrid' : 'grid';\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== role) rowsBody.setAttribute('role', role);\n }\n}\n","/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type {\n DefaultExpandedValue,\n GroupDefinition,\n GroupingRowsConfig,\n GroupOnContext,\n GroupRowModelItem,\n RenderRow,\n} from './types';\n\n/** Context for the root row list (no parent). */\nconst ROOT_CONTEXT: GroupOnContext = { parent: null, depth: 0 };\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n /** Sort direction per group depth level. 1 = ascending, -1 = descending.\n * When omitted, groups at all levels sort ascending. */\n groupSortDirections?: Map<number, 1 | -1>;\n /** Passed to `groupOn` as its second argument (default: the root list). */\n context?: GroupOnContext;\n /** Prepended to every top-level group key so nested lists get unique keys. */\n keyPrefix?: string;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n context = ROOT_CONTEXT,\n keyPrefix = '',\n}: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r, context);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? keyPrefix + seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Sort sibling groups by their group value so that group header order is\n // deterministic and respects the active sort direction for each depth level.\n // Default: ascending. When a user sorts a grouped column, the corresponding\n // depth level's direction flips, keeping groups in predictable order.\n const sortedChildren = (node: GroupNode): GroupNode[] => {\n const children = [...node.children.values()];\n // Determine direction for this depth level (children are one level deeper than the node)\n const childDepth = node === root ? 0 : node.depth + 1;\n const dir = groupSortDirections?.get(childDepth) ?? 1;\n children.sort((a, b) => {\n const av = a.value;\n const bv = b.value;\n if (av == null && bv == null) return 0;\n if (av == null) return dir;\n if (bv == null) return -dir;\n return av > bv ? dir : av < bv ? -dir : 0;\n });\n return children;\n };\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n for (const c of sortedChildren(node)) visit(c);\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n for (const c of sortedChildren(node)) visit(c);\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Discover which column field produces the group value at each depth level.\n *\n * Samples the first grouped row's `groupOn` output to get the group path, then checks\n * which column fields produce matching values for that row. This mapping allows\n * the plugin to apply user-invoked column sort directions to the correct group\n * depth levels.\n *\n * @param context - Passed to `groupOn` (default: the root list).\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: NonNullable<GroupingRowsConfig['groupOn']>,\n columnFields: string[],\n context: GroupOnContext = ROOT_CONTEXT,\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n let sampleRow: any;\n let path: any = null;\n for (const row of rows) {\n path = groupOn(row, context);\n if (path != null && path !== false) {\n sampleRow = row;\n break;\n }\n }\n if (sampleRow === undefined) return depthToField;\n if (!Array.isArray(path)) path = [path];\n\n for (let depth = 0; depth < path.length; depth++) {\n const groupValue = path[depth];\n for (const field of columnFields) {\n if (sampleRow[field] === groupValue) {\n depthToField.set(depth, field);\n break;\n }\n }\n }\n return depthToField;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport type { HierarchyContributor, HierarchyNode, HierarchySiblingContext } from '../hierarchy';\nimport { buildHierarchy, HierarchyPlugin } from '../hierarchy';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupOnContext,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> implements HierarchyContributor {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n modifiesRowStructure: true,\n hookPriority: {\n processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n {\n type: 'grouping:get-grouped-fields',\n description: 'Returns the column field names that match group depth levels (string[])',\n },\n {\n type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /**\n * Optional dependency on MultiSort for coordinated sort management.\n * When MultiSort is loaded, GroupingRows queries its sort model to determine\n * group header ordering. Without it, falls back to core sort state.\n */\n static override readonly dependencies = [\n {\n name: 'hierarchy',\n required: true,\n reason: 'Builds the hierarchical row model',\n provide: () => new HierarchyPlugin(),\n },\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n readonly hierarchyStage = 'transform';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Column fields that produce group values (depth 0, 1, ...). Cached for\n * the `grouping:get-grouped-fields` query so MultiSort can filter them out. */\n private groupedFields: string[] = [];\n /** User-specified sort directions per group depth level. Toggled via\n * header clicks on grouped columns. */\n private userGroupSortDirections = new Map<number, 1 | -1>();\n\n // Per-rebuild state, set in `beginHierarchy`.\n #mode: 'groupOn' | 'predefined' = 'groupOn';\n #pending: DefaultExpandedValue | undefined;\n #pendingApplied = false;\n #groupSortDirections: Map<number, 1 | -1> | undefined;\n /** Set once a sibling list yielded the grouped column fields (nested-only grouping skips the root list). */\n #groupFieldsResolved = false;\n /** Render-model item of every row emitted by this rebuild, keyed by row object. */\n #renderItems = new WeakMap<object, RenderRow>();\n #anyGroup = false;\n /** Top-level keys of nested (tree child) lists already seen, for `defaultExpanded: true`. */\n #seenNestedKeys = new Set<string>();\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n this.#seenNestedKeys.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n if (query.type === 'grouping:get-grouped-fields') {\n return [...this.groupedFields];\n }\n if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level group indices for ServerSide pagination\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0) return undefined;\n // Only respond in pre-defined groups mode (when datasource data is being consumed)\n if (this.preDefinedGroups.length === 0 && !Array.isArray(this.config.groups)) return undefined;\n\n const activeGroups = this.getActiveGroups();\n const startNode = this.getTopLevelGroupIndex(viewportStart);\n const endNode = this.getTopLevelGroupIndex(viewportEnd) + 1; // exclusive\n const totalLoadedNodes = activeGroups.length;\n\n return { startNode, endNode, totalLoadedNodes } satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n /**\n * Translate a flat row index to the top-level group index it belongs to.\n * Walks flattenedRows up to the given index and counts depth-0 groups encountered.\n */\n private getTopLevelGroupIndex(flatIndex: number): number {\n let groupIndex = -1;\n const clampedIndex = Math.min(flatIndex, this.flattenedRows.length - 1);\n for (let i = 0; i <= clampedIndex; i++) {\n const row = this.flattenedRows[i];\n if (row.kind === 'group' && row.depth === 0) {\n groupIndex++;\n }\n }\n return Math.max(0, groupIndex);\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail;\n if (!d.claimed) {\n d.claimed = true;\n // Interpret incoming rows as group definitions\n this.preDefinedGroups = d.rows as unknown as GroupDefinition[];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n }\n\n /**\n * Intercept header clicks on grouped columns to toggle group sort direction.\n * Returns `true` for grouped columns to prevent MultiSort from handling them.\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n const fieldIndex = this.groupedFields.indexOf(event.field);\n if (fieldIndex === -1) return; // Not a grouped column — let other plugins handle it\n\n if (!event.column.sortable) return;\n\n // The fieldIndex in groupedFields corresponds to the group depth level\n // (resolveGroupFields populates them in depth order: 0, 1, 2, ...)\n const targetDepth = fieldIndex;\n\n // Toggle direction: asc → desc → asc\n const current = this.userGroupSortDirections.get(targetDepth) ?? 1;\n this.userGroupSortDirections.set(targetDepth, current === 1 ? -1 : 1);\n\n this.requestRender();\n return true; // Prevent MultiSort from handling this column\n }\n // #endregion\n\n // #region Sort State Resolution\n\n /**\n * Build a sort-direction map for each group depth level by cross-referencing\n * the active sort state with the column fields that produce group values.\n *\n * Supports three direction sources (in priority order):\n * 1. User-set directions via header clicks on grouped columns\n * 2. MultiSort plugin model (for backwards compatibility / state restore)\n * 3. Core single-column sort state\n */\n private resolveGroupSortDirections(rows: readonly any[], context: GroupOnContext): Map<number, 1 | -1> | undefined {\n const config = this.config;\n if (typeof config.groupOn !== 'function' || rows.length === 0) {\n this.groupedFields = [];\n return undefined;\n }\n\n // Discover depth → field mapping by sampling rows\n const columnFields = this.columns.map((c) => c.field);\n const depthToField = resolveGroupFields([...rows], config.groupOn, columnFields, context);\n\n // Cache grouped field names for the grouping:get-grouped-fields query\n this.groupedFields = [...depthToField.values()];\n\n if (depthToField.size === 0) return undefined;\n\n // Start with user-set directions (from header clicks on grouped columns)\n const directions = new Map<number, 1 | -1>(this.userGroupSortDirections);\n\n // Fill missing depths from MultiSort model or core sort state\n if (directions.size < depthToField.size) {\n const activeSorts = new Map<string, 1 | -1>();\n\n const multiSortResults = this.grid?.query?.('sort:get-model', null);\n if (Array.isArray(multiSortResults) && multiSortResults.length > 0) {\n const sortModel = multiSortResults[0] as Array<{ field: string; direction: 'asc' | 'desc' }>;\n if (Array.isArray(sortModel)) {\n for (const entry of sortModel) {\n activeSorts.set(entry.field, entry.direction === 'desc' ? -1 : 1);\n }\n }\n }\n\n if (activeSorts.size === 0) {\n const sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, sortState.direction);\n }\n }\n\n for (const [depth, field] of depthToField) {\n if (!directions.has(depth)) {\n const dir = activeSorts.get(field);\n if (dir !== undefined) {\n directions.set(depth, dir);\n }\n }\n }\n }\n\n return directions.size > 0 ? directions : undefined;\n }\n\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups)\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n const hierarchy = this.grid?.getPluginByName?.('hierarchy');\n return hierarchy ? hierarchy.process(rows, this) : buildHierarchy(rows, [this]);\n }\n\n /** @internal Hierarchy contributor hook */\n beginHierarchy(_rows: readonly unknown[]): boolean {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n this.#pending = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.#pendingApplied = false;\n this.#anyGroup = false;\n this.#renderItems = new WeakMap();\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n this.#mode = 'predefined';\n return true;\n }\n\n if (typeof this.config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n this.groupedFields = [];\n return false;\n }\n this.#mode = 'groupOn';\n this.groupedFields = [];\n this.#groupSortDirections = undefined;\n this.#groupFieldsResolved = false;\n return true;\n }\n\n /** @internal Hierarchy contributor hook */\n processSiblings(nodes: HierarchyNode[], context: HierarchySiblingContext): HierarchyNode[] {\n const isRoot = context.parentKey === null && context.parent === null;\n if (this.#mode === 'predefined') return isRoot ? this.#preDefinedNodes(context.depth) : nodes;\n\n const config = this.config;\n const rows = nodes.map((node) => node.row);\n const groupOnContext = { parent: context.parent, depth: context.depth };\n if (!this.#groupFieldsResolved) {\n this.#groupSortDirections = this.resolveGroupSortDirections(rows, groupOnContext);\n this.#groupFieldsResolved = this.groupedFields.length > 0;\n }\n const build = (expanded: Set<string>, initialExpanded?: Set<string>) =>\n buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections: this.#groupSortDirections,\n context: groupOnContext,\n keyPrefix: isRoot ? '' : `${context.parentKey ?? ''}::`,\n });\n\n const grouped = isRoot ? this.#buildRootGroups(build) : this.#buildNestedGroups(build);\n if (grouped.length === 0) return nodes;\n\n const byRow = new Map<unknown, HierarchyNode>();\n for (const node of nodes) byRow.set(node.row, node);\n return this.#nest(grouped, context.depth, (row) => byRow.get(row) ?? { row }, getGroupRowCount);\n }\n\n /** @internal Hierarchy contributor hook */\n endHierarchy(rows: readonly unknown[]): boolean {\n if (!this.#anyGroup) {\n this.isActive = false;\n this.flattenedRows = [];\n return false;\n }\n this.isActive = true;\n this.flattenedRows = rows.map((row) => this.#renderItems.get(row as object) ?? { kind: 'data', row, rowIndex: -1 });\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (this.#pendingApplied) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n this.flattenedRows.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n return true;\n }\n\n /** Group the root rows, resolving `defaultExpanded` / a pending expansion against the fresh group keys. */\n #buildRootGroups(build: (expanded: Set<string>, initialExpanded?: Set<string>) => RenderRow[]): RenderRow[] {\n const config = this.config;\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const initialBuild = build(new Set());\n if (initialBuild.length === 0) return initialBuild;\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n const pendingExpansion = this.#pending;\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n this.#pendingApplied = isPendingApply;\n\n // Build with proper expanded state\n return build(this.expandedKeys, initialExpanded);\n }\n\n /**\n * Group a tree node's children. A pending expansion (`setGroupOn(fn, expanded)`, deferred\n * `expandAll`/`collapseAll`) resolves against each list's own groups; otherwise\n * `defaultExpanded: true` opens each list's new top-level groups.\n */\n #buildNestedGroups(build: (expanded: Set<string>) => RenderRow[]): RenderRow[] {\n const grouped = build(this.expandedKeys);\n const pending = this.#pending;\n if (pending === undefined && this.config.defaultExpanded !== true) return grouped;\n const listKeys: string[] = [];\n for (const item of grouped) if (item.kind === 'group' && item.depth === 0) listKeys.push(item.key);\n const open =\n pending !== undefined\n ? resolveDefaultExpanded(pending, listKeys)\n : new Set(listKeys.filter((key) => !this.#seenNestedKeys.has(key)));\n if (pending !== undefined) this.#pendingApplied = true;\n let added = false;\n for (const key of listKeys) {\n this.#seenNestedKeys.add(key);\n if (open.has(key) && !this.expandedKeys.has(key)) {\n this.expandedKeys.add(key);\n added = true;\n }\n }\n return added ? build(this.expandedKeys) : grouped;\n }\n\n /**\n * Re-nest a flat group model (visible rows only) into hierarchy nodes,\n * creating the synthetic group-header row objects.\n */\n #nest(\n flat: RenderRow[],\n baseDepth: number,\n dataNode: (row: unknown) => HierarchyNode,\n rowCount: (item: GroupRowModelItem) => number,\n ): HierarchyNode[] {\n this.#anyGroup = true;\n const roots: HierarchyNode[] = [];\n const open: HierarchyNode[] = [];\n for (const item of flat) {\n if (item.kind === 'data') {\n const node = dataNode(item.row);\n (open.length > 0 ? (open[open.length - 1].children as HierarchyNode[]) : roots).push(node);\n if (typeof item.row === 'object' && item.row !== null) this.#renderItems.set(item.row, item);\n continue;\n }\n const groupItem: GroupRowModelItem = baseDepth === 0 ? item : { ...item, depth: baseDepth + item.depth };\n const row = {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: groupItem.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n this.#renderItems.set(row, groupItem);\n const node: HierarchyNode = { row, key: item.key, expanded: item.expanded, shaped: true, children: [] };\n open.length = item.depth;\n (item.depth > 0 ? (open[item.depth - 1].children as HierarchyNode[]) : roots).push(node);\n open.push(node);\n }\n return roots;\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // &nbsp; for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!row.__groupExpanded);\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n const style = this.animationStyle;\n const animate = style !== false && this.keysToAnimate.size > 0;\n if (!animate) return;\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n if (this.flattenedRows[idx]?.kind === 'data' && this.keysToAnimate.has(`data-${idx}`)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the root nodes from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n #preDefinedNodes(baseDepth: number): HierarchyNode[] {\n const groups = this.getActiveGroups();\n if (groups.length === 0) return [];\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n // Prefer the server-reported row count over the loaded rows.\n return this.#nest(\n grouped,\n baseDepth,\n (row) => ({ row }),\n (item) => this.findGroupDefinition(groups, item.key)?.rowCount ?? item.rows.length,\n );\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n const accordion =\n typeof config.accordion === 'function' ? !!group && config.accordion(group.depth) : config.accordion;\n\n // In accordion mode, collapse sibling groups when expanding\n if (accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n GroupRowModelItem | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Request child rows via unified DataSource if not already cached\n if (!this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.loadingGroups.add(key);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group: groupDef, groupPath },\n } satisfies FetchChildrenQuery);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: GroupingRowsConfig['groupOn'], expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}