kempo-ui 0.0.3 → 0.0.4

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 (137) hide show
  1. package/.github/workflows/publish-major.yml +39 -0
  2. package/.github/workflows/publish-minor.yml +39 -0
  3. package/.github/workflows/{publish-npm.yml → publish-patch.yml} +1 -1
  4. package/.vscode/settings.json +2 -0
  5. package/dist/kempo-hljs.css +1 -0
  6. package/dist/src/components/Accordion.js +36 -0
  7. package/dist/src/components/Card.js +37 -0
  8. package/dist/src/components/Collapsible.js +41 -0
  9. package/dist/src/components/ContentSlider.js +44 -0
  10. package/dist/src/components/Dialog.js +113 -0
  11. package/dist/src/components/Icon.js +2 -2
  12. package/dist/src/components/PersistantCollapsible.js +1 -0
  13. package/dist/src/components/PhotoViewer.js +135 -0
  14. package/dist/src/components/Resize.js +96 -0
  15. package/dist/src/components/ShowMore.js +42 -0
  16. package/dist/src/components/SideMenu.js +1 -1
  17. package/dist/src/components/Split.js +84 -0
  18. package/dist/src/components/Tabs.js +155 -0
  19. package/dist/src/components/Tags.js +69 -0
  20. package/dist/src/components/ThemeSwitcher.js +23 -0
  21. package/dist/src/components/Timestamp.js +1 -0
  22. package/dist/src/utils/debounce.js +1 -0
  23. package/dist/src/utils/drag.js +1 -0
  24. package/dist/src/utils/formatTimestamp.js +1 -0
  25. package/dist/src/utils/propConverters.js +1 -0
  26. package/dist/src/utils/watchWindowSize.js +1 -0
  27. package/docs/components/accordion.html +69 -25
  28. package/docs/components/card.html +6 -5
  29. package/docs/components/collapsible.html +198 -0
  30. package/docs/components/content-slider.html +56 -26
  31. package/docs/components/dialog.html +54 -48
  32. package/docs/components/focus-capture.html +2 -3
  33. package/docs/components/icon.html +5 -8
  34. package/docs/components/import.html +1 -6
  35. package/docs/components/persistant-collapsible.html +123 -0
  36. package/docs/components/photo-viewer.html +226 -0
  37. package/docs/components/resize.html +44 -18
  38. package/docs/components/show-more.html +12 -6
  39. package/docs/components/side-menu.html +3 -12
  40. package/docs/components/split.html +26 -5
  41. package/docs/components/tablePagination.html +4 -4
  42. package/docs/components/tableRecordFiltering.html +16 -3
  43. package/docs/components/tableRecordSelection.html +2 -2
  44. package/docs/components/tabs.html +24 -7
  45. package/docs/components/tags.html +24 -8
  46. package/docs/components/theme-switcher.html +3 -6
  47. package/docs/components/timestamp.html +20 -24
  48. package/docs/components/toast.html +23 -32
  49. package/docs/components/toggle.html +4 -3
  50. package/docs/kempo-hljs.css +1 -0
  51. package/docs/media/civic.jpg +0 -0
  52. package/docs/media/corvette.jpg +0 -0
  53. package/docs/media/evo.jpg +0 -0
  54. package/docs/media/gtr.jpg +0 -0
  55. package/docs/media/nsx.jpg +0 -0
  56. package/docs/nav.inc.html +11 -17
  57. package/docs/src/components/Accordion.js +36 -0
  58. package/docs/src/components/Card.js +37 -0
  59. package/docs/src/components/Collapsible.js +41 -0
  60. package/docs/src/components/ContentSlider.js +44 -0
  61. package/docs/src/components/Dialog.js +113 -0
  62. package/docs/src/components/Icon.js +2 -2
  63. package/docs/src/components/PersistantCollapsible.js +1 -0
  64. package/docs/src/components/PhotoViewer.js +135 -0
  65. package/docs/src/components/Resize.js +96 -0
  66. package/docs/src/components/ShowMore.js +42 -0
  67. package/docs/src/components/SideMenu.js +1 -1
  68. package/docs/src/components/Split.js +84 -0
  69. package/docs/src/components/Tabs.js +155 -0
  70. package/docs/src/components/Tags.js +69 -0
  71. package/docs/src/components/ThemeSwitcher.js +23 -0
  72. package/docs/src/components/Timestamp.js +1 -0
  73. package/docs/src/utils/debounce.js +1 -0
  74. package/docs/src/utils/drag.js +1 -0
  75. package/docs/src/utils/formatTimestamp.js +1 -0
  76. package/docs/src/utils/propConverters.js +1 -0
  77. package/docs/src/utils/watchWindowSize.js +1 -0
  78. package/docs/utils/debounce.html +78 -0
  79. package/docs/utils/drag.html +104 -0
  80. package/docs/utils/formatTimestamp.html +114 -0
  81. package/docs/utils/propConverters.html +132 -0
  82. package/docs/utils/watchWindowSize.html +166 -0
  83. package/icons/error.svg +1 -1
  84. package/icons/warning.svg +1 -1
  85. package/package.json +1 -1
  86. package/scripts/build.js +1 -1
  87. package/scripts/docs.js +4 -2
  88. package/src/components/Accordion.js +229 -0
  89. package/src/components/Collapsible.js +109 -0
  90. package/src/components/ContentSlider.js +205 -0
  91. package/src/components/Dialog.js +368 -0
  92. package/src/components/PersistantCollapsible.js +69 -0
  93. package/src/components/PhotoViewer.js +368 -0
  94. package/src/components/Resize.js +210 -0
  95. package/src/components/ShadowComponent.js +3 -1
  96. package/src/components/ShowMore.js +109 -0
  97. package/src/components/Sortable.js +193 -0
  98. package/src/components/Split.js +192 -0
  99. package/src/components/Table.js +1202 -0
  100. package/src/components/Tabs.js +428 -0
  101. package/src/components/Tags.js +253 -0
  102. package/src/components/Timestamp.js +31 -0
  103. package/src/components/Toast.js +454 -0
  104. package/src/components/Toggle.js +173 -0
  105. package/src/components/tableControls/DeleteRecord.js +29 -0
  106. package/src/components/tableControls/Edit.js +88 -0
  107. package/src/components/tableControls/ExportCSV.js +71 -0
  108. package/src/components/tableControls/ExportJson.js +55 -0
  109. package/src/components/tableControls/FieldSortHide.js +76 -0
  110. package/src/components/tableControls/Filters.js +114 -0
  111. package/src/components/tableControls/FirstPage.js +65 -0
  112. package/src/components/tableControls/HiddenCount.js +45 -0
  113. package/src/components/tableControls/Hide.js +34 -0
  114. package/src/components/tableControls/LastPage.js +65 -0
  115. package/src/components/tableControls/NextPage.js +65 -0
  116. package/src/components/tableControls/PageSelect.js +109 -0
  117. package/src/components/tableControls/PageSize.js +68 -0
  118. package/src/components/tableControls/PrevPage.js +65 -0
  119. package/src/components/tableControls/Search.js +58 -0
  120. package/src/components/tableControls/ShowAll.js +34 -0
  121. package/src/components/tableControls/TableControl.js +105 -0
  122. package/src/utils/debounce.js +9 -0
  123. package/src/utils/drag.js +80 -0
  124. package/src/utils/formatTimestamp.js +27 -0
  125. package/src/utils/toTitleCase.js +9 -0
  126. package/src/utils/watchWindowSize.js +16 -0
  127. package/dist/src/utils/cli.js +0 -1
  128. package/dist/src/utils/fs-utils.js +0 -1
  129. package/docs/components/component.html +0 -90
  130. package/docs/components/directory-viewer.html +0 -90
  131. package/docs/components/lazy-component.html +0 -84
  132. package/docs/components/reactive-component.html +0 -69
  133. package/docs/components/reactive-lazy-component.html +0 -69
  134. package/docs/components/search.html +0 -102
  135. package/docs/components/sortable.html +0 -96
  136. package/src/utils/cli.js +0 -43
  137. package/src/utils/fs-utils.js +0 -41
@@ -0,0 +1,1202 @@
1
+ import { html, css, unsafeStatic, literal } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import toTitleCase from '../utils/toTitleCase.js';
4
+ import { boolExists } from '../utils/propConverters.js';
5
+
6
+ const selected = Symbol('selected');
7
+ const hidden = Symbol('hidden');
8
+ const index = Symbol('index');
9
+ const editing = Symbol('editing');
10
+
11
+ export default class Table extends ShadowComponent {
12
+ static properties = {
13
+ enablePages: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-pages' },
14
+ pageSize: { type: Number, reflect: true, attribute: 'page-size' },
15
+ currentPage: { type: Number, reflect: true, attribute: 'current-page' },
16
+ pageSizeOptions: { type: Array, attribute: 'page-size-options' },
17
+ enableSelection: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-selection' },
18
+ enableSorting: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-sorting' },
19
+ caseSensitiveFilters: { type: Boolean, reflect: true, converter: boolExists, attribute: 'case-sensitive-filters' },
20
+ fields: { type: Array },
21
+ records: { type: Array },
22
+ filters: { type: Array },
23
+ sort: { type: Array },
24
+ columnSizes: { type: Object },
25
+ fetchPending: { type: Boolean }
26
+ };
27
+
28
+ static styles = css`
29
+ :host {
30
+ display: block;
31
+ width: 100%;
32
+ overflow: auto;
33
+ margin-bottom: var(--spacer);
34
+ }
35
+ #wrapper {
36
+ width: min-content;
37
+ border: 1px solid var(--c_border);
38
+ border-radius: var(--radius);
39
+ }
40
+ #table {
41
+ width: min-content;
42
+ }
43
+ #fields,
44
+ .record {
45
+ display: flex;
46
+ }
47
+ #fields {
48
+ background-color: var(--c_bg__alt);
49
+ border-bottom: 1px solid var(--c_border);
50
+ }
51
+ .record:not([editing="true"]) .cell:not(.controls),
52
+ #fields .cell:not(.controls) {
53
+ padding: calc(0.5 * var(--spacer)) var(--spacer);
54
+ }
55
+ .cell {
56
+ display: flex;
57
+ align-items: center;
58
+ }
59
+ .cell:not(:first-child) {
60
+ border-left: 1px solid var(--c_border);
61
+ }
62
+ .record:not(:last-child) .cell {
63
+ border-bottom: 1px solid var(--c_border);
64
+ }
65
+ #top, #bottom {
66
+ display: flex;
67
+ width: 100%;
68
+ }
69
+ #top slot {
70
+ display: block;
71
+ width: 100%;
72
+ border-bottom: 1px solid var(--c_border);
73
+ }
74
+ #bottom slot {
75
+ display: block;
76
+ width: 100%;
77
+ border-top: 1px solid var(--c_border);
78
+ }
79
+ :host(:not([top-controls])) #top,
80
+ :host(:not([bottom-controls])) #bottom {
81
+ display: none;
82
+ }
83
+ .field-select,
84
+ .selection {
85
+ display: flex;
86
+ justify-content: center;
87
+ align-items: center;
88
+ }
89
+ .field-select input,
90
+ .selection input {
91
+ width: 1.25rem;
92
+ height: 1.25rem;
93
+ }
94
+ .icon-sort {
95
+ float: right;
96
+ opacity: 0.5;
97
+ }
98
+ `;
99
+
100
+ constructor(options = {}) {
101
+ super();
102
+
103
+ // Set defaults for reactive properties (must be after super())
104
+ if(this.pageSize === undefined) this.pageSize = 50;
105
+ if(this.currentPage === undefined) this.currentPage = 1;
106
+ if(this.pageSizeOptions === undefined) this.pageSizeOptions = [10, 25, 50, 100, 500];
107
+ if(this.records === undefined) this.records = options.records || [];
108
+ if(this.fields === undefined) this.fields = options.fields || [];
109
+ if(this.filters === undefined) this.filters = options.filters || [];
110
+ if(this.sort === undefined) this.sort = [];
111
+ if(this.columnSizes === undefined) this.columnSizes = {};
112
+ if(this.fetchPending === undefined) this.fetchPending = false;
113
+ }
114
+
115
+ /*
116
+ Event Handlers
117
+ */
118
+
119
+ handleSelectAllChange = (e) => {
120
+ if (e.target.checked) {
121
+ this.selectAllOnPage();
122
+ } else {
123
+ this.deselectAllOnPage();
124
+ }
125
+ };
126
+
127
+ handleFieldClick = (fieldName) => {
128
+ const currentSort = this.sort.find(item => item.name === fieldName);
129
+ const asc = currentSort ? !currentSort.asc : true;
130
+ this.sortBy(fieldName, asc);
131
+ };
132
+
133
+ handleRecordSelectionChange = (record, e) => {
134
+ record[selected] = !!e.target.checked;
135
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
136
+ };
137
+
138
+ /*
139
+ Lifecycle Callbacks
140
+ */
141
+
142
+ firstUpdated() {
143
+ this.setData({
144
+ records: this.records,
145
+ fields: this.fields,
146
+ filters: this.filters
147
+ });
148
+ }
149
+
150
+ updated(changedProperties) {
151
+ super.updated(changedProperties);
152
+
153
+ // Update select-all checkbox state when selection or page changes
154
+ if (this.enableSelection) {
155
+ const selectAllCheckbox = this.shadowRoot.getElementById('select-all');
156
+ if (selectAllCheckbox) {
157
+ selectAllCheckbox.checked = this.allOnPageSelected();
158
+ }
159
+ }
160
+
161
+ // Update container widths
162
+ this.updateContainerWidths();
163
+ }
164
+
165
+ updateContainerWidths() {
166
+ const totalWidth = this.columnSizes.total + 'px';
167
+ const fieldsEl = this.shadowRoot.getElementById('fields');
168
+ const topEl = this.shadowRoot.getElementById('top');
169
+ const bottomEl = this.shadowRoot.getElementById('bottom');
170
+ const recordsEl = this.shadowRoot.getElementById('records');
171
+
172
+ if (fieldsEl) fieldsEl.style.width = totalWidth;
173
+ if (topEl) topEl.style.width = totalWidth;
174
+ if (bottomEl) bottomEl.style.width = totalWidth;
175
+ if (recordsEl) recordsEl.style.width = totalWidth;
176
+ }
177
+
178
+ /*
179
+ Rendering Functions
180
+ */
181
+
182
+ renderFieldsTemplate() {
183
+ this.calculateColumnSizes();
184
+ this.hasTopControls() ? this.setAttribute('top-controls', 'true') : this.removeAttribute('top-controls');
185
+ this.hasBottomControls() ? this.setAttribute('bottom-controls', 'true') : this.removeAttribute('bottom-controls');
186
+
187
+ const fieldCells = [];
188
+
189
+ if (this.enableSelection) {
190
+ fieldCells.push(html`
191
+ <div class="field controls cell field-select" style="width: 40px">
192
+ <input
193
+ type="checkbox"
194
+ id="select-all"
195
+ @change=${this.handleSelectAllChange}
196
+ />
197
+ </div>
198
+ `);
199
+ }
200
+
201
+ if (this.hasBeforeControls()) {
202
+ fieldCells.push(html`
203
+ <div class="field cell field-before-controls" style="width: ${this.columnSizes.beforeControls}px"></div>
204
+ `);
205
+ }
206
+
207
+ this.fields.forEach(({ name, label, hidden }) => {
208
+ if (hidden) return;
209
+ const sortItem = this.sort.find(item => item.name === name);
210
+ const isCurrentSort = this.sort.length > 0 && this.sort[this.sort.length - 1].name === name;
211
+ const sortClass = sortItem ? (sortItem.asc ? 'sort-asc' : 'sort-desc') : '';
212
+
213
+ fieldCells.push(html`
214
+ <div
215
+ class="field cell ${sortClass}"
216
+ style="width: ${this.columnSizes[name]}px; ${this.enableSorting ? 'cursor: pointer;' : ''}"
217
+ @click=${this.enableSorting ? () => this.handleFieldClick(name) : null}
218
+ >
219
+ ${label}
220
+ ${isCurrentSort ? html`<k-icon name="${sortItem.asc ? 'arrow-down' : 'arrow-up'}" class="icon-sort"></k-icon>` : ''}
221
+ </div>
222
+ `);
223
+ });
224
+
225
+ if (this.hasAfterControls()) {
226
+ fieldCells.push(html`
227
+ <div class="field cell field-after-controls" style="width: ${this.columnSizes.afterControls}px"></div>
228
+ `);
229
+ }
230
+
231
+ return fieldCells;
232
+ }
233
+
234
+ renderRecordsTemplate() {
235
+ let displayedRecords = this.getDisplayedRecords();
236
+ let start = 0;
237
+ let end = this.pageSize;
238
+
239
+ if (this.enablePages) {
240
+ start = (this.currentPage - 1) * this.pageSize;
241
+ end = start + this.pageSize;
242
+ displayedRecords = displayedRecords.slice(start, end);
243
+ }
244
+
245
+ let fetchStart = null;
246
+ let fetchCount = 0;
247
+
248
+ const recordTemplates = displayedRecords.map((record, idx) => {
249
+ if (record !== null) {
250
+ return this.renderRecordTemplate(record);
251
+ } else {
252
+ if (fetchStart === null) fetchStart = start + idx;
253
+ fetchCount++;
254
+ return html`<div class="record fetching"><div class="cell">Loading...</div></div>`;
255
+ }
256
+ });
257
+
258
+ if (fetchStart !== null && !this.fetchPending) {
259
+ setTimeout(() => {
260
+ if (!this.fetchPending) {
261
+ this.dispatchEvent(new CustomEvent('fetchRecords', {
262
+ detail: { start: fetchStart, count: fetchCount },
263
+ bubbles: true
264
+ }));
265
+ }
266
+ }, 0);
267
+ }
268
+
269
+ return recordTemplates;
270
+ }
271
+
272
+ renderRecordTemplate(record) {
273
+ const recordCells = [];
274
+
275
+ if (this.enableSelection) {
276
+ recordCells.push(html`
277
+ <div class="cell selection controls" style="width: 40px">
278
+ <input
279
+ type="checkbox"
280
+ .checked=${record[selected]}
281
+ @change=${(e) => this.handleRecordSelectionChange(record, e)}
282
+ />
283
+ </div>
284
+ `);
285
+ }
286
+
287
+ if (this.hasBeforeControls()) {
288
+ recordCells.push(this.renderBeforeControlsTemplate());
289
+ }
290
+
291
+ this.fields.forEach(({ name, formatter, calculator, type, editor, hidden }) => {
292
+ if (hidden) return;
293
+ let value = record[name] || '';
294
+
295
+ recordCells.push(html`
296
+ <div class="cell" data-field=${name} style="width: ${this.columnSizes[name]}px">
297
+ ${record[editing] ? this.renderEditingCell(record, name, value, calculator, editor, type) : this.renderDisplayCell(record, name, value, calculator, formatter)}
298
+ </div>
299
+ `);
300
+ });
301
+
302
+ if (this.hasAfterControls()) {
303
+ recordCells.push(this.renderAfterControlsTemplate());
304
+ }
305
+
306
+ return html`
307
+ <div class="record ${record[editing] ? 'editing' : ''}" data-index=${record[index]}>
308
+ ${recordCells}
309
+ </div>
310
+ `;
311
+ }
312
+
313
+ renderEditingCell(record, name, value, calculator, editor, type) {
314
+ if (calculator) {
315
+ return html`<input disabled .value=${calculator(record, this)} />`;
316
+ } else if (editor) {
317
+ // Custom editors return DOM elements, need to handle differently
318
+ const editorElement = editor(value);
319
+ return html`${editorElement}`;
320
+ } else {
321
+ const editorType = type || typeof value;
322
+ switch (editorType) {
323
+ case 'number':
324
+ return html`<input type="number" .value=${value} />`;
325
+ case 'date':
326
+ return html`<input type="date" .value=${value} />`;
327
+ case 'boolean':
328
+ return html`
329
+ <select .value=${value}>
330
+ <option value="true" ?selected=${value}>True</option>
331
+ <option value="false" ?selected=${!value}>False</option>
332
+ </select>
333
+ `;
334
+ default:
335
+ return html`<input type="text" .value=${value} />`;
336
+ }
337
+ }
338
+ }
339
+
340
+ renderDisplayCell(record, name, value, calculator, formatter) {
341
+ if (calculator) {
342
+ return calculator(record, this);
343
+ } else if (formatter) {
344
+ return formatter(value);
345
+ } else {
346
+ return value;
347
+ }
348
+ }
349
+
350
+ renderBeforeControlsTemplate() {
351
+ const controls = [];
352
+
353
+ this.querySelectorAll('[slot="before"]').forEach(control => {
354
+ const newControl = new control.constructor();
355
+
356
+ // Copy attributes
357
+ Array.from(control.attributes).forEach(attr => {
358
+ if(attr.name !== 'slot'){
359
+ newControl.setAttribute(attr.name, attr.value);
360
+ }
361
+ });
362
+
363
+ // Copy innerHTML if needed
364
+ if(control.innerHTML){
365
+ newControl.innerHTML = control.innerHTML;
366
+ }
367
+
368
+ controls.push(newControl);
369
+ });
370
+
371
+ return html`
372
+ <div class="cell controls controls-before" style="width: ${this.columnSizes.beforeControls}px">
373
+ ${controls}
374
+ </div>
375
+ `;
376
+ }
377
+
378
+ renderAfterControlsTemplate() {
379
+ const controls = [];
380
+
381
+ this.querySelectorAll('[slot="after"]').forEach(control => {
382
+ const newControl = new control.constructor();
383
+
384
+ // Copy attributes
385
+ Array.from(control.attributes).forEach(attr => {
386
+ if(attr.name !== 'slot'){
387
+ newControl.setAttribute(attr.name, attr.value);
388
+ }
389
+ });
390
+
391
+ // Copy innerHTML if needed
392
+ if(control.innerHTML){
393
+ newControl.innerHTML = control.innerHTML;
394
+ }
395
+
396
+ controls.push(newControl);
397
+ });
398
+
399
+ return html`
400
+ <div class="cell controls controls-after" style="width: ${this.columnSizes.afterControls}px">
401
+ ${controls}
402
+ </div>
403
+ `;
404
+ }
405
+
406
+ /*
407
+ Utility Functions
408
+ */
409
+
410
+ hasBeforeControls() {
411
+ return !!this.querySelector('[slot="before"]');
412
+ }
413
+
414
+ hasAfterControls() {
415
+ return !!this.querySelector('[slot="after"]');
416
+ }
417
+
418
+ hasTopControls() {
419
+ return !!this.querySelector('[slot="top"]');
420
+ }
421
+
422
+ hasBottomControls() {
423
+ return !!this.querySelector(':scope > :not([slot])');
424
+ }
425
+
426
+ /*
427
+ Public Methods
428
+ */
429
+
430
+ editRecord(record) {
431
+ record[editing] = true;
432
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
433
+ if(recordEl){
434
+ recordEl.classList.add('editing');
435
+ recordEl.setAttribute('editing', 'true');
436
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
437
+ const field = $cell.dataset.field;
438
+ const fieldDef = this.fields.find(f => f.name === field);
439
+ if(fieldDef){
440
+ const value = record[field] || '';
441
+ $cell.innerHTML = '';
442
+ if(fieldDef.calculator){
443
+ const input = document.createElement('input');
444
+ input.disabled = true;
445
+ input.value = fieldDef.calculator(record, this);
446
+ $cell.appendChild(input);
447
+ } else if(fieldDef.editor){
448
+ $cell.appendChild(fieldDef.editor(value));
449
+ } else {
450
+ const type = fieldDef.type || typeof value;
451
+ const editorGen = Table.editors[type] || Table.editors.string;
452
+ $cell.appendChild(editorGen(value));
453
+ }
454
+ }
455
+ });
456
+ }
457
+ this.dispatchEvent(new CustomEvent('editingChange', {
458
+ detail: { record, editing: true },
459
+ bubbles: true
460
+ }));
461
+ }
462
+
463
+ saveEditedRecord(record) {
464
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
465
+ if(recordEl){
466
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
467
+ const field = $cell.dataset.field;
468
+ const fieldDef = this.fields.find(f => f.name === field);
469
+ if(fieldDef && !fieldDef.calculator){
470
+ const $input = $cell.querySelector('input, select');
471
+ if($input){
472
+ record[field] = $input.value;
473
+ }
474
+ }
475
+ });
476
+ record[editing] = false;
477
+ recordEl.classList.remove('editing');
478
+ recordEl.removeAttribute('editing');
479
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
480
+ const field = $cell.dataset.field;
481
+ const fieldDef = this.fields.find(f => f.name === field);
482
+ if(fieldDef){
483
+ const value = record[field] || '';
484
+ if(fieldDef.calculator){
485
+ $cell.textContent = fieldDef.calculator(record, this);
486
+ } else if(fieldDef.formatter){
487
+ $cell.innerHTML = fieldDef.formatter(value);
488
+ } else {
489
+ $cell.textContent = value;
490
+ }
491
+ }
492
+ });
493
+ }
494
+ this.dispatchEvent(new CustomEvent('editingChange', {
495
+ detail: { record, editing: false },
496
+ bubbles: true
497
+ }));
498
+ }
499
+
500
+ cancelEditedRecord(record) {
501
+ record[editing] = false;
502
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
503
+ if(recordEl){
504
+ recordEl.classList.remove('editing');
505
+ recordEl.removeAttribute('editing');
506
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
507
+ const field = $cell.dataset.field;
508
+ const fieldDef = this.fields.find(f => f.name === field);
509
+ if(fieldDef){
510
+ const value = record[field] || '';
511
+ if(fieldDef.calculator){
512
+ $cell.textContent = fieldDef.calculator(record, this);
513
+ } else if(fieldDef.formatter){
514
+ $cell.innerHTML = fieldDef.formatter(value);
515
+ } else {
516
+ $cell.textContent = value;
517
+ }
518
+ }
519
+ });
520
+ }
521
+ this.dispatchEvent(new CustomEvent('editingChange', {
522
+ detail: { record, editing: false },
523
+ bubbles: true
524
+ }));
525
+ }
526
+
527
+ recordIsEditing(record) {
528
+ return record[editing];
529
+ }
530
+
531
+ getCurrentPage() {
532
+ return this.currentPage;
533
+ }
534
+
535
+ getTotalPages() {
536
+ return Math.ceil(this.getDisplayedRecords().length / this.pageSize);
537
+ }
538
+
539
+ setPage(page) {
540
+ if (page < 1 || page > this.getTotalPages()) return;
541
+ this.currentPage = page;
542
+ this.requestUpdate();
543
+ this.dispatchEvent(new CustomEvent('pageChange', { bubbles: true }));
544
+ }
545
+
546
+ firstPage() {
547
+ if (this.currentPage !== 1) {
548
+ this.setPage(1);
549
+ }
550
+ }
551
+
552
+ nextPage() {
553
+ if (this.currentPage < this.getTotalPages()) {
554
+ this.setPage(this.currentPage + 1);
555
+ }
556
+ }
557
+
558
+ prevPage() {
559
+ if (this.currentPage > 1) {
560
+ this.setPage(this.currentPage - 1);
561
+ }
562
+ }
563
+
564
+ lastPage() {
565
+ if (this.currentPage !== this.getTotalPages()) {
566
+ this.setPage(this.getTotalPages());
567
+ }
568
+ }
569
+
570
+ setPageSize(pageSize) {
571
+ this.pageSize = pageSize;
572
+ this.currentPage = 1;
573
+ this.requestUpdate();
574
+ this.dispatchEvent(new CustomEvent('pageSizeChange', { bubbles: true }));
575
+ }
576
+
577
+ getPageSize() {
578
+ return this.pageSize;
579
+ }
580
+
581
+ getPageSizeOptions() {
582
+ return this.pageSizeOptions;
583
+ }
584
+
585
+ setPageSizeOptions(options) {
586
+ this.pageSizeOptions = options;
587
+ this.requestUpdate();
588
+ }
589
+
590
+ setData({ records = false, fields = false, pageSize = false, pageSizeOptions = false, currentPage = false, enableSelection } = {}) {
591
+ let rerender = false;
592
+ let pageCountBefore = this.getTotalPages();
593
+ let pageBefore = this.currentPage;
594
+
595
+ if (records) {
596
+ this.records = records.map(r => ({ ...r }));
597
+ this.records.forEach((record, idx) => {
598
+ record[index] = idx;
599
+ record[selected] = false;
600
+ record[hidden] = false;
601
+ record[editing] = false;
602
+ });
603
+ this.fields = fields || Table.extractFieldsFromRecords(this.records);
604
+ rerender = true;
605
+ }
606
+
607
+ if (pageSize) {
608
+ this.pageSize = pageSize;
609
+ rerender = true;
610
+ }
611
+
612
+ if (pageSizeOptions) {
613
+ this.pageSizeOptions = pageSizeOptions;
614
+ }
615
+
616
+ if (currentPage) {
617
+ this.currentPage = currentPage;
618
+ rerender = true;
619
+ }
620
+
621
+ if (enableSelection !== undefined) {
622
+ this.enableSelection = enableSelection;
623
+ rerender = true;
624
+ }
625
+
626
+ if (rerender) {
627
+ this.requestUpdate();
628
+ }
629
+
630
+ const newPageCount = this.getTotalPages();
631
+ if (newPageCount !== pageCountBefore) {
632
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
633
+ detail: { totalPages: this.getTotalPages() },
634
+ bubbles: true
635
+ }));
636
+ }
637
+
638
+ if (pageBefore > newPageCount) {
639
+ this.setPage(newPageCount);
640
+ }
641
+ }
642
+
643
+ setRecords(records, fields) {
644
+ let pageCountBefore = this.getTotalPages();
645
+ let pageBefore = this.currentPage;
646
+
647
+ this.records = records.map(r => ({ ...r }));
648
+ this.records.forEach((record, idx) => {
649
+ record[index] = idx;
650
+ record[selected] = false;
651
+ record[hidden] = false;
652
+ record[editing] = false;
653
+ });
654
+ this.fields = fields || Table.extractFieldsFromRecords(this.records);
655
+ this.requestUpdate();
656
+ this.dispatchEvent(new CustomEvent('recordsSet', {
657
+ detail: { records },
658
+ bubbles: true
659
+ }));
660
+
661
+ const newPageCount = this.getTotalPages();
662
+ if (newPageCount !== pageCountBefore) {
663
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
664
+ detail: { totalPages: this.getTotalPages() },
665
+ bubbles: true
666
+ }));
667
+ }
668
+
669
+ if (pageBefore > newPageCount) {
670
+ this.setPage(newPageCount);
671
+ }
672
+ }
673
+
674
+ setupFetchRecords(totalRecords, callback) {
675
+ const previousLength = this.records.length;
676
+ const pageCountBefore = this.getTotalPages();
677
+
678
+ if (previousLength < totalRecords) {
679
+ this.records.length = totalRecords;
680
+ this.records.fill(null, previousLength);
681
+
682
+ // Trigger re-render and page count changed event
683
+ this.requestUpdate();
684
+
685
+ const newPageCount = this.getTotalPages();
686
+ if (newPageCount !== pageCountBefore) {
687
+ setTimeout(() => {
688
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
689
+ detail: { totalPages: newPageCount },
690
+ bubbles: true
691
+ }));
692
+ }, 0);
693
+ }
694
+ }
695
+
696
+ this.addEventListener('fetchRecords', async (event) => {
697
+ if (this.fetchPending) return;
698
+ this.fetchPending = true;
699
+ const { start, count } = event.detail;
700
+ const records = await callback(start, count);
701
+
702
+ // Initialize symbol properties on fetched records
703
+ records.forEach((record, idx) => {
704
+ record[index] = start + idx;
705
+ if(record[selected] === undefined) record[selected] = false;
706
+ if(record[hidden] === undefined) record[hidden] = false;
707
+ if(record[editing] === undefined) record[editing] = false;
708
+ });
709
+
710
+ this.records.splice(start, records.length, ...records);
711
+ this.fetchPending = false;
712
+ this.requestUpdate();
713
+ });
714
+ }
715
+
716
+ addRecord(record) {
717
+ record[selected] = false;
718
+ record[hidden] = false;
719
+ record[index] = this.records.length;
720
+ this.records.push(record);
721
+ this.requestUpdate();
722
+ this.dispatchEvent(new CustomEvent('recordAdded', {
723
+ detail: { record },
724
+ bubbles: true
725
+ }));
726
+ }
727
+
728
+ updateRecord(record, newData) {
729
+ let updated = false;
730
+ let originalRecord = this.records.find(r => r === record);
731
+ if (!originalRecord && record[index] !== undefined) {
732
+ originalRecord = this.records[record[index]];
733
+ }
734
+ Object.keys(newData).forEach(key => {
735
+ if (originalRecord.hasOwnProperty(key)) {
736
+ originalRecord[key] = newData[key];
737
+ updated = true;
738
+ }
739
+ });
740
+ if (updated) {
741
+ const start = (this.currentPage - 1) * this.pageSize;
742
+ const end = start + this.pageSize;
743
+ if (!this.enablePages || (originalRecord[index] >= start && originalRecord[index] < end)) {
744
+ this.requestUpdate();
745
+ }
746
+ }
747
+ }
748
+
749
+ deleteRecord(record) {
750
+ let originalRecord = this.records.find(r => r === record);
751
+ let totalPagesBefore = this.getTotalPages();
752
+
753
+ if (!originalRecord && record[index] !== undefined) {
754
+ originalRecord = this.records[record[index]];
755
+ }
756
+
757
+ if (originalRecord) {
758
+ const recordIndex = this.records.indexOf(originalRecord);
759
+ this.records.splice(recordIndex, 1);
760
+ this.records.forEach((rec, idx) => {
761
+ rec[index] = idx;
762
+ });
763
+ this.requestUpdate();
764
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
765
+ this.dispatchEvent(new CustomEvent('recordDeleted', {
766
+ detail: { index: recordIndex },
767
+ bubbles: true
768
+ }));
769
+
770
+ const totalPages = this.getTotalPages();
771
+ if (this.currentPage > totalPages) {
772
+ this.setPage(totalPages);
773
+ }
774
+ if (totalPages !== totalPagesBefore) {
775
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
776
+ detail: { totalPages },
777
+ bubbles: true
778
+ }));
779
+ }
780
+ }
781
+ }
782
+
783
+ deleteSelected() {
784
+ let totalPagesBefore = this.getTotalPages();
785
+ const selectedRecords = this.getSelectedRecords();
786
+
787
+ selectedRecords.forEach(record => {
788
+ let originalRecord = this.records.find(r => r === record);
789
+ if (!originalRecord && record[index] !== undefined) {
790
+ originalRecord = this.records[record[index]];
791
+ }
792
+ if (originalRecord) {
793
+ const recordIndex = this.records.indexOf(originalRecord);
794
+ this.records.splice(recordIndex, 1);
795
+ }
796
+ });
797
+
798
+ this.records.forEach((rec, idx) => {
799
+ rec[index] = idx;
800
+ });
801
+ this.requestUpdate();
802
+
803
+ const totalPages = this.getTotalPages();
804
+ if (this.currentPage > totalPages) {
805
+ this.setPage(totalPages);
806
+ }
807
+ if (totalPages !== totalPagesBefore) {
808
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
809
+ detail: { totalPages },
810
+ bubbles: true
811
+ }));
812
+ }
813
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
814
+ }
815
+
816
+ getSelectedRecords() {
817
+ return this.records.filter(record => record[selected]);
818
+ }
819
+
820
+ selectAllOnPage() {
821
+ const start = (this.currentPage - 1) * this.pageSize;
822
+ const end = Math.min(start + this.pageSize, this.records.length);
823
+ for (let i = start; i < end; i++) {
824
+ this.records[i][selected] = true;
825
+ }
826
+ this.requestUpdate();
827
+ setTimeout(() => {
828
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
829
+ }, 0);
830
+ }
831
+
832
+ deselectAllOnPage() {
833
+ const start = (this.currentPage - 1) * this.pageSize;
834
+ const end = Math.min(start + this.pageSize, this.records.length);
835
+ for (let i = start; i < end; i++) {
836
+ this.records[i][selected] = false;
837
+ }
838
+ this.requestUpdate();
839
+ setTimeout(() => {
840
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
841
+ }, 0);
842
+ }
843
+
844
+ allOnPageSelected() {
845
+ const start = (this.currentPage - 1) * this.pageSize;
846
+ const end = Math.min(start + this.pageSize, this.records.length);
847
+ for (let i = start; i < end; i++) {
848
+ if (!this.records[i][selected]) {
849
+ return false;
850
+ }
851
+ }
852
+ return true;
853
+ }
854
+
855
+ sortBy(field, asc = true) {
856
+ this.sort = this.sort.filter(item => item.name !== field);
857
+ this.sort.push({ name: field, asc });
858
+ this.requestUpdate();
859
+ }
860
+
861
+ hideRecord(record) {
862
+ let originalRecord = this.records.find(r => r === record);
863
+ if (!originalRecord && record[index] !== undefined) {
864
+ originalRecord = this.records[record[index]];
865
+ }
866
+ if (originalRecord) {
867
+ originalRecord[hidden] = true;
868
+ this.requestUpdate();
869
+ this.dispatchEvent(new CustomEvent('recordHidden', { bubbles: true }));
870
+ }
871
+ }
872
+
873
+ showRecord(record) {
874
+ let originalRecord = this.records.find(r => r === record);
875
+ if (!originalRecord && record[index] !== undefined) {
876
+ originalRecord = this.records[record[index]];
877
+ }
878
+ if (originalRecord) {
879
+ originalRecord[hidden] = false;
880
+ this.requestUpdate();
881
+ this.dispatchEvent(new CustomEvent('recordShown', { bubbles: true }));
882
+ }
883
+ }
884
+
885
+ showAllRecords() {
886
+ this.records.forEach(record => {
887
+ record[hidden] = false;
888
+ });
889
+ if (this.filters.length) {
890
+ this.filters = [];
891
+ this.dispatchEvent(new CustomEvent('filterRemoved', { bubbles: true }));
892
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
893
+ }
894
+ this.requestUpdate();
895
+ this.dispatchEvent(new CustomEvent('recordShown', { bubbles: true }));
896
+ this.dispatchEvent(new CustomEvent('allRecordsShown', { bubbles: true }));
897
+ }
898
+
899
+ addFilter(field, condition, value) {
900
+ this.filters.push({ field, condition, value });
901
+ this.dispatchEvent(new CustomEvent('filterAdded', { bubbles: true }));
902
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
903
+ this.requestUpdate();
904
+ }
905
+
906
+ removeFilter(field, condition, value, rerender = true) {
907
+ const filterIndex = this.filters.findIndex(f => f.field === field && f.condition === condition && f.value === value);
908
+ if (filterIndex !== -1) {
909
+ this.records.forEach(record => {
910
+ if (!this.testFilter(record, field, condition, value)) {
911
+ record[hidden] = false;
912
+ }
913
+ });
914
+ this.filters.splice(filterIndex, 1);
915
+ this.dispatchEvent(new CustomEvent('filterRemoved', { bubbles: true }));
916
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
917
+ if (rerender) this.requestUpdate();
918
+ }
919
+ }
920
+
921
+ testFilter(record, field, condition, value) {
922
+ let recordValue = record[field];
923
+ let compareValue = value;
924
+
925
+ if(!this.caseSensitiveFilters && typeof recordValue === 'string' && typeof value === 'string'){
926
+ recordValue = recordValue.toLowerCase();
927
+ compareValue = value.toLowerCase();
928
+ }
929
+
930
+ switch(condition){
931
+ case 'equals':
932
+ return recordValue === compareValue;
933
+ case 'not-equals':
934
+ return recordValue !== compareValue;
935
+ case 'contains':
936
+ return recordValue.includes(compareValue);
937
+ case 'not-contains':
938
+ return !recordValue.includes(compareValue);
939
+ case 'greater-than':
940
+ return recordValue > compareValue;
941
+ case 'less-than':
942
+ return recordValue < compareValue;
943
+ case 'greater-than-or-equal':
944
+ return recordValue >= compareValue;
945
+ case 'less-than-or-equal':
946
+ return recordValue <= compareValue;
947
+ default:
948
+ return true;
949
+ }
950
+ }
951
+
952
+ removeAllFilters() {
953
+ if (this.filters.length) {
954
+ this.filters.forEach(({ field, condition, value }) => {
955
+ this.removeFilter(field, condition, value, false);
956
+ });
957
+ this.requestUpdate();
958
+ }
959
+ }
960
+
961
+ search(term) {
962
+ const t = term.trim().toLowerCase();
963
+ let changed = false;
964
+ this.records.forEach(record => {
965
+ if (record[hidden]) return;
966
+ let match = false;
967
+ this.fields.forEach(({ name }) => {
968
+ const val = record[name]?.toString().toLowerCase() || '';
969
+ if (val.includes(t)) {
970
+ match = true;
971
+ }
972
+ });
973
+ if (record[hidden] !== !match) {
974
+ record[hidden] = !match;
975
+ changed = true;
976
+ }
977
+ });
978
+ if (changed) {
979
+ this.dispatchEvent(new CustomEvent('recordHidden', { bubbles: true }));
980
+ this.requestUpdate();
981
+ }
982
+ this.dispatchEvent(new CustomEvent('search', {
983
+ detail: { term },
984
+ bubbles: true
985
+ }));
986
+ }
987
+
988
+ getDisplayedRecords() {
989
+ this.filters.forEach(({ field, condition, value }) => {
990
+ this.records.forEach(record => {
991
+ if(record === null) return;
992
+ if (!this.testFilter(record, field, condition, value)) {
993
+ record[hidden] = true;
994
+ }
995
+ });
996
+ });
997
+
998
+ let displayedRecords = this.records.filter(record => record === null || !record[hidden]);
999
+
1000
+ this.sort.forEach(({ name, asc }) => {
1001
+ displayedRecords.sort((a, b) => {
1002
+ if(a === null || b === null) return 0;
1003
+ if (a[name] < b[name]) return asc ? -1 : 1;
1004
+ if (a[name] > b[name]) return asc ? 1 : -1;
1005
+ return 0;
1006
+ });
1007
+ });
1008
+
1009
+ return displayedRecords;
1010
+ }
1011
+
1012
+ getHiddenRecords() {
1013
+ return this.records.filter(record => record[hidden]);
1014
+ }
1015
+
1016
+ calculateColumnSizes() {
1017
+ this.columnSizes = {};
1018
+ this.columnSizes.total = 0;
1019
+
1020
+ if (this.enableSelection) this.columnSizes.total += 40;
1021
+
1022
+ this.columnSizes.beforeControls = Array.from(this.querySelectorAll('[slot="before"]')).reduce((total, el) => total + (el.maxWidth || 40), 0);
1023
+ this.columnSizes.afterControls = Array.from(this.querySelectorAll('[slot="after"]')).reduce((total, el) => total + (el.maxWidth || 40), 0);
1024
+
1025
+ if (this.hasBeforeControls()) this.columnSizes.total += this.columnSizes.beforeControls;
1026
+ if (this.hasAfterControls()) this.columnSizes.total += this.columnSizes.afterControls;
1027
+
1028
+ this.fields.forEach(field => {
1029
+ if (field.size) {
1030
+ this.columnSizes[field.name] = field.size;
1031
+ this.columnSizes.total += field.size;
1032
+ } else {
1033
+ let maxLength = 0;
1034
+ this.records.slice(0, 100).forEach(record => {
1035
+ if(record === null) return;
1036
+ let value = record[field.name];
1037
+ if (field.calculator) {
1038
+ value = field.calculator(record, this);
1039
+ }
1040
+ if (field.formatter) {
1041
+ value = field.formatter(value);
1042
+ }
1043
+ if (value && value.toString().length > maxLength) {
1044
+ maxLength = value.toString().length;
1045
+ }
1046
+ });
1047
+ this.columnSizes[field.name] = Math.max((maxLength * 10 + 32), 128);
1048
+ if (!field.hidden) this.columnSizes.total += this.columnSizes[field.name];
1049
+ }
1050
+ });
1051
+
1052
+ return this.columnSizes;
1053
+ }
1054
+
1055
+ setFieldHiddenState(fieldName, hidden) {
1056
+ const field = this.fields.find(f => f.name === fieldName);
1057
+ if (field) {
1058
+ field.hidden = hidden;
1059
+ this.calculateColumnSizes();
1060
+ this.requestUpdate();
1061
+ this.dispatchEvent(new CustomEvent('fieldVisibilityChanged', {
1062
+ detail: { field },
1063
+ bubbles: true
1064
+ }));
1065
+ this.dispatchEvent(new CustomEvent(hidden ? 'fieldHidden' : 'fieldShown', {
1066
+ detail: { field },
1067
+ bubbles: true
1068
+ }));
1069
+ }
1070
+ }
1071
+
1072
+ hideField(fieldName) {
1073
+ this.setFieldHiddenState(fieldName, true);
1074
+ }
1075
+
1076
+ showField(fieldName) {
1077
+ this.setFieldHiddenState(fieldName, false);
1078
+ }
1079
+
1080
+ reorderFields(newOrder) {
1081
+ const newFields = [];
1082
+ newOrder.forEach(fieldName => {
1083
+ const field = this.fields.find(f => f.name === fieldName);
1084
+ if (field) {
1085
+ newFields.push(field);
1086
+ }
1087
+ });
1088
+ this.fields = newFields;
1089
+ this.requestUpdate();
1090
+ }
1091
+
1092
+ /*
1093
+ Rendering Logic
1094
+ */
1095
+
1096
+ render() {
1097
+ if (!this.records || !this.fields) {
1098
+ return html`
1099
+ <div id="wrapper">
1100
+ <div id="top"><slot name="top"></slot></div>
1101
+ <div id="table">
1102
+ <div id="fields"></div>
1103
+ <div id="records"></div>
1104
+ </div>
1105
+ <div id="bottom"><slot></slot></div>
1106
+ </div>
1107
+ <div style="display: none">
1108
+ <slot name="before"></slot>
1109
+ <slot name="after"></slot>
1110
+ </div>
1111
+ `;
1112
+ }
1113
+
1114
+ this.calculateColumnSizes();
1115
+ this.hasTopControls() ? this.setAttribute('top-controls', 'true') : this.removeAttribute('top-controls');
1116
+ this.hasBottomControls() ? this.setAttribute('bottom-controls', 'true') : this.removeAttribute('bottom-controls');
1117
+
1118
+ return html`
1119
+ <div id="wrapper">
1120
+ <div id="top" style="width: ${this.columnSizes.total}px"><slot name="top"></slot></div>
1121
+ <div id="table">
1122
+ <div id="fields" style="width: ${this.columnSizes.total}px">
1123
+ ${this.renderFieldsTemplate()}
1124
+ </div>
1125
+ <div id="records" style="width: ${this.columnSizes.total}px">
1126
+ ${this.renderRecordsTemplate()}
1127
+ </div>
1128
+ </div>
1129
+ <div id="bottom" style="width: ${this.columnSizes.total}px"><slot></slot></div>
1130
+ </div>
1131
+ <div style="display: none">
1132
+ <slot name="before"></slot>
1133
+ <slot name="after"></slot>
1134
+ </div>
1135
+ `;
1136
+ }
1137
+
1138
+ /*
1139
+ Static Methods
1140
+ */
1141
+
1142
+ static extractFieldsFromRecords(records, recordLimit = 100) {
1143
+ const names = new Set();
1144
+ records.slice(0, recordLimit).forEach(record => {
1145
+ Object.keys(record).forEach(name => names.add(name));
1146
+ });
1147
+ return [...names].map(name => ({ name, label: toTitleCase(name) }));
1148
+ }
1149
+
1150
+ static format(value) {
1151
+ const f = Array.isArray(value) ? Table.formatters.array : Table.formatters[typeof value];
1152
+ return f(value);
1153
+ }
1154
+
1155
+ static formatters = {
1156
+ string: v => v,
1157
+ number: v => `${v}`,
1158
+ date: v => v.toLocaleDateString(),
1159
+ boolean: v => v ? 'True' : 'False',
1160
+ array: v => v.map(i => Table.format(i)).join(', '),
1161
+ 'undefined': v => '',
1162
+ 'null': v => '<code>null</code>'
1163
+ };
1164
+
1165
+ static editors = {
1166
+ string: (value) => {
1167
+ const $i = document.createElement('input');
1168
+ $i.value = value;
1169
+ return $i;
1170
+ },
1171
+ number: (value) => {
1172
+ const $i = document.createElement('input');
1173
+ $i.type = 'number';
1174
+ $i.value = value;
1175
+ return $i;
1176
+ },
1177
+ date: (value) => {
1178
+ const $i = document.createElement('input');
1179
+ $i.type = 'date';
1180
+ $i.value = value;
1181
+ return $i;
1182
+ },
1183
+ boolean: (value) => {
1184
+ const $i = document.createElement('select');
1185
+ $i.innerHTML = `
1186
+ <option value="true" ${value ? 'selected' : ''}>True</option>
1187
+ <option value="false" ${!value ? 'selected' : ''}>False</option>
1188
+ `;
1189
+ $i.value = value;
1190
+ return $i;
1191
+ },
1192
+ calculated: (value) => {
1193
+ const $i = document.createElement('input');
1194
+ $i.disabled = true;
1195
+ $i.value = value;
1196
+ return $i;
1197
+ }
1198
+ };
1199
+ }
1200
+
1201
+ // Register custom element
1202
+ window.customElements.define('k-table', Table);