@toolbox-web/grid 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/all.d.ts +3518 -0
  2. package/all.js +3762 -0
  3. package/all.js.map +1 -0
  4. package/index.d.ts +2367 -0
  5. package/index.js +3105 -0
  6. package/index.js.map +1 -0
  7. package/lib/plugins/clipboard/index.js +365 -0
  8. package/lib/plugins/clipboard/index.js.map +1 -0
  9. package/lib/plugins/column-virtualization/index.js +255 -0
  10. package/lib/plugins/column-virtualization/index.js.map +1 -0
  11. package/lib/plugins/context-menu/index.js +341 -0
  12. package/lib/plugins/context-menu/index.js.map +1 -0
  13. package/lib/plugins/export/index.js +305 -0
  14. package/lib/plugins/export/index.js.map +1 -0
  15. package/lib/plugins/filtering/index.js +759 -0
  16. package/lib/plugins/filtering/index.js.map +1 -0
  17. package/lib/plugins/grouping-columns/index.js +283 -0
  18. package/lib/plugins/grouping-columns/index.js.map +1 -0
  19. package/lib/plugins/grouping-rows/index.js +494 -0
  20. package/lib/plugins/grouping-rows/index.js.map +1 -0
  21. package/lib/plugins/master-detail/index.js +303 -0
  22. package/lib/plugins/master-detail/index.js.map +1 -0
  23. package/lib/plugins/multi-sort/index.js +270 -0
  24. package/lib/plugins/multi-sort/index.js.map +1 -0
  25. package/lib/plugins/pinned-columns/index.js +221 -0
  26. package/lib/plugins/pinned-columns/index.js.map +1 -0
  27. package/lib/plugins/pinned-rows/index.js +459 -0
  28. package/lib/plugins/pinned-rows/index.js.map +1 -0
  29. package/lib/plugins/pivot/index.js +326 -0
  30. package/lib/plugins/pivot/index.js.map +1 -0
  31. package/lib/plugins/reorder/index.js +260 -0
  32. package/lib/plugins/reorder/index.js.map +1 -0
  33. package/lib/plugins/selection/index.js +426 -0
  34. package/lib/plugins/selection/index.js.map +1 -0
  35. package/lib/plugins/server-side/index.js +241 -0
  36. package/lib/plugins/server-side/index.js.map +1 -0
  37. package/lib/plugins/tree/index.js +383 -0
  38. package/lib/plugins/tree/index.js.map +1 -0
  39. package/lib/plugins/undo-redo/index.js +289 -0
  40. package/lib/plugins/undo-redo/index.js.map +1 -0
  41. package/lib/plugins/visibility/index.js +430 -0
  42. package/lib/plugins/visibility/index.js.map +1 -0
  43. package/package.json +53 -0
  44. package/themes/dg-theme-contrast.css +43 -0
  45. package/themes/dg-theme-large.css +54 -0
  46. package/themes/dg-theme-standard.css +19 -0
  47. package/themes/dg-theme-vibrant.css +16 -0
  48. package/umd/grid.all.umd.js +660 -0
  49. package/umd/grid.all.umd.js.map +1 -0
  50. package/umd/grid.umd.js +105 -0
  51. package/umd/grid.umd.js.map +1 -0
  52. package/umd/plugins/clipboard.umd.js +9 -0
  53. package/umd/plugins/clipboard.umd.js.map +1 -0
  54. package/umd/plugins/column-virtualization.umd.js +2 -0
  55. package/umd/plugins/column-virtualization.umd.js.map +1 -0
  56. package/umd/plugins/context-menu.umd.js +53 -0
  57. package/umd/plugins/context-menu.umd.js.map +1 -0
  58. package/umd/plugins/export.umd.js +14 -0
  59. package/umd/plugins/export.umd.js.map +1 -0
  60. package/umd/plugins/filtering.umd.js +175 -0
  61. package/umd/plugins/filtering.umd.js.map +1 -0
  62. package/umd/plugins/grouping-columns.umd.js +29 -0
  63. package/umd/plugins/grouping-columns.umd.js.map +1 -0
  64. package/umd/plugins/grouping-rows.umd.js +40 -0
  65. package/umd/plugins/grouping-rows.umd.js.map +1 -0
  66. package/umd/plugins/master-detail.umd.js +27 -0
  67. package/umd/plugins/master-detail.umd.js.map +1 -0
  68. package/umd/plugins/multi-sort.umd.js +26 -0
  69. package/umd/plugins/multi-sort.umd.js.map +1 -0
  70. package/umd/plugins/pinned-columns.umd.js +2 -0
  71. package/umd/plugins/pinned-columns.umd.js.map +1 -0
  72. package/umd/plugins/pinned-rows.umd.js +73 -0
  73. package/umd/plugins/pinned-rows.umd.js.map +1 -0
  74. package/umd/plugins/pivot.umd.js +8 -0
  75. package/umd/plugins/pivot.umd.js.map +1 -0
  76. package/umd/plugins/reorder.umd.js +31 -0
  77. package/umd/plugins/reorder.umd.js.map +1 -0
  78. package/umd/plugins/selection.umd.js +34 -0
  79. package/umd/plugins/selection.umd.js.map +1 -0
  80. package/umd/plugins/server-side.umd.js +2 -0
  81. package/umd/plugins/server-side.umd.js.map +1 -0
  82. package/umd/plugins/tree.umd.js +11 -0
  83. package/umd/plugins/tree.umd.js.map +1 -0
  84. package/umd/plugins/undo-redo.umd.js +2 -0
  85. package/umd/plugins/undo-redo.umd.js.map +1 -0
  86. package/umd/plugins/visibility.umd.js +94 -0
  87. package/umd/plugins/visibility.umd.js.map +1 -0
@@ -0,0 +1,759 @@
1
+ class P {
2
+ /** Plugin version - override in subclass if needed */
3
+ version = "1.0.0";
4
+ /** CSS styles to inject into the grid's shadow DOM */
5
+ styles;
6
+ /** Custom cell renderers keyed by type name */
7
+ cellRenderers;
8
+ /** Custom header renderers keyed by type name */
9
+ headerRenderers;
10
+ /** Custom cell editors keyed by type name */
11
+ cellEditors;
12
+ /** The grid instance this plugin is attached to */
13
+ grid;
14
+ /** Plugin configuration - merged with defaults in attach() */
15
+ config;
16
+ /** User-provided configuration from constructor */
17
+ userConfig;
18
+ /**
19
+ * Default configuration - subclasses should override this getter.
20
+ * Note: This must be a getter (not property initializer) for proper inheritance
21
+ * since property initializers run after parent constructor.
22
+ */
23
+ get defaultConfig() {
24
+ return {};
25
+ }
26
+ constructor(e = {}) {
27
+ this.userConfig = e;
28
+ }
29
+ /**
30
+ * Called when the plugin is attached to a grid.
31
+ * Override to set up event listeners, initialize state, etc.
32
+ */
33
+ attach(e) {
34
+ this.grid = e, this.config = { ...this.defaultConfig, ...this.userConfig };
35
+ }
36
+ /**
37
+ * Called when the plugin is detached from a grid.
38
+ * Override to clean up event listeners, timers, etc.
39
+ */
40
+ detach() {
41
+ }
42
+ /**
43
+ * Get another plugin instance from the same grid.
44
+ * Use for inter-plugin communication.
45
+ */
46
+ getPlugin(e) {
47
+ return this.grid?.getPlugin(e);
48
+ }
49
+ /**
50
+ * Emit a custom event from the grid.
51
+ */
52
+ emit(e, t) {
53
+ this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: t, bubbles: !0 }));
54
+ }
55
+ /**
56
+ * Request a re-render of the grid.
57
+ */
58
+ requestRender() {
59
+ this.grid?.requestRender?.();
60
+ }
61
+ /**
62
+ * Request a lightweight style update without rebuilding DOM.
63
+ * Use this instead of requestRender() when only CSS classes need updating.
64
+ */
65
+ requestAfterRender() {
66
+ this.grid?.requestAfterRender?.();
67
+ }
68
+ /**
69
+ * Get the current rows from the grid.
70
+ */
71
+ get rows() {
72
+ return this.grid?.rows ?? [];
73
+ }
74
+ /**
75
+ * Get the original unfiltered/unprocessed rows from the grid.
76
+ * Use this when you need all source data regardless of active filters.
77
+ */
78
+ get sourceRows() {
79
+ return this.grid?.sourceRows ?? [];
80
+ }
81
+ /**
82
+ * Get the current columns from the grid.
83
+ */
84
+ get columns() {
85
+ return this.grid?.columns ?? [];
86
+ }
87
+ /**
88
+ * Get only visible columns from the grid (excludes hidden).
89
+ * Use this for rendering that needs to match the grid template.
90
+ */
91
+ get visibleColumns() {
92
+ return this.grid?.visibleColumns ?? [];
93
+ }
94
+ /**
95
+ * Get the shadow root of the grid.
96
+ */
97
+ get shadowRoot() {
98
+ return this.grid?.shadowRoot ?? null;
99
+ }
100
+ /**
101
+ * Log a warning message.
102
+ */
103
+ warn(e) {
104
+ console.warn(`[tbw-grid:${this.name}] ${e}`);
105
+ }
106
+ }
107
+ function A(f) {
108
+ const { totalRows: e, viewportHeight: t, scrollTop: r, rowHeight: l, overscan: s } = f, i = Math.ceil(t / l);
109
+ let n = Math.floor(r / l) - s;
110
+ n < 0 && (n = 0);
111
+ let u = n + i + s * 2;
112
+ return u > e && (u = e), u === e && n > 0 && (n = Math.max(0, u - i - s * 2)), {
113
+ start: n,
114
+ end: u,
115
+ offsetY: n * l,
116
+ totalHeight: e * l
117
+ };
118
+ }
119
+ function V(f, e) {
120
+ return f <= e;
121
+ }
122
+ function _(f, e, t = !1) {
123
+ const r = f[e.field];
124
+ if (e.operator === "blank")
125
+ return r == null || r === "";
126
+ if (e.operator === "notBlank")
127
+ return r != null && r !== "";
128
+ if (r == null) return !1;
129
+ const l = String(r), s = t ? l : l.toLowerCase(), i = t ? String(e.value) : String(e.value).toLowerCase();
130
+ switch (e.operator) {
131
+ // Text operators
132
+ case "contains":
133
+ return s.includes(i);
134
+ case "notContains":
135
+ return !s.includes(i);
136
+ case "equals":
137
+ return s === i;
138
+ case "notEquals":
139
+ return s !== i;
140
+ case "startsWith":
141
+ return s.startsWith(i);
142
+ case "endsWith":
143
+ return s.endsWith(i);
144
+ // Number/Date operators (use raw numeric values)
145
+ case "lessThan":
146
+ return Number(r) < Number(e.value);
147
+ case "lessThanOrEqual":
148
+ return Number(r) <= Number(e.value);
149
+ case "greaterThan":
150
+ return Number(r) > Number(e.value);
151
+ case "greaterThanOrEqual":
152
+ return Number(r) >= Number(e.value);
153
+ case "between":
154
+ return Number(r) >= Number(e.value) && Number(r) <= Number(e.valueTo);
155
+ // Set operators
156
+ case "in":
157
+ return Array.isArray(e.value) && e.value.includes(r);
158
+ case "notIn":
159
+ return Array.isArray(e.value) && !e.value.includes(r);
160
+ default:
161
+ return !0;
162
+ }
163
+ }
164
+ function q(f, e, t = !1) {
165
+ return e.length ? f.filter((r) => e.every((l) => _(r, l, t))) : f;
166
+ }
167
+ function B(f) {
168
+ return JSON.stringify(
169
+ f.map((e) => ({
170
+ field: e.field,
171
+ operator: e.operator,
172
+ value: e.value,
173
+ valueTo: e.valueTo
174
+ }))
175
+ );
176
+ }
177
+ function M(f, e) {
178
+ const t = /* @__PURE__ */ new Set();
179
+ for (const r of f) {
180
+ const l = r[e];
181
+ l != null && t.add(l);
182
+ }
183
+ return [...t].sort((r, l) => typeof r == "number" && typeof l == "number" ? r - l : String(r).localeCompare(String(l)));
184
+ }
185
+ const z = `
186
+ .tbw-filter-panel {
187
+ position: fixed;
188
+ background: var(--tbw-filter-panel-bg, var(--tbw-color-panel-bg, light-dark(#eeeeee, #222222)));
189
+ color: var(--tbw-filter-panel-fg, var(--tbw-color-fg, light-dark(#222222, #eeeeee)));
190
+ border: 1px solid var(--tbw-filter-panel-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));
191
+ border-radius: var(--tbw-filter-panel-radius, var(--tbw-border-radius, 4px));
192
+ 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))));
193
+ padding: 12px;
194
+ z-index: 10000;
195
+ min-width: 200px;
196
+ max-width: 280px;
197
+ max-height: 350px;
198
+ display: flex;
199
+ flex-direction: column;
200
+ font-family: var(--tbw-font-family, system-ui, sans-serif);
201
+ font-size: var(--tbw-font-size, 13px);
202
+ }
203
+
204
+ .tbw-filter-search {
205
+ margin-bottom: 8px;
206
+ }
207
+
208
+ .tbw-filter-search-input {
209
+ width: 100%;
210
+ padding: 6px 10px;
211
+ background: var(--tbw-filter-input-bg, var(--tbw-color-bg, transparent));
212
+ color: inherit;
213
+ border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));
214
+ border-radius: var(--tbw-filter-input-radius, 4px);
215
+ font-size: inherit;
216
+ box-sizing: border-box;
217
+ }
218
+
219
+ .tbw-filter-search-input:focus {
220
+ outline: none;
221
+ border-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
222
+ box-shadow: 0 0 0 2px rgba(from var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6)) r g b / 15%);
223
+ }
224
+
225
+ .tbw-filter-actions {
226
+ display: flex;
227
+ padding: 4px 2px;
228
+ margin-bottom: 8px;
229
+ border-bottom: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));
230
+ }
231
+
232
+ .tbw-filter-action-btn {
233
+ background: transparent;
234
+ border: none;
235
+ color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
236
+ cursor: pointer;
237
+ font-size: 12px;
238
+ padding: 2px 0;
239
+ }
240
+
241
+ .tbw-filter-action-btn:hover {
242
+ text-decoration: underline;
243
+ }
244
+
245
+ .tbw-filter-values {
246
+ flex: 1;
247
+ overflow-y: auto;
248
+ margin-bottom: 8px;
249
+ max-height: 180px;
250
+ position: relative;
251
+ }
252
+
253
+ .tbw-filter-values-spacer {
254
+ width: 1px;
255
+ }
256
+
257
+ .tbw-filter-values-content {
258
+ position: absolute;
259
+ top: 0;
260
+ left: 0;
261
+ right: 0;
262
+ }
263
+
264
+ .tbw-filter-value-item {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 8px;
268
+ padding: 4px 2px;
269
+ cursor: pointer;
270
+ border-radius: 3px;
271
+ }
272
+
273
+ .tbw-filter-value-item:hover {
274
+ background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));
275
+ }
276
+
277
+ .tbw-filter-checkbox {
278
+ margin: 0;
279
+ cursor: pointer;
280
+ accent-color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
281
+ }
282
+
283
+ .tbw-filter-no-match {
284
+ color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));
285
+ padding: 8px 0;
286
+ text-align: center;
287
+ font-style: italic;
288
+ }
289
+
290
+ .tbw-filter-buttons {
291
+ display: flex;
292
+ gap: 8px;
293
+ padding-top: 8px;
294
+ border-top: 1px solid var(--tbw-filter-divider, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));
295
+ }
296
+
297
+ .tbw-filter-apply-btn {
298
+ flex: 1;
299
+ padding: 6px 12px;
300
+ background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
301
+ color: var(--tbw-filter-accent-fg, var(--tbw-color-accent-fg, light-dark(#ffffff, #000000)));
302
+ border: none;
303
+ border-radius: 4px;
304
+ cursor: pointer;
305
+ font-size: 13px;
306
+ }
307
+
308
+ .tbw-filter-apply-btn:hover {
309
+ filter: brightness(0.9);
310
+ }
311
+
312
+ .tbw-filter-clear-btn {
313
+ flex: 1;
314
+ padding: 6px 12px;
315
+ background: transparent;
316
+ color: var(--tbw-filter-muted, var(--tbw-color-fg-muted, light-dark(#555555, #aaaaaa)));
317
+ border: 1px solid var(--tbw-filter-input-border, var(--tbw-color-border, light-dark(#d0d0d4, #454545)));
318
+ border-radius: 4px;
319
+ cursor: pointer;
320
+ font-size: 13px;
321
+ }
322
+
323
+ .tbw-filter-clear-btn:hover {
324
+ background: var(--tbw-filter-hover, var(--tbw-color-row-hover, light-dark(#f0f6ff, #1c1c1c)));
325
+ }
326
+ `;
327
+ class y extends P {
328
+ name = "filtering";
329
+ version = "1.0.0";
330
+ get defaultConfig() {
331
+ return {
332
+ enabled: !0,
333
+ debounceMs: 300,
334
+ caseSensitive: !1,
335
+ trimInput: !0,
336
+ useWorker: !0
337
+ };
338
+ }
339
+ // ===== Internal State =====
340
+ filters = /* @__PURE__ */ new Map();
341
+ cachedResult = null;
342
+ cacheKey = null;
343
+ openPanelField = null;
344
+ panelElement = null;
345
+ searchText = /* @__PURE__ */ new Map();
346
+ excludedValues = /* @__PURE__ */ new Map();
347
+ documentClickHandler = null;
348
+ globalStylesInjected = !1;
349
+ // Virtualization constants for filter value list
350
+ static LIST_ITEM_HEIGHT = 28;
351
+ static LIST_OVERSCAN = 3;
352
+ static LIST_BYPASS_THRESHOLD = 50;
353
+ // Don't virtualize if < 50 items
354
+ // ===== Lifecycle =====
355
+ attach(e) {
356
+ super.attach(e), this.injectGlobalStyles();
357
+ }
358
+ detach() {
359
+ 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.removeDocumentClickHandler();
360
+ }
361
+ // ===== Hooks =====
362
+ processRows(e) {
363
+ const t = [...this.filters.values()];
364
+ if (!t.length) return [...e];
365
+ const r = B(t);
366
+ if (this.cacheKey === r && this.cachedResult)
367
+ return this.cachedResult;
368
+ const l = q([...e], t, this.config.caseSensitive);
369
+ return this.cachedResult = l, this.cacheKey = r, l;
370
+ }
371
+ afterRender() {
372
+ if (!this.config.enabled) return;
373
+ const e = this.shadowRoot;
374
+ if (!e) return;
375
+ e.querySelectorAll('[part~="header-cell"]').forEach((r) => {
376
+ const l = r.getAttribute("data-col");
377
+ if (l === null) return;
378
+ const s = this.columns[parseInt(l, 10)];
379
+ if (!s || s.filterable === !1 || r.querySelector(".tbw-filter-btn")) return;
380
+ const i = s.field;
381
+ if (!i) return;
382
+ const n = document.createElement("button");
383
+ n.className = "tbw-filter-btn", n.setAttribute("aria-label", `Filter ${s.header ?? i}`), n.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) && (n.classList.add("active"), r.classList.add("filtered")), n.addEventListener("click", (u) => {
384
+ u.stopPropagation(), this.toggleFilterPanel(i, s, n);
385
+ }), r.appendChild(n);
386
+ });
387
+ }
388
+ // ===== Public API =====
389
+ /**
390
+ * Set a filter on a specific field.
391
+ * Pass null to remove the filter.
392
+ */
393
+ setFilter(e, t) {
394
+ t === null ? this.filters.delete(e) : this.filters.set(e, { ...t, field: e }), this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
395
+ filters: [...this.filters.values()],
396
+ filteredRowCount: 0
397
+ // Will be accurate after processRows
398
+ }), this.requestRender();
399
+ }
400
+ /**
401
+ * Get the current filter for a field.
402
+ */
403
+ getFilter(e) {
404
+ return this.filters.get(e);
405
+ }
406
+ /**
407
+ * Get all active filters.
408
+ */
409
+ getFilters() {
410
+ return [...this.filters.values()];
411
+ }
412
+ /**
413
+ * Alias for getFilters() to match functional API naming.
414
+ */
415
+ getFilterModel() {
416
+ return this.getFilters();
417
+ }
418
+ /**
419
+ * Set filters from an array (replaces all existing filters).
420
+ */
421
+ setFilterModel(e) {
422
+ this.filters.clear();
423
+ for (const t of e)
424
+ this.filters.set(t.field, t);
425
+ this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
426
+ filters: [...this.filters.values()],
427
+ filteredRowCount: 0
428
+ }), this.requestRender();
429
+ }
430
+ /**
431
+ * Clear all filters.
432
+ */
433
+ clearAllFilters() {
434
+ this.filters.clear(), this.excludedValues.clear(), this.searchText.clear(), this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
435
+ filters: [],
436
+ filteredRowCount: this.rows.length
437
+ }), this.requestRender();
438
+ }
439
+ /**
440
+ * Clear filter for a specific field.
441
+ */
442
+ clearFieldFilter(e) {
443
+ this.filters.delete(e), this.excludedValues.delete(e), this.searchText.delete(e), this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
444
+ filters: [...this.filters.values()],
445
+ filteredRowCount: 0
446
+ }), this.requestRender();
447
+ }
448
+ /**
449
+ * Check if a field has an active filter.
450
+ */
451
+ isFieldFiltered(e) {
452
+ return this.filters.has(e);
453
+ }
454
+ /**
455
+ * Get the count of filtered rows (from cache).
456
+ */
457
+ getFilteredRowCount() {
458
+ return this.cachedResult?.length ?? this.rows.length;
459
+ }
460
+ /**
461
+ * Get all active filters (alias for getFilters).
462
+ */
463
+ getActiveFilters() {
464
+ return this.getFilters();
465
+ }
466
+ /**
467
+ * Get unique values for a field (for set filter dropdowns).
468
+ * Uses sourceRows to include all values regardless of current filter.
469
+ */
470
+ getUniqueValues(e) {
471
+ return M(this.sourceRows, e);
472
+ }
473
+ // ===== Private Methods =====
474
+ /**
475
+ * Inject global styles for filter panel (rendered in document.body)
476
+ */
477
+ injectGlobalStyles() {
478
+ if (this.globalStylesInjected) return;
479
+ if (document.getElementById("tbw-filter-panel-styles")) {
480
+ this.globalStylesInjected = !0;
481
+ return;
482
+ }
483
+ const e = document.createElement("style");
484
+ e.id = "tbw-filter-panel-styles", e.textContent = z, document.head.appendChild(e), this.globalStylesInjected = !0;
485
+ }
486
+ /**
487
+ * Toggle the filter panel for a field
488
+ */
489
+ toggleFilterPanel(e, t, r) {
490
+ if (this.openPanelField === e) {
491
+ this.closeFilterPanel();
492
+ return;
493
+ }
494
+ this.closeFilterPanel();
495
+ const l = document.createElement("div");
496
+ l.className = "tbw-filter-panel", this.panelElement = l, this.openPanelField = e;
497
+ const s = M(this.sourceRows, e);
498
+ let i = this.excludedValues.get(e);
499
+ i || (i = /* @__PURE__ */ new Set(), this.excludedValues.set(e, i));
500
+ const n = this.searchText.get(e) ?? "", u = {
501
+ field: e,
502
+ column: t,
503
+ uniqueValues: s,
504
+ excludedValues: i,
505
+ searchText: n,
506
+ applySetFilter: (m) => {
507
+ this.applySetFilter(e, m), this.closeFilterPanel();
508
+ },
509
+ applyTextFilter: (m, k, w) => {
510
+ this.applyTextFilter(e, m, k, w), this.closeFilterPanel();
511
+ },
512
+ clearFilter: () => {
513
+ this.clearFieldFilter(e), this.closeFilterPanel();
514
+ },
515
+ closePanel: () => this.closeFilterPanel()
516
+ };
517
+ let g = !1;
518
+ this.config.filterPanelRenderer && (this.config.filterPanelRenderer(l, u), g = l.children.length > 0), g || this.renderDefaultFilterPanel(l, u, s, i), document.body.appendChild(l), this.positionPanel(l, r);
519
+ const p = (m) => {
520
+ !l.contains(m.target) && m.target !== r && this.closeFilterPanel();
521
+ };
522
+ this.documentClickHandler = p, setTimeout(() => {
523
+ document.addEventListener("click", p);
524
+ }, 0);
525
+ }
526
+ /**
527
+ * Close the filter panel
528
+ */
529
+ closeFilterPanel() {
530
+ this.panelElement && (this.panelElement.remove(), this.panelElement = null), this.openPanelField = null, this.removeDocumentClickHandler();
531
+ }
532
+ /**
533
+ * Remove the document click handler
534
+ */
535
+ removeDocumentClickHandler() {
536
+ this.documentClickHandler && (document.removeEventListener("click", this.documentClickHandler), this.documentClickHandler = null);
537
+ }
538
+ /**
539
+ * Position the panel below the button
540
+ */
541
+ positionPanel(e, t) {
542
+ const r = t.getBoundingClientRect();
543
+ e.style.position = "fixed", e.style.top = `${r.bottom + 4}px`, e.style.left = `${r.left}px`, requestAnimationFrame(() => {
544
+ const l = e.getBoundingClientRect();
545
+ l.right > window.innerWidth - 8 && (e.style.left = `${window.innerWidth - l.width - 8}px`), l.bottom > window.innerHeight - 8 && (e.style.top = `${r.top - l.height - 4}px`);
546
+ });
547
+ }
548
+ /**
549
+ * Render the default filter panel content
550
+ */
551
+ renderDefaultFilterPanel(e, t, r, l) {
552
+ const { field: s } = t, i = document.createElement("div");
553
+ i.className = "tbw-filter-search";
554
+ const n = document.createElement("input");
555
+ n.type = "text", n.placeholder = "Search...", n.className = "tbw-filter-search-input", n.value = this.searchText.get(s) ?? "", i.appendChild(n), e.appendChild(i);
556
+ const u = document.createElement("div");
557
+ u.className = "tbw-filter-actions";
558
+ const g = document.createElement("label");
559
+ g.className = "tbw-filter-value-item", g.style.padding = "0", g.style.margin = "0";
560
+ const p = document.createElement("input");
561
+ p.type = "checkbox", p.className = "tbw-filter-checkbox";
562
+ const m = document.createElement("span");
563
+ m.textContent = "Select All", g.appendChild(p), g.appendChild(m), u.appendChild(g);
564
+ const k = () => {
565
+ const a = [...x.values()], d = a.every((c) => c), h = a.every((c) => !c);
566
+ p.checked = d, p.indeterminate = !d && !h;
567
+ };
568
+ p.addEventListener("change", () => {
569
+ const a = p.checked;
570
+ for (const d of x.keys())
571
+ x.set(d, a);
572
+ k(), F();
573
+ }), e.appendChild(u);
574
+ const w = document.createElement("div");
575
+ w.className = "tbw-filter-values";
576
+ const E = document.createElement("div");
577
+ E.className = "tbw-filter-values-spacer", w.appendChild(E);
578
+ const b = document.createElement("div");
579
+ b.className = "tbw-filter-values-content", w.appendChild(b);
580
+ const x = /* @__PURE__ */ new Map();
581
+ r.forEach((a) => {
582
+ const d = a == null ? "__null__" : String(a);
583
+ x.set(d, !l.has(a));
584
+ }), k();
585
+ let C = [];
586
+ const H = (a, d) => {
587
+ const h = a == null ? "(Blank)" : String(a), c = a == null ? "__null__" : String(a), o = document.createElement("label");
588
+ o.className = "tbw-filter-value-item", o.style.position = "absolute", o.style.top = `${d * y.LIST_ITEM_HEIGHT}px`, o.style.left = "0", o.style.right = "0", o.style.height = `${y.LIST_ITEM_HEIGHT}px`, o.style.boxSizing = "border-box";
589
+ const v = document.createElement("input");
590
+ v.type = "checkbox", v.className = "tbw-filter-checkbox", v.checked = x.get(c) ?? !0, v.dataset.value = c, v.addEventListener("change", () => {
591
+ x.set(c, v.checked), k();
592
+ });
593
+ const N = document.createElement("span");
594
+ return N.textContent = h, o.appendChild(v), o.appendChild(N), o;
595
+ }, F = () => {
596
+ const a = C.length, d = w.clientHeight, h = w.scrollTop;
597
+ if (E.style.height = `${a * y.LIST_ITEM_HEIGHT}px`, V(a, y.LIST_BYPASS_THRESHOLD / 3)) {
598
+ b.innerHTML = "", b.style.transform = "translateY(0px)", C.forEach((o, v) => {
599
+ b.appendChild(H(o, v));
600
+ });
601
+ return;
602
+ }
603
+ const c = A({
604
+ totalRows: a,
605
+ viewportHeight: d,
606
+ scrollTop: h,
607
+ rowHeight: y.LIST_ITEM_HEIGHT,
608
+ overscan: y.LIST_OVERSCAN
609
+ });
610
+ b.style.transform = `translateY(${c.offsetY}px)`, b.innerHTML = "";
611
+ for (let o = c.start; o < c.end; o++)
612
+ b.appendChild(H(C[o], o - c.start));
613
+ }, I = (a) => {
614
+ const d = a.toLowerCase();
615
+ if (C = r.filter((h) => {
616
+ const c = h == null ? "(Blank)" : String(h);
617
+ return !a || c.toLowerCase().includes(d);
618
+ }), C.length === 0) {
619
+ E.style.height = "0px", b.innerHTML = "";
620
+ const h = document.createElement("div");
621
+ h.className = "tbw-filter-no-match", h.textContent = "No matching values", b.appendChild(h);
622
+ return;
623
+ }
624
+ F();
625
+ };
626
+ w.addEventListener(
627
+ "scroll",
628
+ () => {
629
+ C.length > 0 && F();
630
+ },
631
+ { passive: !0 }
632
+ ), I(n.value), e.appendChild(w);
633
+ let L;
634
+ n.addEventListener("input", () => {
635
+ clearTimeout(L), L = setTimeout(() => {
636
+ this.searchText.set(s, n.value), I(n.value);
637
+ }, this.config.debounceMs ?? 150);
638
+ });
639
+ const R = document.createElement("div");
640
+ R.className = "tbw-filter-buttons";
641
+ const S = document.createElement("button");
642
+ S.className = "tbw-filter-apply-btn", S.textContent = "Apply", S.addEventListener("click", () => {
643
+ const a = [];
644
+ for (const [d, h] of x)
645
+ if (!h)
646
+ if (d === "__null__")
647
+ a.push(null);
648
+ else {
649
+ const c = r.find((o) => String(o) === d);
650
+ a.push(c !== void 0 ? c : d);
651
+ }
652
+ t.applySetFilter(a);
653
+ }), R.appendChild(S);
654
+ const T = document.createElement("button");
655
+ T.className = "tbw-filter-clear-btn", T.textContent = "Clear Filter", T.addEventListener("click", () => {
656
+ t.clearFilter();
657
+ }), R.appendChild(T), e.appendChild(R);
658
+ }
659
+ /**
660
+ * Apply a set filter (exclude values)
661
+ */
662
+ applySetFilter(e, t) {
663
+ this.excludedValues.set(e, new Set(t)), t.length === 0 ? this.filters.delete(e) : this.filters.set(e, {
664
+ field: e,
665
+ type: "set",
666
+ operator: "notIn",
667
+ value: t
668
+ }), this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
669
+ filters: [...this.filters.values()],
670
+ filteredRowCount: 0
671
+ }), this.requestRender();
672
+ }
673
+ /**
674
+ * Apply a text filter
675
+ */
676
+ applyTextFilter(e, t, r, l) {
677
+ this.filters.set(e, {
678
+ field: e,
679
+ type: "text",
680
+ operator: t,
681
+ value: r,
682
+ valueTo: l
683
+ }), this.cachedResult = null, this.cacheKey = null, this.emit("filter-change", {
684
+ filters: [...this.filters.values()],
685
+ filteredRowCount: 0
686
+ }), this.requestRender();
687
+ }
688
+ // ===== Column State Hooks =====
689
+ /**
690
+ * Return filter state for a column if it has an active filter.
691
+ */
692
+ getColumnState(e) {
693
+ const t = this.filters.get(e);
694
+ if (t)
695
+ return {
696
+ filter: {
697
+ type: t.type,
698
+ operator: t.operator,
699
+ value: t.value,
700
+ valueTo: t.valueTo
701
+ }
702
+ };
703
+ }
704
+ /**
705
+ * Apply filter state from column state.
706
+ */
707
+ applyColumnState(e, t) {
708
+ if (!t.filter) {
709
+ this.filters.delete(e);
710
+ return;
711
+ }
712
+ const r = {
713
+ field: e,
714
+ type: t.filter.type,
715
+ operator: t.filter.operator,
716
+ value: t.filter.value,
717
+ valueTo: t.filter.valueTo
718
+ };
719
+ this.filters.set(e, r), this.cachedResult = null, this.cacheKey = null;
720
+ }
721
+ // ===== Styles =====
722
+ styles = `
723
+ .header-cell.filtered::before {
724
+ content: '';
725
+ position: absolute;
726
+ top: 4px;
727
+ right: 4px;
728
+ width: 6px;
729
+ height: 6px;
730
+ background: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
731
+ border-radius: 50%;
732
+ }
733
+ .tbw-filter-btn {
734
+ display: inline-flex;
735
+ align-items: center;
736
+ justify-content: center;
737
+ background: transparent;
738
+ border: none;
739
+ cursor: pointer;
740
+ padding: 2px;
741
+ margin-left: 4px;
742
+ opacity: 0.4;
743
+ transition: opacity 0.15s;
744
+ color: inherit;
745
+ vertical-align: middle;
746
+ }
747
+ .tbw-filter-btn:hover,
748
+ .tbw-filter-btn.active {
749
+ opacity: 1;
750
+ }
751
+ .tbw-filter-btn.active {
752
+ color: var(--tbw-filter-accent, var(--tbw-color-accent, #3b82f6));
753
+ }
754
+ `;
755
+ }
756
+ export {
757
+ y as FilteringPlugin
758
+ };
759
+ //# sourceMappingURL=index.js.map