@cahyo-dimas/freeday 1.7.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/adapters/blazor/freeday-blazor.js +79 -0
  5. package/adapters/react/components/FdyAutocomplete.tsx +159 -0
  6. package/adapters/react/components/FdyCascade.tsx +230 -0
  7. package/adapters/react/components/FdyCfl.tsx +362 -0
  8. package/adapters/react/components/FdyChart.tsx +69 -0
  9. package/adapters/react/components/FdyCombo.tsx +166 -0
  10. package/adapters/react/components/FdyDateRange.tsx +66 -0
  11. package/adapters/react/components/FdyDatepicker.tsx +354 -0
  12. package/adapters/react/index.d.ts +37 -0
  13. package/adapters/react/index.js +9 -0
  14. package/adapters/react/tsconfig.json +14 -0
  15. package/adapters/react/useFreeday.js +44 -0
  16. package/adapters/react/usePopover.ts +64 -0
  17. package/adapters/vue/components/FdyAutocomplete.vue +174 -0
  18. package/adapters/vue/components/FdyCascade.vue +241 -0
  19. package/adapters/vue/components/FdyCfl.vue +354 -0
  20. package/adapters/vue/components/FdyChart.vue +89 -0
  21. package/adapters/vue/components/FdyCombo.vue +232 -0
  22. package/adapters/vue/components/FdyDateRange.vue +78 -0
  23. package/adapters/vue/components/FdyDatepicker.vue +333 -0
  24. package/adapters/vue/index.d.ts +44 -0
  25. package/adapters/vue/index.js +8 -0
  26. package/adapters/vue/useFreeday.js +44 -0
  27. package/adapters/vue/usePopover.ts +78 -0
  28. package/dist/freeday-autocomplete.js +135 -0
  29. package/dist/freeday-breakpoint.js +51 -0
  30. package/dist/freeday-carousel.js +111 -0
  31. package/dist/freeday-cascade.js +256 -0
  32. package/dist/freeday-cfl.js +213 -0
  33. package/dist/freeday-chart.js +403 -0
  34. package/dist/freeday-chip.js +83 -0
  35. package/dist/freeday-datepicker.js +321 -0
  36. package/dist/freeday-datetime.js +59 -0
  37. package/dist/freeday-drawer.js +43 -0
  38. package/dist/freeday-form.js +181 -0
  39. package/dist/freeday-mask.js +114 -0
  40. package/dist/freeday-menu.js +93 -0
  41. package/dist/freeday-popover.js +69 -0
  42. package/dist/freeday-rating.js +50 -0
  43. package/dist/freeday-select.js +194 -0
  44. package/dist/freeday-slider.js +34 -0
  45. package/dist/freeday-stepper.js +90 -0
  46. package/dist/freeday-table.js +475 -0
  47. package/dist/freeday-tabs.js +68 -0
  48. package/dist/freeday-timepicker.js +180 -0
  49. package/dist/freeday-toast.js +84 -0
  50. package/dist/freeday-tree.js +94 -0
  51. package/dist/freeday-upload.js +206 -0
  52. package/dist/freeday.bundle.css +1352 -0
  53. package/dist/freeday.css +1007 -0
  54. package/dist/freeday.js +3632 -0
  55. package/dist/freeday.tokens.css +343 -0
  56. package/package.json +72 -0
  57. package/src/base.css +20 -0
  58. package/src/components/.gitkeep +0 -0
  59. package/src/components/accordion.css +13 -0
  60. package/src/components/alert.css +18 -0
  61. package/src/components/app-shell.css +88 -0
  62. package/src/components/appbar.css +15 -0
  63. package/src/components/autocomplete.css +12 -0
  64. package/src/components/avatar.css +8 -0
  65. package/src/components/badge.css +27 -0
  66. package/src/components/breadcrumb.css +9 -0
  67. package/src/components/breakpoints.css +15 -0
  68. package/src/components/button.css +58 -0
  69. package/src/components/card.css +9 -0
  70. package/src/components/carousel.css +17 -0
  71. package/src/components/cascade.css +36 -0
  72. package/src/components/cfl.css +28 -0
  73. package/src/components/chart.css +55 -0
  74. package/src/components/chip.css +18 -0
  75. package/src/components/combo.css +27 -0
  76. package/src/components/datepicker.css +44 -0
  77. package/src/components/datetimepicker.css +8 -0
  78. package/src/components/description-list.css +6 -0
  79. package/src/components/divider.css +3 -0
  80. package/src/components/drawer.css +25 -0
  81. package/src/components/file-upload.css +34 -0
  82. package/src/components/filterbar.css +25 -0
  83. package/src/components/form-grid.css +8 -0
  84. package/src/components/input-group.css +22 -0
  85. package/src/components/input.css +13 -0
  86. package/src/components/kbd.css +2 -0
  87. package/src/components/menu.css +28 -0
  88. package/src/components/modal.css +22 -0
  89. package/src/components/pagination.css +8 -0
  90. package/src/components/progress.css +7 -0
  91. package/src/components/rating.css +10 -0
  92. package/src/components/selection.css +25 -0
  93. package/src/components/skeleton.css +7 -0
  94. package/src/components/slider.css +12 -0
  95. package/src/components/spinner.css +6 -0
  96. package/src/components/states.css +8 -0
  97. package/src/components/stepper.css +21 -0
  98. package/src/components/table.css +55 -0
  99. package/src/components/tabs.css +10 -0
  100. package/src/components/timeline.css +12 -0
  101. package/src/components/timepicker.css +22 -0
  102. package/src/components/toast.css +17 -0
  103. package/src/components/tooltip.css +6 -0
  104. package/src/components/tree.css +21 -0
  105. package/src/freeday-autocomplete.js +135 -0
  106. package/src/freeday-breakpoint.js +51 -0
  107. package/src/freeday-carousel.js +111 -0
  108. package/src/freeday-cascade.js +256 -0
  109. package/src/freeday-cfl.js +213 -0
  110. package/src/freeday-chart.js +403 -0
  111. package/src/freeday-chip.js +83 -0
  112. package/src/freeday-datepicker.js +321 -0
  113. package/src/freeday-datetime.js +59 -0
  114. package/src/freeday-drawer.js +43 -0
  115. package/src/freeday-form.js +181 -0
  116. package/src/freeday-mask.js +114 -0
  117. package/src/freeday-menu.js +93 -0
  118. package/src/freeday-popover.js +69 -0
  119. package/src/freeday-rating.js +50 -0
  120. package/src/freeday-select.js +194 -0
  121. package/src/freeday-slider.js +34 -0
  122. package/src/freeday-stepper.js +90 -0
  123. package/src/freeday-table.js +475 -0
  124. package/src/freeday-tabs.js +68 -0
  125. package/src/freeday-timepicker.js +180 -0
  126. package/src/freeday-toast.js +84 -0
  127. package/src/freeday-tree.js +94 -0
  128. package/src/freeday-upload.js +206 -0
  129. package/tokens/breakpoints.mjs +4 -0
  130. package/tokens/tokens.json +123 -0
@@ -0,0 +1,475 @@
1
+ /* Freeday — data-table enhancer (optional, zero-dependency).
2
+ * Adds client-side search, column sort, pagination and row selection to any
3
+ * [data-fdy-table]. Progressive enhancement over a plain semantic <table>;
4
+ * in a framework app, do this server- or store-side instead.
5
+ *
6
+ * Markup contract (see docs): a `.fdy-datatable[data-fdy-table]` containing a
7
+ * `.fdy-table-toolbar` (with `[data-fdy-table-search]`, `[data-fdy-table-count]`),
8
+ * a `<table class="fdy-table">` whose sortable headers hold
9
+ * `<button class="fdy-table__sortbtn" data-fdy-sort[="number"]>`, optional
10
+ * selection checkboxes (`[data-fdy-select-all]`, `[data-fdy-row-select]`), and a
11
+ * `.fdy-table-footer` (with `[data-fdy-table-info]`, `[data-fdy-table-pagination]`).
12
+ * Numeric columns should give each cell a `data-sort-value`.
13
+ *
14
+ * Column filters: mark a header `<th data-fdy-filter="text|enum|number">` and a funnel
15
+ * button is injected; text filters by contains, enum by a value checklist, number by a
16
+ * min/max range. All active column filters AND the global search must pass (logical AND).
17
+ *
18
+ * Bulk actions: an optional `[data-fdy-table-bulk]` bar (hidden by default) with a
19
+ * `[data-fdy-table-bulk-count]` label and a `[data-fdy-table-bulk-clear]` button shows
20
+ * while any row is selected.
21
+ *
22
+ * Emits a bubbling "fdy-table-change" CustomEvent (detail: {shown, total, page}).
23
+ */
24
+ (function () {
25
+ 'use strict';
26
+
27
+ function cellText(row, index) {
28
+ var cell = row.cells[index];
29
+ if (!cell) return '';
30
+ return (cell.getAttribute('data-sort-value') != null
31
+ ? cell.getAttribute('data-sort-value')
32
+ : cell.textContent).trim();
33
+ }
34
+
35
+ function initTable(root) {
36
+ if (root.dataset.fdyTableReady === '1') return;
37
+ var table = root.querySelector('table.fdy-table');
38
+ var tbody = table && table.querySelector('tbody');
39
+ if (!table || !tbody) return;
40
+ root.dataset.fdyTableReady = '1';
41
+
42
+ var allRows = Array.prototype.slice.call(tbody.querySelectorAll(':scope > tr'));
43
+ var searchInput = root.querySelector('[data-fdy-table-search]');
44
+ var countEl = root.querySelector('[data-fdy-table-count]');
45
+ var infoEl = root.querySelector('[data-fdy-table-info]');
46
+ var pagerEl = root.querySelector('[data-fdy-table-pagination]');
47
+ var selectAll = root.querySelector('[data-fdy-select-all]');
48
+ var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
49
+
50
+ var sortButtons = Array.prototype.slice.call(root.querySelectorAll('.fdy-table__sortbtn[data-fdy-sort]'));
51
+ var sortCol = -1; // column index currently sorted
52
+ var sortDir = 0; // 1 asc, -1 desc
53
+ var sortType = 'text';
54
+ var filter = '';
55
+ var page = 1;
56
+
57
+ var colFilters = {}; // colIndex -> {type, text, set, min, max}
58
+ var openPopover = null;
59
+ var bulkBar = root.querySelector('[data-fdy-table-bulk]');
60
+ var bulkCount = root.querySelector('[data-fdy-table-bulk-count]');
61
+ var bulkClear = root.querySelector('[data-fdy-table-bulk-clear]');
62
+
63
+ function colIndexOf(button) {
64
+ var th = button.closest('th');
65
+ return th ? th.cellIndex : -1;
66
+ }
67
+
68
+ function parseNum(s) {
69
+ var n = parseFloat(String(s).replace(/[^\d.-]/g, ''));
70
+ return isNaN(n) ? null : n;
71
+ }
72
+
73
+ function passesColumnFilters(row) {
74
+ for (var idx in colFilters) {
75
+ if (!Object.prototype.hasOwnProperty.call(colFilters, idx)) continue;
76
+ var f = colFilters[idx];
77
+ var raw = cellText(row, Number(idx));
78
+ if (f.type === 'text') {
79
+ if (f.text && raw.toLowerCase().indexOf(f.text.toLowerCase()) === -1) return false;
80
+ } else if (f.type === 'enum') {
81
+ if (f.set && f.set.size && !f.set.has(raw)) return false;
82
+ } else if (f.type === 'number') {
83
+ var v = parseNum(raw);
84
+ if (f.min != null && (v == null || v < f.min)) return false;
85
+ if (f.max != null && (v == null || v > f.max)) return false;
86
+ }
87
+ }
88
+ return true;
89
+ }
90
+
91
+ function filtered() {
92
+ var q = filter.toLowerCase();
93
+ return allRows.filter(function (row) {
94
+ if (q && row.textContent.toLowerCase().indexOf(q) === -1) return false;
95
+ return passesColumnFilters(row);
96
+ });
97
+ }
98
+
99
+ function sorted(rows) {
100
+ if (sortCol < 0 || sortDir === 0) return rows;
101
+ var copy = rows.slice();
102
+ copy.sort(function (a, b) {
103
+ var av = cellText(a, sortCol);
104
+ var bv = cellText(b, sortCol);
105
+ var cmp;
106
+ if (sortType === 'number') {
107
+ cmp = (parseFloat(av.replace(/[^\d.-]/g, '')) || 0) - (parseFloat(bv.replace(/[^\d.-]/g, '')) || 0);
108
+ } else {
109
+ cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
110
+ }
111
+ return cmp * sortDir;
112
+ });
113
+ return copy;
114
+ }
115
+
116
+ function syncSelectAll(visibleRows) {
117
+ if (!selectAll) return;
118
+ var boxes = visibleRows
119
+ .map(function (r) { return r.querySelector('[data-fdy-row-select]'); })
120
+ .filter(Boolean);
121
+ var checked = boxes.filter(function (b) { return b.checked; }).length;
122
+ selectAll.checked = boxes.length > 0 && checked === boxes.length;
123
+ selectAll.indeterminate = checked > 0 && checked < boxes.length;
124
+ }
125
+
126
+ function buildPager(totalPages) {
127
+ if (!pagerEl) return;
128
+ pagerEl.innerHTML = '';
129
+ if (totalPages <= 1) return;
130
+ var ul = document.createElement('ul');
131
+ ul.className = 'fdy-pagination__list';
132
+
133
+ function addBtn(label, targetPage, opts) {
134
+ opts = opts || {};
135
+ var li = document.createElement('li');
136
+ var el = document.createElement(opts.disabled || opts.current ? 'span' : 'button');
137
+ el.className = 'fdy-pagination__link';
138
+ el.textContent = label;
139
+ if (opts.disabled) el.setAttribute('aria-disabled', 'true');
140
+ if (opts.current) el.setAttribute('aria-current', 'page');
141
+ if (opts.label) el.setAttribute('aria-label', opts.label);
142
+ if (!opts.disabled && !opts.current) {
143
+ el.type = 'button';
144
+ el.addEventListener('click', function () { page = targetPage; render(); });
145
+ }
146
+ li.appendChild(el);
147
+ ul.appendChild(li);
148
+ }
149
+ function addEllipsis() {
150
+ var li = document.createElement('li');
151
+ var sp = document.createElement('span');
152
+ sp.className = 'fdy-pagination__ellipsis';
153
+ sp.textContent = '…';
154
+ li.appendChild(sp);
155
+ ul.appendChild(li);
156
+ }
157
+
158
+ addBtn('‹', page - 1, { disabled: page === 1, label: 'Sebelumnya' });
159
+ var pages = [];
160
+ for (var p = 1; p <= totalPages; p++) {
161
+ if (p === 1 || p === totalPages || (p >= page - 1 && p <= page + 1)) pages.push(p);
162
+ else if (pages[pages.length - 1] !== '…') pages.push('…');
163
+ }
164
+ pages.forEach(function (p) {
165
+ if (p === '…') addEllipsis();
166
+ else addBtn(String(p), p, { current: p === page });
167
+ });
168
+ addBtn('›', page + 1, { disabled: page === totalPages, label: 'Berikutnya' });
169
+ pagerEl.appendChild(ul);
170
+ }
171
+
172
+ // --- Bulk-action bar ---------------------------------------------------
173
+ function selectedRows() {
174
+ return allRows.filter(function (r) {
175
+ var b = r.querySelector('[data-fdy-row-select]');
176
+ return b && b.checked;
177
+ });
178
+ }
179
+ function updateBulk() {
180
+ if (!bulkBar) return;
181
+ var n = selectedRows().length;
182
+ bulkBar.hidden = n === 0;
183
+ if (bulkCount) bulkCount.textContent = n + ' dipilih';
184
+ }
185
+ function clearSelection() {
186
+ allRows.forEach(function (r) {
187
+ var b = r.querySelector('[data-fdy-row-select]');
188
+ if (b) b.checked = false;
189
+ r.setAttribute('aria-selected', 'false');
190
+ });
191
+ if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
192
+ updateBulk();
193
+ }
194
+
195
+ // --- Column filters ----------------------------------------------------
196
+ function closePopover(returnFocus) {
197
+ if (!openPopover) return;
198
+ var btn = openPopover._trigger;
199
+ openPopover.remove();
200
+ openPopover = null;
201
+ window.removeEventListener('scroll', closeOnScroll, true);
202
+ window.removeEventListener('resize', closeOnScroll);
203
+ if (returnFocus === true && btn) btn.focus(); // only on keyboard/button dismissal
204
+ }
205
+ function closeOnScroll() { closePopover(false); }
206
+ function markActive(button, active) {
207
+ button.classList.toggle('is-active', active);
208
+ button.setAttribute('aria-pressed', String(active));
209
+ }
210
+ function filterTitle(text) {
211
+ var t = document.createElement('div');
212
+ t.className = 'fdy-filter__title';
213
+ t.textContent = text;
214
+ return t;
215
+ }
216
+ function numberInput(placeholder, value) {
217
+ var i = document.createElement('input');
218
+ i.className = 'fdy-input';
219
+ i.type = 'text';
220
+ i.inputMode = 'numeric';
221
+ i.placeholder = placeholder;
222
+ i.value = value != null ? value : '';
223
+ return i;
224
+ }
225
+ function distinctValues(idx) {
226
+ var seen = {};
227
+ var out = [];
228
+ allRows.forEach(function (r) {
229
+ var v = cellText(r, idx);
230
+ if (v && !seen[v]) { seen[v] = 1; out.push(v); }
231
+ });
232
+ out.sort(function (a, b) { return a.localeCompare(b, undefined, { numeric: true }); });
233
+ return out;
234
+ }
235
+ function openFilterPopover(type, idx, btn) {
236
+ var pop = document.createElement('div');
237
+ pop.className = 'fdy-filter';
238
+ pop.setAttribute('role', 'dialog');
239
+ pop.setAttribute('aria-label', 'Filter kolom');
240
+ var f = colFilters[idx] || { type: type };
241
+
242
+ if (type === 'text') {
243
+ pop.appendChild(filterTitle('Berisi teks'));
244
+ var inp = document.createElement('input');
245
+ inp.className = 'fdy-input';
246
+ inp.type = 'search';
247
+ inp.placeholder = 'Berisi…';
248
+ inp.value = f.text || '';
249
+ inp.addEventListener('input', function () {
250
+ f.text = inp.value.trim();
251
+ colFilters[idx] = f;
252
+ markActive(btn, !!f.text);
253
+ page = 1; render();
254
+ });
255
+ pop.appendChild(inp);
256
+ } else if (type === 'enum') {
257
+ pop.appendChild(filterTitle('Tampilkan nilai'));
258
+ var list = document.createElement('div');
259
+ list.className = 'fdy-filter__list';
260
+ var set = f.set || new Set();
261
+ distinctValues(idx).forEach(function (val) {
262
+ var lab = document.createElement('label');
263
+ lab.className = 'fdy-filter__check';
264
+ var cb = document.createElement('input');
265
+ cb.type = 'checkbox';
266
+ cb.className = 'fdy-checkbox';
267
+ cb.checked = set.has(val);
268
+ cb.addEventListener('change', function () {
269
+ if (cb.checked) set.add(val); else set.delete(val);
270
+ f.set = set; colFilters[idx] = f;
271
+ markActive(btn, set.size > 0);
272
+ page = 1; render();
273
+ });
274
+ lab.appendChild(cb);
275
+ lab.appendChild(document.createTextNode(val));
276
+ list.appendChild(lab);
277
+ });
278
+ pop.appendChild(list);
279
+ } else if (type === 'number') {
280
+ pop.appendChild(filterTitle('Rentang nilai'));
281
+ var range = document.createElement('div');
282
+ range.className = 'fdy-filter__range';
283
+ var minI = numberInput('Min', f.min);
284
+ var maxI = numberInput('Maks', f.max);
285
+ var applyRange = function () {
286
+ f.min = minI.value !== '' ? parseNum(minI.value) : null;
287
+ f.max = maxI.value !== '' ? parseNum(maxI.value) : null;
288
+ colFilters[idx] = f;
289
+ markActive(btn, f.min != null || f.max != null);
290
+ page = 1; render();
291
+ };
292
+ minI.addEventListener('input', applyRange);
293
+ maxI.addEventListener('input', applyRange);
294
+ var sep = document.createElement('span');
295
+ sep.textContent = '–';
296
+ range.appendChild(minI); range.appendChild(sep); range.appendChild(maxI);
297
+ pop.appendChild(range);
298
+ }
299
+
300
+ var foot = document.createElement('div');
301
+ foot.className = 'fdy-filter__foot';
302
+ var reset = document.createElement('button');
303
+ reset.type = 'button';
304
+ reset.className = 'fdy-btn fdy-btn--ghost fdy-btn--sm';
305
+ reset.textContent = 'Reset';
306
+ reset.addEventListener('click', function () {
307
+ delete colFilters[idx];
308
+ markActive(btn, false);
309
+ page = 1; render();
310
+ closePopover(true);
311
+ });
312
+ var done = document.createElement('button');
313
+ done.type = 'button';
314
+ done.className = 'fdy-btn fdy-btn--sm';
315
+ done.textContent = 'Tutup';
316
+ done.addEventListener('click', function () { closePopover(true); });
317
+ foot.appendChild(reset); foot.appendChild(done);
318
+ pop.appendChild(foot);
319
+
320
+ document.body.appendChild(pop);
321
+ pop._trigger = btn;
322
+ var rect = btn.getBoundingClientRect();
323
+ pop.style.top = (rect.bottom + 4) + 'px';
324
+ pop.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - pop.offsetWidth - 8)) + 'px';
325
+ openPopover = pop;
326
+ window.addEventListener('scroll', closeOnScroll, true);
327
+ window.addEventListener('resize', closeOnScroll);
328
+ var firstControl = pop.querySelector('input, button');
329
+ if (firstControl) firstControl.focus();
330
+ }
331
+ function buildColumnFilters() {
332
+ Array.prototype.forEach.call(table.querySelectorAll('thead th[data-fdy-filter]'), function (th) {
333
+ var type = th.getAttribute('data-fdy-filter');
334
+ var idx = th.cellIndex;
335
+ var btn = document.createElement('button');
336
+ btn.type = 'button';
337
+ btn.className = 'fdy-table__filterbtn';
338
+ btn.setAttribute('aria-haspopup', 'dialog');
339
+ btn.setAttribute('aria-pressed', 'false');
340
+ btn.setAttribute('aria-label', 'Filter kolom');
341
+ btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18l-7 8v5l-4 2v-7z"></path></svg>';
342
+ btn.addEventListener('click', function (e) {
343
+ e.stopPropagation();
344
+ var wasOpen = openPopover && openPopover._trigger === btn;
345
+ closePopover();
346
+ if (!wasOpen) openFilterPopover(type, idx, btn);
347
+ });
348
+ th.appendChild(btn);
349
+ });
350
+ }
351
+
352
+ function render() {
353
+ var view = sorted(filtered());
354
+ var total = view.length;
355
+ var totalPages = pageSize ? Math.max(1, Math.ceil(total / pageSize)) : 1;
356
+ if (page > totalPages) page = totalPages;
357
+ if (page < 1) page = 1;
358
+
359
+ var start = pageSize ? (page - 1) * pageSize : 0;
360
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
361
+
362
+ // Hide every row, then append the visible slice in order (reorders DOM).
363
+ allRows.forEach(function (row) { row.style.display = 'none'; });
364
+ slice.forEach(function (row) {
365
+ row.style.display = '';
366
+ tbody.appendChild(row);
367
+ });
368
+
369
+ var shownFrom = total === 0 ? 0 : start + 1;
370
+ var shownTo = start + slice.length;
371
+ if (countEl) countEl.textContent = total + ' baris';
372
+ if (infoEl) infoEl.textContent = 'Menampilkan ' + shownFrom + '–' + shownTo + ' dari ' + total;
373
+ buildPager(totalPages);
374
+ syncSelectAll(slice);
375
+ updateBulk();
376
+
377
+ root.dispatchEvent(new CustomEvent('fdy-table-change', {
378
+ bubbles: true,
379
+ detail: { shown: slice.length, total: total, page: page }
380
+ }));
381
+ }
382
+
383
+ sortButtons.forEach(function (button) {
384
+ button.addEventListener('click', function () {
385
+ var index = colIndexOf(button);
386
+ if (index < 0) return;
387
+ var th = button.closest('th');
388
+ if (sortCol === index) {
389
+ sortDir = sortDir === 1 ? -1 : 1;
390
+ } else {
391
+ sortCol = index;
392
+ sortDir = 1;
393
+ sortType = button.getAttribute('data-fdy-sort') === 'number' ? 'number' : 'text';
394
+ }
395
+ // Reset all header sort indicators, set the active one.
396
+ sortButtons.forEach(function (b) {
397
+ var h = b.closest('th');
398
+ if (h) h.removeAttribute('aria-sort');
399
+ });
400
+ if (th) th.setAttribute('aria-sort', sortDir === 1 ? 'ascending' : 'descending');
401
+ page = 1;
402
+ render();
403
+ });
404
+ });
405
+
406
+ if (searchInput) {
407
+ searchInput.addEventListener('input', function () {
408
+ filter = searchInput.value.trim();
409
+ page = 1;
410
+ render();
411
+ });
412
+ }
413
+
414
+ if (selectAll) {
415
+ selectAll.addEventListener('change', function () {
416
+ var start = pageSize ? (page - 1) * pageSize : 0;
417
+ var view = sorted(filtered());
418
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
419
+ slice.forEach(function (row) {
420
+ var box = row.querySelector('[data-fdy-row-select]');
421
+ if (box) {
422
+ box.checked = selectAll.checked;
423
+ row.setAttribute('aria-selected', selectAll.checked ? 'true' : 'false');
424
+ }
425
+ });
426
+ selectAll.indeterminate = false;
427
+ updateBulk();
428
+ });
429
+ }
430
+
431
+ tbody.addEventListener('change', function (e) {
432
+ var box = e.target.closest('[data-fdy-row-select]');
433
+ if (!box) return;
434
+ var row = box.closest('tr');
435
+ if (row) row.setAttribute('aria-selected', box.checked ? 'true' : 'false');
436
+ var view = sorted(filtered());
437
+ var start = pageSize ? (page - 1) * pageSize : 0;
438
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
439
+ syncSelectAll(slice);
440
+ updateBulk();
441
+ });
442
+
443
+ if (bulkClear) bulkClear.addEventListener('click', clearSelection);
444
+
445
+ // Dismiss an open filter popover on outside click or Escape.
446
+ document.addEventListener('click', function (e) {
447
+ if (!openPopover) return;
448
+ var t = openPopover._trigger;
449
+ if (!openPopover.contains(e.target) && e.target !== t && (!t || !t.contains(e.target))) {
450
+ closePopover();
451
+ }
452
+ });
453
+ document.addEventListener('keydown', function (e) {
454
+ if (e.key === 'Escape' && openPopover) closePopover(true);
455
+ });
456
+
457
+ buildColumnFilters();
458
+ render();
459
+ }
460
+
461
+ function initAll(context) {
462
+ Array.prototype.forEach.call(
463
+ (context || document).querySelectorAll('[data-fdy-table]'),
464
+ initTable
465
+ );
466
+ }
467
+
468
+ if (document.readyState === 'loading') {
469
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
470
+ } else {
471
+ initAll();
472
+ }
473
+
474
+ window.FreedayTable = { init: initTable, initAll: initAll };
475
+ })();
@@ -0,0 +1,68 @@
1
+ /* Freeday — tabs enhancer (optional, zero-dependency, WAI-ARIA APG tabs).
2
+ * Auto-inits every [data-fdy-tabs]. Roving tabindex + arrow/Home/End nav.
3
+ *
4
+ * Markup: a [data-fdy-tabs] with a [role="tablist"] of
5
+ * <button role="tab" aria-controls="PANEL" aria-selected> and matching
6
+ * <div role="tabpanel" id="PANEL" aria-labelledby="TAB" [hidden]>.
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ function initTabs(root) {
12
+ if (root.dataset.fdyTabsReady === '1') return;
13
+ var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
14
+ if (tabs.length === 0) return;
15
+ root.dataset.fdyTabsReady = '1';
16
+
17
+ function panelFor(tab) {
18
+ var id = tab.getAttribute('aria-controls');
19
+ return id ? document.getElementById(id) : null;
20
+ }
21
+ function select(tab, focus) {
22
+ tabs.forEach(function (t) {
23
+ var on = t === tab;
24
+ t.setAttribute('aria-selected', on ? 'true' : 'false');
25
+ t.tabIndex = on ? 0 : -1;
26
+ var panel = panelFor(t);
27
+ if (panel) panel.hidden = !on;
28
+ });
29
+ if (focus) tab.focus();
30
+ }
31
+
32
+ tabs.forEach(function (tab, i) {
33
+ tab.addEventListener('click', function () { select(tab, false); });
34
+ tab.addEventListener('keydown', function (e) {
35
+ switch (e.key) {
36
+ case 'ArrowRight':
37
+ e.preventDefault(); select(tabs[(i + 1) % tabs.length], true); break;
38
+ case 'ArrowLeft':
39
+ e.preventDefault(); select(tabs[(i - 1 + tabs.length) % tabs.length], true); break;
40
+ case 'Home':
41
+ e.preventDefault(); select(tabs[0], true); break;
42
+ case 'End':
43
+ e.preventDefault(); select(tabs[tabs.length - 1], true); break;
44
+ }
45
+ });
46
+ });
47
+
48
+ var current = tabs.filter(function (t) {
49
+ return t.getAttribute('aria-selected') === 'true';
50
+ })[0] || tabs[0];
51
+ select(current, false);
52
+ }
53
+
54
+ function initAll(context) {
55
+ Array.prototype.forEach.call(
56
+ (context || document).querySelectorAll('[data-fdy-tabs]'),
57
+ initTabs
58
+ );
59
+ }
60
+
61
+ if (document.readyState === 'loading') {
62
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
63
+ } else {
64
+ initAll();
65
+ }
66
+
67
+ window.FreedayTabs = { init: initTabs, initAll: initAll };
68
+ })();