@uibit/table 0.1.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/LICENSE +21 -0
- package/README.md +25 -0
- package/custom-elements.json +978 -0
- package/dist/frameworks/angular/index.ts +93 -0
- package/dist/frameworks/astro/index.d.ts +10 -0
- package/dist/frameworks/nuxt/index.ts +7 -0
- package/dist/frameworks/preact/index.d.ts +25 -0
- package/dist/frameworks/qwik/index.tsx +10 -0
- package/dist/frameworks/react/index.d.ts +29 -0
- package/dist/frameworks/solid/index.d.ts +26 -0
- package/dist/frameworks/stencil/index.d.ts +24 -0
- package/dist/frameworks/svelte/index.svelte +91 -0
- package/dist/frameworks/vue/index.ts +34 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +530 -0
- package/dist/styles.js.map +1 -0
- package/dist/table.d.ts +98 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +1121 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
package/dist/table.js
ADDED
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
|
+
import { customElement, fromLucide, getIcon, msg, str, UIBitElement } from '@uibit/core';
|
|
9
|
+
import { ChevronLeft, ChevronRight } from 'lucide';
|
|
10
|
+
import { property, state } from 'lit/decorators.js';
|
|
11
|
+
import { styles } from './styles';
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a standard HTML `<table>` in the default slot and layers on a full
|
|
14
|
+
* datagrid feature set. The source table is parsed once on slot assignment and
|
|
15
|
+
* never mutated; all rendering happens inside shadow DOM.
|
|
16
|
+
*
|
|
17
|
+
* @slot - A standard `<table>` element
|
|
18
|
+
* @slot search-placeholder - A slot to customize/translate the search input placeholder
|
|
19
|
+
*
|
|
20
|
+
* @fires {{ query: string }} search
|
|
21
|
+
* @fires {{ sorts: SortEntry[] }} sort
|
|
22
|
+
* @fires {{ page: number }} page-change
|
|
23
|
+
* @fires {{ indices: number[], rows: string[][] }} row-select
|
|
24
|
+
* @fires load-more - Dispatched when infiniteScroll is enabled and scrolled near the bottom
|
|
25
|
+
*
|
|
26
|
+
* @cssprop [--uibit-table-font-size=0.875rem]
|
|
27
|
+
* @cssprop [--uibit-table-font-family=inherit]
|
|
28
|
+
* @cssprop [--uibit-table-color=#111827]
|
|
29
|
+
* @cssprop [--uibit-table-border-color=#e5e7eb]
|
|
30
|
+
* @cssprop [--uibit-table-radius=0.375rem]
|
|
31
|
+
* @cssprop [--uibit-table-bg=#ffffff]
|
|
32
|
+
* @cssprop [--uibit-table-head-bg=#f9fafb]
|
|
33
|
+
* @cssprop [--uibit-table-head-color=#6b7280]
|
|
34
|
+
* @cssprop [--uibit-table-hover-bg=#f9fafb]
|
|
35
|
+
* @cssprop [--uibit-table-selected-bg=#eff6ff]
|
|
36
|
+
* @cssprop [--uibit-table-stripe-bg=#f9fafb]
|
|
37
|
+
* @cssprop [--uibit-table-highlight-bg=#fef9c3]
|
|
38
|
+
* @cssprop [--uibit-table-filter-highlight-bg=#dcfce7]
|
|
39
|
+
* @cssprop [--uibit-table-focus-color=#6b7280]
|
|
40
|
+
* @cssprop [--uibit-table-cell-padding=0.625rem 0.875rem]
|
|
41
|
+
* @cssprop [--uibit-table-max-height=24rem] - Max height when sticky-header is set
|
|
42
|
+
*/
|
|
43
|
+
const defaultTrueBoolean = {
|
|
44
|
+
fromAttribute: (value) => value === null ? true : value !== 'false',
|
|
45
|
+
toAttribute: (value) => value ? '' : null
|
|
46
|
+
};
|
|
47
|
+
let Table = class Table extends UIBitElement {
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
// ── Feature toggles ────────────────────────────────────────────
|
|
51
|
+
/** Show the global search input. */
|
|
52
|
+
this.searchable = true;
|
|
53
|
+
/** Show pagination controls and per-page selector. */
|
|
54
|
+
this.paginated = true;
|
|
55
|
+
/** Show CSV export button. */
|
|
56
|
+
this.exportable = true;
|
|
57
|
+
/** Enable row-selection checkboxes. */
|
|
58
|
+
this.selectable = false;
|
|
59
|
+
/** Show per-column filter row below the header. */
|
|
60
|
+
this.filterable = false;
|
|
61
|
+
/** Enable drag-to-resize column handles. */
|
|
62
|
+
this.resizable = false;
|
|
63
|
+
/** Show column visibility toggle in the toolbar. */
|
|
64
|
+
this.columnChooser = false;
|
|
65
|
+
/** Alternating row background. */
|
|
66
|
+
this.striped = false;
|
|
67
|
+
/** Stick the header row when the table overflows vertically. */
|
|
68
|
+
this.stickyHeader = false;
|
|
69
|
+
// ── Config ─────────────────────────────────────────────────────
|
|
70
|
+
/** Comma-separated rows-per-page options. */
|
|
71
|
+
this.pageSizes = '10,25,50,100';
|
|
72
|
+
/** Controls layout: 'inline' or consolidated 'menu' under a dropdown. */
|
|
73
|
+
this.controlsLayout = 'inline';
|
|
74
|
+
/** Enable infinite scroll behavior instead of page pagination. */
|
|
75
|
+
this.infiniteScroll = false;
|
|
76
|
+
/** Indicates the table is loading more items in infinite scroll mode. */
|
|
77
|
+
this.loading = false;
|
|
78
|
+
// ── Reactive state ──────────────────────────────────────────────
|
|
79
|
+
this._cols = [];
|
|
80
|
+
this._rows = [];
|
|
81
|
+
this._query = '';
|
|
82
|
+
this._sorts = [];
|
|
83
|
+
this._page = 1;
|
|
84
|
+
this._perPage = 10;
|
|
85
|
+
this._selected = new Set();
|
|
86
|
+
this._hiddenCols = new Set();
|
|
87
|
+
this._colFilters = new Map();
|
|
88
|
+
this._colMenuOpen = false;
|
|
89
|
+
this._optionsMenuOpen = false;
|
|
90
|
+
this._allFilteredSelected = false;
|
|
91
|
+
this._searchPlaceholderText = '';
|
|
92
|
+
// ── Non-reactive internal ───────────────────────────────────────
|
|
93
|
+
this._colWidths = new Map();
|
|
94
|
+
this._resizing = null;
|
|
95
|
+
this._rafId = 0;
|
|
96
|
+
// ── Header alignment ───────────────────────────────────────────
|
|
97
|
+
this._resizeHandler = () => this._updateHeaderHeight();
|
|
98
|
+
this._cleanupResizeListeners = () => { };
|
|
99
|
+
}
|
|
100
|
+
static { this.styles = styles; }
|
|
101
|
+
// ── Lifecycle ───────────────────────────────────────────────────
|
|
102
|
+
connectedCallback() {
|
|
103
|
+
super.connectedCallback();
|
|
104
|
+
const sizes = this._pageSizeOptions;
|
|
105
|
+
if (sizes.length)
|
|
106
|
+
this._perPage = sizes[0];
|
|
107
|
+
window.addEventListener('resize', this._resizeHandler);
|
|
108
|
+
}
|
|
109
|
+
disconnectedCallback() {
|
|
110
|
+
super.disconnectedCallback();
|
|
111
|
+
this._cleanupResizeListeners();
|
|
112
|
+
this._removeMenuListener();
|
|
113
|
+
window.removeEventListener('resize', this._resizeHandler);
|
|
114
|
+
}
|
|
115
|
+
firstUpdated(changedProperties) {
|
|
116
|
+
super.firstUpdated(changedProperties);
|
|
117
|
+
this._updateHeaderHeight();
|
|
118
|
+
const root = this.shadowRoot;
|
|
119
|
+
if (root) {
|
|
120
|
+
// Scroll listener on wrapper
|
|
121
|
+
const wrap = root.querySelector('.table-wrap');
|
|
122
|
+
if (wrap) {
|
|
123
|
+
wrap.addEventListener('scroll', (e) => this._onScroll(e));
|
|
124
|
+
}
|
|
125
|
+
// Slotchange listener delegation
|
|
126
|
+
root.addEventListener('slotchange', (e) => {
|
|
127
|
+
const slot = e.target;
|
|
128
|
+
if (slot.name === 'search-placeholder') {
|
|
129
|
+
this._onSearchPlaceholderChange(e);
|
|
130
|
+
}
|
|
131
|
+
else if (!slot.name) {
|
|
132
|
+
this._onSlotChange(e);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Mousedown listener delegation for resize handles
|
|
136
|
+
root.addEventListener('mousedown', (e) => {
|
|
137
|
+
const target = e.target;
|
|
138
|
+
if (target.classList.contains('resize-handle')) {
|
|
139
|
+
const th = target.closest('th');
|
|
140
|
+
const colKey = th?.getAttribute('data-col-key');
|
|
141
|
+
const col = this._cols.find(c => c.key === colKey);
|
|
142
|
+
if (col) {
|
|
143
|
+
this._onResizeStart(e, col);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// Click listener delegation
|
|
148
|
+
root.addEventListener('click', (e) => {
|
|
149
|
+
const target = e.target;
|
|
150
|
+
// Column sort click
|
|
151
|
+
const th = target.closest('th');
|
|
152
|
+
if (th && th.classList.contains('sortable')) {
|
|
153
|
+
const isResize = target.classList.contains('resize-handle');
|
|
154
|
+
if (!isResize) {
|
|
155
|
+
const colKey = th.getAttribute('data-col-key');
|
|
156
|
+
const col = this._cols.find(c => c.key === colKey);
|
|
157
|
+
if (col) {
|
|
158
|
+
this._onSort(col, e);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Options menu button click
|
|
164
|
+
const optionsBtn = target.closest('.options-menu-wrap button.ctrl-btn');
|
|
165
|
+
if (optionsBtn) {
|
|
166
|
+
this._optionsMenuOpen ? (this._optionsMenuOpen = false, this._removeMenuListener()) : this._openOptionsMenu();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Column chooser button click
|
|
170
|
+
const colBtn = target.closest('.col-menu-wrap button.ctrl-btn');
|
|
171
|
+
if (colBtn && !optionsBtn) {
|
|
172
|
+
this._colMenuOpen ? (this._colMenuOpen = false, this._removeMenuListener()) : this._openColMenu();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Selection banner buttons
|
|
176
|
+
const clearBtn = target.closest('.sel-banner-btn');
|
|
177
|
+
if (clearBtn) {
|
|
178
|
+
if (clearBtn.classList.contains('sel-banner-btn-clear') || clearBtn.textContent?.includes('Clear')) {
|
|
179
|
+
this._clearSelection();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this._onSelectAllFiltered();
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Options dropdown item click
|
|
187
|
+
const dropdownBtn = target.closest('.options-dropdown .dropdown-btn');
|
|
188
|
+
if (dropdownBtn) {
|
|
189
|
+
if (dropdownBtn.classList.contains('dropdown-btn-danger')) {
|
|
190
|
+
this._query = '';
|
|
191
|
+
this._colFilters = new Map();
|
|
192
|
+
this._page = 1;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this._exportCsv();
|
|
196
|
+
}
|
|
197
|
+
this._optionsMenuOpen = false;
|
|
198
|
+
this._removeMenuListener();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Pagination buttons click
|
|
202
|
+
const pageBtn = target.closest('.pagination .page-btn');
|
|
203
|
+
if (pageBtn) {
|
|
204
|
+
const action = pageBtn.getAttribute('data-page-action');
|
|
205
|
+
const numAttr = pageBtn.getAttribute('data-page-num');
|
|
206
|
+
if (action === 'prev') {
|
|
207
|
+
this._page = Math.max(1, this._page - 1);
|
|
208
|
+
this.dispatchCustomEvent('page-change', { page: this._page });
|
|
209
|
+
}
|
|
210
|
+
else if (action === 'next') {
|
|
211
|
+
this._page = Math.min(this._totalPages, this._page + 1);
|
|
212
|
+
this.dispatchCustomEvent('page-change', { page: this._page });
|
|
213
|
+
}
|
|
214
|
+
else if (numAttr !== null) {
|
|
215
|
+
this._page = parseInt(numAttr, 10);
|
|
216
|
+
this.dispatchCustomEvent('page-change', { page: this._page });
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Inline Clear Filters button click
|
|
221
|
+
const inlineClearBtn = target.closest('.controls .ctrl-btn-clear-filters');
|
|
222
|
+
if (inlineClearBtn) {
|
|
223
|
+
this._query = '';
|
|
224
|
+
this._colFilters = new Map();
|
|
225
|
+
this._page = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Export CSV button click
|
|
229
|
+
const exportBtn = target.closest('[part="export-btn"]');
|
|
230
|
+
if (exportBtn) {
|
|
231
|
+
this._exportCsv();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Row click delegation (for selection)
|
|
235
|
+
this._onTableClick(e);
|
|
236
|
+
});
|
|
237
|
+
// Change listener delegation
|
|
238
|
+
root.addEventListener('change', (e) => {
|
|
239
|
+
const target = e.target;
|
|
240
|
+
// Select all header checkbox
|
|
241
|
+
if (target.closest('thead th.col-check input')) {
|
|
242
|
+
this._onSelectAllPage(target.checked);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Row select checkbox
|
|
246
|
+
if (target.closest('tbody td.col-check input')) {
|
|
247
|
+
this._onTableChange(e);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Column show/hide checkbox in options/column dropdowns
|
|
251
|
+
if (target.closest('.col-dropdown-item input')) {
|
|
252
|
+
const label = target.closest('.col-dropdown-item');
|
|
253
|
+
const colLabel = label?.textContent?.trim();
|
|
254
|
+
const col = this._cols.find(c => c.label === colLabel);
|
|
255
|
+
if (col) {
|
|
256
|
+
this._onToggleCol(col);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Rows per page dropdown
|
|
261
|
+
if (target.classList.contains('footer-select')) {
|
|
262
|
+
this._onPerPage(e);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// Input listener delegation
|
|
267
|
+
root.addEventListener('input', (e) => {
|
|
268
|
+
const target = e.target;
|
|
269
|
+
if (target.classList.contains('search')) {
|
|
270
|
+
this._onSearch(e);
|
|
271
|
+
}
|
|
272
|
+
else if (target.classList.contains('filter-input')) {
|
|
273
|
+
const th = target.closest('th');
|
|
274
|
+
const colKey = th?.getAttribute('data-col-key');
|
|
275
|
+
const col = this._cols.find(c => c.key === colKey);
|
|
276
|
+
if (col) {
|
|
277
|
+
this._onColFilter(col, e);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// If slotchange hasn't fired yet (e.g. in test environment), parse manually
|
|
283
|
+
if (this._rows.length === 0) {
|
|
284
|
+
const slot = this.shadowRoot?.querySelector('slot');
|
|
285
|
+
if (slot) {
|
|
286
|
+
this._onSlotChange({ target: slot });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const placeholderSlot = this.shadowRoot?.querySelector('slot[name="search-placeholder"]');
|
|
290
|
+
if (placeholderSlot) {
|
|
291
|
+
this._onSearchPlaceholderChange({ target: placeholderSlot });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
updated(changedProperties) {
|
|
295
|
+
super.updated(changedProperties);
|
|
296
|
+
const selectAllCheckbox = this.shadowRoot?.querySelector('thead th.col-check input');
|
|
297
|
+
if (selectAllCheckbox) {
|
|
298
|
+
selectAllCheckbox.indeterminate = !this._allPageSelected && this._somePageSelected;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ── Data pipeline ───────────────────────────────────────────────
|
|
302
|
+
get _pageSizeOptions() {
|
|
303
|
+
return this.pageSizes.split(',').map(n => parseInt(n.trim(), 10)).filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
get _visibleCols() {
|
|
306
|
+
return this._cols.filter(c => !this._hiddenCols.has(c.key));
|
|
307
|
+
}
|
|
308
|
+
get _indexed() {
|
|
309
|
+
return this._rows.map((row, i) => ({ i, row }));
|
|
310
|
+
}
|
|
311
|
+
get _colFiltered() {
|
|
312
|
+
if (!this._colFilters.size)
|
|
313
|
+
return this._indexed;
|
|
314
|
+
return this._indexed.filter(({ row }) => {
|
|
315
|
+
for (const [key, val] of this._colFilters) {
|
|
316
|
+
if (!val)
|
|
317
|
+
continue;
|
|
318
|
+
const ci = this._cols.findIndex(c => c.key === key);
|
|
319
|
+
if (ci < 0)
|
|
320
|
+
continue;
|
|
321
|
+
if (!(row[ci] ?? '').toLowerCase().includes(val.toLowerCase()))
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
get _searched() {
|
|
328
|
+
if (!this._query)
|
|
329
|
+
return this._colFiltered;
|
|
330
|
+
const q = this._query.toLowerCase();
|
|
331
|
+
return this._colFiltered.filter(({ row }) => row.some(cell => cell.toLowerCase().includes(q)));
|
|
332
|
+
}
|
|
333
|
+
get _sortedRows() {
|
|
334
|
+
const rows = [...this._searched];
|
|
335
|
+
if (!this._sorts.length)
|
|
336
|
+
return rows;
|
|
337
|
+
return rows.sort((a, b) => {
|
|
338
|
+
for (const { key, dir } of this._sorts) {
|
|
339
|
+
const ci = this._cols.findIndex(c => c.key === key);
|
|
340
|
+
if (ci < 0)
|
|
341
|
+
continue;
|
|
342
|
+
const numeric = this._cols[ci]?.numeric ?? false;
|
|
343
|
+
const av = a.row[ci] ?? '';
|
|
344
|
+
const bv = b.row[ci] ?? '';
|
|
345
|
+
const cmp = numeric
|
|
346
|
+
? parseFloat(av.replace(/[^0-9.-]/g, '')) - parseFloat(bv.replace(/[^0-9.-]/g, ''))
|
|
347
|
+
: av.localeCompare(bv, undefined, { sensitivity: 'base' });
|
|
348
|
+
if (cmp !== 0)
|
|
349
|
+
return dir === 'asc' ? cmp : -cmp;
|
|
350
|
+
}
|
|
351
|
+
return 0;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
get _pageRows() {
|
|
355
|
+
if (this.infiniteScroll || !this.paginated)
|
|
356
|
+
return this._sortedRows;
|
|
357
|
+
const start = (this._page - 1) * this._perPage;
|
|
358
|
+
return this._sortedRows.slice(start, start + this._perPage);
|
|
359
|
+
}
|
|
360
|
+
get _totalPages() {
|
|
361
|
+
return Math.max(1, Math.ceil(this._sortedRows.length / this._perPage));
|
|
362
|
+
}
|
|
363
|
+
get _allPageSelected() {
|
|
364
|
+
const page = this._pageRows;
|
|
365
|
+
return page.length > 0 && page.every(({ i }) => this._selected.has(i));
|
|
366
|
+
}
|
|
367
|
+
get _somePageSelected() {
|
|
368
|
+
return this._pageRows.some(({ i }) => this._selected.has(i));
|
|
369
|
+
}
|
|
370
|
+
_updateHeaderHeight() {
|
|
371
|
+
if (!this.shadowRoot)
|
|
372
|
+
return;
|
|
373
|
+
const firstRow = this.shadowRoot.querySelector('thead tr:first-child');
|
|
374
|
+
if (firstRow) {
|
|
375
|
+
const height = firstRow.getBoundingClientRect().height;
|
|
376
|
+
this.style.setProperty('--uibit-table-header-height', `${height}px`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ── Slot parsing ────────────────────────────────────────────────
|
|
380
|
+
_onSlotChange(e) {
|
|
381
|
+
const slot = e.target;
|
|
382
|
+
let table = slot.assignedElements({ flatten: true }).find(el => el.tagName === 'TABLE');
|
|
383
|
+
if (!table) {
|
|
384
|
+
table = this.querySelector('table');
|
|
385
|
+
}
|
|
386
|
+
if (!table)
|
|
387
|
+
return;
|
|
388
|
+
this._cols = Array.from(table.querySelectorAll('thead th, thead td')).map(th => {
|
|
389
|
+
const label = th.textContent?.trim() ?? '';
|
|
390
|
+
return {
|
|
391
|
+
key: label.toLowerCase().replace(/\W+/g, '_') || `col_${Math.random().toString(36).slice(2)}`,
|
|
392
|
+
label,
|
|
393
|
+
sortable: th.getAttribute('data-sortable') !== 'false',
|
|
394
|
+
numeric: th.getAttribute('data-type') === 'number',
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
this._rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => Array.from(tr.querySelectorAll('td, th')).map(td => td.textContent?.trim() ?? ''));
|
|
398
|
+
this._page = 1;
|
|
399
|
+
this._selected = new Set();
|
|
400
|
+
this._colFilters = new Map();
|
|
401
|
+
this._sorts = [];
|
|
402
|
+
this.updateComplete.then(() => {
|
|
403
|
+
this._updateHeaderHeight();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
_onSearchPlaceholderChange(e) {
|
|
407
|
+
const slot = e.target;
|
|
408
|
+
let text = slot.assignedNodes({ flatten: true }).map(n => n.textContent).join('').trim();
|
|
409
|
+
if (!text) {
|
|
410
|
+
text = this.querySelector('[slot="search-placeholder"]')?.textContent?.trim() || '';
|
|
411
|
+
}
|
|
412
|
+
this._searchPlaceholderText = text;
|
|
413
|
+
}
|
|
414
|
+
// ── Event handlers ──────────────────────────────────────────────
|
|
415
|
+
_onSearch(e) {
|
|
416
|
+
this._query = e.target.value;
|
|
417
|
+
this._page = 1;
|
|
418
|
+
this._allFilteredSelected = false;
|
|
419
|
+
this.dispatchCustomEvent('search', { query: this._query });
|
|
420
|
+
}
|
|
421
|
+
_onSort(col, e) {
|
|
422
|
+
if (!col.sortable)
|
|
423
|
+
return;
|
|
424
|
+
const shift = e.shiftKey && this._sorts.length > 0;
|
|
425
|
+
if (shift) {
|
|
426
|
+
const idx = this._sorts.findIndex(s => s.key === col.key);
|
|
427
|
+
if (idx >= 0) {
|
|
428
|
+
const cur = this._sorts[idx];
|
|
429
|
+
if (cur.dir === 'asc') {
|
|
430
|
+
this._sorts = [...this._sorts.slice(0, idx), { key: col.key, dir: 'desc' }, ...this._sorts.slice(idx + 1)];
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this._sorts = [...this._sorts.slice(0, idx), ...this._sorts.slice(idx + 1)];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
this._sorts = [...this._sorts, { key: col.key, dir: 'asc' }];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const cur = this._sorts.length === 1 && this._sorts[0].key === col.key ? this._sorts[0] : null;
|
|
442
|
+
if (!cur) {
|
|
443
|
+
this._sorts = [{ key: col.key, dir: 'asc' }];
|
|
444
|
+
}
|
|
445
|
+
else if (cur.dir === 'asc') {
|
|
446
|
+
this._sorts = [{ key: col.key, dir: 'desc' }];
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
this._sorts = [];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
this._page = 1;
|
|
453
|
+
if (this._sorts.length) {
|
|
454
|
+
this.dispatchCustomEvent('sort', { sorts: this._sorts });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
_onPerPage(e) {
|
|
458
|
+
this._perPage = parseInt(e.target.value, 10);
|
|
459
|
+
this._page = 1;
|
|
460
|
+
}
|
|
461
|
+
_onColFilter(col, e) {
|
|
462
|
+
const val = e.target.value;
|
|
463
|
+
const next = new Map(this._colFilters);
|
|
464
|
+
if (val)
|
|
465
|
+
next.set(col.key, val);
|
|
466
|
+
else
|
|
467
|
+
next.delete(col.key);
|
|
468
|
+
this._colFilters = next;
|
|
469
|
+
this._page = 1;
|
|
470
|
+
this._allFilteredSelected = false;
|
|
471
|
+
}
|
|
472
|
+
_onSelectRow(i) {
|
|
473
|
+
const next = new Set(this._selected);
|
|
474
|
+
if (next.has(i))
|
|
475
|
+
next.delete(i);
|
|
476
|
+
else
|
|
477
|
+
next.add(i);
|
|
478
|
+
this._selected = next;
|
|
479
|
+
this._allFilteredSelected = false;
|
|
480
|
+
this._emitSelect();
|
|
481
|
+
}
|
|
482
|
+
_onSelectAllPage(checked) {
|
|
483
|
+
const next = new Set(this._selected);
|
|
484
|
+
for (const { i } of this._pageRows) {
|
|
485
|
+
if (checked)
|
|
486
|
+
next.add(i);
|
|
487
|
+
else
|
|
488
|
+
next.delete(i);
|
|
489
|
+
}
|
|
490
|
+
this._selected = next;
|
|
491
|
+
this._allFilteredSelected = false;
|
|
492
|
+
this._emitSelect();
|
|
493
|
+
}
|
|
494
|
+
_onTableClick(e) {
|
|
495
|
+
if (!this.selectable)
|
|
496
|
+
return;
|
|
497
|
+
const target = e.target;
|
|
498
|
+
const isCheckbox = target.tagName === 'INPUT' && target.type === 'checkbox';
|
|
499
|
+
if (isCheckbox)
|
|
500
|
+
return;
|
|
501
|
+
const tr = target.closest('tr');
|
|
502
|
+
if (!tr)
|
|
503
|
+
return;
|
|
504
|
+
const indexAttr = tr.getAttribute('data-row-index');
|
|
505
|
+
if (indexAttr === null)
|
|
506
|
+
return;
|
|
507
|
+
const i = parseInt(indexAttr, 10);
|
|
508
|
+
this._onSelectRow(i);
|
|
509
|
+
}
|
|
510
|
+
_onTableChange(e) {
|
|
511
|
+
const target = e.target;
|
|
512
|
+
if (target.tagName === 'INPUT' && target.type === 'checkbox') {
|
|
513
|
+
const tr = target.closest('tr');
|
|
514
|
+
if (!tr)
|
|
515
|
+
return;
|
|
516
|
+
const indexAttr = tr.getAttribute('data-row-index');
|
|
517
|
+
if (indexAttr === null)
|
|
518
|
+
return;
|
|
519
|
+
const i = parseInt(indexAttr, 10);
|
|
520
|
+
this._onSelectRow(i);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
_onSelectAllFiltered() {
|
|
524
|
+
const next = new Set();
|
|
525
|
+
for (const { i } of this._sortedRows)
|
|
526
|
+
next.add(i);
|
|
527
|
+
this._selected = next;
|
|
528
|
+
this._allFilteredSelected = true;
|
|
529
|
+
this._emitSelect();
|
|
530
|
+
}
|
|
531
|
+
_clearSelection() {
|
|
532
|
+
this._selected = new Set();
|
|
533
|
+
this._allFilteredSelected = false;
|
|
534
|
+
this._emitSelect();
|
|
535
|
+
}
|
|
536
|
+
_emitSelect() {
|
|
537
|
+
const indices = [...this._selected];
|
|
538
|
+
const rows = indices.map(i => this._rows[i]);
|
|
539
|
+
this.dispatchCustomEvent('row-select', { indices, rows });
|
|
540
|
+
}
|
|
541
|
+
_onToggleCol(col) {
|
|
542
|
+
const next = new Set(this._hiddenCols);
|
|
543
|
+
if (next.has(col.key))
|
|
544
|
+
next.delete(col.key);
|
|
545
|
+
else
|
|
546
|
+
next.add(col.key);
|
|
547
|
+
this._hiddenCols = next;
|
|
548
|
+
this.updateComplete.then(() => {
|
|
549
|
+
this._updateHeaderHeight();
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
_openColMenu() {
|
|
553
|
+
this._colMenuOpen = true;
|
|
554
|
+
this._removeMenuListener();
|
|
555
|
+
this._closeMenuHandler = (e) => {
|
|
556
|
+
const path = e.composedPath();
|
|
557
|
+
const wrap = this.shadowRoot?.querySelector('.col-menu-wrap');
|
|
558
|
+
if (wrap && !path.includes(wrap)) {
|
|
559
|
+
this._colMenuOpen = false;
|
|
560
|
+
this._removeMenuListener();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
this._escMenuHandler = (e) => {
|
|
564
|
+
if (e.key === 'Escape') {
|
|
565
|
+
this._colMenuOpen = false;
|
|
566
|
+
this._removeMenuListener();
|
|
567
|
+
const btn = this.shadowRoot?.querySelector('.col-menu-wrap .ctrl-btn');
|
|
568
|
+
btn?.focus();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
setTimeout(() => {
|
|
572
|
+
document.addEventListener('click', this._closeMenuHandler);
|
|
573
|
+
document.addEventListener('keydown', this._escMenuHandler);
|
|
574
|
+
}, 0);
|
|
575
|
+
}
|
|
576
|
+
_openOptionsMenu() {
|
|
577
|
+
this._optionsMenuOpen = true;
|
|
578
|
+
this._removeMenuListener();
|
|
579
|
+
this._closeMenuHandler = (e) => {
|
|
580
|
+
const path = e.composedPath();
|
|
581
|
+
const wrap = this.shadowRoot?.querySelector('.options-menu-wrap');
|
|
582
|
+
if (wrap && !path.includes(wrap)) {
|
|
583
|
+
this._optionsMenuOpen = false;
|
|
584
|
+
this._removeMenuListener();
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
this._escMenuHandler = (e) => {
|
|
588
|
+
if (e.key === 'Escape') {
|
|
589
|
+
this._optionsMenuOpen = false;
|
|
590
|
+
this._removeMenuListener();
|
|
591
|
+
const btn = this.shadowRoot?.querySelector('.options-menu-wrap .ctrl-btn');
|
|
592
|
+
btn?.focus();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
setTimeout(() => {
|
|
596
|
+
document.addEventListener('click', this._closeMenuHandler);
|
|
597
|
+
document.addEventListener('keydown', this._escMenuHandler);
|
|
598
|
+
}, 0);
|
|
599
|
+
}
|
|
600
|
+
_removeMenuListener() {
|
|
601
|
+
if (this._closeMenuHandler) {
|
|
602
|
+
document.removeEventListener('click', this._closeMenuHandler);
|
|
603
|
+
this._closeMenuHandler = undefined;
|
|
604
|
+
}
|
|
605
|
+
if (this._escMenuHandler) {
|
|
606
|
+
document.removeEventListener('keydown', this._escMenuHandler);
|
|
607
|
+
this._escMenuHandler = undefined;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// ── Infinite scroll ──────────────────────────────────────────────
|
|
611
|
+
_onScroll(e) {
|
|
612
|
+
if (!this.infiniteScroll || this.loading)
|
|
613
|
+
return;
|
|
614
|
+
const wrap = e.currentTarget;
|
|
615
|
+
const threshold = 30; // px threshold from bottom
|
|
616
|
+
const isNearBottom = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight <= threshold;
|
|
617
|
+
if (isNearBottom) {
|
|
618
|
+
this.dispatchCustomEvent('load-more');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// ── Column resize ───────────────────────────────────────────────
|
|
622
|
+
_onResizeStart(e, col) {
|
|
623
|
+
e.stopPropagation();
|
|
624
|
+
e.preventDefault();
|
|
625
|
+
const th = e.currentTarget.closest('th');
|
|
626
|
+
const startW = th.getBoundingClientRect().width;
|
|
627
|
+
this._resizing = { key: col.key, startX: e.clientX, startW };
|
|
628
|
+
this.shadowRoot?.querySelector('.table-wrap')?.classList.add('resizing');
|
|
629
|
+
const onMove = (ev) => {
|
|
630
|
+
if (!this._resizing)
|
|
631
|
+
return;
|
|
632
|
+
const diff = ev.clientX - this._resizing.startX;
|
|
633
|
+
const newW = Math.max(60, this._resizing.startW + diff);
|
|
634
|
+
this._colWidths.set(this._resizing.key, newW);
|
|
635
|
+
if (!this._rafId) {
|
|
636
|
+
this._rafId = requestAnimationFrame(() => {
|
|
637
|
+
this._rafId = 0;
|
|
638
|
+
this.requestUpdate();
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const onUp = () => {
|
|
643
|
+
this._resizing = null;
|
|
644
|
+
this.shadowRoot?.querySelector('.table-wrap')?.classList.remove('resizing');
|
|
645
|
+
this._cleanupResizeListeners();
|
|
646
|
+
};
|
|
647
|
+
document.addEventListener('mousemove', onMove);
|
|
648
|
+
document.addEventListener('mouseup', onUp, { once: true });
|
|
649
|
+
this._cleanupResizeListeners = () => {
|
|
650
|
+
document.removeEventListener('mousemove', onMove);
|
|
651
|
+
document.removeEventListener('mouseup', onUp);
|
|
652
|
+
this._cleanupResizeListeners = () => { };
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// ── Export ──────────────────────────────────────────────────────
|
|
656
|
+
_exportCsv() {
|
|
657
|
+
const exportRows = this._selected.size > 0
|
|
658
|
+
? [...this._selected].map(i => this._rows[i])
|
|
659
|
+
: this._sortedRows.map(r => r.row);
|
|
660
|
+
const visKeys = new Set(this._visibleCols.map(c => c.key));
|
|
661
|
+
const visCols = this._cols.filter(c => visKeys.has(c.key));
|
|
662
|
+
const csvCell = (val) => {
|
|
663
|
+
const escaped = val.replace(/"/g, '""');
|
|
664
|
+
return /^[=+\-@\t\r]/.test(val) ? `"'${escaped}"` : `"${escaped}"`;
|
|
665
|
+
};
|
|
666
|
+
const header = visCols.map(c => csvCell(c.label)).join(',');
|
|
667
|
+
const colIdxs = visCols.map(c => this._cols.findIndex(col => col.key === c.key));
|
|
668
|
+
const body = exportRows.map(row => colIdxs.map(ci => csvCell(row[ci] ?? '')).join(','));
|
|
669
|
+
const csv = [header, ...body].join('\n');
|
|
670
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
671
|
+
const url = URL.createObjectURL(blob);
|
|
672
|
+
const a = document.createElement('a');
|
|
673
|
+
a.href = url;
|
|
674
|
+
a.download = 'table-export.csv';
|
|
675
|
+
a.click();
|
|
676
|
+
URL.revokeObjectURL(url);
|
|
677
|
+
}
|
|
678
|
+
// ── Highlight helpers ───────────────────────────────────────────
|
|
679
|
+
_highlight(text, colKey) {
|
|
680
|
+
let result = text;
|
|
681
|
+
const colFilter = this._colFilters.get(colKey);
|
|
682
|
+
if (colFilter) {
|
|
683
|
+
result = this._applyHighlight(text, colFilter, 'filter-highlight');
|
|
684
|
+
}
|
|
685
|
+
if (this._query) {
|
|
686
|
+
const src = typeof result === 'string' ? result : text;
|
|
687
|
+
return this._applyHighlight(src, this._query, 'highlight');
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
_applyHighlight(text, term, cls) {
|
|
692
|
+
const idx = text.toLowerCase().indexOf(term.toLowerCase());
|
|
693
|
+
if (idx < 0)
|
|
694
|
+
return text;
|
|
695
|
+
return html `${text.slice(0, idx)}<mark class=${cls}>${text.slice(idx, idx + term.length)}</mark>${text.slice(idx + term.length)}`;
|
|
696
|
+
}
|
|
697
|
+
// ── Render helpers ──────────────────────────────────────────────
|
|
698
|
+
_colStyle(col) {
|
|
699
|
+
const w = this._colWidths.get(col.key);
|
|
700
|
+
return w ? `width:${w}px;min-width:${w}px` : '';
|
|
701
|
+
}
|
|
702
|
+
_renderSortIcon(col) {
|
|
703
|
+
if (!col.sortable)
|
|
704
|
+
return nothing;
|
|
705
|
+
const entry = this._sorts.find(s => s.key === col.key);
|
|
706
|
+
const asc = entry?.dir === 'asc';
|
|
707
|
+
const desc = entry?.dir === 'desc';
|
|
708
|
+
const sortIdx = this._sorts.findIndex(s => s.key === col.key);
|
|
709
|
+
return html `
|
|
710
|
+
<svg class="sort-icon" viewBox="0 0 10 14" fill="none" aria-hidden="true">
|
|
711
|
+
<path d="M5 1v12M2 4l3-3 3 3" stroke="currentColor" stroke-width="1.4"
|
|
712
|
+
stroke-linecap="round" stroke-linejoin="round"
|
|
713
|
+
opacity=${asc ? '1' : entry ? '0.25' : '0.35'} />
|
|
714
|
+
<path d="M5 13V1M2 10l3 3 3-3" stroke="currentColor" stroke-width="1.4"
|
|
715
|
+
stroke-linecap="round" stroke-linejoin="round"
|
|
716
|
+
opacity=${desc ? '1' : entry ? '0.25' : '0.35'} />
|
|
717
|
+
</svg>
|
|
718
|
+
${this._sorts.length > 1 && sortIdx >= 0
|
|
719
|
+
? html `<span class="sort-badge">${sortIdx + 1}</span>`
|
|
720
|
+
: nothing}
|
|
721
|
+
`;
|
|
722
|
+
}
|
|
723
|
+
_renderPagination() {
|
|
724
|
+
const total = this._totalPages;
|
|
725
|
+
if (total <= 1)
|
|
726
|
+
return nothing;
|
|
727
|
+
const pages = [];
|
|
728
|
+
if (total <= 7) {
|
|
729
|
+
for (let i = 1; i <= total; i++)
|
|
730
|
+
pages.push(i);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
pages.push(1);
|
|
734
|
+
if (this._page > 3)
|
|
735
|
+
pages.push('…');
|
|
736
|
+
for (let i = Math.max(2, this._page - 1); i <= Math.min(total - 1, this._page + 1); i++)
|
|
737
|
+
pages.push(i);
|
|
738
|
+
if (this._page < total - 2)
|
|
739
|
+
pages.push('…');
|
|
740
|
+
pages.push(total);
|
|
741
|
+
}
|
|
742
|
+
return html `
|
|
743
|
+
<nav class="pagination" aria-label=${msg('Pagination')}>
|
|
744
|
+
<button class="page-btn" ?disabled=${this._page === 1} data-page-action="prev" aria-label=${msg('Previous')}>${getIcon('chevron-left', 14, fromLucide(ChevronLeft))}</button>
|
|
745
|
+
${pages.map(p => p === '…'
|
|
746
|
+
? html `<span class="page-btn" style="cursor:default;border-color:transparent;background:transparent">…</span>`
|
|
747
|
+
: html `<button
|
|
748
|
+
class="page-btn ${this._page === p ? 'active' : ''}"
|
|
749
|
+
data-page-num=${p}
|
|
750
|
+
aria-current=${this._page === p ? 'page' : nothing}
|
|
751
|
+
>${p}</button>`)}
|
|
752
|
+
<button class="page-btn" ?disabled=${this._page === this._totalPages} data-page-action="next" aria-label=${msg('Next')}>${getIcon('chevron-right', 14, fromLucide(ChevronRight))}</button>
|
|
753
|
+
</nav>
|
|
754
|
+
`;
|
|
755
|
+
}
|
|
756
|
+
_renderSelectionBanner() {
|
|
757
|
+
if (!this.selectable || this._selected.size === 0)
|
|
758
|
+
return nothing;
|
|
759
|
+
const filteredTotal = this._sortedRows.length;
|
|
760
|
+
const selCount = this._selected.size;
|
|
761
|
+
const allFiltered = this._allFilteredSelected || selCount === filteredTotal;
|
|
762
|
+
return html `
|
|
763
|
+
<div class="sel-banner" role="status" aria-live="polite">
|
|
764
|
+
<span class="sel-banner-count">${msg(str `${selCount} row${selCount === 1 ? '' : 's'} selected`)}</span>
|
|
765
|
+
${!allFiltered && filteredTotal > this._perPage ? html `
|
|
766
|
+
<span class="sel-banner-sep">·</span>
|
|
767
|
+
<button class="sel-banner-btn sel-banner-btn-all">
|
|
768
|
+
${msg(str `Select all ${filteredTotal} rows`)}
|
|
769
|
+
</button>
|
|
770
|
+
` : nothing}
|
|
771
|
+
<span class="sel-banner-sep">·</span>
|
|
772
|
+
<button class="sel-banner-btn sel-banner-btn-clear">${msg('Clear selection')}</button>
|
|
773
|
+
</div>
|
|
774
|
+
`;
|
|
775
|
+
}
|
|
776
|
+
_renderColMenu() {
|
|
777
|
+
if (!this.columnChooser)
|
|
778
|
+
return nothing;
|
|
779
|
+
return html `
|
|
780
|
+
<div class="col-menu-wrap">
|
|
781
|
+
<button
|
|
782
|
+
class="ctrl-btn"
|
|
783
|
+
aria-haspopup="true"
|
|
784
|
+
aria-expanded=${this._colMenuOpen ? 'true' : 'false'}
|
|
785
|
+
>${msg('Columns')} <span class="chevron">▾</span></button>
|
|
786
|
+
${this._colMenuOpen ? html `
|
|
787
|
+
<div class="col-dropdown" role="menu">
|
|
788
|
+
${this._cols.map(col => html `
|
|
789
|
+
<label class="col-dropdown-item" role="menuitemcheckbox">
|
|
790
|
+
<input
|
|
791
|
+
type="checkbox"
|
|
792
|
+
?checked=${!this._hiddenCols.has(col.key)}
|
|
793
|
+
/>
|
|
794
|
+
${col.label}
|
|
795
|
+
</label>
|
|
796
|
+
`)}
|
|
797
|
+
</div>
|
|
798
|
+
` : nothing}
|
|
799
|
+
</div>
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
_renderOptionsMenu() {
|
|
803
|
+
const hasActiveFilters = this._colFilters.size > 0 || this._query;
|
|
804
|
+
return html `
|
|
805
|
+
<div class="options-menu-wrap col-menu-wrap">
|
|
806
|
+
<button
|
|
807
|
+
class="ctrl-btn"
|
|
808
|
+
aria-haspopup="true"
|
|
809
|
+
aria-expanded=${this._optionsMenuOpen ? 'true' : 'false'}
|
|
810
|
+
>
|
|
811
|
+
<svg class="icon-cog" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true" style="width:14px;height:14px;display:inline-block;vertical-align:middle;margin-right:2px;">
|
|
812
|
+
<circle cx="8" cy="8" r="2"/>
|
|
813
|
+
<path d="M8 1v2m0 10v2M1 8h2m10 0h2m-2.5-4.5l-1.5 1.5M4 12l-1.5 1.5m9.5 0l-1.5-1.5M4 4L2.5 2.5"/>
|
|
814
|
+
</svg>
|
|
815
|
+
${msg('Options')}
|
|
816
|
+
<span class="chevron">▾</span>
|
|
817
|
+
</button>
|
|
818
|
+
${this._optionsMenuOpen ? html `
|
|
819
|
+
<div class="col-dropdown options-dropdown" role="menu">
|
|
820
|
+
${this.columnChooser ? html `
|
|
821
|
+
<div class="dropdown-section-title">${msg('Columns')}</div>
|
|
822
|
+
${this._cols.map(col => html `
|
|
823
|
+
<label class="col-dropdown-item" role="menuitemcheckbox">
|
|
824
|
+
<input
|
|
825
|
+
type="checkbox"
|
|
826
|
+
?checked=${!this._hiddenCols.has(col.key)}
|
|
827
|
+
/>
|
|
828
|
+
${col.label}
|
|
829
|
+
</label>
|
|
830
|
+
`)}
|
|
831
|
+
<div class="dropdown-divider"></div>
|
|
832
|
+
` : nothing}
|
|
833
|
+
|
|
834
|
+
${this.exportable ? html `
|
|
835
|
+
<button class="dropdown-btn" role="menuitem">
|
|
836
|
+
${this._selected.size > 0 ? msg(str `Export ${this._selected.size} rows`) : msg('Export CSV')}
|
|
837
|
+
</button>
|
|
838
|
+
` : nothing}
|
|
839
|
+
|
|
840
|
+
${hasActiveFilters ? html `
|
|
841
|
+
<button class="dropdown-btn dropdown-btn-danger" role="menuitem">
|
|
842
|
+
${msg('Clear filters')}
|
|
843
|
+
</button>
|
|
844
|
+
` : nothing}
|
|
845
|
+
</div>
|
|
846
|
+
` : nothing}
|
|
847
|
+
</div>
|
|
848
|
+
`;
|
|
849
|
+
}
|
|
850
|
+
_renderFilterRow() {
|
|
851
|
+
if (!this.filterable)
|
|
852
|
+
return nothing;
|
|
853
|
+
const vis = this._visibleCols;
|
|
854
|
+
return html `
|
|
855
|
+
<tr class="filter-row" aria-label=${msg('Column filters')}>
|
|
856
|
+
${this.selectable ? html `<th class="col-check"></th>` : nothing}
|
|
857
|
+
${vis.map(col => html `
|
|
858
|
+
<th style=${this._colStyle(col)} data-col-key=${col.key}>
|
|
859
|
+
<input
|
|
860
|
+
class="filter-input ${this._colFilters.get(col.key) ? 'active' : ''}"
|
|
861
|
+
type="search"
|
|
862
|
+
placeholder=${msg('Filter…')}
|
|
863
|
+
value=${this._colFilters.get(col.key) ?? ''}
|
|
864
|
+
aria-label=${msg(str `Filter ${col.label}`)}
|
|
865
|
+
/>
|
|
866
|
+
</th>
|
|
867
|
+
`)}
|
|
868
|
+
</tr>
|
|
869
|
+
`;
|
|
870
|
+
}
|
|
871
|
+
render() {
|
|
872
|
+
const vis = this._visibleCols;
|
|
873
|
+
const pageRows = this._pageRows;
|
|
874
|
+
const total = this._sortedRows.length;
|
|
875
|
+
const start = this.paginated && !this.infiniteScroll ? (this._page - 1) * this._perPage + 1 : 1;
|
|
876
|
+
const end = this.paginated && !this.infiniteScroll ? Math.min(start + this._perPage - 1, total) : total;
|
|
877
|
+
const selectAllChecked = this._allPageSelected;
|
|
878
|
+
const showToolbar = this.searchable || this.exportable || this.columnChooser || (this.paginated && !this.infiniteScroll);
|
|
879
|
+
const hasActiveFilters = this._colFilters.size > 0 || this._query;
|
|
880
|
+
return html `
|
|
881
|
+
<slot></slot>
|
|
882
|
+
<div style="display: none;">
|
|
883
|
+
<slot name="search-placeholder">${msg('Search…')}</slot>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<div style="display: contents;">
|
|
887
|
+
${showToolbar ? html `
|
|
888
|
+
<div class="toolbar" part="toolbar">
|
|
889
|
+
${this.searchable ? html `
|
|
890
|
+
<div class="search-wrap">
|
|
891
|
+
<svg class="search-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
892
|
+
<circle cx="6.5" cy="6.5" r="5" stroke="currentColor" stroke-width="1.4"/>
|
|
893
|
+
<path d="M10 10l4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
|
894
|
+
</svg>
|
|
895
|
+
<input
|
|
896
|
+
class="search"
|
|
897
|
+
part="search"
|
|
898
|
+
type="search"
|
|
899
|
+
placeholder=${this._searchPlaceholderText || msg('Search…')}
|
|
900
|
+
value=${this._query}
|
|
901
|
+
aria-label=${msg('Search table')}
|
|
902
|
+
/>
|
|
903
|
+
</div>
|
|
904
|
+
` : nothing}
|
|
905
|
+
|
|
906
|
+
<div class="controls">
|
|
907
|
+
${this.controlsLayout === 'menu' ? this._renderOptionsMenu() : html `
|
|
908
|
+
${this._renderColMenu()}
|
|
909
|
+
|
|
910
|
+
${this.exportable ? html `
|
|
911
|
+
<button class="ctrl-btn" part="export-btn" aria-label=${msg('Export as CSV')}>
|
|
912
|
+
${this._selected.size > 0 ? msg(str `Export ${this._selected.size} rows`) : msg('Export CSV')}
|
|
913
|
+
</button>
|
|
914
|
+
` : nothing}
|
|
915
|
+
|
|
916
|
+
${hasActiveFilters ? html `
|
|
917
|
+
<button class="ctrl-btn ctrl-btn-clear-filters" aria-label=${msg('Clear all filters')}>
|
|
918
|
+
${msg('Clear filters')}
|
|
919
|
+
</button>
|
|
920
|
+
` : nothing}
|
|
921
|
+
`}
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
` : nothing}
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<div style="display: contents;">
|
|
928
|
+
${this._renderSelectionBanner()}
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
<div class="table-wrap" part="table-wrap" role="region" aria-label="Data table">
|
|
932
|
+
<table part="table">
|
|
933
|
+
<thead part="thead">
|
|
934
|
+
${vis.length ? html `
|
|
935
|
+
<tr>
|
|
936
|
+
${this.selectable ? html `
|
|
937
|
+
<th class="col-check" part="th">
|
|
938
|
+
<input
|
|
939
|
+
type="checkbox"
|
|
940
|
+
checked=${selectAllChecked ? '' : nothing}
|
|
941
|
+
aria-label=${msg('Select all rows on this page')}
|
|
942
|
+
/>
|
|
943
|
+
</th>
|
|
944
|
+
` : nothing}
|
|
945
|
+
${vis.map(col => {
|
|
946
|
+
const entry = this._sorts.find(s => s.key === col.key);
|
|
947
|
+
const sortClass = entry ? `sort-${entry.dir}` : '';
|
|
948
|
+
return html `
|
|
949
|
+
<th
|
|
950
|
+
class="${col.sortable ? 'sortable' : ''} ${sortClass}"
|
|
951
|
+
part="th"
|
|
952
|
+
style=${this._colStyle(col)}
|
|
953
|
+
data-col-key=${col.key}
|
|
954
|
+
aria-sort=${entry
|
|
955
|
+
? entry.dir === 'asc' ? 'ascending' : 'descending'
|
|
956
|
+
: nothing}
|
|
957
|
+
title=${this._sorts.length > 0 ? 'Click to sort · Shift+click to add secondary sort' : nothing}
|
|
958
|
+
>
|
|
959
|
+
<span class="th-inner">
|
|
960
|
+
${col.label}
|
|
961
|
+
${this._renderSortIcon(col)}
|
|
962
|
+
</span>
|
|
963
|
+
${this.resizable ? html `
|
|
964
|
+
<div
|
|
965
|
+
class="resize-handle"
|
|
966
|
+
aria-hidden="true"
|
|
967
|
+
></div>
|
|
968
|
+
` : nothing}
|
|
969
|
+
</th>
|
|
970
|
+
`;
|
|
971
|
+
})}
|
|
972
|
+
</tr>
|
|
973
|
+
${this._renderFilterRow()}
|
|
974
|
+
` : nothing}
|
|
975
|
+
</thead>
|
|
976
|
+
|
|
977
|
+
<tbody part="tbody">
|
|
978
|
+
${pageRows.length === 0 ? html `
|
|
979
|
+
<tr>
|
|
980
|
+
<td
|
|
981
|
+
colspan=${(this.selectable ? 1 : 0) + vis.length || 1}
|
|
982
|
+
class="empty"
|
|
983
|
+
part="empty"
|
|
984
|
+
>${msg('No results found')}</td>
|
|
985
|
+
</tr>
|
|
986
|
+
` : pageRows.map(({ i, row }) => html `
|
|
987
|
+
<tr
|
|
988
|
+
class=${this._selected.has(i) ? 'row-selected' : ''}
|
|
989
|
+
part="row"
|
|
990
|
+
data-row-index=${i}
|
|
991
|
+
style=${this.selectable ? 'cursor:pointer' : ''}
|
|
992
|
+
>
|
|
993
|
+
${this.selectable ? html `
|
|
994
|
+
<td class="col-check" part="cell">
|
|
995
|
+
<input
|
|
996
|
+
type="checkbox"
|
|
997
|
+
checked=${this._selected.has(i) ? '' : nothing}
|
|
998
|
+
aria-label=${msg('Select row')}
|
|
999
|
+
/>
|
|
1000
|
+
</td>
|
|
1001
|
+
` : nothing}
|
|
1002
|
+
${vis.map((col) => html `
|
|
1003
|
+
<td part="cell" style=${this._colStyle(col)}>
|
|
1004
|
+
${this._highlight(row[this._cols.findIndex(c => c.key === col.key)] ?? '', col.key)}
|
|
1005
|
+
</td>
|
|
1006
|
+
`)}
|
|
1007
|
+
</tr>
|
|
1008
|
+
`)}
|
|
1009
|
+
</tbody>
|
|
1010
|
+
</table>
|
|
1011
|
+
</div>
|
|
1012
|
+
|
|
1013
|
+
<div style="display: contents;">
|
|
1014
|
+
${(this.paginated || this.infiniteScroll) && total > 0 ? html `
|
|
1015
|
+
<div class="footer" part="footer">
|
|
1016
|
+
<div class="footer-left">
|
|
1017
|
+
<span part="count">
|
|
1018
|
+
${total === 0 ? msg('No results') : (this.infiniteScroll ? msg(str `${total} rows`) : msg(str `${start}–${end} of ${total} rows`))}
|
|
1019
|
+
${this._selected.size > 0 ? html ` · <strong>${this._selected.size} selected</strong>` : nothing}
|
|
1020
|
+
</span>
|
|
1021
|
+
${this.paginated && !this.infiniteScroll ? html `
|
|
1022
|
+
<span class="footer-sep">·</span>
|
|
1023
|
+
<label class="footer-label" for="uibit-per-page-footer">${msg('Rows per page:')}</label>
|
|
1024
|
+
<select id="uibit-per-page-footer" class="ctrl-select footer-select" aria-label=${msg('Rows per page')}>
|
|
1025
|
+
${this._pageSizeOptions.map(n => html `<option value=${n} selected=${this._perPage === n ? '' : nothing}>${n}</option>`)}
|
|
1026
|
+
</select>
|
|
1027
|
+
` : nothing}
|
|
1028
|
+
</div>
|
|
1029
|
+
${this.infiniteScroll && this.loading ? html `
|
|
1030
|
+
<span class="loading-spinner" part="loading-spinner">${msg('Loading more…')}</span>
|
|
1031
|
+
` : (this.infiniteScroll ? nothing : this._renderPagination())}
|
|
1032
|
+
</div>
|
|
1033
|
+
` : nothing}
|
|
1034
|
+
</div>
|
|
1035
|
+
`;
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
__decorate([
|
|
1039
|
+
property({ converter: defaultTrueBoolean })
|
|
1040
|
+
], Table.prototype, "searchable", void 0);
|
|
1041
|
+
__decorate([
|
|
1042
|
+
property({ converter: defaultTrueBoolean })
|
|
1043
|
+
], Table.prototype, "paginated", void 0);
|
|
1044
|
+
__decorate([
|
|
1045
|
+
property({ converter: defaultTrueBoolean })
|
|
1046
|
+
], Table.prototype, "exportable", void 0);
|
|
1047
|
+
__decorate([
|
|
1048
|
+
property({ type: Boolean })
|
|
1049
|
+
], Table.prototype, "selectable", void 0);
|
|
1050
|
+
__decorate([
|
|
1051
|
+
property({ type: Boolean })
|
|
1052
|
+
], Table.prototype, "filterable", void 0);
|
|
1053
|
+
__decorate([
|
|
1054
|
+
property({ type: Boolean })
|
|
1055
|
+
], Table.prototype, "resizable", void 0);
|
|
1056
|
+
__decorate([
|
|
1057
|
+
property({ type: Boolean, attribute: 'column-chooser' })
|
|
1058
|
+
], Table.prototype, "columnChooser", void 0);
|
|
1059
|
+
__decorate([
|
|
1060
|
+
property({ type: Boolean, reflect: true })
|
|
1061
|
+
], Table.prototype, "striped", void 0);
|
|
1062
|
+
__decorate([
|
|
1063
|
+
property({ type: Boolean, attribute: 'sticky-header', reflect: true })
|
|
1064
|
+
], Table.prototype, "stickyHeader", void 0);
|
|
1065
|
+
__decorate([
|
|
1066
|
+
property({ attribute: 'page-sizes' })
|
|
1067
|
+
], Table.prototype, "pageSizes", void 0);
|
|
1068
|
+
__decorate([
|
|
1069
|
+
property({ attribute: 'controls-layout' })
|
|
1070
|
+
], Table.prototype, "controlsLayout", void 0);
|
|
1071
|
+
__decorate([
|
|
1072
|
+
property({ type: Boolean, attribute: 'infinite-scroll' })
|
|
1073
|
+
], Table.prototype, "infiniteScroll", void 0);
|
|
1074
|
+
__decorate([
|
|
1075
|
+
property({ type: Boolean })
|
|
1076
|
+
], Table.prototype, "loading", void 0);
|
|
1077
|
+
__decorate([
|
|
1078
|
+
state()
|
|
1079
|
+
], Table.prototype, "_cols", void 0);
|
|
1080
|
+
__decorate([
|
|
1081
|
+
state()
|
|
1082
|
+
], Table.prototype, "_rows", void 0);
|
|
1083
|
+
__decorate([
|
|
1084
|
+
state()
|
|
1085
|
+
], Table.prototype, "_query", void 0);
|
|
1086
|
+
__decorate([
|
|
1087
|
+
state()
|
|
1088
|
+
], Table.prototype, "_sorts", void 0);
|
|
1089
|
+
__decorate([
|
|
1090
|
+
state()
|
|
1091
|
+
], Table.prototype, "_page", void 0);
|
|
1092
|
+
__decorate([
|
|
1093
|
+
state()
|
|
1094
|
+
], Table.prototype, "_perPage", void 0);
|
|
1095
|
+
__decorate([
|
|
1096
|
+
state()
|
|
1097
|
+
], Table.prototype, "_selected", void 0);
|
|
1098
|
+
__decorate([
|
|
1099
|
+
state()
|
|
1100
|
+
], Table.prototype, "_hiddenCols", void 0);
|
|
1101
|
+
__decorate([
|
|
1102
|
+
state()
|
|
1103
|
+
], Table.prototype, "_colFilters", void 0);
|
|
1104
|
+
__decorate([
|
|
1105
|
+
state()
|
|
1106
|
+
], Table.prototype, "_colMenuOpen", void 0);
|
|
1107
|
+
__decorate([
|
|
1108
|
+
state()
|
|
1109
|
+
], Table.prototype, "_optionsMenuOpen", void 0);
|
|
1110
|
+
__decorate([
|
|
1111
|
+
state()
|
|
1112
|
+
], Table.prototype, "_allFilteredSelected", void 0);
|
|
1113
|
+
__decorate([
|
|
1114
|
+
state()
|
|
1115
|
+
], Table.prototype, "_searchPlaceholderText", void 0);
|
|
1116
|
+
Table = __decorate([
|
|
1117
|
+
customElement('uibit-table')
|
|
1118
|
+
], Table);
|
|
1119
|
+
export { Table };
|
|
1120
|
+
export default Table;
|
|
1121
|
+
//# sourceMappingURL=table.js.map
|