@toolbox-web/grid 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -13
- package/all.js +1124 -1047
- package/all.js.map +1 -1
- package/index.js +688 -515
- package/index.js.map +1 -1
- package/lib/core/grid.d.ts +10 -0
- package/lib/core/grid.d.ts.map +1 -1
- package/lib/core/internal/config-manager.d.ts +1 -0
- package/lib/core/internal/config-manager.d.ts.map +1 -1
- package/lib/core/internal/keyboard.d.ts.map +1 -1
- package/lib/core/internal/utils.d.ts +1 -0
- package/lib/core/internal/utils.d.ts.map +1 -1
- package/lib/core/internal/validate-config.d.ts +14 -0
- package/lib/core/internal/validate-config.d.ts.map +1 -1
- package/lib/core/plugin/base-plugin.d.ts +105 -1
- package/lib/core/plugin/base-plugin.d.ts.map +1 -1
- package/lib/core/plugin/expander-column.d.ts +51 -0
- package/lib/core/plugin/expander-column.d.ts.map +1 -0
- package/lib/core/plugin/index.d.ts +1 -0
- package/lib/core/plugin/index.d.ts.map +1 -1
- package/lib/core/plugin/plugin-manager.d.ts +1 -1
- package/lib/core/plugin/plugin-manager.d.ts.map +1 -1
- package/lib/core/plugin/types.d.ts +117 -1
- package/lib/core/plugin/types.d.ts.map +1 -1
- package/lib/core/types.d.ts +4 -2
- package/lib/core/types.d.ts.map +1 -1
- package/lib/plugins/clipboard/ClipboardPlugin.d.ts +9 -2
- package/lib/plugins/clipboard/ClipboardPlugin.d.ts.map +1 -1
- package/lib/plugins/clipboard/index.d.ts +1 -1
- package/lib/plugins/clipboard/index.d.ts.map +1 -1
- package/lib/plugins/clipboard/index.js +303 -185
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/clipboard/types.d.ts +72 -2
- package/lib/plugins/clipboard/types.d.ts.map +1 -1
- package/lib/plugins/column-virtualization/ColumnVirtualizationPlugin.d.ts +0 -1
- package/lib/plugins/column-virtualization/ColumnVirtualizationPlugin.d.ts.map +1 -1
- package/lib/plugins/column-virtualization/index.js +116 -24
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/ContextMenuPlugin.d.ts +0 -1
- package/lib/plugins/context-menu/ContextMenuPlugin.d.ts.map +1 -1
- package/lib/plugins/context-menu/index.js +164 -72
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/EditingPlugin.d.ts +1 -7
- package/lib/plugins/editing/EditingPlugin.d.ts.map +1 -1
- package/lib/plugins/editing/index.js +213 -133
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/export/ExportPlugin.d.ts +0 -1
- package/lib/plugins/export/ExportPlugin.d.ts.map +1 -1
- package/lib/plugins/export/index.js +195 -103
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/FilteringPlugin.d.ts +5 -2
- package/lib/plugins/filtering/FilteringPlugin.d.ts.map +1 -1
- package/lib/plugins/filtering/index.js +145 -43
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/GroupingColumnsPlugin.d.ts +1 -2
- package/lib/plugins/grouping-columns/GroupingColumnsPlugin.d.ts.map +1 -1
- package/lib/plugins/grouping-columns/grouping-columns.d.ts +1 -1
- package/lib/plugins/grouping-columns/grouping-columns.d.ts.map +1 -1
- package/lib/plugins/grouping-columns/index.js +162 -68
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/GroupingRowsPlugin.d.ts +14 -2
- package/lib/plugins/grouping-rows/GroupingRowsPlugin.d.ts.map +1 -1
- package/lib/plugins/grouping-rows/index.js +246 -138
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/master-detail/MasterDetailPlugin.d.ts +13 -11
- package/lib/plugins/master-detail/MasterDetailPlugin.d.ts.map +1 -1
- package/lib/plugins/master-detail/index.js +281 -196
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/master-detail/types.d.ts +0 -10
- package/lib/plugins/master-detail/types.d.ts.map +1 -1
- package/lib/plugins/multi-sort/MultiSortPlugin.d.ts +1 -2
- package/lib/plugins/multi-sort/MultiSortPlugin.d.ts.map +1 -1
- package/lib/plugins/multi-sort/index.js +121 -31
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/PinnedColumnsPlugin.d.ts +0 -1
- package/lib/plugins/pinned-columns/PinnedColumnsPlugin.d.ts.map +1 -1
- package/lib/plugins/pinned-columns/index.js +144 -52
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/PinnedRowsPlugin.d.ts +1 -2
- package/lib/plugins/pinned-rows/PinnedRowsPlugin.d.ts.map +1 -1
- package/lib/plugins/pinned-rows/index.js +178 -88
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/PivotPlugin.d.ts +26 -4
- package/lib/plugins/pivot/PivotPlugin.d.ts.map +1 -1
- package/lib/plugins/pivot/index.js +414 -310
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/pivot/pivot-rows.d.ts +2 -1
- package/lib/plugins/pivot/pivot-rows.d.ts.map +1 -1
- package/lib/plugins/reorder/ReorderPlugin.d.ts +13 -10
- package/lib/plugins/reorder/ReorderPlugin.d.ts.map +1 -1
- package/lib/plugins/reorder/index.js +304 -226
- package/lib/plugins/reorder/index.js.map +1 -1
- package/lib/plugins/selection/SelectionPlugin.d.ts +21 -3
- package/lib/plugins/selection/SelectionPlugin.d.ts.map +1 -1
- package/lib/plugins/selection/index.d.ts +2 -2
- package/lib/plugins/selection/index.d.ts.map +1 -1
- package/lib/plugins/selection/index.js +292 -145
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/selection/types.d.ts +24 -0
- package/lib/plugins/selection/types.d.ts.map +1 -1
- package/lib/plugins/server-side/ServerSidePlugin.d.ts +0 -1
- package/lib/plugins/server-side/ServerSidePlugin.d.ts.map +1 -1
- package/lib/plugins/server-side/index.js +95 -3
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/tree/TreePlugin.d.ts +5 -1
- package/lib/plugins/tree/TreePlugin.d.ts.map +1 -1
- package/lib/plugins/tree/index.js +213 -112
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/tree/types.d.ts +0 -10
- package/lib/plugins/tree/types.d.ts.map +1 -1
- package/lib/plugins/undo-redo/UndoRedoPlugin.d.ts +7 -2
- package/lib/plugins/undo-redo/UndoRedoPlugin.d.ts.map +1 -1
- package/lib/plugins/undo-redo/index.js +112 -12
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/VisibilityPlugin.d.ts +14 -5
- package/lib/plugins/visibility/VisibilityPlugin.d.ts.map +1 -1
- package/lib/plugins/visibility/index.js +168 -65
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +21 -17
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +14 -8
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/clipboard.umd.js +5 -7
- package/umd/plugins/clipboard.umd.js.map +1 -1
- package/umd/plugins/column-virtualization.umd.js +1 -1
- package/umd/plugins/column-virtualization.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/export.umd.js +1 -1
- package/umd/plugins/export.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.umd.js +1 -1
- package/umd/plugins/grouping-columns.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pinned-rows.umd.js +1 -1
- package/umd/plugins/pinned-rows.umd.js.map +1 -1
- package/umd/plugins/pivot.umd.js +1 -1
- package/umd/plugins/pivot.umd.js.map +1 -1
- package/umd/plugins/reorder.umd.js +1 -1
- package/umd/plugins/reorder.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/undo-redo.umd.js +1 -1
- package/umd/plugins/undo-redo.umd.js.map +1 -1
- package/umd/plugins/visibility.umd.js +1 -1
- package/umd/plugins/visibility.umd.js.map +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(c,f){typeof exports=="object"&&typeof module<"u"?f(exports,require("../../core/internal/sanitize"),require("../../core/plugin/base-plugin"),require("../../core/plugin/expander-column")):typeof define=="function"&&define.amd?define(["exports","../../core/internal/sanitize","../../core/plugin/base-plugin","../../core/plugin/expander-column"],f):(c=typeof globalThis<"u"?globalThis:c||self,f(c.TbwGridPlugin_masterDetail={},c.TbwGrid,c.TbwGrid,c.TbwGrid))})(this,(function(c,f,m,p){"use strict";function w(u,e){const t=new Set(u);return t.has(e)?t.delete(e):t.add(e),t}function x(u,e){const t=new Set(u);return t.add(e),t}function R(u,e){const t=new Set(u);return t.delete(e),t}function b(u,e){return u.has(e)}function E(u,e,t,o){const s=document.createElement("div");s.className="master-detail-row",s.setAttribute("data-detail-for",String(e)),s.setAttribute("role","row");const d=document.createElement("div");d.className="master-detail-cell",d.setAttribute("role","cell"),d.style.gridColumn=`1 / ${o+1}`;const r=t(u,e);return typeof r=="string"?d.innerHTML=r:r instanceof HTMLElement&&d.appendChild(r),s.appendChild(d),s}const y=".cell[data-field=__tbw_expander]{border-right:none!important;padding:0;display:flex;align-items:center;justify-content:center}.header-row .cell[data-field=__tbw_expander]{visibility:hidden;border:none!important;padding:0;overflow:hidden}.header-row .cell[data-field=__tbw_expander]+.cell{margin-left:-32px;padding-left:calc(var(--tbw-cell-padding, 8px) + 32px)}.master-detail-expander{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.master-detail-toggle{cursor:pointer;opacity:.7;-webkit-user-select:none;user-select:none;display:inline-flex;align-items:center;justify-content:center}.master-detail-toggle:hover{opacity:1}.master-detail-row{grid-column:1 / -1;display:grid;background:var(--tbw-master-detail-bg, var(--tbw-color-row-alt));border-bottom:1px solid var(--tbw-master-detail-border, var(--tbw-color-border));overflow:hidden}.master-detail-cell{padding:16px;overflow:auto}.master-detail-row.tbw-expanding{animation:tbw-detail-expand var(--tbw-animation-duration, .2s) var(--tbw-animation-easing, ease-out) forwards}.master-detail-row.tbw-collapsing{animation:tbw-detail-collapse var(--tbw-animation-duration, .2s) var(--tbw-animation-easing, ease-out) forwards}@keyframes tbw-detail-expand{0%{opacity:0;max-height:0;padding-top:0;padding-bottom:0}to{opacity:1;max-height:500px;padding-top:16px;padding-bottom:16px}}@keyframes tbw-detail-collapse{0%{opacity:1;max-height:500px}to{opacity:0;max-height:0}}";class g extends m.BaseGridPlugin{name="masterDetail";styles=y;get defaultConfig(){return{detailHeight:"auto",expandOnRowClick:!1,collapseOnClickOutside:!1,showExpandColumn:!0,animation:"slide"}}attach(e){super.attach(e),this.parseLightDomDetail()}parseLightDomDetail(){const e=this.grid;if(!e||typeof e.querySelector!="function")return;const t=e.querySelector("tbw-grid-detail");if(!t)return;const o=e;if(o.__frameworkAdapter?.parseDetailElement){const h=o.__frameworkAdapter.parseDetailElement(t);if(h){this.config={...this.config,detailRenderer:h};return}}const s=t.getAttribute("animation"),d=t.getAttribute("show-expand-column"),r=t.getAttribute("expand-on-row-click"),i=t.getAttribute("collapse-on-click-outside"),n=t.getAttribute("height"),a={};s!==null&&(a.animation=s==="false"?!1:s),d!==null&&(a.showExpandColumn=d!=="false"),r!==null&&(a.expandOnRowClick=r==="true"),i!==null&&(a.collapseOnClickOutside=i==="true"),n!==null&&(a.detailHeight=n==="auto"?"auto":parseInt(n,10));const l=t.innerHTML.trim();l&&!this.config.detailRenderer&&(a.detailRenderer=(h,D)=>{const A=f.evalTemplateString(l,{value:h,row:h});return f.sanitizeHTML(A)}),Object.keys(a).length>0&&(this.config={...this.config,...a})}get animationStyle(){return this.isAnimationEnabled?this.config.animation??"slide":!1}animateExpand(e){!this.isAnimationEnabled||this.animationStyle===!1||(e.classList.add("tbw-expanding"),e.addEventListener("animationend",()=>{e.classList.remove("tbw-expanding")},{once:!0}))}animateCollapse(e,t){if(!this.isAnimationEnabled||this.animationStyle===!1){t();return}e.classList.add("tbw-collapsing");const o=()=>{e.classList.remove("tbw-collapsing"),t()};e.addEventListener("animationend",o,{once:!0}),setTimeout(o,this.animationDuration+50)}expandedRows=new Set;detailElements=new Map;static DEFAULT_DETAIL_HEIGHT=150;getDetailHeight(e){const t=this.detailElements.get(e);return t?t.offsetHeight:typeof this.config?.detailHeight=="number"?this.config.detailHeight:g.DEFAULT_DETAIL_HEIGHT}toggleAndEmit(e,t){this.expandedRows=w(this.expandedRows,e),this.emit("detail-expand",{rowIndex:t,row:e,expanded:this.expandedRows.has(e)}),this.requestRender()}detach(){this.expandedRows.clear(),this.detailElements.clear()}processColumns(e){if(!this.config.detailRenderer||this.config.showExpandColumn===!1)return[...e];const t=[...e];if(p.findExpanderColumn(t))return t;const s=p.createExpanderColumnConfig(this.name);return s.viewRenderer=d=>{const{row:r}=d,i=this.expandedRows.has(r),n=document.createElement("span");n.className="master-detail-expander expander-cell";const a=document.createElement("span");return a.className=`master-detail-toggle${i?" expanded":""}`,this.setIcon(a,this.resolveIcon(i?"collapse":"expand")),a.setAttribute("role","button"),a.setAttribute("tabindex","0"),a.setAttribute("aria-expanded",String(i)),a.setAttribute("aria-label",i?"Collapse details":"Expand details"),n.appendChild(a),n},[s,...t]}onRowClick(e){if(!(!this.config.expandOnRowClick||!this.config.detailRenderer))return this.toggleAndEmit(e.row,e.rowIndex),!1}onCellClick(e){if(e.originalEvent?.target?.classList.contains("master-detail-toggle"))return this.toggleAndEmit(e.row,e.rowIndex),!0;this.expandedRows.size>0&&queueMicrotask(()=>this.#e())}onKeyDown(e){if(e.key!==" ")return;const t=this.grid._focusCol,o=this.grid._focusRow,s=this.columns[t];if(!s||!p.isExpanderColumn(s))return;const d=this.rows[o];if(d)return e.preventDefault(),this.toggleAndEmit(d,o),this.requestRenderWithFocus(),!0}afterRender(){this.#e()}onScrollRender(){!this.config.detailRenderer||this.expandedRows.size===0||this.#e()}#e(){if(!this.config.detailRenderer)return;const e=this.shadowRoot?.querySelector(".rows");if(!e)return;const t=new Map,o=e.querySelectorAll(".data-grid-row"),s=this.columns.length;for(const r of o){const i=r.querySelector(".cell[data-row]"),n=i?parseInt(i.getAttribute("data-row")??"-1",10):-1;n>=0&&t.set(n,r)}const d=e.querySelectorAll(".master-detail-row");for(const r of d){const i=parseInt(r.getAttribute("data-detail-for")??"-1",10),n=i>=0?this.rows[i]:void 0,a=n&&this.expandedRows.has(n),l=t.has(i);(!a||!l)&&(r.remove(),n&&this.detailElements.delete(n))}for(const[r,i]of t){const n=this.rows[r];if(!n||!this.expandedRows.has(n))continue;const a=this.detailElements.get(n);if(a){a.previousElementSibling!==i&&i.after(a);continue}const l=E(n,r,this.config.detailRenderer,s);typeof this.config.detailHeight=="number"&&(l.style.height=`${this.config.detailHeight}px`),i.after(l),this.detailElements.set(n,l),this.animateExpand(l)}}getExtraHeight(){let e=0;for(const t of this.expandedRows)e+=this.getDetailHeight(t);return e}getExtraHeightBefore(e){let t=0;for(const o of this.expandedRows){const s=this.rows.indexOf(o);s>=0&&s<e&&(t+=this.getDetailHeight(o))}return t}adjustVirtualStart(e,t,o){if(this.expandedRows.size===0)return e;const s=[];for(const i of this.expandedRows){const n=this.rows.indexOf(i);n>=0&&s.push({index:n,row:i})}s.sort((i,n)=>i.index-n.index);let d=e,r=0;for(const{index:i,row:n}of s){const a=i*o+r,l=this.getDetailHeight(n),h=a+o+l;r+=l,!(i>=e)&&h>t&&i<d&&(d=i)}return d}expand(e){const t=this.rows[e];t&&(this.expandedRows=x(this.expandedRows,t),this.requestRender())}collapse(e){const t=this.rows[e];t&&(this.expandedRows=R(this.expandedRows,t),this.requestRender())}toggle(e){const t=this.rows[e];t&&(this.expandedRows=w(this.expandedRows,t),this.requestRender())}isExpanded(e){const t=this.rows[e];return t?b(this.expandedRows,t):!1}expandAll(){for(const e of this.rows)this.expandedRows.add(e);this.requestRender()}collapseAll(){this.expandedRows.clear(),this.requestRender()}getExpandedRows(){const e=[];for(const t of this.expandedRows){const o=this.rows.indexOf(t);o>=0&&e.push(o)}return e}getDetailElement(e){const t=this.rows[e];return t?this.detailElements.get(t):void 0}refreshDetailRenderer(){const e=this.config.detailRenderer;if(this.config={...this.config,detailRenderer:void 0},this.parseLightDomDetail(),!this.config.detailRenderer&&e&&(this.config={...this.config,detailRenderer:e}),this.config.detailRenderer){const t=this.grid;typeof t.refreshColumns=="function"?t.refreshColumns():this.requestRender()}}}c.MasterDetailPlugin=g,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}));
|
|
2
2
|
//# sourceMappingURL=master-detail.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"master-detail.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/master-detail/master-detail.ts","../../../../../libs/grid/src/lib/plugins/master-detail/MasterDetailPlugin.ts"],"sourcesContent":["/**\n * Master/Detail Core Logic\n *\n * Pure functions for managing detail row expansion state.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// Uses `any` for maximum flexibility with user-defined row types.\n\n/**\n * Toggle the expansion state of a detail row.\n * Returns a new Set with the updated state.\n */\nexport function toggleDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n if (newExpanded.has(row)) {\n newExpanded.delete(row);\n } else {\n newExpanded.add(row);\n }\n return newExpanded;\n}\n\n/**\n * Expand a detail row.\n * Returns a new Set with the row added.\n */\nexport function expandDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n newExpanded.add(row);\n return newExpanded;\n}\n\n/**\n * Collapse a detail row.\n * Returns a new Set with the row removed.\n */\nexport function collapseDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n newExpanded.delete(row);\n return newExpanded;\n}\n\n/**\n * Check if a detail row is expanded.\n */\nexport function isDetailExpanded(expandedRows: Set<object>, row: object): boolean {\n return expandedRows.has(row);\n}\n\n/**\n * Create a detail element for a given row.\n * The element spans all columns and contains the rendered content.\n */\nexport function createDetailElement(\n row: any,\n rowIndex: number,\n renderer: (row: any, rowIndex: number) => HTMLElement | string,\n columnCount: number\n): HTMLElement {\n const detailRow = document.createElement('div');\n detailRow.className = 'master-detail-row';\n detailRow.setAttribute('data-detail-for', String(rowIndex));\n detailRow.setAttribute('role', 'row');\n\n const detailCell = document.createElement('div');\n detailCell.className = 'master-detail-cell';\n detailCell.setAttribute('role', 'cell');\n detailCell.style.gridColumn = `1 / ${columnCount + 1}`;\n\n const content = renderer(row, rowIndex);\n if (typeof content === 'string') {\n detailCell.innerHTML = content;\n } else if (content instanceof HTMLElement) {\n detailCell.appendChild(content);\n }\n\n detailRow.appendChild(detailCell);\n return detailRow;\n}\n","/**\n * Master/Detail Plugin (Class-based)\n *\n * Enables expandable detail rows showing additional content for each row.\n * Animation style is plugin-configured; respects grid-level animation.mode.\n */\n\nimport { evalTemplateString, sanitizeHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, CellClickEvent, GridElement, RowClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig, GridConfig } from '../../core/types';\nimport {\n collapseDetailRow,\n createDetailElement,\n expandDetailRow,\n isDetailExpanded,\n toggleDetailRow,\n} from './master-detail';\nimport styles from './master-detail.css?inline';\nimport type {\n DetailExpandDetail,\n ExpandCollapseAnimation,\n MasterDetailConfig,\n MasterDetailWrappedRenderer,\n} from './types';\n\n/** Extended grid interface for accessing effective config */\ninterface GridWithConfig {\n effectiveConfig?: GridConfig;\n}\n\n/**\n * Master/Detail Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new MasterDetailPlugin({\n * enabled: true,\n * detailRenderer: (row) => `<div>Details for ${row.name}</div>`,\n * expandOnRowClick: true,\n * })\n * ```\n */\nexport class MasterDetailPlugin extends BaseGridPlugin<MasterDetailConfig> {\n readonly name = 'masterDetail';\n override readonly version = '1.0.0';\n\n protected override get defaultConfig(): Partial<MasterDetailConfig> {\n return {\n detailHeight: 'auto',\n expandOnRowClick: false,\n collapseOnClickOutside: false,\n showExpandColumn: true,\n animation: 'slide', // Plugin's own default\n };\n }\n\n // #region Light DOM Parsing\n\n /**\n * Called when plugin is attached to the grid.\n * Parses light DOM for `<tbw-grid-detail>` elements to configure detail templates.\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.parseLightDomDetail();\n }\n\n /**\n * Parse `<tbw-grid-detail>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-detail>\n * <div class=\"detail-content\">\n * <p>Name: {{ row.name }}</p>\n * <p>Email: {{ row.email }}</p>\n * </div>\n * </tbw-grid-detail>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `animation`: 'slide' | 'fade' | 'false' (default: 'slide')\n * - `show-expand-column`: 'true' | 'false' (default: 'true')\n * - `expand-on-row-click`: 'true' | 'false' (default: 'false')\n * - `collapse-on-click-outside`: 'true' | 'false' (default: 'false')\n * - `height`: number (pixels) or 'auto' (default: 'auto')\n */\n private parseLightDomDetail(): void {\n const gridEl = this.grid as unknown as Element;\n if (!gridEl || typeof gridEl.querySelector !== 'function') return;\n\n const detailEl = gridEl.querySelector('tbw-grid-detail');\n if (!detailEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., Angular adapter intercepts for ng-template rendering)\n const gridWithAdapter = gridEl as unknown as {\n __frameworkAdapter?: {\n parseDetailElement?: (el: Element) => ((row: any, rowIndex: number) => HTMLElement | string) | undefined;\n };\n };\n if (gridWithAdapter.__frameworkAdapter?.parseDetailElement) {\n const adapterRenderer = gridWithAdapter.__frameworkAdapter.parseDetailElement(detailEl);\n if (adapterRenderer) {\n this.config = { ...this.config, detailRenderer: adapterRenderer };\n return;\n }\n }\n\n // Parse attributes for configuration\n const animation = detailEl.getAttribute('animation');\n const showExpandColumn = detailEl.getAttribute('show-expand-column');\n const expandOnRowClick = detailEl.getAttribute('expand-on-row-click');\n const collapseOnClickOutside = detailEl.getAttribute('collapse-on-click-outside');\n const heightAttr = detailEl.getAttribute('height');\n\n const configUpdates: Partial<MasterDetailConfig> = {};\n\n if (animation !== null) {\n configUpdates.animation = animation === 'false' ? false : (animation as 'slide' | 'fade');\n }\n if (showExpandColumn !== null) {\n configUpdates.showExpandColumn = showExpandColumn !== 'false';\n }\n if (expandOnRowClick !== null) {\n configUpdates.expandOnRowClick = expandOnRowClick === 'true';\n }\n if (collapseOnClickOutside !== null) {\n configUpdates.collapseOnClickOutside = collapseOnClickOutside === 'true';\n }\n if (heightAttr !== null) {\n configUpdates.detailHeight = heightAttr === 'auto' ? 'auto' : parseInt(heightAttr, 10);\n }\n\n // Get template content from innerHTML\n const templateHTML = detailEl.innerHTML.trim();\n if (templateHTML && !this.config.detailRenderer) {\n // Create a template-based renderer using the inner HTML\n configUpdates.detailRenderer = (row: any, _rowIndex: number): string => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row });\n // Sanitize the result to prevent XSS\n return sanitizeHTML(evaluated);\n };\n }\n\n // Merge updates into config\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and CSS variable.\n */\n private get isAnimationEnabled(): boolean {\n const gridEl = this.grid as unknown as GridWithConfig;\n const mode = gridEl.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n if (mode === false || mode === 'off') return false;\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable\n const host = this.shadowRoot?.host as HTMLElement | undefined;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n return true;\n }\n\n /**\n * Get expand/collapse animation style from plugin config.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide'; // Plugin default\n }\n\n /**\n * Get animation duration from CSS variable (set by grid).\n */\n private get animationDuration(): number {\n const host = this.shadowRoot?.host as HTMLElement | undefined;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n /**\n * Apply expand animation to a detail element.\n */\n private animateExpand(detailEl: HTMLElement): void {\n if (!this.isAnimationEnabled || this.animationStyle === false) return;\n\n detailEl.classList.add('tbw-expanding');\n detailEl.addEventListener(\n 'animationend',\n () => {\n detailEl.classList.remove('tbw-expanding');\n },\n { once: true },\n );\n }\n\n /**\n * Apply collapse animation to a detail element and remove after animation.\n */\n private animateCollapse(detailEl: HTMLElement, onComplete: () => void): void {\n if (!this.isAnimationEnabled || this.animationStyle === false) {\n onComplete();\n return;\n }\n\n detailEl.classList.add('tbw-collapsing');\n const cleanup = () => {\n detailEl.classList.remove('tbw-collapsing');\n onComplete();\n };\n detailEl.addEventListener('animationend', cleanup, { once: true });\n // Fallback timeout in case animation doesn't fire\n setTimeout(cleanup, this.animationDuration + 50);\n }\n\n // #endregion\n\n // #region Internal State\n private expandedRows: Set<any> = new Set();\n private detailElements: Map<any, HTMLElement> = new Map();\n // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.expandedRows.clear();\n this.detailElements.clear();\n }\n // #endregion\n\n // #region Hooks\n\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n if (!this.config.detailRenderer) {\n return [...columns];\n }\n\n // Wrap first column's renderer to add expand/collapse toggle\n const cols = [...columns];\n if (cols.length > 0) {\n const firstCol = { ...cols[0] };\n const originalRenderer = firstCol.viewRenderer as MasterDetailWrappedRenderer | undefined;\n\n // Skip if already wrapped by this plugin (prevents double-wrapping on re-render)\n if (originalRenderer?.__masterDetailWrapped) {\n return cols;\n }\n\n const wrappedRenderer: MasterDetailWrappedRenderer = (renderCtx) => {\n const { value, row } = renderCtx;\n const isExpanded = this.expandedRows.has(row as object);\n\n const container = document.createElement('span');\n container.className = 'master-detail-cell-wrapper';\n\n // Expand/collapse toggle icon\n const toggle = document.createElement('span');\n toggle.className = `master-detail-toggle${isExpanded ? ' expanded' : ''}`;\n // Use grid-level icons (fall back to defaults)\n this.setIcon(toggle, this.resolveIcon(isExpanded ? 'collapse' : 'expand'));\n // role=\"button\" is required for aria-expanded to be valid\n toggle.setAttribute('role', 'button');\n toggle.setAttribute('tabindex', '0');\n toggle.setAttribute('aria-expanded', String(isExpanded));\n toggle.setAttribute('aria-label', isExpanded ? 'Collapse details' : 'Expand details');\n // Click handling is done via onCellClick hook (not direct addEventListener)\n // This ensures proper event delegation through the grid's event system\n container.appendChild(toggle);\n\n // Cell content\n const content = document.createElement('span');\n if (originalRenderer) {\n const rendered = originalRenderer(renderCtx);\n if (rendered instanceof Node) {\n content.appendChild(rendered);\n } else {\n content.textContent = String(rendered ?? value ?? '');\n }\n } else {\n content.textContent = String(value ?? '');\n }\n container.appendChild(content);\n\n return container;\n };\n\n // Mark renderer as wrapped to prevent double-wrapping\n wrappedRenderer.__masterDetailWrapped = true;\n firstCol.viewRenderer = wrappedRenderer;\n\n cols[0] = firstCol;\n }\n\n return cols;\n }\n\n override onRowClick(event: RowClickEvent): boolean | void {\n if (!this.config.expandOnRowClick || !this.config.detailRenderer) return;\n\n this.expandedRows = toggleDetailRow(this.expandedRows, event.row as object);\n\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex: event.rowIndex,\n row: event.row as Record<string, unknown>,\n expanded: this.expandedRows.has(event.row as object),\n });\n\n this.requestRender();\n return false;\n }\n\n override onCellClick(event: CellClickEvent): boolean | void {\n // Handle click on master-detail toggle icon (same pattern as TreePlugin)\n const target = event.originalEvent?.target as HTMLElement;\n if (target?.classList.contains('master-detail-toggle')) {\n const row = event.row;\n const rowIndex = event.rowIndex;\n this.expandedRows = toggleDetailRow(this.expandedRows, row as object);\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex,\n row: row as Record<string, unknown>,\n expanded: this.expandedRows.has(row as object),\n });\n this.requestRender();\n return true; // Prevent default handling\n }\n\n // Sync detail rows after cell click triggers refreshVirtualWindow\n // This runs in microtask to ensure DOM updates are complete\n if (this.expandedRows.size > 0) {\n queueMicrotask(() => this.#syncDetailRows());\n }\n return; // Don't prevent default\n }\n\n override afterRender(): void {\n this.#syncDetailRows();\n }\n\n /**\n * Called on scroll to sync detail elements with visible rows.\n * Removes details for rows that scrolled out of view and reattaches for visible rows.\n */\n override onScrollRender(): void {\n if (!this.config.detailRenderer || this.expandedRows.size === 0) return;\n // Full sync needed on scroll to clean up orphaned details\n this.#syncDetailRows();\n }\n\n /**\n * Full sync of detail rows - cleans up stale elements and creates new ones.\n * Detail rows are inserted as siblings AFTER their master row to survive row rebuilds.\n */\n #syncDetailRows(): void {\n if (!this.config.detailRenderer) return;\n\n const body = this.shadowRoot?.querySelector('.rows');\n if (!body) return;\n\n // Build a map of row index -> row element for visible rows\n const visibleRowMap = new Map<number, Element>();\n const dataRows = body.querySelectorAll('.data-grid-row');\n const columnCount = this.columns.length;\n\n for (const rowEl of dataRows) {\n const firstCell = rowEl.querySelector('.cell[data-row]');\n const rowIndex = firstCell ? parseInt(firstCell.getAttribute('data-row') ?? '-1', 10) : -1;\n if (rowIndex >= 0) {\n visibleRowMap.set(rowIndex, rowEl);\n }\n }\n\n // Remove detail rows whose parent row is no longer visible or no longer expanded\n const existingDetails = body.querySelectorAll('.master-detail-row');\n for (const detailEl of existingDetails) {\n const forIndex = parseInt(detailEl.getAttribute('data-detail-for') ?? '-1', 10);\n const row = forIndex >= 0 ? this.rows[forIndex] : undefined;\n const isStillExpanded = row && this.expandedRows.has(row);\n const isRowVisible = visibleRowMap.has(forIndex);\n\n // Remove detail if not expanded or if parent row scrolled out\n if (!isStillExpanded || !isRowVisible) {\n detailEl.remove();\n if (row) this.detailElements.delete(row);\n }\n }\n\n // Insert detail rows for expanded rows that are visible\n for (const [rowIndex, rowEl] of visibleRowMap) {\n const row = this.rows[rowIndex];\n if (!row || !this.expandedRows.has(row)) continue;\n\n // Check if detail already exists for this row\n const existingDetail = this.detailElements.get(row);\n if (existingDetail) {\n // Ensure it's positioned correctly (as next sibling of row element)\n if (existingDetail.previousElementSibling !== rowEl) {\n rowEl.after(existingDetail);\n }\n continue;\n }\n\n // Create new detail element\n const detailEl = createDetailElement(row, rowIndex, this.config.detailRenderer, columnCount);\n\n if (typeof this.config.detailHeight === 'number') {\n detailEl.style.height = `${this.config.detailHeight}px`;\n }\n\n // Insert as sibling after the row element (not as child)\n rowEl.after(detailEl);\n this.detailElements.set(row, detailEl);\n\n // Apply expand animation\n this.animateExpand(detailEl);\n }\n }\n\n /**\n * Return total extra height from all expanded detail rows.\n * Used by grid virtualization to adjust scrollbar height.\n */\n override getExtraHeight(): number {\n let totalHeight = 0;\n for (const row of this.expandedRows) {\n const detailEl = this.detailElements.get(row);\n if (detailEl) {\n totalHeight += detailEl.offsetHeight;\n } else {\n // Detail not yet rendered - estimate based on config or default\n const configHeight = this.config?.detailHeight;\n totalHeight += typeof configHeight === 'number' ? configHeight : 150;\n }\n }\n return totalHeight;\n }\n\n /**\n * Return extra height that appears before a given row index.\n * This is the sum of heights of all expanded details whose parent row is before the given index.\n */\n override getExtraHeightBefore(beforeRowIndex: number): number {\n let totalHeight = 0;\n for (const row of this.expandedRows) {\n const rowIndex = this.rows.indexOf(row);\n // Include detail if it's for a row before the given index\n if (rowIndex >= 0 && rowIndex < beforeRowIndex) {\n const detailEl = this.detailElements.get(row);\n if (detailEl) {\n totalHeight += detailEl.offsetHeight;\n } else {\n const configHeight = this.config?.detailHeight;\n totalHeight += typeof configHeight === 'number' ? configHeight : 150;\n }\n }\n }\n return totalHeight;\n }\n\n /**\n * Adjust the virtualization start index to keep expanded row visible while its detail is visible.\n * This ensures the detail scrolls smoothly out of view instead of disappearing abruptly.\n */\n override adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n if (this.expandedRows.size === 0) return start;\n\n // Build sorted list of expanded row indices for cumulative height calculation\n const expandedIndices: Array<{ index: number; row: any }> = [];\n for (const row of this.expandedRows) {\n const index = this.rows.indexOf(row);\n if (index >= 0) {\n expandedIndices.push({ index, row });\n }\n }\n expandedIndices.sort((a, b) => a.index - b.index);\n\n let minStart = start;\n\n // Calculate actual scroll position for each expanded row,\n // accounting for cumulative detail heights before it\n let cumulativeExtraHeight = 0;\n\n for (const { index: rowIndex, row } of expandedIndices) {\n // Actual position includes all detail heights before this row\n const actualRowTop = rowIndex * rowHeight + cumulativeExtraHeight;\n const detailEl = this.detailElements.get(row);\n const detailHeight =\n detailEl?.offsetHeight ?? (typeof this.config?.detailHeight === 'number' ? this.config.detailHeight : 150);\n const actualDetailBottom = actualRowTop + rowHeight + detailHeight;\n\n // Update cumulative height for next iteration\n cumulativeExtraHeight += detailHeight;\n\n // Skip rows that are at or after the calculated start\n if (rowIndex >= start) continue;\n\n // If any part of the detail is still visible (below the scroll position),\n // we need to keep the parent row in the render range\n if (actualDetailBottom > scrollTop) {\n if (rowIndex < minStart) {\n minStart = rowIndex;\n }\n }\n }\n\n return minStart;\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand the detail row at the given index.\n * @param rowIndex - Index of the row to expand\n */\n expand(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = expandDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Collapse the detail row at the given index.\n * @param rowIndex - Index of the row to collapse\n */\n collapse(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = collapseDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Toggle the detail row at the given index.\n * @param rowIndex - Index of the row to toggle\n */\n toggle(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = toggleDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Check if the detail row at the given index is expanded.\n * @param rowIndex - Index of the row to check\n * @returns Whether the detail row is expanded\n */\n isExpanded(rowIndex: number): boolean {\n const row = this.rows[rowIndex];\n return row ? isDetailExpanded(this.expandedRows, row) : false;\n }\n\n /**\n * Expand all detail rows.\n */\n expandAll(): void {\n for (const row of this.rows) {\n this.expandedRows.add(row);\n }\n this.requestRender();\n }\n\n /**\n * Collapse all detail rows.\n */\n collapseAll(): void {\n this.expandedRows.clear();\n this.requestRender();\n }\n\n /**\n * Get the indices of all expanded rows.\n * @returns Array of row indices that are expanded\n */\n getExpandedRows(): number[] {\n const indices: number[] = [];\n for (const row of this.expandedRows) {\n const idx = this.rows.indexOf(row);\n if (idx >= 0) indices.push(idx);\n }\n return indices;\n }\n\n /**\n * Get the detail element for a specific row.\n * @param rowIndex - Index of the row\n * @returns The detail HTMLElement or undefined\n */\n getDetailElement(rowIndex: number): HTMLElement | undefined {\n const row = this.rows[rowIndex];\n return row ? this.detailElements.get(row) : undefined;\n }\n\n /**\n * Re-parse light DOM to refresh the detail renderer.\n * Call this after framework templates are registered (e.g., Angular ngAfterContentInit).\n *\n * This allows frameworks to register templates asynchronously and then\n * update the plugin's detailRenderer.\n */\n refreshDetailRenderer(): void {\n // Force re-parse by temporarily clearing the renderer\n const currentRenderer = this.config.detailRenderer;\n this.config = { ...this.config, detailRenderer: undefined };\n this.parseLightDomDetail();\n\n // If no new renderer was found, restore the original\n if (!this.config.detailRenderer && currentRenderer) {\n this.config = { ...this.config, detailRenderer: currentRenderer };\n }\n\n // Request a COLUMNS phase re-render so processColumns runs again with the new detailRenderer\n // This ensures the expand toggle is added to the first column.\n // Must use refreshColumns() (COLUMNS phase) not requestRender() (ROWS phase)\n // because processColumns only runs at COLUMNS phase or higher.\n if (this.config.detailRenderer) {\n const grid = this.grid as unknown as { refreshColumns?: () => void };\n if (typeof grid.refreshColumns === 'function') {\n grid.refreshColumns();\n } else {\n // Fallback to requestRender if refreshColumns not available\n this.requestRender();\n }\n }\n }\n // #endregion\n\n // #region Styles\n override readonly styles = styles;\n // #endregion\n}\n"],"names":["toggleDetailRow","expandedRows","row","newExpanded","expandDetailRow","collapseDetailRow","isDetailExpanded","createDetailElement","rowIndex","renderer","columnCount","detailRow","detailCell","content","MasterDetailPlugin","BaseGridPlugin","grid","gridEl","detailEl","gridWithAdapter","adapterRenderer","animation","showExpandColumn","expandOnRowClick","collapseOnClickOutside","heightAttr","configUpdates","templateHTML","_rowIndex","evaluated","evalTemplateString","sanitizeHTML","mode","host","durationStr","parsed","onComplete","cleanup","columns","cols","firstCol","originalRenderer","wrappedRenderer","renderCtx","value","isExpanded","container","toggle","rendered","event","#syncDetailRows","body","visibleRowMap","dataRows","rowEl","firstCell","existingDetails","forIndex","isStillExpanded","isRowVisible","existingDetail","totalHeight","configHeight","beforeRowIndex","start","scrollTop","rowHeight","expandedIndices","index","a","b","minStart","cumulativeExtraHeight","actualRowTop","detailHeight","actualDetailBottom","indices","idx","currentRenderer","styles"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"master-detail.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/master-detail/master-detail.ts","../../../../../libs/grid/src/lib/plugins/master-detail/MasterDetailPlugin.ts"],"sourcesContent":["/**\n * Master/Detail Core Logic\n *\n * Pure functions for managing detail row expansion state.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n// Uses `any` for maximum flexibility with user-defined row types.\n\n/**\n * Toggle the expansion state of a detail row.\n * Returns a new Set with the updated state.\n */\nexport function toggleDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n if (newExpanded.has(row)) {\n newExpanded.delete(row);\n } else {\n newExpanded.add(row);\n }\n return newExpanded;\n}\n\n/**\n * Expand a detail row.\n * Returns a new Set with the row added.\n */\nexport function expandDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n newExpanded.add(row);\n return newExpanded;\n}\n\n/**\n * Collapse a detail row.\n * Returns a new Set with the row removed.\n */\nexport function collapseDetailRow(expandedRows: Set<object>, row: object): Set<object> {\n const newExpanded = new Set(expandedRows);\n newExpanded.delete(row);\n return newExpanded;\n}\n\n/**\n * Check if a detail row is expanded.\n */\nexport function isDetailExpanded(expandedRows: Set<object>, row: object): boolean {\n return expandedRows.has(row);\n}\n\n/**\n * Create a detail element for a given row.\n * The element spans all columns and contains the rendered content.\n */\nexport function createDetailElement(\n row: any,\n rowIndex: number,\n renderer: (row: any, rowIndex: number) => HTMLElement | string,\n columnCount: number\n): HTMLElement {\n const detailRow = document.createElement('div');\n detailRow.className = 'master-detail-row';\n detailRow.setAttribute('data-detail-for', String(rowIndex));\n detailRow.setAttribute('role', 'row');\n\n const detailCell = document.createElement('div');\n detailCell.className = 'master-detail-cell';\n detailCell.setAttribute('role', 'cell');\n detailCell.style.gridColumn = `1 / ${columnCount + 1}`;\n\n const content = renderer(row, rowIndex);\n if (typeof content === 'string') {\n detailCell.innerHTML = content;\n } else if (content instanceof HTMLElement) {\n detailCell.appendChild(content);\n }\n\n detailRow.appendChild(detailCell);\n return detailRow;\n}\n","/**\n * Master/Detail Plugin (Class-based)\n *\n * Enables expandable detail rows showing additional content for each row.\n * Animation style is plugin-configured; respects grid-level animation.mode.\n */\n\nimport { evalTemplateString, sanitizeHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, CellClickEvent, GridElement, RowClickEvent } from '../../core/plugin/base-plugin';\nimport { createExpanderColumnConfig, findExpanderColumn, isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig } from '../../core/types';\nimport {\n collapseDetailRow,\n createDetailElement,\n expandDetailRow,\n isDetailExpanded,\n toggleDetailRow,\n} from './master-detail';\nimport styles from './master-detail.css?inline';\nimport type { DetailExpandDetail, ExpandCollapseAnimation, MasterDetailConfig } from './types';\n\n/**\n * Master/Detail Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new MasterDetailPlugin({\n * enabled: true,\n * detailRenderer: (row) => `<div>Details for ${row.name}</div>`,\n * expandOnRowClick: true,\n * })\n * ```\n */\nexport class MasterDetailPlugin extends BaseGridPlugin<MasterDetailConfig> {\n readonly name = 'masterDetail';\n override readonly styles = styles;\n\n protected override get defaultConfig(): Partial<MasterDetailConfig> {\n return {\n detailHeight: 'auto',\n expandOnRowClick: false,\n collapseOnClickOutside: false,\n showExpandColumn: true,\n animation: 'slide', // Plugin's own default\n };\n }\n\n // #region Light DOM Parsing\n\n /**\n * Called when plugin is attached to the grid.\n * Parses light DOM for `<tbw-grid-detail>` elements to configure detail templates.\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.parseLightDomDetail();\n }\n\n /**\n * Parse `<tbw-grid-detail>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-detail>\n * <div class=\"detail-content\">\n * <p>Name: {{ row.name }}</p>\n * <p>Email: {{ row.email }}</p>\n * </div>\n * </tbw-grid-detail>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `animation`: 'slide' | 'fade' | 'false' (default: 'slide')\n * - `show-expand-column`: 'true' | 'false' (default: 'true')\n * - `expand-on-row-click`: 'true' | 'false' (default: 'false')\n * - `collapse-on-click-outside`: 'true' | 'false' (default: 'false')\n * - `height`: number (pixels) or 'auto' (default: 'auto')\n */\n private parseLightDomDetail(): void {\n const gridEl = this.grid as unknown as Element;\n if (!gridEl || typeof gridEl.querySelector !== 'function') return;\n\n const detailEl = gridEl.querySelector('tbw-grid-detail');\n if (!detailEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., Angular adapter intercepts for ng-template rendering)\n const gridWithAdapter = gridEl as unknown as {\n __frameworkAdapter?: {\n parseDetailElement?: (el: Element) => ((row: any, rowIndex: number) => HTMLElement | string) | undefined;\n };\n };\n if (gridWithAdapter.__frameworkAdapter?.parseDetailElement) {\n const adapterRenderer = gridWithAdapter.__frameworkAdapter.parseDetailElement(detailEl);\n if (adapterRenderer) {\n this.config = { ...this.config, detailRenderer: adapterRenderer };\n return;\n }\n }\n\n // Parse attributes for configuration\n const animation = detailEl.getAttribute('animation');\n const showExpandColumn = detailEl.getAttribute('show-expand-column');\n const expandOnRowClick = detailEl.getAttribute('expand-on-row-click');\n const collapseOnClickOutside = detailEl.getAttribute('collapse-on-click-outside');\n const heightAttr = detailEl.getAttribute('height');\n\n const configUpdates: Partial<MasterDetailConfig> = {};\n\n if (animation !== null) {\n configUpdates.animation = animation === 'false' ? false : (animation as 'slide' | 'fade');\n }\n if (showExpandColumn !== null) {\n configUpdates.showExpandColumn = showExpandColumn !== 'false';\n }\n if (expandOnRowClick !== null) {\n configUpdates.expandOnRowClick = expandOnRowClick === 'true';\n }\n if (collapseOnClickOutside !== null) {\n configUpdates.collapseOnClickOutside = collapseOnClickOutside === 'true';\n }\n if (heightAttr !== null) {\n configUpdates.detailHeight = heightAttr === 'auto' ? 'auto' : parseInt(heightAttr, 10);\n }\n\n // Get template content from innerHTML\n const templateHTML = detailEl.innerHTML.trim();\n if (templateHTML && !this.config.detailRenderer) {\n // Create a template-based renderer using the inner HTML\n configUpdates.detailRenderer = (row: any, _rowIndex: number): string => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row });\n // Sanitize the result to prevent XSS\n return sanitizeHTML(evaluated);\n };\n }\n\n // Merge updates into config\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n // #region Animation Helpers\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n /**\n * Apply expand animation to a detail element.\n */\n private animateExpand(detailEl: HTMLElement): void {\n if (!this.isAnimationEnabled || this.animationStyle === false) return;\n\n detailEl.classList.add('tbw-expanding');\n detailEl.addEventListener(\n 'animationend',\n () => {\n detailEl.classList.remove('tbw-expanding');\n },\n { once: true },\n );\n }\n\n /**\n * Apply collapse animation to a detail element and remove after animation.\n */\n private animateCollapse(detailEl: HTMLElement, onComplete: () => void): void {\n if (!this.isAnimationEnabled || this.animationStyle === false) {\n onComplete();\n return;\n }\n\n detailEl.classList.add('tbw-collapsing');\n const cleanup = () => {\n detailEl.classList.remove('tbw-collapsing');\n onComplete();\n };\n detailEl.addEventListener('animationend', cleanup, { once: true });\n // Fallback timeout in case animation doesn't fire\n setTimeout(cleanup, this.animationDuration + 50);\n }\n\n // #endregion\n\n // #region Internal State\n private expandedRows: Set<any> = new Set();\n private detailElements: Map<any, HTMLElement> = new Map();\n\n /** Default height for detail rows when not configured */\n private static readonly DEFAULT_DETAIL_HEIGHT = 150;\n\n /**\n * Get the estimated height for a detail row.\n */\n private getDetailHeight(row: any): number {\n const detailEl = this.detailElements.get(row);\n if (detailEl) return detailEl.offsetHeight;\n return typeof this.config?.detailHeight === 'number'\n ? this.config.detailHeight\n : MasterDetailPlugin.DEFAULT_DETAIL_HEIGHT;\n }\n\n /**\n * Toggle a row's detail and emit event.\n */\n private toggleAndEmit(row: any, rowIndex: number): void {\n this.expandedRows = toggleDetailRow(this.expandedRows, row as object);\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex,\n row: row as Record<string, unknown>,\n expanded: this.expandedRows.has(row as object),\n });\n this.requestRender();\n }\n // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.expandedRows.clear();\n this.detailElements.clear();\n }\n // #endregion\n\n // #region Hooks\n\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n if (!this.config.detailRenderer || this.config.showExpandColumn === false) {\n return [...columns];\n }\n\n const cols = [...columns];\n\n // Check if expander column already exists (from this or another plugin)\n const existingExpander = findExpanderColumn(cols);\n if (existingExpander) {\n // Another plugin already added an expander column - don't add duplicate\n // Our expand logic will be handled via onCellClick on the expander column\n return cols;\n }\n\n // Create dedicated expander column that stays fixed at position 0\n const expanderCol = createExpanderColumnConfig(this.name);\n expanderCol.viewRenderer = (renderCtx) => {\n const { row } = renderCtx;\n const isExpanded = this.expandedRows.has(row as object);\n\n const container = document.createElement('span');\n container.className = 'master-detail-expander expander-cell';\n\n // Expand/collapse toggle icon\n const toggle = document.createElement('span');\n toggle.className = `master-detail-toggle${isExpanded ? ' expanded' : ''}`;\n // Use grid-level icons (fall back to defaults)\n this.setIcon(toggle, this.resolveIcon(isExpanded ? 'collapse' : 'expand'));\n // role=\"button\" is required for aria-expanded to be valid\n toggle.setAttribute('role', 'button');\n toggle.setAttribute('tabindex', '0');\n toggle.setAttribute('aria-expanded', String(isExpanded));\n toggle.setAttribute('aria-label', isExpanded ? 'Collapse details' : 'Expand details');\n container.appendChild(toggle);\n\n return container;\n };\n\n // Prepend expander column to ensure it's always first\n return [expanderCol, ...cols];\n }\n\n override onRowClick(event: RowClickEvent): boolean | void {\n if (!this.config.expandOnRowClick || !this.config.detailRenderer) return;\n this.toggleAndEmit(event.row, event.rowIndex);\n return false;\n }\n\n override onCellClick(event: CellClickEvent): boolean | void {\n // Handle click on master-detail toggle icon (same pattern as TreePlugin)\n const target = event.originalEvent?.target as HTMLElement;\n if (target?.classList.contains('master-detail-toggle')) {\n this.toggleAndEmit(event.row, event.rowIndex);\n return true; // Prevent default handling\n }\n\n // Sync detail rows after cell click triggers refreshVirtualWindow\n // This runs in microtask to ensure DOM updates are complete\n if (this.expandedRows.size > 0) {\n queueMicrotask(() => this.#syncDetailRows());\n }\n return; // Don't prevent default\n }\n\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion when focus is on the expander column\n if (event.key !== ' ') return;\n\n const focusCol = this.grid._focusCol;\n const focusRow = this.grid._focusRow;\n const column = this.columns[focusCol];\n\n // Only handle SPACE on expander column\n if (!column || !isExpanderColumn(column)) return;\n\n const row = this.rows[focusRow];\n if (!row) return;\n\n event.preventDefault();\n this.toggleAndEmit(row, focusRow);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n override afterRender(): void {\n this.#syncDetailRows();\n }\n\n /**\n * Called on scroll to sync detail elements with visible rows.\n * Removes details for rows that scrolled out of view and reattaches for visible rows.\n */\n override onScrollRender(): void {\n if (!this.config.detailRenderer || this.expandedRows.size === 0) return;\n // Full sync needed on scroll to clean up orphaned details\n this.#syncDetailRows();\n }\n\n /**\n * Full sync of detail rows - cleans up stale elements and creates new ones.\n * Detail rows are inserted as siblings AFTER their master row to survive row rebuilds.\n */\n #syncDetailRows(): void {\n if (!this.config.detailRenderer) return;\n\n const body = this.shadowRoot?.querySelector('.rows');\n if (!body) return;\n\n // Build a map of row index -> row element for visible rows\n const visibleRowMap = new Map<number, Element>();\n const dataRows = body.querySelectorAll('.data-grid-row');\n const columnCount = this.columns.length;\n\n for (const rowEl of dataRows) {\n const firstCell = rowEl.querySelector('.cell[data-row]');\n const rowIndex = firstCell ? parseInt(firstCell.getAttribute('data-row') ?? '-1', 10) : -1;\n if (rowIndex >= 0) {\n visibleRowMap.set(rowIndex, rowEl);\n }\n }\n\n // Remove detail rows whose parent row is no longer visible or no longer expanded\n const existingDetails = body.querySelectorAll('.master-detail-row');\n for (const detailEl of existingDetails) {\n const forIndex = parseInt(detailEl.getAttribute('data-detail-for') ?? '-1', 10);\n const row = forIndex >= 0 ? this.rows[forIndex] : undefined;\n const isStillExpanded = row && this.expandedRows.has(row);\n const isRowVisible = visibleRowMap.has(forIndex);\n\n // Remove detail if not expanded or if parent row scrolled out\n if (!isStillExpanded || !isRowVisible) {\n detailEl.remove();\n if (row) this.detailElements.delete(row);\n }\n }\n\n // Insert detail rows for expanded rows that are visible\n for (const [rowIndex, rowEl] of visibleRowMap) {\n const row = this.rows[rowIndex];\n if (!row || !this.expandedRows.has(row)) continue;\n\n // Check if detail already exists for this row\n const existingDetail = this.detailElements.get(row);\n if (existingDetail) {\n // Ensure it's positioned correctly (as next sibling of row element)\n if (existingDetail.previousElementSibling !== rowEl) {\n rowEl.after(existingDetail);\n }\n continue;\n }\n\n // Create new detail element\n const detailEl = createDetailElement(row, rowIndex, this.config.detailRenderer, columnCount);\n\n if (typeof this.config.detailHeight === 'number') {\n detailEl.style.height = `${this.config.detailHeight}px`;\n }\n\n // Insert as sibling after the row element (not as child)\n rowEl.after(detailEl);\n this.detailElements.set(row, detailEl);\n\n // Apply expand animation\n this.animateExpand(detailEl);\n }\n }\n\n /**\n * Return total extra height from all expanded detail rows.\n * Used by grid virtualization to adjust scrollbar height.\n */\n override getExtraHeight(): number {\n let totalHeight = 0;\n for (const row of this.expandedRows) {\n totalHeight += this.getDetailHeight(row);\n }\n return totalHeight;\n }\n\n /**\n * Return extra height that appears before a given row index.\n * This is the sum of heights of all expanded details whose parent row is before the given index.\n */\n override getExtraHeightBefore(beforeRowIndex: number): number {\n let totalHeight = 0;\n for (const row of this.expandedRows) {\n const rowIndex = this.rows.indexOf(row);\n // Include detail if it's for a row before the given index\n if (rowIndex >= 0 && rowIndex < beforeRowIndex) {\n totalHeight += this.getDetailHeight(row);\n }\n }\n return totalHeight;\n }\n\n /**\n * Adjust the virtualization start index to keep expanded row visible while its detail is visible.\n * This ensures the detail scrolls smoothly out of view instead of disappearing abruptly.\n */\n override adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n if (this.expandedRows.size === 0) return start;\n\n // Build sorted list of expanded row indices for cumulative height calculation\n const expandedIndices: Array<{ index: number; row: any }> = [];\n for (const row of this.expandedRows) {\n const index = this.rows.indexOf(row);\n if (index >= 0) {\n expandedIndices.push({ index, row });\n }\n }\n expandedIndices.sort((a, b) => a.index - b.index);\n\n let minStart = start;\n\n // Calculate actual scroll position for each expanded row,\n // accounting for cumulative detail heights before it\n let cumulativeExtraHeight = 0;\n\n for (const { index: rowIndex, row } of expandedIndices) {\n // Actual position includes all detail heights before this row\n const actualRowTop = rowIndex * rowHeight + cumulativeExtraHeight;\n const detailHeight = this.getDetailHeight(row);\n const actualDetailBottom = actualRowTop + rowHeight + detailHeight;\n\n // Update cumulative height for next iteration\n cumulativeExtraHeight += detailHeight;\n\n // Skip rows that are at or after the calculated start\n if (rowIndex >= start) continue;\n\n // If any part of the detail is still visible (below the scroll position),\n // we need to keep the parent row in the render range\n if (actualDetailBottom > scrollTop) {\n if (rowIndex < minStart) {\n minStart = rowIndex;\n }\n }\n }\n\n return minStart;\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand the detail row at the given index.\n * @param rowIndex - Index of the row to expand\n */\n expand(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = expandDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Collapse the detail row at the given index.\n * @param rowIndex - Index of the row to collapse\n */\n collapse(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = collapseDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Toggle the detail row at the given index.\n * @param rowIndex - Index of the row to toggle\n */\n toggle(rowIndex: number): void {\n const row = this.rows[rowIndex];\n if (row) {\n this.expandedRows = toggleDetailRow(this.expandedRows, row);\n this.requestRender();\n }\n }\n\n /**\n * Check if the detail row at the given index is expanded.\n * @param rowIndex - Index of the row to check\n * @returns Whether the detail row is expanded\n */\n isExpanded(rowIndex: number): boolean {\n const row = this.rows[rowIndex];\n return row ? isDetailExpanded(this.expandedRows, row) : false;\n }\n\n /**\n * Expand all detail rows.\n */\n expandAll(): void {\n for (const row of this.rows) {\n this.expandedRows.add(row);\n }\n this.requestRender();\n }\n\n /**\n * Collapse all detail rows.\n */\n collapseAll(): void {\n this.expandedRows.clear();\n this.requestRender();\n }\n\n /**\n * Get the indices of all expanded rows.\n * @returns Array of row indices that are expanded\n */\n getExpandedRows(): number[] {\n const indices: number[] = [];\n for (const row of this.expandedRows) {\n const idx = this.rows.indexOf(row);\n if (idx >= 0) indices.push(idx);\n }\n return indices;\n }\n\n /**\n * Get the detail element for a specific row.\n * @param rowIndex - Index of the row\n * @returns The detail HTMLElement or undefined\n */\n getDetailElement(rowIndex: number): HTMLElement | undefined {\n const row = this.rows[rowIndex];\n return row ? this.detailElements.get(row) : undefined;\n }\n\n /**\n * Re-parse light DOM to refresh the detail renderer.\n * Call this after framework templates are registered (e.g., Angular ngAfterContentInit).\n *\n * This allows frameworks to register templates asynchronously and then\n * update the plugin's detailRenderer.\n */\n refreshDetailRenderer(): void {\n // Force re-parse by temporarily clearing the renderer\n const currentRenderer = this.config.detailRenderer;\n this.config = { ...this.config, detailRenderer: undefined };\n this.parseLightDomDetail();\n\n // If no new renderer was found, restore the original\n if (!this.config.detailRenderer && currentRenderer) {\n this.config = { ...this.config, detailRenderer: currentRenderer };\n }\n\n // Request a COLUMNS phase re-render so processColumns runs again with the new detailRenderer\n // This ensures the expand toggle is added to the first column.\n // Must use refreshColumns() (COLUMNS phase) not requestRender() (ROWS phase)\n // because processColumns only runs at COLUMNS phase or higher.\n if (this.config.detailRenderer) {\n const grid = this.grid as unknown as { refreshColumns?: () => void };\n if (typeof grid.refreshColumns === 'function') {\n grid.refreshColumns();\n } else {\n // Fallback to requestRender if refreshColumns not available\n this.requestRender();\n }\n }\n }\n // #endregion\n}\n"],"names":["toggleDetailRow","expandedRows","row","newExpanded","expandDetailRow","collapseDetailRow","isDetailExpanded","createDetailElement","rowIndex","renderer","columnCount","detailRow","detailCell","content","MasterDetailPlugin","BaseGridPlugin","styles","grid","gridEl","detailEl","gridWithAdapter","adapterRenderer","animation","showExpandColumn","expandOnRowClick","collapseOnClickOutside","heightAttr","configUpdates","templateHTML","_rowIndex","evaluated","evalTemplateString","sanitizeHTML","onComplete","cleanup","columns","cols","findExpanderColumn","expanderCol","createExpanderColumnConfig","renderCtx","isExpanded","container","toggle","event","#syncDetailRows","focusCol","focusRow","column","isExpanderColumn","body","visibleRowMap","dataRows","rowEl","firstCell","existingDetails","forIndex","isStillExpanded","isRowVisible","existingDetail","totalHeight","beforeRowIndex","start","scrollTop","rowHeight","expandedIndices","index","a","b","minStart","cumulativeExtraHeight","actualRowTop","detailHeight","actualDetailBottom","indices","idx","currentRenderer"],"mappings":"wfAaO,SAASA,EAAgBC,EAA2BC,EAA0B,CACnF,MAAMC,EAAc,IAAI,IAAIF,CAAY,EACxC,OAAIE,EAAY,IAAID,CAAG,EACrBC,EAAY,OAAOD,CAAG,EAEtBC,EAAY,IAAID,CAAG,EAEdC,CACT,CAMO,SAASC,EAAgBH,EAA2BC,EAA0B,CACnF,MAAMC,EAAc,IAAI,IAAIF,CAAY,EACxC,OAAAE,EAAY,IAAID,CAAG,EACZC,CACT,CAMO,SAASE,EAAkBJ,EAA2BC,EAA0B,CACrF,MAAMC,EAAc,IAAI,IAAIF,CAAY,EACxC,OAAAE,EAAY,OAAOD,CAAG,EACfC,CACT,CAKO,SAASG,EAAiBL,EAA2BC,EAAsB,CAChF,OAAOD,EAAa,IAAIC,CAAG,CAC7B,CAMO,SAASK,EACdL,EACAM,EACAC,EACAC,EACa,CACb,MAAMC,EAAY,SAAS,cAAc,KAAK,EAC9CA,EAAU,UAAY,oBACtBA,EAAU,aAAa,kBAAmB,OAAOH,CAAQ,CAAC,EAC1DG,EAAU,aAAa,OAAQ,KAAK,EAEpC,MAAMC,EAAa,SAAS,cAAc,KAAK,EAC/CA,EAAW,UAAY,qBACvBA,EAAW,aAAa,OAAQ,MAAM,EACtCA,EAAW,MAAM,WAAa,OAAOF,EAAc,CAAC,GAEpD,MAAMG,EAAUJ,EAASP,EAAKM,CAAQ,EACtC,OAAI,OAAOK,GAAY,SACrBD,EAAW,UAAYC,EACdA,aAAmB,aAC5BD,EAAW,YAAYC,CAAO,EAGhCF,EAAU,YAAYC,CAAU,EACzBD,CACT,g7CC9CO,MAAMG,UAA2BC,EAAAA,cAAmC,CAChE,KAAO,eACE,OAASC,EAE3B,IAAuB,eAA6C,CAClE,MAAO,CACL,aAAc,OACd,iBAAkB,GAClB,uBAAwB,GACxB,iBAAkB,GAClB,UAAW,OAAA,CAEf,CAQS,OAAOC,EAAyB,CACvC,MAAM,OAAOA,CAAI,EACjB,KAAK,oBAAA,CACP,CAwBQ,qBAA4B,CAClC,MAAMC,EAAS,KAAK,KACpB,GAAI,CAACA,GAAU,OAAOA,EAAO,eAAkB,WAAY,OAE3D,MAAMC,EAAWD,EAAO,cAAc,iBAAiB,EACvD,GAAI,CAACC,EAAU,OAIf,MAAMC,EAAkBF,EAKxB,GAAIE,EAAgB,oBAAoB,mBAAoB,CAC1D,MAAMC,EAAkBD,EAAgB,mBAAmB,mBAAmBD,CAAQ,EACtF,GAAIE,EAAiB,CACnB,KAAK,OAAS,CAAE,GAAG,KAAK,OAAQ,eAAgBA,CAAA,EAChD,MACF,CACF,CAGA,MAAMC,EAAYH,EAAS,aAAa,WAAW,EAC7CI,EAAmBJ,EAAS,aAAa,oBAAoB,EAC7DK,EAAmBL,EAAS,aAAa,qBAAqB,EAC9DM,EAAyBN,EAAS,aAAa,2BAA2B,EAC1EO,EAAaP,EAAS,aAAa,QAAQ,EAE3CQ,EAA6C,CAAA,EAE/CL,IAAc,OAChBK,EAAc,UAAYL,IAAc,QAAU,GAASA,GAEzDC,IAAqB,OACvBI,EAAc,iBAAmBJ,IAAqB,SAEpDC,IAAqB,OACvBG,EAAc,iBAAmBH,IAAqB,QAEpDC,IAA2B,OAC7BE,EAAc,uBAAyBF,IAA2B,QAEhEC,IAAe,OACjBC,EAAc,aAAeD,IAAe,OAAS,OAAS,SAASA,EAAY,EAAE,GAIvF,MAAME,EAAeT,EAAS,UAAU,KAAA,EACpCS,GAAgB,CAAC,KAAK,OAAO,iBAE/BD,EAAc,eAAiB,CAACzB,EAAU2B,IAA8B,CAEtE,MAAMC,EAAYC,EAAAA,mBAAmBH,EAAc,CAAE,MAAO1B,EAAK,IAAAA,EAAK,EAEtE,OAAO8B,EAAAA,aAAaF,CAAS,CAC/B,GAIE,OAAO,KAAKH,CAAa,EAAE,OAAS,IACtC,KAAK,OAAS,CAAE,GAAG,KAAK,OAAQ,GAAGA,CAAA,EAEvC,CAUA,IAAY,gBAA0C,CACpD,OAAK,KAAK,mBACH,KAAK,OAAO,WAAa,QADK,EAEvC,CAKQ,cAAcR,EAA6B,CAC7C,CAAC,KAAK,oBAAsB,KAAK,iBAAmB,KAExDA,EAAS,UAAU,IAAI,eAAe,EACtCA,EAAS,iBACP,eACA,IAAM,CACJA,EAAS,UAAU,OAAO,eAAe,CAC3C,EACA,CAAE,KAAM,EAAA,CAAK,EAEjB,CAKQ,gBAAgBA,EAAuBc,EAA8B,CAC3E,GAAI,CAAC,KAAK,oBAAsB,KAAK,iBAAmB,GAAO,CAC7DA,EAAA,EACA,MACF,CAEAd,EAAS,UAAU,IAAI,gBAAgB,EACvC,MAAMe,EAAU,IAAM,CACpBf,EAAS,UAAU,OAAO,gBAAgB,EAC1Cc,EAAA,CACF,EACAd,EAAS,iBAAiB,eAAgBe,EAAS,CAAE,KAAM,GAAM,EAEjE,WAAWA,EAAS,KAAK,kBAAoB,EAAE,CACjD,CAKQ,iBAA6B,IAC7B,mBAA4C,IAGpD,OAAwB,sBAAwB,IAKxC,gBAAgBhC,EAAkB,CACxC,MAAMiB,EAAW,KAAK,eAAe,IAAIjB,CAAG,EAC5C,OAAIiB,EAAiBA,EAAS,aACvB,OAAO,KAAK,QAAQ,cAAiB,SACxC,KAAK,OAAO,aACZL,EAAmB,qBACzB,CAKQ,cAAcZ,EAAUM,EAAwB,CACtD,KAAK,aAAeR,EAAgB,KAAK,aAAcE,CAAa,EACpE,KAAK,KAAyB,gBAAiB,CAC7C,SAAAM,EACA,IAAAN,EACA,SAAU,KAAK,aAAa,IAAIA,CAAa,CAAA,CAC9C,EACD,KAAK,cAAA,CACP,CAKS,QAAe,CACtB,KAAK,aAAa,MAAA,EAClB,KAAK,eAAe,MAAA,CACtB,CAKS,eAAeiC,EAAkD,CACxE,GAAI,CAAC,KAAK,OAAO,gBAAkB,KAAK,OAAO,mBAAqB,GAClE,MAAO,CAAC,GAAGA,CAAO,EAGpB,MAAMC,EAAO,CAAC,GAAGD,CAAO,EAIxB,GADyBE,EAAAA,mBAAmBD,CAAI,EAI9C,OAAOA,EAIT,MAAME,EAAcC,EAAAA,2BAA2B,KAAK,IAAI,EACxD,OAAAD,EAAY,aAAgBE,GAAc,CACxC,KAAM,CAAE,IAAAtC,GAAQsC,EACVC,EAAa,KAAK,aAAa,IAAIvC,CAAa,EAEhDwC,EAAY,SAAS,cAAc,MAAM,EAC/CA,EAAU,UAAY,uCAGtB,MAAMC,EAAS,SAAS,cAAc,MAAM,EAC5C,OAAAA,EAAO,UAAY,uBAAuBF,EAAa,YAAc,EAAE,GAEvE,KAAK,QAAQE,EAAQ,KAAK,YAAYF,EAAa,WAAa,QAAQ,CAAC,EAEzEE,EAAO,aAAa,OAAQ,QAAQ,EACpCA,EAAO,aAAa,WAAY,GAAG,EACnCA,EAAO,aAAa,gBAAiB,OAAOF,CAAU,CAAC,EACvDE,EAAO,aAAa,aAAcF,EAAa,mBAAqB,gBAAgB,EACpFC,EAAU,YAAYC,CAAM,EAErBD,CACT,EAGO,CAACJ,EAAa,GAAGF,CAAI,CAC9B,CAES,WAAWQ,EAAsC,CACxD,GAAI,GAAC,KAAK,OAAO,kBAAoB,CAAC,KAAK,OAAO,gBAClD,YAAK,cAAcA,EAAM,IAAKA,EAAM,QAAQ,EACrC,EACT,CAES,YAAYA,EAAuC,CAG1D,GADeA,EAAM,eAAe,QACxB,UAAU,SAAS,sBAAsB,EACnD,YAAK,cAAcA,EAAM,IAAKA,EAAM,QAAQ,EACrC,GAKL,KAAK,aAAa,KAAO,GAC3B,eAAe,IAAM,KAAKC,IAAiB,CAG/C,CAES,UAAUD,EAAsC,CAEvD,GAAIA,EAAM,MAAQ,IAAK,OAEvB,MAAME,EAAW,KAAK,KAAK,UACrBC,EAAW,KAAK,KAAK,UACrBC,EAAS,KAAK,QAAQF,CAAQ,EAGpC,GAAI,CAACE,GAAU,CAACC,EAAAA,iBAAiBD,CAAM,EAAG,OAE1C,MAAM9C,EAAM,KAAK,KAAK6C,CAAQ,EAC9B,GAAK7C,EAEL,OAAA0C,EAAM,eAAA,EACN,KAAK,cAAc1C,EAAK6C,CAAQ,EAGhC,KAAK,uBAAA,EACE,EACT,CAES,aAAoB,CAC3B,KAAKF,GAAA,CACP,CAMS,gBAAuB,CAC1B,CAAC,KAAK,OAAO,gBAAkB,KAAK,aAAa,OAAS,GAE9D,KAAKA,GAAA,CACP,CAMAA,IAAwB,CACtB,GAAI,CAAC,KAAK,OAAO,eAAgB,OAEjC,MAAMK,EAAO,KAAK,YAAY,cAAc,OAAO,EACnD,GAAI,CAACA,EAAM,OAGX,MAAMC,MAAoB,IACpBC,EAAWF,EAAK,iBAAiB,gBAAgB,EACjDxC,EAAc,KAAK,QAAQ,OAEjC,UAAW2C,KAASD,EAAU,CAC5B,MAAME,EAAYD,EAAM,cAAc,iBAAiB,EACjD7C,EAAW8C,EAAY,SAASA,EAAU,aAAa,UAAU,GAAK,KAAM,EAAE,EAAI,GACpF9C,GAAY,GACd2C,EAAc,IAAI3C,EAAU6C,CAAK,CAErC,CAGA,MAAME,EAAkBL,EAAK,iBAAiB,oBAAoB,EAClE,UAAW/B,KAAYoC,EAAiB,CACtC,MAAMC,EAAW,SAASrC,EAAS,aAAa,iBAAiB,GAAK,KAAM,EAAE,EACxEjB,EAAMsD,GAAY,EAAI,KAAK,KAAKA,CAAQ,EAAI,OAC5CC,EAAkBvD,GAAO,KAAK,aAAa,IAAIA,CAAG,EAClDwD,EAAeP,EAAc,IAAIK,CAAQ,GAG3C,CAACC,GAAmB,CAACC,KACvBvC,EAAS,OAAA,EACLjB,GAAK,KAAK,eAAe,OAAOA,CAAG,EAE3C,CAGA,SAAW,CAACM,EAAU6C,CAAK,IAAKF,EAAe,CAC7C,MAAMjD,EAAM,KAAK,KAAKM,CAAQ,EAC9B,GAAI,CAACN,GAAO,CAAC,KAAK,aAAa,IAAIA,CAAG,EAAG,SAGzC,MAAMyD,EAAiB,KAAK,eAAe,IAAIzD,CAAG,EAClD,GAAIyD,EAAgB,CAEdA,EAAe,yBAA2BN,GAC5CA,EAAM,MAAMM,CAAc,EAE5B,QACF,CAGA,MAAMxC,EAAWZ,EAAoBL,EAAKM,EAAU,KAAK,OAAO,eAAgBE,CAAW,EAEvF,OAAO,KAAK,OAAO,cAAiB,WACtCS,EAAS,MAAM,OAAS,GAAG,KAAK,OAAO,YAAY,MAIrDkC,EAAM,MAAMlC,CAAQ,EACpB,KAAK,eAAe,IAAIjB,EAAKiB,CAAQ,EAGrC,KAAK,cAAcA,CAAQ,CAC7B,CACF,CAMS,gBAAyB,CAChC,IAAIyC,EAAc,EAClB,UAAW1D,KAAO,KAAK,aACrB0D,GAAe,KAAK,gBAAgB1D,CAAG,EAEzC,OAAO0D,CACT,CAMS,qBAAqBC,EAAgC,CAC5D,IAAID,EAAc,EAClB,UAAW1D,KAAO,KAAK,aAAc,CACnC,MAAMM,EAAW,KAAK,KAAK,QAAQN,CAAG,EAElCM,GAAY,GAAKA,EAAWqD,IAC9BD,GAAe,KAAK,gBAAgB1D,CAAG,EAE3C,CACA,OAAO0D,CACT,CAMS,mBAAmBE,EAAeC,EAAmBC,EAA2B,CACvF,GAAI,KAAK,aAAa,OAAS,EAAG,OAAOF,EAGzC,MAAMG,EAAsD,CAAA,EAC5D,UAAW/D,KAAO,KAAK,aAAc,CACnC,MAAMgE,EAAQ,KAAK,KAAK,QAAQhE,CAAG,EAC/BgE,GAAS,GACXD,EAAgB,KAAK,CAAE,MAAAC,EAAO,IAAAhE,CAAA,CAAK,CAEvC,CACA+D,EAAgB,KAAK,CAACE,EAAGC,IAAMD,EAAE,MAAQC,EAAE,KAAK,EAEhD,IAAIC,EAAWP,EAIXQ,EAAwB,EAE5B,SAAW,CAAE,MAAO9D,EAAU,IAAAN,CAAA,IAAS+D,EAAiB,CAEtD,MAAMM,EAAe/D,EAAWwD,EAAYM,EACtCE,EAAe,KAAK,gBAAgBtE,CAAG,EACvCuE,EAAqBF,EAAeP,EAAYQ,EAGtDF,GAAyBE,EAGrB,EAAAhE,GAAYsD,IAIZW,EAAqBV,GACnBvD,EAAW6D,IACbA,EAAW7D,EAGjB,CAEA,OAAO6D,CACT,CASA,OAAO7D,EAAwB,CAC7B,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC1BN,IACF,KAAK,aAAeE,EAAgB,KAAK,aAAcF,CAAG,EAC1D,KAAK,cAAA,EAET,CAMA,SAASM,EAAwB,CAC/B,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC1BN,IACF,KAAK,aAAeG,EAAkB,KAAK,aAAcH,CAAG,EAC5D,KAAK,cAAA,EAET,CAMA,OAAOM,EAAwB,CAC7B,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC1BN,IACF,KAAK,aAAeF,EAAgB,KAAK,aAAcE,CAAG,EAC1D,KAAK,cAAA,EAET,CAOA,WAAWM,EAA2B,CACpC,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC9B,OAAON,EAAMI,EAAiB,KAAK,aAAcJ,CAAG,EAAI,EAC1D,CAKA,WAAkB,CAChB,UAAWA,KAAO,KAAK,KACrB,KAAK,aAAa,IAAIA,CAAG,EAE3B,KAAK,cAAA,CACP,CAKA,aAAoB,CAClB,KAAK,aAAa,MAAA,EAClB,KAAK,cAAA,CACP,CAMA,iBAA4B,CAC1B,MAAMwE,EAAoB,CAAA,EAC1B,UAAWxE,KAAO,KAAK,aAAc,CACnC,MAAMyE,EAAM,KAAK,KAAK,QAAQzE,CAAG,EAC7ByE,GAAO,GAAGD,EAAQ,KAAKC,CAAG,CAChC,CACA,OAAOD,CACT,CAOA,iBAAiBlE,EAA2C,CAC1D,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC9B,OAAON,EAAM,KAAK,eAAe,IAAIA,CAAG,EAAI,MAC9C,CASA,uBAA8B,CAE5B,MAAM0E,EAAkB,KAAK,OAAO,eAapC,GAZA,KAAK,OAAS,CAAE,GAAG,KAAK,OAAQ,eAAgB,MAAA,EAChD,KAAK,oBAAA,EAGD,CAAC,KAAK,OAAO,gBAAkBA,IACjC,KAAK,OAAS,CAAE,GAAG,KAAK,OAAQ,eAAgBA,CAAA,GAO9C,KAAK,OAAO,eAAgB,CAC9B,MAAM3D,EAAO,KAAK,KACd,OAAOA,EAAK,gBAAmB,WACjCA,EAAK,eAAA,EAGL,KAAK,cAAA,CAET,CACF,CAEF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(d,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],l):(d=typeof globalThis<"u"?globalThis:d||self,l(d.TbwGridPlugin_multiSort={},d.TbwGrid))})(this,(function(d,l){"use strict";function S(o,t,i){return t.length?[...o].sort((n,e)=>{for(const r of t){const c=i.find(u=>u.field===r.field)?.sortComparator??M,p=n[r.field],m=e[r.field],s=c(p,m,n,e);if(s!==0)return r.direction==="asc"?s:-s}return 0}):[...o]}function M(o,t){return o==null&&t==null?0:o==null?1:t==null?-1:typeof o=="number"&&typeof t=="number"?o-t:o instanceof Date&&t instanceof Date?o.getTime()-t.getTime():typeof o=="boolean"&&typeof t=="boolean"?o===t?0:o?-1:1:String(o).localeCompare(String(t))}function y(o,t,i,n){const e=o.find(r=>r.field===t);return i?e?e.direction==="asc"?o.map(r=>r.field===t?{...r,direction:"desc"}:r):o.filter(r=>r.field!==t):o.length<n?[...o,{field:t,direction:"asc"}]:o:e?.direction==="asc"?[{field:t,direction:"desc"}]:e?.direction==="desc"?[]:[{field:t,direction:"asc"}]}function h(o,t){const i=o.findIndex(n=>n.field===t);return i>=0?i+1:void 0}function g(o,t){return o.find(i=>i.field===t)?.direction}const b='.header-cell[data-sort=asc]:after{content:"↑";margin-left:4px;opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:4px;opacity:.8}.sort-indicator{margin-left:4px;opacity:.8}.sort-index{font-size:10px;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:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;margin-left:2px;font-weight:600}';class w extends l.BaseGridPlugin{name="multiSort";
|
|
1
|
+
(function(d,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],l):(d=typeof globalThis<"u"?globalThis:d||self,l(d.TbwGridPlugin_multiSort={},d.TbwGrid))})(this,(function(d,l){"use strict";function S(o,t,i){return t.length?[...o].sort((n,e)=>{for(const r of t){const c=i.find(u=>u.field===r.field)?.sortComparator??M,p=n[r.field],m=e[r.field],s=c(p,m,n,e);if(s!==0)return r.direction==="asc"?s:-s}return 0}):[...o]}function M(o,t){return o==null&&t==null?0:o==null?1:t==null?-1:typeof o=="number"&&typeof t=="number"?o-t:o instanceof Date&&t instanceof Date?o.getTime()-t.getTime():typeof o=="boolean"&&typeof t=="boolean"?o===t?0:o?-1:1:String(o).localeCompare(String(t))}function y(o,t,i,n){const e=o.find(r=>r.field===t);return i?e?e.direction==="asc"?o.map(r=>r.field===t?{...r,direction:"desc"}:r):o.filter(r=>r.field!==t):o.length<n?[...o,{field:t,direction:"asc"}]:o:e?.direction==="asc"?[{field:t,direction:"desc"}]:e?.direction==="desc"?[]:[{field:t,direction:"asc"}]}function h(o,t){const i=o.findIndex(n=>n.field===t);return i>=0?i+1:void 0}function g(o,t){return o.find(i=>i.field===t)?.direction}const b='.header-cell[data-sort=asc]:after{content:"↑";margin-left:4px;opacity:.8}.header-cell[data-sort=desc]:after{content:"↓";margin-left:4px;opacity:.8}.sort-indicator{margin-left:4px;opacity:.8}.sort-index{font-size:10px;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:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;margin-left:2px;font-weight:600}';class w extends l.BaseGridPlugin{name="multiSort";styles=b;get defaultConfig(){return{maxSortColumns:3,showSortIndex:!0}}sortModel=[];detach(){this.sortModel=[]}processRows(t){return this.sortModel.length===0?[...t]:S([...t],this.sortModel,[...this.columns])}onHeaderClick(t){if(!this.columns.find(r=>r.field===t.field)?.sortable)return!1;const n=t.originalEvent.shiftKey,e=this.config.maxSortColumns??3;return this.sortModel=y(this.sortModel,t.field,n,e),this.emit("sort-change",{sortModel:[...this.sortModel]}),this.requestRender(),!0}afterRender(){const t=this.shadowRoot;if(!t)return;const i=this.config.showSortIndex!==!1;t.querySelectorAll(".header-row .cell[data-field]").forEach(e=>{const r=e.getAttribute("data-field");if(!r)return;const f=h(this.sortModel,r),c=g(this.sortModel,r);if(e.querySelector(".sort-index")?.remove(),c){e.querySelector('[part~="sort-indicator"], .sort-indicator')?.remove(),e.setAttribute("data-sort",c);const s=document.createElement("span");s.className="sort-indicator",this.setIcon(s,this.resolveIcon(c==="asc"?"sortAsc":"sortDesc"));const u=e.querySelector(".tbw-filter-btn"),C=e.querySelector(".resize-handle"),x=u??C;if(x?e.insertBefore(s,x):e.appendChild(s),i&&this.sortModel.length>1&&f!==void 0){const a=document.createElement("span");a.className="sort-index",a.textContent=String(f),s.nextSibling?e.insertBefore(a,s.nextSibling):e.appendChild(a)}}else e.removeAttribute("data-sort")})}getSortModel(){return[...this.sortModel]}setSortModel(t){this.sortModel=[...t],this.emit("sort-change",{sortModel:[...t]}),this.requestRender()}clearSort(){this.sortModel=[],this.emit("sort-change",{sortModel:[]}),this.requestRender()}getSortIndex(t){return h(this.sortModel,t)}getSortDirection(t){return g(this.sortModel,t)}getColumnState(t){const i=this.sortModel.findIndex(e=>e.field===t);return i===-1?void 0:{sort:{direction:this.sortModel[i].direction,priority:i}}}applyColumnState(t,i){if(!i.sort){this.sortModel=this.sortModel.filter(r=>r.field!==t);return}const n=this.sortModel.findIndex(r=>r.field===t),e={field:t,direction:i.sort.direction};n!==-1?this.sortModel[n]=e:this.sortModel.splice(i.sort.priority,0,e)}}d.MultiSortPlugin=w,Object.defineProperty(d,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 return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\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 { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, 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 * @example\n * ```ts\n * new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })\n * ```\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n readonly name = 'multiSort';\n override readonly version = '1.0.0';\n\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 // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.sortModel = [];\n }\n // #endregion\n\n // #region Hooks\n\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n return [...rows];\n }\n return applySorts([...rows], this.sortModel, [...this.columns]);\n }\n\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\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n\n return true;\n }\n\n override afterRender(): void {\n const shadowRoot = this.shadowRoot;\n if (!shadowRoot) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n // Update all sortable header cells with sort indicators\n const headerCells = shadowRoot.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 const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\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 // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // For unsorted columns, leave the base indicator (⇅) alone\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.emit('sort-change', { sortModel: [...model] });\n this.requestRender();\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\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 */\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 */\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 // Re-sort the model by priority to ensure correct order\n // This is handled after all columns are processed, but we maintain order here\n }\n // #endregion\n\n // #region Styles\n\n override readonly styles = styles;\n // #endregion\n}\n"],"names":["applySorts","rows","sorts","columns","a","b","sort","comparator","c","defaultComparator","aVal","bVal","result","toggleSort","current","field","shiftKey","maxColumns","existing","s","getSortIndex","sortModel","index","getSortDirection","MultiSortPlugin","BaseGridPlugin","event","shadowRoot","showIndex","cell","sortIndex","sortDir","indicator","filterBtn","resizeHandle","insertBefore","badge","model","state","existingIndex","newEntry","styles"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multi-sort.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/multi-sort/multi-sort.ts","../../../../../libs/grid/src/lib/plugins/multi-sort/MultiSortPlugin.ts"],"sourcesContent":["/**\n * Multi-Sort Core Logic\n *\n * Pure functions for multi-column sorting operations.\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SortModel } from './types';\n\n/**\n * Apply multiple sort columns to a row array.\n * Sorts are applied in order - first sort has highest priority.\n *\n * @param rows - Array of row objects to sort\n * @param sorts - Ordered array of sort configurations\n * @param columns - Column configurations (for custom comparators)\n * @returns New sorted array (does not mutate original)\n */\nexport function applySorts<TRow = unknown>(rows: TRow[], sorts: SortModel[], columns: ColumnConfig<TRow>[]): TRow[] {\n if (!sorts.length) return [...rows];\n\n return [...rows].sort((a, b) => {\n for (const sort of sorts) {\n const col = columns.find((c) => c.field === sort.field);\n const comparator = col?.sortComparator ?? defaultComparator;\n const aVal = (a as Record<string, unknown>)[sort.field];\n const bVal = (b as Record<string, unknown>)[sort.field];\n const result = comparator(aVal, bVal, a, b);\n if (result !== 0) {\n return sort.direction === 'asc' ? result : -result;\n }\n }\n return 0;\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 { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\nimport type { ColumnState } from '../../core/types';\nimport { applySorts, getSortDirection, getSortIndex, 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 * @example\n * ```ts\n * new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })\n * ```\n */\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\n readonly name = 'multiSort';\n override readonly styles = styles;\n\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 // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.sortModel = [];\n }\n // #endregion\n\n // #region Hooks\n\n override processRows(rows: readonly unknown[]): unknown[] {\n if (this.sortModel.length === 0) {\n return [...rows];\n }\n return applySorts([...rows], this.sortModel, [...this.columns]);\n }\n\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\n this.emit('sort-change', { sortModel: [...this.sortModel] });\n this.requestRender();\n\n return true;\n }\n\n override afterRender(): void {\n const shadowRoot = this.shadowRoot;\n if (!shadowRoot) return;\n\n const showIndex = this.config.showSortIndex !== false;\n\n // Update all sortable header cells with sort indicators\n const headerCells = shadowRoot.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 const existingBadge = cell.querySelector('.sort-index');\n existingBadge?.remove();\n\n if (sortDir) {\n // Column is sorted - remove base indicator and add our own\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\n existingIndicator?.remove();\n\n cell.setAttribute('data-sort', sortDir);\n\n // Add sort arrow indicator - insert BEFORE filter button and resize handle\n // to maintain consistent order: [label, sort-indicator, sort-index, filter-btn, resize-handle]\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n // Use grid-level icons (fall back to defaults)\n this.setIcon(indicator, this.resolveIcon(sortDir === 'asc' ? 'sortAsc' : 'sortDesc'));\n\n // Find insertion point: before filter button or resize handle\n const filterBtn = cell.querySelector('.tbw-filter-btn');\n const resizeHandle = cell.querySelector('.resize-handle');\n const insertBefore = filterBtn ?? resizeHandle;\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\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 // Insert badge right after the indicator\n if (indicator.nextSibling) {\n cell.insertBefore(badge, indicator.nextSibling);\n } else {\n cell.appendChild(badge);\n }\n }\n } else {\n cell.removeAttribute('data-sort');\n // For unsorted columns, leave the base indicator (⇅) alone\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.emit('sort-change', { sortModel: [...model] });\n this.requestRender();\n }\n\n /**\n * Clear all sorting.\n */\n clearSort(): void {\n this.sortModel = [];\n this.emit('sort-change', { sortModel: [] });\n this.requestRender();\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 */\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 */\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 // Re-sort the model by priority to ensure correct order\n // This is handled after all columns are processed, but we maintain order here\n }\n // #endregion\n}\n"],"names":["applySorts","rows","sorts","columns","a","b","sort","comparator","c","defaultComparator","aVal","bVal","result","toggleSort","current","field","shiftKey","maxColumns","existing","s","getSortIndex","sortModel","index","getSortDirection","MultiSortPlugin","BaseGridPlugin","styles","event","shadowRoot","showIndex","cell","sortIndex","sortDir","indicator","filterBtn","resizeHandle","insertBefore","badge","model","state","existingIndex","newEntry"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(f,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],d):(f=typeof globalThis<"u"?globalThis:f||self,d(f.TbwGridPlugin_pinnedColumns={},f.TbwGrid))})(this,(function(f,d){"use strict";function p(r){return r.filter(t=>t.sticky==="left")}function k(r){return r.filter(t=>t.sticky==="right")}function h(r){return r.some(t=>t.sticky==="left"||t.sticky==="right")}function y(r,t){const s=r.shadowRoot;if(!s)return;const e=Array.from(s.querySelectorAll(".header-row .cell"));if(!e.length)return;const c=new Map;t.forEach((o,l)=>{o.field&&c.set(o.field,l)});let u=0;for(const o of t)if(o.sticky==="left"){const l=c.get(o.field),i=e.find(n=>n.getAttribute("data-field")===o.field);i&&(i.classList.add("sticky-left"),i.style.position="sticky",i.style.left=u+"px",l!==void 0&&s.querySelectorAll(`.data-grid-row .cell[data-col="${l}"]`).forEach(n=>{n.classList.add("sticky-left"),n.style.position="sticky",n.style.left=u+"px"}),u+=i.offsetWidth)}let a=0;for(const o of[...t].reverse())if(o.sticky==="right"){const l=c.get(o.field),i=e.find(n=>n.getAttribute("data-field")===o.field);i&&(i.classList.add("sticky-right"),i.style.position="sticky",i.style.right=a+"px",l!==void 0&&s.querySelectorAll(`.data-grid-row .cell[data-col="${l}"]`).forEach(n=>{n.classList.add("sticky-right"),n.style.position="sticky",n.style.right=a+"px"}),a+=i.offsetWidth)}}function g(r){const t=r.shadowRoot;if(!t)return;t.querySelectorAll(".sticky-left, .sticky-right").forEach(e=>{e.classList.remove("sticky-left","sticky-right"),e.style.position="",e.style.left="",e.style.right=""})}class m extends d.BaseGridPlugin{name="pinnedColumns";
|
|
1
|
+
(function(f,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],d):(f=typeof globalThis<"u"?globalThis:f||self,d(f.TbwGridPlugin_pinnedColumns={},f.TbwGrid))})(this,(function(f,d){"use strict";function p(r){return r.filter(t=>t.sticky==="left")}function k(r){return r.filter(t=>t.sticky==="right")}function h(r){return r.some(t=>t.sticky==="left"||t.sticky==="right")}function y(r,t){const s=r.shadowRoot;if(!s)return;const e=Array.from(s.querySelectorAll(".header-row .cell"));if(!e.length)return;const c=new Map;t.forEach((o,l)=>{o.field&&c.set(o.field,l)});let u=0;for(const o of t)if(o.sticky==="left"){const l=c.get(o.field),i=e.find(n=>n.getAttribute("data-field")===o.field);i&&(i.classList.add("sticky-left"),i.style.position="sticky",i.style.left=u+"px",l!==void 0&&s.querySelectorAll(`.data-grid-row .cell[data-col="${l}"]`).forEach(n=>{n.classList.add("sticky-left"),n.style.position="sticky",n.style.left=u+"px"}),u+=i.offsetWidth)}let a=0;for(const o of[...t].reverse())if(o.sticky==="right"){const l=c.get(o.field),i=e.find(n=>n.getAttribute("data-field")===o.field);i&&(i.classList.add("sticky-right"),i.style.position="sticky",i.style.right=a+"px",l!==void 0&&s.querySelectorAll(`.data-grid-row .cell[data-col="${l}"]`).forEach(n=>{n.classList.add("sticky-right"),n.style.position="sticky",n.style.right=a+"px"}),a+=i.offsetWidth)}}function g(r){const t=r.shadowRoot;if(!t)return;t.querySelectorAll(".sticky-left, .sticky-right").forEach(e=>{e.classList.remove("sticky-left","sticky-right"),e.style.position="",e.style.left="",e.style.right=""})}class m extends d.BaseGridPlugin{name="pinnedColumns";get defaultConfig(){return{}}isApplied=!1;leftOffsets=new Map;rightOffsets=new Map;detach(){this.leftOffsets.clear(),this.rightOffsets.clear(),this.isApplied=!1}static detect(t,s){const e=s?.columns;return Array.isArray(e)?h(e):!1}processColumns(t){return this.isApplied=h([...t]),[...t]}afterRender(){if(!this.isApplied)return;const t=this.grid,s=[...this.columns];if(!h(s)){g(t),this.isApplied=!1;return}queueMicrotask(()=>{y(t,s)})}onPluginQuery(t){switch(t.type){case d.PLUGIN_QUERIES.CAN_MOVE_COLUMN:{const s=t.context,e=s.sticky;if(e==="left"||e==="right")return!1;const c=s.meta?.sticky;return c==="left"||c==="right"?!1:void 0}default:return}}refreshStickyOffsets(){const t=[...this.columns];y(this.grid,t)}getLeftPinnedColumns(){const t=[...this.columns];return p(t)}getRightPinnedColumns(){const t=[...this.columns];return k(t)}clearStickyPositions(){g(this.grid)}getHorizontalScrollOffsets(t,s){if(!this.isApplied)return;let e=0,c=0;if(t){const a=t.querySelectorAll(".sticky-left"),o=t.querySelectorAll(".sticky-right");a.forEach(l=>{e+=l.offsetWidth}),o.forEach(l=>{c+=l.offsetWidth})}else{const o=this.grid.shadowRoot;o&&o.querySelectorAll(".header-row .cell").forEach(i=>{i.classList.contains("sticky-left")?e+=i.offsetWidth:i.classList.contains("sticky-right")&&(c+=i.offsetWidth)})}const u=s?.classList.contains("sticky-left")||s?.classList.contains("sticky-right");return{left:e,right:c,skipScroll:u}}}f.PinnedColumnsPlugin=m,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
|
|
2
2
|
//# sourceMappingURL=pinned-columns.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-columns.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-columns/pinned-columns.ts","../../../../../libs/grid/src/lib/plugins/pinned-columns/PinnedColumnsPlugin.ts"],"sourcesContent":["/**\n * Sticky Columns Core Logic\n *\n * Pure functions for applying sticky (pinned) column positioning.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { StickyPosition } from './types';\n\n/**\n * Get columns that should be sticky on the left.\n *\n * @param columns - Array of column configurations\n * @returns Array of columns with sticky='left'\n */\nexport function getLeftStickyColumns(columns: any[]): any[] {\n return columns.filter((col) => col.sticky === 'left');\n}\n\n/**\n * Get columns that should be sticky on the right.\n *\n * @param columns - Array of column configurations\n * @returns Array of columns with sticky='right'\n */\nexport function getRightStickyColumns(columns: any[]): any[] {\n return columns.filter((col) => col.sticky === 'right');\n}\n\n/**\n * Check if any columns have sticky positioning.\n *\n * @param columns - Array of column configurations\n * @returns True if any column has sticky position\n */\nexport function hasStickyColumns(columns: any[]): boolean {\n return columns.some((col) => col.sticky === 'left' || col.sticky === 'right');\n}\n\n/**\n * Get the sticky position of a column.\n *\n * @param column - Column configuration\n * @returns The sticky position or null if not sticky\n */\nexport function getColumnStickyPosition(column: any): StickyPosition | null {\n if (column.sticky === 'left') return 'left';\n if (column.sticky === 'right') return 'right';\n return null;\n}\n\n/**\n * Calculate left offsets for sticky-left columns.\n * Returns a map of field -> offset in pixels.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @returns Map of field to left offset\n */\nexport function calculateLeftStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n for (const col of columns) {\n if (col.sticky === 'left') {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Calculate right offsets for sticky-right columns.\n * Processes columns in reverse order.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @returns Map of field to right offset\n */\nexport function calculateRightStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n // Process in reverse for right-sticky columns\n const reversed = [...columns].reverse();\n for (const col of reversed) {\n if (col.sticky === 'right') {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Apply sticky offsets to header and body cells.\n * This modifies the DOM elements in place.\n *\n * @param host - The grid host element\n * @param columns - Array of column configurations\n */\nexport function applyStickyOffsets(host: HTMLElement, columns: any[]): void {\n const shadowRoot = host.shadowRoot;\n if (!shadowRoot) return;\n\n const headerCells = Array.from(shadowRoot.querySelectorAll('.header-row .cell')) as HTMLElement[];\n if (!headerCells.length) return;\n\n // Build column index map for matching body cells (which use data-col, not data-field)\n const fieldToIndex = new Map<string, number>();\n columns.forEach((col, i) => {\n if (col.field) fieldToIndex.set(col.field, i);\n });\n\n // Apply left sticky\n let left = 0;\n for (const col of columns) {\n if (col.sticky === 'left') {\n const colIndex = fieldToIndex.get(col.field);\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-left');\n cell.style.position = 'sticky';\n cell.style.left = left + 'px';\n // Body cells use data-col (column index), not data-field\n if (colIndex !== undefined) {\n shadowRoot.querySelectorAll(`.data-grid-row .cell[data-col=\"${colIndex}\"]`).forEach((el) => {\n el.classList.add('sticky-left');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.left = left + 'px';\n });\n }\n left += cell.offsetWidth;\n }\n }\n }\n\n // Apply right sticky (process in reverse)\n let right = 0;\n for (const col of [...columns].reverse()) {\n if (col.sticky === 'right') {\n const colIndex = fieldToIndex.get(col.field);\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-right');\n cell.style.position = 'sticky';\n cell.style.right = right + 'px';\n // Body cells use data-col (column index), not data-field\n if (colIndex !== undefined) {\n shadowRoot.querySelectorAll(`.data-grid-row .cell[data-col=\"${colIndex}\"]`).forEach((el) => {\n el.classList.add('sticky-right');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.right = right + 'px';\n });\n }\n right += cell.offsetWidth;\n }\n }\n }\n}\n\n/**\n * Clear sticky positioning from all cells.\n *\n * @param host - The grid host element\n */\nexport function clearStickyOffsets(host: HTMLElement): void {\n const shadowRoot = host.shadowRoot;\n if (!shadowRoot) return;\n\n const cells = shadowRoot.querySelectorAll('.sticky-left, .sticky-right');\n cells.forEach((cell) => {\n cell.classList.remove('sticky-left', 'sticky-right');\n (cell as HTMLElement).style.position = '';\n (cell as HTMLElement).style.left = '';\n (cell as HTMLElement).style.right = '';\n });\n}\n","/**\n * Pinned Columns Plugin (Class-based)\n *\n * Enables column pinning (sticky left/right positioning).\n */\n\nimport { BaseGridPlugin, PLUGIN_QUERIES, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport {\n applyStickyOffsets,\n clearStickyOffsets,\n getLeftStickyColumns,\n getRightStickyColumns,\n hasStickyColumns,\n} from './pinned-columns';\nimport type { PinnedColumnsConfig } from './types';\n\n/**\n * Pinned Columns Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new PinnedColumnsPlugin({ enabled: true })\n * ```\n */\nexport class PinnedColumnsPlugin extends BaseGridPlugin<PinnedColumnsConfig> {\n readonly name = 'pinnedColumns';\n override readonly version = '1.0.0';\n\n protected override get defaultConfig(): Partial<PinnedColumnsConfig> {\n return {};\n }\n\n // #region Internal State\n private isApplied = false;\n private leftOffsets = new Map<string, number>();\n private rightOffsets = new Map<string, number>();\n // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.leftOffsets.clear();\n this.rightOffsets.clear();\n this.isApplied = false;\n }\n // #endregion\n\n // #region Detection\n\n /**\n * Auto-detect sticky columns from column configuration.\n */\n static detect(rows: readonly unknown[], config: { columns?: ColumnConfig[] }): boolean {\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasStickyColumns(columns);\n }\n // #endregion\n\n // #region Hooks\n\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n // Mark that we have sticky columns to apply\n this.isApplied = hasStickyColumns([...columns]);\n return [...columns];\n }\n\n override afterRender(): void {\n if (!this.isApplied) {\n return;\n }\n\n const host = this.grid as unknown as HTMLElement;\n const columns = [...this.columns];\n\n if (!hasStickyColumns(columns)) {\n clearStickyOffsets(host);\n this.isApplied = false;\n return;\n }\n\n // Apply sticky offsets after a microtask to ensure DOM is ready\n queueMicrotask(() => {\n applyStickyOffsets(host, columns);\n });\n }\n\n /**\n * Handle inter-plugin queries.\n */\n override onPluginQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case PLUGIN_QUERIES.CAN_MOVE_COLUMN: {\n // Prevent pinned columns from being moved/reordered.\n // Pinned columns have fixed positions and should not be draggable.\n const column = query.context as ColumnConfig;\n const sticky = (column as ColumnConfig & { sticky?: 'left' | 'right' }).sticky;\n if (sticky === 'left' || sticky === 'right') {\n return false;\n }\n // Also check meta.sticky for backwards compatibility\n const metaSticky = (column.meta as { sticky?: 'left' | 'right' } | undefined)?.sticky;\n if (metaSticky === 'left' || metaSticky === 'right') {\n return false;\n }\n return undefined; // Let other plugins or default behavior decide\n }\n default:\n return undefined;\n }\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Re-apply sticky offsets (e.g., after column resize).\n */\n refreshStickyOffsets(): void {\n const columns = [...this.columns];\n applyStickyOffsets(this.grid as unknown as HTMLElement, columns);\n }\n\n /**\n * Get columns pinned to the left.\n */\n getLeftPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n return getLeftStickyColumns(columns);\n }\n\n /**\n * Get columns pinned to the right.\n */\n getRightPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n return getRightStickyColumns(columns);\n }\n\n /**\n * Clear all sticky positioning.\n */\n clearStickyPositions(): void {\n clearStickyOffsets(this.grid as unknown as HTMLElement);\n }\n\n /**\n * Report horizontal scroll boundary offsets for pinned columns.\n * Used by keyboard navigation to ensure focused cells aren't hidden behind sticky columns.\n */\n override getHorizontalScrollOffsets(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined {\n if (!this.isApplied) {\n return undefined;\n }\n\n let left = 0;\n let right = 0;\n\n if (rowEl) {\n // Calculate from rendered cells in the row\n const stickyLeftCells = rowEl.querySelectorAll('.sticky-left');\n const stickyRightCells = rowEl.querySelectorAll('.sticky-right');\n stickyLeftCells.forEach((el) => {\n left += (el as HTMLElement).offsetWidth;\n });\n stickyRightCells.forEach((el) => {\n right += (el as HTMLElement).offsetWidth;\n });\n } else {\n // Fall back to header row if no row element provided\n const host = this.grid as unknown as HTMLElement;\n const shadowRoot = host.shadowRoot;\n if (shadowRoot) {\n const headerCells = shadowRoot.querySelectorAll('.header-row .cell');\n headerCells.forEach((cell) => {\n if (cell.classList.contains('sticky-left')) {\n left += (cell as HTMLElement).offsetWidth;\n } else if (cell.classList.contains('sticky-right')) {\n right += (cell as HTMLElement).offsetWidth;\n }\n });\n }\n }\n\n // Skip horizontal scrolling if focused cell is pinned (it's always visible)\n const skipScroll =\n focusedCell?.classList.contains('sticky-left') || focusedCell?.classList.contains('sticky-right');\n\n return { left, right, skipScroll };\n }\n // #endregion\n}\n"],"names":["getLeftStickyColumns","columns","col","getRightStickyColumns","hasStickyColumns","applyStickyOffsets","host","shadowRoot","headerCells","fieldToIndex","i","left","colIndex","cell","c","el","right","clearStickyOffsets","PinnedColumnsPlugin","BaseGridPlugin","rows","config","query","PLUGIN_QUERIES","column","sticky","metaSticky","rowEl","focusedCell","stickyLeftCells","stickyRightCells","skipScroll"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pinned-columns.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/pinned-columns/pinned-columns.ts","../../../../../libs/grid/src/lib/plugins/pinned-columns/PinnedColumnsPlugin.ts"],"sourcesContent":["/**\n * Sticky Columns Core Logic\n *\n * Pure functions for applying sticky (pinned) column positioning.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { StickyPosition } from './types';\n\n/**\n * Get columns that should be sticky on the left.\n *\n * @param columns - Array of column configurations\n * @returns Array of columns with sticky='left'\n */\nexport function getLeftStickyColumns(columns: any[]): any[] {\n return columns.filter((col) => col.sticky === 'left');\n}\n\n/**\n * Get columns that should be sticky on the right.\n *\n * @param columns - Array of column configurations\n * @returns Array of columns with sticky='right'\n */\nexport function getRightStickyColumns(columns: any[]): any[] {\n return columns.filter((col) => col.sticky === 'right');\n}\n\n/**\n * Check if any columns have sticky positioning.\n *\n * @param columns - Array of column configurations\n * @returns True if any column has sticky position\n */\nexport function hasStickyColumns(columns: any[]): boolean {\n return columns.some((col) => col.sticky === 'left' || col.sticky === 'right');\n}\n\n/**\n * Get the sticky position of a column.\n *\n * @param column - Column configuration\n * @returns The sticky position or null if not sticky\n */\nexport function getColumnStickyPosition(column: any): StickyPosition | null {\n if (column.sticky === 'left') return 'left';\n if (column.sticky === 'right') return 'right';\n return null;\n}\n\n/**\n * Calculate left offsets for sticky-left columns.\n * Returns a map of field -> offset in pixels.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @returns Map of field to left offset\n */\nexport function calculateLeftStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n for (const col of columns) {\n if (col.sticky === 'left') {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Calculate right offsets for sticky-right columns.\n * Processes columns in reverse order.\n *\n * @param columns - Array of column configurations (in order)\n * @param getColumnWidth - Function to get column width by field\n * @returns Map of field to right offset\n */\nexport function calculateRightStickyOffsets(\n columns: any[],\n getColumnWidth: (field: string) => number,\n): Map<string, number> {\n const offsets = new Map<string, number>();\n let currentOffset = 0;\n\n // Process in reverse for right-sticky columns\n const reversed = [...columns].reverse();\n for (const col of reversed) {\n if (col.sticky === 'right') {\n offsets.set(col.field, currentOffset);\n currentOffset += getColumnWidth(col.field);\n }\n }\n\n return offsets;\n}\n\n/**\n * Apply sticky offsets to header and body cells.\n * This modifies the DOM elements in place.\n *\n * @param host - The grid host element\n * @param columns - Array of column configurations\n */\nexport function applyStickyOffsets(host: HTMLElement, columns: any[]): void {\n const shadowRoot = host.shadowRoot;\n if (!shadowRoot) return;\n\n const headerCells = Array.from(shadowRoot.querySelectorAll('.header-row .cell')) as HTMLElement[];\n if (!headerCells.length) return;\n\n // Build column index map for matching body cells (which use data-col, not data-field)\n const fieldToIndex = new Map<string, number>();\n columns.forEach((col, i) => {\n if (col.field) fieldToIndex.set(col.field, i);\n });\n\n // Apply left sticky\n let left = 0;\n for (const col of columns) {\n if (col.sticky === 'left') {\n const colIndex = fieldToIndex.get(col.field);\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-left');\n cell.style.position = 'sticky';\n cell.style.left = left + 'px';\n // Body cells use data-col (column index), not data-field\n if (colIndex !== undefined) {\n shadowRoot.querySelectorAll(`.data-grid-row .cell[data-col=\"${colIndex}\"]`).forEach((el) => {\n el.classList.add('sticky-left');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.left = left + 'px';\n });\n }\n left += cell.offsetWidth;\n }\n }\n }\n\n // Apply right sticky (process in reverse)\n let right = 0;\n for (const col of [...columns].reverse()) {\n if (col.sticky === 'right') {\n const colIndex = fieldToIndex.get(col.field);\n const cell = headerCells.find((c) => c.getAttribute('data-field') === col.field);\n if (cell) {\n cell.classList.add('sticky-right');\n cell.style.position = 'sticky';\n cell.style.right = right + 'px';\n // Body cells use data-col (column index), not data-field\n if (colIndex !== undefined) {\n shadowRoot.querySelectorAll(`.data-grid-row .cell[data-col=\"${colIndex}\"]`).forEach((el) => {\n el.classList.add('sticky-right');\n (el as HTMLElement).style.position = 'sticky';\n (el as HTMLElement).style.right = right + 'px';\n });\n }\n right += cell.offsetWidth;\n }\n }\n }\n}\n\n/**\n * Clear sticky positioning from all cells.\n *\n * @param host - The grid host element\n */\nexport function clearStickyOffsets(host: HTMLElement): void {\n const shadowRoot = host.shadowRoot;\n if (!shadowRoot) return;\n\n const cells = shadowRoot.querySelectorAll('.sticky-left, .sticky-right');\n cells.forEach((cell) => {\n cell.classList.remove('sticky-left', 'sticky-right');\n (cell as HTMLElement).style.position = '';\n (cell as HTMLElement).style.left = '';\n (cell as HTMLElement).style.right = '';\n });\n}\n","/**\n * Pinned Columns Plugin (Class-based)\n *\n * Enables column pinning (sticky left/right positioning).\n */\n\nimport { BaseGridPlugin, PLUGIN_QUERIES, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport {\n applyStickyOffsets,\n clearStickyOffsets,\n getLeftStickyColumns,\n getRightStickyColumns,\n hasStickyColumns,\n} from './pinned-columns';\nimport type { PinnedColumnsConfig } from './types';\n\n/**\n * Pinned Columns Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new PinnedColumnsPlugin({ enabled: true })\n * ```\n */\nexport class PinnedColumnsPlugin extends BaseGridPlugin<PinnedColumnsConfig> {\n readonly name = 'pinnedColumns';\n\n protected override get defaultConfig(): Partial<PinnedColumnsConfig> {\n return {};\n }\n\n // #region Internal State\n private isApplied = false;\n private leftOffsets = new Map<string, number>();\n private rightOffsets = new Map<string, number>();\n // #endregion\n\n // #region Lifecycle\n\n override detach(): void {\n this.leftOffsets.clear();\n this.rightOffsets.clear();\n this.isApplied = false;\n }\n // #endregion\n\n // #region Detection\n\n /**\n * Auto-detect sticky columns from column configuration.\n */\n static detect(rows: readonly unknown[], config: { columns?: ColumnConfig[] }): boolean {\n const columns = config?.columns;\n if (!Array.isArray(columns)) return false;\n return hasStickyColumns(columns);\n }\n // #endregion\n\n // #region Hooks\n\n override processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n // Mark that we have sticky columns to apply\n this.isApplied = hasStickyColumns([...columns]);\n return [...columns];\n }\n\n override afterRender(): void {\n if (!this.isApplied) {\n return;\n }\n\n const host = this.grid as unknown as HTMLElement;\n const columns = [...this.columns];\n\n if (!hasStickyColumns(columns)) {\n clearStickyOffsets(host);\n this.isApplied = false;\n return;\n }\n\n // Apply sticky offsets after a microtask to ensure DOM is ready\n queueMicrotask(() => {\n applyStickyOffsets(host, columns);\n });\n }\n\n /**\n * Handle inter-plugin queries.\n */\n override onPluginQuery(query: PluginQuery): unknown {\n switch (query.type) {\n case PLUGIN_QUERIES.CAN_MOVE_COLUMN: {\n // Prevent pinned columns from being moved/reordered.\n // Pinned columns have fixed positions and should not be draggable.\n const column = query.context as ColumnConfig;\n const sticky = (column as ColumnConfig & { sticky?: 'left' | 'right' }).sticky;\n if (sticky === 'left' || sticky === 'right') {\n return false;\n }\n // Also check meta.sticky for backwards compatibility\n const metaSticky = (column.meta as { sticky?: 'left' | 'right' } | undefined)?.sticky;\n if (metaSticky === 'left' || metaSticky === 'right') {\n return false;\n }\n return undefined; // Let other plugins or default behavior decide\n }\n default:\n return undefined;\n }\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Re-apply sticky offsets (e.g., after column resize).\n */\n refreshStickyOffsets(): void {\n const columns = [...this.columns];\n applyStickyOffsets(this.grid as unknown as HTMLElement, columns);\n }\n\n /**\n * Get columns pinned to the left.\n */\n getLeftPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n return getLeftStickyColumns(columns);\n }\n\n /**\n * Get columns pinned to the right.\n */\n getRightPinnedColumns(): ColumnConfig[] {\n const columns = [...this.columns];\n return getRightStickyColumns(columns);\n }\n\n /**\n * Clear all sticky positioning.\n */\n clearStickyPositions(): void {\n clearStickyOffsets(this.grid as unknown as HTMLElement);\n }\n\n /**\n * Report horizontal scroll boundary offsets for pinned columns.\n * Used by keyboard navigation to ensure focused cells aren't hidden behind sticky columns.\n */\n override getHorizontalScrollOffsets(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined {\n if (!this.isApplied) {\n return undefined;\n }\n\n let left = 0;\n let right = 0;\n\n if (rowEl) {\n // Calculate from rendered cells in the row\n const stickyLeftCells = rowEl.querySelectorAll('.sticky-left');\n const stickyRightCells = rowEl.querySelectorAll('.sticky-right');\n stickyLeftCells.forEach((el) => {\n left += (el as HTMLElement).offsetWidth;\n });\n stickyRightCells.forEach((el) => {\n right += (el as HTMLElement).offsetWidth;\n });\n } else {\n // Fall back to header row if no row element provided\n const host = this.grid as unknown as HTMLElement;\n const shadowRoot = host.shadowRoot;\n if (shadowRoot) {\n const headerCells = shadowRoot.querySelectorAll('.header-row .cell');\n headerCells.forEach((cell) => {\n if (cell.classList.contains('sticky-left')) {\n left += (cell as HTMLElement).offsetWidth;\n } else if (cell.classList.contains('sticky-right')) {\n right += (cell as HTMLElement).offsetWidth;\n }\n });\n }\n }\n\n // Skip horizontal scrolling if focused cell is pinned (it's always visible)\n const skipScroll =\n focusedCell?.classList.contains('sticky-left') || focusedCell?.classList.contains('sticky-right');\n\n return { left, right, skipScroll };\n }\n // #endregion\n}\n"],"names":["getLeftStickyColumns","columns","col","getRightStickyColumns","hasStickyColumns","applyStickyOffsets","host","shadowRoot","headerCells","fieldToIndex","i","left","colIndex","cell","c","el","right","clearStickyOffsets","PinnedColumnsPlugin","BaseGridPlugin","rows","config","query","PLUGIN_QUERIES","column","sticky","metaSticky","rowEl","focusedCell","stickyLeftCells","stickyRightCells","skipScroll"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(f,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("../../core/plugin/base-plugin"),require("../../core/internal/aggregators")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin","../../core/internal/aggregators"],p):(f=typeof globalThis<"u"?globalThis:f||self,p(f.TbwGridPlugin_pinnedRows={},f.TbwGrid,f.TbwGrid))})(this,(function(f,p,w){"use strict";function v(o){return typeof o=="object"&&o!==null&&"aggFunc"in o}function u(o,e){const n=document.createElement("div");n.className="tbw-pinned-rows",n.setAttribute("role","presentation"),n.setAttribute("aria-live","polite");const r=document.createElement("div");r.className="tbw-pinned-rows-left";const s=document.createElement("div");s.className="tbw-pinned-rows-center";const i=document.createElement("div");if(i.className="tbw-pinned-rows-right",o.showRowCount!==!1){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-row-count",t.textContent=`Total: ${e.totalRows} rows`,r.appendChild(t)}if(o.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}`,r.appendChild(t)}if(o.showSelectedCount&&e.selectedRows>0){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-selected-count",t.textContent=`Selected: ${e.selectedRows}`,i.appendChild(t)}if(o.customPanels)for(const t of o.customPanels){const a=A(t,e);switch(t.position){case"left":r.appendChild(a);break;case"center":s.appendChild(a);break;case"right":i.appendChild(a);break}}return n.appendChild(r),n.appendChild(s),n.appendChild(i),n}function m(o){const e=document.createElement("div");return e.className=`tbw-aggregation-rows tbw-aggregation-rows-${o}`,e.setAttribute("role","presentation"),e}function b(o,e,n,r){o.innerHTML="";for(const s of e){const i=document.createElement("div");if(i.className="tbw-aggregation-row",i.setAttribute("role","presentation"),s.id&&i.setAttribute("data-aggregation-id",s.id),s.fullWidth){const t=document.createElement("div");t.className="tbw-aggregation-cell tbw-aggregation-cell-full",t.style.gridColumn="1 / -1",t.textContent=s.label||"",i.appendChild(t)}else for(const t of n){const a=document.createElement("div");a.className="tbw-aggregation-cell",a.setAttribute("data-field",t.field);let l,h;const d=s.aggregators?.[t.field];if(d)if(v(d)){const g=w.getAggregator(d.aggFunc);g&&(l=g(r,t.field,t)),h=d.formatter}else{const g=w.getAggregator(d);g&&(l=g(r,t.field,t))}else if(s.cells&&Object.prototype.hasOwnProperty.call(s.cells,t.field)){const g=s.cells[t.field];typeof g=="function"?l=g(r,t.field,t):l=g}l!=null?a.textContent=h?h(l,t.field,t):String(l):a.textContent="",i.appendChild(a)}o.appendChild(i)}}function A(o,e){const n=document.createElement("div");n.className="tbw-status-panel tbw-status-panel-custom",n.id=`status-panel-${o.id}`;const r=o.render(e);return typeof r=="string"?n.innerHTML=r:n.appendChild(r),n}function C(o,e,n,r,s){return{totalRows:o.length,filteredRows:s?.cachedResult?.length??o.length,selectedRows:r?.selected?.size??0,columns:e,rows:o,grid:n}}const E=".tbw-footer{flex-shrink:0;z-index:var(--tbw-z-layer-pinned-rows, 20);background:var(--tbw-color-panel-bg)}.tbw-pinned-rows{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;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:12px;color:var(--tbw-pinned-rows-color, var(--tbw-color-fg-muted));min-height:32px;box-sizing:border-box;min-width:fit-content}.tbw-pinned-rows-left,.tbw-pinned-rows-center,.tbw-pinned-rows-right{display:flex;align-items:center;gap:16px}.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)}.tbw-aggregation-cell{padding:var(--tbw-cell-padding, 2px 8px);min-height:var(--tbw-row-height, 28px);display:flex;align-items:center;border-right:1px solid var(--tbw-color-border-cell)}.tbw-aggregation-cell:last-child{border-right:0}.tbw-aggregation-cell-full{grid-column:1 / -1;border-right:0}";class R extends p.BaseGridPlugin{name="pinnedRows";
|
|
1
|
+
(function(f,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("../../core/plugin/base-plugin"),require("../../core/internal/aggregators")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin","../../core/internal/aggregators"],p):(f=typeof globalThis<"u"?globalThis:f||self,p(f.TbwGridPlugin_pinnedRows={},f.TbwGrid,f.TbwGrid))})(this,(function(f,p,w){"use strict";function v(o){return typeof o=="object"&&o!==null&&"aggFunc"in o}function u(o,e){const n=document.createElement("div");n.className="tbw-pinned-rows",n.setAttribute("role","presentation"),n.setAttribute("aria-live","polite");const r=document.createElement("div");r.className="tbw-pinned-rows-left";const s=document.createElement("div");s.className="tbw-pinned-rows-center";const i=document.createElement("div");if(i.className="tbw-pinned-rows-right",o.showRowCount!==!1){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-row-count",t.textContent=`Total: ${e.totalRows} rows`,r.appendChild(t)}if(o.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}`,r.appendChild(t)}if(o.showSelectedCount&&e.selectedRows>0){const t=document.createElement("span");t.className="tbw-status-panel tbw-status-panel-selected-count",t.textContent=`Selected: ${e.selectedRows}`,i.appendChild(t)}if(o.customPanels)for(const t of o.customPanels){const a=A(t,e);switch(t.position){case"left":r.appendChild(a);break;case"center":s.appendChild(a);break;case"right":i.appendChild(a);break}}return n.appendChild(r),n.appendChild(s),n.appendChild(i),n}function m(o){const e=document.createElement("div");return e.className=`tbw-aggregation-rows tbw-aggregation-rows-${o}`,e.setAttribute("role","presentation"),e}function b(o,e,n,r){o.innerHTML="";for(const s of e){const i=document.createElement("div");if(i.className="tbw-aggregation-row",i.setAttribute("role","presentation"),s.id&&i.setAttribute("data-aggregation-id",s.id),s.fullWidth){const t=document.createElement("div");t.className="tbw-aggregation-cell tbw-aggregation-cell-full",t.style.gridColumn="1 / -1",t.textContent=s.label||"",i.appendChild(t)}else for(const t of n){const a=document.createElement("div");a.className="tbw-aggregation-cell",a.setAttribute("data-field",t.field);let l,h;const d=s.aggregators?.[t.field];if(d)if(v(d)){const g=w.getAggregator(d.aggFunc);g&&(l=g(r,t.field,t)),h=d.formatter}else{const g=w.getAggregator(d);g&&(l=g(r,t.field,t))}else if(s.cells&&Object.prototype.hasOwnProperty.call(s.cells,t.field)){const g=s.cells[t.field];typeof g=="function"?l=g(r,t.field,t):l=g}l!=null?a.textContent=h?h(l,t.field,t):String(l):a.textContent="",i.appendChild(a)}o.appendChild(i)}}function A(o,e){const n=document.createElement("div");n.className="tbw-status-panel tbw-status-panel-custom",n.id=`status-panel-${o.id}`;const r=o.render(e);return typeof r=="string"?n.innerHTML=r:n.appendChild(r),n}function C(o,e,n,r,s){return{totalRows:o.length,filteredRows:s?.cachedResult?.length??o.length,selectedRows:r?.selected?.size??0,columns:e,rows:o,grid:n}}const E=".tbw-footer{flex-shrink:0;z-index:var(--tbw-z-layer-pinned-rows, 20);background:var(--tbw-color-panel-bg)}.tbw-pinned-rows{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;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:12px;color:var(--tbw-pinned-rows-color, var(--tbw-color-fg-muted));min-height:32px;box-sizing:border-box;min-width:fit-content}.tbw-pinned-rows-left,.tbw-pinned-rows-center,.tbw-pinned-rows-right{display:flex;align-items:center;gap:16px}.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)}.tbw-aggregation-cell{padding:var(--tbw-cell-padding, 2px 8px);min-height:var(--tbw-row-height, 28px);display:flex;align-items:center;border-right:1px solid var(--tbw-color-border-cell)}.tbw-aggregation-cell:last-child{border-right:0}.tbw-aggregation-cell-full{grid-column:1 / -1;border-right:0}";class R extends p.BaseGridPlugin{name="pinnedRows";styles=E;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 e=this.shadowRoot;if(!e)return;const n=e.querySelector(".tbw-scroll-area")??e.querySelector(".tbw-grid-content")??e.children[0];if(!n)return;this.footerWrapper&&!n.contains(this.footerWrapper)&&(this.footerWrapper=null,this.bottomAggregationContainer=null,this.infoBarElement=null),this.topAggregationContainer&&!n.contains(this.topAggregationContainer)&&(this.topAggregationContainer=null),this.infoBarElement&&!n.contains(this.infoBarElement)&&(this.infoBarElement=null);const r=this.getSelectionState(),s=this.getFilterState(),i=C(this.rows,this.columns,this.grid,r,s),t=this.config.aggregationRows||[],a=t.filter(c=>c.position==="top"),l=t.filter(c=>c.position!=="top");if(a.length>0){if(!this.topAggregationContainer){this.topAggregationContainer=m("top");const c=e.querySelector(".header");c&&c.nextSibling?n.insertBefore(this.topAggregationContainer,c.nextSibling):n.appendChild(this.topAggregationContainer)}b(this.topAggregationContainer,a,this.visibleColumns,this.rows)}else this.topAggregationContainer&&(this.topAggregationContainer.remove(),this.topAggregationContainer=null);const h=this.config.showRowCount!==!1||this.config.showSelectedCount&&i.selectedRows>0||this.config.showFilteredCount&&i.filteredRows!==i.totalRows||this.config.customPanels&&this.config.customPanels.length>0,d=h&&this.config.position!=="top",g=l.length>0||d;if(h&&this.config.position==="top")if(!this.infoBarElement)this.infoBarElement=u(this.config,i),n.insertBefore(this.infoBarElement,n.firstChild);else{const c=u(this.config,i);this.infoBarElement.replaceWith(c),this.infoBarElement=c}else this.config.position==="top"&&this.infoBarElement&&(this.infoBarElement.remove(),this.infoBarElement=null);g?(this.footerWrapper||(this.footerWrapper=document.createElement("div"),this.footerWrapper.className="tbw-footer",n.appendChild(this.footerWrapper)),this.footerWrapper.innerHTML="",l.length>0&&(this.bottomAggregationContainer||(this.bottomAggregationContainer=m("bottom")),this.footerWrapper.appendChild(this.bottomAggregationContainer),b(this.bottomAggregationContainer,l,this.visibleColumns,this.rows)),d&&(this.infoBarElement=u(this.config,i),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&&this.config.position!=="top"&&(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 e=this.getSelectionState(),n=this.getFilterState();return C(this.rows,this.columns,this.grid,e,n)}addPanel(e){this.config.customPanels||(this.config.customPanels=[]),this.config.customPanels.push(e),this.requestRender()}removePanel(e){this.config.customPanels&&(this.config.customPanels=this.config.customPanels.filter(n=>n.id!==e),this.requestRender())}addAggregationRow(e){this.config.aggregationRows||(this.config.aggregationRows=[]),this.config.aggregationRows.push(e),this.requestRender()}removeAggregationRow(e){this.config.aggregationRows&&(this.config.aggregationRows=this.config.aggregationRows.filter(n=>n.id!==e),this.requestRender())}}f.PinnedRowsPlugin=R,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
|
|
2
2
|
//# sourceMappingURL=pinned-rows.umd.js.map
|