@toolbox-web/grid 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/all.js +2 -2
  2. package/all.js.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/lib/core/constants.d.ts +18 -0
  6. package/lib/core/internal/value-accessor.d.ts +33 -0
  7. package/lib/core/types.d.ts +114 -0
  8. package/lib/plugins/clipboard/index.js +1 -1
  9. package/lib/plugins/clipboard/index.js.map +1 -1
  10. package/lib/plugins/column-virtualization/index.js.map +1 -1
  11. package/lib/plugins/context-menu/index.js.map +1 -1
  12. package/lib/plugins/editing/index.js +1 -1
  13. package/lib/plugins/editing/index.js.map +1 -1
  14. package/lib/plugins/export/index.js +1 -1
  15. package/lib/plugins/export/index.js.map +1 -1
  16. package/lib/plugins/filtering/FilteringPlugin.d.ts +19 -1
  17. package/lib/plugins/filtering/index.js +1 -1
  18. package/lib/plugins/filtering/index.js.map +1 -1
  19. package/lib/plugins/grouping-columns/index.js.map +1 -1
  20. package/lib/plugins/grouping-rows/index.js +2 -2
  21. package/lib/plugins/grouping-rows/index.js.map +1 -1
  22. package/lib/plugins/master-detail/index.js.map +1 -1
  23. package/lib/plugins/multi-sort/index.js +1 -1
  24. package/lib/plugins/multi-sort/index.js.map +1 -1
  25. package/lib/plugins/pinned-columns/index.js.map +1 -1
  26. package/lib/plugins/pinned-rows/index.js +1 -1
  27. package/lib/plugins/pinned-rows/index.js.map +1 -1
  28. package/lib/plugins/pivot/index.js.map +1 -1
  29. package/lib/plugins/print/index.js.map +1 -1
  30. package/lib/plugins/reorder-columns/index.js.map +1 -1
  31. package/lib/plugins/reorder-rows/index.js.map +1 -1
  32. package/lib/plugins/responsive/index.js.map +1 -1
  33. package/lib/plugins/selection/index.js.map +1 -1
  34. package/lib/plugins/server-side/ServerSidePlugin.d.ts +4 -0
  35. package/lib/plugins/server-side/index.js +1 -1
  36. package/lib/plugins/server-side/index.js.map +1 -1
  37. package/lib/plugins/server-side/types.d.ts +48 -0
  38. package/lib/plugins/tooltip/index.js.map +1 -1
  39. package/lib/plugins/tree/index.js.map +1 -1
  40. package/lib/plugins/undo-redo/index.js.map +1 -1
  41. package/lib/plugins/visibility/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/public.d.ts +4 -1
  44. package/umd/grid.all.umd.js +1 -1
  45. package/umd/grid.all.umd.js.map +1 -1
  46. package/umd/grid.umd.js +1 -1
  47. package/umd/grid.umd.js.map +1 -1
  48. package/umd/plugins/clipboard.umd.js +1 -1
  49. package/umd/plugins/clipboard.umd.js.map +1 -1
  50. package/umd/plugins/editing.umd.js +1 -1
  51. package/umd/plugins/editing.umd.js.map +1 -1
  52. package/umd/plugins/export.umd.js +1 -1
  53. package/umd/plugins/export.umd.js.map +1 -1
  54. package/umd/plugins/filtering.umd.js +1 -1
  55. package/umd/plugins/filtering.umd.js.map +1 -1
  56. package/umd/plugins/multi-sort.umd.js +1 -1
  57. package/umd/plugins/multi-sort.umd.js.map +1 -1
  58. package/umd/plugins/pinned-rows.umd.js +1 -1
  59. package/umd/plugins/pinned-rows.umd.js.map +1 -1
  60. package/umd/plugins/server-side.umd.js +1 -1
  61. package/umd/plugins/server-side.umd.js.map +1 -1
@@ -1,2 +1,2 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/internal/aria"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/aria","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_multiSort={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,r){"use strict";function i(t,e){return null==t&&null==e?0:null==t?1:null==e?-1:"number"==typeof t&&"number"==typeof e?t-e:t instanceof Date&&e instanceof Date?t.getTime()-e.getTime():"boolean"==typeof t&&"boolean"==typeof e?t===e?0:t?-1:1:String(t).localeCompare(String(e))}function o(t,e){const r=t.findIndex(t=>t.field===e);return r>=0?r+1:void 0}function s(t,e){return t.find(t=>t.field===e)?.direction}class n extends r.BaseGridPlugin{static manifest={queries:[{type:"sort:get-model",description:"Returns the current multi-sort model as SortModel[]"},{type:"sort:set-model",description:"Sets the multi-sort model from context (SortModel[])"}]};name="multiSort";styles='@layer tbw-plugins{.header-cell[data-sort=asc]:after{content:"↑";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-indicator{margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-index{font-size:var(--tbw-font-size-2xs, .7em);background:var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));color:var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));border-radius:50%;width:var(--tbw-multi-sort-badge-size, 1em);height:var(--tbw-multi-sort-badge-size, 1em);display:inline-flex;align-items:center;justify-content:center;margin-left:var(--tbw-spacing-xs, .125em);font-weight:600}}';get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];cachedSortResult=null;get#t(){return this.grid}clearCoreSortState(){this.#t._sortState=null}#e(t){const e=this.grid?.query?.("grouping:get-grouped-fields",null);if(!Array.isArray(e)||0===e.length)return t;const r=e[0];if(!Array.isArray(r)||0===r.length)return t;const i=new Set(r),o=t.filter(t=>!i.has(t.field));return o.length===t.length?t:o}detach(){this.sortModel=[],this.cachedSortResult=null}handleQuery(t){switch(t.type){case"sort:get-model":return[...this.sortModel];case"sort:set-model":{const e=t.context;return!!Array.isArray(e)&&(this.sortModel=[...e],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),!0)}default:return}}processRows(t){if(0===this.sortModel.length)return this.cachedSortResult=null,[...t];const e=this.#t;if(!e._isGridEditMode&&"number"==typeof e._activeEditRows&&-1!==e._activeEditRows&&this.cachedSortResult&&this.cachedSortResult.length===t.length)return[...this.cachedSortResult];const r=this.#e(this.sortModel),o=t;return r.length>0&&function(t,e,r){if(!e.length)return;const o=e.map(t=>{const e=r.find(e=>e.field===t.field);return{field:t.field,asc:"asc"===t.direction,comparator:e?.sortComparator??i}});if(1===o.length){const{field:e,asc:r,comparator:i}=o[0];t.sort((t,o)=>{const s=i(t[e],o[e],t,o);return r?s:-s})}else t.sort((t,e)=>{for(let r=0;r<o.length;r++){const{field:i,asc:s,comparator:n}=o[r],l=n(t[i],e[i],t,e);if(0!==l)return s?l:-l}return 0})}(o,r,this.columns),this.cachedSortResult=o,o}onHeaderClick(t){const r=this.columns.find(e=>e.field===t.field);if(!r?.sortable)return!1;const i=t.originalEvent.shiftKey,o=this.config.maxSortColumns??3;if(this.sortModel=function(t,e,r,i){const o=t.find(t=>t.field===e);return r?o?"asc"===o.direction?t.map(t=>t.field===e?{...t,direction:"desc"}:t):t.filter(t=>t.field!==e):t.length<i?[...t,{field:e,direction:"asc"}]:t:"asc"===o?.direction?[{field:e,direction:"desc"}]:"desc"===o?.direction?[]:[{field:e,direction:"asc"}]}(this.sortModel,t.field,i,o),this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),this.grid?.requestStateChange?.(),this.sortModel.length>0){const t=this.sortModel.map(t=>{const e=this.columns.find(e=>e.field===t.field);return`${e?.header??t.field} ${"asc"===t.direction?"ascending":"descending"}`});e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortApplied",t.join(", then "),""))}else e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortCleared"));return!0}afterRender(){const t=this.gridElement;if(!t)return;const e=!1!==this.config.showSortIndex;t.querySelectorAll(".header-row .cell[data-field]").forEach(t=>{const r=t.getAttribute("data-field");if(!r)return;const i=o(this.sortModel,r),n=s(this.sortModel,r);if(t.querySelector(".sort-index")?.remove(),n){const r=this.updateSortIndicator(t,n);if(e&&this.sortModel.length>1&&void 0!==i){const e=document.createElement("span");e.className="sort-index",e.textContent=String(i),r.nextSibling?t.insertBefore(e,r.nextSibling):t.appendChild(e)}}else t.classList.contains("sortable")&&this.updateSortIndicator(t,null)})}getSortModel(){return[...this.sortModel]}setSortModel(t){if(this.sortModel=[...t],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...t]}),this.requestRender(),this.grid?.requestStateChange?.(),t.length>0){const r=t.map(t=>{const e=this.columns.find(e=>e.field===t.field);return`${e?.header??t.field} ${"asc"===t.direction?"ascending":"descending"}`});e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortApplied",r.join(", then "),""))}}clearSort(){this.sortModel=[],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[]}),this.requestRender(),this.grid?.requestStateChange?.(),e.announce(this.gridElement,e.getA11yMessage(this.gridElement,"sortCleared"))}getSortIndex(t){return o(this.sortModel,t)}getSortDirection(t){return s(this.sortModel,t)}getColumnState(t){const e=this.sortModel.findIndex(e=>e.field===t);if(-1===e)return;return{sort:{direction:this.sortModel[e].direction,priority:e}}}applyColumnState(t,e){if(!e.sort)return void(this.sortModel=this.sortModel.filter(e=>e.field!==t));const r=this.sortModel.findIndex(e=>e.field===t),i={field:t,direction:e.sort.direction};-1!==r?this.sortModel[r]=i:this.sortModel.splice(e.sort.priority,0,i),this.clearCoreSortState()}}t.MultiSortPlugin=n,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("../../core/internal/aria"),require("../../core/plugin/base-plugin"),require("../../core/internal/value-accessor")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/aria","../../core/plugin/base-plugin","../../core/internal/value-accessor"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_multiSort={},e.TbwGrid,e.TbwGrid,e.TbwGrid)}(this,function(e,t,r,i){"use strict";function o(e,t){const r=!0===e?.__loading;return r===(!0===t?.__loading)?0:r?1:-1}function s(e,t){return null==e&&null==t?0:null==e?1:null==t?-1:"number"==typeof e&&"number"==typeof t?e-t:e instanceof Date&&t instanceof Date?e.getTime()-t.getTime():"boolean"==typeof e&&"boolean"==typeof t?e===t?0:e?-1:1:String(e).localeCompare(String(t))}function n(e,t){const r=e.findIndex(e=>e.field===t);return r>=0?r+1:void 0}function l(e,t){return e.find(e=>e.field===t)?.direction}class d extends r.BaseGridPlugin{static manifest={queries:[{type:"sort:get-model",description:"Returns the current multi-sort model as SortModel[]"},{type:"sort:set-model",description:"Sets the multi-sort model from context (SortModel[])"}]};name="multiSort";styles='@layer tbw-plugins{.header-cell[data-sort=asc]:after{content:"↑";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-indicator{margin-left:var(--tbw-spacing-xs, .25em);opacity:.8}.sort-index{font-size:var(--tbw-font-size-2xs, .7em);background:var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));color:var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));border-radius:50%;width:var(--tbw-multi-sort-badge-size, 1em);height:var(--tbw-multi-sort-badge-size, 1em);display:inline-flex;align-items:center;justify-content:center;margin-left:var(--tbw-spacing-xs, .125em);font-weight:600}}';get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];cachedSortResult=null;get#e(){return this.grid}clearCoreSortState(){this.#e._sortState=null}#t(e){const t=this.grid?.query?.("grouping:get-grouped-fields",null);if(!Array.isArray(t)||0===t.length)return e;const r=t[0];if(!Array.isArray(r)||0===r.length)return e;const i=new Set(r),o=e.filter(e=>!i.has(e.field));return o.length===e.length?e:o}detach(){this.sortModel=[],this.cachedSortResult=null}handleQuery(e){switch(e.type){case"sort:get-model":return[...this.sortModel];case"sort:set-model":{const t=e.context;return!!Array.isArray(t)&&(this.sortModel=[...t],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),!0)}default:return}}processRows(e){if(0===this.sortModel.length)return this.cachedSortResult=null,[...e];const t=this.#e;if(!t._isGridEditMode&&"number"==typeof t._activeEditRows&&-1!==t._activeEditRows&&this.cachedSortResult&&this.cachedSortResult.length===e.length)return[...this.cachedSortResult];const r=this.#t(this.sortModel),n=e;return r.length>0&&function(e,t,r){if(!t.length)return;const n=t.map(e=>{const t=r.find(t=>t.field===e.field),i=t?.sortComparator??s;return{field:e.field,asc:"asc"===e.direction,comparator:i,pinPlaceholders:!t?.sortComparator,column:t}}),l=(e,t)=>t.column?.valueAccessor?i.resolveCellValue(e,t.column):e[t.field];if(1===n.length){const t=n[0];e.sort((e,r)=>{if(t.pinPlaceholders){const t=o(e,r);if(0!==t)return t}const i=t.comparator(l(e,t),l(r,t),e,r);return t.asc?i:-i})}else e.sort((e,t)=>{if(n.some(e=>e.pinPlaceholders)){const r=o(e,t);if(0!==r)return r}for(let r=0;r<n.length;r++){const i=n[r],o=i.comparator(l(e,i),l(t,i),e,t);if(0!==o)return i.asc?o:-o}return 0})}(n,r,this.columns),this.cachedSortResult=n,n}onHeaderClick(e){const r=this.columns.find(t=>t.field===e.field);if(!r?.sortable)return!1;const i=e.originalEvent.shiftKey,o=this.config.maxSortColumns??3;if(this.sortModel=function(e,t,r,i){const o=e.find(e=>e.field===t);return r?o?"asc"===o.direction?e.map(e=>e.field===t?{...e,direction:"desc"}:e):e.filter(e=>e.field!==t):e.length<i?[...e,{field:t,direction:"asc"}]:e:"asc"===o?.direction?[{field:t,direction:"desc"}]:"desc"===o?.direction?[]:[{field:t,direction:"asc"}]}(this.sortModel,e.field,i,o),this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),this.grid?.requestStateChange?.(),this.sortModel.length>0){const e=this.sortModel.map(e=>{const t=this.columns.find(t=>t.field===e.field);return`${t?.header??e.field} ${"asc"===e.direction?"ascending":"descending"}`});t.announce(this.gridElement,t.getA11yMessage(this.gridElement,"sortApplied",e.join(", then "),""))}else t.announce(this.gridElement,t.getA11yMessage(this.gridElement,"sortCleared"));return!0}afterRender(){const e=this.gridElement;if(!e)return;const t=!1!==this.config.showSortIndex;e.querySelectorAll(".header-row .cell[data-field]").forEach(e=>{const r=e.getAttribute("data-field");if(!r)return;const i=n(this.sortModel,r),o=l(this.sortModel,r);if(e.querySelector(".sort-index")?.remove(),o){const r=this.updateSortIndicator(e,o);if(t&&this.sortModel.length>1&&void 0!==i){const t=document.createElement("span");t.className="sort-index",t.textContent=String(i),r.nextSibling?e.insertBefore(t,r.nextSibling):e.appendChild(t)}}else e.classList.contains("sortable")&&this.updateSortIndicator(e,null)})}getSortModel(){return[...this.sortModel]}setSortModel(e){if(this.sortModel=[...e],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[...e]}),this.requestRender(),this.grid?.requestStateChange?.(),e.length>0){const r=e.map(e=>{const t=this.columns.find(t=>t.field===e.field);return`${t?.header??e.field} ${"asc"===e.direction?"ascending":"descending"}`});t.announce(this.gridElement,t.getA11yMessage(this.gridElement,"sortApplied",r.join(", then "),""))}}clearSort(){this.sortModel=[],this.clearCoreSortState(),this.broadcast("sort-change",{sortModel:[]}),this.requestRender(),this.grid?.requestStateChange?.(),t.announce(this.gridElement,t.getA11yMessage(this.gridElement,"sortCleared"))}getSortIndex(e){return n(this.sortModel,e)}getSortDirection(e){return l(this.sortModel,e)}getColumnState(e){const t=this.sortModel.findIndex(t=>t.field===e);if(-1===t)return;return{sort:{direction:this.sortModel[t].direction,priority:t}}}applyColumnState(e,t){if(!t.sort)return void(this.sortModel=this.sortModel.filter(t=>t.field!==e));const r=this.sortModel.findIndex(t=>t.field===e),i={field:e,direction:t.sort.direction};-1!==r?this.sortModel[r]=i:this.sortModel.splice(t.sort.priority,0,i),this.clearCoreSortState()}}e.MultiSortPlugin=d,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})});
2
2
  //# sourceMappingURL=multi-sort.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n return {\n field: sort.field,\n asc: sort.direction === 'asc',\n comparator: col?.sortComparator ?? defaultComparator,\n };\n });\n\n if (chain.length === 1) {\n // Single-sort fast path — avoid loop overhead\n const { field, asc, comparator } = chain[0];\n rows.sort((a: any, b: any) => {\n const result = comparator(a[field], b[field], a, b);\n return asc ? result : -result;\n });\n } else {\n rows.sort((a: any, b: any) => {\n for (let i = 0; i < chain.length; i++) {\n const { field, asc, comparator } = chain[i];\n const result = comparator(a[field], b[field], a, b);\n if (result !== 0) return asc ? result : -result;\n }\n return 0;\n });\n }\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback)\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n /** Cached sort result — returned as-is while a row edit is active to prevent\n * the edited row from jumping to a new sorted position mid-edit. Row data\n * mutations are still visible because the array holds shared object refs. */\n private cachedSortResult: unknown[] | null = null;\n\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\n\n /**\n * Remove sorts on fields that are owned by the grouping plugin.\n * GroupingRowsPlugin handles group header ordering independently, so\n * multi-sort should only sort by non-grouped data columns.\n */\n #filterGroupedFields(model: SortModel[]): SortModel[] {\n const results = this.grid?.query?.('grouping:get-grouped-fields', null);\n if (!Array.isArray(results) || results.length === 0) return model;\n\n const groupedFields = results[0] as string[];\n if (!Array.isArray(groupedFields) || groupedFields.length === 0) return model;\n\n const groupedSet = new Set(groupedFields);\n const filtered = model.filter((s) => !groupedSet.has(s.field));\n return filtered.length === model.length ? model : filtered;\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n this.cachedSortResult = null;\n }\n // #endregion\n\n // #region Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\n\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return [...rows];\n }\n\n // Freeze sort order while a row is actively being edited (row mode only).\n // Re-sorting mid-edit would move the edited row to a new index while the\n // editors remain at the old position, causing data/UI mismatch.\n // In grid mode (_isGridEditMode) sorting is safe — afterCellRender\n // re-injects editors into the re-sorted cells.\n // We return the cached previous sort result (same object references, so\n // in-place value mutations are already visible) instead of unsorted input.\n const grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort in-place — the input array is already a mutable copy from plugin-manager.\n // Pre-resolved comparator chain avoids column lookup on every pair comparison.\n // Exclude fields owned by the grouping plugin — group header order is handled\n // by GroupingRowsPlugin, so multi-sort should only affect within-group data order.\n const effectiveModel = this.#filterGroupedFields(this.sortModel);\n\n const mutableRows = rows as unknown[];\n if (effectiveModel.length > 0) {\n sortRowsInPlace(mutableRows, effectiveModel, this.columns);\n }\n this.cachedSortResult = mutableRows;\n return mutableRows;\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n this.clearCoreSortState();\n\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n\n // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n }\n // #endregion\n}\n"],"names":["defaultComparator","a","b","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","static","queries","type","description","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","filterGroupedFields","model","results","query","Array","isArray","length","groupedFields","groupedSet","Set","filtered","filter","has","detach","handleQuery","context","broadcast","requestRender","processRows","rows","_isGridEditMode","_activeEditRows","effectiveModel","mutableRows","sorts","columns","chain","map","sort","col","c","asc","comparator","sortComparator","result","i","sortRowsInPlace","onHeaderClick","event","column","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","toggleSort","requestStateChange","labels","header","announce","gridElement","getA11yMessage","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","querySelector","remove","indicator","updateSortIndicator","badge","document","createElement","className","textContent","nextSibling","insertBefore","appendChild","classList","contains","getSortModel","setSortModel","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"mappings":"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"}
1
+ {"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n const copy = [...rows];\n sortRowsInPlace(copy, sorts, columns);\n return copy;\n}\n\n/**\n * Sort an array in-place using multiple sort columns.\n * Pre-resolves column comparators to avoid O(n·log·n·m) column lookups\n * inside the comparator.\n * @internal\n */\nexport function sortRowsInPlace<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): void {\n if (!sorts.length) return;\n\n // Pre-resolve comparator chain — avoids columns.find() on every pair comparison\n const chain = sorts.map((sort) => {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n return {\n field: sort.field,\n asc: sort.direction === 'asc',\n comparator,\n // Auto-pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n // to the end ONLY when no custom comparator is configured. Custom comparators receive\n // the row pair as 3rd/4th args and own placeholder handling themselves.\n pinPlaceholders: !col?.sortComparator,\n column: col,\n };\n });\n\n // sortComparator (when present) takes precedence over valueAccessor; both still\n // receive the accessor-resolved value when no comparator is given. Documented\n // precedence: sortComparator → valueAccessor → field.\n const getValue = (row: any, link: (typeof chain)[number]) =>\n link.column?.valueAccessor ? resolveCellValue(row, link.column) : row[link.field];\n\n if (chain.length === 1) {\n // Single-sort fast path — avoid loop overhead\n const link = chain[0];\n rows.sort((a: any, b: any) => {\n if (link.pinPlaceholders) {\n const pinned = pinLoadingRows(a, b);\n if (pinned !== 0) return pinned;\n }\n const result = link.comparator(getValue(a, link), getValue(b, link), a, b);\n return link.asc ? result : -result;\n });\n } else {\n rows.sort((a: any, b: any) => {\n // Pin placeholders ahead of the chain — independent of every column's direction.\n // We only need to check once per pair, using the most permissive flag in the chain.\n const anyPin = chain.some((l) => l.pinPlaceholders);\n if (anyPin) {\n const pinned = pinLoadingRows(a, b);\n if (pinned !== 0) return pinned;\n }\n for (let i = 0; i < chain.length; i++) {\n const link = chain[i];\n const result = link.comparator(getValue(a, link), getValue(b, link), a, b);\n if (result !== 0) return link.asc ? result : -result;\n }\n return 0;\n });\n }\n}\n\n/**\n * Pin `__loading` placeholder rows (e.g. ServerSidePlugin under `sortMode: 'local'`)\n * to the end of the sorted array regardless of sort direction.\n *\n * Returns 0 when neither row is a placeholder, +1 when `a` is, -1 when `b` is.\n *\n * @internal\n */\nfunction pinLoadingRows(a: unknown, b: unknown): number {\n const aLoading = (a as { __loading?: unknown } | null)?.__loading === true;\n const bLoading = (b as { __loading?: unknown } | null)?.__loading === true;\n if (aLoading === bLoading) return 0;\n return aLoading ? 1 : -1;\n}\n\n/**\n * Default comparator for sorting values.\n * Handles nulls, numbers, dates, and strings.\n *\n * @param a - First value\n * @param b - Second value\n * @returns Comparison result (-1, 0, 1)\n */\nexport function defaultComparator(a: unknown, b: unknown): number {\n // Handle nulls/undefined - push to end\n if (a == null && b == null) return 0;\n if (a == null) return 1;\n if (b == null) return -1;\n\n // Type-aware comparison\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime();\n }\n\n // Boolean comparison\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? -1 : 1;\n }\n\n // String comparison (fallback)\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Toggle sort state for a field.\n * With shift key: adds/toggles in multi-sort list\n * Without shift key: replaces entire sort with single column\n *\n * @param current - Current sort model\n * @param field - Field to toggle\n * @param shiftKey - Whether shift key is held (multi-sort mode)\n * @param maxColumns - Maximum columns allowed in sort\n * @returns New sort model\n */\nexport function toggleSort(current: SortModel[], field: string, shiftKey: boolean, maxColumns: number): SortModel[] {\n const existing = current.find((s) => s.field === field);\n\n if (shiftKey) {\n // Multi-sort: add/toggle in list\n if (existing) {\n if (existing.direction === 'asc') {\n // Flip to descending\n return current.map((s) => (s.field === field ? { ...s, direction: 'desc' as const } : s));\n } else {\n // Remove from sort\n return current.filter((s) => s.field !== field);\n }\n } else if (current.length < maxColumns) {\n // Add new sort column\n return [...current, { field, direction: 'asc' as const }];\n }\n // Max columns reached, return unchanged\n return current;\n } else {\n // Single sort: replace all\n if (existing?.direction === 'asc') {\n return [{ field, direction: 'desc' }];\n } else if (existing?.direction === 'desc') {\n return [];\n }\n return [{ field, direction: 'asc' }];\n }\n}\n\n/**\n * Get the sort index (1-based) for a field in the sort model.\n * Returns undefined if the field is not in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns 1-based index or undefined\n */\nexport function getSortIndex(sortModel: SortModel[], field: string): number | undefined {\n const index = sortModel.findIndex((s) => s.field === field);\n return index >= 0 ? index + 1 : undefined;\n}\n\n/**\n * Get the sort direction for a field in the sort model.\n *\n * @param sortModel - Current sort model\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\nexport function getSortDirection(sortModel: SortModel[], field: string): 'asc' | 'desc' | undefined {\n return sortModel.find((s) => s.field === field)?.direction;\n}\n","/**\n * Multi-Sort Plugin (Class-based)\n *\n * Provides multi-column sorting capabilities for tbw-grid.\n * Supports shift+click for adding secondary sort columns.\n */\n\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { BaseGridPlugin, HeaderClickEvent, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnState, GridHost } from '../../core/types';\nimport { getSortDirection, getSortIndex, sortRowsInPlace, toggleSort } from './multi-sort';\nimport styles from './multi-sort.css?inline';\nimport type { MultiSortConfig, SortModel } from './types';\n\n/**\n * Multi-Sort Plugin for tbw-grid\n *\n * Enables sorting by multiple columns at once—hold Shift and click additional column\n * headers to build up a sort stack. Priority badges show the sort order, so users\n * always know which column takes precedence.\n *\n * ## Installation\n *\n * ```ts\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Click header` | Sort by column (clears other sorts) |\n * | `Shift + Click` | Add column to multi-sort stack |\n * | `Ctrl + Click` | Toggle sort direction |\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `sort-change` | `{ sortModel: SortModel[] }` | Fired when sort changes |\n *\n * @example Basic Multi-Column Sorting\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { MultiSortPlugin } from '@toolbox-web/grid/plugins/multi-sort';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', sortable: true },\n * { field: 'department', header: 'Department', sortable: true },\n * { field: 'salary', header: 'Salary', type: 'number', sortable: true },\n * ],\n * plugins: [new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })],\n * };\n *\n * grid.on('sort-change', ({ sortModel }) => {\n * console.log('Active sorts:', sortModel);\n * });\n * ```\n *\n * @example Initial Sort Configuration\n * ```ts\n * new MultiSortPlugin({\n * initialSort: [\n * { field: 'department', direction: 'asc' },\n * { field: 'salary', direction: 'desc' },\n * ],\n * })\n * ```\n *\n * @see {@link MultiSortConfig} for all configuration options\n * @see {@link SortModel} for the sort model structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n /**\n * Plugin manifest declaring query types this plugin responds to.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n { type: 'sort:get-model', description: 'Returns the current multi-sort model as SortModel[]' },\n { type: 'sort:set-model', description: 'Sets the multi-sort model from context (SortModel[])' },\n ],\n };\n\n /** @internal */\n readonly name = 'multiSort';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<MultiSortConfig> {\n return {\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // #region Internal State\n private sortModel: SortModel[] = [];\n /** Cached sort result — returned as-is while a row edit is active to prevent\n * the edited row from jumping to a new sorted position mid-edit. Row data\n * mutations are still visible because the array holds shared object refs. */\n private cachedSortResult: unknown[] | null = null;\n\n /** Typed internal grid accessor. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Clear the core `_sortState` so that only this plugin's `processRows`\n * sorting applies. `ConfigManager.applyState()` always sets the core sort\n * state when restoring from storage, even when a plugin handles sorting.\n * Without this, the stale core state leaks into `collectState()` and\n * `reapplyCoreSort()` after the plugin clears its own model.\n */\n private clearCoreSortState(): void {\n this.#internalGrid._sortState = null;\n }\n\n /**\n * Remove sorts on fields that are owned by the grouping plugin.\n * GroupingRowsPlugin handles group header ordering independently, so\n * multi-sort should only sort by non-grouped data columns.\n */\n #filterGroupedFields(model: SortModel[]): SortModel[] {\n const results = this.grid?.query?.('grouping:get-grouped-fields', null);\n if (!Array.isArray(results) || results.length === 0) return model;\n\n const groupedFields = results[0] as string[];\n if (!Array.isArray(groupedFields) || groupedFields.length === 0) return model;\n\n const groupedSet = new Set(groupedFields);\n const filtered = model.filter((s) => !groupedSet.has(s.field));\n return filtered.length === model.length ? model : filtered;\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n this.sortModel = [];\n this.cachedSortResult = null;\n }\n // #endregion\n\n // #region Query System\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case 'sort:get-model':\n return [...this.sortModel];\n case 'sort:set-model': {\n const model = query.context;\n if (!Array.isArray(model)) return false;\n this.sortModel = [...model] as SortModel[];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n return true;\n }\n default:\n return undefined;\n }\n }\n\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n this.cachedSortResult = null;\n return [...rows];\n }\n\n // Freeze sort order while a row is actively being edited (row mode only).\n // Re-sorting mid-edit would move the edited row to a new index while the\n // editors remain at the old position, causing data/UI mismatch.\n // In grid mode (_isGridEditMode) sorting is safe — afterCellRender\n // re-injects editors into the re-sorted cells.\n // We return the cached previous sort result (same object references, so\n // in-place value mutations are already visible) instead of unsorted input.\n const grid = this.#internalGrid;\n if (!grid._isGridEditMode && typeof grid._activeEditRows === 'number' && grid._activeEditRows !== -1) {\n if (this.cachedSortResult && this.cachedSortResult.length === rows.length) {\n return [...this.cachedSortResult];\n }\n }\n\n // Sort in-place — the input array is already a mutable copy from plugin-manager.\n // Pre-resolved comparator chain avoids column lookup on every pair comparison.\n // Exclude fields owned by the grouping plugin — group header order is handled\n // by GroupingRowsPlugin, so multi-sort should only affect within-group data order.\n const effectiveModel = this.#filterGroupedFields(this.sortModel);\n\n const mutableRows = rows as unknown[];\n if (effectiveModel.length > 0) {\n sortRowsInPlace(mutableRows, effectiveModel, this.columns);\n }\n this.cachedSortResult = mutableRows;\n return mutableRows;\n }\n\n /** @internal */\n override onHeaderClick(event: HeaderClickEvent): boolean {\n const column = this.columns.find((c) => c.field === event.field);\n if (!column?.sortable) return false;\n\n const shiftKey = event.originalEvent.shiftKey;\n const maxColumns = this.config.maxSortColumns ?? 3;\n\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\n this.clearCoreSortState();\n\n this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n\n // Announce for screen readers\n if (this.sortModel.length > 0) {\n const labels = this.sortModel.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n } else {\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n const headerCells = gridEl.querySelectorAll('.header-row .cell[data-field]');\n headerCells.forEach((cell) => {\n const field = cell.getAttribute('data-field');\n if (!field) return;\n\n const sortIndex = getSortIndex(this.sortModel, field);\n const sortDir = getSortDirection(this.sortModel, field);\n\n // Remove existing sort index badge (always clean up)\n cell.querySelector('.sort-index')?.remove();\n\n if (sortDir) {\n const indicator = this.updateSortIndicator(cell, sortDir);\n\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\n const badge = document.createElement('span');\n badge.className = 'sort-index';\n badge.textContent = String(sortIndex);\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else if (cell.classList.contains('sortable')) {\n this.updateSortIndicator(cell, null);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current sort model.\n * @returns Copy of the current sort model\n */\n getSortModel(): SortModel[] {\n return [...this.sortModel];\n }\n\n /**\n * Set the sort model programmatically.\n * @param model - New sort model to apply\n */\n setSortModel(model: SortModel[]): void {\n this.sortModel = [...model];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [...model] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n if (model.length > 0) {\n const labels = model.map((s) => {\n const col = this.columns.find((c) => c.field === s.field);\n return `${col?.header ?? s.field} ${s.direction === 'asc' ? 'ascending' : 'descending'}`;\n });\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortApplied', labels.join(', then '), ''));\n }\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.clearCoreSortState();\n this.broadcast('sort-change', { sortModel: [] });\n this.requestRender();\n this.grid?.requestStateChange?.();\n announce(this.gridElement!, getA11yMessage(this.gridElement!, 'sortCleared'));\n }\n\n /**\n * Get the sort index (1-based) for a specific field.\n * @param field - Field to check\n * @returns 1-based index or undefined if not sorted\n */\n getSortIndex(field: string): number | undefined {\n return getSortIndex(this.sortModel, field);\n }\n\n /**\n * Get the sort direction for a specific field.\n * @param field - Field to check\n * @returns Sort direction or undefined if not sorted\n */\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\n return getSortDirection(this.sortModel, field);\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return sort state for a column if it's in the sort model.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const index = this.sortModel.findIndex((s) => s.field === field);\n if (index === -1) return undefined;\n\n const sortEntry = this.sortModel[index];\n return {\n sort: {\n direction: sortEntry.direction,\n priority: index,\n },\n };\n }\n\n /**\n * Apply sort state from column state.\n * Rebuilds the sort model from all column states.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has sort state\n if (!state.sort) {\n // Remove this field from sortModel if it exists\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\n return;\n }\n\n // Find existing entry or add new one\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\n const newEntry: SortModel = {\n field,\n direction: state.sort.direction,\n };\n\n if (existingIndex !== -1) {\n // Update existing entry\n this.sortModel[existingIndex] = newEntry;\n } else {\n // Add at the correct priority position\n this.sortModel.splice(state.sort.priority, 0, newEntry);\n }\n\n // Clear core sort state — this plugin exclusively handles sorting via\n // processRows. The core _sortState is set by ConfigManager.applyState()\n // before plugins run; null it so reapplyCoreSort() is a no-op.\n this.clearCoreSortState();\n }\n // #endregion\n}\n"],"names":["pinLoadingRows","a","b","aLoading","__loading","defaultComparator","Date","getTime","String","localeCompare","getSortIndex","sortModel","field","index","findIndex","s","getSortDirection","find","direction","MultiSortPlugin","BaseGridPlugin","static","queries","type","description","name","styles","defaultConfig","maxSortColumns","showSortIndex","cachedSortResult","internalGrid","this","grid","clearCoreSortState","_sortState","filterGroupedFields","model","results","query","Array","isArray","length","groupedFields","groupedSet","Set","filtered","filter","has","detach","handleQuery","context","broadcast","requestRender","processRows","rows","_isGridEditMode","_activeEditRows","effectiveModel","mutableRows","sorts","columns","chain","map","sort","col","c","comparator","sortComparator","asc","pinPlaceholders","column","getValue","row","link","valueAccessor","resolveCellValue","pinned","result","some","l","i","sortRowsInPlace","onHeaderClick","event","sortable","shiftKey","originalEvent","maxColumns","config","current","existing","toggleSort","requestStateChange","labels","header","announce","gridElement","getA11yMessage","join","afterRender","gridEl","showIndex","querySelectorAll","forEach","cell","getAttribute","sortIndex","sortDir","querySelector","remove","indicator","updateSortIndicator","badge","document","createElement","className","textContent","nextSibling","insertBefore","appendChild","classList","contains","getSortModel","setSortModel","clearSort","getColumnState","priority","applyColumnState","state","existingIndex","newEntry","splice"],"mappings":"6fAgGA,SAASA,EAAeC,EAAYC,GAClC,MAAMC,GAAgE,IAApDF,GAAsCG,UAExD,OAAID,MADkE,IAApDD,GAAsCE,WACtB,EAC3BD,EAAW,GAAI,CACxB,CAUO,SAASE,EAAkBJ,EAAYC,GAE5C,OAAS,MAALD,GAAkB,MAALC,EAAkB,EAC1B,MAALD,EAAkB,EACb,MAALC,GAAkB,EAGL,iBAAND,GAA+B,iBAANC,EAC3BD,EAAIC,EAGTD,aAAaK,MAAQJ,aAAaI,KAC7BL,EAAEM,UAAYL,EAAEK,UAIR,kBAANN,GAAgC,kBAANC,EAC5BD,IAAMC,EAAI,EAAID,GAAI,EAAK,EAIzBO,OAAOP,GAAGQ,cAAcD,OAAON,GACxC,CAmDO,SAASQ,EAAaC,EAAwBC,GACnD,MAAMC,EAAQF,EAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GACrD,OAAOC,GAAS,EAAIA,EAAQ,OAAI,CAClC,CASO,SAASG,EAAiBL,EAAwBC,GACvD,OAAOD,EAAUM,KAAMF,GAAMA,EAAEH,QAAUA,IAAQM,SACnD,CC1HO,MAAMC,UAAwBC,EAAAA,eAKnCC,gBAAoD,CAClDC,QAAS,CACP,CAAEC,KAAM,iBAAkBC,YAAa,uDACvC,CAAED,KAAM,iBAAkBC,YAAa,0DAKlCC,KAAO,YAEEC,isBAGlB,iBAAuBC,GACrB,MAAO,CACLC,eAAgB,EAChBC,eAAe,EAEnB,CAGQlB,UAAyB,GAIzBmB,iBAAqC,KAG7C,KAAIC,GACF,OAAOC,KAAKC,IACd,CASQ,kBAAAC,GACNF,MAAKD,EAAcI,WAAa,IAClC,CAOA,EAAAC,CAAqBC,GACnB,MAAMC,EAAUN,KAAKC,MAAMM,QAAQ,8BAA+B,MAClE,IAAKC,MAAMC,QAAQH,IAA+B,IAAnBA,EAAQI,OAAc,OAAOL,EAE5D,MAAMM,EAAgBL,EAAQ,GAC9B,IAAKE,MAAMC,QAAQE,IAA2C,IAAzBA,EAAcD,OAAc,OAAOL,EAExE,MAAMO,EAAa,IAAIC,IAAIF,GACrBG,EAAWT,EAAMU,OAAQhC,IAAO6B,EAAWI,IAAIjC,EAAEH,QACvD,OAAOkC,EAASJ,SAAWL,EAAMK,OAASL,EAAQS,CACpD,CAMS,MAAAG,GACPjB,KAAKrB,UAAY,GACjBqB,KAAKF,iBAAmB,IAC1B,CAMS,WAAAoB,CAAYX,GACnB,OAAQA,EAAMhB,MACZ,IAAK,iBACH,MAAO,IAAIS,KAAKrB,WAClB,IAAK,iBAAkB,CACrB,MAAM0B,EAAQE,EAAMY,QACpB,QAAKX,MAAMC,QAAQJ,KACnBL,KAAKrB,UAAY,IAAI0B,GACrBL,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAIqB,KAAKrB,aACpDqB,KAAKqB,iBACE,EACT,CACA,QACE,OAEN,CAOS,WAAAC,CAAYC,GACnB,GAA8B,IAA1BvB,KAAKrB,UAAU+B,OAEjB,OADAV,KAAKF,iBAAmB,KACjB,IAAIyB,GAUb,MAAMtB,EAAOD,MAAKD,EAClB,IAAKE,EAAKuB,iBAAmD,iBAAzBvB,EAAKwB,kBAAyD,IAAzBxB,EAAKwB,iBACxEzB,KAAKF,kBAAoBE,KAAKF,iBAAiBY,SAAWa,EAAKb,OACjE,MAAO,IAAIV,KAAKF,kBAQpB,MAAM4B,EAAiB1B,MAAKI,EAAqBJ,KAAKrB,WAEhDgD,EAAcJ,EAKpB,OAJIG,EAAehB,OAAS,GD3KzB,SAAyCa,EAAcK,EAAoBC,GAChF,IAAKD,EAAMlB,OAAQ,OAGnB,MAAMoB,EAAQF,EAAMG,IAAKC,IACvB,MAAMC,EAAMJ,EAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUoD,EAAKpD,OAC3CuD,EAAaF,GAAKG,gBAAkB/D,EAC1C,MAAO,CACLO,MAAOoD,EAAKpD,MACZyD,IAAwB,QAAnBL,EAAK9C,UACViD,aAIAG,iBAAkBL,GAAKG,eACvBG,OAAQN,KAONO,EAAW,CAACC,EAAUC,IAC1BA,EAAKH,QAAQI,cAAgBC,EAAAA,iBAAiBH,EAAKC,EAAKH,QAAUE,EAAIC,EAAK9D,OAE7E,GAAqB,IAAjBkD,EAAMpB,OAAc,CAEtB,MAAMgC,EAAOZ,EAAM,GACnBP,EAAKS,KAAK,CAAC/D,EAAQC,KACjB,GAAIwE,EAAKJ,gBAAiB,CACxB,MAAMO,EAAS7E,EAAeC,EAAGC,GACjC,GAAe,IAAX2E,EAAc,OAAOA,CAC3B,CACA,MAAMC,EAASJ,EAAKP,WAAWK,EAASvE,EAAGyE,GAAOF,EAAStE,EAAGwE,GAAOzE,EAAGC,GACxE,OAAOwE,EAAKL,IAAMS,GAAUA,GAEhC,MACEvB,EAAKS,KAAK,CAAC/D,EAAQC,KAIjB,GADe4D,EAAMiB,KAAMC,GAAMA,EAAEV,iBACvB,CACV,MAAMO,EAAS7E,EAAeC,EAAGC,GACjC,GAAe,IAAX2E,EAAc,OAAOA,CAC3B,CACA,IAAA,IAASI,EAAI,EAAGA,EAAInB,EAAMpB,OAAQuC,IAAK,CACrC,MAAMP,EAAOZ,EAAMmB,GACbH,EAASJ,EAAKP,WAAWK,EAASvE,EAAGyE,GAAOF,EAAStE,EAAGwE,GAAOzE,EAAGC,GACxE,GAAe,IAAX4E,EAAc,OAAOJ,EAAKL,IAAMS,GAAUA,CAChD,CACA,OAAO,GAGb,CCuHMI,CAAgBvB,EAAaD,EAAgB1B,KAAK6B,SAEpD7B,KAAKF,iBAAmB6B,EACjBA,CACT,CAGS,aAAAwB,CAAcC,GACrB,MAAMb,EAASvC,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUwE,EAAMxE,OAC1D,IAAK2D,GAAQc,SAAU,OAAO,EAE9B,MAAMC,EAAWF,EAAMG,cAAcD,SAC/BE,EAAaxD,KAAKyD,OAAO7D,gBAAkB,EAUjD,GARAI,KAAKrB,UDzEF,SAAoB+E,EAAsB9E,EAAe0E,EAAmBE,GACjF,MAAMG,EAAWD,EAAQzE,KAAMF,GAAMA,EAAEH,QAAUA,GAEjD,OAAI0E,EAEEK,EACyB,QAAvBA,EAASzE,UAEJwE,EAAQ3B,IAAKhD,GAAOA,EAAEH,QAAUA,EAAQ,IAAKG,EAAGG,UAAW,QAAoBH,GAG/E2E,EAAQ3C,OAAQhC,GAAMA,EAAEH,QAAUA,GAElC8E,EAAQhD,OAAS8C,EAEnB,IAAIE,EAAS,CAAE9E,QAAOM,UAAW,QAGnCwE,EAGqB,QAAxBC,GAAUzE,UACL,CAAC,CAAEN,QAAOM,UAAW,SACK,SAAxByE,GAAUzE,UACZ,GAEF,CAAC,CAAEN,QAAOM,UAAW,OAEhC,CC6CqB0E,CAAW5D,KAAKrB,UAAWyE,EAAMxE,MAAO0E,EAAUE,GACnExD,KAAKE,qBAELF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAIqB,KAAKrB,aACpDqB,KAAKqB,gBACLrB,KAAKC,MAAM4D,uBAGP7D,KAAKrB,UAAU+B,OAAS,EAAG,CAC7B,MAAMoD,EAAS9D,KAAKrB,UAAUoD,IAAKhD,IACjC,MAAMkD,EAAMjC,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUG,EAAEH,OACnD,MAAO,GAAGqD,GAAK8B,QAAUhF,EAAEH,SAAyB,QAAhBG,EAAEG,UAAsB,YAAc,iBAE5E8E,EAAAA,SAAShE,KAAKiE,YAAcC,EAAAA,eAAelE,KAAKiE,YAAc,cAAeH,EAAOK,KAAK,WAAY,IACvG,MACEH,EAAAA,SAAShE,KAAKiE,YAAcC,EAAAA,eAAelE,KAAKiE,YAAc,gBAGhE,OAAO,CACT,CAGS,WAAAG,GACP,MAAMC,EAASrE,KAAKiE,YACpB,IAAKI,EAAQ,OAEb,MAAMC,GAA0C,IAA9BtE,KAAKyD,OAAO5D,cAEVwE,EAAOE,iBAAiB,iCAChCC,QAASC,IACnB,MAAM7F,EAAQ6F,EAAKC,aAAa,cAChC,IAAK9F,EAAO,OAEZ,MAAM+F,EAAYjG,EAAasB,KAAKrB,UAAWC,GACzCgG,EAAU5F,EAAiBgB,KAAKrB,UAAWC,GAKjD,GAFA6F,EAAKI,cAAc,gBAAgBC,SAE/BF,EAAS,CACX,MAAMG,EAAY/E,KAAKgF,oBAAoBP,EAAMG,GAGjD,GAAIN,GAAatE,KAAKrB,UAAU+B,OAAS,QAAmB,IAAdiE,EAAyB,CACrE,MAAMM,EAAQC,SAASC,cAAc,QACrCF,EAAMG,UAAY,aAClBH,EAAMI,YAAc7G,OAAOmG,GACvBI,EAAUO,YACZb,EAAKc,aAAaN,EAAOF,EAAUO,aAEnCb,EAAKe,YAAYP,EAErB,CACF,MAAWR,EAAKgB,UAAUC,SAAS,aACjC1F,KAAKgF,oBAAoBP,EAAM,OAGrC,CASA,YAAAkB,GACE,MAAO,IAAI3F,KAAKrB,UAClB,CAMA,YAAAiH,CAAavF,GAMX,GALAL,KAAKrB,UAAY,IAAI0B,GACrBL,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,IAAI0B,KAC/CL,KAAKqB,gBACLrB,KAAKC,MAAM4D,uBACPxD,EAAMK,OAAS,EAAG,CACpB,MAAMoD,EAASzD,EAAM0B,IAAKhD,IACxB,MAAMkD,EAAMjC,KAAK6B,QAAQ5C,KAAMiD,GAAMA,EAAEtD,QAAUG,EAAEH,OACnD,MAAO,GAAGqD,GAAK8B,QAAUhF,EAAEH,SAAyB,QAAhBG,EAAEG,UAAsB,YAAc,iBAE5E8E,EAAAA,SAAShE,KAAKiE,YAAcC,EAAAA,eAAelE,KAAKiE,YAAc,cAAeH,EAAOK,KAAK,WAAY,IACvG,CACF,CAKA,SAAA0B,GACE7F,KAAKrB,UAAY,GACjBqB,KAAKE,qBACLF,KAAKoB,UAAU,cAAe,CAAEzC,UAAW,KAC3CqB,KAAKqB,gBACLrB,KAAKC,MAAM4D,uBACXG,EAAAA,SAAShE,KAAKiE,YAAcC,EAAAA,eAAelE,KAAKiE,YAAc,eAChE,CAOA,YAAAvF,CAAaE,GACX,OAAOF,EAAasB,KAAKrB,UAAWC,EACtC,CAOA,gBAAAI,CAAiBJ,GACf,OAAOI,EAAiBgB,KAAKrB,UAAWC,EAC1C,CASS,cAAAkH,CAAelH,GACtB,MAAMC,EAAQmB,KAAKrB,UAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GAC1D,QAAIC,EAAc,OAGlB,MAAO,CACLmD,KAAM,CACJ9C,UAHcc,KAAKrB,UAAUE,GAGRK,UACrB6G,SAAUlH,GAGhB,CAOS,gBAAAmH,CAAiBpH,EAAeqH,GAEvC,IAAKA,EAAMjE,KAGT,YADAhC,KAAKrB,UAAYqB,KAAKrB,UAAUoC,OAAQhC,GAAMA,EAAEH,QAAUA,IAK5D,MAAMsH,EAAgBlG,KAAKrB,UAAUG,UAAWC,GAAMA,EAAEH,QAAUA,GAC5DuH,EAAsB,CAC1BvH,QACAM,UAAW+G,EAAMjE,KAAK9C,YAGF,IAAlBgH,EAEFlG,KAAKrB,UAAUuH,GAAiBC,EAGhCnG,KAAKrB,UAAUyH,OAAOH,EAAMjE,KAAK+D,SAAU,EAAGI,GAMhDnG,KAAKE,oBACP"}
@@ -1,2 +1,2 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/plugin/base-plugin"),require("../../core/internal/aggregators")):"function"==typeof define&&define.amd?define(["exports","../../core/plugin/base-plugin","../../core/internal/aggregators"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_pinnedRows={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,n){"use strict";function o(t,e){const n=document.createElement("div");n.className="tbw-pinned-rows",n.setAttribute("role","presentation"),n.setAttribute("aria-live","polite");const o=document.createElement("div");o.className="tbw-pinned-rows-left";const i=document.createElement("div");i.className="tbw-pinned-rows-center";const r=document.createElement("div");if(r.className="tbw-pinned-rows-right",!1!==t.showRowCount){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-row-count",t.textContent=`Total: ${e.totalRows} rows`,o.appendChild(t)}if(t.showFilteredCount&&e.filteredRows!==e.totalRows){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-filtered-count",t.textContent=`Filtered: ${e.filteredRows}`,o.appendChild(t)}if(t.showSelectedCount&&e.selectedRows>0){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-selected-count",t.textContent=`Selected: ${e.selectedRows}`,r.appendChild(t)}if(t.customPanels)for(const a of t.customPanels){const t=g(a,e);switch(a.position){case"left":o.appendChild(t);break;case"center":i.appendChild(t);break;case"right":r.appendChild(t)}}return n.appendChild(o),n.appendChild(i),n.appendChild(r),n}function i(t){const e=document.createElement("div");return e.className=`tbw-aggregation-rows tbw-aggregation-rows-${t}`,e.setAttribute("role","presentation"),e}function r(t,e,n,o,i=!1){t.innerHTML="";for(const r of e){const e=document.createElement("div");e.className="tbw-aggregation-row",e.setAttribute("role","presentation"),r.id&&e.setAttribute("data-aggregation-id",r.id);r.fullWidth??i?a(e,r,n,o):s(e,r,n,o),t.appendChild(e)}}function a(t,e,n,o){const i=document.createElement("div");i.className="tbw-aggregation-cell tbw-aggregation-cell-full",i.style.gridColumn="1 / -1";const r="function"==typeof e.label?e.label(o,n):e.label;if(r){const t=document.createElement("span");t.className="tbw-aggregation-label",t.textContent=r,i.appendChild(t)}const a=function(t,e,n){const o=t.aggregators&&Object.keys(t.aggregators).length>0,i=t.cells&&Object.keys(t.cells).length>0;if(!o&&!i)return null;const r=document.createElement("span");r.className="tbw-aggregation-aggregates";for(const a of e){const{value:e,formatter:o}=l(t,a,n);if(null!=e){const t=document.createElement("span");t.className="tbw-aggregation-aggregate",t.setAttribute("data-field",a.field);const n=a.header??a.field,i=o?o(e,a.field,a):String(e);t.textContent=`${n}: ${i}`,r.appendChild(t)}}return r.children.length>0?r:null}(e,n,o);a&&i.appendChild(a),t.appendChild(i)}function s(t,e,n,o){for(const r of n){const n=document.createElement("div");n.className="tbw-aggregation-cell",n.setAttribute("data-field",r.field);const{value:i,formatter:a}=l(e,r,o);n.textContent=null!=i?a?a(i,r.field,r):String(i):"",t.appendChild(n)}const i="function"==typeof e.label?e.label(o,n):e.label;if(i){const e=document.createElement("span");e.className="tbw-aggregation-label",e.textContent=i,t.appendChild(e)}}function l(t,e,o){let i,r;const a=t.aggregators?.[e.field];if(a)if("object"==typeof(s=a)&&null!==s&&"aggFunc"in s){const t=n.aggregatorRegistry.get(a.aggFunc);t&&(i=t(o,e.field,e)),r=a.formatter}else{const t=n.aggregatorRegistry.get(a);t&&(i=t(o,e.field,e))}else if(t.cells&&Object.prototype.hasOwnProperty.call(t.cells,e.field)){const n=t.cells[e.field];i="function"==typeof n?n(o,e.field,e):n}var s;return{value:i,formatter:r}}function g(t,e){const n=document.createElement("div");n.className="tbw-status-panel tbw-status-panel-custom",n.id=`status-panel-${t.id}`;const o=t.render(e);return"string"==typeof o?n.innerHTML=o:n.appendChild(o),n}function c(t,e,n,o,i){const r=n?.sourceRows,a=n?.rows,s=Array.isArray(r)?r.length:t.length,l=Array.isArray(a)?a.length:t.length;return{totalRows:s,filteredRows:i?.cachedResult?.length??l,selectedRows:o?.selected?.size??0,columns:e,rows:t,grid:n}}class d extends e.BaseGridPlugin{name="pinnedRows";styles="@layer tbw-plugins{.tbw-scroll-area{container-type:inline-size}.tbw-footer{flex-shrink:0;z-index:var(--tbw-z-layer-pinned-rows, 20);background:var(--tbw-color-panel-bg);min-width:fit-content}.tbw-pinned-rows{display:flex;align-items:center;justify-content:space-between;padding:var(--tbw-button-padding, var(--tbw-spacing-md, .5rem) var(--tbw-spacing-lg, .75rem));background:var(--tbw-pinned-rows-bg, var(--tbw-color-panel-bg));border-top:1px solid var(--tbw-pinned-rows-border, var(--tbw-color-border));font-size:var(--tbw-font-size-xs, .75rem);color:var(--tbw-pinned-rows-color, var(--tbw-color-fg-muted));min-height:32px;box-sizing:border-box;position:sticky;left:0;min-width:0;width:100cqi}.tbw-pinned-rows-left,.tbw-pinned-rows-center,.tbw-pinned-rows-right{display:flex;align-items:center;gap:var(--tbw-spacing-xl, 1rem)}.tbw-pinned-rows-left{justify-content:flex-start}.tbw-pinned-rows-center{justify-content:center;flex:1}.tbw-pinned-rows-right{justify-content:flex-end}.tbw-status-panel{white-space:nowrap}.tbw-aggregation-rows{min-width:fit-content;background:var(--tbw-aggregation-bg, var(--tbw-color-header-bg))}.tbw-aggregation-rows-top{border-bottom:1px solid var(--tbw-aggregation-border, var(--tbw-color-border))}.tbw-aggregation-rows-bottom{border-top:1px solid var(--tbw-aggregation-border, var(--tbw-color-border))}.tbw-aggregation-row{display:grid;grid-template-columns:var(--tbw-column-template);font-size:var(--tbw-aggregation-font-size, .8em);font-weight:var(--tbw-aggregation-font-weight, 600);position:relative;background:inherit}.tbw-aggregation-row>.tbw-aggregation-label{position:sticky;left:0;grid-row:1;grid-column:1;display:flex;align-items:center;padding:var(--tbw-cell-padding, .125rem .5rem);background:inherit;z-index:1;pointer-events:none}.tbw-aggregation-row>.tbw-aggregation-cell:first-child{grid-column:1;grid-row:1}.tbw-aggregation-cell:not(:empty){position:relative;z-index:2;background:inherit}.tbw-aggregation-cell{padding:var(--tbw-cell-padding, .125rem .5rem);min-height:var(--tbw-row-height, 1.75rem);display:block;align-items:center;align-content:center;border-right:1px solid var(--tbw-color-border-cell);overflow:hidden;text-overflow:ellipsis;white-space:var(--tbw-cell-white-space, nowrap)}.tbw-aggregation-cell:last-child{border-right:0}.tbw-aggregation-cell-full{grid-column:1 / -1;border-right:0;display:flex;align-items:center;gap:var(--tbw-spacing-lg, .75rem)}.tbw-aggregation-label{white-space:nowrap}.tbw-aggregation-aggregates{display:flex;align-items:center;gap:var(--tbw-spacing-lg, .75rem);font-weight:400;opacity:.85}.tbw-aggregation-aggregate{white-space:nowrap}}";get defaultConfig(){return{position:"bottom",showRowCount:!0,showSelectedCount:!0,showFilteredCount:!0}}infoBarElement=null;topAggregationContainer=null;bottomAggregationContainer=null;footerWrapper=null;detach(){this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null),this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null)}afterRender(){const t=this.gridElement;if(!t)return;const e=t.querySelector(".tbw-scroll-area")??t.querySelector(".tbw-grid-content")??t.querySelector(".tbw-grid-root");if(!e)return;this.footerWrapper&&!e.contains(this.footerWrapper)&&(this.footerWrapper=null,this.bottomAggregationContainer=null,this.infoBarElement=null),this.topAggregationContainer&&!e.contains(this.topAggregationContainer)&&(this.topAggregationContainer=null),this.infoBarElement&&!e.contains(this.infoBarElement)&&(this.infoBarElement=null);const n=this.getSelectionState(),a=this.getFilterState(),s=c(this.sourceRows,this.columns,this.gridElement,n,a),l=this.config.aggregationRows||[],g=l.filter(t=>"top"===t.position),d=l.filter(t=>"top"!==t.position);if(g.length>0){if(!this.topAggregationContainer){this.topAggregationContainer=i("top");const n=t.querySelector(".header");n&&n.nextSibling?e.insertBefore(this.topAggregationContainer,n.nextSibling):e.appendChild(this.topAggregationContainer)}r(this.topAggregationContainer,g,this.visibleColumns,this.sourceRows,this.config.fullWidth)}else this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null);const p=!1!==this.config.showRowCount||this.config.showSelectedCount&&s.selectedRows>0||this.config.showFilteredCount&&s.filteredRows!==s.totalRows||this.config.customPanels&&this.config.customPanels.length>0,h=p&&"top"!==this.config.position,f=d.length>0||h;if(p&&"top"===this.config.position)if(this.infoBarElement){const t=o(this.config,s);this.infoBarElement.replaceWith(t),this.infoBarElement=t}else this.infoBarElement=o(this.config,s),e.insertBefore(this.infoBarElement,e.firstChild);else"top"===this.config.position&&this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null);f?(this.footerWrapper||(this.footerWrapper=document.createElement("div"),this.footerWrapper.className="tbw-footer",e.appendChild(this.footerWrapper)),this.footerWrapper.innerHTML="",d.length>0&&(this.bottomAggregationContainer||(this.bottomAggregationContainer=i("bottom")),this.footerWrapper.appendChild(this.bottomAggregationContainer),r(this.bottomAggregationContainer,d,this.visibleColumns,this.sourceRows,this.config.fullWidth)),h&&(this.infoBarElement=o(this.config,s),this.footerWrapper.appendChild(this.infoBarElement))):this.cleanupFooter()}cleanup(){this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null),this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null)}cleanupFooter(){this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.infoBarElement&&"top"!==this.config.position&&(this.infoBarElement.remove(),this.infoBarElement=null)}getSelectionState(){try{return this.grid?.getPluginState?.("selection")??null}catch{return null}}getFilterState(){try{return this.grid?.getPluginState?.("filtering")??null}catch{return null}}refresh(){this.requestRender()}getContext(){const t=this.getSelectionState(),e=this.getFilterState();return c(this.rows,this.columns,this.gridElement,t,e)}addPanel(t){this.config.customPanels||(this.config.customPanels=[]),this.config.customPanels.push(t),this.requestRender()}removePanel(t){this.config.customPanels&&(this.config.customPanels=this.config.customPanels.filter(e=>e.id!==t),this.requestRender())}addAggregationRow(t){this.config.aggregationRows||(this.config.aggregationRows=[]),this.config.aggregationRows.push(t),this.requestRender()}removeAggregationRow(t){this.config.aggregationRows&&(this.config.aggregationRows=this.config.aggregationRows.filter(e=>e.id!==t),this.requestRender())}}t.PinnedRowsPlugin=d,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/plugin/base-plugin"),require("../../core/internal/aggregators")):"function"==typeof define&&define.amd?define(["exports","../../core/plugin/base-plugin","../../core/internal/aggregators"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_pinnedRows={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,n){"use strict";function o(t,e){const n=document.createElement("div");n.className="tbw-pinned-rows",n.setAttribute("role","presentation"),n.setAttribute("aria-live","polite");const o=document.createElement("div");o.className="tbw-pinned-rows-left";const i=document.createElement("div");i.className="tbw-pinned-rows-center";const r=document.createElement("div");if(r.className="tbw-pinned-rows-right",!1!==t.showRowCount){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-row-count",t.textContent=`Total: ${e.totalRows} rows`,o.appendChild(t)}if(t.showFilteredCount&&e.filteredRows!==e.totalRows){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-filtered-count",t.textContent=`Filtered: ${e.filteredRows}`,o.appendChild(t)}if(t.showSelectedCount&&e.selectedRows>0){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-selected-count",t.textContent=`Selected: ${e.selectedRows}`,r.appendChild(t)}if(t.customPanels)for(const a of t.customPanels){const t=g(a,e);switch(a.position){case"left":o.appendChild(t);break;case"center":i.appendChild(t);break;case"right":r.appendChild(t)}}return n.appendChild(o),n.appendChild(i),n.appendChild(r),n}function i(t){const e=document.createElement("div");return e.className=`tbw-aggregation-rows tbw-aggregation-rows-${t}`,e.setAttribute("role","presentation"),e}function r(t,e,n,o,i=!1){t.innerHTML="";for(const r of e){const e=document.createElement("div");e.className="tbw-aggregation-row",e.setAttribute("role","presentation"),r.id&&e.setAttribute("data-aggregation-id",r.id);r.fullWidth??i?a(e,r,n,o):s(e,r,n,o),t.appendChild(e)}}function a(t,e,n,o){const i=document.createElement("div");i.className="tbw-aggregation-cell tbw-aggregation-cell-full",i.style.gridColumn="1 / -1";const r="function"==typeof e.label?e.label(o,n):e.label;if(r){const t=document.createElement("span");t.className="tbw-aggregation-label",t.textContent=r,i.appendChild(t)}const a=function(t,e,n){const o=t.aggregators&&Object.keys(t.aggregators).length>0,i=t.cells&&Object.keys(t.cells).length>0;if(!o&&!i)return null;const r=document.createElement("span");r.className="tbw-aggregation-aggregates";for(const a of e){const{value:e,formatter:o}=l(t,a,n);if(null!=e){const t=document.createElement("span");t.className="tbw-aggregation-aggregate",t.setAttribute("data-field",a.field);const n=a.header??a.field,i=o?o(e,a.field,a):String(e);t.textContent=`${n}: ${i}`,r.appendChild(t)}}return r.children.length>0?r:null}(e,n,o);a&&i.appendChild(a),t.appendChild(i)}function s(t,e,n,o){for(const r of n){const n=document.createElement("div");n.className="tbw-aggregation-cell",n.setAttribute("data-field",r.field);const{value:i,formatter:a}=l(e,r,o);n.textContent=null!=i?a?a(i,r.field,r):String(i):"",t.appendChild(n)}const i="function"==typeof e.label?e.label(o,n):e.label;if(i){const e=document.createElement("span");e.className="tbw-aggregation-label",e.textContent=i,t.appendChild(e)}}function l(t,e,o){let i,r;const a=t.aggregators?.[e.field];if(a)if("object"==typeof(s=a)&&null!==s&&"aggFunc"in s){const t=n.aggregatorRegistry.get(a.aggFunc);t&&(i=t(o,e.field,e)),r=a.formatter}else{const t=n.aggregatorRegistry.get(a);t&&(i=t(o,e.field,e))}else if(t.cells&&Object.prototype.hasOwnProperty.call(t.cells,e.field)){const n=t.cells[e.field];i="function"==typeof n?n(o,e.field,e):n}var s;return{value:i,formatter:r}}function g(t,e){const n=document.createElement("div");n.className="tbw-status-panel tbw-status-panel-custom",n.id=`status-panel-${t.id}`;const o=t.render(e);return"string"==typeof o?n.innerHTML=o:n.appendChild(o),n}function c(t,e,n,o,i){const r=n?.sourceRows,a=n?.rows,s=Array.isArray(r)?r.length:t.length,l=Array.isArray(a)?a.length:t.length,g=s>0?s:l;return{totalRows:g,filteredRows:i?.cachedResult?.length??(l<s?l:g),selectedRows:o?.selected?.size??0,columns:e,rows:t,grid:n}}class d extends e.BaseGridPlugin{name="pinnedRows";styles="@layer tbw-plugins{.tbw-scroll-area{container-type:inline-size}.tbw-footer{flex-shrink:0;z-index:var(--tbw-z-layer-pinned-rows, 20);background:var(--tbw-color-panel-bg);min-width:fit-content}.tbw-pinned-rows{display:flex;align-items:center;justify-content:space-between;padding:var(--tbw-button-padding, var(--tbw-spacing-md, .5rem) var(--tbw-spacing-lg, .75rem));background:var(--tbw-pinned-rows-bg, var(--tbw-color-panel-bg));border-top:1px solid var(--tbw-pinned-rows-border, var(--tbw-color-border));font-size:var(--tbw-font-size-xs, .75rem);color:var(--tbw-pinned-rows-color, var(--tbw-color-fg-muted));min-height:32px;box-sizing:border-box;position:sticky;left:0;min-width:0;width:100cqi}.tbw-pinned-rows-left,.tbw-pinned-rows-center,.tbw-pinned-rows-right{display:flex;align-items:center;gap:var(--tbw-spacing-xl, 1rem)}.tbw-pinned-rows-left{justify-content:flex-start}.tbw-pinned-rows-center{justify-content:center;flex:1}.tbw-pinned-rows-right{justify-content:flex-end}.tbw-status-panel{white-space:nowrap}.tbw-aggregation-rows{min-width:fit-content;background:var(--tbw-aggregation-bg, var(--tbw-color-header-bg))}.tbw-aggregation-rows-top{border-bottom:1px solid var(--tbw-aggregation-border, var(--tbw-color-border))}.tbw-aggregation-rows-bottom{border-top:1px solid var(--tbw-aggregation-border, var(--tbw-color-border))}.tbw-aggregation-row{display:grid;grid-template-columns:var(--tbw-column-template);font-size:var(--tbw-aggregation-font-size, .8em);font-weight:var(--tbw-aggregation-font-weight, 600);position:relative;background:inherit}.tbw-aggregation-row>.tbw-aggregation-label{position:sticky;left:0;grid-row:1;grid-column:1;display:flex;align-items:center;padding:var(--tbw-cell-padding, .125rem .5rem);background:inherit;z-index:1;pointer-events:none}.tbw-aggregation-row>.tbw-aggregation-cell:first-child{grid-column:1;grid-row:1}.tbw-aggregation-cell:not(:empty){position:relative;z-index:2;background:inherit}.tbw-aggregation-cell{padding:var(--tbw-cell-padding, .125rem .5rem);min-height:var(--tbw-row-height, 1.75rem);display:block;align-items:center;align-content:center;border-right:1px solid var(--tbw-color-border-cell);overflow:hidden;text-overflow:ellipsis;white-space:var(--tbw-cell-white-space, nowrap)}.tbw-aggregation-cell:last-child{border-right:0}.tbw-aggregation-cell-full{grid-column:1 / -1;border-right:0;display:flex;align-items:center;gap:var(--tbw-spacing-lg, .75rem)}.tbw-aggregation-label{white-space:nowrap}.tbw-aggregation-aggregates{display:flex;align-items:center;gap:var(--tbw-spacing-lg, .75rem);font-weight:400;opacity:.85}.tbw-aggregation-aggregate{white-space:nowrap}}";get defaultConfig(){return{position:"bottom",showRowCount:!0,showSelectedCount:!0,showFilteredCount:!0}}infoBarElement=null;topAggregationContainer=null;bottomAggregationContainer=null;footerWrapper=null;detach(){this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null),this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null)}afterRender(){const t=this.gridElement;if(!t)return;const e=t.querySelector(".tbw-scroll-area")??t.querySelector(".tbw-grid-content")??t.querySelector(".tbw-grid-root");if(!e)return;this.footerWrapper&&!e.contains(this.footerWrapper)&&(this.footerWrapper=null,this.bottomAggregationContainer=null,this.infoBarElement=null),this.topAggregationContainer&&!e.contains(this.topAggregationContainer)&&(this.topAggregationContainer=null),this.infoBarElement&&!e.contains(this.infoBarElement)&&(this.infoBarElement=null);const n=this.getSelectionState(),a=this.getFilterState(),s=c(this.sourceRows,this.columns,this.gridElement,n,a),l=this.config.aggregationRows||[],g=l.filter(t=>"top"===t.position),d=l.filter(t=>"top"!==t.position);if(g.length>0){if(!this.topAggregationContainer){this.topAggregationContainer=i("top");const n=t.querySelector(".header");n&&n.nextSibling?e.insertBefore(this.topAggregationContainer,n.nextSibling):e.appendChild(this.topAggregationContainer)}r(this.topAggregationContainer,g,this.visibleColumns,this.sourceRows,this.config.fullWidth)}else this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null);const p=!1!==this.config.showRowCount||this.config.showSelectedCount&&s.selectedRows>0||this.config.showFilteredCount&&s.filteredRows!==s.totalRows||this.config.customPanels&&this.config.customPanels.length>0,h=p&&"top"!==this.config.position,f=d.length>0||h;if(p&&"top"===this.config.position)if(this.infoBarElement){const t=o(this.config,s);this.infoBarElement.replaceWith(t),this.infoBarElement=t}else this.infoBarElement=o(this.config,s),e.insertBefore(this.infoBarElement,e.firstChild);else"top"===this.config.position&&this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null);f?(this.footerWrapper||(this.footerWrapper=document.createElement("div"),this.footerWrapper.className="tbw-footer",e.appendChild(this.footerWrapper)),this.footerWrapper.innerHTML="",d.length>0&&(this.bottomAggregationContainer||(this.bottomAggregationContainer=i("bottom")),this.footerWrapper.appendChild(this.bottomAggregationContainer),r(this.bottomAggregationContainer,d,this.visibleColumns,this.sourceRows,this.config.fullWidth)),h&&(this.infoBarElement=o(this.config,s),this.footerWrapper.appendChild(this.infoBarElement))):this.cleanupFooter()}cleanup(){this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null),this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null)}cleanupFooter(){this.footerWrapper&&(this.footerWrapper.remove(),this.footerWrapper=null),this.bottomAggregationContainer&&(this.bottomAggregationContainer.remove(),this.bottomAggregationContainer=null),this.infoBarElement&&"top"!==this.config.position&&(this.infoBarElement.remove(),this.infoBarElement=null)}getSelectionState(){try{return this.grid?.getPluginState?.("selection")??null}catch{return null}}getFilterState(){try{return this.grid?.getPluginState?.("filtering")??null}catch{return null}}refresh(){this.requestRender()}getContext(){const t=this.getSelectionState(),e=this.getFilterState();return c(this.rows,this.columns,this.gridElement,t,e)}addPanel(t){this.config.customPanels||(this.config.customPanels=[]),this.config.customPanels.push(t),this.requestRender()}removePanel(t){this.config.customPanels&&(this.config.customPanels=this.config.customPanels.filter(e=>e.id!==t),this.requestRender())}addAggregationRow(t){this.config.aggregationRows||(this.config.aggregationRows=[]),this.config.aggregationRows.push(t),this.requestRender()}removeAggregationRow(t){this.config.aggregationRows&&(this.config.aggregationRows=this.config.aggregationRows.filter(e=>e.id!==t),this.requestRender())}}t.PinnedRowsPlugin=d,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
2
  //# sourceMappingURL=pinned-rows.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-rows.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-rows/pinned-rows.ts","../../../../../libs/grid/src/lib/plugins/pinned-rows/PinnedRowsPlugin.ts"],"sourcesContent":["/**\n * Status Bar Rendering Logic\n *\n * Pure functions for creating and updating the status bar UI.\n * Includes both info bar and aggregation row rendering.\n */\n\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport type { ColumnConfig } from '../../core/types';\nimport type {\n AggregationRowConfig,\n AggregatorConfig,\n AggregatorDefinition,\n PinnedRowsConfig,\n PinnedRowsContext,\n PinnedRowsPanel,\n} from './types';\n\n/**\n * Check if an aggregator definition is a full config object (with aggFunc and optional formatter).\n */\nfunction isAggregatorConfig(def: AggregatorDefinition): def is AggregatorConfig {\n return typeof def === 'object' && def !== null && 'aggFunc' in def;\n}\n\n/**\n * Creates the info bar DOM element with all configured panels.\n *\n * @param config - The status bar configuration\n * @param context - The current grid context for rendering\n * @returns The complete info bar element\n */\nexport function createInfoBarElement(config: PinnedRowsConfig, context: PinnedRowsContext): HTMLElement {\n const pinnedRows = document.createElement('div');\n pinnedRows.className = 'tbw-pinned-rows';\n pinnedRows.setAttribute('role', 'presentation');\n pinnedRows.setAttribute('aria-live', 'polite');\n\n const left = document.createElement('div');\n left.className = 'tbw-pinned-rows-left';\n\n const center = document.createElement('div');\n center.className = 'tbw-pinned-rows-center';\n\n const right = document.createElement('div');\n right.className = 'tbw-pinned-rows-right';\n\n // Default panels - row count\n if (config.showRowCount !== false) {\n const rowCount = document.createElement('span');\n rowCount.className = 'tbw-status-panel tbw-status-panel-row-count';\n rowCount.textContent = `Total: ${context.totalRows} rows`;\n left.appendChild(rowCount);\n }\n\n // Filtered count panel (only shows when filter is active)\n if (config.showFilteredCount && context.filteredRows !== context.totalRows) {\n const filteredCount = document.createElement('span');\n filteredCount.className = 'tbw-status-panel tbw-status-panel-filtered-count';\n filteredCount.textContent = `Filtered: ${context.filteredRows}`;\n left.appendChild(filteredCount);\n }\n\n // Selected count panel (only shows when rows are selected)\n if (config.showSelectedCount && context.selectedRows > 0) {\n const selectedCount = document.createElement('span');\n selectedCount.className = 'tbw-status-panel tbw-status-panel-selected-count';\n selectedCount.textContent = `Selected: ${context.selectedRows}`;\n right.appendChild(selectedCount);\n }\n\n // Render custom panels\n if (config.customPanels) {\n for (const panel of config.customPanels) {\n const panelEl = renderCustomPanel(panel, context);\n switch (panel.position) {\n case 'left':\n left.appendChild(panelEl);\n break;\n case 'center':\n center.appendChild(panelEl);\n break;\n case 'right':\n right.appendChild(panelEl);\n break;\n }\n }\n }\n\n pinnedRows.appendChild(left);\n pinnedRows.appendChild(center);\n pinnedRows.appendChild(right);\n\n return pinnedRows;\n}\n\n/**\n * Creates a container for aggregation rows at top or bottom.\n *\n * @param position - 'top' or 'bottom'\n * @returns The container element\n */\nexport function createAggregationContainer(position: 'top' | 'bottom'): HTMLElement {\n const container = document.createElement('div');\n container.className = `tbw-aggregation-rows tbw-aggregation-rows-${position}`;\n // Use presentation role since aggregation rows are outside the role=\"grid\" element for layout reasons\n container.setAttribute('role', 'presentation');\n return container;\n}\n\n/**\n * Renders aggregation rows into a container.\n *\n * @param container - The container to render into\n * @param rows - Aggregation row configurations\n * @param columns - Current column configuration\n * @param dataRows - Current row data for aggregation calculations\n * @param globalFullWidth - Global fullWidth default from PinnedRowsConfig (default: false)\n */\nexport function renderAggregationRows(\n container: HTMLElement,\n rows: AggregationRowConfig[],\n columns: ColumnConfig[],\n dataRows: unknown[],\n globalFullWidth = false,\n): void {\n container.innerHTML = '';\n\n for (const rowConfig of rows) {\n const rowEl = document.createElement('div');\n rowEl.className = 'tbw-aggregation-row';\n // Use presentation role since aggregation rows are outside the role=\"grid\" element\n rowEl.setAttribute('role', 'presentation');\n if (rowConfig.id) {\n rowEl.setAttribute('data-aggregation-id', rowConfig.id);\n }\n\n // Per-row fullWidth overrides global default\n const isFullWidth = rowConfig.fullWidth ?? globalFullWidth;\n\n if (isFullWidth) {\n renderFullWidthAggregationRow(rowEl, rowConfig, columns, dataRows);\n } else {\n renderPerColumnAggregationRow(rowEl, rowConfig, columns, dataRows);\n }\n\n container.appendChild(rowEl);\n }\n}\n\n/**\n * Renders a full-width aggregation row: single spanning cell with label and inline aggregated values.\n */\nfunction renderFullWidthAggregationRow(\n rowEl: HTMLElement,\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): void {\n const cell = document.createElement('div');\n cell.className = 'tbw-aggregation-cell tbw-aggregation-cell-full';\n cell.style.gridColumn = '1 / -1';\n\n // Label (static string or dynamic function)\n const labelValue = typeof rowConfig.label === 'function' ? rowConfig.label(dataRows, columns) : rowConfig.label;\n if (labelValue) {\n const labelSpan = document.createElement('span');\n labelSpan.className = 'tbw-aggregation-label';\n labelSpan.textContent = labelValue;\n cell.appendChild(labelSpan);\n }\n\n // Inline aggregated values\n const aggregatesContainer = renderInlineAggregates(rowConfig, columns, dataRows);\n if (aggregatesContainer) {\n cell.appendChild(aggregatesContainer);\n }\n\n // If nothing was added (no label, no aggregates), ensure cell is empty but present\n rowEl.appendChild(cell);\n}\n\n/**\n * Renders per-column aggregation cells aligned to the grid template.\n */\nfunction renderPerColumnAggregationRow(\n rowEl: HTMLElement,\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): void {\n for (const col of columns) {\n const cell = document.createElement('div');\n cell.className = 'tbw-aggregation-cell';\n cell.setAttribute('data-field', col.field);\n\n const { value, formatter } = resolveAggregatedValue(rowConfig, col, dataRows);\n\n if (value != null) {\n cell.textContent = formatter ? formatter(value, col.field, col) : String(value);\n } else {\n cell.textContent = '';\n }\n rowEl.appendChild(cell);\n }\n\n // Overlay label: positioned at the left edge, independent of column alignment\n const labelValue = typeof rowConfig.label === 'function' ? rowConfig.label(dataRows, columns) : rowConfig.label;\n if (labelValue) {\n const labelEl = document.createElement('span');\n labelEl.className = 'tbw-aggregation-label';\n labelEl.textContent = labelValue;\n rowEl.appendChild(labelEl);\n }\n}\n\n/**\n * Resolves the aggregated value for a single column in an aggregation row.\n * Returns the computed value and an optional formatter function.\n */\nfunction resolveAggregatedValue(\n rowConfig: AggregationRowConfig,\n col: ColumnConfig,\n dataRows: unknown[],\n): { value: unknown; formatter?: (value: unknown, field: string, column?: ColumnConfig) => string } {\n let value: unknown;\n let formatter: ((value: unknown, field: string, column?: ColumnConfig) => string) | undefined;\n\n // Check for aggregator first\n const aggDef = rowConfig.aggregators?.[col.field];\n if (aggDef) {\n if (isAggregatorConfig(aggDef)) {\n const aggFn = aggregatorRegistry.get(aggDef.aggFunc);\n if (aggFn) {\n value = aggFn(dataRows, col.field, col);\n }\n formatter = aggDef.formatter;\n } else {\n const aggFn = aggregatorRegistry.get(aggDef);\n if (aggFn) {\n value = aggFn(dataRows, col.field, col);\n }\n }\n } else if (rowConfig.cells && Object.prototype.hasOwnProperty.call(rowConfig.cells, col.field)) {\n const staticVal = rowConfig.cells[col.field];\n if (typeof staticVal === 'function') {\n value = staticVal(dataRows, col.field, col);\n } else {\n value = staticVal;\n }\n }\n\n return { value, formatter };\n}\n\n/**\n * Renders inline aggregate values for a full-width aggregation row.\n * Returns a container element with aggregate spans, or null if no aggregates are defined.\n */\nfunction renderInlineAggregates(\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): HTMLElement | null {\n // Collect fields that have aggregators or cell values\n const hasAggregators = rowConfig.aggregators && Object.keys(rowConfig.aggregators).length > 0;\n const hasCells = rowConfig.cells && Object.keys(rowConfig.cells).length > 0;\n if (!hasAggregators && !hasCells) return null;\n\n const container = document.createElement('span');\n container.className = 'tbw-aggregation-aggregates';\n\n for (const col of columns) {\n const { value, formatter } = resolveAggregatedValue(rowConfig, col, dataRows);\n if (value != null) {\n const span = document.createElement('span');\n span.className = 'tbw-aggregation-aggregate';\n span.setAttribute('data-field', col.field);\n const header = col.header ?? col.field;\n const displayValue = formatter ? formatter(value, col.field, col) : String(value);\n span.textContent = `${header}: ${displayValue}`;\n container.appendChild(span);\n }\n }\n\n return container.children.length > 0 ? container : null;\n}\n\n/**\n * Renders a custom panel element.\n *\n * @param panel - The panel definition\n * @param context - The current grid context\n * @returns The panel DOM element\n */\nfunction renderCustomPanel(panel: PinnedRowsPanel, context: PinnedRowsContext): HTMLElement {\n const panelEl = document.createElement('div');\n panelEl.className = 'tbw-status-panel tbw-status-panel-custom';\n panelEl.id = `status-panel-${panel.id}`;\n\n const content = panel.render(context);\n\n if (typeof content === 'string') {\n panelEl.innerHTML = content;\n } else {\n panelEl.appendChild(content);\n }\n\n return panelEl;\n}\n\n/**\n * Builds the status bar context from grid state and plugin states.\n *\n * @param rows - Current row data\n * @param columns - Current column configuration\n * @param grid - Grid element reference\n * @param selectionState - Optional selection plugin state\n * @param filterState - Optional filtering plugin state\n * @returns The status bar context\n */\nexport function buildContext(\n rows: unknown[],\n columns: unknown[],\n grid: HTMLElement,\n selectionState?: { selected: Set<number> } | null,\n filterState?: { cachedResult: unknown[] | null } | null,\n): PinnedRowsContext {\n // Prefer live counts from the grid element so filteredRows reflects the\n // actual processed row count regardless of which mechanism did the\n // filtering (built-in filter plugin, column filters, custom pipeline, etc.).\n // Fall back to the passed `rows` when the grid element does not expose\n // these properties (e.g. in unit tests using a plain <div>).\n const gridSourceRows = (grid as unknown as { sourceRows?: unknown[] })?.sourceRows;\n const gridProcessedRows = (grid as unknown as { rows?: unknown[] })?.rows;\n const totalRows = Array.isArray(gridSourceRows) ? gridSourceRows.length : rows.length;\n const processedCount = Array.isArray(gridProcessedRows) ? gridProcessedRows.length : rows.length;\n\n return {\n totalRows,\n filteredRows: filterState?.cachedResult?.length ?? processedCount,\n selectedRows: selectionState?.selected?.size ?? 0,\n columns: columns as PinnedRowsContext['columns'],\n rows,\n grid,\n };\n}\n","/**\n * Pinned Rows Plugin (Class-based)\n *\n * Adds info bars and aggregation rows to the grid.\n * - Info bar: Shows row counts, selection info, and custom panels\n * - Aggregation rows: Footer/header rows with computed values (sum, avg, etc.)\n */\n\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport { buildContext, createAggregationContainer, createInfoBarElement, renderAggregationRows } from './pinned-rows';\nimport styles from './pinned-rows.css?inline';\nimport type { AggregationRowConfig, PinnedRowsConfig, PinnedRowsContext, PinnedRowsPanel } from './types';\n\n/**\n * Pinned Rows (Status Bar) Plugin for tbw-grid\n *\n * Creates fixed status bars at the top or bottom of the grid for displaying aggregations,\n * row counts, or custom content. Think of it as the \"totals row\" you'd see in a spreadsheet—\n * always visible regardless of scroll position.\n *\n * ## Installation\n *\n * ```ts\n * import { PinnedRowsPlugin } from '@toolbox-web/grid/plugins/pinned-rows';\n * ```\n *\n * ## Built-in Aggregation Functions\n *\n * | Function | Description |\n * |----------|-------------|\n * | `sum` | Sum of values |\n * | `avg` | Average of values |\n * | `count` | Count of rows |\n * | `min` | Minimum value |\n * | `max` | Maximum value |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-pinned-rows-bg` | `var(--tbw-color-panel-bg)` | Status bar background |\n * | `--tbw-pinned-rows-border` | `var(--tbw-color-border)` | Status bar border |\n *\n * @example Status Bar with Aggregation\n * ```ts\n * import '@toolbox-web/grid';\n * import { PinnedRowsPlugin } from '@toolbox-web/grid/plugins/pinned-rows';\n *\n * grid.gridConfig = {\n * columns: [\n * { field: 'product', header: 'Product' },\n * { field: 'quantity', header: 'Qty', type: 'number' },\n * { field: 'price', header: 'Price', type: 'currency' },\n * ],\n * plugins: [\n * new PinnedRowsPlugin({\n * position: 'bottom',\n * showRowCount: true,\n * aggregationRows: [\n * {\n * id: 'totals',\n * aggregators: { quantity: 'sum', price: 'sum' },\n * cells: { product: 'Totals:' },\n * },\n * ],\n * }),\n * ],\n * };\n * ```\n *\n * @see {@link PinnedRowsConfig} for all configuration options\n * @see {@link AggregationRowConfig} for aggregation row structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class PinnedRowsPlugin extends BaseGridPlugin<PinnedRowsConfig> {\n /** @internal */\n readonly name = 'pinnedRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<PinnedRowsConfig> {\n return {\n position: 'bottom',\n showRowCount: true,\n showSelectedCount: true,\n showFilteredCount: true,\n };\n }\n\n // #region Internal State\n private infoBarElement: HTMLElement | null = null;\n private topAggregationContainer: HTMLElement | null = null;\n private bottomAggregationContainer: HTMLElement | null = null;\n private footerWrapper: HTMLElement | null = null;\n // #endregion\n\n // #region Lifecycle\n /** @internal */\n override detach(): void {\n if (this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n }\n // #endregion\n\n // #region Hooks\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Use .tbw-scroll-area so footer is inside the horizontal scroll area,\n // otherwise fall back to .tbw-grid-content or root container\n const container =\n gridEl.querySelector('.tbw-scroll-area') ??\n gridEl.querySelector('.tbw-grid-content') ??\n gridEl.querySelector('.tbw-grid-root');\n if (!container) return;\n\n // Clear orphaned element references if they were removed from the DOM\n // (e.g., by buildGridDOMIntoShadow calling replaceChildren())\n // We check if the element is still inside the container rather than isConnected,\n // because in unit tests the mock grid may not be attached to document.body\n if (this.footerWrapper && !container.contains(this.footerWrapper)) {\n this.footerWrapper = null;\n this.bottomAggregationContainer = null;\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer && !container.contains(this.topAggregationContainer)) {\n this.topAggregationContainer = null;\n }\n if (this.infoBarElement && !container.contains(this.infoBarElement)) {\n this.infoBarElement = null;\n }\n\n // Build context with plugin states\n const selectionState = this.getSelectionState();\n const filterState = this.getFilterState();\n\n const context = buildContext(\n this.sourceRows as unknown[],\n this.columns as unknown[],\n this.gridElement,\n selectionState,\n filterState,\n );\n\n // #region Handle Aggregation Rows\n const aggregationRows = this.config.aggregationRows || [];\n const topRows = aggregationRows.filter((r) => r.position === 'top');\n const bottomRows = aggregationRows.filter((r) => r.position !== 'top');\n\n // Top aggregation rows\n if (topRows.length > 0) {\n if (!this.topAggregationContainer) {\n this.topAggregationContainer = createAggregationContainer('top');\n const header = gridEl.querySelector('.header');\n if (header && header.nextSibling) {\n container.insertBefore(this.topAggregationContainer, header.nextSibling);\n } else {\n container.appendChild(this.topAggregationContainer);\n }\n }\n renderAggregationRows(\n this.topAggregationContainer,\n topRows,\n this.visibleColumns as ColumnConfig[],\n this.sourceRows as unknown[],\n this.config.fullWidth,\n );\n } else if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n\n // Handle footer\n const hasInfoContent =\n this.config.showRowCount !== false ||\n (this.config.showSelectedCount && context.selectedRows > 0) ||\n (this.config.showFilteredCount && context.filteredRows !== context.totalRows) ||\n (this.config.customPanels && this.config.customPanels.length > 0);\n const hasBottomInfoBar = hasInfoContent && this.config.position !== 'top';\n const needsFooter = bottomRows.length > 0 || hasBottomInfoBar;\n\n // Handle top info bar\n if (hasInfoContent && this.config.position === 'top') {\n if (!this.infoBarElement) {\n this.infoBarElement = createInfoBarElement(this.config, context);\n container.insertBefore(this.infoBarElement, container.firstChild);\n } else {\n const newInfoBar = createInfoBarElement(this.config, context);\n this.infoBarElement.replaceWith(newInfoBar);\n this.infoBarElement = newInfoBar;\n }\n } else if (this.config.position === 'top' && this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n\n // Create/manage footer wrapper\n if (needsFooter) {\n if (!this.footerWrapper) {\n this.footerWrapper = document.createElement('div');\n this.footerWrapper.className = 'tbw-footer';\n container.appendChild(this.footerWrapper);\n }\n\n this.footerWrapper.innerHTML = '';\n\n if (bottomRows.length > 0) {\n if (!this.bottomAggregationContainer) {\n this.bottomAggregationContainer = createAggregationContainer('bottom');\n }\n this.footerWrapper.appendChild(this.bottomAggregationContainer);\n renderAggregationRows(\n this.bottomAggregationContainer,\n bottomRows,\n this.visibleColumns as ColumnConfig[],\n this.sourceRows as unknown[],\n this.config.fullWidth,\n );\n }\n\n if (hasBottomInfoBar) {\n this.infoBarElement = createInfoBarElement(this.config, context);\n this.footerWrapper.appendChild(this.infoBarElement);\n }\n } else {\n this.cleanupFooter();\n }\n // #endregion\n }\n // #endregion\n\n // #region Private Methods\n private cleanup(): void {\n if (this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n }\n\n private cleanupFooter(): void {\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.infoBarElement && this.config.position !== 'top') {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n }\n\n private getSelectionState(): { selected: Set<number> } | null {\n // Try to get selection plugin state\n try {\n return (this.grid?.getPluginState?.('selection') as { selected: Set<number> } | null) ?? null;\n } catch {\n return null;\n }\n }\n\n private getFilterState(): { cachedResult: unknown[] | null } | null {\n try {\n return (this.grid?.getPluginState?.('filtering') as { cachedResult: unknown[] | null } | null) ?? null;\n } catch {\n return null;\n }\n }\n // #endregion\n\n // #region Public API\n /**\n * Refresh the status bar to reflect current grid state.\n */\n refresh(): void {\n this.requestRender();\n }\n\n /**\n * Get the current status bar context.\n * @returns The context with row counts and other info\n */\n getContext(): PinnedRowsContext {\n const selectionState = this.getSelectionState();\n const filterState = this.getFilterState();\n\n return buildContext(\n this.rows as unknown[],\n this.columns as unknown[],\n this.gridElement,\n selectionState,\n filterState,\n );\n }\n\n /**\n * Add a custom panel to the info bar.\n * @param panel - The panel configuration to add\n */\n addPanel(panel: PinnedRowsPanel): void {\n if (!this.config.customPanels) {\n this.config.customPanels = [];\n }\n this.config.customPanels.push(panel);\n this.requestRender();\n }\n\n /**\n * Remove a custom panel by ID.\n * @param id - The panel ID to remove\n */\n removePanel(id: string): void {\n if (this.config.customPanels) {\n this.config.customPanels = this.config.customPanels.filter((p) => p.id !== id);\n this.requestRender();\n }\n }\n\n /**\n * Add an aggregation row.\n * @param row - The aggregation row configuration\n */\n addAggregationRow(row: AggregationRowConfig): void {\n if (!this.config.aggregationRows) {\n this.config.aggregationRows = [];\n }\n this.config.aggregationRows.push(row);\n this.requestRender();\n }\n\n /**\n * Remove an aggregation row by ID.\n * @param id - The aggregation row ID to remove\n */\n removeAggregationRow(id: string): void {\n if (this.config.aggregationRows) {\n this.config.aggregationRows = this.config.aggregationRows.filter((r) => r.id !== id);\n this.requestRender();\n }\n }\n // #endregion\n}\n"],"names":["createInfoBarElement","config","context","pinnedRows","document","createElement","className","setAttribute","left","center","right","showRowCount","rowCount","textContent","totalRows","appendChild","showFilteredCount","filteredRows","filteredCount","showSelectedCount","selectedRows","selectedCount","customPanels","panel","panelEl","renderCustomPanel","position","createAggregationContainer","container","renderAggregationRows","rows","columns","dataRows","globalFullWidth","innerHTML","rowConfig","rowEl","id","fullWidth","renderFullWidthAggregationRow","renderPerColumnAggregationRow","cell","style","gridColumn","labelValue","label","labelSpan","aggregatesContainer","hasAggregators","aggregators","Object","keys","length","hasCells","cells","col","value","formatter","resolveAggregatedValue","span","field","header","displayValue","String","children","renderInlineAggregates","labelEl","aggDef","def","aggFn","aggregatorRegistry","get","aggFunc","prototype","hasOwnProperty","call","staticVal","content","render","buildContext","grid","selectionState","filterState","gridSourceRows","sourceRows","gridProcessedRows","Array","isArray","processedCount","cachedResult","selected","size","PinnedRowsPlugin","BaseGridPlugin","name","styles","defaultConfig","infoBarElement","topAggregationContainer","bottomAggregationContainer","footerWrapper","detach","this","remove","afterRender","gridEl","gridElement","querySelector","contains","getSelectionState","getFilterState","aggregationRows","topRows","filter","r","bottomRows","nextSibling","insertBefore","visibleColumns","hasInfoContent","hasBottomInfoBar","needsFooter","newInfoBar","replaceWith","firstChild","cleanupFooter","cleanup","getPluginState","refresh","requestRender","getContext","addPanel","push","removePanel","p","addAggregationRow","row","removeAggregationRow"],"mappings":"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"}
1
+ {"version":3,"file":"pinned-rows.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-rows/pinned-rows.ts","../../../../../libs/grid/src/lib/plugins/pinned-rows/PinnedRowsPlugin.ts"],"sourcesContent":["/**\n * Status Bar Rendering Logic\n *\n * Pure functions for creating and updating the status bar UI.\n * Includes both info bar and aggregation row rendering.\n */\n\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport type { ColumnConfig } from '../../core/types';\nimport type {\n AggregationRowConfig,\n AggregatorConfig,\n AggregatorDefinition,\n PinnedRowsConfig,\n PinnedRowsContext,\n PinnedRowsPanel,\n} from './types';\n\n/**\n * Check if an aggregator definition is a full config object (with aggFunc and optional formatter).\n */\nfunction isAggregatorConfig(def: AggregatorDefinition): def is AggregatorConfig {\n return typeof def === 'object' && def !== null && 'aggFunc' in def;\n}\n\n/**\n * Creates the info bar DOM element with all configured panels.\n *\n * @param config - The status bar configuration\n * @param context - The current grid context for rendering\n * @returns The complete info bar element\n */\nexport function createInfoBarElement(config: PinnedRowsConfig, context: PinnedRowsContext): HTMLElement {\n const pinnedRows = document.createElement('div');\n pinnedRows.className = 'tbw-pinned-rows';\n pinnedRows.setAttribute('role', 'presentation');\n pinnedRows.setAttribute('aria-live', 'polite');\n\n const left = document.createElement('div');\n left.className = 'tbw-pinned-rows-left';\n\n const center = document.createElement('div');\n center.className = 'tbw-pinned-rows-center';\n\n const right = document.createElement('div');\n right.className = 'tbw-pinned-rows-right';\n\n // Default panels - row count\n if (config.showRowCount !== false) {\n const rowCount = document.createElement('span');\n rowCount.className = 'tbw-status-panel tbw-status-panel-row-count';\n rowCount.textContent = `Total: ${context.totalRows} rows`;\n left.appendChild(rowCount);\n }\n\n // Filtered count panel (only shows when filter is active)\n if (config.showFilteredCount && context.filteredRows !== context.totalRows) {\n const filteredCount = document.createElement('span');\n filteredCount.className = 'tbw-status-panel tbw-status-panel-filtered-count';\n filteredCount.textContent = `Filtered: ${context.filteredRows}`;\n left.appendChild(filteredCount);\n }\n\n // Selected count panel (only shows when rows are selected)\n if (config.showSelectedCount && context.selectedRows > 0) {\n const selectedCount = document.createElement('span');\n selectedCount.className = 'tbw-status-panel tbw-status-panel-selected-count';\n selectedCount.textContent = `Selected: ${context.selectedRows}`;\n right.appendChild(selectedCount);\n }\n\n // Render custom panels\n if (config.customPanels) {\n for (const panel of config.customPanels) {\n const panelEl = renderCustomPanel(panel, context);\n switch (panel.position) {\n case 'left':\n left.appendChild(panelEl);\n break;\n case 'center':\n center.appendChild(panelEl);\n break;\n case 'right':\n right.appendChild(panelEl);\n break;\n }\n }\n }\n\n pinnedRows.appendChild(left);\n pinnedRows.appendChild(center);\n pinnedRows.appendChild(right);\n\n return pinnedRows;\n}\n\n/**\n * Creates a container for aggregation rows at top or bottom.\n *\n * @param position - 'top' or 'bottom'\n * @returns The container element\n */\nexport function createAggregationContainer(position: 'top' | 'bottom'): HTMLElement {\n const container = document.createElement('div');\n container.className = `tbw-aggregation-rows tbw-aggregation-rows-${position}`;\n // Use presentation role since aggregation rows are outside the role=\"grid\" element for layout reasons\n container.setAttribute('role', 'presentation');\n return container;\n}\n\n/**\n * Renders aggregation rows into a container.\n *\n * @param container - The container to render into\n * @param rows - Aggregation row configurations\n * @param columns - Current column configuration\n * @param dataRows - Current row data for aggregation calculations\n * @param globalFullWidth - Global fullWidth default from PinnedRowsConfig (default: false)\n */\nexport function renderAggregationRows(\n container: HTMLElement,\n rows: AggregationRowConfig[],\n columns: ColumnConfig[],\n dataRows: unknown[],\n globalFullWidth = false,\n): void {\n container.innerHTML = '';\n\n for (const rowConfig of rows) {\n const rowEl = document.createElement('div');\n rowEl.className = 'tbw-aggregation-row';\n // Use presentation role since aggregation rows are outside the role=\"grid\" element\n rowEl.setAttribute('role', 'presentation');\n if (rowConfig.id) {\n rowEl.setAttribute('data-aggregation-id', rowConfig.id);\n }\n\n // Per-row fullWidth overrides global default\n const isFullWidth = rowConfig.fullWidth ?? globalFullWidth;\n\n if (isFullWidth) {\n renderFullWidthAggregationRow(rowEl, rowConfig, columns, dataRows);\n } else {\n renderPerColumnAggregationRow(rowEl, rowConfig, columns, dataRows);\n }\n\n container.appendChild(rowEl);\n }\n}\n\n/**\n * Renders a full-width aggregation row: single spanning cell with label and inline aggregated values.\n */\nfunction renderFullWidthAggregationRow(\n rowEl: HTMLElement,\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): void {\n const cell = document.createElement('div');\n cell.className = 'tbw-aggregation-cell tbw-aggregation-cell-full';\n cell.style.gridColumn = '1 / -1';\n\n // Label (static string or dynamic function)\n const labelValue = typeof rowConfig.label === 'function' ? rowConfig.label(dataRows, columns) : rowConfig.label;\n if (labelValue) {\n const labelSpan = document.createElement('span');\n labelSpan.className = 'tbw-aggregation-label';\n labelSpan.textContent = labelValue;\n cell.appendChild(labelSpan);\n }\n\n // Inline aggregated values\n const aggregatesContainer = renderInlineAggregates(rowConfig, columns, dataRows);\n if (aggregatesContainer) {\n cell.appendChild(aggregatesContainer);\n }\n\n // If nothing was added (no label, no aggregates), ensure cell is empty but present\n rowEl.appendChild(cell);\n}\n\n/**\n * Renders per-column aggregation cells aligned to the grid template.\n */\nfunction renderPerColumnAggregationRow(\n rowEl: HTMLElement,\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): void {\n for (const col of columns) {\n const cell = document.createElement('div');\n cell.className = 'tbw-aggregation-cell';\n cell.setAttribute('data-field', col.field);\n\n const { value, formatter } = resolveAggregatedValue(rowConfig, col, dataRows);\n\n if (value != null) {\n cell.textContent = formatter ? formatter(value, col.field, col) : String(value);\n } else {\n cell.textContent = '';\n }\n rowEl.appendChild(cell);\n }\n\n // Overlay label: positioned at the left edge, independent of column alignment\n const labelValue = typeof rowConfig.label === 'function' ? rowConfig.label(dataRows, columns) : rowConfig.label;\n if (labelValue) {\n const labelEl = document.createElement('span');\n labelEl.className = 'tbw-aggregation-label';\n labelEl.textContent = labelValue;\n rowEl.appendChild(labelEl);\n }\n}\n\n/**\n * Resolves the aggregated value for a single column in an aggregation row.\n * Returns the computed value and an optional formatter function.\n */\nfunction resolveAggregatedValue(\n rowConfig: AggregationRowConfig,\n col: ColumnConfig,\n dataRows: unknown[],\n): { value: unknown; formatter?: (value: unknown, field: string, column?: ColumnConfig) => string } {\n let value: unknown;\n let formatter: ((value: unknown, field: string, column?: ColumnConfig) => string) | undefined;\n\n // Check for aggregator first\n const aggDef = rowConfig.aggregators?.[col.field];\n if (aggDef) {\n if (isAggregatorConfig(aggDef)) {\n const aggFn = aggregatorRegistry.get(aggDef.aggFunc);\n if (aggFn) {\n value = aggFn(dataRows, col.field, col);\n }\n formatter = aggDef.formatter;\n } else {\n const aggFn = aggregatorRegistry.get(aggDef);\n if (aggFn) {\n value = aggFn(dataRows, col.field, col);\n }\n }\n } else if (rowConfig.cells && Object.prototype.hasOwnProperty.call(rowConfig.cells, col.field)) {\n const staticVal = rowConfig.cells[col.field];\n if (typeof staticVal === 'function') {\n value = staticVal(dataRows, col.field, col);\n } else {\n value = staticVal;\n }\n }\n\n return { value, formatter };\n}\n\n/**\n * Renders inline aggregate values for a full-width aggregation row.\n * Returns a container element with aggregate spans, or null if no aggregates are defined.\n */\nfunction renderInlineAggregates(\n rowConfig: AggregationRowConfig,\n columns: ColumnConfig[],\n dataRows: unknown[],\n): HTMLElement | null {\n // Collect fields that have aggregators or cell values\n const hasAggregators = rowConfig.aggregators && Object.keys(rowConfig.aggregators).length > 0;\n const hasCells = rowConfig.cells && Object.keys(rowConfig.cells).length > 0;\n if (!hasAggregators && !hasCells) return null;\n\n const container = document.createElement('span');\n container.className = 'tbw-aggregation-aggregates';\n\n for (const col of columns) {\n const { value, formatter } = resolveAggregatedValue(rowConfig, col, dataRows);\n if (value != null) {\n const span = document.createElement('span');\n span.className = 'tbw-aggregation-aggregate';\n span.setAttribute('data-field', col.field);\n const header = col.header ?? col.field;\n const displayValue = formatter ? formatter(value, col.field, col) : String(value);\n span.textContent = `${header}: ${displayValue}`;\n container.appendChild(span);\n }\n }\n\n return container.children.length > 0 ? container : null;\n}\n\n/**\n * Renders a custom panel element.\n *\n * @param panel - The panel definition\n * @param context - The current grid context\n * @returns The panel DOM element\n */\nfunction renderCustomPanel(panel: PinnedRowsPanel, context: PinnedRowsContext): HTMLElement {\n const panelEl = document.createElement('div');\n panelEl.className = 'tbw-status-panel tbw-status-panel-custom';\n panelEl.id = `status-panel-${panel.id}`;\n\n const content = panel.render(context);\n\n if (typeof content === 'string') {\n panelEl.innerHTML = content;\n } else {\n panelEl.appendChild(content);\n }\n\n return panelEl;\n}\n\n/**\n * Builds the status bar context from grid state and plugin states.\n *\n * @param rows - Current row data\n * @param columns - Current column configuration\n * @param grid - Grid element reference\n * @param selectionState - Optional selection plugin state\n * @param filterState - Optional filtering plugin state\n * @returns The status bar context\n */\nexport function buildContext(\n rows: unknown[],\n columns: unknown[],\n grid: HTMLElement,\n selectionState?: { selected: Set<number> } | null,\n filterState?: { cachedResult: unknown[] | null } | null,\n): PinnedRowsContext {\n // Prefer live counts from the grid element so filteredRows reflects the\n // actual processed row count regardless of which mechanism did the\n // filtering (built-in filter plugin, column filters, custom pipeline, etc.).\n // Fall back to the passed `rows` when the grid element does not expose\n // these properties (e.g. in unit tests using a plain <div>).\n //\n // When `sourceRows` is empty (e.g. ServerSidePlugin owns the data and the\n // user never assigned `grid.rows = ...`), fall back to the processed count\n // so we report a meaningful total instead of 0.\n const gridSourceRows = (grid as unknown as { sourceRows?: unknown[] })?.sourceRows;\n const gridProcessedRows = (grid as unknown as { rows?: unknown[] })?.rows;\n const sourceLen = Array.isArray(gridSourceRows) ? gridSourceRows.length : rows.length;\n const processedCount = Array.isArray(gridProcessedRows) ? gridProcessedRows.length : rows.length;\n const totalRows = sourceLen > 0 ? sourceLen : processedCount;\n\n // filteredRows resolution (in priority order):\n // 1. Plugin filter state's cachedResult (authoritative when filtering plugin owns the data)\n // 2. Custom pipeline signal: processed < source means the host filtered rows itself\n // 3. Default to totalRows so the renderer's `filteredRows !== totalRows` check\n // hides the panel when no filter is active. This is critical for the\n // server-side case where processedCount > sourceLen (placeholders inflate\n // grid.rows beyond grid.sourceRows.length) and would otherwise show a\n // spurious \"Filtered: N\" panel.\n const filteredRows = filterState?.cachedResult?.length ?? (processedCount < sourceLen ? processedCount : totalRows);\n\n return {\n totalRows,\n filteredRows,\n selectedRows: selectionState?.selected?.size ?? 0,\n columns: columns as PinnedRowsContext['columns'],\n rows,\n grid,\n };\n}\n","/**\n * Pinned Rows Plugin (Class-based)\n *\n * Adds info bars and aggregation rows to the grid.\n * - Info bar: Shows row counts, selection info, and custom panels\n * - Aggregation rows: Footer/header rows with computed values (sum, avg, etc.)\n */\n\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport { buildContext, createAggregationContainer, createInfoBarElement, renderAggregationRows } from './pinned-rows';\nimport styles from './pinned-rows.css?inline';\nimport type { AggregationRowConfig, PinnedRowsConfig, PinnedRowsContext, PinnedRowsPanel } from './types';\n\n/**\n * Pinned Rows (Status Bar) Plugin for tbw-grid\n *\n * Creates fixed status bars at the top or bottom of the grid for displaying aggregations,\n * row counts, or custom content. Think of it as the \"totals row\" you'd see in a spreadsheet—\n * always visible regardless of scroll position.\n *\n * ## Installation\n *\n * ```ts\n * import { PinnedRowsPlugin } from '@toolbox-web/grid/plugins/pinned-rows';\n * ```\n *\n * ## Built-in Aggregation Functions\n *\n * | Function | Description |\n * |----------|-------------|\n * | `sum` | Sum of values |\n * | `avg` | Average of values |\n * | `count` | Count of rows |\n * | `min` | Minimum value |\n * | `max` | Maximum value |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-pinned-rows-bg` | `var(--tbw-color-panel-bg)` | Status bar background |\n * | `--tbw-pinned-rows-border` | `var(--tbw-color-border)` | Status bar border |\n *\n * @example Status Bar with Aggregation\n * ```ts\n * import '@toolbox-web/grid';\n * import { PinnedRowsPlugin } from '@toolbox-web/grid/plugins/pinned-rows';\n *\n * grid.gridConfig = {\n * columns: [\n * { field: 'product', header: 'Product' },\n * { field: 'quantity', header: 'Qty', type: 'number' },\n * { field: 'price', header: 'Price', type: 'currency' },\n * ],\n * plugins: [\n * new PinnedRowsPlugin({\n * position: 'bottom',\n * showRowCount: true,\n * aggregationRows: [\n * {\n * id: 'totals',\n * aggregators: { quantity: 'sum', price: 'sum' },\n * cells: { product: 'Totals:' },\n * },\n * ],\n * }),\n * ],\n * };\n * ```\n *\n * @see {@link PinnedRowsConfig} for all configuration options\n * @see {@link AggregationRowConfig} for aggregation row structure\n *\n * @internal Extends BaseGridPlugin\n */\nexport class PinnedRowsPlugin extends BaseGridPlugin<PinnedRowsConfig> {\n /** @internal */\n readonly name = 'pinnedRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<PinnedRowsConfig> {\n return {\n position: 'bottom',\n showRowCount: true,\n showSelectedCount: true,\n showFilteredCount: true,\n };\n }\n\n // #region Internal State\n private infoBarElement: HTMLElement | null = null;\n private topAggregationContainer: HTMLElement | null = null;\n private bottomAggregationContainer: HTMLElement | null = null;\n private footerWrapper: HTMLElement | null = null;\n // #endregion\n\n // #region Lifecycle\n /** @internal */\n override detach(): void {\n if (this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n }\n // #endregion\n\n // #region Hooks\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Use .tbw-scroll-area so footer is inside the horizontal scroll area,\n // otherwise fall back to .tbw-grid-content or root container\n const container =\n gridEl.querySelector('.tbw-scroll-area') ??\n gridEl.querySelector('.tbw-grid-content') ??\n gridEl.querySelector('.tbw-grid-root');\n if (!container) return;\n\n // Clear orphaned element references if they were removed from the DOM\n // (e.g., by buildGridDOMIntoShadow calling replaceChildren())\n // We check if the element is still inside the container rather than isConnected,\n // because in unit tests the mock grid may not be attached to document.body\n if (this.footerWrapper && !container.contains(this.footerWrapper)) {\n this.footerWrapper = null;\n this.bottomAggregationContainer = null;\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer && !container.contains(this.topAggregationContainer)) {\n this.topAggregationContainer = null;\n }\n if (this.infoBarElement && !container.contains(this.infoBarElement)) {\n this.infoBarElement = null;\n }\n\n // Build context with plugin states\n const selectionState = this.getSelectionState();\n const filterState = this.getFilterState();\n\n const context = buildContext(\n this.sourceRows as unknown[],\n this.columns as unknown[],\n this.gridElement,\n selectionState,\n filterState,\n );\n\n // #region Handle Aggregation Rows\n const aggregationRows = this.config.aggregationRows || [];\n const topRows = aggregationRows.filter((r) => r.position === 'top');\n const bottomRows = aggregationRows.filter((r) => r.position !== 'top');\n\n // Top aggregation rows\n if (topRows.length > 0) {\n if (!this.topAggregationContainer) {\n this.topAggregationContainer = createAggregationContainer('top');\n const header = gridEl.querySelector('.header');\n if (header && header.nextSibling) {\n container.insertBefore(this.topAggregationContainer, header.nextSibling);\n } else {\n container.appendChild(this.topAggregationContainer);\n }\n }\n renderAggregationRows(\n this.topAggregationContainer,\n topRows,\n this.visibleColumns as ColumnConfig[],\n this.sourceRows as unknown[],\n this.config.fullWidth,\n );\n } else if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n\n // Handle footer\n const hasInfoContent =\n this.config.showRowCount !== false ||\n (this.config.showSelectedCount && context.selectedRows > 0) ||\n (this.config.showFilteredCount && context.filteredRows !== context.totalRows) ||\n (this.config.customPanels && this.config.customPanels.length > 0);\n const hasBottomInfoBar = hasInfoContent && this.config.position !== 'top';\n const needsFooter = bottomRows.length > 0 || hasBottomInfoBar;\n\n // Handle top info bar\n if (hasInfoContent && this.config.position === 'top') {\n if (!this.infoBarElement) {\n this.infoBarElement = createInfoBarElement(this.config, context);\n container.insertBefore(this.infoBarElement, container.firstChild);\n } else {\n const newInfoBar = createInfoBarElement(this.config, context);\n this.infoBarElement.replaceWith(newInfoBar);\n this.infoBarElement = newInfoBar;\n }\n } else if (this.config.position === 'top' && this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n\n // Create/manage footer wrapper\n if (needsFooter) {\n if (!this.footerWrapper) {\n this.footerWrapper = document.createElement('div');\n this.footerWrapper.className = 'tbw-footer';\n container.appendChild(this.footerWrapper);\n }\n\n this.footerWrapper.innerHTML = '';\n\n if (bottomRows.length > 0) {\n if (!this.bottomAggregationContainer) {\n this.bottomAggregationContainer = createAggregationContainer('bottom');\n }\n this.footerWrapper.appendChild(this.bottomAggregationContainer);\n renderAggregationRows(\n this.bottomAggregationContainer,\n bottomRows,\n this.visibleColumns as ColumnConfig[],\n this.sourceRows as unknown[],\n this.config.fullWidth,\n );\n }\n\n if (hasBottomInfoBar) {\n this.infoBarElement = createInfoBarElement(this.config, context);\n this.footerWrapper.appendChild(this.infoBarElement);\n }\n } else {\n this.cleanupFooter();\n }\n // #endregion\n }\n // #endregion\n\n // #region Private Methods\n private cleanup(): void {\n if (this.infoBarElement) {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n if (this.topAggregationContainer) {\n this.topAggregationContainer.remove();\n this.topAggregationContainer = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n }\n\n private cleanupFooter(): void {\n if (this.footerWrapper) {\n this.footerWrapper.remove();\n this.footerWrapper = null;\n }\n if (this.bottomAggregationContainer) {\n this.bottomAggregationContainer.remove();\n this.bottomAggregationContainer = null;\n }\n if (this.infoBarElement && this.config.position !== 'top') {\n this.infoBarElement.remove();\n this.infoBarElement = null;\n }\n }\n\n private getSelectionState(): { selected: Set<number> } | null {\n // Try to get selection plugin state\n try {\n return (this.grid?.getPluginState?.('selection') as { selected: Set<number> } | null) ?? null;\n } catch {\n return null;\n }\n }\n\n private getFilterState(): { cachedResult: unknown[] | null } | null {\n try {\n return (this.grid?.getPluginState?.('filtering') as { cachedResult: unknown[] | null } | null) ?? null;\n } catch {\n return null;\n }\n }\n // #endregion\n\n // #region Public API\n /**\n * Refresh the status bar to reflect current grid state.\n */\n refresh(): void {\n this.requestRender();\n }\n\n /**\n * Get the current status bar context.\n * @returns The context with row counts and other info\n */\n getContext(): PinnedRowsContext {\n const selectionState = this.getSelectionState();\n const filterState = this.getFilterState();\n\n return buildContext(\n this.rows as unknown[],\n this.columns as unknown[],\n this.gridElement,\n selectionState,\n filterState,\n );\n }\n\n /**\n * Add a custom panel to the info bar.\n * @param panel - The panel configuration to add\n */\n addPanel(panel: PinnedRowsPanel): void {\n if (!this.config.customPanels) {\n this.config.customPanels = [];\n }\n this.config.customPanels.push(panel);\n this.requestRender();\n }\n\n /**\n * Remove a custom panel by ID.\n * @param id - The panel ID to remove\n */\n removePanel(id: string): void {\n if (this.config.customPanels) {\n this.config.customPanels = this.config.customPanels.filter((p) => p.id !== id);\n this.requestRender();\n }\n }\n\n /**\n * Add an aggregation row.\n * @param row - The aggregation row configuration\n */\n addAggregationRow(row: AggregationRowConfig): void {\n if (!this.config.aggregationRows) {\n this.config.aggregationRows = [];\n }\n this.config.aggregationRows.push(row);\n this.requestRender();\n }\n\n /**\n * Remove an aggregation row by ID.\n * @param id - The aggregation row ID to remove\n */\n removeAggregationRow(id: string): void {\n if (this.config.aggregationRows) {\n this.config.aggregationRows = this.config.aggregationRows.filter((r) => r.id !== id);\n this.requestRender();\n }\n }\n // #endregion\n}\n"],"names":["createInfoBarElement","config","context","pinnedRows","document","createElement","className","setAttribute","left","center","right","showRowCount","rowCount","textContent","totalRows","appendChild","showFilteredCount","filteredRows","filteredCount","showSelectedCount","selectedRows","selectedCount","customPanels","panel","panelEl","renderCustomPanel","position","createAggregationContainer","container","renderAggregationRows","rows","columns","dataRows","globalFullWidth","innerHTML","rowConfig","rowEl","id","fullWidth","renderFullWidthAggregationRow","renderPerColumnAggregationRow","cell","style","gridColumn","labelValue","label","labelSpan","aggregatesContainer","hasAggregators","aggregators","Object","keys","length","hasCells","cells","col","value","formatter","resolveAggregatedValue","span","field","header","displayValue","String","children","renderInlineAggregates","labelEl","aggDef","def","aggFn","aggregatorRegistry","get","aggFunc","prototype","hasOwnProperty","call","staticVal","content","render","buildContext","grid","selectionState","filterState","gridSourceRows","sourceRows","gridProcessedRows","sourceLen","Array","isArray","processedCount","cachedResult","selected","size","PinnedRowsPlugin","BaseGridPlugin","name","styles","defaultConfig","infoBarElement","topAggregationContainer","bottomAggregationContainer","footerWrapper","detach","this","remove","afterRender","gridEl","gridElement","querySelector","contains","getSelectionState","getFilterState","aggregationRows","topRows","filter","r","bottomRows","nextSibling","insertBefore","visibleColumns","hasInfoContent","hasBottomInfoBar","needsFooter","newInfoBar","replaceWith","firstChild","cleanupFooter","cleanup","getPluginState","refresh","requestRender","getContext","addPanel","push","removePanel","p","addAggregationRow","row","removeAggregationRow"],"mappings":"6aAgCO,SAASA,EAAqBC,EAA0BC,GAC7D,MAAMC,EAAaC,SAASC,cAAc,OAC1CF,EAAWG,UAAY,kBACvBH,EAAWI,aAAa,OAAQ,gBAChCJ,EAAWI,aAAa,YAAa,UAErC,MAAMC,EAAOJ,SAASC,cAAc,OACpCG,EAAKF,UAAY,uBAEjB,MAAMG,EAASL,SAASC,cAAc,OACtCI,EAAOH,UAAY,yBAEnB,MAAMI,EAAQN,SAASC,cAAc,OAIrC,GAHAK,EAAMJ,UAAY,yBAGU,IAAxBL,EAAOU,aAAwB,CACjC,MAAMC,EAAWR,SAASC,cAAc,QACxCO,EAASN,UAAY,8CACrBM,EAASC,YAAc,UAAUX,EAAQY,iBACzCN,EAAKO,YAAYH,EACnB,CAGA,GAAIX,EAAOe,mBAAqBd,EAAQe,eAAiBf,EAAQY,UAAW,CAC1E,MAAMI,EAAgBd,SAASC,cAAc,QAC7Ca,EAAcZ,UAAY,mDAC1BY,EAAcL,YAAc,aAAaX,EAAQe,eACjDT,EAAKO,YAAYG,EACnB,CAGA,GAAIjB,EAAOkB,mBAAqBjB,EAAQkB,aAAe,EAAG,CACxD,MAAMC,EAAgBjB,SAASC,cAAc,QAC7CgB,EAAcf,UAAY,mDAC1Be,EAAcR,YAAc,aAAaX,EAAQkB,eACjDV,EAAMK,YAAYM,EACpB,CAGA,GAAIpB,EAAOqB,aACT,IAAA,MAAWC,KAAStB,EAAOqB,aAAc,CACvC,MAAME,EAAUC,EAAkBF,EAAOrB,GACzC,OAAQqB,EAAMG,UACZ,IAAK,OACHlB,EAAKO,YAAYS,GACjB,MACF,IAAK,SACHf,EAAOM,YAAYS,GACnB,MACF,IAAK,QACHd,EAAMK,YAAYS,GAGxB,CAOF,OAJArB,EAAWY,YAAYP,GACvBL,EAAWY,YAAYN,GACvBN,EAAWY,YAAYL,GAEhBP,CACT,CAQO,SAASwB,EAA2BD,GACzC,MAAME,EAAYxB,SAASC,cAAc,OAIzC,OAHAuB,EAAUtB,UAAY,6CAA6CoB,IAEnEE,EAAUrB,aAAa,OAAQ,gBACxBqB,CACT,CAWO,SAASC,EACdD,EACAE,EACAC,EACAC,EACAC,GAAkB,GAElBL,EAAUM,UAAY,GAEtB,IAAA,MAAWC,KAAaL,EAAM,CAC5B,MAAMM,EAAQhC,SAASC,cAAc,OACrC+B,EAAM9B,UAAY,sBAElB8B,EAAM7B,aAAa,OAAQ,gBACvB4B,EAAUE,IACZD,EAAM7B,aAAa,sBAAuB4B,EAAUE,IAIlCF,EAAUG,WAAaL,EAGzCM,EAA8BH,EAAOD,EAAWJ,EAASC,GAEzDQ,EAA8BJ,EAAOD,EAAWJ,EAASC,GAG3DJ,EAAUb,YAAYqB,EACxB,CACF,CAKA,SAASG,EACPH,EACAD,EACAJ,EACAC,GAEA,MAAMS,EAAOrC,SAASC,cAAc,OACpCoC,EAAKnC,UAAY,iDACjBmC,EAAKC,MAAMC,WAAa,SAGxB,MAAMC,EAAwC,mBAApBT,EAAUU,MAAuBV,EAAUU,MAAMb,EAAUD,GAAWI,EAAUU,MAC1G,GAAID,EAAY,CACd,MAAME,EAAY1C,SAASC,cAAc,QACzCyC,EAAUxC,UAAY,wBACtBwC,EAAUjC,YAAc+B,EACxBH,EAAK1B,YAAY+B,EACnB,CAGA,MAAMC,EAsFR,SACEZ,EACAJ,EACAC,GAGA,MAAMgB,EAAiBb,EAAUc,aAAeC,OAAOC,KAAKhB,EAAUc,aAAaG,OAAS,EACtFC,EAAWlB,EAAUmB,OAASJ,OAAOC,KAAKhB,EAAUmB,OAAOF,OAAS,EAC1E,IAAKJ,IAAmBK,EAAU,OAAO,KAEzC,MAAMzB,EAAYxB,SAASC,cAAc,QACzCuB,EAAUtB,UAAY,6BAEtB,IAAA,MAAWiD,KAAOxB,EAAS,CACzB,MAAMyB,MAAEA,EAAAC,UAAOA,GAAcC,EAAuBvB,EAAWoB,EAAKvB,GACpE,GAAa,MAATwB,EAAe,CACjB,MAAMG,EAAOvD,SAASC,cAAc,QACpCsD,EAAKrD,UAAY,4BACjBqD,EAAKpD,aAAa,aAAcgD,EAAIK,OACpC,MAAMC,EAASN,EAAIM,QAAUN,EAAIK,MAC3BE,EAAeL,EAAYA,EAAUD,EAAOD,EAAIK,MAAOL,GAAOQ,OAAOP,GAC3EG,EAAK9C,YAAc,GAAGgD,MAAWC,IACjClC,EAAUb,YAAY4C,EACxB,CACF,CAEA,OAAO/B,EAAUoC,SAASZ,OAAS,EAAIxB,EAAY,IACrD,CAjH8BqC,CAAuB9B,EAAWJ,EAASC,GACnEe,GACFN,EAAK1B,YAAYgC,GAInBX,EAAMrB,YAAY0B,EACpB,CAKA,SAASD,EACPJ,EACAD,EACAJ,EACAC,GAEA,IAAA,MAAWuB,KAAOxB,EAAS,CACzB,MAAMU,EAAOrC,SAASC,cAAc,OACpCoC,EAAKnC,UAAY,uBACjBmC,EAAKlC,aAAa,aAAcgD,EAAIK,OAEpC,MAAMJ,MAAEA,EAAAC,UAAOA,GAAcC,EAAuBvB,EAAWoB,EAAKvB,GAGlES,EAAK5B,YADM,MAAT2C,EACiBC,EAAYA,EAAUD,EAAOD,EAAIK,MAAOL,GAAOQ,OAAOP,GAEtD,GAErBpB,EAAMrB,YAAY0B,EACpB,CAGA,MAAMG,EAAwC,mBAApBT,EAAUU,MAAuBV,EAAUU,MAAMb,EAAUD,GAAWI,EAAUU,MAC1G,GAAID,EAAY,CACd,MAAMsB,EAAU9D,SAASC,cAAc,QACvC6D,EAAQ5D,UAAY,wBACpB4D,EAAQrD,YAAc+B,EACtBR,EAAMrB,YAAYmD,EACpB,CACF,CAMA,SAASR,EACPvB,EACAoB,EACAvB,GAEA,IAAIwB,EACAC,EAGJ,MAAMU,EAAShC,EAAUc,cAAcM,EAAIK,OAC3C,GAAIO,EACF,GAjNoB,iBADIC,EAkNDD,IAjNiB,OAARC,GAAgB,YAAaA,EAiN7B,CAC9B,MAAMC,EAAQC,EAAAA,mBAAmBC,IAAIJ,EAAOK,SACxCH,IACFb,EAAQa,EAAMrC,EAAUuB,EAAIK,MAAOL,IAErCE,EAAYU,EAAOV,SACrB,KAAO,CACL,MAAMY,EAAQC,EAAAA,mBAAmBC,IAAIJ,GACjCE,IACFb,EAAQa,EAAMrC,EAAUuB,EAAIK,MAAOL,GAEvC,MACF,GAAWpB,EAAUmB,OAASJ,OAAOuB,UAAUC,eAAeC,KAAKxC,EAAUmB,MAAOC,EAAIK,OAAQ,CAC9F,MAAMgB,EAAYzC,EAAUmB,MAAMC,EAAIK,OAEpCJ,EADuB,mBAAdoB,EACDA,EAAU5C,EAAUuB,EAAIK,MAAOL,GAE/BqB,CAEZ,CArOF,IAA4BR,EAuO1B,MAAO,CAAEZ,QAAOC,YAClB,CA0CA,SAAShC,EAAkBF,EAAwBrB,GACjD,MAAMsB,EAAUpB,SAASC,cAAc,OACvCmB,EAAQlB,UAAY,2CACpBkB,EAAQa,GAAK,gBAAgBd,EAAMc,KAEnC,MAAMwC,EAAUtD,EAAMuD,OAAO5E,GAQ7B,MANuB,iBAAZ2E,EACTrD,EAAQU,UAAY2C,EAEpBrD,EAAQT,YAAY8D,GAGfrD,CACT,CAYO,SAASuD,EACdjD,EACAC,EACAiD,EACAC,EACAC,GAWA,MAAMC,EAAkBH,GAAgDI,WAClEC,EAAqBL,GAA0ClD,KAC/DwD,EAAYC,MAAMC,QAAQL,GAAkBA,EAAe/B,OAAStB,EAAKsB,OACzEqC,EAAiBF,MAAMC,QAAQH,GAAqBA,EAAkBjC,OAAStB,EAAKsB,OACpFtC,EAAYwE,EAAY,EAAIA,EAAYG,EAY9C,MAAO,CACL3E,YACAG,aAJmBiE,GAAaQ,cAActC,SAAWqC,EAAiBH,EAAYG,EAAiB3E,GAKvGM,aAAc6D,GAAgBU,UAAUC,MAAQ,EAChD7D,UACAD,OACAkD,OAEJ,CC7RO,MAAMa,UAAyBC,EAAAA,eAE3BC,KAAO,aAEEC,4kFAGlB,iBAAuBC,GACrB,MAAO,CACLvE,SAAU,SACVf,cAAc,EACdQ,mBAAmB,EACnBH,mBAAmB,EAEvB,CAGQkF,eAAqC,KACrCC,wBAA8C,KAC9CC,2BAAiD,KACjDC,cAAoC,KAKnC,MAAAC,GACHC,KAAKL,iBACPK,KAAKL,eAAeM,SACpBD,KAAKL,eAAiB,MAEpBK,KAAKJ,0BACPI,KAAKJ,wBAAwBK,SAC7BD,KAAKJ,wBAA0B,MAE7BI,KAAKH,6BACPG,KAAKH,2BAA2BI,SAChCD,KAAKH,2BAA6B,MAEhCG,KAAKF,gBACPE,KAAKF,cAAcG,SACnBD,KAAKF,cAAgB,KAEzB,CAKS,WAAAI,GACP,MAAMC,EAASH,KAAKI,YACpB,IAAKD,EAAQ,OAIb,MAAM9E,EACJ8E,EAAOE,cAAc,qBACrBF,EAAOE,cAAc,sBACrBF,EAAOE,cAAc,kBACvB,IAAKhF,EAAW,OAMZ2E,KAAKF,gBAAkBzE,EAAUiF,SAASN,KAAKF,iBACjDE,KAAKF,cAAgB,KACrBE,KAAKH,2BAA6B,KAClCG,KAAKL,eAAiB,MAEpBK,KAAKJ,0BAA4BvE,EAAUiF,SAASN,KAAKJ,2BAC3DI,KAAKJ,wBAA0B,MAE7BI,KAAKL,iBAAmBtE,EAAUiF,SAASN,KAAKL,kBAClDK,KAAKL,eAAiB,MAIxB,MAAMjB,EAAiBsB,KAAKO,oBACtB5B,EAAcqB,KAAKQ,iBAEnB7G,EAAU6E,EACdwB,KAAKnB,WACLmB,KAAKxE,QACLwE,KAAKI,YACL1B,EACAC,GAII8B,EAAkBT,KAAKtG,OAAO+G,iBAAmB,GACjDC,EAAUD,EAAgBE,OAAQC,GAAqB,QAAfA,EAAEzF,UAC1C0F,EAAaJ,EAAgBE,OAAQC,GAAqB,QAAfA,EAAEzF,UAGnD,GAAIuF,EAAQ7D,OAAS,EAAG,CACtB,IAAKmD,KAAKJ,wBAAyB,CACjCI,KAAKJ,wBAA0BxE,EAA2B,OAC1D,MAAMkC,EAAS6C,EAAOE,cAAc,WAChC/C,GAAUA,EAAOwD,YACnBzF,EAAU0F,aAAaf,KAAKJ,wBAAyBtC,EAAOwD,aAE5DzF,EAAUb,YAAYwF,KAAKJ,wBAE/B,CACAtE,EACE0E,KAAKJ,wBACLc,EACAV,KAAKgB,eACLhB,KAAKnB,WACLmB,KAAKtG,OAAOqC,UAEhB,MAAWiE,KAAKJ,0BACdI,KAAKJ,wBAAwBK,SAC7BD,KAAKJ,wBAA0B,MAIjC,MAAMqB,GACyB,IAA7BjB,KAAKtG,OAAOU,cACX4F,KAAKtG,OAAOkB,mBAAqBjB,EAAQkB,aAAe,GACxDmF,KAAKtG,OAAOe,mBAAqBd,EAAQe,eAAiBf,EAAQY,WAClEyF,KAAKtG,OAAOqB,cAAgBiF,KAAKtG,OAAOqB,aAAa8B,OAAS,EAC3DqE,EAAmBD,GAA2C,QAAzBjB,KAAKtG,OAAOyB,SACjDgG,EAAcN,EAAWhE,OAAS,GAAKqE,EAG7C,GAAID,GAA2C,QAAzBjB,KAAKtG,OAAOyB,SAChC,GAAK6E,KAAKL,eAGH,CACL,MAAMyB,EAAa3H,EAAqBuG,KAAKtG,OAAQC,GACrDqG,KAAKL,eAAe0B,YAAYD,GAChCpB,KAAKL,eAAiByB,CACxB,MANEpB,KAAKL,eAAiBlG,EAAqBuG,KAAKtG,OAAQC,GACxD0B,EAAU0F,aAAaf,KAAKL,eAAgBtE,EAAUiG,gBAMtB,QAAzBtB,KAAKtG,OAAOyB,UAAsB6E,KAAKL,iBAChDK,KAAKL,eAAeM,SACpBD,KAAKL,eAAiB,MAIpBwB,GACGnB,KAAKF,gBACRE,KAAKF,cAAgBjG,SAASC,cAAc,OAC5CkG,KAAKF,cAAc/F,UAAY,aAC/BsB,EAAUb,YAAYwF,KAAKF,gBAG7BE,KAAKF,cAAcnE,UAAY,GAE3BkF,EAAWhE,OAAS,IACjBmD,KAAKH,6BACRG,KAAKH,2BAA6BzE,EAA2B,WAE/D4E,KAAKF,cAActF,YAAYwF,KAAKH,4BACpCvE,EACE0E,KAAKH,2BACLgB,EACAb,KAAKgB,eACLhB,KAAKnB,WACLmB,KAAKtG,OAAOqC,YAIZmF,IACFlB,KAAKL,eAAiBlG,EAAqBuG,KAAKtG,OAAQC,GACxDqG,KAAKF,cAActF,YAAYwF,KAAKL,kBAGtCK,KAAKuB,eAGT,CAIQ,OAAAC,GACFxB,KAAKL,iBACPK,KAAKL,eAAeM,SACpBD,KAAKL,eAAiB,MAEpBK,KAAKJ,0BACPI,KAAKJ,wBAAwBK,SAC7BD,KAAKJ,wBAA0B,MAE7BI,KAAKH,6BACPG,KAAKH,2BAA2BI,SAChCD,KAAKH,2BAA6B,MAEhCG,KAAKF,gBACPE,KAAKF,cAAcG,SACnBD,KAAKF,cAAgB,KAEzB,CAEQ,aAAAyB,GACFvB,KAAKF,gBACPE,KAAKF,cAAcG,SACnBD,KAAKF,cAAgB,MAEnBE,KAAKH,6BACPG,KAAKH,2BAA2BI,SAChCD,KAAKH,2BAA6B,MAEhCG,KAAKL,gBAA2C,QAAzBK,KAAKtG,OAAOyB,WACrC6E,KAAKL,eAAeM,SACpBD,KAAKL,eAAiB,KAE1B,CAEQ,iBAAAY,GAEN,IACE,OAAQP,KAAKvB,MAAMgD,iBAAiB,cAAqD,IAC3F,CAAA,MACE,OAAO,IACT,CACF,CAEQ,cAAAjB,GACN,IACE,OAAQR,KAAKvB,MAAMgD,iBAAiB,cAA8D,IACpG,CAAA,MACE,OAAO,IACT,CACF,CAOA,OAAAC,GACE1B,KAAK2B,eACP,CAMA,UAAAC,GACE,MAAMlD,EAAiBsB,KAAKO,oBACtB5B,EAAcqB,KAAKQ,iBAEzB,OAAOhC,EACLwB,KAAKzE,KACLyE,KAAKxE,QACLwE,KAAKI,YACL1B,EACAC,EAEJ,CAMA,QAAAkD,CAAS7G,GACFgF,KAAKtG,OAAOqB,eACfiF,KAAKtG,OAAOqB,aAAe,IAE7BiF,KAAKtG,OAAOqB,aAAa+G,KAAK9G,GAC9BgF,KAAK2B,eACP,CAMA,WAAAI,CAAYjG,GACNkE,KAAKtG,OAAOqB,eACdiF,KAAKtG,OAAOqB,aAAeiF,KAAKtG,OAAOqB,aAAa4F,OAAQqB,GAAMA,EAAElG,KAAOA,GAC3EkE,KAAK2B,gBAET,CAMA,iBAAAM,CAAkBC,GACXlC,KAAKtG,OAAO+G,kBACfT,KAAKtG,OAAO+G,gBAAkB,IAEhCT,KAAKtG,OAAO+G,gBAAgBqB,KAAKI,GACjClC,KAAK2B,eACP,CAMA,oBAAAQ,CAAqBrG,GACfkE,KAAKtG,OAAO+G,kBACdT,KAAKtG,OAAO+G,gBAAkBT,KAAKtG,OAAO+G,gBAAgBE,OAAQC,GAAMA,EAAE9E,KAAOA,GACjFkE,KAAK2B,gBAET"}
@@ -1,2 +1,2 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/internal/diagnostics"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_serverSide={},t.TbwGrid,t.TbwGrid)}(this,function(t,e,o){"use strict";function i(t,e){return Math.floor(t/e)}async function s(t,e,o,i){const s=function(t,e){return{start:t*e,end:(t+1)*e}}(e,o);return t.getRows({startNode:s.start,endNode:s.end,sortModel:i.sortModel,filterModel:i.filterModel})}function a(t,e,o){const s=i(t,e),a=o.get(s);if(!a)return;return a[t%e]}class r extends o.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:-10},incompatibleWith:[{name:"pivot",reason:"PivotPlugin requires the full dataset to compute aggregations. ServerSidePlugin lazy-loads rows in blocks, so pivot aggregation cannot be performed client-side."}],events:[{type:"datasource:data",description:"Root data page/block loaded"},{type:"datasource:children",description:"Child data loaded for a parent context"},{type:"datasource:loading",description:"Loading state changed"},{type:"datasource:error",description:"Fetch operation failed"}],queries:[{type:"datasource:fetch-children",description:"Request child rows for a parent context"},{type:"datasource:is-active",description:"Check if ServerSide plugin has an active data source"}]};name="serverSide";get defaultConfig(){return{pageSize:100,cacheBlockSize:100,maxConcurrentRequests:2}}dataSource=null;totalNodeCount=0;infiniteScrollMode=!1;loadedBlocks=new Map;loadingBlocks=new Set;lastRequestId=0;scrollDebounceTimer;managedNodes=[];attach(t){super.attach(t),this.on("sort-change",()=>this.onModelChange()),this.on("filter-change",()=>this.onModelChange()),this.config.dataSource&&this.setDataSource(this.config.dataSource)}detach(){this.dataSource=null,this.totalNodeCount=0,this.infiniteScrollMode=!1,this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.lastRequestId=0,this.scrollDebounceTimer&&(clearTimeout(this.scrollDebounceTimer),this.scrollDebounceTimer=void 0)}getEnrichmentParams(){const t=this.grid?.query?.("sort:get-model",null),e=this.grid?.query?.("filter:get-model",null);return{sortModel:t?.[0],filterModel:e?.[0]}}getViewportMapping(t,e){const o={viewportStart:t,viewportEnd:e},i=this.grid?.query?.("datasource:viewport-mapping",o);return i?.[0]?i[0]:{startNode:t,endNode:e,totalLoadedNodes:this.totalNodeCount}}onModelChange(){this.dataSource&&(this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1,this.requestRender())}applyServerResult(t,e,o){void 0!==t.lastNode?(this.totalNodeCount=t.lastNode+1,this.infiniteScrollMode=!1):-1===t.totalNodeCount?this.infiniteScrollMode=!0:(this.totalNodeCount=t.totalNodeCount,this.infiniteScrollMode=!1),this.infiniteScrollMode&&t.rows.length<o&&(this.totalNodeCount=e*o+t.rows.length,this.infiniteScrollMode=!1)}getInfiniteScrollEstimate(t){let e=0;for(const[o,i]of this.loadedBlocks){const s=o*t+i.length;s>e&&(e=s)}return e+t}loadRequiredBlocks(){if(!this.dataSource)return;const t=this.grid,o=this.config.cacheBlockSize??100,a=this.getViewportMapping(t._virtualization.start,t._virtualization.end),r=function(t,e,o){const s=i(t,o),a=i(e-1,o),r=[];for(let i=s;i<=a;i++)r.push(i);return r}(a.startNode,a.endNode,o),d=this.getEnrichmentParams(),n=this.grid?.getAttribute?.("id")??void 0;for(const i of r)if(!this.loadedBlocks.has(i)&&!this.loadingBlocks.has(i)){if(this.loadingBlocks.size>=(this.config.maxConcurrentRequests??2)){e.debugDiagnostic(e.DATASOURCE_THROTTLED,"Concurrent request limit reached, deferring block load",n);break}this.loadingBlocks.add(i),this.broadcast("datasource:loading",{loading:!0}),s(this.dataSource,i,o,d).then(t=>{this.loadedBlocks.set(i,t.rows),this.applyServerResult(t,i,o),this.loadingBlocks.delete(i);const e=i*o;for(let o=0;o<t.rows.length;o++)e+o<this.managedNodes.length&&(this.managedNodes[e+o]=t.rows[o]);const s={rows:t.rows,totalNodeCount:t.totalNodeCount,startNode:e,endNode:e+t.rows.length,claimed:!1};this.broadcast("datasource:data",s),0===this.loadingBlocks.size&&this.broadcast("datasource:loading",{loading:!1}),this.requestVirtualRefresh(),this.loadRequiredBlocks()}).catch(t=>{this.loadingBlocks.delete(i);const o=t instanceof Error?t:new Error(String(t));e.errorDiagnostic(e.DATASOURCE_FETCH_ERROR,`getRows() failed: ${o.message}`,n),this.broadcast("datasource:error",{error:o}),0===this.loadingBlocks.size&&this.broadcast("datasource:loading",{loading:!1})})}}processRows(t){if(!this.dataSource)return[...t];const e=this.config.cacheBlockSize??100,o=this.infiniteScrollMode?this.getInfiniteScrollEstimate(e):Number.isFinite(this.totalNodeCount)&&this.totalNodeCount>=0?this.totalNodeCount:0;for(;this.managedNodes.length<o;){const t=this.managedNodes.length;this.managedNodes.push({__loading:!0,__index:t})}this.managedNodes.length=o;for(let i=0;i<o;i++){const t=a(i,e,this.loadedBlocks);t&&(this.managedNodes[i]=t)}return this.managedNodes}onScroll(t){this.dataSource&&(this.loadRequiredBlocks(),this.scrollDebounceTimer&&clearTimeout(this.scrollDebounceTimer),this.scrollDebounceTimer=setTimeout(()=>{this.loadRequiredBlocks()},100))}handleQuery(t){switch(t.type){case"datasource:is-active":return null!=this.dataSource;case"datasource:fetch-children":{const{context:e}=t.context;return void this.fetchChildren(e)}}}fetchChildren(t){if(!this.dataSource)return;const o=this.grid?.getAttribute?.("id")??void 0;if(!this.dataSource.getChildRows)return void e.warnDiagnostic(e.DATASOURCE_NO_CHILD_HANDLER,`Plugin "${t.source}" requested child rows but getChildRows() is not implemented on the dataSource`,o);const i=this.getEnrichmentParams();this.broadcast("datasource:loading",{loading:!0,context:t}),this.dataSource.getChildRows({context:t,sortModel:i.sortModel,filterModel:i.filterModel}).then(e=>{const o={rows:e.rows,context:t,claimed:!1};this.broadcast("datasource:children",o),this.broadcast("datasource:loading",{loading:!1,context:t})}).catch(i=>{const s=i instanceof Error?i:new Error(String(i));e.errorDiagnostic(e.DATASOURCE_CHILD_FETCH_ERROR,`getChildRows() failed: ${s.message}`,o),this.broadcast("datasource:error",{error:s,context:t}),this.broadcast("datasource:loading",{loading:!1,context:t})})}setDataSource(t){this.dataSource=t,this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1;const o=this.config.cacheBlockSize??100,i=this.getEnrichmentParams(),a=this.grid?.getAttribute?.("id")??void 0;this.broadcast("datasource:loading",{loading:!0}),s(t,0,o,i).then(t=>{this.loadedBlocks.set(0,t.rows),this.applyServerResult(t,0,o);const e={rows:t.rows,totalNodeCount:t.totalNodeCount,startNode:0,endNode:t.rows.length,claimed:!1};this.broadcast("datasource:data",e),this.broadcast("datasource:loading",{loading:!1}),this.requestRender()}).catch(t=>{const o=t instanceof Error?t:new Error(String(t));e.errorDiagnostic(e.DATASOURCE_FETCH_ERROR,`getRows() failed: ${o.message}`,a),this.broadcast("datasource:error",{error:o}),this.broadcast("datasource:loading",{loading:!1})})}refresh(){if(!this.dataSource)return;const t=this.dataSource;this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1,this.setDataSource(t)}purgeCache(){this.loadedBlocks.clear(),this.managedNodes=[]}getTotalNodeCount(){return this.totalNodeCount}getTotalRowCount(){return this.totalNodeCount}isNodeLoaded(t){const e=i(t,this.config.cacheBlockSize??100);return this.loadedBlocks.has(e)}isRowLoaded(t){return this.isNodeLoaded(t)}getLoadedBlockCount(){return this.loadedBlocks.size}}t.ServerSidePlugin=r,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/internal/diagnostics"),require("../../core/internal/sorting"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/internal/sorting","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_serverSide={},t.TbwGrid,t.TbwGrid,t.TbwGrid)}(this,function(t,e,o,i){"use strict";function s(t,e){return Math.floor(t/e)}async function a(t,e,o,i){const s=function(t,e){return{start:t*e,end:(t+1)*e}}(e,o);return t.getRows({startNode:s.start,endNode:s.end,sortModel:i.sortModel,filterModel:i.filterModel})}function r(t,e,o){const i=s(t,e),a=o.get(i);if(!a)return;return a[t%e]}class d extends i.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:-10},incompatibleWith:[{name:"pivot",reason:"PivotPlugin requires the full dataset to compute aggregations. ServerSidePlugin lazy-loads rows in blocks, so pivot aggregation cannot be performed client-side."}],events:[{type:"datasource:data",description:"Root data page/block loaded"},{type:"datasource:children",description:"Child data loaded for a parent context"},{type:"datasource:loading",description:"Loading state changed"},{type:"datasource:error",description:"Fetch operation failed"}],queries:[{type:"datasource:fetch-children",description:"Request child rows for a parent context"},{type:"datasource:is-active",description:"Check if ServerSide plugin has an active data source"}]};name="serverSide";get defaultConfig(){return{pageSize:100,cacheBlockSize:100,maxConcurrentRequests:2}}dataSource=null;totalNodeCount=0;infiniteScrollMode=!1;loadedBlocks=new Map;loadingBlocks=new Set;lastRequestId=0;scrollDebounceTimer;managedNodes=[];attach(t){super.attach(t),this.on("sort-change",()=>{"local"===this.config.sortMode?this.requestRender():this.onModelChange()}),this.on("filter-change",()=>{"local"===this.config.filterMode?this.requestRender():this.onModelChange()}),this.config.dataSource&&this.setDataSource(this.config.dataSource)}detach(){this.dataSource=null,this.totalNodeCount=0,this.infiniteScrollMode=!1,this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.lastRequestId=0,this.scrollDebounceTimer&&(clearTimeout(this.scrollDebounceTimer),this.scrollDebounceTimer=void 0)}getEnrichmentParams(){const t="local"===this.config.sortMode,e="local"===this.config.filterMode,o=t?void 0:this.grid?.query?.("sort:get-model",null),i=e?void 0:this.grid?.query?.("filter:get-model",null);let s=o?.[0];const a=this.grid;if(!t&&!s&&a?._sortState){const{field:t,direction:e}=a._sortState;s=[{field:t,direction:1===e?"asc":"desc"}]}return{sortModel:s,filterModel:i?.[0]}}getViewportMapping(t,e){const o={viewportStart:t,viewportEnd:e},i=this.grid?.query?.("datasource:viewport-mapping",o);return i?.[0]?i[0]:{startNode:t,endNode:e,totalLoadedNodes:this.totalNodeCount}}onModelChange(){this.dataSource&&(this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1,this.requestRender(),this.loadRequiredBlocks())}applyServerResult(t,e,o){void 0!==t.lastNode?(this.totalNodeCount=t.lastNode+1,this.infiniteScrollMode=!1):-1===t.totalNodeCount?this.infiniteScrollMode=!0:(this.totalNodeCount=t.totalNodeCount,this.infiniteScrollMode=!1),this.infiniteScrollMode&&t.rows.length<o&&(this.totalNodeCount=e*o+t.rows.length,this.infiniteScrollMode=!1)}getInfiniteScrollEstimate(t){let e=0;for(const[o,i]of this.loadedBlocks){const s=o*t+i.length;s>e&&(e=s)}return e+t}loadRequiredBlocks(){if(!this.dataSource)return;const t=this.grid,o=this.config.cacheBlockSize??100,i=this.getViewportMapping(t._virtualization.start,t._virtualization.end),r=Math.max(0,this.config.loadThreshold??0),d=Math.max(0,i.startNode-r),n=this.totalNodeCount>0?this.totalNodeCount:1/0,l=Math.min(n,i.endNode+r);if(l<=d)return;const c=function(t,e,o){const i=s(t,o),a=s(e-1,o),r=[];for(let s=i;s<=a;s++)r.push(s);return r}(d,l,o),h=this.getEnrichmentParams(),u=this.grid?.getAttribute?.("id")??void 0;for(const s of c)if(!this.loadedBlocks.has(s)&&!this.loadingBlocks.has(s)){if(this.loadingBlocks.size>=(this.config.maxConcurrentRequests??2)){e.debugDiagnostic(e.DATASOURCE_THROTTLED,"Concurrent request limit reached, deferring block load",u);break}this.loadingBlocks.add(s),this.broadcast("datasource:loading",{loading:!0}),a(this.dataSource,s,o,h).then(t=>{this.loadedBlocks.set(s,t.rows);const e=this.managedNodes.length;this.applyServerResult(t,s,o),this.loadingBlocks.delete(s);const i=s*o;for(let o=0;o<t.rows.length;o++)i+o<this.managedNodes.length&&(this.managedNodes[i+o]=t.rows[o]);const a={rows:t.rows,totalNodeCount:t.totalNodeCount,startNode:i,endNode:i+t.rows.length,claimed:!1};this.broadcast("datasource:data",a),0===this.loadingBlocks.size&&this.broadcast("datasource:loading",{loading:!1});0===e||this.managedNodes.length<(Number.isFinite(this.totalNodeCount)?this.totalNodeCount:0)?this.requestRender():this.requestVirtualRefresh(),this.loadRequiredBlocks()}).catch(t=>{this.loadingBlocks.delete(s);const o=t instanceof Error?t:new Error(String(t));e.errorDiagnostic(e.DATASOURCE_FETCH_ERROR,`getRows() failed: ${o.message}`,u),this.broadcast("datasource:error",{error:o}),0===this.loadingBlocks.size&&this.broadcast("datasource:loading",{loading:!1})})}}processRows(t){if(!this.dataSource)return[...t];const e=this.config.cacheBlockSize??100,i=this.infiniteScrollMode?this.getInfiniteScrollEstimate(e):Number.isFinite(this.totalNodeCount)&&this.totalNodeCount>=0?this.totalNodeCount:0;for(;this.managedNodes.length<i;){const t=this.managedNodes.length;this.managedNodes.push({__loading:!0,__index:t})}this.managedNodes.length=i;for(let o=0;o<i;o++){const t=r(o,e,this.loadedBlocks);t&&(this.managedNodes[o]=t)}const s=this.grid;if("local"===this.config.sortMode&&s?._sortState){const t=s._columns??[];return o.builtInSort(this.managedNodes,s._sortState,t)}return this.managedNodes}onScroll(t){this.dataSource&&(this.loadRequiredBlocks(),this.scrollDebounceTimer&&clearTimeout(this.scrollDebounceTimer),this.scrollDebounceTimer=setTimeout(()=>{this.loadRequiredBlocks()},100))}handleQuery(t){switch(t.type){case"datasource:is-active":return null!=this.dataSource;case"datasource:fetch-children":{const{context:e}=t.context;return void this.fetchChildren(e)}}}fetchChildren(t){if(!this.dataSource)return;const o=this.grid?.getAttribute?.("id")??void 0;if(!this.dataSource.getChildRows)return void e.warnDiagnostic(e.DATASOURCE_NO_CHILD_HANDLER,`Plugin "${t.source}" requested child rows but getChildRows() is not implemented on the dataSource`,o);const i=this.getEnrichmentParams();this.broadcast("datasource:loading",{loading:!0,context:t}),this.dataSource.getChildRows({context:t,sortModel:i.sortModel,filterModel:i.filterModel}).then(e=>{const o={rows:e.rows,context:t,claimed:!1};this.broadcast("datasource:children",o),this.broadcast("datasource:loading",{loading:!1,context:t})}).catch(i=>{const s=i instanceof Error?i:new Error(String(i));e.errorDiagnostic(e.DATASOURCE_CHILD_FETCH_ERROR,`getChildRows() failed: ${s.message}`,o),this.broadcast("datasource:error",{error:s,context:t}),this.broadcast("datasource:loading",{loading:!1,context:t})})}setDataSource(t){this.dataSource=t,this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1;const o=this.config.cacheBlockSize??100,i=this.getEnrichmentParams(),s=this.grid?.getAttribute?.("id")??void 0;this.broadcast("datasource:loading",{loading:!0}),a(t,0,o,i).then(t=>{this.loadedBlocks.set(0,t.rows),this.applyServerResult(t,0,o);const e={rows:t.rows,totalNodeCount:t.totalNodeCount,startNode:0,endNode:t.rows.length,claimed:!1};this.broadcast("datasource:data",e),this.broadcast("datasource:loading",{loading:!1}),this.requestRender(),(this.config.loadThreshold??0)>0&&this.loadRequiredBlocks()}).catch(t=>{const o=t instanceof Error?t:new Error(String(t));e.errorDiagnostic(e.DATASOURCE_FETCH_ERROR,`getRows() failed: ${o.message}`,s),this.broadcast("datasource:error",{error:o}),this.broadcast("datasource:loading",{loading:!1})})}refresh(){if(!this.dataSource)return;const t=this.dataSource;this.loadedBlocks.clear(),this.loadingBlocks.clear(),this.managedNodes=[],this.totalNodeCount=0,this.infiniteScrollMode=!1,this.setDataSource(t)}purgeCache(){this.loadedBlocks.clear(),this.managedNodes=[]}getTotalNodeCount(){return this.totalNodeCount}getTotalRowCount(){return this.totalNodeCount}isNodeLoaded(t){const e=s(t,this.config.cacheBlockSize??100);return this.loadedBlocks.has(e)}isRowLoaded(t){return this.isNodeLoaded(t)}getLoadedBlockCount(){return this.loadedBlocks.size}}t.ServerSidePlugin=d,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
2
  //# sourceMappingURL=server-side.umd.js.map