neiki-table 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1609 @@
1
+ /*!
2
+ * Neiki's Table 1.0.0
3
+ * A lightweight, dependency-free data table Web Component.
4
+ * https://github.com/neikiri/neiki-table
5
+ * MIT License
6
+ */
7
+ (function () {
8
+ 'use strict';
9
+
10
+ if (customElements.get('neiki-table')) {
11
+ return;
12
+ }
13
+
14
+ // ---------------------------------------------------------------------
15
+ // i18n
16
+ // ---------------------------------------------------------------------
17
+
18
+ var I18N = {
19
+ en: {
20
+ searchPlaceholder: 'Search…',
21
+ filterPlaceholder: 'Filter…',
22
+ noResults: 'No matching rows found.',
23
+ noData: 'No data available.',
24
+ selectAll: 'Select all rows',
25
+ selectRow: 'Select row',
26
+ actions: 'Actions',
27
+ edit: 'Edit',
28
+ save: 'Save',
29
+ cancel: 'Cancel',
30
+ clearFilters: 'Clear filters',
31
+ exportCsv: 'Export CSV',
32
+ exportJson: 'Export JSON',
33
+ rowsPerPage: 'Rows per page',
34
+ previous: 'Previous',
35
+ next: 'Next',
36
+ pageOf: 'Page {page} of {pages}',
37
+ showingRange: 'Showing {start}–{end} of {total}',
38
+ showingNone: 'No rows to show',
39
+ all: 'All',
40
+ yes: 'Yes',
41
+ no: 'No',
42
+ selectedCount: '{count} selected',
43
+ copy: 'Copy',
44
+ copied: 'Copied',
45
+ loading: 'Loading…',
46
+ firstPage: 'First page',
47
+ lastPage: 'Last page',
48
+ gotoPage: 'Go to page {page}',
49
+ resultsAnnounce: '{total} rows'
50
+ },
51
+ cs: {
52
+ searchPlaceholder: 'Hledat…',
53
+ filterPlaceholder: 'Filtrovat…',
54
+ noResults: 'Nebyly nalezeny žádné odpovídající řádky.',
55
+ noData: 'Žádná data k dispozici.',
56
+ selectAll: 'Vybrat všechny řádky',
57
+ selectRow: 'Vybrat řádek',
58
+ actions: 'Akce',
59
+ edit: 'Upravit',
60
+ save: 'Uložit',
61
+ cancel: 'Zrušit',
62
+ clearFilters: 'Vymazat filtry',
63
+ exportCsv: 'Export CSV',
64
+ exportJson: 'Export JSON',
65
+ rowsPerPage: 'Řádků na stránku',
66
+ previous: 'Předchozí',
67
+ next: 'Další',
68
+ pageOf: 'Stránka {page} z {pages}',
69
+ showingRange: 'Zobrazeno {start}–{end} z {total}',
70
+ showingNone: 'Žádné řádky k zobrazení',
71
+ all: 'Vše',
72
+ yes: 'Ano',
73
+ no: 'Ne',
74
+ selectedCount: 'Vybráno: {count}',
75
+ copy: 'Kopírovat',
76
+ copied: 'Zkopírováno',
77
+ loading: 'Načítání…',
78
+ firstPage: 'První stránka',
79
+ lastPage: 'Poslední stránka',
80
+ gotoPage: 'Přejít na stránku {page}',
81
+ resultsAnnounce: 'Řádků: {total}'
82
+ },
83
+ de: {
84
+ searchPlaceholder: 'Suchen…',
85
+ filterPlaceholder: 'Filtern…',
86
+ noResults: 'Keine passenden Zeilen gefunden.',
87
+ noData: 'Keine Daten verfügbar.',
88
+ selectAll: 'Alle Zeilen auswählen',
89
+ selectRow: 'Zeile auswählen',
90
+ actions: 'Aktionen',
91
+ edit: 'Bearbeiten',
92
+ save: 'Speichern',
93
+ cancel: 'Abbrechen',
94
+ clearFilters: 'Filter zurücksetzen',
95
+ exportCsv: 'CSV exportieren',
96
+ exportJson: 'JSON exportieren',
97
+ rowsPerPage: 'Zeilen pro Seite',
98
+ previous: 'Zurück',
99
+ next: 'Weiter',
100
+ pageOf: 'Seite {page} von {pages}',
101
+ showingRange: '{start}–{end} von {total} angezeigt',
102
+ showingNone: 'Keine Zeilen vorhanden',
103
+ all: 'Alle',
104
+ yes: 'Ja',
105
+ no: 'Nein',
106
+ selectedCount: '{count} ausgewählt',
107
+ copy: 'Kopieren',
108
+ copied: 'Kopiert',
109
+ loading: 'Wird geladen…',
110
+ firstPage: 'Erste Seite',
111
+ lastPage: 'Letzte Seite',
112
+ gotoPage: 'Zu Seite {page}',
113
+ resultsAnnounce: '{total} Zeilen'
114
+ },
115
+ es: {
116
+ searchPlaceholder: 'Buscar…',
117
+ filterPlaceholder: 'Filtrar…',
118
+ noResults: 'No se encontraron filas coincidentes.',
119
+ noData: 'No hay datos disponibles.',
120
+ selectAll: 'Seleccionar todas las filas',
121
+ selectRow: 'Seleccionar fila',
122
+ actions: 'Acciones',
123
+ edit: 'Editar',
124
+ save: 'Guardar',
125
+ cancel: 'Cancelar',
126
+ clearFilters: 'Borrar filtros',
127
+ exportCsv: 'Exportar CSV',
128
+ exportJson: 'Exportar JSON',
129
+ rowsPerPage: 'Filas por página',
130
+ previous: 'Anterior',
131
+ next: 'Siguiente',
132
+ pageOf: 'Página {page} de {pages}',
133
+ showingRange: 'Mostrando {start}–{end} de {total}',
134
+ showingNone: 'No hay filas que mostrar',
135
+ all: 'Todos',
136
+ yes: 'Sí',
137
+ no: 'No',
138
+ selectedCount: '{count} seleccionadas',
139
+ copy: 'Copiar',
140
+ copied: 'Copiado',
141
+ loading: 'Cargando…',
142
+ firstPage: 'Primera página',
143
+ lastPage: 'Última página',
144
+ gotoPage: 'Ir a la página {page}',
145
+ resultsAnnounce: '{total} filas'
146
+ },
147
+ fr: {
148
+ searchPlaceholder: 'Rechercher…',
149
+ filterPlaceholder: 'Filtrer…',
150
+ noResults: 'Aucune ligne correspondante trouvée.',
151
+ noData: 'Aucune donnée disponible.',
152
+ selectAll: 'Sélectionner toutes les lignes',
153
+ selectRow: 'Sélectionner la ligne',
154
+ actions: 'Actions',
155
+ edit: 'Modifier',
156
+ save: 'Enregistrer',
157
+ cancel: 'Annuler',
158
+ clearFilters: 'Effacer les filtres',
159
+ exportCsv: 'Exporter en CSV',
160
+ exportJson: 'Exporter en JSON',
161
+ rowsPerPage: 'Lignes par page',
162
+ previous: 'Précédent',
163
+ next: 'Suivant',
164
+ pageOf: 'Page {page} sur {pages}',
165
+ showingRange: 'Affichage de {start}–{end} sur {total}',
166
+ showingNone: 'Aucune ligne à afficher',
167
+ all: 'Tous',
168
+ yes: 'Oui',
169
+ no: 'Non',
170
+ selectedCount: '{count} sélectionnée(s)',
171
+ copy: 'Copier',
172
+ copied: 'Copié',
173
+ loading: 'Chargement…',
174
+ firstPage: 'Première page',
175
+ lastPage: 'Dernière page',
176
+ gotoPage: 'Aller à la page {page}',
177
+ resultsAnnounce: '{total} lignes'
178
+ },
179
+ it: {
180
+ searchPlaceholder: 'Cerca…',
181
+ filterPlaceholder: 'Filtra…',
182
+ noResults: 'Nessuna riga corrispondente trovata.',
183
+ noData: 'Nessun dato disponibile.',
184
+ selectAll: 'Seleziona tutte le righe',
185
+ selectRow: 'Seleziona riga',
186
+ actions: 'Azioni',
187
+ edit: 'Modifica',
188
+ save: 'Salva',
189
+ cancel: 'Annulla',
190
+ clearFilters: 'Cancella filtri',
191
+ exportCsv: 'Esporta CSV',
192
+ exportJson: 'Esporta JSON',
193
+ rowsPerPage: 'Righe per pagina',
194
+ previous: 'Precedente',
195
+ next: 'Successivo',
196
+ pageOf: 'Pagina {page} di {pages}',
197
+ showingRange: 'Visualizzazione di {start}–{end} su {total}',
198
+ showingNone: 'Nessuna riga da mostrare',
199
+ all: 'Tutti',
200
+ yes: 'Sì',
201
+ no: 'No',
202
+ selectedCount: '{count} selezionate',
203
+ copy: 'Copia',
204
+ copied: 'Copiato',
205
+ loading: 'Caricamento…',
206
+ firstPage: 'Prima pagina',
207
+ lastPage: 'Ultima pagina',
208
+ gotoPage: 'Vai alla pagina {page}',
209
+ resultsAnnounce: '{total} righe'
210
+ },
211
+ pl: {
212
+ searchPlaceholder: 'Szukaj…',
213
+ filterPlaceholder: 'Filtruj…',
214
+ noResults: 'Nie znaleziono pasujących wierszy.',
215
+ noData: 'Brak dostępnych danych.',
216
+ selectAll: 'Zaznacz wszystkie wiersze',
217
+ selectRow: 'Zaznacz wiersz',
218
+ actions: 'Akcje',
219
+ edit: 'Edytuj',
220
+ save: 'Zapisz',
221
+ cancel: 'Anuluj',
222
+ clearFilters: 'Wyczyść filtry',
223
+ exportCsv: 'Eksportuj CSV',
224
+ exportJson: 'Eksportuj JSON',
225
+ rowsPerPage: 'Wierszy na stronę',
226
+ previous: 'Poprzednia',
227
+ next: 'Następna',
228
+ pageOf: 'Strona {page} z {pages}',
229
+ showingRange: 'Wyświetlanie {start}–{end} z {total}',
230
+ showingNone: 'Brak wierszy do wyświetlenia',
231
+ all: 'Wszystkie',
232
+ yes: 'Tak',
233
+ no: 'Nie',
234
+ selectedCount: 'Zaznaczono: {count}',
235
+ copy: 'Kopiuj',
236
+ copied: 'Skopiowano',
237
+ loading: 'Ładowanie…',
238
+ firstPage: 'Pierwsza strona',
239
+ lastPage: 'Ostatnia strona',
240
+ gotoPage: 'Przejdź do strony {page}',
241
+ resultsAnnounce: 'Wierszy: {total}'
242
+ },
243
+ sk: {
244
+ searchPlaceholder: 'Hľadať…',
245
+ filterPlaceholder: 'Filtrovať…',
246
+ noResults: 'Neboli nájdené žiadne zodpovedajúce riadky.',
247
+ noData: 'Nie sú k dispozícii žiadne dáta.',
248
+ selectAll: 'Vybrať všetky riadky',
249
+ selectRow: 'Vybrať riadok',
250
+ actions: 'Akcie',
251
+ edit: 'Upraviť',
252
+ save: 'Uložiť',
253
+ cancel: 'Zrušiť',
254
+ clearFilters: 'Vymazať filtre',
255
+ exportCsv: 'Exportovať CSV',
256
+ exportJson: 'Exportovať JSON',
257
+ rowsPerPage: 'Riadkov na stránku',
258
+ previous: 'Predchádzajúca',
259
+ next: 'Ďalšia',
260
+ pageOf: 'Stránka {page} z {pages}',
261
+ showingRange: 'Zobrazené {start}–{end} z {total}',
262
+ showingNone: 'Žiadne riadky na zobrazenie',
263
+ all: 'Všetky',
264
+ yes: 'Áno',
265
+ no: 'Nie',
266
+ selectedCount: 'Vybraté: {count}',
267
+ copy: 'Kopírovať',
268
+ copied: 'Skopírované',
269
+ loading: 'Načítava sa…',
270
+ firstPage: 'Prvá stránka',
271
+ lastPage: 'Posledná stránka',
272
+ gotoPage: 'Prejsť na stránku {page}',
273
+ resultsAnnounce: 'Riadkov: {total}'
274
+ }
275
+ };
276
+
277
+ var FALLBACK_LOCALE = 'en';
278
+
279
+ function translate(locale, dictionaries, key, vars) {
280
+ var dict = dictionaries[locale] || dictionaries[FALLBACK_LOCALE] || {};
281
+ var fallback = dictionaries[FALLBACK_LOCALE] || {};
282
+ var text = dict[key] !== undefined ? dict[key] : fallback[key] !== undefined ? fallback[key] : key;
283
+ if (vars) {
284
+ Object.keys(vars).forEach(function (name) {
285
+ text = text.replace('{' + name + '}', String(vars[name]));
286
+ });
287
+ }
288
+ return text;
289
+ }
290
+
291
+ // ---------------------------------------------------------------------
292
+ // Constants
293
+ // ---------------------------------------------------------------------
294
+
295
+ var VALID_THEMES = ['light', 'dark', 'auto'];
296
+ var VALID_TYPES = ['text', 'number', 'boolean', 'date', 'select'];
297
+ var VALID_DENSITIES = ['compact', 'normal', 'spacious'];
298
+ var VALID_ALIGN = ['left', 'center', 'right'];
299
+ var PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
300
+ var MIN_COLUMN_WIDTH = 60;
301
+
302
+ var DEFAULT_CONFIG = {
303
+ locale: 'en',
304
+ theme: 'auto',
305
+ density: 'normal',
306
+ rowKey: 'id',
307
+ searchable: true,
308
+ filterable: true,
309
+ selectable: true,
310
+ editable: true,
311
+ paginated: true,
312
+ exportable: true,
313
+ resizable: true,
314
+ pageSize: 10,
315
+ searchDebounce: 180
316
+ };
317
+
318
+ function oneOf(value, list, fallback) {
319
+ return list.indexOf(value) !== -1 ? value : fallback;
320
+ }
321
+
322
+ function toBool(value, fallback) {
323
+ if (value === true || value === false) return value;
324
+ if (value === 'true') return true;
325
+ if (value === 'false') return false;
326
+ return fallback;
327
+ }
328
+
329
+ function getValue(row, key) {
330
+ return row ? row[key] : undefined;
331
+ }
332
+
333
+ function formatDisplay(value, column, locale) {
334
+ if (value === null || value === undefined || value === '') return '';
335
+ switch (column.type) {
336
+ case 'number': {
337
+ var num = Number(value);
338
+ return isNaN(num) ? String(value) : num.toLocaleString(locale);
339
+ }
340
+ case 'boolean':
341
+ return value ? 'yes' : 'no';
342
+ case 'date': {
343
+ var date = new Date(value);
344
+ return isNaN(date.getTime()) ? String(value) : date.toLocaleDateString(locale);
345
+ }
346
+ case 'select': {
347
+ var opt = findOption(column, value);
348
+ return opt ? opt.label : String(value);
349
+ }
350
+ default:
351
+ return String(value);
352
+ }
353
+ }
354
+
355
+ function findOption(column, value) {
356
+ var options = normalizeOptions(column);
357
+ for (var i = 0; i < options.length; i++) {
358
+ if (String(options[i].value) === String(value)) return options[i];
359
+ }
360
+ return null;
361
+ }
362
+
363
+ function normalizeOptions(column) {
364
+ var raw = column.options || [];
365
+ return raw.map(function (opt) {
366
+ if (opt && typeof opt === 'object') {
367
+ return { value: opt.value, label: opt.label !== undefined ? opt.label : String(opt.value) };
368
+ }
369
+ return { value: opt, label: String(opt) };
370
+ });
371
+ }
372
+
373
+ function compareValues(a, b, type) {
374
+ var av = a, bv = b;
375
+ if (av === null || av === undefined) av = '';
376
+ if (bv === null || bv === undefined) bv = '';
377
+ if (type === 'number') {
378
+ return (Number(av) || 0) - (Number(bv) || 0);
379
+ }
380
+ if (type === 'boolean') {
381
+ return (av ? 1 : 0) - (bv ? 1 : 0);
382
+ }
383
+ if (type === 'date') {
384
+ return new Date(av).getTime() - new Date(bv).getTime();
385
+ }
386
+ return String(av).localeCompare(String(bv));
387
+ }
388
+
389
+ function csvEscape(value) {
390
+ var text = value === null || value === undefined ? '' : String(value);
391
+ if (/^[=+\-@\t\r]/.test(text)) {
392
+ text = "'" + text;
393
+ }
394
+ if (/[",\n\r]/.test(text)) {
395
+ text = '"' + text.replace(/"/g, '""') + '"';
396
+ }
397
+ return text;
398
+ }
399
+
400
+ function downloadBlob(content, mime, filename) {
401
+ var blob = new Blob([content], { type: mime });
402
+ var url = URL.createObjectURL(blob);
403
+ var link = document.createElement('a');
404
+ link.href = url;
405
+ link.download = filename;
406
+ link.style.display = 'none';
407
+ document.body.appendChild(link);
408
+ link.click();
409
+ document.body.removeChild(link);
410
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
411
+ }
412
+
413
+ // ---------------------------------------------------------------------
414
+ // Styling (shared adopted stylesheet, mirrors neiki-social-bar)
415
+ // ---------------------------------------------------------------------
416
+
417
+ // Replaced by minify.py at build time with the actual (minified) CSS text.
418
+ // Stays empty in src/ so development can edit neiki-table.css without
419
+ // rebuilding — the component falls back to a sibling <link> in that case.
420
+ var EMBEDDED_CSS = "/*!\n * Neiki's Table 1.0.0 \u2014 styles\n * MIT License\n */\n\n:host {\n --ntbl-radius: 14px;\n --ntbl-radius-inner: 10px;\n --ntbl-radius-control: 9px;\n --ntbl-font-size: 14px;\n --ntbl-transition: 140ms ease;\n --ntbl-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 20px rgba(16, 24, 40, 0.08);\n --ntbl-row-height: 44px;\n\n /* Density (overridden by [density] below) */\n --ntbl-cell-py: 9px;\n --ntbl-cell-px: 13px;\n --ntbl-head-py: 10px;\n\n --ntbl-bg: #ffffff;\n --ntbl-color: #1f2328;\n --ntbl-muted: #6b7280;\n --ntbl-border: rgba(16, 24, 40, 0.09);\n --ntbl-border-strong: rgba(16, 24, 40, 0.14);\n --ntbl-header-bg: #f7f8fa;\n --ntbl-row-hover: #f3f6fc;\n --ntbl-row-selected: #e9f0ff;\n --ntbl-stripe: rgba(16, 24, 40, 0.018);\n --ntbl-accent: #2563eb;\n --ntbl-accent-hover: #1d4ed8;\n --ntbl-accent-color: #ffffff;\n --ntbl-accent-soft: rgba(37, 99, 235, 0.10);\n --ntbl-focus-ring: rgba(37, 99, 235, 0.45);\n --ntbl-input-bg: #ffffff;\n --ntbl-input-border: rgba(16, 24, 40, 0.16);\n --ntbl-badge-true-bg: #dcfce7;\n --ntbl-badge-true-color: #166534;\n --ntbl-badge-false-bg: #f3f4f6;\n --ntbl-badge-false-color: #6b7280;\n --ntbl-skeleton: linear-gradient(90deg, rgba(16,24,40,0.05) 25%, rgba(16,24,40,0.11) 37%, rgba(16,24,40,0.05) 63%);\n\n display: block;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n font-size: var(--ntbl-font-size);\n color: var(--ntbl-color);\n line-height: 1.45;\n -webkit-text-size-adjust: 100%;\n}\n\n:host([hidden]) {\n display: none !important;\n}\n\n:host([resolved-theme=\"dark\"]) {\n --ntbl-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 8px 26px rgba(0, 0, 0, 0.40);\n --ntbl-bg: #1a1d23;\n --ntbl-color: #eef0f3;\n --ntbl-muted: #9aa3af;\n --ntbl-border: rgba(255, 255, 255, 0.09);\n --ntbl-border-strong: rgba(255, 255, 255, 0.16);\n --ntbl-header-bg: #21252c;\n --ntbl-row-hover: #262b33;\n --ntbl-row-selected: #22314f;\n --ntbl-stripe: rgba(255, 255, 255, 0.02);\n --ntbl-accent: #3b82f6;\n --ntbl-accent-hover: #60a5fa;\n --ntbl-accent-soft: rgba(59, 130, 246, 0.16);\n --ntbl-focus-ring: rgba(96, 165, 250, 0.55);\n --ntbl-input-bg: #21252c;\n --ntbl-input-border: rgba(255, 255, 255, 0.16);\n --ntbl-badge-true-bg: rgba(34, 197, 94, 0.18);\n --ntbl-badge-true-color: #4ade80;\n --ntbl-badge-false-bg: rgba(255, 255, 255, 0.08);\n --ntbl-badge-false-color: #9aa3af;\n --ntbl-skeleton: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.09) 37%, rgba(255,255,255,0.04) 63%);\n}\n\n/* Density */\n:host([density=\"compact\"]) {\n --ntbl-cell-py: 5px;\n --ntbl-cell-px: 10px;\n --ntbl-head-py: 7px;\n --ntbl-font-size: 13px;\n}\n:host([density=\"spacious\"]) {\n --ntbl-cell-py: 14px;\n --ntbl-cell-px: 16px;\n --ntbl-head-py: 14px;\n}\n\n* {\n box-sizing: border-box;\n}\n\n.ntbl-root {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 12px;\n background: var(--ntbl-bg);\n border: 1px solid var(--ntbl-border);\n border-radius: var(--ntbl-radius);\n box-shadow: var(--ntbl-shadow);\n padding: 14px;\n}\n\n/* Toolbar */\n.ntbl-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.ntbl-search-wrap {\n position: relative;\n flex: 1 1 240px;\n min-width: 160px;\n display: flex;\n align-items: center;\n}\n.ntbl-search-wrap[hidden] {\n display: none;\n}\n.ntbl-search-wrap::before {\n content: \"\";\n position: absolute;\n left: 11px;\n width: 16px;\n height: 16px;\n pointer-events: none;\n opacity: 0.5;\n background: currentColor;\n -webkit-mask: no-repeat center / contain url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E\");\n mask: no-repeat center / contain url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E\");\n}\n\n.ntbl-search {\n width: 100%;\n padding: 9px 12px 9px 34px;\n font: inherit;\n color: var(--ntbl-color);\n background: var(--ntbl-input-bg);\n border: 1px solid var(--ntbl-input-border);\n border-radius: var(--ntbl-radius-control);\n outline: none;\n transition: border-color var(--ntbl-transition), box-shadow var(--ntbl-transition);\n}\n.ntbl-search::placeholder {\n color: var(--ntbl-muted);\n}\n.ntbl-search:hover {\n border-color: var(--ntbl-border-strong);\n}\n.ntbl-search:focus-visible {\n border-color: var(--ntbl-accent);\n box-shadow: 0 0 0 3px var(--ntbl-focus-ring);\n}\n\n.ntbl-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ntbl-selected-count {\n font-size: 0.85em;\n font-weight: 600;\n color: var(--ntbl-accent);\n background: var(--ntbl-accent-soft);\n padding: 4px 10px;\n border-radius: 999px;\n}\n.ntbl-selected-count[hidden] {\n display: none;\n}\n\n.ntbl-btn,\n.ntbl-toolbar-actions button,\n.ntbl-pagination button {\n font: inherit;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 13px;\n border: 1px solid var(--ntbl-input-border);\n border-radius: var(--ntbl-radius-control);\n background: var(--ntbl-input-bg);\n color: var(--ntbl-color);\n cursor: pointer;\n white-space: nowrap;\n transition: background var(--ntbl-transition), border-color var(--ntbl-transition), color var(--ntbl-transition), opacity var(--ntbl-transition), transform var(--ntbl-transition);\n}\n.ntbl-toolbar-actions button[hidden] {\n display: none;\n}\n.ntbl-toolbar-actions button:hover,\n.ntbl-pagination button:not(:disabled):hover {\n background: var(--ntbl-row-hover);\n border-color: var(--ntbl-border-strong);\n}\n.ntbl-toolbar-actions button:active,\n.ntbl-pagination button:not(:disabled):active {\n transform: translateY(1px);\n}\n.ntbl-export-csv {\n color: var(--ntbl-accent);\n border-color: color-mix(in srgb, var(--ntbl-accent) 40%, transparent);\n}\n.ntbl-export-csv:hover {\n background: var(--ntbl-accent-soft) !important;\n}\n.ntbl-toolbar-actions button:focus-visible,\n.ntbl-pagination button:focus-visible,\n.ntbl-page-size:focus-visible {\n outline: none;\n border-color: var(--ntbl-accent);\n box-shadow: 0 0 0 3px var(--ntbl-focus-ring);\n}\n.ntbl-pagination button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* Icons inside buttons */\n.ntbl-icon {\n width: 15px;\n height: 15px;\n flex: none;\n}\n\n/* Scroll area / table */\n.ntbl-scroll {\n position: relative;\n overflow-x: auto;\n border: 1px solid var(--ntbl-border);\n border-radius: var(--ntbl-radius-inner);\n}\n\n.ntbl-table {\n width: 100%;\n border-collapse: collapse;\n min-width: 480px;\n}\n\n.ntbl-thead {\n position: sticky;\n top: 0;\n z-index: 2;\n}\n\n.ntbl-th {\n position: relative;\n background: var(--ntbl-header-bg);\n color: var(--ntbl-color);\n text-align: left;\n padding: var(--ntbl-head-py) var(--ntbl-cell-px);\n font-weight: 600;\n font-size: 0.9em;\n letter-spacing: 0.01em;\n border-bottom: 1px solid var(--ntbl-border-strong);\n white-space: nowrap;\n user-select: none;\n}\n\n.ntbl-th-label {\n display: inline-flex;\n align-items: center;\n vertical-align: middle;\n}\n\n.ntbl-th-align-center { text-align: center; }\n.ntbl-th-align-right { text-align: right; }\n\n.ntbl-th-sortable {\n cursor: pointer;\n}\n.ntbl-th-sortable:hover {\n background: var(--ntbl-row-hover);\n color: var(--ntbl-accent);\n}\n.ntbl-th-sortable:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 2px var(--ntbl-accent);\n}\n\n.ntbl-sort-icon {\n display: inline-block;\n width: 0.85em;\n height: 0.85em;\n margin-left: 5px;\n vertical-align: middle;\n opacity: 0.3;\n background: currentColor;\n -webkit-mask: no-repeat center / contain var(--ntbl-sort-mask, url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 9l4-4 4 4M8 15l4 4 4-4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\"));\n mask: no-repeat center / contain var(--ntbl-sort-mask, url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 9l4-4 4 4M8 15l4 4 4-4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\"));\n}\n.ntbl-th-sorted-asc,\n.ntbl-th-sorted-desc {\n color: var(--ntbl-accent);\n}\n.ntbl-th-sorted-asc .ntbl-sort-icon,\n.ntbl-th-sorted-desc .ntbl-sort-icon {\n opacity: 1;\n}\n.ntbl-th-sorted-asc .ntbl-sort-icon {\n --ntbl-sort-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 14l6-6 6 6' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n}\n.ntbl-th-sorted-desc .ntbl-sort-icon {\n --ntbl-sort-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 10l6 6 6-6' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");\n}\n\n/* Column resize handle */\n.ntbl-resize-handle {\n position: absolute;\n top: 0;\n right: 0;\n width: 9px;\n height: 100%;\n cursor: col-resize;\n z-index: 3;\n touch-action: none;\n}\n.ntbl-resize-handle::after {\n content: \"\";\n position: absolute;\n top: 20%;\n right: 3px;\n width: 2px;\n height: 60%;\n background: var(--ntbl-border-strong);\n opacity: 0;\n border-radius: 2px;\n transition: opacity var(--ntbl-transition);\n}\n.ntbl-resize-handle:hover::after,\n.ntbl-th-resizing .ntbl-resize-handle::after {\n opacity: 1;\n background: var(--ntbl-accent);\n}\n\n.ntbl-th-select {\n width: 44px;\n padding-left: 12px;\n padding-right: 8px;\n}\n\n.ntbl-filter-row {\n background: var(--ntbl-bg);\n}\n.ntbl-filter-row[hidden] {\n display: none;\n}\n.ntbl-filter-cell {\n padding: 6px 10px;\n font-weight: normal;\n background: var(--ntbl-bg);\n border-bottom: 1px solid var(--ntbl-border);\n}\n\n.ntbl-filter-input {\n width: 100%;\n min-width: 80px;\n font: inherit;\n font-size: 0.9em;\n padding: 6px 9px;\n color: var(--ntbl-color);\n background: var(--ntbl-input-bg);\n border: 1px solid var(--ntbl-input-border);\n border-radius: var(--ntbl-radius-control);\n transition: border-color var(--ntbl-transition), box-shadow var(--ntbl-transition);\n}\n.ntbl-filter-input:hover {\n border-color: var(--ntbl-border-strong);\n}\n.ntbl-filter-input:focus-visible {\n outline: none;\n border-color: var(--ntbl-accent);\n box-shadow: 0 0 0 3px var(--ntbl-focus-ring);\n}\n\n.ntbl-tbody .ntbl-tr {\n transition: background var(--ntbl-transition);\n}\n.ntbl-tbody .ntbl-tr:nth-child(even) {\n background: var(--ntbl-stripe);\n}\n.ntbl-tbody .ntbl-tr:hover {\n background: var(--ntbl-row-hover);\n}\n.ntbl-tbody .ntbl-tr-selected,\n.ntbl-tbody .ntbl-tr-selected:hover {\n background: var(--ntbl-row-selected);\n}\n.ntbl-tbody .ntbl-tr-selected .ntbl-td-select {\n box-shadow: inset 3px 0 0 var(--ntbl-accent);\n}\n\n.ntbl-td {\n padding: var(--ntbl-cell-py) var(--ntbl-cell-px);\n border-bottom: 1px solid var(--ntbl-border);\n vertical-align: middle;\n}\n.ntbl-td-align-center { text-align: center; }\n.ntbl-td-align-right { text-align: right; font-variant-numeric: tabular-nums; }\n.ntbl-td-num { font-variant-numeric: tabular-nums; }\n.ntbl-tbody .ntbl-tr:last-child .ntbl-td {\n border-bottom: none;\n}\n\n.ntbl-td-select {\n width: 44px;\n padding-left: 12px;\n padding-right: 8px;\n}\n\n/* Checkboxes */\n.ntbl-td-select input,\n.ntbl-th-select input {\n width: 16px;\n height: 16px;\n accent-color: var(--ntbl-accent);\n cursor: pointer;\n}\n\n.ntbl-td-editable {\n cursor: text;\n border-radius: 6px;\n transition: box-shadow var(--ntbl-transition);\n}\n.ntbl-td-editable:hover {\n box-shadow: inset 0 0 0 1px var(--ntbl-border-strong);\n}\n.ntbl-td-editable:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 2px var(--ntbl-accent);\n}\n\n.ntbl-td-editing {\n padding: 4px 6px;\n}\n\n.ntbl-edit-input {\n width: 100%;\n font: inherit;\n padding: 6px 9px;\n color: var(--ntbl-color);\n background: var(--ntbl-input-bg);\n border: 1px solid var(--ntbl-accent);\n border-radius: var(--ntbl-radius-control);\n box-shadow: 0 0 0 3px var(--ntbl-focus-ring);\n outline: none;\n}\n\n.ntbl-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 2px 10px;\n border-radius: 999px;\n font-size: 0.82em;\n font-weight: 600;\n line-height: 1.6;\n}\n.ntbl-badge::before {\n content: \"\";\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.ntbl-badge-true {\n background: var(--ntbl-badge-true-bg);\n color: var(--ntbl-badge-true-color);\n}\n.ntbl-badge-false {\n background: var(--ntbl-badge-false-bg);\n color: var(--ntbl-badge-false-color);\n}\n.ntbl-td-editable .ntbl-badge {\n cursor: pointer;\n}\n\n.ntbl-empty {\n padding: 44px 16px;\n text-align: center;\n color: var(--ntbl-muted);\n}\n.ntbl-empty[hidden] {\n display: none;\n}\n\n/* Loading overlay */\n.ntbl-loading-overlay {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--ntbl-bg) 62%, transparent);\n backdrop-filter: blur(1px);\n border-radius: var(--ntbl-radius-inner);\n z-index: 4;\n}\n:host([loading]) .ntbl-loading-overlay {\n display: flex;\n}\n:host([loading]) .ntbl-scroll {\n min-height: 160px;\n}\n.ntbl-spinner {\n width: 26px;\n height: 26px;\n border-radius: 50%;\n border: 3px solid var(--ntbl-border-strong);\n border-top-color: var(--ntbl-accent);\n animation: ntbl-spin 0.7s linear infinite;\n}\n@keyframes ntbl-spin {\n to { transform: rotate(360deg); }\n}\n\n/* Footer / pagination */\n.ntbl-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n font-size: 0.88em;\n color: var(--ntbl-muted);\n}\n\n.ntbl-info {\n font-variant-numeric: tabular-nums;\n}\n\n.ntbl-pagination {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.ntbl-pagination[hidden] {\n display: none;\n}\n\n.ntbl-pagination button {\n padding: 7px 10px;\n min-width: 36px;\n justify-content: center;\n}\n.ntbl-page-numbers {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.ntbl-page-btn {\n font-variant-numeric: tabular-nums;\n}\n.ntbl-page-btn.ntbl-page-current {\n background: var(--ntbl-accent);\n border-color: var(--ntbl-accent);\n color: var(--ntbl-accent-color);\n cursor: default;\n}\n.ntbl-page-btn.ntbl-page-current:hover {\n background: var(--ntbl-accent) !important;\n}\n.ntbl-page-ellipsis {\n padding: 0 2px;\n color: var(--ntbl-muted);\n user-select: none;\n}\n\n.ntbl-page-size {\n font: inherit;\n font-size: 0.9em;\n padding: 7px 8px;\n color: var(--ntbl-color);\n background: var(--ntbl-input-bg);\n border: 1px solid var(--ntbl-input-border);\n border-radius: var(--ntbl-radius-control);\n cursor: pointer;\n}\n\n.ntbl-page-indicator {\n min-width: 84px;\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n\n/* Visually-hidden live region for screen readers */\n.ntbl-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n}\n\n/* Reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .ntbl-search,\n .ntbl-tbody .ntbl-tr,\n .ntbl-toolbar-actions button,\n .ntbl-pagination button,\n .ntbl-filter-input,\n .ntbl-td-editable,\n .ntbl-resize-handle::after {\n transition: none !important;\n }\n .ntbl-spinner {\n animation-duration: 1.4s;\n }\n}\n\n/* Mobile */\n@media (max-width: 640px) {\n .ntbl-root {\n padding: 10px;\n }\n .ntbl-footer {\n flex-direction: column;\n align-items: stretch;\n }\n .ntbl-pagination {\n justify-content: center;\n flex-wrap: wrap;\n }\n}\n";
421
+
422
+ var sharedSheet = null;
423
+ var sharedSheetFailed = false;
424
+
425
+ function getSharedSheet(cssText) {
426
+ if (sharedSheet || sharedSheetFailed) return sharedSheet;
427
+ if (typeof CSSStyleSheet === 'undefined' || !('adoptedStyleSheets' in Document.prototype)) {
428
+ sharedSheetFailed = true;
429
+ return null;
430
+ }
431
+ try {
432
+ sharedSheet = new CSSStyleSheet();
433
+ sharedSheet.replaceSync(cssText);
434
+ } catch (err) {
435
+ sharedSheet = null;
436
+ sharedSheetFailed = true;
437
+ }
438
+ return sharedSheet;
439
+ }
440
+
441
+ var TEMPLATE = document.createElement('template');
442
+ TEMPLATE.innerHTML =
443
+ '<div class="ntbl-root" part="root">' +
444
+ '<div class="ntbl-toolbar" part="toolbar">' +
445
+ '<div class="ntbl-search-wrap">' +
446
+ '<input type="search" class="ntbl-search" part="search">' +
447
+ '</div>' +
448
+ '<div class="ntbl-toolbar-actions">' +
449
+ '<span class="ntbl-selected-count" part="selected-count" hidden></span>' +
450
+ '<button type="button" class="ntbl-clear-filters" part="button"></button>' +
451
+ '<button type="button" class="ntbl-copy" part="button"></button>' +
452
+ '<button type="button" class="ntbl-export-csv" part="button"></button>' +
453
+ '<button type="button" class="ntbl-export-json" part="button"></button>' +
454
+ '</div>' +
455
+ '</div>' +
456
+ '<div class="ntbl-scroll" part="scroll">' +
457
+ '<table class="ntbl-table" part="table">' +
458
+ '<colgroup class="ntbl-colgroup"></colgroup>' +
459
+ '<thead class="ntbl-thead" part="thead">' +
460
+ '<tr class="ntbl-header-row"></tr>' +
461
+ '<tr class="ntbl-filter-row"></tr>' +
462
+ '</thead>' +
463
+ '<tbody class="ntbl-tbody" part="tbody"></tbody>' +
464
+ '</table>' +
465
+ '<div class="ntbl-empty" part="empty" hidden></div>' +
466
+ '<div class="ntbl-loading-overlay" part="loading" aria-hidden="true">' +
467
+ '<span class="ntbl-spinner"></span>' +
468
+ '</div>' +
469
+ '</div>' +
470
+ '<div class="ntbl-footer" part="footer">' +
471
+ '<div class="ntbl-info" part="info"></div>' +
472
+ '<div class="ntbl-pagination" part="pagination">' +
473
+ '<label class="ntbl-page-size-label">' +
474
+ '<select class="ntbl-page-size" part="page-size"></select>' +
475
+ '</label>' +
476
+ '<button type="button" class="ntbl-first" part="button">&laquo;</button>' +
477
+ '<button type="button" class="ntbl-prev" part="button">&lsaquo;</button>' +
478
+ '<span class="ntbl-page-numbers" part="page-numbers"></span>' +
479
+ '<span class="ntbl-page-indicator" part="page-indicator"></span>' +
480
+ '<button type="button" class="ntbl-next" part="button">&rsaquo;</button>' +
481
+ '<button type="button" class="ntbl-last" part="button">&raquo;</button>' +
482
+ '</div>' +
483
+ '</div>' +
484
+ '<div class="ntbl-sr-only" role="status" aria-live="polite"></div>' +
485
+ '</div>';
486
+
487
+ class NeikiTable extends HTMLElement {
488
+ constructor() {
489
+ super();
490
+ this._init();
491
+ }
492
+ }
493
+
494
+ NeikiTable.observedAttributes = [
495
+ 'locale', 'theme', 'density', 'row-key', 'searchable', 'filterable', 'selectable',
496
+ 'editable', 'paginated', 'exportable', 'resizable', 'loading', 'search-debounce',
497
+ 'page-size', 'columns', 'data'
498
+ ];
499
+
500
+ NeikiTable.prototype._init = function () {
501
+ this._ready = false;
502
+ this._reflecting = false;
503
+ this._mediaQuery = null;
504
+ this._i18n = Object.assign({}, I18N);
505
+ this._config = Object.assign({}, DEFAULT_CONFIG);
506
+ this._columns = [];
507
+ this._rows = [];
508
+ this._state = {
509
+ search: '',
510
+ filters: {},
511
+ sort: { key: null, dir: null },
512
+ page: 1,
513
+ selected: {},
514
+ editing: null,
515
+ columnWidths: {}
516
+ };
517
+ this._searchTimer = null;
518
+ this._copyResetTimer = null;
519
+ this._resize = null;
520
+
521
+ this.attachShadow({ mode: 'open' });
522
+ this.shadowRoot.appendChild(TEMPLATE.content.cloneNode(true));
523
+ this._injectStyles();
524
+
525
+ this._root = this.shadowRoot.querySelector('.ntbl-root');
526
+ this._searchInput = this.shadowRoot.querySelector('.ntbl-search');
527
+ this._selectedCountEl = this.shadowRoot.querySelector('.ntbl-selected-count');
528
+ this._clearFiltersBtn = this.shadowRoot.querySelector('.ntbl-clear-filters');
529
+ this._copyBtn = this.shadowRoot.querySelector('.ntbl-copy');
530
+ this._exportCsvBtn = this.shadowRoot.querySelector('.ntbl-export-csv');
531
+ this._exportJsonBtn = this.shadowRoot.querySelector('.ntbl-export-json');
532
+ this._colgroup = this.shadowRoot.querySelector('.ntbl-colgroup');
533
+ this._headerRow = this.shadowRoot.querySelector('.ntbl-header-row');
534
+ this._filterRow = this.shadowRoot.querySelector('.ntbl-filter-row');
535
+ this._tbody = this.shadowRoot.querySelector('.ntbl-tbody');
536
+ this._emptyEl = this.shadowRoot.querySelector('.ntbl-empty');
537
+ this._infoEl = this.shadowRoot.querySelector('.ntbl-info');
538
+ this._liveRegion = this.shadowRoot.querySelector('.ntbl-sr-only');
539
+ this._pageSizeSelect = this.shadowRoot.querySelector('.ntbl-page-size');
540
+ this._firstBtn = this.shadowRoot.querySelector('.ntbl-first');
541
+ this._prevBtn = this.shadowRoot.querySelector('.ntbl-prev');
542
+ this._nextBtn = this.shadowRoot.querySelector('.ntbl-next');
543
+ this._lastBtn = this.shadowRoot.querySelector('.ntbl-last');
544
+ this._pageNumbers = this.shadowRoot.querySelector('.ntbl-page-numbers');
545
+ this._pageIndicator = this.shadowRoot.querySelector('.ntbl-page-indicator');
546
+
547
+ this._onMediaChange = this._onMediaChange.bind(this);
548
+ this._onResizeMove = this._onResizeMove.bind(this);
549
+ this._onResizeEnd = this._onResizeEnd.bind(this);
550
+
551
+ var self = this;
552
+ this._searchInput.addEventListener('input', function () {
553
+ var value = self._searchInput.value;
554
+ var apply = function () {
555
+ self._searchTimer = null;
556
+ self._state.search = value;
557
+ self._state.page = 1;
558
+ self._render();
559
+ self._emit('search', { query: self._state.search });
560
+ };
561
+ if (self._searchTimer) clearTimeout(self._searchTimer);
562
+ var delay = self._config.searchDebounce;
563
+ if (delay > 0) self._searchTimer = setTimeout(apply, delay);
564
+ else apply();
565
+ });
566
+ this._clearFiltersBtn.addEventListener('click', function () {
567
+ if (self._searchTimer) { clearTimeout(self._searchTimer); self._searchTimer = null; }
568
+ self._state.filters = {};
569
+ self._state.search = '';
570
+ self._state.page = 1;
571
+ self._searchInput.value = '';
572
+ self._render();
573
+ self._emit('filter', { filters: {} });
574
+ });
575
+ this._copyBtn.addEventListener('click', function () { self.copyCSV(); });
576
+ this._exportCsvBtn.addEventListener('click', function () { self.exportCSV(); });
577
+ this._exportJsonBtn.addEventListener('click', function () { self.exportJSON(); });
578
+ this._pageSizeSelect.addEventListener('change', function () {
579
+ self.setPageSize(parseInt(self._pageSizeSelect.value, 10));
580
+ });
581
+ this._firstBtn.addEventListener('click', function () { self.goToPage(1); });
582
+ this._prevBtn.addEventListener('click', function () { self.goToPage(self._state.page - 1); });
583
+ this._nextBtn.addEventListener('click', function () { self.goToPage(self._state.page + 1); });
584
+ this._lastBtn.addEventListener('click', function () { self.goToPage(self._lastPageCount || 1); });
585
+ };
586
+
587
+ NeikiTable.prototype._injectStyles = function () {
588
+ if (EMBEDDED_CSS) {
589
+ var sheet = getSharedSheet(EMBEDDED_CSS);
590
+ if (sheet) {
591
+ this.shadowRoot.adoptedStyleSheets = [sheet];
592
+ return;
593
+ }
594
+ var style = document.createElement('style');
595
+ style.textContent = EMBEDDED_CSS;
596
+ this.shadowRoot.insertBefore(style, this.shadowRoot.firstChild);
597
+ return;
598
+ }
599
+ var link = document.createElement('link');
600
+ link.rel = 'stylesheet';
601
+ link.href = this._resolveStylesheetUrl();
602
+ this.shadowRoot.insertBefore(link, this.shadowRoot.firstChild);
603
+ };
604
+
605
+ NeikiTable.prototype._resolveStylesheetUrl = function () {
606
+ var scriptEl = document.currentScript;
607
+ if (!scriptEl) {
608
+ var scripts = document.querySelectorAll('script[src]');
609
+ for (var i = scripts.length - 1; i >= 0; i--) {
610
+ if (/neiki-table(\.min)?\.js/.test(scripts[i].src)) {
611
+ scriptEl = scripts[i];
612
+ break;
613
+ }
614
+ }
615
+ }
616
+ var src = scriptEl ? scriptEl.src : '';
617
+ if (/\.min\.js(\?.*)?$/.test(src)) {
618
+ return src.replace(/\.min\.js(\?.*)?$/, '.min.css$1');
619
+ }
620
+ if (/\.js(\?.*)?$/.test(src)) {
621
+ return src.replace(/\.js(\?.*)?$/, '.css$1');
622
+ }
623
+ return 'neiki-table.css';
624
+ };
625
+
626
+ NeikiTable.prototype.connectedCallback = function () {
627
+ this._readAttributesIntoConfig();
628
+ this._render();
629
+ if (!this._ready) {
630
+ this._ready = true;
631
+ this._emit('ready', { config: this.getConfig() });
632
+ }
633
+ };
634
+
635
+ NeikiTable.prototype.disconnectedCallback = function () {
636
+ if (this._mediaQuery) {
637
+ this._mediaQuery.removeEventListener('change', this._onMediaChange);
638
+ this._mediaQuery = null;
639
+ }
640
+ if (this._searchTimer) { clearTimeout(this._searchTimer); this._searchTimer = null; }
641
+ if (this._copyResetTimer) { clearTimeout(this._copyResetTimer); this._copyResetTimer = null; }
642
+ this._endResize();
643
+ };
644
+
645
+ NeikiTable.prototype.attributeChangedCallback = function (name, oldValue, newValue) {
646
+ if (this._reflecting || oldValue === newValue) return;
647
+ if (name === 'columns') {
648
+ if (newValue) {
649
+ try { this.setColumns(JSON.parse(newValue)); } catch (err) { this._emit('error', { reason: 'invalid-columns-json' }); }
650
+ }
651
+ return;
652
+ }
653
+ if (name === 'data') {
654
+ if (newValue) {
655
+ try { this.setData(JSON.parse(newValue)); } catch (err) { this._emit('error', { reason: 'invalid-data-json' }); }
656
+ }
657
+ return;
658
+ }
659
+ this._readAttributesIntoConfig();
660
+ if (this.isConnected) this._render();
661
+ };
662
+
663
+ NeikiTable.prototype._readAttributesIntoConfig = function () {
664
+ var cfg = this._config;
665
+ cfg.locale = this.getAttribute('locale') || cfg.locale || DEFAULT_CONFIG.locale;
666
+ cfg.theme = oneOf(this.getAttribute('theme'), VALID_THEMES, cfg.theme || DEFAULT_CONFIG.theme);
667
+ cfg.density = oneOf(this.getAttribute('density'), VALID_DENSITIES, cfg.density || DEFAULT_CONFIG.density);
668
+ cfg.rowKey = this.getAttribute('row-key') || cfg.rowKey || DEFAULT_CONFIG.rowKey;
669
+ cfg.searchable = toBool(this.getAttribute('searchable'), cfg.searchable);
670
+ cfg.filterable = toBool(this.getAttribute('filterable'), cfg.filterable);
671
+ cfg.selectable = toBool(this.getAttribute('selectable'), cfg.selectable);
672
+ cfg.editable = toBool(this.getAttribute('editable'), cfg.editable);
673
+ cfg.paginated = toBool(this.getAttribute('paginated'), cfg.paginated);
674
+ cfg.exportable = toBool(this.getAttribute('exportable'), cfg.exportable);
675
+ cfg.resizable = toBool(this.getAttribute('resizable'), cfg.resizable);
676
+ cfg.loading = this.hasAttribute('loading') && this.getAttribute('loading') !== 'false';
677
+ var debounce = parseInt(this.getAttribute('search-debounce'), 10);
678
+ if (!isNaN(debounce) && debounce >= 0) cfg.searchDebounce = debounce;
679
+ var pageSize = parseInt(this.getAttribute('page-size'), 10);
680
+ if (!isNaN(pageSize) && pageSize > 0) cfg.pageSize = pageSize;
681
+ };
682
+
683
+ NeikiTable.prototype._reflectAttributes = function () {
684
+ this._reflecting = true;
685
+ var cfg = this._config;
686
+ this.setAttribute('locale', cfg.locale);
687
+ this.setAttribute('theme', cfg.theme);
688
+ this.setAttribute('density', cfg.density);
689
+ if (cfg.loading) this.setAttribute('loading', '');
690
+ else this.removeAttribute('loading');
691
+ this._reflecting = false;
692
+ };
693
+
694
+ NeikiTable.prototype._resolveTheme = function () {
695
+ if (this._config.theme !== 'auto') return this._config.theme;
696
+ if (!this._mediaQuery) {
697
+ this._mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
698
+ this._mediaQuery.addEventListener('change', this._onMediaChange);
699
+ }
700
+ return this._mediaQuery.matches ? 'dark' : 'light';
701
+ };
702
+
703
+ NeikiTable.prototype._onMediaChange = function () {
704
+ if (this._config.theme === 'auto') this._render();
705
+ };
706
+
707
+ NeikiTable.prototype._t = function (key, vars) {
708
+ return translate(this._config.locale, this._i18n, key, vars);
709
+ };
710
+
711
+ // ---------------------------------------------------------------------
712
+ // Data pipeline
713
+ // ---------------------------------------------------------------------
714
+
715
+ NeikiTable.prototype._filterableColumns = function () {
716
+ return this._columns.filter(function (col) { return col.filterable !== false; });
717
+ };
718
+
719
+ NeikiTable.prototype._cellText = function (row, col) {
720
+ var value = getValue(row, col.key);
721
+ if (typeof col.format === 'function') {
722
+ try {
723
+ var out = col.format(value, row);
724
+ return out === null || out === undefined ? '' : String(out);
725
+ } catch (err) { /* fall through to default formatting */ }
726
+ }
727
+ return formatDisplay(value, col, this._config.locale);
728
+ };
729
+
730
+ NeikiTable.prototype._computeView = function () {
731
+ var self = this;
732
+ var cfg = this._config;
733
+ var state = this._state;
734
+ var rows = this._rows.slice();
735
+
736
+ if (cfg.searchable && state.search) {
737
+ var query = state.search.toLowerCase();
738
+ rows = rows.filter(function (row) {
739
+ return self._columns.some(function (col) {
740
+ return self._cellText(row, col).toLowerCase().indexOf(query) !== -1;
741
+ });
742
+ });
743
+ }
744
+
745
+ if (cfg.filterable) {
746
+ Object.keys(state.filters).forEach(function (key) {
747
+ var filterValue = state.filters[key];
748
+ if (filterValue === undefined || filterValue === null || filterValue === '') return;
749
+ var col = self._columns.filter(function (c) { return c.key === key; })[0];
750
+ if (!col) return;
751
+ rows = rows.filter(function (row) {
752
+ var raw = getValue(row, key);
753
+ if (col.type === 'boolean') {
754
+ return String(!!raw) === filterValue;
755
+ }
756
+ if (col.type === 'select') {
757
+ return String(raw) === filterValue;
758
+ }
759
+ var display = self._cellText(row, col).toLowerCase();
760
+ return display.indexOf(String(filterValue).toLowerCase()) !== -1;
761
+ });
762
+ });
763
+ }
764
+
765
+ if (state.sort.key) {
766
+ var sortCol = this._columns.filter(function (c) { return c.key === state.sort.key; })[0];
767
+ var type = sortCol ? sortCol.type : 'text';
768
+ var dir = state.sort.dir === 'desc' ? -1 : 1;
769
+ rows.sort(function (a, b) {
770
+ return dir * compareValues(getValue(a, state.sort.key), getValue(b, state.sort.key), type);
771
+ });
772
+ }
773
+
774
+ return rows;
775
+ };
776
+
777
+ NeikiTable.prototype._rowKeyValue = function (row, index) {
778
+ var key = this._config.rowKey;
779
+ return row && row[key] !== undefined ? row[key] : '__index_' + index;
780
+ };
781
+
782
+ // ---------------------------------------------------------------------
783
+ // Rendering
784
+ // ---------------------------------------------------------------------
785
+
786
+ NeikiTable.prototype._captureFocus = function () {
787
+ var active = this.shadowRoot.activeElement;
788
+ if (!active) return null;
789
+ if (active === this._searchInput) {
790
+ return { type: 'search', start: active.selectionStart, end: active.selectionEnd };
791
+ }
792
+ if (active.classList && active.classList.contains('ntbl-filter-input')) {
793
+ return { type: 'filter', key: active.dataset.key, start: active.selectionStart, end: active.selectionEnd };
794
+ }
795
+ return null;
796
+ };
797
+
798
+ NeikiTable.prototype._restoreFocus = function (info) {
799
+ if (!info) return;
800
+ var el = null;
801
+ if (info.type === 'search') {
802
+ el = this._searchInput;
803
+ } else if (info.type === 'filter') {
804
+ el = this._filterRow.querySelector('.ntbl-filter-input[data-key="' + info.key + '"]');
805
+ }
806
+ if (!el) return;
807
+ el.focus();
808
+ if (typeof info.start === 'number' && typeof el.setSelectionRange === 'function') {
809
+ try { el.setSelectionRange(info.start, info.end); } catch (err) { /* not a text selection input */ }
810
+ }
811
+ };
812
+
813
+ NeikiTable.prototype._render = function () {
814
+ var focusInfo = this._captureFocus();
815
+ this._reflectAttributes();
816
+ this.setAttribute('resolved-theme', this._resolveTheme());
817
+
818
+ this._searchInput.placeholder = this._t('searchPlaceholder');
819
+ if (document.activeElement !== this && this.shadowRoot.activeElement !== this._searchInput) {
820
+ this._searchInput.value = this._state.search;
821
+ }
822
+ this._searchInput.parentElement.hidden = !this._config.searchable;
823
+ this._clearFiltersBtn.textContent = this._t('clearFilters');
824
+ if (!this._copyResetTimer) this._copyBtn.textContent = this._t('copy');
825
+ this._exportCsvBtn.textContent = this._t('exportCsv');
826
+ this._exportJsonBtn.textContent = this._t('exportJson');
827
+ this._clearFiltersBtn.hidden = !(this._config.filterable || this._config.searchable);
828
+ this._copyBtn.hidden = !this._config.exportable;
829
+ this._exportCsvBtn.hidden = !this._config.exportable;
830
+ this._exportJsonBtn.hidden = !this._config.exportable;
831
+
832
+ var selectedKeys = Object.keys(this._state.selected).filter(function (k) { return this[k]; }, this._state.selected);
833
+ if (this._config.selectable && selectedKeys.length > 0) {
834
+ this._selectedCountEl.hidden = false;
835
+ this._selectedCountEl.textContent = this._t('selectedCount', { count: selectedKeys.length });
836
+ } else {
837
+ this._selectedCountEl.hidden = true;
838
+ }
839
+
840
+ var view = this._computeView();
841
+ var total = view.length;
842
+
843
+ var pageSize = this._config.paginated ? this._config.pageSize : total || 1;
844
+ var pageCount = Math.max(1, Math.ceil(total / pageSize));
845
+ if (this._state.page > pageCount) this._state.page = pageCount;
846
+ if (this._state.page < 1) this._state.page = 1;
847
+
848
+ this._lastPageCount = pageCount;
849
+
850
+ var start = this._config.paginated ? (this._state.page - 1) * pageSize : 0;
851
+ var pageRows = this._config.paginated ? view.slice(start, start + pageSize) : view;
852
+
853
+ this._renderColgroup();
854
+ this._renderHeader();
855
+ this._renderFilterRow();
856
+ this._renderBody(pageRows, view, start);
857
+ this._renderFooter(total, pageCount, start, pageRows.length);
858
+ this._announce(total);
859
+ this._restoreFocus(focusInfo);
860
+ };
861
+
862
+ NeikiTable.prototype._announce = function (total) {
863
+ if (!this._liveRegion) return;
864
+ var msg = this._t('resultsAnnounce', { total: total });
865
+ if (this._liveRegion.textContent !== msg) this._liveRegion.textContent = msg;
866
+ };
867
+
868
+ NeikiTable.prototype._renderColgroup = function () {
869
+ var self = this;
870
+ this._colgroup.textContent = '';
871
+ if (this._config.selectable) {
872
+ var selCol = document.createElement('col');
873
+ selCol.style.width = '44px';
874
+ this._colgroup.appendChild(selCol);
875
+ }
876
+ this._columns.forEach(function (col) {
877
+ var c = document.createElement('col');
878
+ var width = self._state.columnWidths[col.key] || col.width;
879
+ if (width) c.style.width = typeof width === 'number' ? width + 'px' : width;
880
+ self._colgroup.appendChild(c);
881
+ });
882
+ };
883
+
884
+ NeikiTable.prototype._renderHeader = function () {
885
+ var self = this;
886
+ var cfg = this._config;
887
+ this._headerRow.textContent = '';
888
+
889
+ if (cfg.selectable) {
890
+ var th = document.createElement('th');
891
+ th.className = 'ntbl-th ntbl-th-select';
892
+ th.setAttribute('part', 'th');
893
+ var checkbox = document.createElement('input');
894
+ checkbox.type = 'checkbox';
895
+ checkbox.className = 'ntbl-select-all';
896
+ checkbox.setAttribute('aria-label', this._t('selectAll'));
897
+ var view = this._computeView();
898
+ var allSelected = view.length > 0 && view.every(function (row, i) {
899
+ return self._state.selected[self._rowKeyValue(row, i)];
900
+ });
901
+ checkbox.checked = allSelected;
902
+ checkbox.addEventListener('change', function () {
903
+ self.selectAll(checkbox.checked);
904
+ });
905
+ th.appendChild(checkbox);
906
+ this._headerRow.appendChild(th);
907
+ }
908
+
909
+ this._columns.forEach(function (col, colIndex) {
910
+ var cell = document.createElement('th');
911
+ cell.className = 'ntbl-th';
912
+ cell.setAttribute('part', 'th');
913
+ var align = oneOf(col.align, VALID_ALIGN, 'left');
914
+ if (align !== 'left') cell.classList.add('ntbl-th-align-' + align);
915
+
916
+ var label = document.createElement('span');
917
+ label.className = 'ntbl-th-label';
918
+ label.textContent = col.label !== undefined ? col.label : col.key;
919
+ cell.appendChild(label);
920
+
921
+ if (col.sortable !== false) {
922
+ cell.classList.add('ntbl-th-sortable');
923
+ var isSorted = self._state.sort.key === col.key;
924
+ if (isSorted) {
925
+ cell.classList.add('ntbl-th-sorted-' + self._state.sort.dir);
926
+ cell.setAttribute('aria-sort', self._state.sort.dir === 'asc' ? 'ascending' : 'descending');
927
+ }
928
+ var icon = document.createElement('span');
929
+ icon.className = 'ntbl-sort-icon';
930
+ icon.setAttribute('aria-hidden', 'true');
931
+ cell.appendChild(icon);
932
+ cell.setAttribute('tabindex', '0');
933
+ cell.setAttribute('role', 'button');
934
+ cell.addEventListener('click', function () { self._toggleSort(col.key); });
935
+ cell.addEventListener('keydown', function (event) {
936
+ if (event.key === 'Enter' || event.key === ' ') {
937
+ event.preventDefault();
938
+ self._toggleSort(col.key);
939
+ }
940
+ });
941
+ }
942
+
943
+ if (self._config.resizable && col.resizable !== false) {
944
+ var handle = document.createElement('span');
945
+ handle.className = 'ntbl-resize-handle';
946
+ handle.setAttribute('aria-hidden', 'true');
947
+ var colOffset = self._config.selectable ? 1 : 0;
948
+ handle.addEventListener('pointerdown', function (event) {
949
+ self._startResize(event, col.key, cell, colOffset + colIndex);
950
+ });
951
+ handle.addEventListener('click', function (event) { event.stopPropagation(); });
952
+ cell.appendChild(handle);
953
+ }
954
+
955
+ self._headerRow.appendChild(cell);
956
+ });
957
+ };
958
+
959
+ // ---------------------------------------------------------------------
960
+ // Column resizing
961
+ // ---------------------------------------------------------------------
962
+
963
+ NeikiTable.prototype._startResize = function (event, key, cell, colElIndex) {
964
+ event.preventDefault();
965
+ event.stopPropagation();
966
+ var colEl = this._colgroup.children[colElIndex];
967
+ this._resize = {
968
+ key: key,
969
+ cell: cell,
970
+ colEl: colEl,
971
+ startX: event.clientX,
972
+ startWidth: cell.getBoundingClientRect().width
973
+ };
974
+ cell.classList.add('ntbl-th-resizing');
975
+ document.addEventListener('pointermove', this._onResizeMove);
976
+ document.addEventListener('pointerup', this._onResizeEnd);
977
+ document.addEventListener('pointercancel', this._onResizeEnd);
978
+ };
979
+
980
+ NeikiTable.prototype._onResizeMove = function (event) {
981
+ if (!this._resize) return;
982
+ var delta = event.clientX - this._resize.startX;
983
+ var width = Math.max(MIN_COLUMN_WIDTH, Math.round(this._resize.startWidth + delta));
984
+ if (this._resize.colEl) this._resize.colEl.style.width = width + 'px';
985
+ this._resize.currentWidth = width;
986
+ };
987
+
988
+ NeikiTable.prototype._onResizeEnd = function () {
989
+ if (!this._resize) return;
990
+ if (this._resize.currentWidth) {
991
+ this._state.columnWidths[this._resize.key] = this._resize.currentWidth;
992
+ this._emit('column-resize', { key: this._resize.key, width: this._resize.currentWidth });
993
+ }
994
+ this._endResize();
995
+ };
996
+
997
+ NeikiTable.prototype._endResize = function () {
998
+ if (this._resize && this._resize.cell) this._resize.cell.classList.remove('ntbl-th-resizing');
999
+ document.removeEventListener('pointermove', this._onResizeMove);
1000
+ document.removeEventListener('pointerup', this._onResizeEnd);
1001
+ document.removeEventListener('pointercancel', this._onResizeEnd);
1002
+ this._resize = null;
1003
+ };
1004
+
1005
+ NeikiTable.prototype._toggleSort = function (key) {
1006
+ var state = this._state;
1007
+ if (state.sort.key !== key) {
1008
+ state.sort.key = key;
1009
+ state.sort.dir = 'asc';
1010
+ } else if (state.sort.dir === 'asc') {
1011
+ state.sort.dir = 'desc';
1012
+ } else {
1013
+ state.sort.key = null;
1014
+ state.sort.dir = null;
1015
+ }
1016
+ this._render();
1017
+ this._emit('sort', { key: state.sort.key, dir: state.sort.dir });
1018
+ };
1019
+
1020
+ NeikiTable.prototype._renderFilterRow = function () {
1021
+ var self = this;
1022
+ var cfg = this._config;
1023
+ this._filterRow.textContent = '';
1024
+ this._filterRow.hidden = !cfg.filterable;
1025
+ if (!cfg.filterable) return;
1026
+
1027
+ if (cfg.selectable) {
1028
+ var spacer = document.createElement('th');
1029
+ spacer.className = 'ntbl-th ntbl-th-select';
1030
+ this._filterRow.appendChild(spacer);
1031
+ }
1032
+
1033
+ this._columns.forEach(function (col) {
1034
+ var th = document.createElement('th');
1035
+ th.className = 'ntbl-th ntbl-filter-cell';
1036
+ if (col.filterable === false) {
1037
+ self._filterRow.appendChild(th);
1038
+ return;
1039
+ }
1040
+
1041
+ var currentValue = self._state.filters[col.key] || '';
1042
+
1043
+ if (col.type === 'boolean') {
1044
+ var boolSelect = document.createElement('select');
1045
+ boolSelect.className = 'ntbl-filter-input';
1046
+ boolSelect.dataset.key = col.key;
1047
+ boolSelect.appendChild(new Option(self._t('all'), ''));
1048
+ boolSelect.appendChild(new Option(self._t('yes'), 'true'));
1049
+ boolSelect.appendChild(new Option(self._t('no'), 'false'));
1050
+ boolSelect.value = currentValue;
1051
+ boolSelect.addEventListener('change', function () {
1052
+ self._setFilterInternal(col.key, boolSelect.value);
1053
+ });
1054
+ th.appendChild(boolSelect);
1055
+ } else if (col.type === 'select') {
1056
+ var select = document.createElement('select');
1057
+ select.className = 'ntbl-filter-input';
1058
+ select.dataset.key = col.key;
1059
+ select.appendChild(new Option(self._t('all'), ''));
1060
+ normalizeOptions(col).forEach(function (opt) {
1061
+ select.appendChild(new Option(opt.label, String(opt.value)));
1062
+ });
1063
+ select.value = currentValue;
1064
+ select.addEventListener('change', function () {
1065
+ self._setFilterInternal(col.key, select.value);
1066
+ });
1067
+ th.appendChild(select);
1068
+ } else {
1069
+ var input = document.createElement('input');
1070
+ input.type = 'text';
1071
+ input.className = 'ntbl-filter-input';
1072
+ input.dataset.key = col.key;
1073
+ input.placeholder = self._t('filterPlaceholder');
1074
+ input.value = currentValue;
1075
+ input.addEventListener('input', function () {
1076
+ self._setFilterInternal(col.key, input.value);
1077
+ });
1078
+ th.appendChild(input);
1079
+ }
1080
+
1081
+ self._filterRow.appendChild(th);
1082
+ });
1083
+ };
1084
+
1085
+ NeikiTable.prototype._setFilterInternal = function (key, value) {
1086
+ if (value === '') delete this._state.filters[key];
1087
+ else this._state.filters[key] = value;
1088
+ this._state.page = 1;
1089
+ this._render();
1090
+ this._emit('filter', { filters: Object.assign({}, this._state.filters) });
1091
+ };
1092
+
1093
+ NeikiTable.prototype._renderBody = function (pageRows, fullView, offset) {
1094
+ var self = this;
1095
+ var cfg = this._config;
1096
+ this._tbody.textContent = '';
1097
+
1098
+ var hasData = this._rows.length > 0;
1099
+ var hasResults = pageRows.length > 0;
1100
+ this._emptyEl.hidden = hasResults;
1101
+ if (!hasResults) {
1102
+ this._emptyEl.textContent = hasData ? this._t('noResults') : this._t('noData');
1103
+ }
1104
+
1105
+ pageRows.forEach(function (row, i) {
1106
+ var index = offset + i;
1107
+ var rowKey = self._rowKeyValue(row, index);
1108
+ var tr = document.createElement('tr');
1109
+ tr.className = 'ntbl-tr';
1110
+ tr.setAttribute('part', 'tr');
1111
+ if (self._state.selected[rowKey]) tr.classList.add('ntbl-tr-selected');
1112
+ tr.addEventListener('click', function (event) {
1113
+ var tag = event.target && event.target.tagName;
1114
+ if (tag === 'INPUT' || tag === 'SELECT' || tag === 'OPTION') return;
1115
+ self._emit('row-click', { rowKey: rowKey, row: Object.assign({}, row) });
1116
+ });
1117
+
1118
+ if (cfg.selectable) {
1119
+ var td = document.createElement('td');
1120
+ td.className = 'ntbl-td ntbl-td-select';
1121
+ var checkbox = document.createElement('input');
1122
+ checkbox.type = 'checkbox';
1123
+ checkbox.setAttribute('aria-label', self._t('selectRow'));
1124
+ checkbox.checked = !!self._state.selected[rowKey];
1125
+ checkbox.addEventListener('change', function () {
1126
+ self.selectRow(rowKey, checkbox.checked);
1127
+ });
1128
+ td.appendChild(checkbox);
1129
+ tr.appendChild(td);
1130
+ }
1131
+
1132
+ self._columns.forEach(function (col) {
1133
+ var td = document.createElement('td');
1134
+ td.className = 'ntbl-td';
1135
+ td.setAttribute('part', 'td');
1136
+ var align = oneOf(col.align, VALID_ALIGN, col.type === 'number' ? 'right' : 'left');
1137
+ if (align !== 'left') td.classList.add('ntbl-td-align-' + align);
1138
+ else if (col.type === 'number') td.classList.add('ntbl-td-num');
1139
+ self._renderCell(td, row, col, rowKey);
1140
+ tr.appendChild(td);
1141
+ });
1142
+
1143
+ self._tbody.appendChild(tr);
1144
+ });
1145
+ };
1146
+
1147
+ NeikiTable.prototype._renderCell = function (td, row, col, rowKey) {
1148
+ var self = this;
1149
+ var value = getValue(row, col.key);
1150
+ var editable = this._config.editable && col.editable !== false;
1151
+ var isEditing = this._state.editing && this._state.editing.rowKey === rowKey && this._state.editing.key === col.key;
1152
+
1153
+ if (isEditing) {
1154
+ td.classList.add('ntbl-td-editing');
1155
+ this._renderEditor(td, row, col, rowKey, value);
1156
+ return;
1157
+ }
1158
+
1159
+ if (col.type === 'boolean') {
1160
+ var badge = document.createElement('span');
1161
+ badge.className = 'ntbl-badge ' + (value ? 'ntbl-badge-true' : 'ntbl-badge-false');
1162
+ badge.textContent = value ? this._t('yes') : this._t('no');
1163
+ td.appendChild(badge);
1164
+ if (editable) {
1165
+ td.classList.add('ntbl-td-editable');
1166
+ td.addEventListener('click', function () {
1167
+ self._commitEdit(rowKey, col.key, !value);
1168
+ });
1169
+ }
1170
+ return;
1171
+ }
1172
+
1173
+ td.textContent = this._cellText(row, col);
1174
+
1175
+ if (editable) {
1176
+ td.classList.add('ntbl-td-editable');
1177
+ td.setAttribute('tabindex', '0');
1178
+ td.addEventListener('dblclick', function () { self._startEdit(rowKey, col.key); });
1179
+ td.addEventListener('keydown', function (event) {
1180
+ if (event.key === 'Enter') self._startEdit(rowKey, col.key);
1181
+ });
1182
+ }
1183
+ };
1184
+
1185
+ NeikiTable.prototype._startEdit = function (rowKey, key) {
1186
+ this._state.editing = { rowKey: rowKey, key: key };
1187
+ this._render();
1188
+ };
1189
+
1190
+ NeikiTable.prototype._renderEditor = function (td, row, col, rowKey, value) {
1191
+ var self = this;
1192
+ td.textContent = '';
1193
+ var input;
1194
+
1195
+ if (col.type === 'select') {
1196
+ input = document.createElement('select');
1197
+ normalizeOptions(col).forEach(function (opt) {
1198
+ input.appendChild(new Option(opt.label, String(opt.value)));
1199
+ });
1200
+ input.value = String(value);
1201
+ } else if (col.type === 'number') {
1202
+ input = document.createElement('input');
1203
+ input.type = 'number';
1204
+ input.value = value === null || value === undefined ? '' : value;
1205
+ } else if (col.type === 'date') {
1206
+ input = document.createElement('input');
1207
+ input.type = 'date';
1208
+ input.value = value ? String(value).slice(0, 10) : '';
1209
+ } else {
1210
+ input = document.createElement('input');
1211
+ input.type = 'text';
1212
+ input.value = value === null || value === undefined ? '' : value;
1213
+ }
1214
+
1215
+ input.className = 'ntbl-edit-input';
1216
+
1217
+ function commit() {
1218
+ var newValue = input.value;
1219
+ if (col.type === 'number') newValue = newValue === '' ? null : Number(newValue);
1220
+ self._commitEdit(rowKey, col.key, newValue);
1221
+ }
1222
+ function cancel() {
1223
+ self._state.editing = null;
1224
+ self._render();
1225
+ }
1226
+
1227
+ input.addEventListener('keydown', function (event) {
1228
+ if (event.key === 'Enter') { event.preventDefault(); commit(); }
1229
+ else if (event.key === 'Escape') { event.preventDefault(); cancel(); }
1230
+ });
1231
+ input.addEventListener('blur', function () { commit(); });
1232
+
1233
+ td.appendChild(input);
1234
+ requestAnimationFrame(function () { input.focus(); input.select && input.select(); });
1235
+ };
1236
+
1237
+ NeikiTable.prototype._commitEdit = function (rowKey, key, newValue) {
1238
+ var self = this;
1239
+ var row = this._rows.filter(function (r, i) { return self._rowKeyValue(r, i) === rowKey; })[0];
1240
+ this._state.editing = null;
1241
+ if (!row) { this._render(); return; }
1242
+ var oldValue = row[key];
1243
+ if (oldValue === newValue) { this._render(); return; }
1244
+ row[key] = newValue;
1245
+ this._render();
1246
+ this._emit('cell-edit', { rowKey: rowKey, key: key, oldValue: oldValue, newValue: newValue, row: Object.assign({}, row) });
1247
+ };
1248
+
1249
+ NeikiTable.prototype._renderFooter = function (total, pageCount, start, pageRowsLength) {
1250
+ var cfg = this._config;
1251
+
1252
+ this._infoEl.textContent = total === 0
1253
+ ? this._t('showingNone')
1254
+ : this._t('showingRange', {
1255
+ start: start + 1,
1256
+ end: start + pageRowsLength,
1257
+ total: total
1258
+ });
1259
+
1260
+ var paginationEl = this.shadowRoot.querySelector('.ntbl-pagination');
1261
+ paginationEl.hidden = !cfg.paginated;
1262
+ if (!cfg.paginated) return;
1263
+
1264
+ this._pageSizeSelect.textContent = '';
1265
+ var self = this;
1266
+ PAGE_SIZE_OPTIONS.forEach(function (size) {
1267
+ var opt = new Option(String(size) + ' / ' + self._t('rowsPerPage'), String(size));
1268
+ opt.selected = size === cfg.pageSize;
1269
+ self._pageSizeSelect.appendChild(opt);
1270
+ });
1271
+
1272
+ var page = this._state.page;
1273
+ this._firstBtn.setAttribute('aria-label', this._t('firstPage'));
1274
+ this._prevBtn.setAttribute('aria-label', this._t('previous'));
1275
+ this._nextBtn.setAttribute('aria-label', this._t('next'));
1276
+ this._lastBtn.setAttribute('aria-label', this._t('lastPage'));
1277
+ this._firstBtn.disabled = page <= 1;
1278
+ this._prevBtn.disabled = page <= 1;
1279
+ this._nextBtn.disabled = page >= pageCount;
1280
+ this._lastBtn.disabled = page >= pageCount;
1281
+
1282
+ var showNumbers = pageCount > 1 && pageCount <= 200;
1283
+ this._firstBtn.hidden = pageCount <= 2;
1284
+ this._lastBtn.hidden = pageCount <= 2;
1285
+
1286
+ this._pageNumbers.textContent = '';
1287
+ if (showNumbers) {
1288
+ this._pageIndicator.hidden = true;
1289
+ this._renderPageNumbers(page, pageCount);
1290
+ } else {
1291
+ this._pageIndicator.hidden = false;
1292
+ this._pageIndicator.textContent = this._t('pageOf', { page: page, pages: pageCount });
1293
+ }
1294
+ };
1295
+
1296
+ NeikiTable.prototype._buildPageList = function (current, total) {
1297
+ var delta = 1;
1298
+ var pages = [];
1299
+ var last = 0;
1300
+ for (var i = 1; i <= total; i++) {
1301
+ if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
1302
+ if (last && i - last > 1) pages.push('…');
1303
+ pages.push(i);
1304
+ last = i;
1305
+ }
1306
+ }
1307
+ return pages;
1308
+ };
1309
+
1310
+ NeikiTable.prototype._renderPageNumbers = function (current, total) {
1311
+ var self = this;
1312
+ this._buildPageList(current, total).forEach(function (entry) {
1313
+ if (entry === '…') {
1314
+ var gap = document.createElement('span');
1315
+ gap.className = 'ntbl-page-ellipsis';
1316
+ gap.textContent = '…';
1317
+ self._pageNumbers.appendChild(gap);
1318
+ return;
1319
+ }
1320
+ var btn = document.createElement('button');
1321
+ btn.type = 'button';
1322
+ btn.className = 'ntbl-page-btn';
1323
+ btn.setAttribute('part', 'button');
1324
+ btn.textContent = String(entry);
1325
+ btn.setAttribute('aria-label', self._t('gotoPage', { page: entry }));
1326
+ if (entry === current) {
1327
+ btn.classList.add('ntbl-page-current');
1328
+ btn.setAttribute('aria-current', 'page');
1329
+ } else {
1330
+ btn.addEventListener('click', function () { self.goToPage(entry); });
1331
+ }
1332
+ self._pageNumbers.appendChild(btn);
1333
+ });
1334
+ };
1335
+
1336
+ NeikiTable.prototype._emit = function (name, detail) {
1337
+ this.dispatchEvent(new CustomEvent('neiki-table:' + name, {
1338
+ detail: detail,
1339
+ bubbles: true,
1340
+ composed: true
1341
+ }));
1342
+ };
1343
+
1344
+ // ---------------------------------------------------------------------
1345
+ // Public API
1346
+ // ---------------------------------------------------------------------
1347
+
1348
+ NeikiTable.prototype.setColumns = function (columns) {
1349
+ this._columns = (columns || []).map(function (col) {
1350
+ return Object.assign({
1351
+ type: 'text',
1352
+ sortable: true,
1353
+ filterable: true,
1354
+ editable: true
1355
+ }, col, {
1356
+ type: oneOf(col.type, VALID_TYPES, 'text')
1357
+ });
1358
+ });
1359
+ this._state.columnWidths = {};
1360
+ if (this.isConnected) this._render();
1361
+ return this;
1362
+ };
1363
+
1364
+ NeikiTable.prototype.getColumns = function () {
1365
+ return this._columns.map(function (col) { return Object.assign({}, col); });
1366
+ };
1367
+
1368
+ NeikiTable.prototype.setData = function (rows) {
1369
+ this._rows = Array.isArray(rows) ? rows.slice() : [];
1370
+ this._state.selected = {};
1371
+ this._state.page = 1;
1372
+ if (this.isConnected) this._render();
1373
+ return this;
1374
+ };
1375
+
1376
+ NeikiTable.prototype.getData = function () {
1377
+ return this._rows.map(function (row) { return Object.assign({}, row); });
1378
+ };
1379
+
1380
+ NeikiTable.prototype.setLocale = function (locale) {
1381
+ this._config.locale = locale;
1382
+ if (this.isConnected) this._render();
1383
+ return this;
1384
+ };
1385
+
1386
+ NeikiTable.prototype.getLocale = function () {
1387
+ return this._config.locale;
1388
+ };
1389
+
1390
+ NeikiTable.prototype.addTranslations = function (locale, dictionary) {
1391
+ this._i18n[locale] = Object.assign({}, this._i18n[locale] || {}, dictionary || {});
1392
+ if (this.isConnected) this._render();
1393
+ return this;
1394
+ };
1395
+
1396
+ NeikiTable.prototype.setConfig = function (config) {
1397
+ config = config || {};
1398
+ var cfg = this._config;
1399
+ if (config.locale !== undefined) cfg.locale = config.locale;
1400
+ if (config.theme !== undefined) cfg.theme = oneOf(config.theme, VALID_THEMES, cfg.theme);
1401
+ if (config.density !== undefined) cfg.density = oneOf(config.density, VALID_DENSITIES, cfg.density);
1402
+ if (config.rowKey !== undefined) cfg.rowKey = config.rowKey;
1403
+ if (config.searchable !== undefined) cfg.searchable = !!config.searchable;
1404
+ if (config.filterable !== undefined) cfg.filterable = !!config.filterable;
1405
+ if (config.selectable !== undefined) cfg.selectable = !!config.selectable;
1406
+ if (config.editable !== undefined) cfg.editable = !!config.editable;
1407
+ if (config.paginated !== undefined) cfg.paginated = !!config.paginated;
1408
+ if (config.exportable !== undefined) cfg.exportable = !!config.exportable;
1409
+ if (config.resizable !== undefined) cfg.resizable = !!config.resizable;
1410
+ if (config.loading !== undefined) cfg.loading = !!config.loading;
1411
+ if (config.searchDebounce !== undefined && config.searchDebounce >= 0) cfg.searchDebounce = config.searchDebounce;
1412
+ if (config.pageSize !== undefined) cfg.pageSize = config.pageSize;
1413
+ if (this.isConnected) this._render();
1414
+ return this;
1415
+ };
1416
+
1417
+ NeikiTable.prototype.getConfig = function () {
1418
+ return Object.assign({}, this._config);
1419
+ };
1420
+
1421
+ NeikiTable.prototype.sortBy = function (key, dir) {
1422
+ this._state.sort.key = key || null;
1423
+ this._state.sort.dir = key ? oneOf(dir, ['asc', 'desc'], 'asc') : null;
1424
+ if (this.isConnected) this._render();
1425
+ this._emit('sort', { key: this._state.sort.key, dir: this._state.sort.dir });
1426
+ return this;
1427
+ };
1428
+
1429
+ NeikiTable.prototype.search = function (query) {
1430
+ this._state.search = query || '';
1431
+ this._state.page = 1;
1432
+ if (this.isConnected) this._render();
1433
+ this._emit('search', { query: this._state.search });
1434
+ return this;
1435
+ };
1436
+
1437
+ NeikiTable.prototype.setFilter = function (key, value) {
1438
+ if (value === undefined || value === null || value === '') delete this._state.filters[key];
1439
+ else this._state.filters[key] = value;
1440
+ this._state.page = 1;
1441
+ if (this.isConnected) this._render();
1442
+ this._emit('filter', { filters: Object.assign({}, this._state.filters) });
1443
+ return this;
1444
+ };
1445
+
1446
+ NeikiTable.prototype.clearFilters = function () {
1447
+ this._state.filters = {};
1448
+ this._state.search = '';
1449
+ this._state.page = 1;
1450
+ if (this.isConnected) this._render();
1451
+ this._emit('filter', { filters: {} });
1452
+ return this;
1453
+ };
1454
+
1455
+ NeikiTable.prototype.goToPage = function (page) {
1456
+ this._state.page = Math.max(1, page);
1457
+ if (this.isConnected) this._render();
1458
+ this._emit('page-change', { page: this._state.page });
1459
+ return this;
1460
+ };
1461
+
1462
+ NeikiTable.prototype.setPageSize = function (size) {
1463
+ this._config.pageSize = size;
1464
+ this._state.page = 1;
1465
+ if (this.isConnected) this._render();
1466
+ this._emit('page-change', { page: this._state.page, pageSize: size });
1467
+ return this;
1468
+ };
1469
+
1470
+ NeikiTable.prototype.selectRow = function (rowKey, selected) {
1471
+ if (selected) this._state.selected[rowKey] = true;
1472
+ else delete this._state.selected[rowKey];
1473
+ if (this.isConnected) this._render();
1474
+ this._emit('select', { selected: this.getSelectedKeys() });
1475
+ return this;
1476
+ };
1477
+
1478
+ NeikiTable.prototype.selectAll = function (selected) {
1479
+ var self = this;
1480
+ var view = this._computeView();
1481
+ if (selected) {
1482
+ view.forEach(function (row, i) { self._state.selected[self._rowKeyValue(row, i)] = true; });
1483
+ } else {
1484
+ view.forEach(function (row, i) { delete self._state.selected[self._rowKeyValue(row, i)]; });
1485
+ }
1486
+ if (this.isConnected) this._render();
1487
+ this._emit('select', { selected: this.getSelectedKeys() });
1488
+ return this;
1489
+ };
1490
+
1491
+ NeikiTable.prototype.clearSelection = function () {
1492
+ this._state.selected = {};
1493
+ if (this.isConnected) this._render();
1494
+ this._emit('select', { selected: [] });
1495
+ return this;
1496
+ };
1497
+
1498
+ NeikiTable.prototype.getSelectedKeys = function () {
1499
+ var selected = this._state.selected;
1500
+ return Object.keys(selected).filter(function (key) { return selected[key]; });
1501
+ };
1502
+
1503
+ NeikiTable.prototype.getSelectedRows = function () {
1504
+ var self = this;
1505
+ var keys = this.getSelectedKeys();
1506
+ return this._rows.filter(function (row, i) {
1507
+ return keys.indexOf(String(self._rowKeyValue(row, i))) !== -1;
1508
+ }).map(function (row) { return Object.assign({}, row); });
1509
+ };
1510
+
1511
+ NeikiTable.prototype._exportRows = function () {
1512
+ var selected = this.getSelectedKeys();
1513
+ if (selected.length > 0) return this.getSelectedRows();
1514
+ return this._computeView();
1515
+ };
1516
+
1517
+ NeikiTable.prototype._buildCSV = function () {
1518
+ var self = this;
1519
+ var rows = this._exportRows();
1520
+ var headers = this._columns.map(function (col) { return col.label || col.key; });
1521
+ var lines = [headers.map(csvEscape).join(',')];
1522
+ rows.forEach(function (row) {
1523
+ var line = self._columns.map(function (col) {
1524
+ return csvEscape(self._cellText(row, col));
1525
+ }).join(',');
1526
+ lines.push(line);
1527
+ });
1528
+ return { text: lines.join('\r\n'), count: rows.length };
1529
+ };
1530
+
1531
+ NeikiTable.prototype.exportCSV = function (filename) {
1532
+ var csv = this._buildCSV();
1533
+ downloadBlob(csv.text, 'text/csv;charset=utf-8', filename || 'neiki-table-export.csv');
1534
+ this._emit('export', { format: 'csv', count: csv.count });
1535
+ return this;
1536
+ };
1537
+
1538
+ NeikiTable.prototype.copyCSV = function () {
1539
+ var self = this;
1540
+ var csv = this._buildCSV();
1541
+ var done = function (ok) {
1542
+ self._emit('copy', { format: 'csv', count: csv.count, ok: ok });
1543
+ if (!ok || !self._copyBtn) return;
1544
+ if (self._copyResetTimer) clearTimeout(self._copyResetTimer);
1545
+ self._copyBtn.textContent = self._t('copied');
1546
+ self._copyResetTimer = setTimeout(function () {
1547
+ self._copyResetTimer = null;
1548
+ if (self._copyBtn) self._copyBtn.textContent = self._t('copy');
1549
+ }, 1600);
1550
+ };
1551
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1552
+ navigator.clipboard.writeText(csv.text).then(function () { done(true); }, function () { done(self._fallbackCopy(csv.text)); });
1553
+ } else {
1554
+ done(this._fallbackCopy(csv.text));
1555
+ }
1556
+ return this;
1557
+ };
1558
+
1559
+ NeikiTable.prototype._fallbackCopy = function (text) {
1560
+ try {
1561
+ var area = document.createElement('textarea');
1562
+ area.value = text;
1563
+ area.setAttribute('readonly', '');
1564
+ area.style.position = 'fixed';
1565
+ area.style.left = '-9999px';
1566
+ document.body.appendChild(area);
1567
+ area.select();
1568
+ var ok = document.execCommand('copy');
1569
+ document.body.removeChild(area);
1570
+ return ok;
1571
+ } catch (err) {
1572
+ return false;
1573
+ }
1574
+ };
1575
+
1576
+ NeikiTable.prototype.setLoading = function (loading) {
1577
+ this._config.loading = !!loading;
1578
+ if (loading) this.setAttribute('loading', '');
1579
+ else this.removeAttribute('loading');
1580
+ if (this.isConnected) this._render();
1581
+ return this;
1582
+ };
1583
+
1584
+ NeikiTable.prototype.setDensity = function (density) {
1585
+ this._config.density = oneOf(density, VALID_DENSITIES, this._config.density);
1586
+ if (this.isConnected) this._render();
1587
+ return this;
1588
+ };
1589
+
1590
+ NeikiTable.prototype.exportJSON = function (filename) {
1591
+ var self = this;
1592
+ var rows = this._exportRows();
1593
+ var payload = rows.map(function (row) {
1594
+ var out = {};
1595
+ self._columns.forEach(function (col) { out[col.key] = getValue(row, col.key); });
1596
+ return out;
1597
+ });
1598
+ downloadBlob(JSON.stringify(payload, null, 2), 'application/json;charset=utf-8', filename || 'neiki-table-export.json');
1599
+ this._emit('export', { format: 'json', count: rows.length });
1600
+ return this;
1601
+ };
1602
+
1603
+ NeikiTable.prototype.refresh = function () {
1604
+ if (this.isConnected) this._render();
1605
+ return this;
1606
+ };
1607
+
1608
+ customElements.define('neiki-table', NeikiTable);
1609
+ })();