@timlassiter11/yatl 0.3.21 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1868 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+
12
+ // src/events.ts
13
+ var YatlEvent = class extends CustomEvent {
14
+ constructor(name, detail, options = {}) {
15
+ super(name, {
16
+ bubbles: true,
17
+ composed: true,
18
+ cancelable: false,
19
+ ...options,
20
+ detail
21
+ });
22
+ }
23
+ };
24
+ var _YatlRowClickEvent = class _YatlRowClickEvent extends YatlEvent {
25
+ constructor(row, index, field, originalEvent) {
26
+ super(_YatlRowClickEvent.EVENT_NAME, {
27
+ row,
28
+ index,
29
+ field,
30
+ originalEvent
31
+ });
32
+ }
33
+ };
34
+ _YatlRowClickEvent.EVENT_NAME = "yatl-row-click";
35
+ var YatlRowClickEvent = _YatlRowClickEvent;
36
+ var _YatlChangeEvent = class _YatlChangeEvent extends YatlEvent {
37
+ constructor(data) {
38
+ super(_YatlChangeEvent.EVENT_NAME, { data });
39
+ }
40
+ };
41
+ _YatlChangeEvent.EVENT_NAME = "yatl-change";
42
+ var YatlChangeEvent = _YatlChangeEvent;
43
+ var _YatlSortEvent = class _YatlSortEvent extends YatlEvent {
44
+ constructor(field, order) {
45
+ super(
46
+ _YatlSortEvent.EVENT_NAME,
47
+ {
48
+ field,
49
+ order
50
+ },
51
+ {
52
+ cancelable: true
53
+ }
54
+ );
55
+ }
56
+ };
57
+ _YatlSortEvent.EVENT_NAME = "yatl-sort";
58
+ var YatlSortEvent = _YatlSortEvent;
59
+ var _YatlColumnToggleEvent = class _YatlColumnToggleEvent extends YatlEvent {
60
+ constructor(field, visible) {
61
+ super(
62
+ _YatlColumnToggleEvent.EVENT_NAME,
63
+ {
64
+ field,
65
+ visible
66
+ },
67
+ {
68
+ cancelable: true
69
+ }
70
+ );
71
+ }
72
+ };
73
+ _YatlColumnToggleEvent.EVENT_NAME = "yatl-column-toggle";
74
+ var YatlColumnToggleEvent = _YatlColumnToggleEvent;
75
+ var _YatlColumnResizeEvent = class _YatlColumnResizeEvent extends YatlEvent {
76
+ constructor(field, width) {
77
+ super(_YatlColumnResizeEvent.EVENT_NAME, {
78
+ field,
79
+ width
80
+ });
81
+ }
82
+ };
83
+ _YatlColumnResizeEvent.EVENT_NAME = "yatl-column-resize";
84
+ var YatlColumnResizeEvent = _YatlColumnResizeEvent;
85
+ var _YatlColumnReorderEvent = class _YatlColumnReorderEvent extends YatlEvent {
86
+ constructor(draggedColumn, droppedColumn, order) {
87
+ super(
88
+ _YatlColumnReorderEvent.EVENT_NAME,
89
+ {
90
+ draggedColumn,
91
+ droppedColumn,
92
+ order
93
+ },
94
+ {
95
+ cancelable: true
96
+ }
97
+ );
98
+ }
99
+ };
100
+ _YatlColumnReorderEvent.EVENT_NAME = "yatl-column-reorder";
101
+ var YatlColumnReorderEvent = _YatlColumnReorderEvent;
102
+ var _YatlSearchEvent = class _YatlSearchEvent extends YatlEvent {
103
+ constructor(query2) {
104
+ super(_YatlSearchEvent.EVENT_NAME, { query: query2 });
105
+ }
106
+ };
107
+ _YatlSearchEvent.EVENT_NAME = "yatl-search";
108
+ var YatlSearchEvent = _YatlSearchEvent;
109
+
110
+ // src/utils.ts
111
+ import { html } from "lit";
112
+ var toHumanReadable = (str) => {
113
+ return str.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (char) => char.toUpperCase());
114
+ };
115
+ var createRegexTokenizer = (exp = "\\S+") => {
116
+ const regex = new RegExp(`"[^"]*"|${exp}`, "g");
117
+ return (value) => {
118
+ const matches = value.match(regex) || [];
119
+ return matches.map((token) => {
120
+ token = token.toLocaleLowerCase().trim();
121
+ if (token.startsWith('"') && token.endsWith('"')) {
122
+ return { value: token.slice(1, -1), quoted: true };
123
+ }
124
+ return { value: token, quoted: false };
125
+ });
126
+ };
127
+ };
128
+ var whitespaceTokenizer = createRegexTokenizer();
129
+ function isValidKey(key, obj) {
130
+ return key in obj;
131
+ }
132
+ function getNestedValue(obj, path) {
133
+ const keys = path.split(".");
134
+ let current = obj;
135
+ for (const key of keys) {
136
+ if (current && isValidKey(key, current)) {
137
+ current = current[key];
138
+ } else {
139
+ return void 0;
140
+ }
141
+ }
142
+ return current;
143
+ }
144
+ function findColumn(field, columns) {
145
+ return columns.find((c) => c.field === field);
146
+ }
147
+ function highlightText(text, ranges) {
148
+ if (!text || !ranges || ranges.length === 0) {
149
+ return text;
150
+ }
151
+ const sortedRanges = [...ranges].sort((a, b) => a[0] - b[0]);
152
+ const mergedRanges = [];
153
+ let currentRange = sortedRanges[0];
154
+ for (let i = 1; i < sortedRanges.length; i++) {
155
+ const nextRange = sortedRanges[i];
156
+ if (nextRange[0] < currentRange[1]) {
157
+ currentRange[1] = Math.max(currentRange[1], nextRange[1]);
158
+ } else {
159
+ mergedRanges.push(currentRange);
160
+ currentRange = nextRange;
161
+ }
162
+ }
163
+ mergedRanges.push(currentRange);
164
+ const result = [];
165
+ let lastIndex = 0;
166
+ for (const [start, end] of mergedRanges) {
167
+ const safeStart = Math.max(0, Math.min(start, text.length));
168
+ const safeEnd = Math.max(0, Math.min(end, text.length));
169
+ if (safeStart > lastIndex) {
170
+ result.push(text.slice(lastIndex, safeStart));
171
+ }
172
+ result.push(
173
+ html`<mark class="highlight">${text.slice(safeStart, safeEnd)}</mark>`
174
+ );
175
+ lastIndex = safeEnd;
176
+ }
177
+ if (lastIndex < text.length) {
178
+ result.push(text.slice(lastIndex));
179
+ }
180
+ return html`${result}`;
181
+ }
182
+ function widthsToGridTemplates(widths, defaultWidth = "1fr") {
183
+ return widths.map((width) => width ? `${width}px` : defaultWidth);
184
+ }
185
+ function didSortStateChange(newState, oldState) {
186
+ if (!oldState) {
187
+ return true;
188
+ }
189
+ const allKeys = /* @__PURE__ */ new Set([
190
+ ...oldState.map((s) => s.field),
191
+ ...newState.map((s) => s.field)
192
+ ]);
193
+ for (const key of allKeys) {
194
+ const oldSort = findColumn(key, oldState)?.sortState;
195
+ const newSort = findColumn(key, newState)?.sortState;
196
+ if (oldSort?.order !== newSort?.order || oldSort?.priority !== newSort?.priority) {
197
+ return true;
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+ function isCompareable(value) {
203
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
204
+ }
205
+
206
+ // src/yatl-table.ts
207
+ import { html as html2, LitElement, nothing } from "lit";
208
+ import { customElement, property, query, state } from "lit/decorators.js";
209
+ import { classMap } from "lit/directives/class-map.js";
210
+ import { ifDefined } from "lit/directives/if-defined.js";
211
+ import { repeat } from "lit/directives/repeat.js";
212
+ import { styleMap } from "lit/directives/style-map.js";
213
+ import "@lit-labs/virtualizer";
214
+
215
+ // src/yatl-table.styles.ts
216
+ import { css } from "lit";
217
+ var yatl_table_styles_default = css`
218
+ /* Style declarations */
219
+ :host {
220
+ /* Typography */
221
+ --yatl-font-family: var(
222
+ --yatl-table-font,
223
+ -apple-system,
224
+ BlinkMacSystemFont,
225
+ 'Segoe UI',
226
+ Roboto,
227
+ Helvetica,
228
+ Arial,
229
+ sans-serif,
230
+ 'Apple Color Emoji',
231
+ 'Segoe UI Emoji'
232
+ );
233
+ --yatl-font-size: var(--yatl-table-font-size, 0.875rem);
234
+ --yatl-line-height: var(--yatl-table-line-height, 1.25rem);
235
+
236
+ /* Spacing */
237
+ --yatl-cell-padding: var(--yatl-table-cell-padding, 10px 16px);
238
+ --yatl-header-padding: var(--yatl-table-header-padding, 12px 16px);
239
+
240
+ /* Colors */
241
+ --yatl-bg: var(--yatl-table-bg, #ffffff);
242
+ --yatl-text: var(--yatl-table-text, #0f172a);
243
+ --yatl-text-muted: var(--yatl-table-text-muted, #64748b);
244
+ --yatl-border-color: var(--yatl-table-border-color, #e2e8f0);
245
+
246
+ --yatl-header-bg: var(--yatl-table-header-bg, #f8fafc);
247
+ --yatl-header-text: var(--yatl-table-header-text, #475569);
248
+
249
+ --yatl-row-hover-bg: var(--yatl-table-row-hover-bg, #f1f5f9);
250
+ --yatl-row-selected-bg: var(--yatl-table-row-selected-bg, #e0f2fe);
251
+
252
+ /* Resize grab handle width */
253
+ --yatl-resizer-width: 10px;
254
+ /* z-index for the header */
255
+ --header-z-index: 2;
256
+ /* Drop target background color */
257
+ --header-drop-color: rgba(255, 255, 255, 0.1);
258
+
259
+ font-family: var(--yatl-font-family);
260
+ font-size: var(--yatl-font-size);
261
+ color: var(--yatl-text);
262
+ }
263
+
264
+ :host(.dark) {
265
+ --yatl-table-bg: #1e293b;
266
+ --yatl-table-text: #f1f5f9;
267
+ --yatl-table-text-muted: #94a3b8;
268
+ --yatl-table-border-color: #334155;
269
+
270
+ --yatl-table-header-bg: #0f172a;
271
+ --yatl-table-header-text: #cbd5e1;
272
+
273
+ --yatl-table-row-hover-bg: #334155;
274
+ --yatl-table-row-selected-bg: #1e3a8a;
275
+ }
276
+
277
+ @media (prefers-color-scheme: dark) {
278
+ :host {
279
+ --yatl-bg: var(--yatl-table-bg, #1e293b);
280
+ --yatl-text: var(--yatl-table-text, #f1f5f9);
281
+ --yatl-text-muted: var(--yatl-table-text-muted, #94a3b8);
282
+ --yatl-border-color: var(--yatl-table-border-color, #334155);
283
+
284
+ --yatl-header-bg: var(--yatl-table-header-bg, #0f172a);
285
+ --yatl-header-text: var(--yatl-table-header-text, #cbd5e1);
286
+
287
+ --yatl-row-hover-bg: var(--yatl-table-row-hover-bg, #334155);
288
+ --yatl-row-selected-bg: var(--yatl-table-row-selected-bg, #1e3a8a);
289
+ }
290
+ }
291
+
292
+ :host {
293
+ font-family: system-ui, sans-serif;
294
+ }
295
+
296
+ .table {
297
+ background-color: var(--yatl-bg);
298
+ border: 1px solid var(--yatl-border-color);
299
+ border-radius: 6px;
300
+ }
301
+
302
+ .header.row {
303
+ background-color: var(--yatl-header-bg);
304
+ border-bottom: 1px solid var(--yatl-border-color);
305
+ font-weight: 600;
306
+ color: var(--yatl-header-text);
307
+ }
308
+
309
+ .row {
310
+ background-color: var(--yatl-bg);
311
+ border-bottom: 1px solid var(--yatl-border-color);
312
+ transition: background-color 50ms;
313
+ position: relative;
314
+ }
315
+
316
+ .row:last-child {
317
+ border-bottom: none;
318
+ }
319
+
320
+ .header .cell::after,
321
+ .row:not(.header)::after {
322
+ content: '';
323
+ position: absolute;
324
+ inset: 0;
325
+ pointer-events: none;
326
+ background-color: transparent;
327
+ transition: background-color 50ms;
328
+ z-index: 1;
329
+ }
330
+
331
+ .header .cell:hover::after,
332
+ .row:not(.header):hover::after {
333
+ background-color: rgba(0, 0, 0, 0.2);
334
+ }
335
+
336
+ .cell {
337
+ align-items: center;
338
+ padding: var(--yatl-cell-padding);
339
+ }
340
+
341
+ .header .cell {
342
+ padding: var(--yatl-header-padding);
343
+ }
344
+
345
+ .footer {
346
+ padding: 8px 12px;
347
+ background-color: var(--yatl-header-bg);
348
+ border-top: 1px solid var(--yatl-border-color);
349
+ color: var(--yatl-text-muted);
350
+ font-size: 0.8em;
351
+ }
352
+
353
+ .resizer::after {
354
+ height: 60%;
355
+ width: 1px;
356
+ background-color: color-mix(in srgb, currentColor 30%, transparent);
357
+ transition: background-color 0.2s;
358
+ }
359
+
360
+ .resizer:hover::after {
361
+ background-color: currentColor;
362
+ width: 2px;
363
+ }
364
+
365
+ .drop-indicator {
366
+ background: rgba(0, 0, 0, 0.4);
367
+ }
368
+
369
+ .message {
370
+ font-size: large;
371
+ }
372
+
373
+ /* Layout stuff
374
+ * Most of this is functional and needed
375
+ * for the table to work properly.
376
+ * Modify with caution!
377
+ */
378
+ :host {
379
+ display: block;
380
+ height: 100%;
381
+ width: 100%;
382
+ }
383
+
384
+ .table {
385
+ display: flex;
386
+ flex-direction: column;
387
+ height: 100%;
388
+ width: 100%;
389
+ min-height: 0;
390
+ overflow: auto;
391
+ box-sizing: border-box;
392
+ }
393
+
394
+ .header {
395
+ z-index: var(--header-z-index);
396
+ flex-shrink: 0;
397
+ position: sticky;
398
+ top: 0;
399
+ }
400
+
401
+ .header-content {
402
+ position: relative;
403
+ width: 100%;
404
+ display: flex;
405
+ flex-direction: row;
406
+ align-items: baseline;
407
+ gap: 0.5rem;
408
+ }
409
+
410
+ .sort-icon {
411
+ position: relative;
412
+ width: 1ch;
413
+ align-self: stretch;
414
+ padding: 0;
415
+ overflow: hidden;
416
+ flex-shrink: 0;
417
+ }
418
+
419
+ .sort-icon::after {
420
+ content: '';
421
+ position: absolute;
422
+ }
423
+
424
+ .sort-icon.descending::after {
425
+ content: '\\2191';
426
+ }
427
+
428
+ .sort-icon.ascending::after {
429
+ content: '\\2193';
430
+ }
431
+
432
+ .resizer {
433
+ position: absolute;
434
+ top: 0;
435
+ bottom: 0;
436
+ right: 0;
437
+ width: var(--yatl-resizer-width);
438
+ cursor: col-resize;
439
+ display: flex;
440
+ justify-content: center;
441
+ align-items: center;
442
+ }
443
+
444
+ .resizer::after {
445
+ content: '';
446
+ display: block;
447
+ }
448
+
449
+ .drop-indicator {
450
+ display: none;
451
+ position: absolute;
452
+ top: 0;
453
+ left: 0;
454
+ right: 0;
455
+ bottom: 0;
456
+ pointer-events: none;
457
+ z-index: calc(var(--header-z-index) + 1);
458
+ }
459
+
460
+ .drop-indicator.active {
461
+ display: block;
462
+ }
463
+
464
+ .sortable {
465
+ cursor: pointer;
466
+ }
467
+
468
+ /* Footer */
469
+ .footer {
470
+ display: flex;
471
+ align-items: center;
472
+ justify-content: space-between;
473
+ flex-shrink: 0;
474
+
475
+ position: sticky;
476
+ bottom: 0;
477
+ z-index: var(--header-z-index);
478
+ }
479
+
480
+ /* Generic table parts */
481
+ .row {
482
+ display: grid;
483
+ grid-template-columns: var(--grid-template);
484
+ min-width: 100%;
485
+ width: fit-content;
486
+ }
487
+
488
+ .cell {
489
+ white-space: nowrap;
490
+ overflow: hidden;
491
+ text-overflow: ellipsis;
492
+ position: relative;
493
+ display: flex;
494
+ align-items: center;
495
+ }
496
+
497
+ .message {
498
+ width: 100%;
499
+ height: 100%;
500
+ text-align: center;
501
+ pointer-events: none;
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: center;
505
+ }
506
+
507
+ .truncate {
508
+ display: block;
509
+ white-space: nowrap;
510
+ overflow: hidden;
511
+ text-overflow: ellipsis;
512
+ }
513
+ `;
514
+
515
+ // src/yatl-table.ts
516
+ var STATE_SAVE_DEBOUNCE = 1e3;
517
+ var DEFAULT_STORAGE_OPTIONS = {
518
+ storage: "local",
519
+ saveColumnSortOrders: true,
520
+ saveColumnVisibility: true,
521
+ saveColumnWidths: true,
522
+ saveColumnOrder: true
523
+ };
524
+ var SAVE_TRIGGERS = /* @__PURE__ */ new Set([
525
+ "searchQuery",
526
+ "filters",
527
+ // Covers column order
528
+ "columns",
529
+ // Covers sort, visibility, and width
530
+ "columnStates",
531
+ "storageOptions"
532
+ ]);
533
+ var MATCH_WEIGHTS = {
534
+ EXACT: 100,
535
+ PREFIX: 50,
536
+ SUBSTRING: 10
537
+ };
538
+ var YatlTable = class extends LitElement {
539
+ constructor() {
540
+ super(...arguments);
541
+ // #region --- State Data ---
542
+ // Property data
543
+ this._enableSearchTokenization = false;
544
+ this._enableSearchScoring = false;
545
+ this._columns = [];
546
+ this._columnStates = [];
547
+ this._storageOptions = null;
548
+ this._data = [];
549
+ this._searchQuery = "";
550
+ this._searchIncludedFields = [];
551
+ this._searchTokenizer = whitespaceTokenizer;
552
+ this._filters = null;
553
+ this._filteredData = [];
554
+ // Flag if we have already restored the state or not.
555
+ this.hasRestoredState = false;
556
+ // save state debounce timer
557
+ this.saveTimer = 0;
558
+ // Flags set when something changes that
559
+ // requires the filter or sort logic to re-run.
560
+ this.filterDirty = false;
561
+ this.sortDirty = false;
562
+ // The last time the data was updated.
563
+ // For displaying in the footer only.
564
+ this.dataLastUpdate = null;
565
+ // Maps rows to their metadata
566
+ this.rowMetadata = /* @__PURE__ */ new WeakMap();
567
+ // List of tokens created from the current query
568
+ this.queryTokens = null;
569
+ // Column resize state
570
+ this.resizeState = null;
571
+ // Column drag & drop state
572
+ this.dragColumn = null;
573
+ this.enableVirtualScroll = false;
574
+ this.enableSearchHighlight = true;
575
+ this.enableColumnReorder = true;
576
+ this.enableFooter = false;
577
+ this.nullValuePlaceholder = "-";
578
+ this.emptyMessage = "No records to display";
579
+ this.noResultsMessage = "No matching records found";
580
+ this.rowParts = null;
581
+ // #endregion
582
+ // #region --- Event Handlers ---
583
+ this.handleHeaderClicked = (event, column) => {
584
+ const target = event.target;
585
+ if (!column.sortable || target.classList.contains("resizer")) {
586
+ return;
587
+ }
588
+ const multiSort = event.shiftKey;
589
+ const state2 = findColumn(column.field, this._columnStates);
590
+ if (!state2?.sortState) {
591
+ this.sort(column.field, "asc", !multiSort);
592
+ } else if (state2.sortState.order === "asc") {
593
+ this.sort(column.field, "desc", !multiSort);
594
+ } else if (state2.sortState.order) {
595
+ this.sort(column.field, null, !multiSort);
596
+ }
597
+ };
598
+ this.handleCellClick = (event, row, field) => {
599
+ if (window.getSelection()?.toString()) return;
600
+ const rowIndex = this.rowMetadata.get(row).index;
601
+ this.dispatchEvent(new YatlRowClickEvent(row, rowIndex, field, event));
602
+ };
603
+ this.handleResizeMouseMove = (event) => {
604
+ if (!this.resizeState?.active) return;
605
+ requestAnimationFrame(() => {
606
+ if (!this.resizeState?.active) return;
607
+ const deltaX = event.pageX - this.resizeState.startX;
608
+ const newWidth = Math.max(50, this.resizeState.startWidth + deltaX);
609
+ this.resizeState.currentWidths[this.resizeState.columnIndex] = `${newWidth}px`;
610
+ this.tableElement.style.setProperty(
611
+ "--grid-template",
612
+ this.resizeState.currentWidths.join(" ")
613
+ );
614
+ });
615
+ };
616
+ this.handleResizeMouseUp = (_event) => {
617
+ window.removeEventListener("mousemove", this.handleResizeMouseMove);
618
+ window.removeEventListener("mouseup", this.handleResizeMouseUp);
619
+ document.body.style.cursor = "";
620
+ if (this.resizeState?.active) {
621
+ const finalWidth = parseFloat(
622
+ this.resizeState.currentWidths[this.resizeState.columnIndex]
623
+ );
624
+ const columnStates = this.columnStates;
625
+ const state2 = findColumn(this.resizeState.columnField, columnStates);
626
+ state2.width = finalWidth;
627
+ this.columnStates = columnStates;
628
+ this.dispatchEvent(new YatlColumnResizeEvent(state2.field, state2.width));
629
+ }
630
+ this.resizeState = null;
631
+ };
632
+ this.handleDragColumnStart = (event, field) => {
633
+ const target = event.target;
634
+ console.log("Starting drag event");
635
+ console.log(target);
636
+ if (target?.classList.contains("resizer")) {
637
+ event.preventDefault();
638
+ return;
639
+ }
640
+ if (event.dataTransfer) {
641
+ event.dataTransfer.effectAllowed = "move";
642
+ event.dataTransfer.setData("text/plain", field);
643
+ this.dragColumn = field;
644
+ }
645
+ };
646
+ this.handleDragColumnEnter = (event) => {
647
+ const cell = event.currentTarget;
648
+ cell.querySelector(".drop-indicator")?.classList.add("active");
649
+ };
650
+ this.handleDragColumnLeave = (event) => {
651
+ const cell = event.currentTarget;
652
+ const enteringElement = event.relatedTarget;
653
+ if (cell.contains(enteringElement)) {
654
+ return;
655
+ }
656
+ cell.querySelector(".drop-indicator")?.classList.remove("active");
657
+ };
658
+ this.handleDragColumnOver = (event) => {
659
+ event.preventDefault();
660
+ if (event.dataTransfer) {
661
+ event.dataTransfer.dropEffect = "move";
662
+ }
663
+ };
664
+ this.handleDragColumnDrop = (event, field) => {
665
+ if (!this.dragColumn || this.dragColumn === field) {
666
+ return;
667
+ }
668
+ event.preventDefault();
669
+ event.stopPropagation();
670
+ const columns = [...this.columns];
671
+ const dragIndex = columns.findIndex((col) => col.field === this.dragColumn);
672
+ const dropIndex = columns.findIndex((col) => col.field === field);
673
+ if (dragIndex > -1 && dropIndex > -1) {
674
+ const [draggedColumn] = columns.splice(dragIndex, 1);
675
+ const droppedColumn = findColumn(field, this.columns);
676
+ if (!droppedColumn) return;
677
+ columns.splice(dropIndex, 0, draggedColumn);
678
+ const newColumnOrder = columns.map((col) => col.field);
679
+ const reorderEvent = new YatlColumnReorderEvent(
680
+ draggedColumn.field,
681
+ droppedColumn.field,
682
+ newColumnOrder
683
+ );
684
+ if (!this.dispatchEvent(reorderEvent)) {
685
+ return;
686
+ }
687
+ this.setColumnOrder(newColumnOrder);
688
+ }
689
+ };
690
+ this.handleDragColumnEnd = () => {
691
+ this.dragColumn = null;
692
+ this.tableElement.querySelectorAll(".drop-indicator.active").forEach((element) => element.classList.remove("active"));
693
+ };
694
+ }
695
+ get enableSearchTokenization() {
696
+ return this._enableSearchTokenization;
697
+ }
698
+ set enableSearchTokenization(enable) {
699
+ if (this._enableSearchTokenization === enable) {
700
+ return;
701
+ }
702
+ const oldValue = this._enableSearchTokenization;
703
+ this._enableSearchTokenization = enable;
704
+ this.updateInternalQuery();
705
+ this.filterDirty = true;
706
+ this.requestUpdate("enableSearchTokenization", oldValue);
707
+ }
708
+ get enableSearchScoring() {
709
+ return this._enableSearchScoring;
710
+ }
711
+ set enableSearchScoring(enable) {
712
+ if (this._enableSearchScoring === enable) {
713
+ return;
714
+ }
715
+ const oldValue = this._enableSearchScoring;
716
+ this._enableSearchScoring = enable;
717
+ this.filterDirty = true;
718
+ this.requestUpdate("enableSearchScoring", oldValue);
719
+ }
720
+ get columns() {
721
+ return this._columns;
722
+ }
723
+ set columns(columns) {
724
+ if (this._columns === columns) {
725
+ return;
726
+ }
727
+ const oldValue = this._columns;
728
+ this._columns = columns;
729
+ this.createColumnStates();
730
+ this.filterDirty = true;
731
+ this.requestUpdate("columns", oldValue);
732
+ }
733
+ get columnStates() {
734
+ return this._columnStates.map((state2) => ({
735
+ ...state2,
736
+ sortState: state2.sortState ? { ...state2.sortState } : void 0
737
+ }));
738
+ }
739
+ set columnStates(states) {
740
+ if (this._columnStates === states) {
741
+ return;
742
+ }
743
+ const oldValue = this._columnStates;
744
+ this._columnStates = states;
745
+ if (didSortStateChange(this._columnStates, oldValue)) {
746
+ this.sortDirty = true;
747
+ }
748
+ this.requestUpdate("columnStates", oldValue);
749
+ }
750
+ get searchQuery() {
751
+ return this._searchQuery;
752
+ }
753
+ set searchQuery(query2) {
754
+ if (this._searchQuery === query2) {
755
+ return;
756
+ }
757
+ const oldValue = this._searchQuery;
758
+ this._searchQuery = query2;
759
+ this.updateInternalQuery();
760
+ this.filterDirty = true;
761
+ this.requestUpdate("searchQuery", oldValue);
762
+ }
763
+ get searchIncludedFields() {
764
+ return this._searchIncludedFields;
765
+ }
766
+ set searchIncludedFields(fields) {
767
+ if (this._searchIncludedFields === fields) {
768
+ return;
769
+ }
770
+ const oldValue = this._searchIncludedFields;
771
+ this._searchIncludedFields = fields;
772
+ this.filterDirty = true;
773
+ this.requestUpdate("searchIncludedFields", oldValue);
774
+ }
775
+ get searchTokenizer() {
776
+ return this._searchTokenizer;
777
+ }
778
+ set searchTokenizer(tokenizer) {
779
+ if (this._searchTokenizer === tokenizer) {
780
+ return;
781
+ }
782
+ const oldValue = this._searchTokenizer;
783
+ this._searchTokenizer = tokenizer;
784
+ this.filterDirty = true;
785
+ this.requestUpdate("searchTokenizer", oldValue);
786
+ }
787
+ get filters() {
788
+ return this._filters;
789
+ }
790
+ set filters(filters) {
791
+ if (this._filters === filters) {
792
+ return;
793
+ }
794
+ const oldValue = this._filters;
795
+ this._filters = filters;
796
+ this.filterDirty = true;
797
+ this.requestUpdate("filters", oldValue);
798
+ }
799
+ get storageOptions() {
800
+ return this._storageOptions;
801
+ }
802
+ set storageOptions(options) {
803
+ if (this._storageOptions === options) {
804
+ return;
805
+ }
806
+ const oldValue = this._storageOptions;
807
+ this._storageOptions = options;
808
+ if (!this.hasRestoredState) {
809
+ this.loadStateFromStorage();
810
+ }
811
+ this.requestUpdate("storageOptions", oldValue);
812
+ }
813
+ get data() {
814
+ return this._data;
815
+ }
816
+ set data(value) {
817
+ const oldValue = this._data;
818
+ this._data = value;
819
+ this.dataLastUpdate = /* @__PURE__ */ new Date();
820
+ this.createMetadata();
821
+ this.filterDirty = true;
822
+ this.requestUpdate("data", oldValue);
823
+ }
824
+ get filteredData() {
825
+ if (this.filterDirty) {
826
+ this.filterRows();
827
+ } else if (this.sortDirty) {
828
+ this.sortRows();
829
+ }
830
+ this.filterDirty = false;
831
+ this.sortDirty = false;
832
+ return [...this._filteredData];
833
+ }
834
+ // #endregion
835
+ // #region --- Public Methods ---
836
+ /**
837
+ * Gets a copy of the current state of the table.
838
+ */
839
+ getState() {
840
+ const states = this.columnStates;
841
+ return {
842
+ searchQuery: this.searchQuery,
843
+ filters: this.filters,
844
+ columnOrder: this.columns.map((column) => column.field),
845
+ columns: this.columns.map((column) => {
846
+ const columnState = findColumn(column.field, states);
847
+ return {
848
+ field: column.field,
849
+ visible: columnState?.visible ?? true,
850
+ sortState: columnState?.sortState,
851
+ width: columnState?.width
852
+ };
853
+ })
854
+ };
855
+ }
856
+ /**
857
+ * Restores the table to the provided state.
858
+ * @param state - The state to restore the table to.
859
+ */
860
+ restoreState(state2) {
861
+ if ("searchQuery" in state2 && state2.searchQuery !== void 0) {
862
+ this.searchQuery = state2.searchQuery;
863
+ }
864
+ if ("filters" in state2 && state2.filters !== void 0) {
865
+ this.filters = state2.filters;
866
+ }
867
+ if ("columnOrder" in state2 && state2.columnOrder !== void 0) {
868
+ this.setColumnOrder(state2.columnOrder);
869
+ }
870
+ if ("columns" in state2 && state2.columns !== void 0) {
871
+ const newColumnStates = [];
872
+ for (const newState of state2.columns) {
873
+ const currentState = findColumn(newState.field, this._columnStates) ?? {
874
+ field: newState.field,
875
+ visible: true
876
+ };
877
+ newColumnStates.push(currentState);
878
+ if (!newState) {
879
+ continue;
880
+ }
881
+ if ("visible" in newState && newState.visible !== void 0) {
882
+ currentState.visible = newState.visible;
883
+ }
884
+ if ("sortState" in newState && newState.sortState !== void 0) {
885
+ currentState.sortState = newState.sortState;
886
+ }
887
+ if ("width" in newState && newState.width !== void 0) {
888
+ currentState.width = newState.width;
889
+ }
890
+ }
891
+ this.columnStates = newColumnStates;
892
+ }
893
+ }
894
+ /**
895
+ * Sorts the table by a specified column and order.
896
+ * If `order` is `null`, the sort on this column is removed.
897
+ * @param field - The field name of the column to sort by.
898
+ * @param order - The sort order: 'asc', 'desc', or `null` to remove sorting for this column.
899
+ * @param clear - Clear all other sorting
900
+ */
901
+ sort(field, order, clear = true) {
902
+ const columnStates = this.columnStates;
903
+ const state2 = findColumn(field, columnStates);
904
+ if (!state2) {
905
+ throw new Error(`Cannot get options for non-existent column "${field}"`);
906
+ }
907
+ if (order === state2.sortState?.order) {
908
+ return;
909
+ }
910
+ if (!this.dispatchEvent(new YatlSortEvent(field, order))) {
911
+ return;
912
+ }
913
+ if (order && !state2.sortState) {
914
+ const priorities = columnStates.map((col) => col.sortState?.priority).filter((priority2) => priority2 !== void 0);
915
+ const maxPriority = this.columns.length + 1;
916
+ const priority = Math.min(maxPriority, ...priorities) - 1;
917
+ state2.sortState = { order, priority };
918
+ } else if (order && state2.sortState) {
919
+ state2.sortState.order = order;
920
+ } else {
921
+ state2.sortState = null;
922
+ }
923
+ if (clear) {
924
+ for (const state3 of columnStates) {
925
+ if (state3.field !== field) {
926
+ state3.sortState = null;
927
+ }
928
+ }
929
+ }
930
+ this.columnStates = columnStates;
931
+ }
932
+ /**
933
+ * Sets the visibility of a specified column.
934
+ * @param field - The field name of the column.
935
+ * @param visible - `true` to show the column, `false` to hide it.
936
+ */
937
+ setColumnVisibility(field, visible) {
938
+ const columnStates = this.columnStates;
939
+ const state2 = findColumn(field, columnStates);
940
+ if (!state2) {
941
+ throw new Error(`Cannot get options for non-existent column "${field}"`);
942
+ }
943
+ if (state2.visible === visible) {
944
+ return;
945
+ }
946
+ if (!this.dispatchEvent(new YatlColumnToggleEvent(field, visible))) {
947
+ return;
948
+ }
949
+ state2.visible = visible;
950
+ this.columnStates = columnStates;
951
+ }
952
+ /**
953
+ * Toggles the visibility of hte specified column
954
+ * @param field - The field name of the column to toggle.
955
+ */
956
+ toggleColumnVisibility(field) {
957
+ const state2 = findColumn(field, this._columnStates);
958
+ this.setColumnVisibility(field, !state2);
959
+ }
960
+ /**
961
+ * Shows the specified column
962
+ * @param field - The field name of the column to show.
963
+ */
964
+ showColumn(field) {
965
+ this.setColumnVisibility(field, true);
966
+ }
967
+ /**
968
+ * Hides the specified column
969
+ * @param field - The field name of the column to hide.
970
+ */
971
+ hideColumn(field) {
972
+ this.setColumnVisibility(field, false);
973
+ }
974
+ /**
975
+ * Export the current visible table data to a CSV file.
976
+ * @param filename - The name of the file to save.
977
+ * @param all - If `true`, exports all original data (ignoring filters). If `false` (default), exports only the currently visible (filtered and sorted) rows.
978
+ */
979
+ export(filename, all = false) {
980
+ const data = all ? this.data : this.filteredData;
981
+ const rows = [...data.values()];
982
+ const columnData = this.columnData;
983
+ const csvHeaders = columnData.filter((col) => all || col.state?.visible).map((col) => `"${col.options.title}"`).join(",");
984
+ const csvRows = rows.map((row) => {
985
+ const list = [];
986
+ for (const col of columnData) {
987
+ let value = getNestedValue(row, col.field);
988
+ if (all || col.state.visible) {
989
+ if (typeof col.options.valueFormatter === "function") {
990
+ value = col.options.valueFormatter(value, row);
991
+ }
992
+ value = String(value).replace('"', '""');
993
+ list.push(`"${value}"`);
994
+ }
995
+ }
996
+ return list.join(",");
997
+ }).join("\n");
998
+ const csvContent = csvHeaders + "\n" + csvRows;
999
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8," });
1000
+ const a = document.createElement("a");
1001
+ a.style.display = "none";
1002
+ a.href = URL.createObjectURL(blob);
1003
+ a.download = `${filename}.csv`;
1004
+ document.body.append(a);
1005
+ a.click();
1006
+ a.remove();
1007
+ }
1008
+ scrollToRow(row) {
1009
+ const index = this.data.findIndex((v) => v === row);
1010
+ if (typeof index === "number") {
1011
+ return this.scrollToOriginalIndex(index);
1012
+ } else {
1013
+ throw new Error("Row not in table");
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Scrolls the table to bring the row at the specified original index into view.
1018
+ * @param index - The original index of the row (from the initial dataset).
1019
+ */
1020
+ scrollToOriginalIndex(index) {
1021
+ const rowData = this.data[index];
1022
+ if (rowData) {
1023
+ const filteredIndex = this.filteredData.indexOf(rowData);
1024
+ if (filteredIndex >= 0) {
1025
+ return this.scrollToFilteredIndex(filteredIndex);
1026
+ } else {
1027
+ throw new Error("Cannot scroll to filtered out row");
1028
+ }
1029
+ } else {
1030
+ throw new RangeError(`Row index ${index} out of range`);
1031
+ }
1032
+ }
1033
+ async scrollToFilteredIndex(index) {
1034
+ const rowData = this.filteredData[index];
1035
+ if (!rowData) {
1036
+ throw new RangeError(`Row index ${index} out of range`);
1037
+ }
1038
+ await this.updateComplete;
1039
+ if (this.virtualizer) {
1040
+ this.virtualizer.element(index)?.scrollIntoView({
1041
+ block: "start",
1042
+ behavior: "instant"
1043
+ });
1044
+ } else {
1045
+ const row = this.tableElement.querySelector(
1046
+ `.row[data-filtered-index="${index}"]`
1047
+ );
1048
+ row?.scrollIntoView({
1049
+ block: "start",
1050
+ behavior: "smooth"
1051
+ });
1052
+ }
1053
+ }
1054
+ async scrollToPx(px) {
1055
+ await this.updateComplete;
1056
+ if (this.virtualizer) {
1057
+ this.virtualizer.scrollTop = px;
1058
+ } else {
1059
+ this.tableElement.scrollTop = px;
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Sets the display order of the columns in the table.
1064
+ *
1065
+ * @param fields - An array of field names representing the new order of columns. Columns not included in the array will be placed at the end.
1066
+ * @throws {TypeError} If `fields` is not an array.
1067
+ */
1068
+ setColumnOrder(fields) {
1069
+ const newColumns = [];
1070
+ for (const field of fields) {
1071
+ const col = findColumn(field, this.columns);
1072
+ if (col) {
1073
+ newColumns.push(col);
1074
+ }
1075
+ }
1076
+ for (const col of this.columns) {
1077
+ if (!findColumn(col.field, newColumns)) {
1078
+ newColumns.push(col);
1079
+ }
1080
+ }
1081
+ this.columns = [...newColumns];
1082
+ }
1083
+ /**
1084
+ * Finds the first row
1085
+ * @param field
1086
+ * @param value
1087
+ * @returns
1088
+ */
1089
+ findRow(field, value) {
1090
+ return this.data.find((row) => {
1091
+ const rowValue = getNestedValue(row, field);
1092
+ return rowValue === value;
1093
+ });
1094
+ }
1095
+ /**
1096
+ * Finds the original index of the first row where the specified field matches the given value.
1097
+ * This searches through the original, unfiltered dataset.
1098
+ * @param field - The field name within the row data to search.
1099
+ * @param value - The value to match against the field's content.
1100
+ * @returns The original index of the found row, or -1 if no match is found.
1101
+ * @example
1102
+ * ```ts
1103
+ * const index = dataTable.indexOf('id', 12345);
1104
+ * if (index >= 0) {
1105
+ * dataTable.updateRow({description: "Updated description"}, index);
1106
+ * }
1107
+ * ```
1108
+ */
1109
+ findRowIndex(field, value) {
1110
+ const row = this.findRow(field, value);
1111
+ if (row) {
1112
+ return this.rowMetadata.get(row).index;
1113
+ }
1114
+ return -1;
1115
+ }
1116
+ /**
1117
+ * Updates the data of a row at a specific original index.
1118
+ * @param index - The original index of the row to update.
1119
+ * @param data - An object containing the new data to assign to the row. Existing fields will be updated, and new fields will be added.
1120
+ *
1121
+ * @example
1122
+ * ```ts
1123
+ * const index = dataTable.indexOf('id', 12345);
1124
+ * if (index >= 0) {
1125
+ * dataTable.updateRow(index, {description: "Updated description"});
1126
+ * }
1127
+ * ```
1128
+ */
1129
+ updateRow(index, data) {
1130
+ const current_row = this.data[index];
1131
+ if (current_row) {
1132
+ Object.assign(current_row, data);
1133
+ this.requestUpdate("data");
1134
+ }
1135
+ }
1136
+ /**
1137
+ * Deletes a row at a specific original index from the table.
1138
+ * @param index - The original index of the row to delete.
1139
+ */
1140
+ deleteRow(index) {
1141
+ this.data = this.data.toSpliced(index, 1);
1142
+ }
1143
+ // #endregion
1144
+ // #region --- Render Methods ---
1145
+ renderColumnSortIcon(column, state2) {
1146
+ return column.sortable ? html2`<div
1147
+ part="header-sort-icon"
1148
+ class=${classMap({
1149
+ "sort-icon": true,
1150
+ ascending: state2.sortState?.order === "asc",
1151
+ descending: state2.sortState?.order === "desc"
1152
+ })}
1153
+ ></div>` : nothing;
1154
+ }
1155
+ renderColumnResizer(column, _state) {
1156
+ return column.resizable ? html2`<div
1157
+ part="header-resizer"
1158
+ class="resizer"
1159
+ @click=${(event) => event.stopPropagation()}
1160
+ @mousedown=${(event) => this.handleResizeMouseDown(event, column.field)}
1161
+ ></div>` : nothing;
1162
+ }
1163
+ renderHeaderCell(column) {
1164
+ const state2 = findColumn(column.field, this._columnStates);
1165
+ if (state2.visible == false) {
1166
+ return nothing;
1167
+ }
1168
+ return html2`
1169
+ <div
1170
+ part="cell header-cell"
1171
+ class=${classMap({
1172
+ cell: true,
1173
+ sortable: column.sortable ?? false
1174
+ })}
1175
+ draggable=${ifDefined(this.enableColumnReorder ? true : void 0)}
1176
+ data-field=${column.field}
1177
+ @dragstart=${(event) => this.handleDragColumnStart(event, column.field)}
1178
+ @dragenter=${this.handleDragColumnEnter}
1179
+ @dragleave=${this.handleDragColumnLeave}
1180
+ @dragover=${this.handleDragColumnOver}
1181
+ @drop=${(event) => this.handleDragColumnDrop(event, column.field)}
1182
+ @dragend=${this.handleDragColumnEnd}
1183
+ @click=${(event) => this.handleHeaderClicked(event, column)}
1184
+ >
1185
+ <div class="header-content">
1186
+ <span class="header-title truncate" part="header-title">
1187
+ ${column.title ?? toHumanReadable(column.field)}
1188
+ </span>
1189
+ ${this.renderColumnSortIcon(column, state2)}
1190
+ </div>
1191
+ ${this.renderColumnResizer(column, state2)}
1192
+ <div part="drop-indicator" class="drop-indicator"></div>
1193
+ </div>
1194
+ `;
1195
+ }
1196
+ renderHeader() {
1197
+ return html2`
1198
+ <div part="header" class="header row">
1199
+ ${this.columns.map((column) => this.renderHeaderCell(column))}
1200
+ </div>
1201
+ `;
1202
+ }
1203
+ renderCellContents(value, column, row) {
1204
+ if (column.cellRenderer) {
1205
+ return column.cellRenderer(value, column.field, row);
1206
+ }
1207
+ if (value == null) {
1208
+ return this.nullValuePlaceholder;
1209
+ }
1210
+ const indices = this.rowMetadata.get(row).highlightIndices;
1211
+ return this.enableSearchHighlight && indices ? highlightText(String(value), indices[column.field]) : value;
1212
+ }
1213
+ renderCell(column, row) {
1214
+ const state2 = findColumn(column.field, this._columnStates);
1215
+ if (state2?.visible == false) {
1216
+ return nothing;
1217
+ }
1218
+ let value = getNestedValue(row, column.field);
1219
+ let userParts = column.cellParts?.call(this, value, column.field, row);
1220
+ if (Array.isArray(userParts)) {
1221
+ userParts = userParts.join(" ");
1222
+ }
1223
+ if (typeof column.valueFormatter === "function") {
1224
+ value = column.valueFormatter(value, row);
1225
+ }
1226
+ return html2`
1227
+ <div
1228
+ part="cell body-cell cell-${column.field} ${userParts}"
1229
+ data-field=${column.field}
1230
+ class="cell"
1231
+ title=${ifDefined(value ? String(value) : void 0)}
1232
+ @click=${(event) => this.handleCellClick(event, row, column.field)}
1233
+ >
1234
+ <span class="truncate">
1235
+ ${this.renderCellContents(value, column, row)}
1236
+ </span>
1237
+ </div>
1238
+ `;
1239
+ }
1240
+ renderRow(row, index) {
1241
+ const metadata = this.rowMetadata.get(row);
1242
+ let userParts = this.rowParts?.(row) ?? "";
1243
+ if (Array.isArray(userParts)) {
1244
+ userParts = userParts.join(" ");
1245
+ }
1246
+ return html2`
1247
+ <div
1248
+ part=${"row " + userParts}
1249
+ class="row"
1250
+ data-index=${metadata.index}
1251
+ data-filtered-index=${index}
1252
+ >
1253
+ ${this.columns.map((column) => this.renderCell(column, row))}
1254
+ </div>
1255
+ `;
1256
+ }
1257
+ renderBody() {
1258
+ if (this.columnWidths.length === 0) {
1259
+ return html2`
1260
+ <div part="message" class="message">No visible columns.</div>
1261
+ `;
1262
+ }
1263
+ if (this.data.length === 0) {
1264
+ return html2`<div part="message" class="message">
1265
+ ${this.emptyMessage}
1266
+ </div>`;
1267
+ }
1268
+ if (this.filteredData.length === 0) {
1269
+ return html2`<div part="message" class="message">
1270
+ ${this.noResultsMessage}
1271
+ </div>`;
1272
+ }
1273
+ if (this.enableVirtualScroll) {
1274
+ return html2`
1275
+ <lit-virtualizer
1276
+ .items=${this.filteredData}
1277
+ .renderItem=${(item, index) => this.renderRow(item, index)}
1278
+ ></lit-virtualizer>
1279
+ `;
1280
+ }
1281
+ return html2`
1282
+ ${repeat(
1283
+ this.filteredData,
1284
+ (item) => this.rowMetadata.get(item).index,
1285
+ (item, index) => this.renderRow(item, index)
1286
+ )}
1287
+ `;
1288
+ }
1289
+ renderFooter() {
1290
+ if (!this.enableFooter) {
1291
+ return nothing;
1292
+ }
1293
+ const total = this.data.length;
1294
+ const filtered = this.filteredData.length;
1295
+ const fmt = new Intl.NumberFormat(void 0);
1296
+ const totalStr = fmt.format(total);
1297
+ const filteredStr = fmt.format(filtered);
1298
+ const rowCountText = total !== filtered ? `Showing ${filteredStr} of ${totalStr} records` : `Total records: ${totalStr}`;
1299
+ const formatter = Intl.DateTimeFormat(void 0, {
1300
+ dateStyle: "short",
1301
+ timeStyle: "short"
1302
+ });
1303
+ const lastUpdateText = this.dataLastUpdate ? formatter.format(this.dataLastUpdate) : "Never";
1304
+ return html2`
1305
+ <div part="footer" class="footer">
1306
+ <slot name="footer">
1307
+ <span part="row-count">${rowCountText}</span>
1308
+ <span part="timestamp">${lastUpdateText}</span>
1309
+ </slot>
1310
+ </div>
1311
+ `;
1312
+ }
1313
+ render() {
1314
+ const gridTemplate = widthsToGridTemplates(this.columnWidths).join(" ");
1315
+ return html2`
1316
+ <div
1317
+ part="table"
1318
+ class="table"
1319
+ style=${styleMap({ "--grid-template": gridTemplate })}
1320
+ >
1321
+ ${this.renderHeader()} ${this.renderBody()} ${this.renderFooter()}
1322
+ </div>
1323
+ `;
1324
+ }
1325
+ // #endregion
1326
+ // #region --- Lifecycle Methods ---
1327
+ updated(changedProperties) {
1328
+ super.updated(changedProperties);
1329
+ if (!this.storageOptions?.key) return;
1330
+ const shouldSave = Array.from(changedProperties.keys()).some(
1331
+ (prop) => SAVE_TRIGGERS.has(prop)
1332
+ );
1333
+ if (shouldSave) {
1334
+ this.scheduleSave();
1335
+ }
1336
+ }
1337
+ disconnectedCallback() {
1338
+ super.disconnectedCallback();
1339
+ window.addEventListener("mousemove", this.handleResizeMouseMove);
1340
+ window.addEventListener("mouseup", this.handleResizeMouseUp);
1341
+ }
1342
+ // #endregion
1343
+ // #region --- Filter Methods ---
1344
+ /**
1345
+ * Calculates a relevance score for a given query against a target string.
1346
+ *
1347
+ * This function implements a tiered matching strategy:
1348
+ * 1. **Exact Match**: The query exactly matches the target. This yields the highest score.
1349
+ * 2. **Prefix Match**: The target starts with the query. This is the next most relevant.
1350
+ * 3. **Substring Match**: The target contains the query somewhere. This is the least relevant.
1351
+ *
1352
+ * The final score is weighted and adjusted by the length difference between the query and the target
1353
+ * to ensure that more specific matches (e.g., "apple" vs "application" for the query "apple") rank higher.
1354
+ *
1355
+ * @param query The search term (e.g., "app").
1356
+ * @param target The string to be searched (e.g., "Apple" or "Application").
1357
+ * @returns A numerical score representing the relevance of the match. Higher is better. Returns 0 if no match is found.
1358
+ */
1359
+ calculateSearchScore(query2, target) {
1360
+ const results = { score: 0, ranges: [] };
1361
+ if (!query2 || !target) {
1362
+ return results;
1363
+ }
1364
+ let baseScore = 0;
1365
+ let matchTypeWeight = 0;
1366
+ if (target === query2) {
1367
+ matchTypeWeight = MATCH_WEIGHTS.EXACT;
1368
+ baseScore = query2.length;
1369
+ results.ranges.push([0, target.length]);
1370
+ } else if (target.startsWith(query2)) {
1371
+ matchTypeWeight = MATCH_WEIGHTS.PREFIX;
1372
+ baseScore = query2.length;
1373
+ results.ranges.push([0, query2.length]);
1374
+ } else {
1375
+ const index = target.indexOf(query2);
1376
+ if (index !== -1) {
1377
+ matchTypeWeight = MATCH_WEIGHTS.SUBSTRING;
1378
+ baseScore = query2.length;
1379
+ let cursor = index;
1380
+ while (cursor !== -1) {
1381
+ results.ranges.push([cursor, cursor + query2.length]);
1382
+ cursor = target.indexOf(query2, cursor + 1);
1383
+ }
1384
+ } else {
1385
+ return results;
1386
+ }
1387
+ }
1388
+ const lengthDifference = target.length - query2.length;
1389
+ const specificityBonus = 1 / (1 + lengthDifference);
1390
+ results.score = baseScore * matchTypeWeight * specificityBonus;
1391
+ return results;
1392
+ }
1393
+ searchField(query2, value, tokens) {
1394
+ const result = { score: 0, ranges: [] };
1395
+ const addRangesFromValue = (searchTerm) => {
1396
+ let idx = value.indexOf(searchTerm);
1397
+ while (idx !== -1) {
1398
+ result.ranges.push([idx, idx + searchTerm.length]);
1399
+ idx = value.indexOf(searchTerm, idx + 1);
1400
+ }
1401
+ };
1402
+ if (query2.quoted || !tokens) {
1403
+ if (!this.enableSearchScoring) {
1404
+ if (value.includes(query2.value)) {
1405
+ result.score = 1;
1406
+ addRangesFromValue(query2.value);
1407
+ }
1408
+ } else {
1409
+ const calculation = this.calculateSearchScore(query2.value, value);
1410
+ result.score = calculation.score;
1411
+ result.ranges = calculation.ranges;
1412
+ }
1413
+ return result;
1414
+ }
1415
+ if (!this.enableSearchScoring) {
1416
+ const isMatch = tokens.some((token) => token.includes(query2.value));
1417
+ if (isMatch) {
1418
+ result.score = 1;
1419
+ addRangesFromValue(query2.value);
1420
+ }
1421
+ return result;
1422
+ }
1423
+ for (const token of tokens) {
1424
+ const calculation = this.calculateSearchScore(query2.value, token);
1425
+ if (calculation.score > 0) {
1426
+ result.score += calculation.score;
1427
+ addRangesFromValue(query2.value);
1428
+ }
1429
+ }
1430
+ return result;
1431
+ }
1432
+ filterField(value, filter, filterFunction = null) {
1433
+ if (Array.isArray(filter)) {
1434
+ if (filter.length === 0) {
1435
+ return true;
1436
+ }
1437
+ return filter.some(
1438
+ (element) => this.filterField(value, element, filterFunction)
1439
+ );
1440
+ }
1441
+ if (Array.isArray(value)) {
1442
+ if (value.length === 0) {
1443
+ return false;
1444
+ }
1445
+ return value.some(
1446
+ (element) => this.filterField(element, filter, filterFunction)
1447
+ );
1448
+ }
1449
+ if (typeof filterFunction === "function") {
1450
+ return filterFunction(value, filter);
1451
+ }
1452
+ if (filter instanceof RegExp) {
1453
+ return filter.test(String(value));
1454
+ }
1455
+ return filter === value;
1456
+ }
1457
+ filterRow(row, index) {
1458
+ if (!this.filters) {
1459
+ return true;
1460
+ }
1461
+ if (typeof this.filters === "function") {
1462
+ return this.filters(row, index);
1463
+ }
1464
+ for (const field in this.filters) {
1465
+ const filter = getNestedValue(this.filters, field);
1466
+ const value = getNestedValue(row, field);
1467
+ if (typeof filter === "function") {
1468
+ if (!filter(value)) {
1469
+ return false;
1470
+ }
1471
+ } else {
1472
+ const col = findColumn(field, this.columns);
1473
+ const filterCallback = col ? col.filter : void 0;
1474
+ if (!this.filterField(value, filter, filterCallback)) {
1475
+ return false;
1476
+ }
1477
+ }
1478
+ }
1479
+ return true;
1480
+ }
1481
+ filterRows() {
1482
+ const searchableFields = [...this.columnData.values()].filter((col) => col.options.searchable).map((c) => c.field);
1483
+ const fields = [...searchableFields, ...this.searchIncludedFields];
1484
+ this._filteredData = this.data.filter((row, index) => {
1485
+ const metadata = this.rowMetadata.get(row);
1486
+ metadata.searchScore = 0;
1487
+ metadata.highlightIndices = {};
1488
+ if (!this.filterRow(row, index)) {
1489
+ return false;
1490
+ }
1491
+ if (!this.queryTokens) {
1492
+ return true;
1493
+ }
1494
+ for (const field of fields) {
1495
+ const originalValue = getNestedValue(row, field);
1496
+ const compareValue = metadata.searchValues[field];
1497
+ const columnTokens = metadata.searchTokens[field];
1498
+ if (typeof originalValue !== "string" || typeof compareValue !== "string") {
1499
+ continue;
1500
+ }
1501
+ const fieldResults = { score: 0, ranges: [] };
1502
+ for (const token of this.queryTokens) {
1503
+ const results = this.searchField(token, compareValue, columnTokens);
1504
+ fieldResults.score += results.score;
1505
+ fieldResults.ranges.push(...results.ranges);
1506
+ }
1507
+ if (fieldResults.score > 0) {
1508
+ metadata.searchScore += fieldResults.score;
1509
+ metadata.highlightIndices[field] = fieldResults.ranges;
1510
+ }
1511
+ }
1512
+ return metadata.searchScore > 0;
1513
+ });
1514
+ this.filterDirty = false;
1515
+ this.sortRows();
1516
+ this.dispatchEvent(new YatlChangeEvent(this.data));
1517
+ }
1518
+ // #endregion
1519
+ // #region --- Sort Methods ---
1520
+ compareRows(a, b, field) {
1521
+ let aValue, bValue;
1522
+ const columnData = findColumn(field, this.columnData);
1523
+ if (!columnData.state.sortState) {
1524
+ return 0;
1525
+ }
1526
+ const aMetadata = this.rowMetadata.get(a);
1527
+ const bMetadata = this.rowMetadata.get(b);
1528
+ if (columnData.state.sortState?.order === "asc") {
1529
+ aValue = aMetadata.sortValues[columnData.field];
1530
+ bValue = bMetadata.sortValues[columnData.field];
1531
+ } else {
1532
+ aValue = bMetadata.sortValues[columnData.field];
1533
+ bValue = aMetadata.sortValues[columnData.field];
1534
+ }
1535
+ if (typeof columnData.options.sorter === "function") {
1536
+ const ret = columnData.options.sorter(aValue, bValue);
1537
+ if (ret !== 0) return ret;
1538
+ }
1539
+ const aIsNull = aValue == null;
1540
+ const bIsNull = bValue == null;
1541
+ if (aIsNull && !bIsNull) return -1;
1542
+ if (bIsNull && !aIsNull) return 1;
1543
+ if (aValue < bValue) return -1;
1544
+ if (aValue > bValue) return 1;
1545
+ return 0;
1546
+ }
1547
+ sortRows() {
1548
+ if (this.filterDirty) {
1549
+ this.filterRows();
1550
+ return;
1551
+ }
1552
+ const sortedColumns = this.columnData.filter((col) => col.state.visible && col.state.sortState).sort(
1553
+ (a, b) => b.state.sortState.priority - a.state.sortState.priority
1554
+ );
1555
+ this._filteredData = this._filteredData.toSorted((a, b) => {
1556
+ const aMetadata = this.rowMetadata.get(a);
1557
+ const bMetadata = this.rowMetadata.get(b);
1558
+ if (this.enableSearchScoring && this.queryTokens) {
1559
+ const aValue = aMetadata.searchScore || 0;
1560
+ const bValue = bMetadata.searchScore || 0;
1561
+ if (aValue > bValue) return -1;
1562
+ if (aValue < bValue) return 1;
1563
+ }
1564
+ for (const col of sortedColumns) {
1565
+ const comp = this.compareRows(a, b, col.field);
1566
+ if (comp !== 0) {
1567
+ return comp;
1568
+ }
1569
+ }
1570
+ return aMetadata.index - bMetadata.index;
1571
+ });
1572
+ this.sortDirty = false;
1573
+ }
1574
+ // #endregion
1575
+ // #region --- State Methods ---
1576
+ createColumnStates() {
1577
+ this.columnStates = this.columns.map((column) => {
1578
+ const previousState = findColumn(column.field, this._columnStates);
1579
+ return {
1580
+ field: column.field,
1581
+ visible: previousState?.visible ?? true,
1582
+ sortState: previousState?.sortState,
1583
+ width: previousState?.width
1584
+ };
1585
+ });
1586
+ }
1587
+ createMetadata() {
1588
+ this.rowMetadata = /* @__PURE__ */ new WeakMap();
1589
+ let index = 0;
1590
+ for (const row of this.data) {
1591
+ const metadata = {
1592
+ index: index++,
1593
+ searchTokens: {},
1594
+ searchValues: {},
1595
+ sortValues: {}
1596
+ };
1597
+ this.rowMetadata.set(row, metadata);
1598
+ for (const column of this.columns) {
1599
+ const value = getNestedValue(row, column.field);
1600
+ if (typeof column.sortValue === "function") {
1601
+ metadata.sortValues[column.field] = column.sortValue(value);
1602
+ } else if (typeof value === "string") {
1603
+ metadata.sortValues[column.field] = value.toLocaleLowerCase();
1604
+ } else if (isCompareable(value)) {
1605
+ metadata.sortValues[column.field] = value;
1606
+ } else {
1607
+ metadata.sortValues[column.field] = String(value);
1608
+ }
1609
+ if (typeof value === "string") {
1610
+ metadata.searchValues[column.field] = value.toLocaleLowerCase();
1611
+ }
1612
+ if (column.searchable && column.tokenize && value) {
1613
+ const tokenizer = column.searchTokenizer ?? this.searchTokenizer;
1614
+ metadata.searchTokens[column.field] = tokenizer(String(value)).map(
1615
+ (token) => token.value
1616
+ );
1617
+ }
1618
+ }
1619
+ for (const field of this.searchIncludedFields) {
1620
+ const value = getNestedValue(row, field);
1621
+ if (typeof value === "string") {
1622
+ metadata.searchValues[field] = value.toLocaleLowerCase();
1623
+ }
1624
+ }
1625
+ }
1626
+ }
1627
+ updateInternalQuery() {
1628
+ if (this.searchQuery.length === 0) {
1629
+ this.queryTokens = null;
1630
+ return;
1631
+ }
1632
+ this.queryTokens = [
1633
+ { value: this.searchQuery.toLocaleLowerCase(), quoted: true }
1634
+ ];
1635
+ if (this.enableSearchTokenization) {
1636
+ this.queryTokens.push(...this.searchTokenizer(this.searchQuery));
1637
+ }
1638
+ }
1639
+ // #endregion
1640
+ // #region --- Utilities ---
1641
+ get columnData() {
1642
+ return this.columns.map((column) => ({
1643
+ field: column.field,
1644
+ options: column,
1645
+ state: findColumn(column.field, this._columnStates) ?? {
1646
+ field: column.field,
1647
+ visible: true
1648
+ }
1649
+ }));
1650
+ }
1651
+ get columnWidths() {
1652
+ return this.columns.map((col) => findColumn(col.field, this._columnStates)).filter((state2) => state2 ? state2.visible : true).map((state2) => state2?.width ?? null);
1653
+ }
1654
+ scheduleSave() {
1655
+ window.clearTimeout(this.saveTimer);
1656
+ this.saveTimer = window.setTimeout(() => {
1657
+ this.saveStateToStorage();
1658
+ }, STATE_SAVE_DEBOUNCE);
1659
+ }
1660
+ // #endregion
1661
+ // #region --- Storage Methods ---
1662
+ saveStateToStorage() {
1663
+ if (!this.storageOptions) {
1664
+ return;
1665
+ }
1666
+ const options = { ...DEFAULT_STORAGE_OPTIONS, ...this.storageOptions };
1667
+ const savedTableState = {
1668
+ columns: []
1669
+ };
1670
+ const tableState = this.getState();
1671
+ if (options.saveColumnOrder) {
1672
+ savedTableState.columnOrder = tableState.columnOrder;
1673
+ }
1674
+ for (const columnState of tableState.columns) {
1675
+ const savedColumnState = {
1676
+ field: columnState.field
1677
+ };
1678
+ if (options.saveColumnSortOrders) {
1679
+ savedColumnState.sortState = columnState.sortState;
1680
+ }
1681
+ if (options.saveColumnVisibility) {
1682
+ savedColumnState.visible = columnState.visible;
1683
+ }
1684
+ if (options.saveColumnWidths) {
1685
+ savedColumnState.width = columnState.width;
1686
+ }
1687
+ savedTableState.columns?.push(savedColumnState);
1688
+ }
1689
+ const storage = options.storage === "session" ? sessionStorage : localStorage;
1690
+ try {
1691
+ storage.setItem(options.key, JSON.stringify(savedTableState));
1692
+ } catch (error) {
1693
+ console.warn("Failed to save table state", error);
1694
+ }
1695
+ }
1696
+ loadStateFromStorage() {
1697
+ if (!this.storageOptions) {
1698
+ return;
1699
+ }
1700
+ const options = { ...DEFAULT_STORAGE_OPTIONS, ...this.storageOptions };
1701
+ const json = localStorage.getItem(options.key);
1702
+ if (!json) {
1703
+ return;
1704
+ }
1705
+ try {
1706
+ const savedTableState = JSON.parse(json);
1707
+ const tableStateToRestore = {};
1708
+ if (options.saveColumnOrder) {
1709
+ tableStateToRestore.columnOrder = savedTableState.columnOrder;
1710
+ }
1711
+ if (savedTableState.columns) {
1712
+ tableStateToRestore.columns = [];
1713
+ for (const savedColumnState of savedTableState.columns) {
1714
+ const columnStateToRestore = {
1715
+ field: savedColumnState.field
1716
+ };
1717
+ if (options.saveColumnVisibility) {
1718
+ columnStateToRestore.visible = savedColumnState.visible;
1719
+ }
1720
+ if (options.saveColumnWidths) {
1721
+ columnStateToRestore.width = savedColumnState.width;
1722
+ }
1723
+ if (options.saveColumnSortOrders) {
1724
+ columnStateToRestore.sortState = savedColumnState.sortState;
1725
+ }
1726
+ tableStateToRestore.columns.push(columnStateToRestore);
1727
+ }
1728
+ }
1729
+ this.restoreState(tableStateToRestore);
1730
+ this.hasRestoredState = true;
1731
+ } catch (error) {
1732
+ console.error("Failed to restore DataTable state:", error);
1733
+ }
1734
+ }
1735
+ handleResizeMouseDown(event, field) {
1736
+ event.preventDefault();
1737
+ event.stopPropagation();
1738
+ const target = event.target;
1739
+ const header = target.closest(".cell");
1740
+ if (!header) {
1741
+ return;
1742
+ }
1743
+ const columnIndex = this.columns.findIndex((col) => col.field === field);
1744
+ if (columnIndex < 0) {
1745
+ return;
1746
+ }
1747
+ this.tableElement.querySelectorAll(".header .cell").forEach((element) => {
1748
+ const field2 = element.dataset.field;
1749
+ if (field2) {
1750
+ const state2 = findColumn(field2, this._columnStates);
1751
+ if (state2) {
1752
+ state2.width = element.getBoundingClientRect().width;
1753
+ }
1754
+ }
1755
+ });
1756
+ this.resizeState = {
1757
+ active: true,
1758
+ startX: event.pageX,
1759
+ startWidth: header.getBoundingClientRect().width,
1760
+ columnIndex,
1761
+ columnField: field,
1762
+ currentWidths: widthsToGridTemplates(this.columnWidths)
1763
+ };
1764
+ this.tableElement.style.setProperty(
1765
+ "--grid-template",
1766
+ this.resizeState.currentWidths.join(" ")
1767
+ );
1768
+ window.addEventListener("mousemove", this.handleResizeMouseMove);
1769
+ window.addEventListener("mouseup", this.handleResizeMouseUp);
1770
+ document.body.style.cursor = "col-resize";
1771
+ }
1772
+ addEventListener(type, listener, options) {
1773
+ super.addEventListener(
1774
+ type,
1775
+ listener,
1776
+ options
1777
+ );
1778
+ }
1779
+ removeEventListener(type, listener, options) {
1780
+ super.removeEventListener(type, listener, options);
1781
+ }
1782
+ dispatchEvent(event) {
1783
+ return super.dispatchEvent(event);
1784
+ }
1785
+ // #endregion
1786
+ };
1787
+ YatlTable.styles = [yatl_table_styles_default];
1788
+ __decorateClass([
1789
+ query(".table")
1790
+ ], YatlTable.prototype, "tableElement", 2);
1791
+ __decorateClass([
1792
+ query("lit-virtualizer")
1793
+ ], YatlTable.prototype, "virtualizer", 2);
1794
+ __decorateClass([
1795
+ state()
1796
+ ], YatlTable.prototype, "_filteredData", 2);
1797
+ __decorateClass([
1798
+ property({ type: Boolean, attribute: "enable-virtual-scroll" })
1799
+ ], YatlTable.prototype, "enableVirtualScroll", 2);
1800
+ __decorateClass([
1801
+ property({ type: Boolean, attribute: "enable-search-highlight" })
1802
+ ], YatlTable.prototype, "enableSearchHighlight", 2);
1803
+ __decorateClass([
1804
+ property({ type: Boolean, attribute: "enable-search-tokenization" })
1805
+ ], YatlTable.prototype, "enableSearchTokenization", 1);
1806
+ __decorateClass([
1807
+ property({ type: Boolean, attribute: "enable-search-scoring" })
1808
+ ], YatlTable.prototype, "enableSearchScoring", 1);
1809
+ __decorateClass([
1810
+ property({ type: Boolean, attribute: "enable-column-reorder" })
1811
+ ], YatlTable.prototype, "enableColumnReorder", 2);
1812
+ __decorateClass([
1813
+ property({ type: Boolean, attribute: "enable-footer" })
1814
+ ], YatlTable.prototype, "enableFooter", 2);
1815
+ __decorateClass([
1816
+ property({ type: String, attribute: "null-value-placeholder" })
1817
+ ], YatlTable.prototype, "nullValuePlaceholder", 2);
1818
+ __decorateClass([
1819
+ property({ type: String, attribute: "empty-message" })
1820
+ ], YatlTable.prototype, "emptyMessage", 2);
1821
+ __decorateClass([
1822
+ property({ type: String, attribute: "no-results-message" })
1823
+ ], YatlTable.prototype, "noResultsMessage", 2);
1824
+ __decorateClass([
1825
+ property({ attribute: false })
1826
+ ], YatlTable.prototype, "columns", 1);
1827
+ __decorateClass([
1828
+ property({ attribute: false })
1829
+ ], YatlTable.prototype, "columnStates", 1);
1830
+ __decorateClass([
1831
+ property({ type: String, attribute: "search-query" })
1832
+ ], YatlTable.prototype, "searchQuery", 1);
1833
+ __decorateClass([
1834
+ property({ type: Array, attribute: "search-included-fields" })
1835
+ ], YatlTable.prototype, "searchIncludedFields", 1);
1836
+ __decorateClass([
1837
+ property({ attribute: false })
1838
+ ], YatlTable.prototype, "searchTokenizer", 1);
1839
+ __decorateClass([
1840
+ property({ attribute: false })
1841
+ ], YatlTable.prototype, "filters", 1);
1842
+ __decorateClass([
1843
+ property({ attribute: false })
1844
+ ], YatlTable.prototype, "rowParts", 2);
1845
+ __decorateClass([
1846
+ property({ type: Object, attribute: "storage-options" })
1847
+ ], YatlTable.prototype, "storageOptions", 1);
1848
+ __decorateClass([
1849
+ property({ attribute: false })
1850
+ ], YatlTable.prototype, "data", 1);
1851
+ YatlTable = __decorateClass([
1852
+ customElement("yatl-table")
1853
+ ], YatlTable);
1854
+ export {
1855
+ YatlChangeEvent,
1856
+ YatlColumnReorderEvent,
1857
+ YatlColumnResizeEvent,
1858
+ YatlColumnToggleEvent,
1859
+ YatlEvent,
1860
+ YatlRowClickEvent,
1861
+ YatlSearchEvent,
1862
+ YatlSortEvent,
1863
+ YatlTable,
1864
+ createRegexTokenizer,
1865
+ findColumn,
1866
+ whitespaceTokenizer
1867
+ };
1868
+ //# sourceMappingURL=index.mjs.map