@toolbox-web/grid 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all.d.ts +50 -6
- package/all.js +101 -98
- package/all.js.map +1 -1
- package/index.d.ts +54 -0
- package/index.js +793 -692
- package/index.js.map +1 -1
- package/lib/plugins/clipboard/index.js +55 -35
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js +49 -29
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js +35 -15
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/export/index.js +52 -32
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/index.js +116 -99
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +42 -22
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/index.js +20 -0
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/master-detail/index.js +50 -27
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/multi-sort/index.js +25 -5
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js +20 -0
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js +20 -0
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/index.js +20 -0
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/reorder/index.js +56 -33
- package/lib/plugins/reorder/index.js.map +1 -1
- package/lib/plugins/selection/index.js +138 -100
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js +20 -0
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/tree/index.js +76 -53
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/undo-redo/index.js +20 -0
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/index.js +20 -0
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +4 -1
- package/themes/dg-theme-contrast.css +43 -43
- package/themes/dg-theme-large.css +54 -54
- package/themes/dg-theme-standard.css +19 -19
- package/themes/dg-theme-vibrant.css +16 -16
- package/umd/grid.all.umd.js +24 -24
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +14 -14
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +3 -3
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +2 -2
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.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 +2 -2
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +2 -2
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/visibility.umd.js.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(g,C){typeof exports=="object"&&typeof module<"u"?C(exports,require("../../core/internal/virtualization"),require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/internal/virtualization","../../core/plugin/base-plugin"],C):(g=typeof globalThis<"u"?globalThis:g||self,C(g.TbwGridPlugin_filtering={},g.TbwGrid,g.TbwGrid))})(this,(function(g,C,M){"use strict";function V(w,e,r=!1){const t=w[e.field];if(e.operator==="blank")return t==null||t==="";if(e.operator==="notBlank")return t!=null&&t!=="";if(t==null)return!1;const l=String(t),o=r?l:l.toLowerCase(),i=r?String(e.value):String(e.value).toLowerCase();switch(e.operator){case"contains":return o.includes(i);case"notContains":return!o.includes(i);case"equals":return o===i;case"notEquals":return o!==i;case"startsWith":return o.startsWith(i);case"endsWith":return o.endsWith(i);case"lessThan":return Number(t)<Number(e.value);case"lessThanOrEqual":return Number(t)<=Number(e.value);case"greaterThan":return Number(t)>Number(e.value);case"greaterThanOrEqual":return Number(t)>=Number(e.value);case"between":return Number(t)>=Number(e.value)&&Number(t)<=Number(e.valueTo);case"in":return Array.isArray(e.value)&&e.value.includes(t);case"notIn":return Array.isArray(e.value)&&!e.value.includes(t);default:return!0}}function q(w,e,r=!1){return e.length?w.filter(t=>e.every(l=>V(t,l,r))):w}function z(w){return JSON.stringify(w.map(e=>({field:e.field,operator:e.operator,value:e.value,valueTo:e.valueTo})))}function L(w,e){const r=new Set;for(const t of w){const l=t[e];l!=null&&r.add(l)}return[...r].sort((t,l)=>typeof t=="number"&&typeof l=="number"?t-l:String(t).localeCompare(String(l)))}const B=`
|
|
2
2
|
.tbw-filter-panel {
|
|
3
3
|
position: fixed;
|
|
4
4
|
background: var(--tbw-filter-panel-bg, var(--tbw-color-panel-bg, light-dark(#eeeeee, #222222)));
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
.tbw-filter-clear-btn:hover {
|
|
140
140
|
background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));
|
|
141
141
|
}
|
|
142
|
-
`;class
|
|
142
|
+
`;class m extends M.BaseGridPlugin{name="filtering";version="1.0.0";get defaultConfig(){return{enabled:!0,debounceMs:300,caseSensitive:!1,trimInput:!0,useWorker:!0}}filters=new Map;cachedResult=null;cacheKey=null;openPanelField=null;panelElement=null;searchText=new Map;excludedValues=new Map;panelAbortController=null;globalStylesInjected=!1;static LIST_ITEM_HEIGHT=28;static LIST_OVERSCAN=3;static LIST_BYPASS_THRESHOLD=50;attach(e){super.attach(e),this.injectGlobalStyles()}detach(){this.filters.clear(),this.cachedResult=null,this.cacheKey=null,this.openPanelField=null,this.panelElement&&(this.panelElement.remove(),this.panelElement=null),this.searchText.clear(),this.excludedValues.clear(),this.panelAbortController?.abort(),this.panelAbortController=null}processRows(e){const r=[...this.filters.values()];if(!r.length)return[...e];const t=z(r);if(this.cacheKey===t&&this.cachedResult)return this.cachedResult;const l=q([...e],r,this.config.caseSensitive);return this.cachedResult=l,this.cacheKey=t,l}afterRender(){if(!this.config.enabled)return;const e=this.shadowRoot;if(!e)return;e.querySelectorAll('[part~="header-cell"]').forEach(t=>{const l=t.getAttribute("data-col");if(l===null)return;const o=this.columns[parseInt(l,10)];if(!o||o.filterable===!1||t.querySelector(".tbw-filter-btn"))return;const i=o.field;if(!i)return;const a=document.createElement("button");a.className="tbw-filter-btn",a.setAttribute("aria-label",`Filter ${o.header??i}`),a.innerHTML='<svg viewBox="0 0 16 16" width="12" height="12"><path fill="currentColor" d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/></svg>',this.filters.has(i)&&(a.classList.add("active"),t.classList.add("filtered")),a.addEventListener("click",v=>{v.stopPropagation(),this.toggleFilterPanel(i,o,a)}),t.appendChild(a)})}setFilter(e,r){r===null?this.filters.delete(e):this.filters.set(e,{...r,field:e}),this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[...this.filters.values()],filteredRowCount:0}),this.requestRender()}getFilter(e){return this.filters.get(e)}getFilters(){return[...this.filters.values()]}getFilterModel(){return this.getFilters()}setFilterModel(e){this.filters.clear();for(const r of e)this.filters.set(r.field,r);this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[...this.filters.values()],filteredRowCount:0}),this.requestRender()}clearAllFilters(){this.filters.clear(),this.excludedValues.clear(),this.searchText.clear(),this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[],filteredRowCount:this.rows.length}),this.requestRender()}clearFieldFilter(e){this.filters.delete(e),this.excludedValues.delete(e),this.searchText.delete(e),this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[...this.filters.values()],filteredRowCount:0}),this.requestRender()}isFieldFiltered(e){return this.filters.has(e)}getFilteredRowCount(){return this.cachedResult?.length??this.rows.length}getActiveFilters(){return this.getFilters()}getUniqueValues(e){return L(this.sourceRows,e)}injectGlobalStyles(){if(this.globalStylesInjected)return;if(document.getElementById("tbw-filter-panel-styles")){this.globalStylesInjected=!0;return}const e=document.createElement("style");e.id="tbw-filter-panel-styles",e.textContent=B,document.head.appendChild(e),this.globalStylesInjected=!0}toggleFilterPanel(e,r,t){if(this.openPanelField===e){this.closeFilterPanel();return}this.closeFilterPanel();const l=document.createElement("div");l.className="tbw-filter-panel",this.panelElement=l,this.openPanelField=e;const o=L(this.sourceRows,e);let i=this.excludedValues.get(e);i||(i=new Set,this.excludedValues.set(e,i));const a=this.searchText.get(e)??"",v={field:e,column:r,uniqueValues:o,excludedValues:i,searchText:a,applySetFilter:u=>{this.applySetFilter(e,u),this.closeFilterPanel()},applyTextFilter:(u,E,S)=>{this.applyTextFilter(e,u,E,S),this.closeFilterPanel()},clearFilter:()=>{this.clearFieldFilter(e),this.closeFilterPanel()},closePanel:()=>this.closeFilterPanel()};let f=!1;this.config.filterPanelRenderer&&(this.config.filterPanelRenderer(l,v),f=l.children.length>0),f||this.renderDefaultFilterPanel(l,v,o,i),document.body.appendChild(l),this.positionPanel(l,t),this.panelAbortController=new AbortController,setTimeout(()=>{document.addEventListener("click",u=>{!l.contains(u.target)&&u.target!==t&&this.closeFilterPanel()},{signal:this.panelAbortController?.signal})},0)}closeFilterPanel(){this.panelElement&&(this.panelElement.remove(),this.panelElement=null),this.openPanelField=null,this.panelAbortController?.abort(),this.panelAbortController=null}positionPanel(e,r){const t=r.getBoundingClientRect();e.style.position="fixed",e.style.top=`${t.bottom+4}px`,e.style.left=`${t.left}px`,requestAnimationFrame(()=>{const l=e.getBoundingClientRect();l.right>window.innerWidth-8&&(e.style.left=`${window.innerWidth-l.width-8}px`),l.bottom>window.innerHeight-8&&(e.style.top=`${t.top-l.height-4}px`)})}renderDefaultFilterPanel(e,r,t,l){const{field:o}=r,i=document.createElement("div");i.className="tbw-filter-search";const a=document.createElement("input");a.type="text",a.placeholder="Search...",a.className="tbw-filter-search-input",a.value=this.searchText.get(o)??"",i.appendChild(a),e.appendChild(i);const v=document.createElement("div");v.className="tbw-filter-actions";const f=document.createElement("label");f.className="tbw-filter-value-item",f.style.padding="0",f.style.margin="0";const u=document.createElement("input");u.type="checkbox",u.className="tbw-filter-checkbox";const E=document.createElement("span");E.textContent="Select All",f.appendChild(u),f.appendChild(E),v.appendChild(f);const S=()=>{const n=[...y.values()],d=n.every(c=>c),h=n.every(c=>!c);u.checked=d,u.indeterminate=!d&&!h};u.addEventListener("change",()=>{const n=u.checked;for(const d of y.keys())y.set(d,n);S(),N()}),e.appendChild(v);const x=document.createElement("div");x.className="tbw-filter-values";const k=document.createElement("div");k.className="tbw-filter-values-spacer",x.appendChild(k);const p=document.createElement("div");p.className="tbw-filter-values-content",x.appendChild(p);const y=new Map;t.forEach(n=>{const d=n==null?"__null__":String(n);y.set(d,!l.has(n))}),S();let T=[];const A=(n,d)=>{const h=n==null?"(Blank)":String(n),c=n==null?"__null__":String(n),s=document.createElement("label");s.className="tbw-filter-value-item",s.style.position="absolute",s.style.top=`${d*m.LIST_ITEM_HEIGHT}px`,s.style.left="0",s.style.right="0",s.style.height=`${m.LIST_ITEM_HEIGHT}px`,s.style.boxSizing="border-box";const b=document.createElement("input");b.type="checkbox",b.className="tbw-filter-checkbox",b.checked=y.get(c)??!0,b.dataset.value=c,b.addEventListener("change",()=>{y.set(c,b.checked),S()});const H=document.createElement("span");return H.textContent=h,s.appendChild(b),s.appendChild(H),s},N=()=>{const n=T.length,d=x.clientHeight,h=x.scrollTop;if(k.style.height=`${n*m.LIST_ITEM_HEIGHT}px`,C.shouldBypassVirtualization(n,m.LIST_BYPASS_THRESHOLD/3)){p.innerHTML="",p.style.transform="translateY(0px)",T.forEach((s,b)=>{p.appendChild(A(s,b))});return}const c=C.computeVirtualWindow({totalRows:n,viewportHeight:d,scrollTop:h,rowHeight:m.LIST_ITEM_HEIGHT,overscan:m.LIST_OVERSCAN});p.style.transform=`translateY(${c.offsetY}px)`,p.innerHTML="";for(let s=c.start;s<c.end;s++)p.appendChild(A(T[s],s-c.start))},P=n=>{const d=n.toLowerCase();if(T=t.filter(h=>{const c=h==null?"(Blank)":String(h);return!n||c.toLowerCase().includes(d)}),T.length===0){k.style.height="0px",p.innerHTML="";const h=document.createElement("div");h.className="tbw-filter-no-match",h.textContent="No matching values",p.appendChild(h);return}N()};x.addEventListener("scroll",()=>{T.length>0&&N()},{passive:!0}),P(a.value),e.appendChild(x);let _;a.addEventListener("input",()=>{clearTimeout(_),_=setTimeout(()=>{this.searchText.set(o,a.value),P(a.value)},this.config.debounceMs??150)});const R=document.createElement("div");R.className="tbw-filter-buttons";const F=document.createElement("button");F.className="tbw-filter-apply-btn",F.textContent="Apply",F.addEventListener("click",()=>{const n=[];for(const[d,h]of y)if(!h)if(d==="__null__")n.push(null);else{const c=t.find(s=>String(s)===d);n.push(c!==void 0?c:d)}r.applySetFilter(n)}),R.appendChild(F);const I=document.createElement("button");I.className="tbw-filter-clear-btn",I.textContent="Clear Filter",I.addEventListener("click",()=>{r.clearFilter()}),R.appendChild(I),e.appendChild(R)}applySetFilter(e,r){this.excludedValues.set(e,new Set(r)),r.length===0?this.filters.delete(e):this.filters.set(e,{field:e,type:"set",operator:"notIn",value:r}),this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[...this.filters.values()],filteredRowCount:0}),this.requestRender()}applyTextFilter(e,r,t,l){this.filters.set(e,{field:e,type:"text",operator:r,value:t,valueTo:l}),this.cachedResult=null,this.cacheKey=null,this.emit("filter-change",{filters:[...this.filters.values()],filteredRowCount:0}),this.requestRender()}getColumnState(e){const r=this.filters.get(e);if(r)return{filter:{type:r.type,operator:r.operator,value:r.value,valueTo:r.valueTo}}}applyColumnState(e,r){if(!r.filter){this.filters.delete(e);return}const t={field:e,type:r.filter.type,operator:r.filter.operator,value:r.filter.value,valueTo:r.filter.valueTo};this.filters.set(e,t),this.cachedResult=null,this.cacheKey=null}styles=`
|
|
143
143
|
.header-cell.filtered::before {
|
|
144
144
|
content: '';
|
|
145
145
|
position: absolute;
|
|
@@ -171,5 +171,5 @@
|
|
|
171
171
|
.tbw-filter-btn.active {
|
|
172
172
|
color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
|
|
173
173
|
}
|
|
174
|
-
`}
|
|
174
|
+
`}g.FilteringPlugin=m,Object.defineProperty(g,Symbol.toStringTag,{value:"Module"})}));
|
|
175
175
|
//# sourceMappingURL=filtering.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filtering.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(row: Record<string, unknown>, filter: FilterModel, caseSensitive = false): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const filterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(filterValue);\n\n case 'notContains':\n return !compareValue.includes(filterValue);\n\n case 'equals':\n return compareValue === filterValue;\n\n case 'notEquals':\n return compareValue !== filterValue;\n\n case 'startsWith':\n return compareValue.startsWith(filterValue);\n\n case 'endsWith':\n return compareValue.endsWith(filterValue);\n\n // Number/Date operators (use raw numeric values)\n case 'lessThan':\n return Number(rawValue) < Number(filter.value);\n\n case 'lessThanOrEqual':\n return Number(rawValue) <= Number(filter.value);\n\n case 'greaterThan':\n return Number(rawValue) > Number(filter.value);\n\n case 'greaterThanOrEqual':\n return Number(rawValue) >= Number(filter.value);\n\n case 'between':\n return Number(rawValue) >= Number(filter.value) && Number(rawValue) <= Number(filter.valueTo);\n\n // Set operators\n case 'in':\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n\n case 'notIn':\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) => filters.every((f) => matchesFilter(row, f, caseSensitive)));\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n }))\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(rows: T[], field: string): unknown[] {\n const values = new Set<unknown>();\n for (const row of rows) {\n const value = row[field];\n if (value != null) {\n values.add(value);\n }\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { BaseGridPlugin, type GridElement } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { computeFilterCacheKey, filterRows, getUniqueValues } from './filter-model';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/** Global styles for filter panel (rendered in document.body) */\nconst filterPanelStyles = `\n.tbw-filter-panel {\n position: fixed;\n background: var(--tbw-filter-panel-bg, var(--tbw-color-panel-bg, light-dark(#eeeeee, #222222)));\n color: var(--tbw-filter-panel-fg, var(--tbw-color-fg, light-dark(#222222, #eeeeee)));\n border: 1px solid var(--tbw-filter-panel-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: var(--tbw-filter-panel-radius, var(--tbw-border-radius, 4px));\n box-shadow: 0 4px 16px var(--tbw-filter-panel-shadow, var(--tbw-color-shadow, light-dark(rgba(0,0,0,0.1), rgba(0,0,0,0.3))));\n padding: 12px;\n z-index: 10000;\n min-width: 200px;\n max-width: 280px;\n max-height: 350px;\n display: flex;\n flex-direction: column;\n font-family: var(--tbw-font-family, system-ui, sans-serif);\n font-size: var(--tbw-font-size, 13px);\n}\n\n.tbw-filter-search {\n margin-bottom: 8px;\n}\n\n.tbw-filter-search-input {\n width: 100%;\n padding: 6px 10px;\n background: var(--tbw-filter-input-bg, var(--tbw-color-bg, transparent));\n color: inherit;\n border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: var(--tbw-filter-input-radius, 4px);\n font-size: inherit;\n box-sizing: border-box;\n}\n\n.tbw-filter-search-input:focus {\n outline: none;\n border-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n box-shadow: 0 0 0 2px rgba(from var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6)) r g b / 15%);\n}\n\n.tbw-filter-actions {\n display: flex;\n padding: 4px 2px;\n margin-bottom: 8px;\n border-bottom: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n}\n\n.tbw-filter-action-btn {\n background: transparent;\n border: none;\n color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n cursor: pointer;\n font-size: 12px;\n padding: 2px 0;\n}\n\n.tbw-filter-action-btn:hover {\n text-decoration: underline;\n}\n\n.tbw-filter-values {\n flex: 1;\n overflow-y: auto;\n margin-bottom: 8px;\n max-height: 180px;\n position: relative;\n}\n\n.tbw-filter-values-spacer {\n width: 1px;\n}\n\n.tbw-filter-values-content {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n}\n\n.tbw-filter-value-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 2px;\n cursor: pointer;\n border-radius: 3px;\n}\n\n.tbw-filter-value-item:hover {\n background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));\n}\n\n.tbw-filter-checkbox {\n margin: 0;\n cursor: pointer;\n accent-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n}\n\n.tbw-filter-no-match {\n color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));\n padding: 8px 0;\n text-align: center;\n font-style: italic;\n}\n\n.tbw-filter-buttons {\n display: flex;\n gap: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n}\n\n.tbw-filter-apply-btn {\n flex: 1;\n padding: 6px 12px;\n background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n color: var(--tbw-filter-accent-fg, var(--tbw-color-accent-fg, light-dark(#ffffff, #000000)));\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n\n.tbw-filter-apply-btn:hover {\n filter: brightness(0.9);\n}\n\n.tbw-filter-clear-btn {\n flex: 1;\n padding: 6px 12px;\n background: transparent;\n color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));\n border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n\n.tbw-filter-clear-btn:hover {\n background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));\n}\n`;\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new FilteringPlugin({ enabled: true, debounceMs: 300 })\n * ```\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n readonly name = 'filtering';\n override readonly version = '1.0.0';\n\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n enabled: true,\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // ===== Internal State =====\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private documentClickHandler: ((e: MouseEvent) => void) | null = null;\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n // ===== Lifecycle =====\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n this.removeDocumentClickHandler();\n }\n\n // ===== Hooks =====\n\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // Check cache\n const newCacheKey = computeFilterCacheKey(filterList);\n if (this.cacheKey === newCacheKey && this.cachedResult) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows([...rows] as Record<string, unknown>[], filterList, this.config.caseSensitive);\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n\n return result;\n }\n\n override afterRender(): void {\n if (!this.config.enabled) return;\n\n const shadowRoot = this.shadowRoot;\n if (!shadowRoot) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = shadowRoot.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n const col = this.columns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || col.filterable === false) return;\n\n // Skip if button already exists\n if (cell.querySelector('.tbw-filter-btn')) return;\n\n const field = col.field;\n if (!field) return;\n\n // Create filter button\n const filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n filterBtn.innerHTML = `<svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\"><path fill=\"currentColor\" d=\"M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z\"/></svg>`;\n\n // Mark button as active if filter exists\n if (this.filters.has(field)) {\n filterBtn.classList.add('active');\n cell.classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn);\n });\n\n // Append to header cell\n cell.appendChild(filterBtn);\n });\n }\n\n // ===== Public API =====\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n } else {\n this.filters.set(field, { ...filter, field });\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [],\n filteredRowCount: this.rows.length,\n });\n this.requestRender();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n */\n getUniqueValues(field: string): unknown[] {\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n }\n\n // ===== Private Methods =====\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n this.panelElement = panel;\n this.openPanelField = field;\n\n // Get unique values for this field (from source rows, not filtered)\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n let usedCustomRenderer = false;\n if (this.config.filterPanelRenderer) {\n const result = this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n if (!usedCustomRenderer) {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n\n // Position and append to body\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n // Add global click handler to close on outside click\n const handler = (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n };\n this.documentClickHandler = handler;\n // Defer to next tick to avoid immediate close\n setTimeout(() => {\n document.addEventListener('click', handler);\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.openPanelField = null;\n this.removeDocumentClickHandler();\n }\n\n /**\n * Remove the document click handler\n */\n private removeDocumentClickHandler(): void {\n if (this.documentClickHandler) {\n document.removeEventListener('click', this.documentClickHandler);\n this.documentClickHandler = null;\n }\n }\n\n /**\n * Position the panel below the button\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n const rect = buttonEl.getBoundingClientRect();\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows right edge\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${window.innerWidth - panelRect.width - 8}px`;\n }\n // Adjust if overflows bottom\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>\n ): void {\n const { field } = params;\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const strValue = value == null ? '(Blank)' : String(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `${index * FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.height = `${FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = strValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: FilteringPlugin.LIST_ITEM_HEIGHT,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const lowerFilter = filterText.toLowerCase();\n\n // Filter the unique values\n filteredValues = uniqueValues.filter((value) => {\n const strValue = value == null ? '(Blank)' : String(value);\n return !filterText || strValue.toLowerCase().includes(lowerFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true }\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Apply a text filter\n */\n private applyTextFilter(field: string, operator: FilterModel['operator'], value: string, valueTo?: string): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n // ===== Column State Hooks =====\n\n /**\n * Return filter state for a column if it has an active filter.\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n }\n\n // ===== Styles =====\n\n override readonly styles = `\n .header-cell.filtered::before {\n content: '';\n position: absolute;\n top: 4px;\n right: 4px;\n width: 6px;\n height: 6px;\n background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n border-radius: 50%;\n }\n .tbw-filter-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 2px;\n margin-left: 4px;\n opacity: 0.4;\n transition: opacity 0.15s;\n color: inherit;\n vertical-align: middle;\n }\n .tbw-filter-btn:hover,\n .tbw-filter-btn.active {\n opacity: 1;\n }\n .tbw-filter-btn.active {\n color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n }\n `;\n}\n"],"names":["matchesFilter","row","filter","caseSensitive","rawValue","stringValue","compareValue","filterValue","filterRows","rows","filters","f","computeFilterCacheKey","getUniqueValues","field","values","value","a","b","filterPanelStyles","FilteringPlugin","BaseGridPlugin","grid","filterList","newCacheKey","result","shadowRoot","cell","colIndex","col","filterBtn","e","style","column","buttonEl","panel","uniqueValues","excludedSet","currentSearchText","params","excluded","operator","valueTo","usedCustomRenderer","handler","rect","panelRect","excludedValues","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","v","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","strValue","item","checkbox","label","totalItems","viewportHeight","scrollTop","shouldBypassVirtualization","idx","window","computeVirtualWindow","i","renderValues","filterText","lowerFilter","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterModel","state"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filtering.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(row: Record<string, unknown>, filter: FilterModel, caseSensitive = false): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const filterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(filterValue);\n\n case 'notContains':\n return !compareValue.includes(filterValue);\n\n case 'equals':\n return compareValue === filterValue;\n\n case 'notEquals':\n return compareValue !== filterValue;\n\n case 'startsWith':\n return compareValue.startsWith(filterValue);\n\n case 'endsWith':\n return compareValue.endsWith(filterValue);\n\n // Number/Date operators (use raw numeric values)\n case 'lessThan':\n return Number(rawValue) < Number(filter.value);\n\n case 'lessThanOrEqual':\n return Number(rawValue) <= Number(filter.value);\n\n case 'greaterThan':\n return Number(rawValue) > Number(filter.value);\n\n case 'greaterThanOrEqual':\n return Number(rawValue) >= Number(filter.value);\n\n case 'between':\n return Number(rawValue) >= Number(filter.value) && Number(rawValue) <= Number(filter.valueTo);\n\n // Set operators\n case 'in':\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n\n case 'notIn':\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) => filters.every((f) => matchesFilter(row, f, caseSensitive)));\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n }))\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(rows: T[], field: string): unknown[] {\n const values = new Set<unknown>();\n for (const row of rows) {\n const value = row[field];\n if (value != null) {\n values.add(value);\n }\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { BaseGridPlugin, type GridElement } from '../../core/plugin/base-plugin';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport { computeFilterCacheKey, filterRows, getUniqueValues } from './filter-model';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/** Global styles for filter panel (rendered in document.body) */\nconst filterPanelStyles = `\n.tbw-filter-panel {\n position: fixed;\n background: var(--tbw-filter-panel-bg, var(--tbw-color-panel-bg, light-dark(#eeeeee, #222222)));\n color: var(--tbw-filter-panel-fg, var(--tbw-color-fg, light-dark(#222222, #eeeeee)));\n border: 1px solid var(--tbw-filter-panel-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: var(--tbw-filter-panel-radius, var(--tbw-border-radius, 4px));\n box-shadow: 0 4px 16px var(--tbw-filter-panel-shadow, var(--tbw-color-shadow, light-dark(rgba(0,0,0,0.1), rgba(0,0,0,0.3))));\n padding: 12px;\n z-index: 10000;\n min-width: 200px;\n max-width: 280px;\n max-height: 350px;\n display: flex;\n flex-direction: column;\n font-family: var(--tbw-font-family, system-ui, sans-serif);\n font-size: var(--tbw-font-size, 13px);\n}\n\n.tbw-filter-search {\n margin-bottom: 8px;\n}\n\n.tbw-filter-search-input {\n width: 100%;\n padding: 6px 10px;\n background: var(--tbw-filter-input-bg, var(--tbw-color-bg, transparent));\n color: inherit;\n border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: var(--tbw-filter-input-radius, 4px);\n font-size: inherit;\n box-sizing: border-box;\n}\n\n.tbw-filter-search-input:focus {\n outline: none;\n border-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n box-shadow: 0 0 0 2px rgba(from var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6)) r g b / 15%);\n}\n\n.tbw-filter-actions {\n display: flex;\n padding: 4px 2px;\n margin-bottom: 8px;\n border-bottom: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n}\n\n.tbw-filter-action-btn {\n background: transparent;\n border: none;\n color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n cursor: pointer;\n font-size: 12px;\n padding: 2px 0;\n}\n\n.tbw-filter-action-btn:hover {\n text-decoration: underline;\n}\n\n.tbw-filter-values {\n flex: 1;\n overflow-y: auto;\n margin-bottom: 8px;\n max-height: 180px;\n position: relative;\n}\n\n.tbw-filter-values-spacer {\n width: 1px;\n}\n\n.tbw-filter-values-content {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n}\n\n.tbw-filter-value-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 2px;\n cursor: pointer;\n border-radius: 3px;\n}\n\n.tbw-filter-value-item:hover {\n background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));\n}\n\n.tbw-filter-checkbox {\n margin: 0;\n cursor: pointer;\n accent-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n}\n\n.tbw-filter-no-match {\n color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));\n padding: 8px 0;\n text-align: center;\n font-style: italic;\n}\n\n.tbw-filter-buttons {\n display: flex;\n gap: 8px;\n padding-top: 8px;\n border-top: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n}\n\n.tbw-filter-apply-btn {\n flex: 1;\n padding: 6px 12px;\n background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n color: var(--tbw-filter-accent-fg, var(--tbw-color-accent-fg, light-dark(#ffffff, #000000)));\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n\n.tbw-filter-apply-btn:hover {\n filter: brightness(0.9);\n}\n\n.tbw-filter-clear-btn {\n flex: 1;\n padding: 6px 12px;\n background: transparent;\n color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));\n border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n\n.tbw-filter-clear-btn:hover {\n background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));\n}\n`;\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * @example\n * ```ts\n * new FilteringPlugin({ enabled: true, debounceMs: 300 })\n * ```\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n readonly name = 'filtering';\n override readonly version = '1.0.0';\n\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n enabled: true,\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // ===== Internal State =====\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private panelAbortController: AbortController | null = null; // For panel-scoped listeners\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n // ===== Lifecycle =====\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n // Abort panel-scoped listeners (document click handler, etc.)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n // ===== Hooks =====\n\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // Check cache\n const newCacheKey = computeFilterCacheKey(filterList);\n if (this.cacheKey === newCacheKey && this.cachedResult) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows([...rows] as Record<string, unknown>[], filterList, this.config.caseSensitive);\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n\n return result;\n }\n\n override afterRender(): void {\n if (!this.config.enabled) return;\n\n const shadowRoot = this.shadowRoot;\n if (!shadowRoot) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = shadowRoot.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n const col = this.columns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || col.filterable === false) return;\n\n // Skip if button already exists\n if (cell.querySelector('.tbw-filter-btn')) return;\n\n const field = col.field;\n if (!field) return;\n\n // Create filter button\n const filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n filterBtn.innerHTML = `<svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\"><path fill=\"currentColor\" d=\"M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z\"/></svg>`;\n\n // Mark button as active if filter exists\n if (this.filters.has(field)) {\n filterBtn.classList.add('active');\n cell.classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn);\n });\n\n // Append to header cell\n cell.appendChild(filterBtn);\n });\n }\n\n // ===== Public API =====\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n } else {\n this.filters.set(field, { ...filter, field });\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [],\n filteredRowCount: this.rows.length,\n });\n this.requestRender();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n */\n getUniqueValues(field: string): unknown[] {\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n }\n\n // ===== Private Methods =====\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n this.panelElement = panel;\n this.openPanelField = field;\n\n // Get unique values for this field (from source rows, not filtered)\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field);\n\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n let usedCustomRenderer = false;\n if (this.config.filterPanelRenderer) {\n const result = this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n if (!usedCustomRenderer) {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n\n // Position and append to body\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n // Create abort controller for panel-scoped listeners\n // This allows cleanup when panel closes OR when grid disconnects\n this.panelAbortController = new AbortController();\n\n // Add global click handler to close on outside click\n // Defer to next tick to avoid immediate close from the click that opened the panel\n setTimeout(() => {\n document.addEventListener(\n 'click',\n (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n },\n { signal: this.panelAbortController?.signal }\n );\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.openPanelField = null;\n // Abort panel-scoped listeners (document click handler)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n /**\n * Position the panel below the button\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n const rect = buttonEl.getBoundingClientRect();\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows right edge\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${window.innerWidth - panelRect.width - 8}px`;\n }\n // Adjust if overflows bottom\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>\n ): void {\n const { field } = params;\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const strValue = value == null ? '(Blank)' : String(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `${index * FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.height = `${FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = strValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * FilteringPlugin.LIST_ITEM_HEIGHT}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: FilteringPlugin.LIST_ITEM_HEIGHT,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const lowerFilter = filterText.toLowerCase();\n\n // Filter the unique values\n filteredValues = uniqueValues.filter((value) => {\n const strValue = value == null ? '(Blank)' : String(value);\n return !filterText || strValue.toLowerCase().includes(lowerFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true }\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n /**\n * Apply a text filter\n */\n private applyTextFilter(field: string, operator: FilterModel['operator'], value: string, valueTo?: string): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.cachedResult = null;\n this.cacheKey = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n });\n this.requestRender();\n }\n\n // ===== Column State Hooks =====\n\n /**\n * Return filter state for a column if it has an active filter.\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n }\n\n // ===== Styles =====\n\n override readonly styles = `\n .header-cell.filtered::before {\n content: '';\n position: absolute;\n top: 4px;\n right: 4px;\n width: 6px;\n height: 6px;\n background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n border-radius: 50%;\n }\n .tbw-filter-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: none;\n cursor: pointer;\n padding: 2px;\n margin-left: 4px;\n opacity: 0.4;\n transition: opacity 0.15s;\n color: inherit;\n vertical-align: middle;\n }\n .tbw-filter-btn:hover,\n .tbw-filter-btn.active {\n opacity: 1;\n }\n .tbw-filter-btn.active {\n color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));\n }\n `;\n}\n"],"names":["matchesFilter","row","filter","caseSensitive","rawValue","stringValue","compareValue","filterValue","filterRows","rows","filters","f","computeFilterCacheKey","getUniqueValues","field","values","value","a","b","filterPanelStyles","FilteringPlugin","BaseGridPlugin","grid","filterList","newCacheKey","result","shadowRoot","cell","colIndex","col","filterBtn","e","style","column","buttonEl","panel","uniqueValues","excludedSet","currentSearchText","params","excluded","operator","valueTo","usedCustomRenderer","rect","panelRect","excludedValues","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","v","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","strValue","item","checkbox","label","totalItems","viewportHeight","scrollTop","shouldBypassVirtualization","idx","window","computeVirtualWindow","i","renderValues","filterText","lowerFilter","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterModel","state"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(d,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],p):(d=typeof globalThis<"u"?globalThis:d||self,p(d.TbwGridPlugin_masterDetail={},d.TbwGrid))})(this,(function(d,p){"use strict";function f(
|
|
1
|
+
(function(d,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],p):(d=typeof globalThis<"u"?globalThis:d||self,p(d.TbwGridPlugin_masterDetail={},d.TbwGrid))})(this,(function(d,p){"use strict";function f(r,e){const t=new Set(r);return t.has(e)?t.delete(e):t.add(e),t}function x(r,e){const t=new Set(r);return t.add(e),t}function m(r,e){const t=new Set(r);return t.delete(e),t}function R(r,e){return r.has(e)}function b(r,e,t,o){const n=document.createElement("div");n.className="master-detail-row",n.setAttribute("data-detail-for",String(e)),n.setAttribute("role","row");const s=document.createElement("div");s.className="master-detail-cell",s.setAttribute("role","cell"),s.style.gridColumn=`1 / ${o+1}`;const i=t(r,e);return typeof i=="string"?s.innerHTML=i:i instanceof HTMLElement&&s.appendChild(i),n.appendChild(s),n}class E extends p.BaseGridPlugin{name="masterDetail";version="1.0.0";get defaultConfig(){return{enabled:!0,detailHeight:"auto",expandOnRowClick:!1,collapseOnClickOutside:!1,showExpandColumn:!0}}expandedRows=new Set;detailElements=new Map;detach(){this.expandedRows.clear(),this.detailElements.clear()}processColumns(e){if(!this.config.detailRenderer)return[...e];const t=[...e];if(t.length>0){const o={...t[0]},n=o.viewRenderer;if(n?.__masterDetailWrapped)return t;const s=i=>{const{value:l,row:a}=i,g=this.expandedRows.has(a),u=document.createElement("span");u.className="master-detail-cell-wrapper";const c=document.createElement("span");c.className="master-detail-toggle",c.textContent=g?"▼":"▶",c.setAttribute("aria-expanded",String(g)),c.setAttribute("aria-label",g?"Collapse details":"Expand details"),c.addEventListener("click",w=>{w.stopPropagation();const C=this.rows.indexOf(a);this.expandedRows=f(this.expandedRows,a),this.emit("detail-expand",{rowIndex:C,row:a,expanded:this.expandedRows.has(a)}),this.requestRender()}),u.appendChild(c);const h=document.createElement("span");if(n){const w=n(i);w instanceof Node?h.appendChild(w):h.textContent=String(w??l??"")}else h.textContent=String(l??"");return u.appendChild(h),u};s.__masterDetailWrapped=!0,o.viewRenderer=s,t[0]=o}return t}onRowClick(e){if(!(!this.config.expandOnRowClick||!this.config.detailRenderer))return this.expandedRows=f(this.expandedRows,e.row),this.emit("detail-expand",{rowIndex:e.rowIndex,row:e.row,expanded:this.expandedRows.has(e.row)}),this.requestRender(),!1}afterRender(){if(!this.config.detailRenderer)return;const e=this.shadowRoot?.querySelector(".rows");if(!e)return;e.querySelectorAll(".master-detail-row").forEach(n=>n.remove()),this.detailElements.clear();const t=e.querySelectorAll(".data-grid-row"),o=this.columns.length;for(const n of t){const s=n.querySelector(".cell[data-row]"),i=s?parseInt(s.getAttribute("data-row")??"-1",10):-1;if(i<0)continue;const l=this.rows[i];if(!l||!this.expandedRows.has(l))continue;const a=b(l,i,this.config.detailRenderer,o);typeof this.config.detailHeight=="number"&&(a.style.height=`${this.config.detailHeight}px`),n.appendChild(a),this.detailElements.set(l,a)}}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=m(this.expandedRows,t),this.requestRender())}toggle(e){const t=this.rows[e];t&&(this.expandedRows=f(this.expandedRows,t),this.requestRender())}isExpanded(e){const t=this.rows[e];return t?R(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}styles=`
|
|
2
2
|
.master-detail-cell-wrapper {
|
|
3
3
|
display: flex;
|
|
4
4
|
align-items: center;
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
padding: 16px;
|
|
24
24
|
overflow: auto;
|
|
25
25
|
}
|
|
26
|
-
`}d.MasterDetailPlugin=
|
|
26
|
+
`}d.MasterDetailPlugin=E,Object.defineProperty(d,Symbol.toStringTag,{value:"Module"})}));
|
|
27
27
|
//# 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 */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { BaseGridPlugin, RowClickEvent } from '../../core/plugin/base-plugin';\nimport {\n collapseDetailRow,\n createDetailElement,\n expandDetailRow,\n isDetailExpanded,\n toggleDetailRow,\n} from './master-detail';\nimport type { DetailExpandDetail, 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 version = '1.0.0';\n\n protected override get defaultConfig(): Partial<MasterDetailConfig> {\n return {\n enabled: true,\n detailHeight: 'auto',\n expandOnRowClick: false,\n collapseOnClickOutside: false,\n showExpandColumn: true,\n };\n }\n\n // ===== Internal State =====\n private expandedRows: Set<any> = new Set();\n private detailElements: Map<any, HTMLElement> = new Map();\n\n // ===== Lifecycle =====\n\n override detach(): void {\n this.expandedRows.clear();\n this.detailElements.clear();\n }\n\n // ===== Hooks =====\n\n override processColumns(\n columns: readonly import('../../core/types').ColumnConfig[]\n ): import('../../core/types').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;\n\n firstCol.viewRenderer = (renderCtx) => {\n const { value, row } = renderCtx;\n const isExpanded = this.expandedRows.has(row);\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';\n toggle.textContent = isExpanded ? '▼' : '▶';\n toggle.setAttribute('aria-expanded', String(isExpanded));\n toggle.setAttribute('aria-label', isExpanded ? 'Collapse details' : 'Expand details');\n toggle.addEventListener('click', (e) => {\n e.stopPropagation();\n const rowIndex = this.rows.indexOf(row);\n this.expandedRows = toggleDetailRow(this.expandedRows, row);\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex,\n row,\n expanded: this.expandedRows.has(row),\n });\n this.requestRender();\n });\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 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);\n\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex: event.rowIndex,\n row: event.row,\n expanded: this.expandedRows.has(event.row),\n });\n\n this.requestRender();\n return false;\n }\n\n override afterRender(): void {\n if (!this.config.detailRenderer) return;\n\n const body = this.shadowRoot?.querySelector('.rows');\n if (!body) return;\n\n // Remove old detail rows\n body.querySelectorAll('.master-detail-row').forEach((el) => el.remove());\n this.detailElements.clear();\n\n // Insert detail rows as last child of expanded row elements\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) continue;\n\n const row = this.rows[rowIndex];\n if (!row || !this.expandedRows.has(row)) continue;\n\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 rowEl.appendChild(detailEl);\n this.detailElements.set(row, detailEl);\n }\n }\n\n // ===== 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 // ===== Styles =====\n\n override readonly styles = `\n .master-detail-cell-wrapper {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .master-detail-toggle {\n cursor: pointer;\n font-size: 10px;\n opacity: 0.7;\n user-select: none;\n }\n .master-detail-toggle:hover {\n opacity: 1;\n }\n .master-detail-row {\n grid-column: 1 / -1;\n display: grid;\n background: var(--tbw-master-detail-bg, var(--tbw-color-row-alt));\n border-bottom: 1px solid var(--tbw-master-detail-border, var(--tbw-color-border));\n }\n .master-detail-cell {\n padding: 16px;\n overflow: auto;\n }\n `;\n}\n"],"names":["toggleDetailRow","expandedRows","row","newExpanded","expandDetailRow","collapseDetailRow","isDetailExpanded","createDetailElement","rowIndex","renderer","columnCount","detailRow","detailCell","content","MasterDetailPlugin","BaseGridPlugin","columns","cols","firstCol","originalRenderer","renderCtx","value","isExpanded","container","toggle","e","rendered","event","body","el","dataRows","rowEl","firstCell","detailEl","indices","idx"],"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 */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { BaseGridPlugin, RowClickEvent } from '../../core/plugin/base-plugin';\nimport {\n collapseDetailRow,\n createDetailElement,\n expandDetailRow,\n isDetailExpanded,\n toggleDetailRow,\n} from './master-detail';\nimport type { DetailExpandDetail, 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 version = '1.0.0';\n\n protected override get defaultConfig(): Partial<MasterDetailConfig> {\n return {\n enabled: true,\n detailHeight: 'auto',\n expandOnRowClick: false,\n collapseOnClickOutside: false,\n showExpandColumn: true,\n };\n }\n\n // ===== Internal State =====\n private expandedRows: Set<any> = new Set();\n private detailElements: Map<any, HTMLElement> = new Map();\n\n // ===== Lifecycle =====\n\n override detach(): void {\n this.expandedRows.clear();\n this.detailElements.clear();\n }\n\n // ===== Hooks =====\n\n override processColumns(\n columns: readonly import('../../core/types').ColumnConfig[]\n ): import('../../core/types').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;\n\n // Skip if already wrapped by this plugin (prevents double-wrapping on re-render)\n if ((originalRenderer as any)?.__masterDetailWrapped) {\n return cols;\n }\n\n const wrappedRenderer = (renderCtx: Parameters<NonNullable<typeof originalRenderer>>[0]) => {\n const { value, row } = renderCtx;\n const isExpanded = this.expandedRows.has(row);\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';\n toggle.textContent = isExpanded ? '▼' : '▶';\n toggle.setAttribute('aria-expanded', String(isExpanded));\n toggle.setAttribute('aria-label', isExpanded ? 'Collapse details' : 'Expand details');\n toggle.addEventListener('click', (e) => {\n e.stopPropagation();\n const rowIndex = this.rows.indexOf(row);\n this.expandedRows = toggleDetailRow(this.expandedRows, row);\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex,\n row,\n expanded: this.expandedRows.has(row),\n });\n this.requestRender();\n });\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 as any).__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);\n\n this.emit<DetailExpandDetail>('detail-expand', {\n rowIndex: event.rowIndex,\n row: event.row,\n expanded: this.expandedRows.has(event.row),\n });\n\n this.requestRender();\n return false;\n }\n\n override afterRender(): void {\n if (!this.config.detailRenderer) return;\n\n const body = this.shadowRoot?.querySelector('.rows');\n if (!body) return;\n\n // Remove old detail rows\n body.querySelectorAll('.master-detail-row').forEach((el) => el.remove());\n this.detailElements.clear();\n\n // Insert detail rows as last child of expanded row elements\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) continue;\n\n const row = this.rows[rowIndex];\n if (!row || !this.expandedRows.has(row)) continue;\n\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 rowEl.appendChild(detailEl);\n this.detailElements.set(row, detailEl);\n }\n }\n\n // ===== 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 // ===== Styles =====\n\n override readonly styles = `\n .master-detail-cell-wrapper {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n .master-detail-toggle {\n cursor: pointer;\n font-size: 10px;\n opacity: 0.7;\n user-select: none;\n }\n .master-detail-toggle:hover {\n opacity: 1;\n }\n .master-detail-row {\n grid-column: 1 / -1;\n display: grid;\n background: var(--tbw-master-detail-bg, var(--tbw-color-row-alt));\n border-bottom: 1px solid var(--tbw-master-detail-border, var(--tbw-color-border));\n }\n .master-detail-cell {\n padding: 16px;\n overflow: auto;\n }\n `;\n}\n"],"names":["toggleDetailRow","expandedRows","row","newExpanded","expandDetailRow","collapseDetailRow","isDetailExpanded","createDetailElement","rowIndex","renderer","columnCount","detailRow","detailCell","content","MasterDetailPlugin","BaseGridPlugin","columns","cols","firstCol","originalRenderer","wrappedRenderer","renderCtx","value","isExpanded","container","toggle","e","rendered","event","body","el","dataRows","rowEl","firstCell","detailEl","indices","idx"],"mappings":"wUAaO,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,CCjDO,MAAMG,UAA2BC,EAAAA,cAAmC,CAChE,KAAO,eACE,QAAU,QAE5B,IAAuB,eAA6C,CAClE,MAAO,CACL,QAAS,GACT,aAAc,OACd,iBAAkB,GAClB,uBAAwB,GACxB,iBAAkB,EAAA,CAEtB,CAGQ,iBAA6B,IAC7B,mBAA4C,IAI3C,QAAe,CACtB,KAAK,aAAa,MAAA,EAClB,KAAK,eAAe,MAAA,CACtB,CAIS,eACPC,EAC2C,CAC3C,GAAI,CAAC,KAAK,OAAO,eACf,MAAO,CAAC,GAAGA,CAAO,EAIpB,MAAMC,EAAO,CAAC,GAAGD,CAAO,EACxB,GAAIC,EAAK,OAAS,EAAG,CACnB,MAAMC,EAAW,CAAE,GAAGD,EAAK,CAAC,CAAA,EACtBE,EAAmBD,EAAS,aAGlC,GAAKC,GAA0B,sBAC7B,OAAOF,EAGT,MAAMG,EAAmBC,GAAmE,CAC1F,KAAM,CAAE,MAAAC,EAAO,IAAApB,CAAA,EAAQmB,EACjBE,EAAa,KAAK,aAAa,IAAIrB,CAAG,EAEtCsB,EAAY,SAAS,cAAc,MAAM,EAC/CA,EAAU,UAAY,6BAGtB,MAAMC,EAAS,SAAS,cAAc,MAAM,EAC5CA,EAAO,UAAY,uBACnBA,EAAO,YAAcF,EAAa,IAAM,IACxCE,EAAO,aAAa,gBAAiB,OAAOF,CAAU,CAAC,EACvDE,EAAO,aAAa,aAAcF,EAAa,mBAAqB,gBAAgB,EACpFE,EAAO,iBAAiB,QAAUC,GAAM,CACtCA,EAAE,gBAAA,EACF,MAAMlB,EAAW,KAAK,KAAK,QAAQN,CAAG,EACtC,KAAK,aAAeF,EAAgB,KAAK,aAAcE,CAAG,EAC1D,KAAK,KAAyB,gBAAiB,CAC7C,SAAAM,EACA,IAAAN,EACA,SAAU,KAAK,aAAa,IAAIA,CAAG,CAAA,CACpC,EACD,KAAK,cAAA,CACP,CAAC,EACDsB,EAAU,YAAYC,CAAM,EAG5B,MAAMZ,EAAU,SAAS,cAAc,MAAM,EAC7C,GAAIM,EAAkB,CACpB,MAAMQ,EAAWR,EAAiBE,CAAS,EACvCM,aAAoB,KACtBd,EAAQ,YAAYc,CAAQ,EAE5Bd,EAAQ,YAAc,OAAOc,GAAYL,GAAS,EAAE,CAExD,MACET,EAAQ,YAAc,OAAOS,GAAS,EAAE,EAE1C,OAAAE,EAAU,YAAYX,CAAO,EAEtBW,CACT,EAGCJ,EAAwB,sBAAwB,GACjDF,EAAS,aAAeE,EAExBH,EAAK,CAAC,EAAIC,CACZ,CAEA,OAAOD,CACT,CAES,WAAWW,EAAsC,CACxD,GAAI,GAAC,KAAK,OAAO,kBAAoB,CAAC,KAAK,OAAO,gBAElD,YAAK,aAAe5B,EAAgB,KAAK,aAAc4B,EAAM,GAAG,EAEhE,KAAK,KAAyB,gBAAiB,CAC7C,SAAUA,EAAM,SAChB,IAAKA,EAAM,IACX,SAAU,KAAK,aAAa,IAAIA,EAAM,GAAG,CAAA,CAC1C,EAED,KAAK,cAAA,EACE,EACT,CAES,aAAoB,CAC3B,GAAI,CAAC,KAAK,OAAO,eAAgB,OAEjC,MAAMC,EAAO,KAAK,YAAY,cAAc,OAAO,EACnD,GAAI,CAACA,EAAM,OAGXA,EAAK,iBAAiB,oBAAoB,EAAE,QAASC,GAAOA,EAAG,QAAQ,EACvE,KAAK,eAAe,MAAA,EAGpB,MAAMC,EAAWF,EAAK,iBAAiB,gBAAgB,EACjDnB,EAAc,KAAK,QAAQ,OAEjC,UAAWsB,KAASD,EAAU,CAC5B,MAAME,EAAYD,EAAM,cAAc,iBAAiB,EACjDxB,EAAWyB,EAAY,SAASA,EAAU,aAAa,UAAU,GAAK,KAAM,EAAE,EAAI,GACxF,GAAIzB,EAAW,EAAG,SAElB,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC9B,GAAI,CAACN,GAAO,CAAC,KAAK,aAAa,IAAIA,CAAG,EAAG,SAEzC,MAAMgC,EAAW3B,EAAoBL,EAAKM,EAAU,KAAK,OAAO,eAAgBE,CAAW,EAEvF,OAAO,KAAK,OAAO,cAAiB,WACtCwB,EAAS,MAAM,OAAS,GAAG,KAAK,OAAO,YAAY,MAGrDF,EAAM,YAAYE,CAAQ,EAC1B,KAAK,eAAe,IAAIhC,EAAKgC,CAAQ,CACvC,CACF,CAQA,OAAO1B,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,MAAMiC,EAAoB,CAAA,EAC1B,UAAWjC,KAAO,KAAK,aAAc,CACnC,MAAMkC,EAAM,KAAK,KAAK,QAAQlC,CAAG,EAC7BkC,GAAO,GAAGD,EAAQ,KAAKC,CAAG,CAChC,CACA,OAAOD,CACT,CAOA,iBAAiB3B,EAA2C,CAC1D,MAAMN,EAAM,KAAK,KAAKM,CAAQ,EAC9B,OAAON,EAAM,KAAK,eAAe,IAAIA,CAAG,EAAI,MAC9C,CAIkB,OAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GA0B7B"}
|
|
@@ -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","/**\r\n * Multi-Sort Plugin (Class-based)\r\n *\r\n * Provides multi-column sorting capabilities for tbw-grid.\r\n * Supports shift+click for adding secondary sort columns.\r\n */\r\n\r\nimport { BaseGridPlugin, HeaderClickEvent } from '../../core/plugin/base-plugin';\r\nimport type { ColumnState } from '../../core/types';\r\nimport { applySorts, getSortDirection, getSortIndex, toggleSort } from './multi-sort';\r\nimport type { MultiSortConfig, SortModel } from './types';\r\n\r\n/**\r\n * Multi-Sort Plugin for tbw-grid\r\n *\r\n * @example\r\n * ```ts\r\n * new MultiSortPlugin({ maxSortColumns: 3, showSortIndex: true })\r\n * ```\r\n */\r\nexport class MultiSortPlugin extends BaseGridPlugin<MultiSortConfig> {\r\n readonly name = 'multiSort';\r\n override readonly version = '1.0.0';\r\n\r\n protected override get defaultConfig(): Partial<MultiSortConfig> {\r\n return {\r\n enabled: true,\r\n maxSortColumns: 3,\r\n showSortIndex: true,\r\n };\r\n }\r\n\r\n // ===== Internal State =====\r\n private sortModel: SortModel[] = [];\r\n\r\n // ===== Lifecycle =====\r\n\r\n override detach(): void {\r\n this.sortModel = [];\r\n }\r\n\r\n // ===== Hooks =====\r\n\r\n override processRows(rows: readonly unknown[]): unknown[] {\r\n if (this.sortModel.length === 0) {\r\n return [...rows];\r\n }\r\n return applySorts([...rows], this.sortModel, [...this.columns]);\r\n }\r\n\r\n override onHeaderClick(event: HeaderClickEvent): boolean {\r\n const column = this.columns.find((c) => c.field === event.field);\r\n if (!column?.sortable) return false;\r\n\r\n const shiftKey = event.originalEvent.shiftKey;\r\n const maxColumns = this.config.maxSortColumns ?? 3;\r\n\r\n this.sortModel = toggleSort(this.sortModel, event.field, shiftKey, maxColumns);\r\n\r\n this.emit('sort-change', { sortModel: [...this.sortModel] });\r\n this.requestRender();\r\n\r\n return true;\r\n }\r\n\r\n override afterRender(): void {\r\n const shadowRoot = this.shadowRoot;\r\n if (!shadowRoot) return;\r\n\r\n const showIndex = this.config.showSortIndex !== false;\r\n\r\n // Update all sortable header cells with sort indicators\r\n const headerCells = shadowRoot.querySelectorAll('.header-row .cell[data-field]');\r\n headerCells.forEach((cell) => {\r\n const field = cell.getAttribute('data-field');\r\n if (!field) return;\r\n\r\n const sortIndex = getSortIndex(this.sortModel, field);\r\n const sortDir = getSortDirection(this.sortModel, field);\r\n\r\n // Remove existing sort index badge (always clean up)\r\n const existingBadge = cell.querySelector('.sort-index');\r\n existingBadge?.remove();\r\n\r\n if (sortDir) {\r\n // Column is sorted - remove base indicator and add our own\r\n const existingIndicator = cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator');\r\n existingIndicator?.remove();\r\n\r\n cell.setAttribute('data-sort', sortDir);\r\n\r\n // Add sort arrow indicator\r\n const indicator = document.createElement('span');\r\n indicator.className = 'sort-indicator';\r\n indicator.style.marginLeft = '4px';\r\n indicator.style.opacity = '0.8';\r\n indicator.textContent = sortDir === 'asc' ? '▲' : '▼';\r\n cell.appendChild(indicator);\r\n\r\n // Add sort index badge if multiple columns sorted and showSortIndex is enabled\r\n if (showIndex && this.sortModel.length > 1 && sortIndex !== undefined) {\r\n const badge = document.createElement('span');\r\n badge.className = 'sort-index';\r\n badge.textContent = String(sortIndex);\r\n cell.appendChild(badge);\r\n }\r\n } else {\r\n cell.removeAttribute('data-sort');\r\n // For unsorted columns, leave the base indicator (⇅) alone\r\n }\r\n });\r\n }\r\n\r\n // ===== Public API =====\r\n\r\n /**\r\n * Get the current sort model.\r\n * @returns Copy of the current sort model\r\n */\r\n getSortModel(): SortModel[] {\r\n return [...this.sortModel];\r\n }\r\n\r\n /**\r\n * Set the sort model programmatically.\r\n * @param model - New sort model to apply\r\n */\r\n setSortModel(model: SortModel[]): void {\r\n this.sortModel = [...model];\r\n this.emit('sort-change', { sortModel: [...model] });\r\n this.requestRender();\r\n }\r\n\r\n /**\r\n * Clear all sorting.\r\n */\r\n clearSort(): void {\r\n this.sortModel = [];\r\n this.emit('sort-change', { sortModel: [] });\r\n this.requestRender();\r\n }\r\n\r\n /**\r\n * Get the sort index (1-based) for a specific field.\r\n * @param field - Field to check\r\n * @returns 1-based index or undefined if not sorted\r\n */\r\n getSortIndex(field: string): number | undefined {\r\n return getSortIndex(this.sortModel, field);\r\n }\r\n\r\n /**\r\n * Get the sort direction for a specific field.\r\n * @param field - Field to check\r\n * @returns Sort direction or undefined if not sorted\r\n */\r\n getSortDirection(field: string): 'asc' | 'desc' | undefined {\r\n return getSortDirection(this.sortModel, field);\r\n }\r\n\r\n // ===== Column State Hooks =====\r\n\r\n /**\r\n * Return sort state for a column if it's in the sort model.\r\n */\r\n override getColumnState(field: string): Partial<ColumnState> | undefined {\r\n const index = this.sortModel.findIndex((s) => s.field === field);\r\n if (index === -1) return undefined;\r\n\r\n const sortEntry = this.sortModel[index];\r\n return {\r\n sort: {\r\n direction: sortEntry.direction,\r\n priority: index,\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Apply sort state from column state.\r\n * Rebuilds the sort model from all column states.\r\n */\r\n override applyColumnState(field: string, state: ColumnState): void {\r\n // Only process if the column has sort state\r\n if (!state.sort) {\r\n // Remove this field from sortModel if it exists\r\n this.sortModel = this.sortModel.filter((s) => s.field !== field);\r\n return;\r\n }\r\n\r\n // Find existing entry or add new one\r\n const existingIndex = this.sortModel.findIndex((s) => s.field === field);\r\n const newEntry: SortModel = {\r\n field,\r\n direction: state.sort.direction,\r\n };\r\n\r\n if (existingIndex !== -1) {\r\n // Update existing entry\r\n this.sortModel[existingIndex] = newEntry;\r\n } else {\r\n // Add at the correct priority position\r\n this.sortModel.splice(state.sort.priority, 0, newEntry);\r\n }\r\n\r\n // Re-sort the model by priority to ensure correct order\r\n // This is handled after all columns are processed, but we maintain order here\r\n }\r\n\r\n // ===== Styles =====\r\n\r\n override readonly styles = `\r\n .header-cell[data-sort=\"asc\"]::after {\r\n content: '↑';\r\n margin-left: 4px;\r\n opacity: 0.8;\r\n }\r\n .header-cell[data-sort=\"desc\"]::after {\r\n content: '↓';\r\n margin-left: 4px;\r\n opacity: 0.8;\r\n }\r\n .sort-index {\r\n font-size: 10px;\r\n background: var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));\r\n color: var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));\r\n border-radius: 50%;\r\n width: 14px;\r\n height: 14px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-left: 2px;\r\n font-weight: 600;\r\n }\r\n `;\r\n}\r\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","badge","model","state","existingIndex","newEntry"],"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 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 enabled: true,\n maxSortColumns: 3,\n showSortIndex: true,\n };\n }\n\n // ===== Internal State =====\n private sortModel: SortModel[] = [];\n\n // ===== Lifecycle =====\n\n override detach(): void {\n this.sortModel = [];\n }\n\n // ===== 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\n const indicator = document.createElement('span');\n indicator.className = 'sort-indicator';\n indicator.style.marginLeft = '4px';\n indicator.style.opacity = '0.8';\n indicator.textContent = sortDir === 'asc' ? '▲' : '▼';\n cell.appendChild(indicator);\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 cell.appendChild(badge);\n }\n } else {\n cell.removeAttribute('data-sort');\n // For unsorted columns, leave the base indicator (⇅) alone\n }\n });\n }\n\n // ===== 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\n // ===== 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\n // ===== Styles =====\n\n override readonly styles = `\n .header-cell[data-sort=\"asc\"]::after {\n content: '↑';\n margin-left: 4px;\n opacity: 0.8;\n }\n .header-cell[data-sort=\"desc\"]::after {\n content: '↓';\n margin-left: 4px;\n opacity: 0.8;\n }\n .sort-index {\n font-size: 10px;\n background: var(--tbw-multi-sort-badge-bg, var(--tbw-color-panel-bg));\n color: var(--tbw-multi-sort-badge-color, var(--tbw-color-fg));\n border-radius: 50%;\n width: 14px;\n height: 14px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n margin-left: 2px;\n font-weight: 600;\n }\n `;\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","badge","model","state","existingIndex","newEntry"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(a,
|
|
1
|
+
(function(a,g){typeof exports=="object"&&typeof module<"u"?g(exports,require("../../core/plugin/base-plugin")):typeof define=="function"&&define.amd?define(["exports","../../core/plugin/base-plugin"],g):(a=typeof globalThis<"u"?globalThis:a||self,g(a.TbwGridPlugin_reorder={},a.TbwGrid))})(this,(function(a,g){"use strict";function m(i){const r=i.sticky;if(r==="left"||r==="right")return!1;const t=i.meta??{},n=t.sticky;return n==="left"||n==="right"?!1:t.lockPosition!==!0&&t.suppressMovable!==!0}function f(i,r,t){if(r===t||r<0||r>=i.length||t<0||t>i.length)return i;const n=[...i],[e]=n.splice(r,1);return n.splice(t,0,e),n}class b extends g.BaseGridPlugin{name="reorder";version="1.0.0";get defaultConfig(){return{enabled:!0,animation:!0,animationDuration:200}}isDragging=!1;draggedField=null;draggedIndex=null;dropIndex=null;attach(r){super.attach(r),r.addEventListener("column-reorder-request",t=>{const n=t.detail;n?.field&&typeof n.toIndex=="number"&&this.moveColumn(n.field,n.toIndex)},{signal:this.disconnectSignal})}detach(){this.isDragging=!1,this.draggedField=null,this.draggedIndex=null,this.dropIndex=null}afterRender(){if(!this.config.enabled)return;const r=this.shadowRoot;if(!r)return;r.querySelectorAll(".header-row > .cell").forEach(n=>{const e=n,o=e.getAttribute("data-field");if(!o)return;const h=this.columns.find(d=>d.field===o);if(!h||!m(h)){e.draggable=!1;return}e.draggable=!0,!e.getAttribute("data-dragstart-bound")&&(e.setAttribute("data-dragstart-bound","true"),e.addEventListener("dragstart",d=>{const s=this.getColumnOrder().indexOf(o);this.isDragging=!0,this.draggedField=o,this.draggedIndex=s,d.dataTransfer&&(d.dataTransfer.effectAllowed="move",d.dataTransfer.setData("text/plain",o)),e.classList.add("dragging")}),e.addEventListener("dragend",()=>{this.isDragging=!1,this.draggedField=null,this.draggedIndex=null,this.dropIndex=null,r.querySelectorAll(".header-row > .cell").forEach(d=>{d.classList.remove("dragging","drop-target","drop-before","drop-after")})}),e.addEventListener("dragover",d=>{if(d.preventDefault(),!this.isDragging||this.draggedField===o)return;const l=e.getBoundingClientRect(),s=l.left+l.width/2,c=this.getColumnOrder().indexOf(o);this.dropIndex=d.clientX<s?c:c+1,e.classList.add("drop-target"),e.classList.toggle("drop-before",d.clientX<s),e.classList.toggle("drop-after",d.clientX>=s)}),e.addEventListener("dragleave",()=>{e.classList.remove("drop-target","drop-before","drop-after")}),e.addEventListener("drop",d=>{d.preventDefault();const l=this.draggedField,s=this.draggedIndex,u=this.dropIndex;if(!this.isDragging||l===null||s===null||u===null)return;const c=u>s?u-1:u,v=this.getColumnOrder(),p=f(v,s,c),O={field:l,fromIndex:s,toIndex:c,columnOrder:p};this.grid.setColumnOrder(p),this.emit("column-move",O),this.grid.requestStateChange?.()}))})}getColumnOrder(){return this.grid.getColumnOrder()}moveColumn(r,t){const n=this.getColumnOrder(),e=n.indexOf(r);if(e===-1)return;const o=f(n,e,t);this.grid.setColumnOrder(o),this.emit("column-move",{field:r,fromIndex:e,toIndex:t,columnOrder:o}),this.grid.requestStateChange?.()}setColumnOrder(r){this.grid.setColumnOrder(r),this.grid.requestStateChange?.()}resetColumnOrder(){const r=this.columns.map(t=>t.field);this.grid.setColumnOrder(r),this.grid.requestStateChange?.()}styles=`
|
|
2
2
|
.header-row > .cell[draggable="true"] {
|
|
3
3
|
cursor: grab;
|
|
4
4
|
position: relative;
|