@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,3632 @@
1
+ /* Freeday enhancers — GENERATED by tokens/build.mjs. Do not edit by hand.
2
+ * Concatenation of every src/*.js enhancer (each a self-contained IIFE). */
3
+ /* Freeday — autocomplete enhancer (optional, zero-dependency).
4
+ * Editable combobox (WAI-ARIA APG): a text input filters a role="listbox" of role="option"
5
+ * items as you type; select fills the input. Auto-inits [data-fdy-autocomplete]. Options are
6
+ * static markup filtered client-side — in an app, fetch/filter server-side and re-render options.
7
+ *
8
+ * Markup: <div data-fdy-autocomplete class="fdy-autocomplete">
9
+ * <input class="fdy-input" role="combobox" aria-expanded="false" aria-autocomplete="list"
10
+ * aria-controls="listId" autocomplete="off">
11
+ * <ul class="fdy-autocomplete__listbox" id="listId" role="listbox" hidden>
12
+ * <li class="fdy-autocomplete__option" role="option">…</li>…
13
+ * <li class="fdy-autocomplete__empty" hidden>Tak ada hasil</li></ul></div>
14
+ * Emits a bubbling "fdy-autocomplete-select" CustomEvent (detail {value, option}).
15
+ */
16
+ (function () {
17
+ 'use strict';
18
+
19
+ var seq = 0;
20
+
21
+ function initAutocomplete(root) {
22
+ if (root.dataset.fdyAcReady === '1') return;
23
+ root.dataset.fdyAcReady = '1';
24
+
25
+ var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
26
+ var listbox = root.querySelector('[role="listbox"]');
27
+ if (!input || !listbox) return;
28
+ var options = Array.prototype.slice.call(listbox.querySelectorAll('[role="option"]'));
29
+ var emptyEl = listbox.querySelector('.fdy-autocomplete__empty');
30
+ var active = -1;
31
+
32
+ options.forEach(function (o) {
33
+ if (!o.id) { seq += 1; o.id = 'fdy-ac-opt-' + seq; }
34
+ o.dataset.label = o.textContent.trim();
35
+ });
36
+ listbox.hidden = true;
37
+ input.setAttribute('aria-expanded', 'false');
38
+
39
+ function visible() { return options.filter(function (o) { return !o.hidden; }); }
40
+ function clearHighlight() { options.forEach(function (o) { o.classList.remove('is-highlighted'); }); }
41
+
42
+ var _pop = null;
43
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, input); return _pop; }
44
+ function open() {
45
+ if (!listbox.hidden) return;
46
+ var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
47
+ input.setAttribute('aria-expanded', 'true');
48
+ document.addEventListener('click', onDocClick, true);
49
+ }
50
+ function close() {
51
+ if (listbox.hidden) return;
52
+ var p = popCtl(); if (p) p.hide(); else listbox.hidden = true;
53
+ input.setAttribute('aria-expanded', 'false');
54
+ input.removeAttribute('aria-activedescendant');
55
+ active = -1;
56
+ clearHighlight();
57
+ document.removeEventListener('click', onDocClick, true);
58
+ }
59
+ function onDocClick(e) { if (!root.contains(e.target)) close(); }
60
+
61
+ function highlight(i) {
62
+ var vis = visible();
63
+ clearHighlight();
64
+ if (!vis.length || i < 0 || i >= vis.length) { active = -1; input.removeAttribute('aria-activedescendant'); return; }
65
+ active = i;
66
+ vis[i].classList.add('is-highlighted');
67
+ input.setAttribute('aria-activedescendant', vis[i].id);
68
+ vis[i].scrollIntoView({ block: 'nearest' });
69
+ }
70
+
71
+ function applyFilter() {
72
+ var q = input.value.trim().toLowerCase();
73
+ var shown = 0;
74
+ options.forEach(function (o) {
75
+ var match = !q || o.dataset.label.toLowerCase().indexOf(q) !== -1;
76
+ o.hidden = !match;
77
+ if (match) shown++;
78
+ });
79
+ if (emptyEl) emptyEl.hidden = shown !== 0;
80
+ active = -1;
81
+ input.removeAttribute('aria-activedescendant');
82
+ clearHighlight();
83
+ open();
84
+ }
85
+
86
+ function choose(opt) {
87
+ input.value = opt.dataset.label;
88
+ options.forEach(function (o) { o.setAttribute('aria-selected', o === opt ? 'true' : 'false'); });
89
+ close();
90
+ root.dispatchEvent(new CustomEvent('fdy-autocomplete-select', { bubbles: true, detail: { value: opt.dataset.label, option: opt } }));
91
+ }
92
+
93
+ input.addEventListener('input', applyFilter);
94
+ input.addEventListener('focus', function () { applyFilter(); });
95
+ input.addEventListener('keydown', function (e) {
96
+ var vis = visible();
97
+ switch (e.key) {
98
+ case 'ArrowDown':
99
+ e.preventDefault();
100
+ if (listbox.hidden) open();
101
+ highlight(active + 1 < vis.length ? active + 1 : 0);
102
+ break;
103
+ case 'ArrowUp':
104
+ e.preventDefault();
105
+ if (listbox.hidden) open();
106
+ highlight(active - 1 >= 0 ? active - 1 : vis.length - 1);
107
+ break;
108
+ case 'Enter':
109
+ if (!listbox.hidden && active >= 0) { e.preventDefault(); choose(vis[active]); }
110
+ break;
111
+ case 'Escape':
112
+ if (!listbox.hidden) { e.preventDefault(); close(); }
113
+ break;
114
+ case 'Tab':
115
+ close();
116
+ break;
117
+ default: break;
118
+ }
119
+ });
120
+ listbox.addEventListener('click', function (e) {
121
+ var opt = e.target.closest('[role="option"]');
122
+ if (opt && !opt.hidden) choose(opt);
123
+ });
124
+ }
125
+
126
+ function initAll(context) {
127
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
128
+ }
129
+
130
+ if (document.readyState === 'loading') {
131
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
132
+ } else {
133
+ initAll();
134
+ }
135
+
136
+ window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll };
137
+ })();
138
+
139
+ /* Freeday — breakpoint provider (optional, zero-dependency).
140
+ * The MudBreakpointProvider equivalent for JS consumers: stamps the current breakpoint on
141
+ * <html data-breakpoint="xs|sm|md|lg|xl"> and emits a bubbling "fdy-breakpoint-change"
142
+ * CustomEvent (detail {breakpoint}) whenever it changes. Also fills any element with
143
+ * [data-fdy-breakpoint-label] with the current name. Read it live via FreedayBreakpoint.get().
144
+ * Scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920 (matches breakpoints.css).
145
+ */
146
+ (function () {
147
+ 'use strict';
148
+
149
+ var STEPS = [['xl', 1920], ['lg', 1280], ['md', 960], ['sm', 600], ['xs', 0]];
150
+
151
+ function current() {
152
+ var w = window.innerWidth;
153
+ for (var i = 0; i < STEPS.length; i++) {
154
+ if (w >= STEPS[i][1]) return STEPS[i][0];
155
+ }
156
+ return 'xs';
157
+ }
158
+
159
+ var root = document.documentElement;
160
+ var last = null;
161
+
162
+ function update() {
163
+ var bp = current();
164
+ if (bp === last) return;
165
+ last = bp;
166
+ root.setAttribute('data-breakpoint', bp);
167
+ Array.prototype.forEach.call(document.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
168
+ el.textContent = bp;
169
+ });
170
+ root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
171
+ }
172
+
173
+ window.addEventListener('resize', update, { passive: true });
174
+ if (document.readyState === 'loading') {
175
+ document.addEventListener('DOMContentLoaded', update);
176
+ } else {
177
+ update();
178
+ }
179
+
180
+ window.FreedayBreakpoint = {
181
+ get: function () { return last || current(); },
182
+ current: current,
183
+ // true if the current breakpoint is `name` or wider (e.g. is('md') → md, lg, or xl)
184
+ isUp: function (name) {
185
+ var order = ['xs', 'sm', 'md', 'lg', 'xl'];
186
+ return order.indexOf(this.get()) >= order.indexOf(name);
187
+ }
188
+ };
189
+ })();
190
+
191
+ /* Freeday — carousel enhancer (optional, zero-dependency).
192
+ * Scroll-snap slider with prev/next arrows, generated dot indicators, keyboard (←/→),
193
+ * and optional autoplay (data-fdy-autoplay="4000", pauses on hover/focus). Auto-inits
194
+ * [data-fdy-carousel].
195
+ *
196
+ * Markup: <div class="fdy-carousel" data-fdy-carousel role="region"
197
+ * aria-roledescription="carousel" aria-label="…" tabindex="0">
198
+ * <button class="fdy-carousel__arrow fdy-carousel__arrow--prev" aria-label="Sebelumnya">…</button>
199
+ * <div class="fdy-carousel__viewport">
200
+ * <div class="fdy-carousel__slide" role="group" aria-roledescription="slide">…</div>…
201
+ * </div>
202
+ * <button class="fdy-carousel__arrow fdy-carousel__arrow--next" aria-label="Berikutnya">…</button>
203
+ * <div class="fdy-carousel__dots"></div> <!-- dots generated here -->
204
+ * </div>
205
+ * Emits a bubbling "fdy-carousel-change" CustomEvent (detail {index}).
206
+ */
207
+ (function () {
208
+ 'use strict';
209
+
210
+ function initCarousel(root) {
211
+ if (root.dataset.fdyCarouselReady === '1') return;
212
+ root.dataset.fdyCarouselReady = '1';
213
+
214
+ var viewport = root.querySelector('.fdy-carousel__viewport');
215
+ var slides = Array.prototype.slice.call(root.querySelectorAll('.fdy-carousel__slide'));
216
+ if (!viewport || !slides.length) return;
217
+ var prev = root.querySelector('.fdy-carousel__arrow--prev');
218
+ var next = root.querySelector('.fdy-carousel__arrow--next');
219
+ var dotsWrap = root.querySelector('.fdy-carousel__dots');
220
+ var index = 0;
221
+
222
+ slides.forEach(function (s, i) {
223
+ if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', (i + 1) + ' dari ' + slides.length);
224
+ });
225
+
226
+ var dots = [];
227
+ if (dotsWrap) {
228
+ slides.forEach(function (s, i) {
229
+ var b = document.createElement('button');
230
+ b.type = 'button';
231
+ b.className = 'fdy-carousel__dot';
232
+ b.setAttribute('aria-label', 'Slide ' + (i + 1));
233
+ b.addEventListener('click', function () { goTo(i); });
234
+ dotsWrap.appendChild(b);
235
+ dots.push(b);
236
+ });
237
+ }
238
+
239
+ function clamp(i) { return Math.max(0, Math.min(slides.length - 1, i)); }
240
+
241
+ function sync() {
242
+ dots.forEach(function (d, i) { d.setAttribute('aria-current', i === index ? 'true' : 'false'); });
243
+ if (prev) prev.disabled = index === 0;
244
+ if (next) next.disabled = index === slides.length - 1;
245
+ }
246
+
247
+ function goTo(i) {
248
+ index = clamp(i);
249
+ viewport.scrollTo({ left: index * viewport.clientWidth, behavior: 'smooth' });
250
+ sync();
251
+ root.dispatchEvent(new CustomEvent('fdy-carousel-change', { bubbles: true, detail: { index: index } }));
252
+ }
253
+
254
+ if (prev) prev.addEventListener('click', function () { goTo(index - 1); });
255
+ if (next) next.addEventListener('click', function () { goTo(index + 1); });
256
+ root.addEventListener('keydown', function (e) {
257
+ if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(index - 1); }
258
+ else if (e.key === 'ArrowRight') { e.preventDefault(); goTo(index + 1); }
259
+ });
260
+
261
+ // Keep index in sync when the user swipes/scrolls the viewport directly.
262
+ var ticking = false;
263
+ viewport.addEventListener('scroll', function () {
264
+ if (ticking) return;
265
+ ticking = true;
266
+ requestAnimationFrame(function () {
267
+ ticking = false;
268
+ var w = viewport.clientWidth || 1;
269
+ var i = clamp(Math.round(viewport.scrollLeft / w));
270
+ if (i !== index) { index = i; sync(); }
271
+ });
272
+ }, { passive: true });
273
+
274
+ var autoplay = parseInt(root.getAttribute('data-fdy-autoplay'), 10);
275
+ if (autoplay > 0) {
276
+ var timer = null;
277
+ var start = function () { if (!timer) timer = setInterval(function () { goTo(index >= slides.length - 1 ? 0 : index + 1); }, autoplay); };
278
+ var stop = function () { if (timer) { clearInterval(timer); timer = null; } };
279
+ root.addEventListener('mouseenter', stop);
280
+ root.addEventListener('mouseleave', start);
281
+ root.addEventListener('focusin', stop);
282
+ root.addEventListener('focusout', start);
283
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
284
+ if (!reduce) start();
285
+ }
286
+
287
+ sync();
288
+ }
289
+
290
+ function initAll(context) {
291
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-carousel]'), initCarousel);
292
+ }
293
+
294
+ if (document.readyState === 'loading') {
295
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
296
+ } else {
297
+ initAll();
298
+ }
299
+
300
+ window.FreedayCarousel = { init: initCarousel, initAll: initAll };
301
+ })();
302
+
303
+ /* Freeday — cascade select enhancer (optional, zero-dependency).
304
+ * Turns a [data-fdy-cascade] wrapper into a hierarchical drill-down picker. The
305
+ * data model is a hidden nested <ul> inside the wrapper: a <li> with a child <ul>
306
+ * is a branch (drills in), a <li> without is a leaf (selects). Each <li> carries
307
+ * its value in data-value; its own text (before any nested <ul>) is the label.
308
+ *
309
+ * The popover shows one level at a time (a listbox); a branch drills in, the back
310
+ * control ascends, a leaf selects and stores the full path as the value.
311
+ * Attributes: data-label, data-placeholder, data-value (pre-select a leaf value),
312
+ * data-separator (path separator in the display, default " / ").
313
+ *
314
+ * Keyboard: trigger opens on click / Enter / Space / ArrowDown. In the list:
315
+ * Up/Down move, Home/End jump, ArrowRight/Enter/Space drill into a branch or
316
+ * select a leaf, ArrowLeft/Backspace go up a level, Esc closes (focus returns to
317
+ * the trigger). Emits a bubbling "fdy-cascade-change" (detail {value, path, labels}).
318
+ */
319
+ (function () {
320
+ 'use strict';
321
+
322
+ var seq = 0;
323
+ var BACK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"></path></svg>';
324
+
325
+ // Own text of an <li>, excluding any nested <ul>.
326
+ function ownLabel(li) {
327
+ var s = '';
328
+ Array.prototype.forEach.call(li.childNodes, function (n) {
329
+ if (n.nodeType === 3) s += n.textContent;
330
+ else if (n.nodeType === 1 && n.tagName !== 'UL') s += n.textContent;
331
+ });
332
+ return s.trim();
333
+ }
334
+ function parse(ul) {
335
+ var nodes = [];
336
+ Array.prototype.forEach.call(ul.children, function (li) {
337
+ if (li.tagName !== 'LI') return;
338
+ var childUl = li.querySelector(':scope > ul');
339
+ nodes.push({
340
+ label: ownLabel(li),
341
+ value: li.getAttribute('data-value') || ownLabel(li),
342
+ children: childUl ? parse(childUl) : null
343
+ });
344
+ });
345
+ return nodes;
346
+ }
347
+ // Depth-first search for the stack of labels leading to a leaf value.
348
+ function pathTo(nodes, value, trail) {
349
+ for (var i = 0; i < nodes.length; i++) {
350
+ var n = nodes[i];
351
+ var here = trail.concat([n]);
352
+ if (!n.children && n.value === value) return here;
353
+ if (n.children) { var found = pathTo(n.children, value, here); if (found) return found; }
354
+ }
355
+ return null;
356
+ }
357
+
358
+ function initCascade(wrap) {
359
+ if (wrap.dataset.fdyCascadeReady === '1') return;
360
+ wrap.dataset.fdyCascadeReady = '1';
361
+ wrap.classList.add('fdy-cascade');
362
+
363
+ var sourceUl = wrap.querySelector('ul');
364
+ var root = sourceUl ? parse(sourceUl) : [];
365
+ if (sourceUl) sourceUl.remove();
366
+
367
+ var label = wrap.getAttribute('data-label') || 'Pilih';
368
+ var placeholder = wrap.getAttribute('data-placeholder') || 'Pilih…';
369
+ var sep = wrap.getAttribute('data-separator') || ' / ';
370
+
371
+ var trigger = document.createElement('button');
372
+ trigger.type = 'button';
373
+ trigger.className = 'fdy-cascade__trigger';
374
+ trigger.setAttribute('aria-haspopup', 'true');
375
+ trigger.setAttribute('aria-expanded', 'false');
376
+ trigger.setAttribute('aria-label', label);
377
+ var valueSpan = document.createElement('span');
378
+ valueSpan.className = 'fdy-cascade__value';
379
+ trigger.appendChild(valueSpan);
380
+
381
+ var panel = document.createElement('div');
382
+ panel.className = 'fdy-cascade__panel';
383
+ panel.hidden = true;
384
+ var head = document.createElement('div');
385
+ head.className = 'fdy-cascade__head';
386
+ var back = document.createElement('button');
387
+ back.type = 'button';
388
+ back.className = 'fdy-cascade__back';
389
+ back.setAttribute('aria-label', 'Kembali satu tingkat');
390
+ back.innerHTML = BACK;
391
+ back.hidden = true;
392
+ var crumb = document.createElement('span');
393
+ crumb.className = 'fdy-cascade__crumb';
394
+ crumb.setAttribute('aria-live', 'polite');
395
+ head.appendChild(back);
396
+ head.appendChild(crumb);
397
+ var list = document.createElement('ul');
398
+ seq += 1;
399
+ list.className = 'fdy-cascade__list';
400
+ list.id = 'fdy-cascade-list-' + seq;
401
+ list.setAttribute('role', 'listbox');
402
+ list.setAttribute('aria-label', label);
403
+ list.tabIndex = -1;
404
+ panel.appendChild(head);
405
+ panel.appendChild(list);
406
+
407
+ wrap.appendChild(trigger);
408
+ wrap.appendChild(panel);
409
+
410
+ var stack = []; // branch nodes drilled through (context)
411
+ var current = root; // nodes at the visible level
412
+ var opts = []; // rendered <li> elements
413
+ var active = -1;
414
+ var selectedValue = wrap.getAttribute('data-value') || '';
415
+
416
+ function render() {
417
+ list.innerHTML = '';
418
+ opts = [];
419
+ back.hidden = stack.length === 0;
420
+ crumb.textContent = stack.length ? stack.map(function (n) { return n.label; }).join(sep) : label;
421
+ current.forEach(function (node, i) {
422
+ var li = document.createElement('li');
423
+ seq += 1; li.id = 'fdy-cascade-opt-' + seq;
424
+ li.className = 'fdy-cascade__opt';
425
+ li.setAttribute('role', 'option');
426
+ li.setAttribute('data-index', String(i));
427
+ var isBranch = !!node.children;
428
+ li.setAttribute('aria-selected', (!isBranch && node.value === selectedValue) ? 'true' : 'false');
429
+ if (isBranch) li.setAttribute('aria-label', node.label + ', submenu');
430
+ var lbl = document.createElement('span');
431
+ lbl.className = 'fdy-cascade__opt-label';
432
+ lbl.textContent = node.label;
433
+ li.appendChild(lbl);
434
+ if (isBranch) {
435
+ var arrow = document.createElement('span');
436
+ arrow.className = 'fdy-cascade__opt-arrow';
437
+ arrow.setAttribute('aria-hidden', 'true');
438
+ li.appendChild(arrow);
439
+ }
440
+ list.appendChild(li);
441
+ opts.push(li);
442
+ });
443
+ setActive(0);
444
+ }
445
+ function setActive(i) {
446
+ if (active >= 0 && opts[active]) opts[active].classList.remove('is-active');
447
+ active = Math.max(-1, Math.min(opts.length - 1, i));
448
+ if (active >= 0) {
449
+ opts[active].classList.add('is-active');
450
+ list.setAttribute('aria-activedescendant', opts[active].id);
451
+ opts[active].scrollIntoView({ block: 'nearest' });
452
+ } else {
453
+ list.removeAttribute('aria-activedescendant');
454
+ }
455
+ }
456
+ function drill(i) {
457
+ var node = current[i];
458
+ if (!node || !node.children) return;
459
+ stack.push(node);
460
+ current = node.children;
461
+ render();
462
+ }
463
+ function ascend() {
464
+ if (!stack.length) return;
465
+ stack.pop();
466
+ current = stack.length ? stack[stack.length - 1].children : root;
467
+ render();
468
+ }
469
+ function activate(i) {
470
+ var node = current[i];
471
+ if (!node) return;
472
+ if (node.children) { drill(i); return; }
473
+ selectedValue = node.value;
474
+ var labels = stack.map(function (n) { return n.label; }).concat([node.label]);
475
+ valueSpan.textContent = labels.join(sep);
476
+ valueSpan.classList.remove('fdy-cascade__value--placeholder');
477
+ wrap.dispatchEvent(new CustomEvent('fdy-cascade-change', { bubbles: true, detail: { value: node.value, path: labels.join(sep), labels: labels } }));
478
+ close(true);
479
+ }
480
+
481
+ var _pop = null;
482
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
483
+ function open() {
484
+ if (!panel.hidden) return;
485
+ // Re-open at the selected leaf's level for quick re-selection.
486
+ var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
487
+ if (trail && trail.length > 1) { stack = trail.slice(0, -1); current = stack[stack.length - 1].children; }
488
+ else { stack = []; current = root; }
489
+ var p = popCtl(); if (p) p.show(); else panel.hidden = false;
490
+ trigger.setAttribute('aria-expanded', 'true');
491
+ trigger.classList.add('is-open');
492
+ render();
493
+ list.focus();
494
+ document.addEventListener('click', onDoc, true);
495
+ }
496
+ function close(returnFocus) {
497
+ if (panel.hidden) return;
498
+ var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
499
+ trigger.setAttribute('aria-expanded', 'false');
500
+ trigger.classList.remove('is-open');
501
+ document.removeEventListener('click', onDoc, true);
502
+ if (returnFocus === true) trigger.focus();
503
+ }
504
+ function onDoc(e) { if (!wrap.contains(e.target)) close(false); }
505
+
506
+ trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
507
+ trigger.addEventListener('keydown', function (e) {
508
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
509
+ });
510
+ back.addEventListener('click', function () { ascend(); list.focus(); });
511
+ list.addEventListener('keydown', function (e) {
512
+ switch (e.key) {
513
+ case 'ArrowDown': e.preventDefault(); setActive(active + 1); break;
514
+ case 'ArrowUp': e.preventDefault(); setActive(active - 1); break;
515
+ case 'Home': e.preventDefault(); setActive(0); break;
516
+ case 'End': e.preventDefault(); setActive(opts.length - 1); break;
517
+ case 'ArrowRight': case 'Enter': case ' ': e.preventDefault(); if (active >= 0) activate(active); break;
518
+ case 'ArrowLeft': case 'Backspace': e.preventDefault(); ascend(); break;
519
+ case 'Escape': e.preventDefault(); close(true); break;
520
+ case 'Tab': close(false); break;
521
+ default: break;
522
+ }
523
+ });
524
+ list.addEventListener('click', function (e) {
525
+ var li = e.target.closest('[role="option"]');
526
+ if (li) activate(opts.indexOf(li));
527
+ });
528
+
529
+ // Initial display: pre-selected leaf shows its full path, else placeholder.
530
+ var initTrail = selectedValue ? pathTo(root, selectedValue, []) : null;
531
+ if (initTrail) {
532
+ valueSpan.textContent = initTrail.map(function (n) { return n.label; }).join(sep);
533
+ } else {
534
+ valueSpan.textContent = placeholder;
535
+ valueSpan.classList.add('fdy-cascade__value--placeholder');
536
+ }
537
+
538
+ var api = {
539
+ wrap: wrap,
540
+ getValue: function () { return selectedValue; },
541
+ clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
542
+ };
543
+ wrap._fdyCascade = api;
544
+ return api;
545
+ }
546
+
547
+ function initAll(context) {
548
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-cascade]'), initCascade);
549
+ }
550
+
551
+ if (document.readyState === 'loading') {
552
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
553
+ } else {
554
+ initAll();
555
+ }
556
+
557
+ window.FreedayCascade = { init: initCascade, initAll: initAll };
558
+ })();
559
+
560
+ /* Freeday — choose-from-list enhancer (optional, zero-dependency).
561
+ * Wires a searchable master-data picker dialog to read-only fields.
562
+ * Progressive enhancement over a native <dialog>; in a framework app, drive the
563
+ * dialog as a controlled component (fetchPage callback + server cache), not a store.
564
+ *
565
+ * Markup contract (see docs):
566
+ * - Field group: [data-fdy-cfl="<dialogId>"] — usually a `.fdy-input-group` holding a
567
+ * read-only `.fdy-input` and a trigger `<button class="fdy-input-group__btn">`.
568
+ * Optional `data-fdy-cfl-display="<key>"` picks which row dataset key fills the input
569
+ * (default "value"); optional `data-fdy-cfl-summary="{n} dipilih"` formats the multi label.
570
+ * - Standalone opener: [data-fdy-cfl-open="<dialogId>"] — opens the dialog with no bound field.
571
+ * - Dialog: <dialog id="<dialogId>" class="fdy-modal fdy-modal--cfl"> containing
572
+ * a `[data-fdy-cfl-search]` input, `.fdy-cfl__row` <tr> rows carrying `data-*` fields,
573
+ * a `[data-fdy-cfl-empty]` element, and (multi only) `[data-fdy-cfl-multiple]` on the
574
+ * dialog plus `[data-fdy-cfl-count]` and `[data-fdy-cfl-confirm]` in the footer.
575
+ *
576
+ * Emits a bubbling "fdy-cfl-select" CustomEvent on the field group (or the dialog when
577
+ * opened standalone): detail {row} for single-select, {rows} for multi-select — each a plain
578
+ * object copy of the row's dataset. Consumers fill sibling fields (code → name) from it.
579
+ */
580
+ (function () {
581
+ 'use strict';
582
+
583
+ function rowData(row) {
584
+ var out = {};
585
+ var ds = row.dataset;
586
+ for (var key in ds) { if (Object.prototype.hasOwnProperty.call(ds, key)) out[key] = ds[key]; }
587
+ return out;
588
+ }
589
+
590
+ function rowSearchText(row) {
591
+ return (row.getAttribute('data-search') != null ? row.getAttribute('data-search') : row.textContent)
592
+ .toLowerCase();
593
+ }
594
+
595
+ function initDialog(dialog) {
596
+ if (dialog.dataset.fdyCflReady === '1') return;
597
+ dialog.dataset.fdyCflReady = '1';
598
+
599
+ var search = dialog.querySelector('[data-fdy-cfl-search]');
600
+ var emptyEl = dialog.querySelector('[data-fdy-cfl-empty]');
601
+ var countEl = dialog.querySelector('[data-fdy-cfl-count]');
602
+ var confirmBtn = dialog.querySelector('[data-fdy-cfl-confirm]');
603
+ var multiple = dialog.hasAttribute('data-fdy-cfl-multiple');
604
+ var rows = Array.prototype.slice.call(dialog.querySelectorAll('.fdy-cfl__row'));
605
+
606
+ var activeGroup = null; // field group that opened the dialog (null = standalone)
607
+ var trigger = null; // control to restore focus to on close
608
+
609
+ rows.forEach(function (row) {
610
+ row.setAttribute('tabindex', '-1');
611
+ if (multiple && !row.hasAttribute('aria-selected')) row.setAttribute('aria-selected', 'false');
612
+ });
613
+
614
+ function visibleRows() {
615
+ return rows.filter(function (r) { return !r.hidden; });
616
+ }
617
+
618
+ function applyFilter() {
619
+ var q = (search ? search.value : '').trim().toLowerCase();
620
+ var shown = 0;
621
+ rows.forEach(function (row) {
622
+ var match = !q || rowSearchText(row).indexOf(q) !== -1;
623
+ row.hidden = !match;
624
+ if (match) shown++;
625
+ });
626
+ if (emptyEl) emptyEl.hidden = shown !== 0;
627
+ }
628
+
629
+ function updateCount() {
630
+ if (!countEl) return;
631
+ var n = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; }).length;
632
+ countEl.textContent = n + ' dipilih';
633
+ }
634
+
635
+ function fillGroup(detail) {
636
+ if (!activeGroup) return;
637
+ var input = activeGroup.querySelector('.fdy-input');
638
+ if (input) {
639
+ if (detail.rows) {
640
+ var tpl = activeGroup.getAttribute('data-fdy-cfl-summary') || '{n} dipilih';
641
+ input.value = detail.rows.length ? tpl.replace('{n}', String(detail.rows.length)) : '';
642
+ } else {
643
+ var key = activeGroup.getAttribute('data-fdy-cfl-display') || 'value';
644
+ input.value = detail.row[key] != null ? detail.row[key] : (detail.row.value || '');
645
+ }
646
+ }
647
+ }
648
+
649
+ function emit(detail) {
650
+ (activeGroup || dialog).dispatchEvent(new CustomEvent('fdy-cfl-select', {
651
+ bubbles: true, detail: detail
652
+ }));
653
+ }
654
+
655
+ function commitSingle(row) {
656
+ var detail = { row: rowData(row) };
657
+ fillGroup(detail);
658
+ emit(detail);
659
+ dialog.close();
660
+ }
661
+
662
+ function toggleSelect(row) {
663
+ var next = row.getAttribute('aria-selected') !== 'true';
664
+ row.setAttribute('aria-selected', String(next));
665
+ var box = row.querySelector('input[type="checkbox"]');
666
+ if (box) box.checked = next;
667
+ updateCount();
668
+ }
669
+
670
+ function confirmMulti() {
671
+ var picked = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; });
672
+ var detail = { rows: picked.map(rowData) };
673
+ fillGroup(detail);
674
+ emit(detail);
675
+ dialog.close();
676
+ }
677
+
678
+ function focusRow(row) { if (row) row.focus(); }
679
+
680
+ function moveFrom(current, delta) {
681
+ var vis = visibleRows();
682
+ var i = vis.indexOf(current);
683
+ if (i === -1) { focusRow(vis[0]); return; }
684
+ var target = i + delta;
685
+ if (target < 0) { if (search) search.focus(); return; }
686
+ if (target >= vis.length) return;
687
+ focusRow(vis[target]);
688
+ }
689
+
690
+ if (search) {
691
+ search.addEventListener('input', applyFilter);
692
+ search.addEventListener('keydown', function (e) {
693
+ if (e.key === 'ArrowDown') { e.preventDefault(); focusRow(visibleRows()[0]); }
694
+ });
695
+ }
696
+
697
+ dialog.addEventListener('click', function (e) {
698
+ var row = e.target.closest('.fdy-cfl__row');
699
+ if (!row || row.hidden) return;
700
+ if (multiple) toggleSelect(row); else commitSingle(row);
701
+ });
702
+
703
+ dialog.addEventListener('keydown', function (e) {
704
+ var row = e.target.closest('.fdy-cfl__row');
705
+ if (!row) return;
706
+ var vis = visibleRows();
707
+ switch (e.key) {
708
+ case 'ArrowDown': e.preventDefault(); moveFrom(row, 1); break;
709
+ case 'ArrowUp': e.preventDefault(); moveFrom(row, -1); break;
710
+ case 'Home': e.preventDefault(); focusRow(vis[0]); break;
711
+ case 'End': e.preventDefault(); focusRow(vis[vis.length - 1]); break;
712
+ case 'Enter':
713
+ case ' ':
714
+ e.preventDefault();
715
+ if (multiple) toggleSelect(row); else commitSingle(row);
716
+ break;
717
+ default: break;
718
+ }
719
+ });
720
+
721
+ if (confirmBtn) confirmBtn.addEventListener('click', confirmMulti);
722
+
723
+ dialog.addEventListener('close', function () {
724
+ if (trigger && typeof trigger.focus === 'function') trigger.focus();
725
+ activeGroup = null;
726
+ trigger = null;
727
+ });
728
+
729
+ // Opening API used by triggers below.
730
+ dialog.fdyCflOpen = function (group, opener) {
731
+ activeGroup = group || null;
732
+ trigger = opener || null;
733
+ if (search) search.value = '';
734
+ applyFilter();
735
+ updateCount();
736
+ if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open', '');
737
+ if (search) search.focus();
738
+ };
739
+ }
740
+
741
+ function initTriggers(context) {
742
+ var ctx = context || document;
743
+
744
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), function (group) {
745
+ if (group.dataset.fdyCflBound === '1') return;
746
+ var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
747
+ if (!dialog) return;
748
+ initDialog(dialog);
749
+ var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
750
+ if (!opener) return;
751
+ group.dataset.fdyCflBound = '1';
752
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
753
+ });
754
+
755
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), function (opener) {
756
+ if (opener.dataset.fdyCflBound === '1') return;
757
+ var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
758
+ if (!dialog) return;
759
+ initDialog(dialog);
760
+ opener.dataset.fdyCflBound = '1';
761
+ opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
762
+ });
763
+ }
764
+
765
+ if (document.readyState === 'loading') {
766
+ document.addEventListener('DOMContentLoaded', function () { initTriggers(); });
767
+ } else {
768
+ initTriggers();
769
+ }
770
+
771
+ window.FreedayCfl = { init: initTriggers };
772
+ })();
773
+
774
+ /* Freeday — chart renderer (optional, zero-dependency, no external chart lib).
775
+ * Renders sparkline / bar / line / area / donut into an element from data attributes.
776
+ * Auto-inits [data-fdy-chart] once; call FreedayChart.update(el) to re-render after the
777
+ * data attributes change (used by the freeday/vue <FdyChart> wrapper for reactive data).
778
+ *
779
+ * Single-series colour (sparkline / simple bar / single line) via data-fdy-color references a
780
+ * semantic token by name (primary, accent, success, warning, danger, info). Multi-series
781
+ * defaults draw from the validated categorical --chart-1..8 palette in fixed order (never
782
+ * cycled, 8-series cap); pass data-fdy-colors to override with semantic names instead.
783
+ * Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
784
+ * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
785
+ * Charts are an enhancement — put a table/text fallback inside the element and it will be
786
+ * replaced on render; give the element role="img" + aria-label.
787
+ *
788
+ * Sparkline: <span data-fdy-chart="sparkline" data-values="4,6,5,8,7,10" data-fdy-color="primary">
789
+ * Bar: <div data-fdy-chart="bar" data-values="12,19,8,15" data-labels="Sen,Sel,Rab,Kam">
790
+ * Donut: <div data-fdy-chart="donut" data-values="45,30,25" data-labels="Lunas,Tertunda,Batal">
791
+ * Line/area/multi bar (axes + legend + formatting):
792
+ * <div data-fdy-chart="line" data-labels="Jan,Feb,Mar,Apr" data-fdy-format="percent"
793
+ * data-series='[{"label":"Posted","values":[12,19,8,15]},{"label":"Draft","values":[4,6,5,8]}]'>
794
+ * <div data-fdy-chart="bar" data-fdy-stacked data-labels="..." data-series='[...]'>
795
+ */
796
+ (function () {
797
+ 'use strict';
798
+
799
+ var NS = 'http://www.w3.org/2000/svg';
800
+
801
+ // Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
802
+ // (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
803
+ function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
804
+ function colorVar(name) { return 'var(--color-' + (name || 'primary') + ')'; }
805
+
806
+ function nums(el, attr) {
807
+ var v = el.getAttribute(attr);
808
+ return v ? v.split(',').map(function (s) { return parseFloat(s.trim()); }).filter(function (n) { return !isNaN(n); }) : [];
809
+ }
810
+ function strs(el, attr) {
811
+ var v = el.getAttribute(attr);
812
+ return v ? v.split(',').map(function (s) { return s.trim(); }) : [];
813
+ }
814
+ function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
815
+ function svgEl(name) { return document.createElementNS(NS, name); }
816
+
817
+ // Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
818
+ function getSeries(el) {
819
+ var raw = el.getAttribute('data-series');
820
+ if (raw) {
821
+ try {
822
+ var arr = JSON.parse(raw);
823
+ if (Array.isArray(arr)) return arr.map(function (s) {
824
+ return { label: s && s.label != null ? String(s.label) : '', values: ((s && s.values) || []).map(Number) };
825
+ });
826
+ } catch (e) { /* fall through to data-values */ }
827
+ }
828
+ var vals = nums(el, 'data-values');
829
+ return vals.length ? [{ label: el.getAttribute('data-fdy-label') || '', values: vals }] : [];
830
+ }
831
+
832
+ // Value formatter for ticks + tooltips.
833
+ function makeFormat(kind) {
834
+ if (kind === 'percent') return function (v) { return (Math.round(v * 10) / 10) + '%'; };
835
+ if (kind === 'currency' || kind === 'usd') return function (v) { return '$' + Math.round(v).toLocaleString('en-US'); };
836
+ return function (v) { return (Math.round(v * 100) / 100).toLocaleString(); };
837
+ }
838
+ // Round a positive number up to a "nice" axis maximum (1/2/2.5/5/10 * 10^n).
839
+ function niceCeil(x) {
840
+ if (x <= 0) return 1;
841
+ var exp = Math.floor(Math.log10(x)), base = Math.pow(10, exp), f = x / base;
842
+ var nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10;
843
+ return nf * base;
844
+ }
845
+
846
+ // ---- Interactive tooltip (Chart.js-style hover readout; purely visual) ----
847
+ function makeTip(container) {
848
+ if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
849
+ var tip = document.createElement('div');
850
+ tip.className = 'fdy-chart__tip';
851
+ tip.setAttribute('aria-hidden', 'true');
852
+ container.appendChild(tip);
853
+ return tip;
854
+ }
855
+ function showTip(tip, container, clientX, clientY, text) {
856
+ var r = container.getBoundingClientRect();
857
+ tip.textContent = text;
858
+ tip.style.left = (clientX - r.left) + 'px';
859
+ tip.style.top = (clientY - r.top) + 'px';
860
+ tip.classList.add('is-visible');
861
+ }
862
+ function hideTip(tip) { tip.classList.remove('is-visible'); }
863
+
864
+ // Donut sector geometry (SVG path for a ring slice between two radii/angles).
865
+ function polar(cx, cy, r, deg) { var a = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }
866
+ function sectorPath(cx, cy, R, r, a0, a1) {
867
+ var large = (a1 - a0) > 180 ? 1 : 0;
868
+ var p0 = polar(cx, cy, R, a0), p1 = polar(cx, cy, R, a1), p2 = polar(cx, cy, r, a1), p3 = polar(cx, cy, r, a0);
869
+ return 'M' + p0[0].toFixed(2) + ' ' + p0[1].toFixed(2)
870
+ + ' A' + R + ' ' + R + ' 0 ' + large + ' 1 ' + p1[0].toFixed(2) + ' ' + p1[1].toFixed(2)
871
+ + ' L' + p2[0].toFixed(2) + ' ' + p2[1].toFixed(2)
872
+ + ' A' + r + ' ' + r + ' 0 ' + large + ' 0 ' + p3[0].toFixed(2) + ' ' + p3[1].toFixed(2) + ' Z';
873
+ }
874
+
875
+ function renderSparkline(el) {
876
+ var vals = nums(el, 'data-values');
877
+ if (vals.length < 2) return;
878
+ el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
879
+ var W = 128, H = 36, P = 3;
880
+ var min = Math.min.apply(null, vals), max = Math.max.apply(null, vals), span = (max - min) || 1;
881
+ var pts = vals.map(function (v, i) {
882
+ var x = P + (i / (vals.length - 1)) * (W - 2 * P);
883
+ var y = H - P - ((v - min) / span) * (H - 2 * P);
884
+ return [x, y];
885
+ });
886
+ var line = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
887
+ var area = P + ',' + H + ' ' + line + ' ' + (W - P) + ',' + H;
888
+ var last = pts[pts.length - 1];
889
+ el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" aria-hidden="true">'
890
+ + '<polygon class="fdy-sparkline__area" points="' + area + '"/>'
891
+ + '<polyline class="fdy-sparkline__line" points="' + line + '" vector-effect="non-scaling-stroke"/>'
892
+ + '<circle class="fdy-sparkline__dot" cx="' + last[0].toFixed(1) + '" cy="' + last[1].toFixed(1) + '" r="2.5"/>'
893
+ + '</svg>';
894
+ ensureImg(el);
895
+ }
896
+
897
+ // Simple single-series flex bar (legacy, unchanged look). Rich/multi-series/stacked bars
898
+ // with axes go through renderCartesian instead.
899
+ function renderBars(el) {
900
+ var vals = nums(el, 'data-values');
901
+ if (!vals.length) return;
902
+ var labels = strs(el, 'data-labels');
903
+ el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
904
+ var max = Math.max.apply(null, vals) || 1;
905
+ el.innerHTML = '';
906
+ var tip = makeTip(el);
907
+ vals.forEach(function (v, i) {
908
+ var col = document.createElement('div'); col.className = 'fdy-bars__col';
909
+ var track = document.createElement('div'); track.className = 'fdy-bars__track';
910
+ var val = document.createElement('span'); val.className = 'fdy-bars__val'; val.textContent = v;
911
+ var bar = document.createElement('div'); bar.className = 'fdy-bars__bar';
912
+ bar.style.height = Math.max(2, (v / max) * 100) + '%';
913
+ var text = (labels[i] || ('#' + (i + 1))) + ': ' + v;
914
+ track.appendChild(val); track.appendChild(bar);
915
+ col.appendChild(track);
916
+ if (labels[i]) {
917
+ var lab = document.createElement('div'); lab.className = 'fdy-bars__label'; lab.textContent = labels[i];
918
+ col.appendChild(lab);
919
+ }
920
+ // Per-column hover so re-render (update) never stacks document/element listeners.
921
+ col.addEventListener('mousemove', function (e) { showTip(tip, el, e.clientX, e.clientY, text); });
922
+ col.addEventListener('mouseleave', function () { hideTip(tip); });
923
+ el.appendChild(col);
924
+ });
925
+ ensureImg(el);
926
+ }
927
+
928
+ // ---- Cartesian engine: line / area / grouped+stacked bar with themed axes + legend ----
929
+ function renderCartesian(el, type) {
930
+ var series = getSeries(el);
931
+ if (!series.length) return;
932
+ var labels = strs(el, 'data-labels');
933
+ var fmt = makeFormat(el.getAttribute('data-fdy-format'));
934
+ var stacked = type === 'bar' && el.hasAttribute('data-fdy-stacked');
935
+ var overrides = strs(el, 'data-fdy-colors');
936
+ var singleColor = el.getAttribute('data-fdy-color');
937
+ function colorFor(i) {
938
+ if (overrides[i]) return colorVar(overrides[i]);
939
+ if (series.length === 1 && singleColor) return colorVar(singleColor);
940
+ return chartSlotVar(i);
941
+ }
942
+
943
+ var n = series.reduce(function (m, s) { return Math.max(m, s.values.length); }, 0);
944
+ if (!n) return;
945
+
946
+ // y-domain (baseline at 0 unless data dips below).
947
+ var dataMax = -Infinity, dataMin = Infinity;
948
+ if (stacked) {
949
+ for (var c = 0; c < n; c++) {
950
+ var sum = 0; series.forEach(function (s) { sum += (s.values[c] || 0); });
951
+ if (sum > dataMax) dataMax = sum; if (sum < dataMin) dataMin = sum;
952
+ }
953
+ } else {
954
+ series.forEach(function (s) { s.values.forEach(function (v) { if (v > dataMax) dataMax = v; if (v < dataMin) dataMin = v; }); });
955
+ }
956
+ if (dataMax === -Infinity) dataMax = 1;
957
+ var base = Math.min(0, dataMin);
958
+ var top = niceCeil(dataMax > base ? dataMax : base + 1);
959
+ if (top <= base) top = base + 1;
960
+
961
+ var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
962
+ var plotW = W - PL - PR, plotH = H - PT - PB;
963
+ function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
964
+ var slot = plotW / n;
965
+ function xCenter(i) { return PL + slot * (i + 0.5); } // bar / band centre
966
+ function xLine(i) { return PL + (n === 1 ? plotW / 2 : (i / (n - 1)) * plotW); }
967
+
968
+ var svg = svgEl('svg');
969
+ svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
970
+ svg.setAttribute('aria-hidden', 'true');
971
+
972
+ // y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
973
+ var TICKS = 4;
974
+ for (var k = 0; k <= TICKS; k++) {
975
+ var val = base + (top - base) * k / TICKS;
976
+ var gy = mapY(val);
977
+ var gl = svgEl('line');
978
+ gl.setAttribute('x1', PL); gl.setAttribute('x2', PL + plotW);
979
+ gl.setAttribute('y1', gy.toFixed(1)); gl.setAttribute('y2', gy.toFixed(1));
980
+ gl.setAttribute('class', 'fdy-chart-xy__grid');
981
+ svg.appendChild(gl);
982
+ var tk = svgEl('text');
983
+ tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
984
+ tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
985
+ tk.textContent = fmt(val);
986
+ svg.appendChild(tk);
987
+ }
988
+
989
+ // x category labels with autoskip.
990
+ var maxTicks = Math.max(2, Math.floor(plotW / 40)), step = Math.ceil(n / maxTicks);
991
+ for (var xi = 0; xi < n; xi++) {
992
+ if (xi % step !== 0 && xi !== n - 1) continue;
993
+ var lx = (type === 'bar') ? xCenter(xi) : xLine(xi);
994
+ var xt = svgEl('text');
995
+ xt.setAttribute('x', lx.toFixed(1)); xt.setAttribute('y', H - 6);
996
+ xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
997
+ xt.textContent = labels[xi] || ('#' + (xi + 1));
998
+ svg.appendChild(xt);
999
+ }
1000
+
1001
+ if (type === 'bar') {
1002
+ var S = series.length;
1003
+ var groupW = slot * 0.72;
1004
+ var barW = stacked ? slot * 0.55 : groupW / S;
1005
+ var acc = []; // running top per category (stacked)
1006
+ for (var ci = 0; ci < n; ci++) acc[ci] = base;
1007
+ series.forEach(function (s, si) {
1008
+ s.values.forEach(function (v, ci2) {
1009
+ if (v == null || isNaN(v)) return;
1010
+ var x, y0, y1;
1011
+ if (stacked) {
1012
+ x = xCenter(ci2) - barW / 2;
1013
+ y0 = mapY(acc[ci2]); y1 = mapY(acc[ci2] + v); acc[ci2] += v;
1014
+ } else {
1015
+ x = PL + slot * ci2 + (slot - groupW) / 2 + barW * si;
1016
+ y0 = mapY(base); y1 = mapY(v);
1017
+ }
1018
+ var rect = svgEl('rect');
1019
+ rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
1020
+ rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
1021
+ rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
1022
+ rect.style.fill = colorFor(si);
1023
+ svg.appendChild(rect);
1024
+ });
1025
+ });
1026
+ } else {
1027
+ // line / area: fill (area) beneath the stroke, then a small dot per vertex.
1028
+ series.forEach(function (s, si) {
1029
+ var pts = [];
1030
+ for (var i = 0; i < n; i++) { var v = s.values[i]; if (v == null || isNaN(v)) continue; pts.push([xLine(i), mapY(v)]); }
1031
+ if (!pts.length) return;
1032
+ var d = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
1033
+ if (type === 'area') {
1034
+ var y0 = mapY(base).toFixed(1);
1035
+ var poly = svgEl('polygon');
1036
+ poly.setAttribute('points', pts[0][0].toFixed(1) + ',' + y0 + ' ' + d + ' ' + pts[pts.length - 1][0].toFixed(1) + ',' + y0);
1037
+ poly.setAttribute('class', 'fdy-chart-xy__area');
1038
+ poly.style.fill = colorFor(si);
1039
+ svg.appendChild(poly);
1040
+ }
1041
+ if (pts.length >= 2) {
1042
+ var pl = svgEl('polyline');
1043
+ pl.setAttribute('points', d); pl.setAttribute('class', 'fdy-chart-xy__line');
1044
+ pl.style.stroke = colorFor(si);
1045
+ svg.appendChild(pl);
1046
+ }
1047
+ pts.forEach(function (p) {
1048
+ var dot = svgEl('circle');
1049
+ dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
1050
+ dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
1051
+ svg.appendChild(dot);
1052
+ });
1053
+ });
1054
+ }
1055
+
1056
+ // Assemble: optional legend on top, plot below, tooltip layer last.
1057
+ var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
1058
+ var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
1059
+ el.classList.add('fdy-chart-xy');
1060
+ el.innerHTML = '';
1061
+ if (showLegend) {
1062
+ var legend = document.createElement('ul');
1063
+ legend.className = 'fdy-chart__legend fdy-chart__legend--row';
1064
+ series.forEach(function (s, si) {
1065
+ var li = document.createElement('li');
1066
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
1067
+ li.appendChild(sw);
1068
+ li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
1069
+ legend.appendChild(li);
1070
+ });
1071
+ el.appendChild(legend);
1072
+ }
1073
+ var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
1074
+ plot.appendChild(svg);
1075
+ el.appendChild(plot);
1076
+
1077
+ // Hover: one transparent band per category → readout of every series at that x.
1078
+ var tip = makeTip(el);
1079
+ var bandW = (type === 'bar') ? slot : (n > 1 ? plotW / (n - 1) : plotW);
1080
+ for (var b = 0; b < n; b++) {
1081
+ var bx = (type === 'bar') ? (PL + slot * b) : (xLine(b) - bandW / 2);
1082
+ var band = svgEl('rect');
1083
+ band.setAttribute('x', Math.max(0, bx).toFixed(1)); band.setAttribute('y', PT);
1084
+ band.setAttribute('width', bandW.toFixed(1)); band.setAttribute('height', plotH);
1085
+ band.setAttribute('class', 'fdy-chart-xy__band');
1086
+ var parts = series.map(function (s) {
1087
+ var v = s.values[b];
1088
+ return (v == null || isNaN(v)) ? null : (series.length > 1 ? s.label + ' ' : '') + fmt(v);
1089
+ }).filter(Boolean);
1090
+ var text = (labels[b] || ('#' + (b + 1))) + (parts.length ? ' · ' + parts.join(', ') : '');
1091
+ band.addEventListener('mousemove', (function (t) { return function (e) { showTip(tip, el, e.clientX, e.clientY, t); }; })(text));
1092
+ band.addEventListener('mouseleave', function () { hideTip(tip); });
1093
+ svg.appendChild(band);
1094
+ }
1095
+ ensureImg(el);
1096
+ }
1097
+
1098
+ function renderDonut(el) {
1099
+ var vals = nums(el, 'data-values');
1100
+ if (!vals.length) return;
1101
+ var labels = strs(el, 'data-labels');
1102
+ var colors = strs(el, 'data-fdy-colors');
1103
+ var total = vals.reduce(function (a, b) { return a + b; }, 0) || 1;
1104
+ var stops = [], acc = 0;
1105
+ var colorAt = function (i) { return colors[i] ? colorVar(colors[i]) : chartSlotVar(i); };
1106
+ vals.forEach(function (v, i) {
1107
+ var from = (acc / total) * 360, to = ((acc + v) / total) * 360;
1108
+ acc += v;
1109
+ stops.push(colorAt(i) + ' ' + from.toFixed(1) + 'deg ' + to.toFixed(1) + 'deg');
1110
+ });
1111
+ el.innerHTML = '';
1112
+ var ring = document.createElement('div'); ring.className = 'fdy-donut__ring';
1113
+ ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
1114
+ var center = document.createElement('div'); center.className = 'fdy-donut__center';
1115
+ var centerLabel = el.getAttribute('data-fdy-center');
1116
+ center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
1117
+ center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
1118
+ ring.appendChild(center);
1119
+ var svg = svgEl('svg');
1120
+ svg.setAttribute('class', 'fdy-donut__hit');
1121
+ svg.setAttribute('viewBox', '0 0 128 128');
1122
+ var ringTip = makeTip(ring), acc2 = 0;
1123
+ vals.forEach(function (v, i) {
1124
+ var a0 = (acc2 / total) * 360, a1 = ((acc2 + v) / total) * 360; acc2 += v;
1125
+ var path = svgEl('path');
1126
+ path.setAttribute('class', 'fdy-donut__seg');
1127
+ path.setAttribute('d', sectorPath(64, 64, 64, 33, a0, a1));
1128
+ var text = (labels[i] || ('#' + (i + 1))) + ': ' + v + ' (' + Math.round((v / total) * 100) + '%)';
1129
+ path.addEventListener('mousemove', function (e) { showTip(ringTip, ring, e.clientX, e.clientY, text); });
1130
+ path.addEventListener('mouseleave', function () { hideTip(ringTip); });
1131
+ svg.appendChild(path);
1132
+ });
1133
+ ring.appendChild(svg);
1134
+ var legend = document.createElement('ul'); legend.className = 'fdy-chart__legend';
1135
+ vals.forEach(function (v, i) {
1136
+ var li = document.createElement('li');
1137
+ var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorAt(i);
1138
+ li.appendChild(sw);
1139
+ li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
1140
+ legend.appendChild(li);
1141
+ });
1142
+ el.appendChild(ring); el.appendChild(legend);
1143
+ ensureImg(el);
1144
+ }
1145
+
1146
+ // Full (re-)render from current data attributes. Idempotent — safe to call repeatedly.
1147
+ function renderChart(el) {
1148
+ var type = el.getAttribute('data-fdy-chart');
1149
+ if (type === 'sparkline') renderSparkline(el);
1150
+ else if (type === 'donut') renderDonut(el);
1151
+ else if (type === 'line' || type === 'area') renderCartesian(el, type);
1152
+ else if (type === 'bar') {
1153
+ if (el.getAttribute('data-series') || el.hasAttribute('data-fdy-stacked') || el.hasAttribute('data-fdy-axes')) renderCartesian(el, 'bar');
1154
+ else renderBars(el);
1155
+ }
1156
+ }
1157
+
1158
+ // Auto-init renders once; explicit update() re-renders after data changes.
1159
+ function initChart(el) {
1160
+ if (el.dataset.fdyChartReady === '1') return;
1161
+ el.dataset.fdyChartReady = '1';
1162
+ renderChart(el);
1163
+ }
1164
+
1165
+ function initAll(context) {
1166
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
1167
+ }
1168
+
1169
+ if (document.readyState === 'loading') {
1170
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1171
+ } else {
1172
+ initAll();
1173
+ }
1174
+
1175
+ window.FreedayChart = { init: initChart, initAll: initAll, update: renderChart };
1176
+ })();
1177
+
1178
+ /* Freeday — chip enhancer (optional, zero-dependency).
1179
+ * Two behaviours, both progressive enhancement:
1180
+ *
1181
+ * 1. Removable chips — any .fdy-chip__remove button removes its chip and emits a
1182
+ * bubbling "fdy-chip-remove" (detail {value}).
1183
+ * 2. Choice / filter groups — a [data-fdy-chips] container of interactive chips
1184
+ * (.fdy-chip--choice / .fdy-chip--filter, each a <button>) toggles aria-pressed
1185
+ * on click. Add data-single for one-at-a-time (radio-like) selection; filter
1186
+ * chips get a leading check when pressed. Emits "fdy-chip-change" on the group
1187
+ * (detail {value, pressed, selected}).
1188
+ */
1189
+ (function () {
1190
+ 'use strict';
1191
+
1192
+ var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
1193
+
1194
+ function initRemove(btn) {
1195
+ if (btn.dataset.fdyChipRmReady === '1') return;
1196
+ btn.dataset.fdyChipRmReady = '1';
1197
+ btn.addEventListener('click', function () {
1198
+ var chip = btn.closest('.fdy-chip');
1199
+ if (!chip) return;
1200
+ var value = chip.getAttribute('data-value');
1201
+ if (!value) {
1202
+ // Derive the label from the chip text, minus the remove button glyph.
1203
+ var clone = chip.cloneNode(true);
1204
+ var rm = clone.querySelector('.fdy-chip__remove');
1205
+ if (rm) rm.remove();
1206
+ value = (clone.textContent || '').trim();
1207
+ }
1208
+ chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
1209
+ chip.remove();
1210
+ });
1211
+ }
1212
+
1213
+ function initGroup(group) {
1214
+ if (group.dataset.fdyChipsReady === '1') return;
1215
+ group.dataset.fdyChipsReady = '1';
1216
+ var single = group.hasAttribute('data-single');
1217
+ if (!group.getAttribute('role')) group.setAttribute('role', 'group');
1218
+ var groupLabel = group.getAttribute('data-label');
1219
+ if (groupLabel && !group.getAttribute('aria-label')) group.setAttribute('aria-label', groupLabel);
1220
+
1221
+ var chips = Array.prototype.slice.call(group.querySelectorAll('.fdy-chip--choice, .fdy-chip--filter'));
1222
+ function valueOf(chip) { return chip.getAttribute('data-value') || (chip.textContent || '').trim(); }
1223
+ function selected() {
1224
+ return chips.filter(function (c) { return c.getAttribute('aria-pressed') === 'true'; }).map(valueOf);
1225
+ }
1226
+
1227
+ chips.forEach(function (chip) {
1228
+ if (!chip.hasAttribute('aria-pressed')) chip.setAttribute('aria-pressed', 'false');
1229
+ if (chip.classList.contains('fdy-chip--filter') && !chip.querySelector('.fdy-chip__check')) {
1230
+ var check = document.createElement('span');
1231
+ check.className = 'fdy-chip__check';
1232
+ check.innerHTML = CHECK;
1233
+ chip.insertBefore(check, chip.firstChild);
1234
+ }
1235
+ chip.addEventListener('click', function () {
1236
+ var wasPressed = chip.getAttribute('aria-pressed') === 'true';
1237
+ if (single) chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
1238
+ chip.setAttribute('aria-pressed', wasPressed ? 'false' : 'true');
1239
+ group.dispatchEvent(new CustomEvent('fdy-chip-change', {
1240
+ bubbles: true,
1241
+ detail: { value: valueOf(chip), pressed: !wasPressed, selected: selected() }
1242
+ }));
1243
+ });
1244
+ });
1245
+ }
1246
+
1247
+ function initAll(context) {
1248
+ var root = context || document;
1249
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-chips]'), initGroup);
1250
+ Array.prototype.forEach.call(root.querySelectorAll('.fdy-chip__remove'), initRemove);
1251
+ }
1252
+
1253
+ if (document.readyState === 'loading') {
1254
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1255
+ } else {
1256
+ initAll();
1257
+ }
1258
+
1259
+ window.FreedayChip = { initGroup: initGroup, initRemove: initRemove, initAll: initAll };
1260
+ })();
1261
+
1262
+ /* Freeday — date picker enhancer (optional, zero-dependency).
1263
+ * Builds an input-styled trigger + calendar popover from an empty wrapper.
1264
+ * Locale comes from <html lang> (via Intl) — month/weekday/value formatting is not hardcoded.
1265
+ *
1266
+ * Markup contract:
1267
+ * - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Tanggal unggah"
1268
+ * data-placeholder="Pilih tanggal" data-min="2026-01-01" data-max="2026-12-31"></div>
1269
+ * - Range: <div data-fdy-daterange role="group" aria-label="Rentang tanggal">
1270
+ * <div data-fdy-datepicker data-role="from" data-placeholder="Dari"></div>
1271
+ * <span class="fdy-daterange__sep">–</span>
1272
+ * <div data-fdy-datepicker data-role="to" data-placeholder="Sampai"></div>
1273
+ * </div>
1274
+ * The range links the two: the end can never precede the start (out-of-range days disable).
1275
+ *
1276
+ * Emits a bubbling "fdy-datepicker-change" CustomEvent on the wrapper: detail {value, date}.
1277
+ * Keyboard in the grid: ←/→ ±1 day, ↑/↓ ±1 week, Home/End week edge, PageUp/Down ±month,
1278
+ * Shift+PageUp/Down ±year, Enter/Space select, Esc close (focus returns to the trigger).
1279
+ */
1280
+ (function () {
1281
+ 'use strict';
1282
+
1283
+ var LOCALE = document.documentElement.getAttribute('lang') || 'id';
1284
+ var uidSeq = 0;
1285
+ function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
1286
+ function pad(n) { return n < 10 ? '0' + n : '' + n; }
1287
+ function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
1288
+ function parseISO(s) {
1289
+ if (!s) return null;
1290
+ var p = String(s).split('-');
1291
+ if (p.length !== 3) return null;
1292
+ var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
1293
+ return isNaN(d.getTime()) ? null : d;
1294
+ }
1295
+ function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
1296
+ function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
1297
+ function addDays(d, n) { var x = new Date(d); x.setDate(x.getDate() + n); return x; }
1298
+ function addMonths(d, n) { var x = new Date(d); x.setMonth(x.getMonth() + n); return x; }
1299
+
1300
+ var monthFmt = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric' });
1301
+ var valueFmt = new Intl.DateTimeFormat(LOCALE, { day: '2-digit', month: 'short', year: 'numeric' });
1302
+ var dayLabelFmt = new Intl.DateTimeFormat(LOCALE, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
1303
+
1304
+ function weekdayHeaders() {
1305
+ var monday = new Date(2024, 0, 1); // a known Monday
1306
+ var fmt = new Intl.DateTimeFormat(LOCALE, { weekday: 'short' });
1307
+ var out = [];
1308
+ for (var i = 0; i < 7; i++) out.push(fmt.format(addDays(monday, i)));
1309
+ return out;
1310
+ }
1311
+
1312
+ var CAL_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"></rect><path d="M16 2v4M8 2v4M3 10h18"></path></svg>';
1313
+
1314
+ function initPicker(wrap) {
1315
+ if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
1316
+ wrap.dataset.fdyDpReady = '1';
1317
+ wrap.classList.add('fdy-datepicker');
1318
+
1319
+ var placeholder = wrap.getAttribute('data-placeholder') || 'Pilih tanggal';
1320
+ var label = wrap.getAttribute('data-label') || 'Tanggal';
1321
+ var selected = parseISO(wrap.getAttribute('data-value'));
1322
+ var minDate = parseISO(wrap.getAttribute('data-min'));
1323
+ var maxDate = parseISO(wrap.getAttribute('data-max'));
1324
+ var rangeStart = null, rangeEnd = null; // for in-range shading
1325
+ var view = startOfDay(selected || new Date());
1326
+ view = new Date(view.getFullYear(), view.getMonth(), 1);
1327
+ var focusDate = null;
1328
+
1329
+ var trigger = document.createElement('button');
1330
+ trigger.type = 'button';
1331
+ trigger.className = 'fdy-datepicker__trigger';
1332
+ trigger.setAttribute('aria-haspopup', 'dialog');
1333
+ trigger.setAttribute('aria-expanded', 'false');
1334
+ trigger.setAttribute('aria-label', label);
1335
+ var valueSpan = document.createElement('span');
1336
+ valueSpan.className = 'fdy-datepicker__value';
1337
+ trigger.appendChild(valueSpan);
1338
+ // Icon variants (Freeday convention for JS-built controls): [data-fdy-no-icon] omits the
1339
+ // icon; a [data-fdy-icon] child supplies a custom SVG; otherwise the default calendar.
1340
+ if (!wrap.hasAttribute('data-fdy-no-icon')) {
1341
+ var customIcon = wrap.querySelector('[data-fdy-icon]');
1342
+ var icon = document.createElement('span');
1343
+ icon.className = 'fdy-datepicker__icon';
1344
+ icon.innerHTML = customIcon ? customIcon.innerHTML : CAL_ICON;
1345
+ if (customIcon) customIcon.remove();
1346
+ trigger.appendChild(icon);
1347
+ }
1348
+
1349
+ var panel = document.createElement('div');
1350
+ panel.className = 'fdy-datepicker__panel';
1351
+ panel.setAttribute('role', 'dialog');
1352
+ panel.setAttribute('aria-modal', 'false');
1353
+ panel.setAttribute('aria-label', label);
1354
+ panel.hidden = true;
1355
+
1356
+ wrap.appendChild(trigger);
1357
+ wrap.appendChild(panel);
1358
+
1359
+ function updateDisplay() {
1360
+ if (selected) {
1361
+ valueSpan.textContent = valueFmt.format(selected);
1362
+ valueSpan.classList.remove('fdy-datepicker__value--placeholder');
1363
+ } else {
1364
+ valueSpan.textContent = placeholder;
1365
+ valueSpan.classList.add('fdy-datepicker__value--placeholder');
1366
+ }
1367
+ }
1368
+
1369
+ function isDisabled(d) {
1370
+ if (minDate && startOfDay(d).getTime() < startOfDay(minDate).getTime()) return true;
1371
+ if (maxDate && startOfDay(d).getTime() > startOfDay(maxDate).getTime()) return true;
1372
+ return false;
1373
+ }
1374
+
1375
+ function navButton(glyph, aria, onClick) {
1376
+ var b = document.createElement('button');
1377
+ b.type = 'button';
1378
+ b.className = 'fdy-cal__nav';
1379
+ b.textContent = glyph;
1380
+ b.setAttribute('aria-label', aria);
1381
+ b.addEventListener('click', onClick);
1382
+ return b;
1383
+ }
1384
+
1385
+ function render() {
1386
+ panel.innerHTML = '';
1387
+ var head = document.createElement('div');
1388
+ head.className = 'fdy-cal__head';
1389
+ var title = document.createElement('div');
1390
+ title.className = 'fdy-cal__title';
1391
+ title.id = uid('fdy-cal-title');
1392
+ title.textContent = monthFmt.format(view);
1393
+ panel.setAttribute('aria-labelledby', title.id);
1394
+ head.appendChild(navButton('‹', 'Bulan sebelumnya', function () { view = addMonths(view, -1); render(); }));
1395
+ head.appendChild(title);
1396
+ head.appendChild(navButton('›', 'Bulan berikutnya', function () { view = addMonths(view, 1); render(); }));
1397
+ panel.appendChild(head);
1398
+
1399
+ var grid = document.createElement('div');
1400
+ grid.className = 'fdy-cal__grid';
1401
+ grid.setAttribute('role', 'grid');
1402
+ grid.setAttribute('aria-labelledby', title.id);
1403
+ weekdayHeaders().forEach(function (w) {
1404
+ var c = document.createElement('div');
1405
+ c.className = 'fdy-cal__dow';
1406
+ c.setAttribute('role', 'columnheader');
1407
+ c.setAttribute('aria-label', w);
1408
+ c.textContent = w;
1409
+ grid.appendChild(c);
1410
+ });
1411
+
1412
+ var first = new Date(view.getFullYear(), view.getMonth(), 1);
1413
+ var offset = (first.getDay() + 6) % 7; // Monday-first
1414
+ var startCell = addDays(first, -offset);
1415
+ var today = new Date();
1416
+ if (!focusDate) focusDate = selected || today;
1417
+ // Keep the roving cell inside the visible month so the grid is always keyboard-reachable.
1418
+ if (focusDate.getMonth() !== view.getMonth() || focusDate.getFullYear() !== view.getFullYear()) {
1419
+ var dim = new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate();
1420
+ focusDate = new Date(view.getFullYear(), view.getMonth(), Math.min(focusDate.getDate(), dim));
1421
+ }
1422
+
1423
+ for (var i = 0; i < 42; i++) {
1424
+ var d = addDays(startCell, i);
1425
+ var btn = document.createElement('button');
1426
+ btn.type = 'button';
1427
+ btn.className = 'fdy-cal__day';
1428
+ btn.setAttribute('role', 'gridcell');
1429
+ btn.setAttribute('aria-label', dayLabelFmt.format(d));
1430
+ btn.textContent = d.getDate();
1431
+ if (d.getMonth() !== view.getMonth()) btn.classList.add('is-outside');
1432
+ if (sameDay(d, today)) btn.classList.add('is-today');
1433
+ if (selected && sameDay(d, selected)) { btn.classList.add('is-selected'); btn.setAttribute('aria-selected', 'true'); }
1434
+ if (rangeStart && rangeEnd) {
1435
+ var t = startOfDay(d).getTime();
1436
+ if (t > startOfDay(rangeStart).getTime() && t < startOfDay(rangeEnd).getTime()) btn.classList.add('in-range');
1437
+ }
1438
+ if (isDisabled(d)) btn.disabled = true;
1439
+ btn.tabIndex = sameDay(d, focusDate) ? 0 : -1;
1440
+ btn._date = d;
1441
+ btn.addEventListener('click', (function (dd) { return function () { pick(dd); }; })(d));
1442
+ grid.appendChild(btn);
1443
+ }
1444
+ grid.addEventListener('keydown', onGridKey);
1445
+ panel.appendChild(grid);
1446
+ }
1447
+
1448
+ function focusFocusDate() {
1449
+ var cell = panel.querySelector('.fdy-cal__day[tabindex="0"]');
1450
+ if (cell) cell.focus();
1451
+ }
1452
+
1453
+ function moveFocus(next) {
1454
+ focusDate = next;
1455
+ if (next.getMonth() !== view.getMonth() || next.getFullYear() !== view.getFullYear()) {
1456
+ view = new Date(next.getFullYear(), next.getMonth(), 1);
1457
+ }
1458
+ render();
1459
+ focusFocusDate();
1460
+ }
1461
+
1462
+ function onGridKey(e) {
1463
+ var handled = true;
1464
+ switch (e.key) {
1465
+ case 'ArrowLeft': moveFocus(addDays(focusDate, -1)); break;
1466
+ case 'ArrowRight': moveFocus(addDays(focusDate, 1)); break;
1467
+ case 'ArrowUp': moveFocus(addDays(focusDate, -7)); break;
1468
+ case 'ArrowDown': moveFocus(addDays(focusDate, 7)); break;
1469
+ case 'Home': moveFocus(addDays(focusDate, -((focusDate.getDay() + 6) % 7))); break;
1470
+ case 'End': moveFocus(addDays(focusDate, 6 - ((focusDate.getDay() + 6) % 7))); break;
1471
+ case 'PageUp': moveFocus(addMonths(focusDate, e.shiftKey ? -12 : -1)); break;
1472
+ case 'PageDown': moveFocus(addMonths(focusDate, e.shiftKey ? 12 : 1)); break;
1473
+ case 'Enter':
1474
+ case ' ': pick(focusDate); break;
1475
+ case 'Escape': close(true); break;
1476
+ default: handled = false;
1477
+ }
1478
+ if (handled) e.preventDefault();
1479
+ }
1480
+
1481
+ function pick(d) {
1482
+ if (isDisabled(d)) return;
1483
+ selected = startOfDay(d);
1484
+ focusDate = selected;
1485
+ updateDisplay();
1486
+ wrap.dispatchEvent(new CustomEvent('fdy-datepicker-change', {
1487
+ bubbles: true, detail: { value: toISO(selected), date: new Date(selected) }
1488
+ }));
1489
+ close(true);
1490
+ }
1491
+
1492
+ var _pop = null;
1493
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
1494
+ function open() {
1495
+ if (!panel.hidden) return;
1496
+ focusDate = selected || focusDate || new Date();
1497
+ view = new Date(focusDate.getFullYear(), focusDate.getMonth(), 1);
1498
+ var p = popCtl(); if (p) p.show(); else panel.hidden = false;
1499
+ trigger.setAttribute('aria-expanded', 'true');
1500
+ trigger.classList.add('is-open');
1501
+ render();
1502
+ focusFocusDate();
1503
+ document.addEventListener('click', onDocClick, true);
1504
+ }
1505
+
1506
+ function close(returnFocus) {
1507
+ if (panel.hidden) return;
1508
+ var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
1509
+ trigger.setAttribute('aria-expanded', 'false');
1510
+ trigger.classList.remove('is-open');
1511
+ document.removeEventListener('click', onDocClick, true);
1512
+ if (returnFocus === true) trigger.focus();
1513
+ }
1514
+
1515
+ function onDocClick(e) {
1516
+ if (!wrap.contains(e.target)) close(false);
1517
+ }
1518
+
1519
+ trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
1520
+
1521
+ updateDisplay();
1522
+
1523
+ var api = {
1524
+ wrap: wrap,
1525
+ getValue: function () { return selected ? toISO(selected) : ''; },
1526
+ clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
1527
+ setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
1528
+ setMax: function (iso) { maxDate = parseISO(iso); if (!panel.hidden) render(); },
1529
+ setRange: function (fromIso, toIso) {
1530
+ rangeStart = parseISO(fromIso);
1531
+ rangeEnd = parseISO(toIso);
1532
+ if (!panel.hidden) render();
1533
+ }
1534
+ };
1535
+ wrap._fdyDp = api;
1536
+ return api;
1537
+ }
1538
+
1539
+ function initRange(root) {
1540
+ if (root.dataset.fdyDrReady === '1') return;
1541
+ root.dataset.fdyDrReady = '1';
1542
+ var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
1543
+ var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
1544
+ if (!fromEl || !toEl) return;
1545
+ var fromApi = initPicker(fromEl);
1546
+ var toApi = initPicker(toEl);
1547
+ if (!fromApi || !toApi) return;
1548
+
1549
+ function sync() {
1550
+ var f = fromApi.getValue();
1551
+ var t = toApi.getValue();
1552
+ toApi.setMin(f || null); // end can't precede start (one-directional, per Foundation)
1553
+ fromApi.setRange(f, t); // start stays free — picking a later start clears the end below
1554
+ toApi.setRange(f, t);
1555
+ }
1556
+ fromEl.addEventListener('fdy-datepicker-change', function () {
1557
+ var f = fromApi.getValue(), t = toApi.getValue();
1558
+ if (f && t && t < f) toApi.clear();
1559
+ sync();
1560
+ });
1561
+ toEl.addEventListener('fdy-datepicker-change', function () {
1562
+ var f = fromApi.getValue(), t = toApi.getValue();
1563
+ if (f && t && t < f) fromApi.clear();
1564
+ sync();
1565
+ });
1566
+ sync();
1567
+ }
1568
+
1569
+ function initAll(context) {
1570
+ var ctx = context || document;
1571
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-daterange]'), initRange);
1572
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-datepicker]'), initPicker);
1573
+ }
1574
+
1575
+ if (document.readyState === 'loading') {
1576
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1577
+ } else {
1578
+ initAll();
1579
+ }
1580
+
1581
+ window.FreedayDatepicker = { init: initPicker, initAll: initAll };
1582
+ })();
1583
+
1584
+ /* Freeday — datetime picker composer (optional, zero-dependency).
1585
+ * Combines a child date picker and time picker into one value. It does not build
1586
+ * anything itself — freeday-datepicker.js and freeday-timepicker.js enhance the
1587
+ * children; this only listens for their change events and emits a single combined
1588
+ * "fdy-datetime-change" (detail {date, time, value}). value is "YYYY-MM-DDTHH:MM"
1589
+ * when both parts are set, else "". Icon variants are controlled per child picker
1590
+ * (data-fdy-no-icon / <template data-fdy-icon>), so a datetime picker inherits them.
1591
+ *
1592
+ * Markup: <div data-fdy-datetimepicker>
1593
+ * <div data-fdy-datepicker data-value="2026-07-21"></div>
1594
+ * <div data-fdy-timepicker data-value="14:30"></div></div>
1595
+ */
1596
+ (function () {
1597
+ 'use strict';
1598
+
1599
+ function initDatetime(wrap) {
1600
+ if (wrap.dataset.fdyDtReady === '1') return;
1601
+ wrap.dataset.fdyDtReady = '1';
1602
+ wrap.classList.add('fdy-datetimepicker');
1603
+
1604
+ var dp = wrap.querySelector('[data-fdy-datepicker]');
1605
+ var tp = wrap.querySelector('[data-fdy-timepicker]');
1606
+ if (!dp || !tp) return;
1607
+
1608
+ var date = dp.getAttribute('data-value') || '';
1609
+ var time = tp.getAttribute('data-value') || '';
1610
+
1611
+ function emit() {
1612
+ var value = (date && time) ? (date + 'T' + time) : '';
1613
+ wrap.dispatchEvent(new CustomEvent('fdy-datetime-change', {
1614
+ bubbles: true, detail: { date: date, time: time, value: value }
1615
+ }));
1616
+ }
1617
+
1618
+ // Listen for the children's bubbling change events (order-independent — no need
1619
+ // for the child enhancers to have run before this composer).
1620
+ wrap.addEventListener('fdy-datepicker-change', function (e) { date = e.detail.value; emit(); });
1621
+ wrap.addEventListener('fdy-time-select', function (e) { time = e.detail.value; emit(); });
1622
+
1623
+ var api = {
1624
+ wrap: wrap,
1625
+ getValue: function () { return (date && time) ? (date + 'T' + time) : ''; }
1626
+ };
1627
+ wrap._fdyDt = api;
1628
+ return api;
1629
+ }
1630
+
1631
+ function initAll(context) {
1632
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
1633
+ }
1634
+
1635
+ if (document.readyState === 'loading') {
1636
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1637
+ } else {
1638
+ initAll();
1639
+ }
1640
+
1641
+ window.FreedayDatetime = { init: initDatetime, initAll: initAll };
1642
+ })();
1643
+
1644
+ /* Freeday — drawer enhancer (optional, zero-dependency).
1645
+ * A trigger [data-fdy-drawer="drawerId"] opens the <dialog class="fdy-drawer" id="drawerId">
1646
+ * as a modal side panel (native showModal gives focus-trap, Esc, and return-focus). Clicking
1647
+ * the backdrop or any [data-close] inside closes it.
1648
+ */
1649
+ (function () {
1650
+ 'use strict';
1651
+
1652
+ function initDrawer(dialog) {
1653
+ if (dialog.dataset.fdyDrawerReady === '1') return;
1654
+ dialog.dataset.fdyDrawerReady = '1';
1655
+ // Click on the backdrop (target is the dialog box itself) closes it.
1656
+ dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
1657
+ Array.prototype.forEach.call(dialog.querySelectorAll('[data-close]'), function (b) {
1658
+ b.addEventListener('click', function () { dialog.close(); });
1659
+ });
1660
+ }
1661
+
1662
+ function initTriggers(context) {
1663
+ var ctx = context || document;
1664
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), function (btn) {
1665
+ if (btn.dataset.fdyDrawerBound === '1') return;
1666
+ var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
1667
+ if (!dialog) return;
1668
+ initDrawer(dialog);
1669
+ btn.dataset.fdyDrawerBound = '1';
1670
+ btn.addEventListener('click', function () {
1671
+ if (typeof dialog.showModal === 'function') dialog.showModal();
1672
+ else dialog.setAttribute('open', '');
1673
+ });
1674
+ });
1675
+ // Init any drawer dialogs even without a wired trigger (backdrop/close still work).
1676
+ Array.prototype.forEach.call(ctx.querySelectorAll('.fdy-drawer'), initDrawer);
1677
+ }
1678
+
1679
+ if (document.readyState === 'loading') {
1680
+ document.addEventListener('DOMContentLoaded', function () { initTriggers(); });
1681
+ } else {
1682
+ initTriggers();
1683
+ }
1684
+
1685
+ window.FreedayDrawer = { init: initTriggers, initAll: initTriggers };
1686
+ })();
1687
+
1688
+ /* Freeday — form validation enhancer (optional, zero-dependency).
1689
+ * Wires the native Constraint Validation API to accessible inline errors on any
1690
+ * [data-fdy-validate] <form>. Native-first: constraints come from the standard
1691
+ * markup attributes (required, type, pattern, min/max, minlength/maxlength, step);
1692
+ * this only handles presentation + the accessible plumbing.
1693
+ *
1694
+ * Per control it toggles aria-invalid (which the CSS mirrors to the --error border)
1695
+ * and shows the message in a linked [data-fdy-error] element (auto-created inside
1696
+ * the .fdy-field if absent, and wired via aria-describedby).
1697
+ *
1698
+ * Custom messages via data attributes on the control (first match wins):
1699
+ * data-fdy-msg-required | -type | -pattern | -minlength | -maxlength |
1700
+ * data-fdy-msg-min | -max | -step | -mismatch, then generic data-fdy-msg.
1701
+ * Cross-field match (e.g. confirm password): data-fdy-match="#otherId".
1702
+ *
1703
+ * Timing: validates on submit (blocks + focuses the first invalid control); after
1704
+ * the first submit attempt each field re-validates on input/blur so errors clear
1705
+ * live. Add data-fdy-eager to validate on blur from the start.
1706
+ *
1707
+ * Emits bubbling CustomEvents on the form: "fdy-form-invalid" (detail {invalid:[]})
1708
+ * and "fdy-form-valid". Exposes window.FreedayForm.
1709
+ */
1710
+ (function () {
1711
+ 'use strict';
1712
+
1713
+ var seq = 0;
1714
+
1715
+ // ValidityState key -> friendly data-attribute alias.
1716
+ var ALIAS = {
1717
+ valueMissing: 'required',
1718
+ typeMismatch: 'type',
1719
+ patternMismatch: 'pattern',
1720
+ tooShort: 'minlength',
1721
+ tooLong: 'maxlength',
1722
+ rangeUnderflow: 'min',
1723
+ rangeOverflow: 'max',
1724
+ stepMismatch: 'step',
1725
+ badInput: 'type',
1726
+ customError: 'mismatch'
1727
+ };
1728
+ var DEFAULTS = {
1729
+ required: 'Wajib diisi.',
1730
+ type: 'Format tidak valid.',
1731
+ pattern: 'Format tidak sesuai.',
1732
+ minlength: 'Terlalu pendek.',
1733
+ maxlength: 'Terlalu panjang.',
1734
+ min: 'Nilai terlalu kecil.',
1735
+ max: 'Nilai terlalu besar.',
1736
+ step: 'Nilai tidak sesuai kelipatan.',
1737
+ mismatch: 'Nilai tidak cocok.'
1738
+ };
1739
+ var VALIDITY_KEYS = ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooShort', 'tooLong', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput'];
1740
+
1741
+ function isCandidate(el) {
1742
+ if (el.disabled || el.type === 'hidden' || el.type === 'submit' || el.type === 'button' || el.type === 'reset') return false;
1743
+ return typeof el.checkValidity === 'function' && el.willValidate;
1744
+ }
1745
+
1746
+ function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
1747
+
1748
+ function ensureErrorEl(el) {
1749
+ var field = fieldOf(el);
1750
+ var errEl = field ? field.querySelector('[data-fdy-error]') : null;
1751
+ if (!errEl) {
1752
+ errEl = document.createElement('p');
1753
+ errEl.className = 'fdy-help fdy-help--error';
1754
+ errEl.setAttribute('data-fdy-error', '');
1755
+ errEl.hidden = true;
1756
+ (field || el.parentElement).appendChild(errEl);
1757
+ }
1758
+ if (!errEl.id) { seq += 1; errEl.id = 'fdy-err-' + seq; }
1759
+ // Link the control to its message without clobbering existing describedby.
1760
+ var described = (el.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
1761
+ if (described.indexOf(errEl.id) === -1) { described.push(errEl.id); el.setAttribute('aria-describedby', described.join(' ')); }
1762
+ return errEl;
1763
+ }
1764
+
1765
+ function messageFor(el) {
1766
+ // Cross-field match runs first (drives customError via setCustomValidity).
1767
+ el.setCustomValidity('');
1768
+ var matchSel = el.getAttribute('data-fdy-match');
1769
+ if (matchSel) {
1770
+ var other = document.querySelector(matchSel);
1771
+ if (other && el.value !== other.value) {
1772
+ el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || DEFAULTS.mismatch);
1773
+ }
1774
+ }
1775
+ if (el.validity.valid) return '';
1776
+ var alias = el.validity.customError ? 'mismatch' : null;
1777
+ if (!alias) {
1778
+ for (var i = 0; i < VALIDITY_KEYS.length; i++) {
1779
+ if (el.validity[VALIDITY_KEYS[i]]) { alias = ALIAS[VALIDITY_KEYS[i]]; break; }
1780
+ }
1781
+ }
1782
+ var custom = alias ? el.getAttribute('data-fdy-msg-' + alias) : null;
1783
+ return custom || el.getAttribute('data-fdy-msg') || (alias && DEFAULTS[alias]) || el.validationMessage || 'Tidak valid.';
1784
+ }
1785
+
1786
+ function paint(el, message) {
1787
+ var errEl = ensureErrorEl(el);
1788
+ if (message) {
1789
+ el.setAttribute('aria-invalid', 'true');
1790
+ errEl.textContent = message;
1791
+ errEl.hidden = false;
1792
+ } else {
1793
+ el.removeAttribute('aria-invalid');
1794
+ errEl.textContent = '';
1795
+ errEl.hidden = true;
1796
+ }
1797
+ }
1798
+
1799
+ function initForm(form) {
1800
+ if (form.dataset.fdyFormReady === '1') return;
1801
+ form.dataset.fdyFormReady = '1';
1802
+ form.setAttribute('novalidate', '');
1803
+ var eager = form.hasAttribute('data-fdy-eager');
1804
+ var submitted = false;
1805
+
1806
+ function controls() {
1807
+ return Array.prototype.filter.call(form.elements, isCandidate);
1808
+ }
1809
+ function validateOne(el) {
1810
+ var msg = messageFor(el);
1811
+ paint(el, msg);
1812
+ return !msg;
1813
+ }
1814
+ function liveBind(el) {
1815
+ if (el.dataset.fdyFieldBound === '1') return;
1816
+ el.dataset.fdyFieldBound = '1';
1817
+ var live = function () { if (submitted || eager) validateOne(el); };
1818
+ el.addEventListener('blur', live, true);
1819
+ el.addEventListener('input', function () { if ((submitted || eager) && el.getAttribute('aria-invalid') === 'true') validateOne(el); });
1820
+ }
1821
+
1822
+ Array.prototype.forEach.call(controls(), liveBind);
1823
+
1824
+ form.addEventListener('submit', function (e) {
1825
+ submitted = true;
1826
+ var invalid = [];
1827
+ Array.prototype.forEach.call(controls(), function (el) {
1828
+ liveBind(el);
1829
+ if (!validateOne(el)) invalid.push(el);
1830
+ });
1831
+ if (invalid.length) {
1832
+ e.preventDefault();
1833
+ invalid[0].focus();
1834
+ form.dispatchEvent(new CustomEvent('fdy-form-invalid', { bubbles: true, detail: { invalid: invalid } }));
1835
+ } else {
1836
+ form.dispatchEvent(new CustomEvent('fdy-form-valid', { bubbles: true }));
1837
+ }
1838
+ });
1839
+
1840
+ var api = {
1841
+ form: form,
1842
+ validate: function () {
1843
+ submitted = true;
1844
+ var invalid = [];
1845
+ Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
1846
+ return invalid;
1847
+ },
1848
+ reset: function () {
1849
+ submitted = false;
1850
+ Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
1851
+ }
1852
+ };
1853
+ form._fdyForm = api;
1854
+ return api;
1855
+ }
1856
+
1857
+ function initAll(context) {
1858
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
1859
+ }
1860
+
1861
+ if (document.readyState === 'loading') {
1862
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1863
+ } else {
1864
+ initAll();
1865
+ }
1866
+
1867
+ window.FreedayForm = { init: initForm, initAll: initAll };
1868
+ })();
1869
+
1870
+ /* Freeday — input mask + password reveal enhancer (optional, zero-dependency).
1871
+ *
1872
+ * [data-fdy-mask]="pattern" formats a text input as you type. Placeholders:
1873
+ * # a digit (0-9) A a letter (a-z/A-Z) * a letter or digit
1874
+ * Any other pattern char is a literal, inserted automatically
1875
+ * (e.g. "####-####-####", "(###) ###-####", "##/##/####").
1876
+ * The raw (unmasked) value is mirrored to el.dataset.fdyRaw and the bubbling
1877
+ * "fdy-mask" CustomEvent (detail {value, raw}). A native input event is
1878
+ * re-dispatched so validation / framework bindings observe the change.
1879
+ *
1880
+ * [data-fdy-password] on a password <input class="fdy-input"> wraps it in the
1881
+ * input-group chrome and adds a show/hide toggle (type swap + aria-pressed).
1882
+ */
1883
+ (function () {
1884
+ 'use strict';
1885
+
1886
+ var EYE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
1887
+ var EYE_OFF = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17 17 0 0 1-3.2 4M6.1 6.1A17 17 0 0 0 2 12s3.5 7 10 7a9.7 9.7 0 0 0 4-.85"></path><path d="m3 3 18 18"></path></svg>';
1888
+
1889
+ function matches(placeholder, ch) {
1890
+ if (placeholder === '#') return /\d/.test(ch);
1891
+ if (placeholder === 'A') return /[a-zA-Z]/.test(ch);
1892
+ if (placeholder === '*') return /[a-zA-Z0-9]/.test(ch);
1893
+ return false;
1894
+ }
1895
+ function isPlaceholder(ch) { return ch === '#' || ch === 'A' || ch === '*'; }
1896
+
1897
+ // Keep only characters the pattern's placeholders could accept.
1898
+ function rawOf(pattern, value) {
1899
+ var allowDigit = pattern.indexOf('#') !== -1 || pattern.indexOf('*') !== -1;
1900
+ var allowAlpha = pattern.indexOf('A') !== -1 || pattern.indexOf('*') !== -1;
1901
+ var out = '';
1902
+ for (var i = 0; i < value.length; i++) {
1903
+ var c = value[i];
1904
+ if ((allowDigit && /\d/.test(c)) || (allowAlpha && /[a-zA-Z]/.test(c))) out += c;
1905
+ }
1906
+ return out;
1907
+ }
1908
+ function format(pattern, raw) {
1909
+ var out = '', ri = 0;
1910
+ for (var pi = 0; pi < pattern.length && ri < raw.length; pi++) {
1911
+ var p = pattern[pi];
1912
+ if (isPlaceholder(p)) {
1913
+ while (ri < raw.length && !matches(p, raw[ri])) ri++;
1914
+ if (ri < raw.length) { out += raw[ri]; ri++; }
1915
+ } else {
1916
+ out += p;
1917
+ }
1918
+ }
1919
+ return out;
1920
+ }
1921
+
1922
+ function initMask(el) {
1923
+ if (el.dataset.fdyMaskReady === '1') return;
1924
+ el.dataset.fdyMaskReady = '1';
1925
+ var pattern = el.getAttribute('data-fdy-mask') || '';
1926
+ if (!pattern) return;
1927
+ el.setAttribute('inputmode', pattern.indexOf('A') === -1 ? 'numeric' : 'text');
1928
+
1929
+ function apply() {
1930
+ var raw = rawOf(pattern, el.value);
1931
+ var formatted = format(pattern, raw);
1932
+ if (formatted !== el.value) {
1933
+ el.value = formatted;
1934
+ // Masked entry is left-to-right; park the caret at the end.
1935
+ try { el.setSelectionRange(formatted.length, formatted.length); } catch (err) { /* number/date inputs */ }
1936
+ }
1937
+ el.dataset.fdyRaw = raw;
1938
+ el.dispatchEvent(new CustomEvent('fdy-mask', { bubbles: true, detail: { value: formatted, raw: raw } }));
1939
+ }
1940
+ el.addEventListener('input', apply);
1941
+ if (el.value) apply();
1942
+ }
1943
+
1944
+ function initReveal(input) {
1945
+ if (input.dataset.fdyPwReady === '1') return;
1946
+ input.dataset.fdyPwReady = '1';
1947
+
1948
+ var group = document.createElement('span');
1949
+ group.className = 'fdy-input-group';
1950
+ input.parentNode.insertBefore(group, input);
1951
+ group.appendChild(input);
1952
+
1953
+ var btn = document.createElement('button');
1954
+ btn.type = 'button';
1955
+ btn.className = 'fdy-input-group__btn';
1956
+ btn.setAttribute('aria-pressed', 'false');
1957
+ btn.setAttribute('aria-label', 'Tampilkan kata sandi');
1958
+ btn.innerHTML = EYE;
1959
+ btn.addEventListener('click', function () {
1960
+ var reveal = input.type === 'password';
1961
+ input.type = reveal ? 'text' : 'password';
1962
+ btn.setAttribute('aria-pressed', reveal ? 'true' : 'false');
1963
+ btn.setAttribute('aria-label', reveal ? 'Sembunyikan kata sandi' : 'Tampilkan kata sandi');
1964
+ btn.innerHTML = reveal ? EYE_OFF : EYE;
1965
+ input.focus();
1966
+ });
1967
+ group.appendChild(btn);
1968
+ }
1969
+
1970
+ function initAll(context) {
1971
+ var root = context || document;
1972
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-mask]'), initMask);
1973
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-password]'), initReveal);
1974
+ }
1975
+
1976
+ if (document.readyState === 'loading') {
1977
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
1978
+ } else {
1979
+ initAll();
1980
+ }
1981
+
1982
+ window.FreedayMask = { initMask: initMask, initReveal: initReveal, initAll: initAll };
1983
+ })();
1984
+
1985
+ /* Freeday — menu enhancer (optional, zero-dependency).
1986
+ * Menu-button pattern (WAI-ARIA APG): a trigger with aria-haspopup="menu" toggles a
1987
+ * role="menu" popup of role="menuitem" buttons. Also powers the split button
1988
+ * (main action + attached caret toggle). Auto-inits [data-fdy-menu].
1989
+ *
1990
+ * Markup: <div data-fdy-menu><button aria-haspopup="menu" aria-expanded="false">…</button>
1991
+ * <ul class="fdy-menu" role="menu" hidden><li role="none">
1992
+ * <button class="fdy-menu__item" role="menuitem">…</button></li>…</ul></div>
1993
+ * Keyboard: trigger ↓/Enter/Space opens to first item, ↑ opens to last; in the menu
1994
+ * ↑/↓/Home/End move, Esc closes (focus → trigger), Tab closes; activating an item closes.
1995
+ */
1996
+ (function () {
1997
+ 'use strict';
1998
+
1999
+ function menuItems(menu) {
2000
+ return Array.prototype.slice.call(menu.querySelectorAll('[role="menuitem"]'))
2001
+ .filter(function (i) { return !i.disabled && !i.hidden; });
2002
+ }
2003
+
2004
+ function initMenu(wrap) {
2005
+ if (wrap.dataset.fdyMenuReady === '1') return;
2006
+ wrap.dataset.fdyMenuReady = '1';
2007
+ var trigger = wrap.querySelector('[aria-haspopup="menu"]');
2008
+ var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
2009
+ if (!trigger || !menu) return;
2010
+
2011
+ Array.prototype.forEach.call(menu.querySelectorAll('[role="menuitem"]'), function (i) {
2012
+ i.setAttribute('tabindex', '-1');
2013
+ });
2014
+ menu.hidden = true;
2015
+ trigger.setAttribute('aria-expanded', 'false');
2016
+
2017
+ function focusAt(index) {
2018
+ var its = menuItems(menu);
2019
+ if (!its.length) return;
2020
+ its[(index + its.length) % its.length].focus();
2021
+ }
2022
+ var _pop = null;
2023
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(menu, trigger, { matchWidth: false }); return _pop; }
2024
+ function open(index) {
2025
+ var p = popCtl(); if (p) p.show(); else menu.hidden = false;
2026
+ trigger.setAttribute('aria-expanded', 'true');
2027
+ document.addEventListener('click', onDocClick, true);
2028
+ focusAt(index);
2029
+ }
2030
+ function close(returnFocus) {
2031
+ if (menu.hidden) return;
2032
+ var p = popCtl(); if (p) p.hide(); else menu.hidden = true;
2033
+ trigger.setAttribute('aria-expanded', 'false');
2034
+ document.removeEventListener('click', onDocClick, true);
2035
+ if (returnFocus === true) trigger.focus();
2036
+ }
2037
+ function onDocClick(e) { if (!wrap.contains(e.target)) close(false); }
2038
+
2039
+ trigger.addEventListener('click', function (e) {
2040
+ e.stopPropagation();
2041
+ if (menu.hidden) open(0); else close(false);
2042
+ });
2043
+ trigger.addEventListener('keydown', function (e) {
2044
+ if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(0); }
2045
+ else if (e.key === 'ArrowUp') { e.preventDefault(); open(-1); }
2046
+ });
2047
+ menu.addEventListener('keydown', function (e) {
2048
+ var its = menuItems(menu);
2049
+ var cur = its.indexOf(document.activeElement);
2050
+ switch (e.key) {
2051
+ case 'ArrowDown': e.preventDefault(); focusAt(cur + 1); break;
2052
+ case 'ArrowUp': e.preventDefault(); focusAt(cur - 1); break;
2053
+ case 'Home': e.preventDefault(); focusAt(0); break;
2054
+ case 'End': e.preventDefault(); focusAt(its.length - 1); break;
2055
+ case 'Escape': e.preventDefault(); close(true); break;
2056
+ case 'Tab': close(false); break;
2057
+ default: break;
2058
+ }
2059
+ });
2060
+ menu.addEventListener('click', function (e) {
2061
+ var item = e.target.closest('[role="menuitem"]');
2062
+ if (item && !item.disabled) close(true);
2063
+ });
2064
+ }
2065
+
2066
+ function initAll(ctx) {
2067
+ Array.prototype.forEach.call((ctx || document).querySelectorAll('[data-fdy-menu]'), initMenu);
2068
+ }
2069
+
2070
+ if (document.readyState === 'loading') {
2071
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2072
+ } else {
2073
+ initAll();
2074
+ }
2075
+
2076
+ window.FreedayMenu = { init: initMenu, initAll: initAll };
2077
+ })();
2078
+
2079
+ /* Freeday — popover positioning helper (optional, zero-dependency).
2080
+ * Lifts a dropdown panel into the top layer via the native Popover API so it escapes ANY
2081
+ * ancestor overflow clip (a .fdy-card with overflow:hidden, an app-shell main with
2082
+ * overflow:auto, …), then positions it `fixed` against its trigger — flipping above when
2083
+ * there is no room below, and matching the trigger width. The panel stays a DOM child of its
2084
+ * component, so focus, outside-click (.closest), and ARIA relationships keep working unchanged.
2085
+ *
2086
+ * Used by the select/combo, datepicker, timepicker, cascade, autocomplete and menu enhancers;
2087
+ * the freeday/vue adapter ships an equivalent `usePopover` composable.
2088
+ *
2089
+ * attach(panel, trigger) -> { show(), hide() }. show()/hide() also toggle the panel's [hidden]
2090
+ * attribute so callers keep using `!panel.hidden` for open-state and the `:not([hidden])`
2091
+ * entrance animation still fires. On browsers without the Popover API it degrades to that
2092
+ * [hidden] toggle over the panel's existing absolute-positioned CSS (clipped, but functional).
2093
+ */
2094
+ (function () {
2095
+ 'use strict';
2096
+
2097
+ var GAP = 4; // ~ --space-1, breathing room between trigger and panel
2098
+ var supported = typeof HTMLElement !== 'undefined'
2099
+ && typeof HTMLElement.prototype.showPopover === 'function';
2100
+
2101
+ // Position `panel` (already shown, so it has a measurable box) against `trigger`'s rect.
2102
+ // matchWidth (default true) floors the panel to the trigger width — right for input-shaped
2103
+ // dropdowns (combo/cascade/date/time), skipped for menus that keep their own min-width.
2104
+ function place(panel, trigger, matchWidth) {
2105
+ var r = trigger.getBoundingClientRect();
2106
+ panel.style.position = 'fixed';
2107
+ panel.style.margin = '0';
2108
+ panel.style.inset = 'auto'; // clear the UA popover centering
2109
+ if (matchWidth !== false) panel.style.minWidth = r.width + 'px'; // at least as wide as the trigger
2110
+ var ph = panel.offsetHeight, pw = panel.offsetWidth;
2111
+ var vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
2112
+ var below = vh - r.bottom - GAP, above = r.top - GAP;
2113
+ // Prefer below; flip above only when it doesn't fit below and above has more room.
2114
+ var top = (ph <= below || below >= above) ? (r.bottom + GAP) : Math.max(GAP, r.top - GAP - ph);
2115
+ var left = r.left;
2116
+ if (left + pw > vw - GAP) left = Math.max(GAP, vw - GAP - pw); // keep within the viewport
2117
+ panel.style.top = Math.round(top) + 'px';
2118
+ panel.style.left = Math.round(left) + 'px';
2119
+ }
2120
+
2121
+ function attach(panel, trigger, opts) {
2122
+ var matchWidth = !(opts && opts.matchWidth === false);
2123
+ if (supported) panel.setAttribute('popover', 'manual');
2124
+ function reposition() { if (supported && panel.matches(':popover-open')) place(panel, trigger, matchWidth); }
2125
+ return {
2126
+ show: function () {
2127
+ panel.hidden = false;
2128
+ if (supported) {
2129
+ if (!panel.matches(':popover-open')) panel.showPopover();
2130
+ place(panel, trigger, matchWidth);
2131
+ window.addEventListener('scroll', reposition, true); // capture: any scrolling ancestor
2132
+ window.addEventListener('resize', reposition);
2133
+ }
2134
+ },
2135
+ hide: function () {
2136
+ if (supported) {
2137
+ window.removeEventListener('scroll', reposition, true);
2138
+ window.removeEventListener('resize', reposition);
2139
+ if (panel.matches(':popover-open')) panel.hidePopover();
2140
+ }
2141
+ panel.hidden = true;
2142
+ }
2143
+ };
2144
+ }
2145
+
2146
+ window.FreedayPopover = { attach: attach, place: place, supported: supported };
2147
+ })();
2148
+
2149
+ /* Freeday — rating enhancer (optional, zero-dependency).
2150
+ * Fills stars up to the checked radio (and previews on hover) for an interactive
2151
+ * [data-fdy-rating] radio group; for [data-fdy-readonly] it fills from data-value.
2152
+ * The radios are native — arrow-key selection and form association come for free.
2153
+ */
2154
+ (function () {
2155
+ 'use strict';
2156
+
2157
+ function initRating(root) {
2158
+ if (root.dataset.fdyRatingReady === '1') return;
2159
+ root.dataset.fdyRatingReady = '1';
2160
+
2161
+ var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));
2162
+ if (!stars.length) return;
2163
+
2164
+ function fill(n) {
2165
+ stars.forEach(function (s, i) { s.classList.toggle('is-filled', i < n); });
2166
+ }
2167
+
2168
+ if (root.hasAttribute('data-fdy-readonly')) {
2169
+ fill(parseInt(root.getAttribute('data-value'), 10) || 0);
2170
+ return;
2171
+ }
2172
+
2173
+ var inputs = stars.map(function (s) { return s.querySelector('input'); });
2174
+ function current() {
2175
+ for (var i = 0; i < inputs.length; i++) { if (inputs[i] && inputs[i].checked) return i + 1; }
2176
+ return 0;
2177
+ }
2178
+
2179
+ fill(current());
2180
+ root.addEventListener('change', function () { fill(current()); });
2181
+ stars.forEach(function (s, i) {
2182
+ s.addEventListener('mouseenter', function () { fill(i + 1); });
2183
+ });
2184
+ root.addEventListener('mouseleave', function () { fill(current()); });
2185
+ }
2186
+
2187
+ function initAll(context) {
2188
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-rating]'), initRating);
2189
+ }
2190
+
2191
+ if (document.readyState === 'loading') {
2192
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2193
+ } else {
2194
+ initAll();
2195
+ }
2196
+
2197
+ window.FreedayRating = { init: initRating, initAll: initAll };
2198
+ })();
2199
+
2200
+ /* Freeday — combobox enhancer (optional, zero-dependency).
2201
+ * Implements the WAI-ARIA APG "select-only combobox" pattern for every
2202
+ * [data-fdy-combo] on the page. Progressive enhancement: the markup is the
2203
+ * component, this script wires behavior. In a Vue/React/Blazor app you would
2204
+ * bind your own state instead — this is the framework-agnostic reference.
2205
+ *
2206
+ * Expected markup:
2207
+ * <div class="fdy-combo" data-fdy-combo data-value="x">
2208
+ * <button type="button" class="fdy-combo__button" role="combobox"
2209
+ * aria-haspopup="listbox" aria-expanded="false"
2210
+ * aria-labelledby="LBL VAL">
2211
+ * <span class="fdy-combo__value" id="VAL">Label</span>
2212
+ * </button>
2213
+ * <ul class="fdy-combo__listbox" role="listbox" hidden>
2214
+ * <li class="fdy-combo__option" role="option" data-value="x"
2215
+ * aria-selected="true"><span class="fdy-combo__check">✓</span>Label</li>
2216
+ * </ul>
2217
+ * </div>
2218
+ *
2219
+ * Emits a bubbling "fdy-change" CustomEvent on the root with detail.value.
2220
+ */
2221
+ (function () {
2222
+ 'use strict';
2223
+
2224
+ var seq = 0;
2225
+ var openClose = null; // close() of the currently-open combo, or null
2226
+
2227
+ function optionLabel(opt) {
2228
+ var check = opt.querySelector('.fdy-combo__check');
2229
+ var node = check && check.nextSibling ? check.nextSibling.textContent : opt.textContent;
2230
+ return (node || '').trim();
2231
+ }
2232
+
2233
+ function initCombo(root) {
2234
+ if (root.dataset.fdyComboReady === '1') return; // idempotent
2235
+ var button = root.querySelector('.fdy-combo__button');
2236
+ var listbox = root.querySelector('.fdy-combo__listbox');
2237
+ var valueEl = root.querySelector('.fdy-combo__value');
2238
+ var options = Array.prototype.slice.call(root.querySelectorAll('.fdy-combo__option'));
2239
+ if (!button || !listbox || !valueEl || options.length === 0) return;
2240
+ root.dataset.fdyComboReady = '1';
2241
+
2242
+ seq += 1;
2243
+ if (!listbox.id) listbox.id = 'fdy-combo-list-' + seq;
2244
+ button.setAttribute('aria-controls', listbox.id);
2245
+ options.forEach(function (opt, i) {
2246
+ if (!opt.id) opt.id = listbox.id + '-opt-' + i;
2247
+ });
2248
+
2249
+ var typed = '';
2250
+ var typedTimer = null;
2251
+
2252
+ function highlightedIndex() {
2253
+ for (var i = 0; i < options.length; i++) {
2254
+ if (options[i].classList.contains('is-highlighted')) return i;
2255
+ }
2256
+ return -1;
2257
+ }
2258
+ function setHighlight(i) {
2259
+ options.forEach(function (opt, idx) {
2260
+ opt.classList.toggle('is-highlighted', idx === i);
2261
+ });
2262
+ if (i >= 0) {
2263
+ button.setAttribute('aria-activedescendant', options[i].id);
2264
+ options[i].scrollIntoView({ block: 'nearest' });
2265
+ } else {
2266
+ button.removeAttribute('aria-activedescendant');
2267
+ }
2268
+ }
2269
+ function isOpen() {
2270
+ return !listbox.hidden;
2271
+ }
2272
+ var _pop = null;
2273
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, button); return _pop; }
2274
+ function open() {
2275
+ if (openClose && openClose !== close) openClose();
2276
+ var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
2277
+ button.classList.add('is-open');
2278
+ button.setAttribute('aria-expanded', 'true');
2279
+ var selected = options.findIndex(function (opt) {
2280
+ return opt.getAttribute('aria-selected') === 'true';
2281
+ });
2282
+ setHighlight(selected < 0 ? 0 : selected);
2283
+ openClose = close;
2284
+ }
2285
+ function close() {
2286
+ var p = popCtl(); if (p) p.hide(); else listbox.hidden = true;
2287
+ button.classList.remove('is-open');
2288
+ button.setAttribute('aria-expanded', 'false');
2289
+ button.removeAttribute('aria-activedescendant');
2290
+ setHighlight(-1);
2291
+ if (openClose === close) openClose = null;
2292
+ }
2293
+ function choose(opt) {
2294
+ options.forEach(function (other) {
2295
+ var on = other === opt;
2296
+ other.setAttribute('aria-selected', on ? 'true' : 'false');
2297
+ var check = other.querySelector('.fdy-combo__check');
2298
+ if (check) check.textContent = on ? '✓' : '';
2299
+ });
2300
+ root.setAttribute('data-value', opt.getAttribute('data-value') || '');
2301
+ valueEl.textContent = optionLabel(opt);
2302
+ valueEl.classList.remove('fdy-combo__value--placeholder');
2303
+ root.dispatchEvent(new CustomEvent('fdy-change', {
2304
+ bubbles: true,
2305
+ detail: { value: opt.getAttribute('data-value') }
2306
+ }));
2307
+ close();
2308
+ button.focus();
2309
+ }
2310
+
2311
+ button.addEventListener('click', function () {
2312
+ isOpen() ? close() : open();
2313
+ });
2314
+ // Pressing an option must not blur the button: a blur fires the focusout
2315
+ // handler below, which closes the listbox before the option's click lands —
2316
+ // so the pick is lost (real mouse only; synthetic .click() never blurs).
2317
+ // preventDefault on mousedown keeps focus on the button without blocking click.
2318
+ listbox.addEventListener('mousedown', function (e) { e.preventDefault(); });
2319
+ options.forEach(function (opt, i) {
2320
+ opt.addEventListener('click', function () { choose(opt); });
2321
+ opt.addEventListener('mousemove', function () { setHighlight(i); });
2322
+ });
2323
+
2324
+ root.addEventListener('keydown', function (e) {
2325
+ var hi = highlightedIndex();
2326
+ switch (e.key) {
2327
+ case 'ArrowDown':
2328
+ e.preventDefault();
2329
+ isOpen() ? setHighlight((hi + 1) % options.length) : open();
2330
+ break;
2331
+ case 'ArrowUp':
2332
+ e.preventDefault();
2333
+ isOpen() ? setHighlight((hi - 1 + options.length) % options.length) : open();
2334
+ break;
2335
+ case 'Home':
2336
+ if (isOpen()) { e.preventDefault(); setHighlight(0); }
2337
+ break;
2338
+ case 'End':
2339
+ if (isOpen()) { e.preventDefault(); setHighlight(options.length - 1); }
2340
+ break;
2341
+ case 'Enter':
2342
+ case ' ':
2343
+ e.preventDefault();
2344
+ if (isOpen() && hi >= 0) choose(options[hi]); else open();
2345
+ break;
2346
+ case 'Escape':
2347
+ if (isOpen()) { e.preventDefault(); close(); button.focus(); }
2348
+ break;
2349
+ case 'Tab':
2350
+ if (isOpen()) close();
2351
+ break;
2352
+ default:
2353
+ if (e.key.length === 1 && /\S/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) {
2354
+ typed += e.key.toLowerCase();
2355
+ clearTimeout(typedTimer);
2356
+ typedTimer = setTimeout(function () { typed = ''; }, 500);
2357
+ var match = options.findIndex(function (opt) {
2358
+ return optionLabel(opt).toLowerCase().indexOf(typed) === 0;
2359
+ });
2360
+ if (match >= 0) { if (!isOpen()) open(); setHighlight(match); }
2361
+ }
2362
+ }
2363
+ });
2364
+
2365
+ // Close when focus leaves the whole combo (e.g. Shift+Tab off the button).
2366
+ root.addEventListener('focusout', function (e) {
2367
+ if (!root.contains(e.relatedTarget)) close();
2368
+ });
2369
+ }
2370
+
2371
+ function initAll(context) {
2372
+ var scope = context || document;
2373
+ Array.prototype.forEach.call(
2374
+ scope.querySelectorAll('[data-fdy-combo]'),
2375
+ initCombo
2376
+ );
2377
+ }
2378
+
2379
+ // One global handler closes the open combo on an outside click.
2380
+ document.addEventListener('click', function (e) {
2381
+ if (openClose && !(e.target.closest && e.target.closest('.fdy-combo'))) {
2382
+ openClose();
2383
+ }
2384
+ });
2385
+
2386
+ if (document.readyState === 'loading') {
2387
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2388
+ } else {
2389
+ initAll();
2390
+ }
2391
+
2392
+ window.FreedayCombo = { init: initCombo, initAll: initAll };
2393
+ })();
2394
+
2395
+ /* Freeday — slider value binding (optional, zero-dependency).
2396
+ * Mirrors a [data-fdy-slider] range input's value into <output for="inputId">, with an
2397
+ * optional prefix/suffix on the output (data-fdy-prefix / data-fdy-suffix). The range itself
2398
+ * is a native input — keyboard and ARIA are built in; this only updates the visible readout.
2399
+ */
2400
+ (function () {
2401
+ 'use strict';
2402
+
2403
+ function formatted(out, value) {
2404
+ return (out.getAttribute('data-fdy-prefix') || '') + value + (out.getAttribute('data-fdy-suffix') || '');
2405
+ }
2406
+
2407
+ function initSlider(input) {
2408
+ if (input.dataset.fdySliderReady === '1') return;
2409
+ input.dataset.fdySliderReady = '1';
2410
+ var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
2411
+ if (!out) return;
2412
+ function sync() { out.textContent = formatted(out, input.value); }
2413
+ input.addEventListener('input', sync);
2414
+ sync();
2415
+ }
2416
+
2417
+ function initAll(context) {
2418
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-slider]'), initSlider);
2419
+ }
2420
+
2421
+ if (document.readyState === 'loading') {
2422
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2423
+ } else {
2424
+ initAll();
2425
+ }
2426
+
2427
+ window.FreedaySlider = { init: initSlider, initAll: initAll };
2428
+ })();
2429
+
2430
+ /* Freeday — stepper/wizard enhancer (optional, zero-dependency).
2431
+ * Drives a step header + panels: prev/next navigation, completed/active state, and
2432
+ * clicking a reached step to revisit it (linear — can't skip ahead). Auto-inits
2433
+ * [data-fdy-stepper].
2434
+ *
2435
+ * Markup: <div data-fdy-stepper>
2436
+ * <ol class="fdy-stepper">
2437
+ * <li class="fdy-step"><button class="fdy-step__btn"><span class="fdy-step__marker">1</span>
2438
+ * <span class="fdy-step__label">…</span></button></li>… </ol>
2439
+ * <div class="fdy-step-panels">
2440
+ * <div class="fdy-step-panel">…</div>… </div>
2441
+ * <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
2442
+ * <button data-fdy-step-next>…</button></div></div>
2443
+ * Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step).
2444
+ */
2445
+ (function () {
2446
+ 'use strict';
2447
+
2448
+ var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>';
2449
+
2450
+ function initStepper(root) {
2451
+ if (root.dataset.fdyStepperReady === '1') return;
2452
+ root.dataset.fdyStepperReady = '1';
2453
+
2454
+ var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));
2455
+ var panels = Array.prototype.slice.call(root.querySelectorAll('.fdy-step-panel'));
2456
+ var prevBtn = root.querySelector('[data-fdy-step-prev]');
2457
+ var nextBtn = root.querySelector('[data-fdy-step-next]');
2458
+ if (!steps.length) return;
2459
+
2460
+ var active = 0;
2461
+ for (var i = 0; i < steps.length; i++) {
2462
+ if (steps[i].classList.contains('is-active')) { active = i; break; }
2463
+ }
2464
+ var maxReached = active;
2465
+ var nextLabel = nextBtn ? nextBtn.textContent : '';
2466
+
2467
+ steps.forEach(function (s) {
2468
+ var marker = s.querySelector('.fdy-step__marker');
2469
+ if (marker && marker.dataset.num == null) marker.dataset.num = marker.textContent.trim();
2470
+ });
2471
+
2472
+ function render() {
2473
+ steps.forEach(function (s, i) {
2474
+ s.classList.toggle('is-active', i === active);
2475
+ s.classList.toggle('is-complete', i < active);
2476
+ if (i === active) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
2477
+ var marker = s.querySelector('.fdy-step__marker');
2478
+ if (marker) marker.innerHTML = i < active ? CHECK : (marker.dataset.num || String(i + 1));
2479
+ var btn = s.querySelector('.fdy-step__btn');
2480
+ if (btn) btn.disabled = i > maxReached;
2481
+ });
2482
+ panels.forEach(function (p, i) { p.hidden = i !== active; });
2483
+ if (prevBtn) prevBtn.disabled = active === 0;
2484
+ if (nextBtn) nextBtn.textContent = active === steps.length - 1 ? 'Selesai' : (nextLabel || 'Lanjut');
2485
+ root.dispatchEvent(new CustomEvent('fdy-step-change', { bubbles: true, detail: { index: active } }));
2486
+ }
2487
+
2488
+ function go(i) {
2489
+ if (i < 0 || i >= steps.length) return;
2490
+ active = i;
2491
+ if (active > maxReached) maxReached = active;
2492
+ render();
2493
+ }
2494
+
2495
+ if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
2496
+ if (nextBtn) nextBtn.addEventListener('click', function () {
2497
+ if (active < steps.length - 1) go(active + 1);
2498
+ else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
2499
+ });
2500
+ steps.forEach(function (s, i) {
2501
+ var btn = s.querySelector('.fdy-step__btn');
2502
+ if (btn) btn.addEventListener('click', function () { if (i <= maxReached) go(i); });
2503
+ });
2504
+
2505
+ render();
2506
+ }
2507
+
2508
+ function initAll(context) {
2509
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-stepper]'), initStepper);
2510
+ }
2511
+
2512
+ if (document.readyState === 'loading') {
2513
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2514
+ } else {
2515
+ initAll();
2516
+ }
2517
+
2518
+ window.FreedayStepper = { init: initStepper, initAll: initAll };
2519
+ })();
2520
+
2521
+ /* Freeday — data-table enhancer (optional, zero-dependency).
2522
+ * Adds client-side search, column sort, pagination and row selection to any
2523
+ * [data-fdy-table]. Progressive enhancement over a plain semantic <table>;
2524
+ * in a framework app, do this server- or store-side instead.
2525
+ *
2526
+ * Markup contract (see docs): a `.fdy-datatable[data-fdy-table]` containing a
2527
+ * `.fdy-table-toolbar` (with `[data-fdy-table-search]`, `[data-fdy-table-count]`),
2528
+ * a `<table class="fdy-table">` whose sortable headers hold
2529
+ * `<button class="fdy-table__sortbtn" data-fdy-sort[="number"]>`, optional
2530
+ * selection checkboxes (`[data-fdy-select-all]`, `[data-fdy-row-select]`), and a
2531
+ * `.fdy-table-footer` (with `[data-fdy-table-info]`, `[data-fdy-table-pagination]`).
2532
+ * Numeric columns should give each cell a `data-sort-value`.
2533
+ *
2534
+ * Column filters: mark a header `<th data-fdy-filter="text|enum|number">` and a funnel
2535
+ * button is injected; text filters by contains, enum by a value checklist, number by a
2536
+ * min/max range. All active column filters AND the global search must pass (logical AND).
2537
+ *
2538
+ * Bulk actions: an optional `[data-fdy-table-bulk]` bar (hidden by default) with a
2539
+ * `[data-fdy-table-bulk-count]` label and a `[data-fdy-table-bulk-clear]` button shows
2540
+ * while any row is selected.
2541
+ *
2542
+ * Emits a bubbling "fdy-table-change" CustomEvent (detail: {shown, total, page}).
2543
+ */
2544
+ (function () {
2545
+ 'use strict';
2546
+
2547
+ function cellText(row, index) {
2548
+ var cell = row.cells[index];
2549
+ if (!cell) return '';
2550
+ return (cell.getAttribute('data-sort-value') != null
2551
+ ? cell.getAttribute('data-sort-value')
2552
+ : cell.textContent).trim();
2553
+ }
2554
+
2555
+ function initTable(root) {
2556
+ if (root.dataset.fdyTableReady === '1') return;
2557
+ var table = root.querySelector('table.fdy-table');
2558
+ var tbody = table && table.querySelector('tbody');
2559
+ if (!table || !tbody) return;
2560
+ root.dataset.fdyTableReady = '1';
2561
+
2562
+ var allRows = Array.prototype.slice.call(tbody.querySelectorAll(':scope > tr'));
2563
+ var searchInput = root.querySelector('[data-fdy-table-search]');
2564
+ var countEl = root.querySelector('[data-fdy-table-count]');
2565
+ var infoEl = root.querySelector('[data-fdy-table-info]');
2566
+ var pagerEl = root.querySelector('[data-fdy-table-pagination]');
2567
+ var selectAll = root.querySelector('[data-fdy-select-all]');
2568
+ var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
2569
+
2570
+ var sortButtons = Array.prototype.slice.call(root.querySelectorAll('.fdy-table__sortbtn[data-fdy-sort]'));
2571
+ var sortCol = -1; // column index currently sorted
2572
+ var sortDir = 0; // 1 asc, -1 desc
2573
+ var sortType = 'text';
2574
+ var filter = '';
2575
+ var page = 1;
2576
+
2577
+ var colFilters = {}; // colIndex -> {type, text, set, min, max}
2578
+ var openPopover = null;
2579
+ var bulkBar = root.querySelector('[data-fdy-table-bulk]');
2580
+ var bulkCount = root.querySelector('[data-fdy-table-bulk-count]');
2581
+ var bulkClear = root.querySelector('[data-fdy-table-bulk-clear]');
2582
+
2583
+ function colIndexOf(button) {
2584
+ var th = button.closest('th');
2585
+ return th ? th.cellIndex : -1;
2586
+ }
2587
+
2588
+ function parseNum(s) {
2589
+ var n = parseFloat(String(s).replace(/[^\d.-]/g, ''));
2590
+ return isNaN(n) ? null : n;
2591
+ }
2592
+
2593
+ function passesColumnFilters(row) {
2594
+ for (var idx in colFilters) {
2595
+ if (!Object.prototype.hasOwnProperty.call(colFilters, idx)) continue;
2596
+ var f = colFilters[idx];
2597
+ var raw = cellText(row, Number(idx));
2598
+ if (f.type === 'text') {
2599
+ if (f.text && raw.toLowerCase().indexOf(f.text.toLowerCase()) === -1) return false;
2600
+ } else if (f.type === 'enum') {
2601
+ if (f.set && f.set.size && !f.set.has(raw)) return false;
2602
+ } else if (f.type === 'number') {
2603
+ var v = parseNum(raw);
2604
+ if (f.min != null && (v == null || v < f.min)) return false;
2605
+ if (f.max != null && (v == null || v > f.max)) return false;
2606
+ }
2607
+ }
2608
+ return true;
2609
+ }
2610
+
2611
+ function filtered() {
2612
+ var q = filter.toLowerCase();
2613
+ return allRows.filter(function (row) {
2614
+ if (q && row.textContent.toLowerCase().indexOf(q) === -1) return false;
2615
+ return passesColumnFilters(row);
2616
+ });
2617
+ }
2618
+
2619
+ function sorted(rows) {
2620
+ if (sortCol < 0 || sortDir === 0) return rows;
2621
+ var copy = rows.slice();
2622
+ copy.sort(function (a, b) {
2623
+ var av = cellText(a, sortCol);
2624
+ var bv = cellText(b, sortCol);
2625
+ var cmp;
2626
+ if (sortType === 'number') {
2627
+ cmp = (parseFloat(av.replace(/[^\d.-]/g, '')) || 0) - (parseFloat(bv.replace(/[^\d.-]/g, '')) || 0);
2628
+ } else {
2629
+ cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
2630
+ }
2631
+ return cmp * sortDir;
2632
+ });
2633
+ return copy;
2634
+ }
2635
+
2636
+ function syncSelectAll(visibleRows) {
2637
+ if (!selectAll) return;
2638
+ var boxes = visibleRows
2639
+ .map(function (r) { return r.querySelector('[data-fdy-row-select]'); })
2640
+ .filter(Boolean);
2641
+ var checked = boxes.filter(function (b) { return b.checked; }).length;
2642
+ selectAll.checked = boxes.length > 0 && checked === boxes.length;
2643
+ selectAll.indeterminate = checked > 0 && checked < boxes.length;
2644
+ }
2645
+
2646
+ function buildPager(totalPages) {
2647
+ if (!pagerEl) return;
2648
+ pagerEl.innerHTML = '';
2649
+ if (totalPages <= 1) return;
2650
+ var ul = document.createElement('ul');
2651
+ ul.className = 'fdy-pagination__list';
2652
+
2653
+ function addBtn(label, targetPage, opts) {
2654
+ opts = opts || {};
2655
+ var li = document.createElement('li');
2656
+ var el = document.createElement(opts.disabled || opts.current ? 'span' : 'button');
2657
+ el.className = 'fdy-pagination__link';
2658
+ el.textContent = label;
2659
+ if (opts.disabled) el.setAttribute('aria-disabled', 'true');
2660
+ if (opts.current) el.setAttribute('aria-current', 'page');
2661
+ if (opts.label) el.setAttribute('aria-label', opts.label);
2662
+ if (!opts.disabled && !opts.current) {
2663
+ el.type = 'button';
2664
+ el.addEventListener('click', function () { page = targetPage; render(); });
2665
+ }
2666
+ li.appendChild(el);
2667
+ ul.appendChild(li);
2668
+ }
2669
+ function addEllipsis() {
2670
+ var li = document.createElement('li');
2671
+ var sp = document.createElement('span');
2672
+ sp.className = 'fdy-pagination__ellipsis';
2673
+ sp.textContent = '…';
2674
+ li.appendChild(sp);
2675
+ ul.appendChild(li);
2676
+ }
2677
+
2678
+ addBtn('‹', page - 1, { disabled: page === 1, label: 'Sebelumnya' });
2679
+ var pages = [];
2680
+ for (var p = 1; p <= totalPages; p++) {
2681
+ if (p === 1 || p === totalPages || (p >= page - 1 && p <= page + 1)) pages.push(p);
2682
+ else if (pages[pages.length - 1] !== '…') pages.push('…');
2683
+ }
2684
+ pages.forEach(function (p) {
2685
+ if (p === '…') addEllipsis();
2686
+ else addBtn(String(p), p, { current: p === page });
2687
+ });
2688
+ addBtn('›', page + 1, { disabled: page === totalPages, label: 'Berikutnya' });
2689
+ pagerEl.appendChild(ul);
2690
+ }
2691
+
2692
+ // --- Bulk-action bar ---------------------------------------------------
2693
+ function selectedRows() {
2694
+ return allRows.filter(function (r) {
2695
+ var b = r.querySelector('[data-fdy-row-select]');
2696
+ return b && b.checked;
2697
+ });
2698
+ }
2699
+ function updateBulk() {
2700
+ if (!bulkBar) return;
2701
+ var n = selectedRows().length;
2702
+ bulkBar.hidden = n === 0;
2703
+ if (bulkCount) bulkCount.textContent = n + ' dipilih';
2704
+ }
2705
+ function clearSelection() {
2706
+ allRows.forEach(function (r) {
2707
+ var b = r.querySelector('[data-fdy-row-select]');
2708
+ if (b) b.checked = false;
2709
+ r.setAttribute('aria-selected', 'false');
2710
+ });
2711
+ if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
2712
+ updateBulk();
2713
+ }
2714
+
2715
+ // --- Column filters ----------------------------------------------------
2716
+ function closePopover(returnFocus) {
2717
+ if (!openPopover) return;
2718
+ var btn = openPopover._trigger;
2719
+ openPopover.remove();
2720
+ openPopover = null;
2721
+ window.removeEventListener('scroll', closeOnScroll, true);
2722
+ window.removeEventListener('resize', closeOnScroll);
2723
+ if (returnFocus === true && btn) btn.focus(); // only on keyboard/button dismissal
2724
+ }
2725
+ function closeOnScroll() { closePopover(false); }
2726
+ function markActive(button, active) {
2727
+ button.classList.toggle('is-active', active);
2728
+ button.setAttribute('aria-pressed', String(active));
2729
+ }
2730
+ function filterTitle(text) {
2731
+ var t = document.createElement('div');
2732
+ t.className = 'fdy-filter__title';
2733
+ t.textContent = text;
2734
+ return t;
2735
+ }
2736
+ function numberInput(placeholder, value) {
2737
+ var i = document.createElement('input');
2738
+ i.className = 'fdy-input';
2739
+ i.type = 'text';
2740
+ i.inputMode = 'numeric';
2741
+ i.placeholder = placeholder;
2742
+ i.value = value != null ? value : '';
2743
+ return i;
2744
+ }
2745
+ function distinctValues(idx) {
2746
+ var seen = {};
2747
+ var out = [];
2748
+ allRows.forEach(function (r) {
2749
+ var v = cellText(r, idx);
2750
+ if (v && !seen[v]) { seen[v] = 1; out.push(v); }
2751
+ });
2752
+ out.sort(function (a, b) { return a.localeCompare(b, undefined, { numeric: true }); });
2753
+ return out;
2754
+ }
2755
+ function openFilterPopover(type, idx, btn) {
2756
+ var pop = document.createElement('div');
2757
+ pop.className = 'fdy-filter';
2758
+ pop.setAttribute('role', 'dialog');
2759
+ pop.setAttribute('aria-label', 'Filter kolom');
2760
+ var f = colFilters[idx] || { type: type };
2761
+
2762
+ if (type === 'text') {
2763
+ pop.appendChild(filterTitle('Berisi teks'));
2764
+ var inp = document.createElement('input');
2765
+ inp.className = 'fdy-input';
2766
+ inp.type = 'search';
2767
+ inp.placeholder = 'Berisi…';
2768
+ inp.value = f.text || '';
2769
+ inp.addEventListener('input', function () {
2770
+ f.text = inp.value.trim();
2771
+ colFilters[idx] = f;
2772
+ markActive(btn, !!f.text);
2773
+ page = 1; render();
2774
+ });
2775
+ pop.appendChild(inp);
2776
+ } else if (type === 'enum') {
2777
+ pop.appendChild(filterTitle('Tampilkan nilai'));
2778
+ var list = document.createElement('div');
2779
+ list.className = 'fdy-filter__list';
2780
+ var set = f.set || new Set();
2781
+ distinctValues(idx).forEach(function (val) {
2782
+ var lab = document.createElement('label');
2783
+ lab.className = 'fdy-filter__check';
2784
+ var cb = document.createElement('input');
2785
+ cb.type = 'checkbox';
2786
+ cb.className = 'fdy-checkbox';
2787
+ cb.checked = set.has(val);
2788
+ cb.addEventListener('change', function () {
2789
+ if (cb.checked) set.add(val); else set.delete(val);
2790
+ f.set = set; colFilters[idx] = f;
2791
+ markActive(btn, set.size > 0);
2792
+ page = 1; render();
2793
+ });
2794
+ lab.appendChild(cb);
2795
+ lab.appendChild(document.createTextNode(val));
2796
+ list.appendChild(lab);
2797
+ });
2798
+ pop.appendChild(list);
2799
+ } else if (type === 'number') {
2800
+ pop.appendChild(filterTitle('Rentang nilai'));
2801
+ var range = document.createElement('div');
2802
+ range.className = 'fdy-filter__range';
2803
+ var minI = numberInput('Min', f.min);
2804
+ var maxI = numberInput('Maks', f.max);
2805
+ var applyRange = function () {
2806
+ f.min = minI.value !== '' ? parseNum(minI.value) : null;
2807
+ f.max = maxI.value !== '' ? parseNum(maxI.value) : null;
2808
+ colFilters[idx] = f;
2809
+ markActive(btn, f.min != null || f.max != null);
2810
+ page = 1; render();
2811
+ };
2812
+ minI.addEventListener('input', applyRange);
2813
+ maxI.addEventListener('input', applyRange);
2814
+ var sep = document.createElement('span');
2815
+ sep.textContent = '–';
2816
+ range.appendChild(minI); range.appendChild(sep); range.appendChild(maxI);
2817
+ pop.appendChild(range);
2818
+ }
2819
+
2820
+ var foot = document.createElement('div');
2821
+ foot.className = 'fdy-filter__foot';
2822
+ var reset = document.createElement('button');
2823
+ reset.type = 'button';
2824
+ reset.className = 'fdy-btn fdy-btn--ghost fdy-btn--sm';
2825
+ reset.textContent = 'Reset';
2826
+ reset.addEventListener('click', function () {
2827
+ delete colFilters[idx];
2828
+ markActive(btn, false);
2829
+ page = 1; render();
2830
+ closePopover(true);
2831
+ });
2832
+ var done = document.createElement('button');
2833
+ done.type = 'button';
2834
+ done.className = 'fdy-btn fdy-btn--sm';
2835
+ done.textContent = 'Tutup';
2836
+ done.addEventListener('click', function () { closePopover(true); });
2837
+ foot.appendChild(reset); foot.appendChild(done);
2838
+ pop.appendChild(foot);
2839
+
2840
+ document.body.appendChild(pop);
2841
+ pop._trigger = btn;
2842
+ var rect = btn.getBoundingClientRect();
2843
+ pop.style.top = (rect.bottom + 4) + 'px';
2844
+ pop.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - pop.offsetWidth - 8)) + 'px';
2845
+ openPopover = pop;
2846
+ window.addEventListener('scroll', closeOnScroll, true);
2847
+ window.addEventListener('resize', closeOnScroll);
2848
+ var firstControl = pop.querySelector('input, button');
2849
+ if (firstControl) firstControl.focus();
2850
+ }
2851
+ function buildColumnFilters() {
2852
+ Array.prototype.forEach.call(table.querySelectorAll('thead th[data-fdy-filter]'), function (th) {
2853
+ var type = th.getAttribute('data-fdy-filter');
2854
+ var idx = th.cellIndex;
2855
+ var btn = document.createElement('button');
2856
+ btn.type = 'button';
2857
+ btn.className = 'fdy-table__filterbtn';
2858
+ btn.setAttribute('aria-haspopup', 'dialog');
2859
+ btn.setAttribute('aria-pressed', 'false');
2860
+ btn.setAttribute('aria-label', 'Filter kolom');
2861
+ 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>';
2862
+ btn.addEventListener('click', function (e) {
2863
+ e.stopPropagation();
2864
+ var wasOpen = openPopover && openPopover._trigger === btn;
2865
+ closePopover();
2866
+ if (!wasOpen) openFilterPopover(type, idx, btn);
2867
+ });
2868
+ th.appendChild(btn);
2869
+ });
2870
+ }
2871
+
2872
+ function render() {
2873
+ var view = sorted(filtered());
2874
+ var total = view.length;
2875
+ var totalPages = pageSize ? Math.max(1, Math.ceil(total / pageSize)) : 1;
2876
+ if (page > totalPages) page = totalPages;
2877
+ if (page < 1) page = 1;
2878
+
2879
+ var start = pageSize ? (page - 1) * pageSize : 0;
2880
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
2881
+
2882
+ // Hide every row, then append the visible slice in order (reorders DOM).
2883
+ allRows.forEach(function (row) { row.style.display = 'none'; });
2884
+ slice.forEach(function (row) {
2885
+ row.style.display = '';
2886
+ tbody.appendChild(row);
2887
+ });
2888
+
2889
+ var shownFrom = total === 0 ? 0 : start + 1;
2890
+ var shownTo = start + slice.length;
2891
+ if (countEl) countEl.textContent = total + ' baris';
2892
+ if (infoEl) infoEl.textContent = 'Menampilkan ' + shownFrom + '–' + shownTo + ' dari ' + total;
2893
+ buildPager(totalPages);
2894
+ syncSelectAll(slice);
2895
+ updateBulk();
2896
+
2897
+ root.dispatchEvent(new CustomEvent('fdy-table-change', {
2898
+ bubbles: true,
2899
+ detail: { shown: slice.length, total: total, page: page }
2900
+ }));
2901
+ }
2902
+
2903
+ sortButtons.forEach(function (button) {
2904
+ button.addEventListener('click', function () {
2905
+ var index = colIndexOf(button);
2906
+ if (index < 0) return;
2907
+ var th = button.closest('th');
2908
+ if (sortCol === index) {
2909
+ sortDir = sortDir === 1 ? -1 : 1;
2910
+ } else {
2911
+ sortCol = index;
2912
+ sortDir = 1;
2913
+ sortType = button.getAttribute('data-fdy-sort') === 'number' ? 'number' : 'text';
2914
+ }
2915
+ // Reset all header sort indicators, set the active one.
2916
+ sortButtons.forEach(function (b) {
2917
+ var h = b.closest('th');
2918
+ if (h) h.removeAttribute('aria-sort');
2919
+ });
2920
+ if (th) th.setAttribute('aria-sort', sortDir === 1 ? 'ascending' : 'descending');
2921
+ page = 1;
2922
+ render();
2923
+ });
2924
+ });
2925
+
2926
+ if (searchInput) {
2927
+ searchInput.addEventListener('input', function () {
2928
+ filter = searchInput.value.trim();
2929
+ page = 1;
2930
+ render();
2931
+ });
2932
+ }
2933
+
2934
+ if (selectAll) {
2935
+ selectAll.addEventListener('change', function () {
2936
+ var start = pageSize ? (page - 1) * pageSize : 0;
2937
+ var view = sorted(filtered());
2938
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
2939
+ slice.forEach(function (row) {
2940
+ var box = row.querySelector('[data-fdy-row-select]');
2941
+ if (box) {
2942
+ box.checked = selectAll.checked;
2943
+ row.setAttribute('aria-selected', selectAll.checked ? 'true' : 'false');
2944
+ }
2945
+ });
2946
+ selectAll.indeterminate = false;
2947
+ updateBulk();
2948
+ });
2949
+ }
2950
+
2951
+ tbody.addEventListener('change', function (e) {
2952
+ var box = e.target.closest('[data-fdy-row-select]');
2953
+ if (!box) return;
2954
+ var row = box.closest('tr');
2955
+ if (row) row.setAttribute('aria-selected', box.checked ? 'true' : 'false');
2956
+ var view = sorted(filtered());
2957
+ var start = pageSize ? (page - 1) * pageSize : 0;
2958
+ var slice = pageSize ? view.slice(start, start + pageSize) : view;
2959
+ syncSelectAll(slice);
2960
+ updateBulk();
2961
+ });
2962
+
2963
+ if (bulkClear) bulkClear.addEventListener('click', clearSelection);
2964
+
2965
+ // Dismiss an open filter popover on outside click or Escape.
2966
+ document.addEventListener('click', function (e) {
2967
+ if (!openPopover) return;
2968
+ var t = openPopover._trigger;
2969
+ if (!openPopover.contains(e.target) && e.target !== t && (!t || !t.contains(e.target))) {
2970
+ closePopover();
2971
+ }
2972
+ });
2973
+ document.addEventListener('keydown', function (e) {
2974
+ if (e.key === 'Escape' && openPopover) closePopover(true);
2975
+ });
2976
+
2977
+ buildColumnFilters();
2978
+ render();
2979
+ }
2980
+
2981
+ function initAll(context) {
2982
+ Array.prototype.forEach.call(
2983
+ (context || document).querySelectorAll('[data-fdy-table]'),
2984
+ initTable
2985
+ );
2986
+ }
2987
+
2988
+ if (document.readyState === 'loading') {
2989
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
2990
+ } else {
2991
+ initAll();
2992
+ }
2993
+
2994
+ window.FreedayTable = { init: initTable, initAll: initAll };
2995
+ })();
2996
+
2997
+ /* Freeday — tabs enhancer (optional, zero-dependency, WAI-ARIA APG tabs).
2998
+ * Auto-inits every [data-fdy-tabs]. Roving tabindex + arrow/Home/End nav.
2999
+ *
3000
+ * Markup: a [data-fdy-tabs] with a [role="tablist"] of
3001
+ * <button role="tab" aria-controls="PANEL" aria-selected> and matching
3002
+ * <div role="tabpanel" id="PANEL" aria-labelledby="TAB" [hidden]>.
3003
+ */
3004
+ (function () {
3005
+ 'use strict';
3006
+
3007
+ function initTabs(root) {
3008
+ if (root.dataset.fdyTabsReady === '1') return;
3009
+ var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
3010
+ if (tabs.length === 0) return;
3011
+ root.dataset.fdyTabsReady = '1';
3012
+
3013
+ function panelFor(tab) {
3014
+ var id = tab.getAttribute('aria-controls');
3015
+ return id ? document.getElementById(id) : null;
3016
+ }
3017
+ function select(tab, focus) {
3018
+ tabs.forEach(function (t) {
3019
+ var on = t === tab;
3020
+ t.setAttribute('aria-selected', on ? 'true' : 'false');
3021
+ t.tabIndex = on ? 0 : -1;
3022
+ var panel = panelFor(t);
3023
+ if (panel) panel.hidden = !on;
3024
+ });
3025
+ if (focus) tab.focus();
3026
+ }
3027
+
3028
+ tabs.forEach(function (tab, i) {
3029
+ tab.addEventListener('click', function () { select(tab, false); });
3030
+ tab.addEventListener('keydown', function (e) {
3031
+ switch (e.key) {
3032
+ case 'ArrowRight':
3033
+ e.preventDefault(); select(tabs[(i + 1) % tabs.length], true); break;
3034
+ case 'ArrowLeft':
3035
+ e.preventDefault(); select(tabs[(i - 1 + tabs.length) % tabs.length], true); break;
3036
+ case 'Home':
3037
+ e.preventDefault(); select(tabs[0], true); break;
3038
+ case 'End':
3039
+ e.preventDefault(); select(tabs[tabs.length - 1], true); break;
3040
+ }
3041
+ });
3042
+ });
3043
+
3044
+ var current = tabs.filter(function (t) {
3045
+ return t.getAttribute('aria-selected') === 'true';
3046
+ })[0] || tabs[0];
3047
+ select(current, false);
3048
+ }
3049
+
3050
+ function initAll(context) {
3051
+ Array.prototype.forEach.call(
3052
+ (context || document).querySelectorAll('[data-fdy-tabs]'),
3053
+ initTabs
3054
+ );
3055
+ }
3056
+
3057
+ if (document.readyState === 'loading') {
3058
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
3059
+ } else {
3060
+ initAll();
3061
+ }
3062
+
3063
+ window.FreedayTabs = { init: initTabs, initAll: initAll };
3064
+ })();
3065
+
3066
+ /* Freeday — time picker enhancer (optional, zero-dependency).
3067
+ * Builds an input-styled trigger + scrollable time-list popover (WAI-ARIA listbox)
3068
+ * from an empty [data-fdy-timepicker] wrapper. 24h HH:MM. Options run from
3069
+ * data-min (default 00:00) to data-max (default 23:59) every data-step minutes
3070
+ * (default 30). Attributes: data-value, data-label, data-placeholder.
3071
+ *
3072
+ * Icon variants (Freeday convention, same as the date picker): [data-fdy-no-icon]
3073
+ * omits the icon; a [data-fdy-icon] child supplies a custom SVG; otherwise a clock.
3074
+ *
3075
+ * Keyboard: trigger opens on click / Enter / Space / ArrowDown; in the list,
3076
+ * Up/Down move, Home/End jump, Enter/Space select, Esc closes (focus returns to
3077
+ * the trigger). Emits a bubbling "fdy-time-select" CustomEvent (detail {value}).
3078
+ */
3079
+ (function () {
3080
+ 'use strict';
3081
+
3082
+ var seq = 0;
3083
+ var CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>';
3084
+
3085
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
3086
+ function valid(s) { return /^\d{1,2}:\d{2}$/.test(s || ''); }
3087
+ function toMin(s) { var p = s.split(':'); return (+p[0]) * 60 + (+p[1]); }
3088
+ function fromMin(m) { return pad(Math.floor(m / 60)) + ':' + pad(m % 60); }
3089
+ function norm(s) { return valid(s) ? fromMin(toMin(s)) : ''; }
3090
+
3091
+ function initTime(wrap) {
3092
+ if (wrap.dataset.fdyTpReady === '1') return;
3093
+ wrap.dataset.fdyTpReady = '1';
3094
+ wrap.classList.add('fdy-timepicker');
3095
+
3096
+ var label = wrap.getAttribute('data-label') || 'Pilih waktu';
3097
+ var placeholder = wrap.getAttribute('data-placeholder') || '--:--';
3098
+ var step = Math.max(1, parseInt(wrap.getAttribute('data-step') || '30', 10));
3099
+ var minM = valid(wrap.getAttribute('data-min')) ? toMin(wrap.getAttribute('data-min')) : 0;
3100
+ var maxM = valid(wrap.getAttribute('data-max')) ? toMin(wrap.getAttribute('data-max')) : 23 * 60 + 59;
3101
+ var value = norm(wrap.getAttribute('data-value'));
3102
+
3103
+ var trigger = document.createElement('button');
3104
+ trigger.type = 'button';
3105
+ trigger.className = 'fdy-timepicker__trigger';
3106
+ trigger.setAttribute('aria-haspopup', 'listbox');
3107
+ trigger.setAttribute('aria-expanded', 'false');
3108
+ trigger.setAttribute('aria-label', label);
3109
+ var valueSpan = document.createElement('span');
3110
+ valueSpan.className = 'fdy-timepicker__value';
3111
+ trigger.appendChild(valueSpan);
3112
+ if (!wrap.hasAttribute('data-fdy-no-icon')) {
3113
+ var customIcon = wrap.querySelector('[data-fdy-icon]');
3114
+ var icon = document.createElement('span');
3115
+ icon.className = 'fdy-timepicker__icon';
3116
+ icon.innerHTML = customIcon ? customIcon.innerHTML : CLOCK;
3117
+ if (customIcon) customIcon.remove();
3118
+ trigger.appendChild(icon);
3119
+ }
3120
+
3121
+ var panel = document.createElement('ul');
3122
+ panel.className = 'fdy-timepicker__panel';
3123
+ panel.setAttribute('role', 'listbox');
3124
+ panel.setAttribute('aria-label', label);
3125
+ panel.tabIndex = -1;
3126
+ panel.hidden = true;
3127
+
3128
+ var opts = [];
3129
+ for (var m = minM; m <= maxM; m += step) {
3130
+ var t = fromMin(m);
3131
+ var li = document.createElement('li');
3132
+ seq += 1; li.id = 'fdy-tp-opt-' + seq;
3133
+ li.className = 'fdy-timepicker__opt';
3134
+ li.setAttribute('role', 'option');
3135
+ li.setAttribute('data-value', t);
3136
+ li.setAttribute('aria-selected', t === value ? 'true' : 'false');
3137
+ li.textContent = t;
3138
+ panel.appendChild(li);
3139
+ opts.push(li);
3140
+ }
3141
+
3142
+ wrap.appendChild(trigger);
3143
+ wrap.appendChild(panel);
3144
+
3145
+ var active = -1;
3146
+
3147
+ function updateDisplay() {
3148
+ if (value) {
3149
+ valueSpan.textContent = value;
3150
+ valueSpan.classList.remove('fdy-timepicker__value--placeholder');
3151
+ } else {
3152
+ valueSpan.textContent = placeholder;
3153
+ valueSpan.classList.add('fdy-timepicker__value--placeholder');
3154
+ }
3155
+ }
3156
+ function indexOfValue() {
3157
+ for (var i = 0; i < opts.length; i++) { if (opts[i].getAttribute('data-value') === value) return i; }
3158
+ return -1;
3159
+ }
3160
+ function setActive(i) {
3161
+ if (active >= 0 && opts[active]) opts[active].classList.remove('is-active');
3162
+ active = i;
3163
+ if (i >= 0 && opts[i]) {
3164
+ opts[i].classList.add('is-active');
3165
+ panel.setAttribute('aria-activedescendant', opts[i].id);
3166
+ opts[i].scrollIntoView({ block: 'nearest' });
3167
+ } else {
3168
+ panel.removeAttribute('aria-activedescendant');
3169
+ }
3170
+ }
3171
+ var _pop = null;
3172
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
3173
+ function open() {
3174
+ if (!panel.hidden) return;
3175
+ var p = popCtl(); if (p) p.show(); else panel.hidden = false;
3176
+ trigger.setAttribute('aria-expanded', 'true');
3177
+ trigger.classList.add('is-open');
3178
+ var i = indexOfValue();
3179
+ setActive(i >= 0 ? i : 0);
3180
+ panel.focus();
3181
+ document.addEventListener('click', onDoc, true);
3182
+ }
3183
+ function close(returnFocus) {
3184
+ if (panel.hidden) return;
3185
+ var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
3186
+ trigger.setAttribute('aria-expanded', 'false');
3187
+ trigger.classList.remove('is-open');
3188
+ panel.removeAttribute('aria-activedescendant');
3189
+ document.removeEventListener('click', onDoc, true);
3190
+ if (returnFocus === true) trigger.focus();
3191
+ }
3192
+ function onDoc(e) { if (!wrap.contains(e.target)) close(false); }
3193
+ function choose(i) {
3194
+ if (i < 0 || i >= opts.length) return;
3195
+ value = opts[i].getAttribute('data-value');
3196
+ for (var j = 0; j < opts.length; j++) opts[j].setAttribute('aria-selected', j === i ? 'true' : 'false');
3197
+ updateDisplay();
3198
+ wrap.dispatchEvent(new CustomEvent('fdy-time-select', { bubbles: true, detail: { value: value } }));
3199
+ close(true);
3200
+ }
3201
+
3202
+ trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
3203
+ trigger.addEventListener('keydown', function (e) {
3204
+ if (e.key === 'ArrowDown') { e.preventDefault(); open(); }
3205
+ });
3206
+ panel.addEventListener('keydown', function (e) {
3207
+ switch (e.key) {
3208
+ case 'ArrowDown': e.preventDefault(); setActive(Math.min(opts.length - 1, active + 1)); break;
3209
+ case 'ArrowUp': e.preventDefault(); setActive(Math.max(0, active - 1)); break;
3210
+ case 'Home': e.preventDefault(); setActive(0); break;
3211
+ case 'End': e.preventDefault(); setActive(opts.length - 1); break;
3212
+ case 'Enter': case ' ': e.preventDefault(); choose(active); break;
3213
+ case 'Escape': e.preventDefault(); close(true); break;
3214
+ case 'Tab': close(false); break;
3215
+ default: break;
3216
+ }
3217
+ });
3218
+ panel.addEventListener('click', function (e) {
3219
+ var li = e.target.closest('[role="option"]');
3220
+ if (li) choose(opts.indexOf(li));
3221
+ });
3222
+
3223
+ updateDisplay();
3224
+
3225
+ var api = {
3226
+ wrap: wrap,
3227
+ getValue: function () { return value; },
3228
+ clear: function () { value = ''; for (var i = 0; i < opts.length; i++) opts[i].setAttribute('aria-selected', 'false'); updateDisplay(); }
3229
+ };
3230
+ wrap._fdyTp = api;
3231
+ return api;
3232
+ }
3233
+
3234
+ function initAll(context) {
3235
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-timepicker]'), initTime);
3236
+ }
3237
+
3238
+ if (document.readyState === 'loading') {
3239
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
3240
+ } else {
3241
+ initAll();
3242
+ }
3243
+
3244
+ window.FreedayTimepicker = { init: initTime, initAll: initAll };
3245
+ })();
3246
+
3247
+ /* Freeday — toast API (optional, zero-dependency).
3248
+ * Freeday.toast({ variant, title, message, timeout }) shows a transient
3249
+ * notification in a live region (created on first use). Returns the element.
3250
+ * variant: 'success' | 'warning' | 'danger' | 'info' (default: neutral)
3251
+ * timeout: ms before auto-dismiss (default 4000; 0 = sticky)
3252
+ * Danger toasts use role="alert"; others role="status".
3253
+ */
3254
+ (function () {
3255
+ 'use strict';
3256
+
3257
+ var region = null;
3258
+
3259
+ function ensureRegion() {
3260
+ if (region && document.body.contains(region)) return region;
3261
+ region = document.querySelector('.fdy-toast-region');
3262
+ if (!region) {
3263
+ region = document.createElement('div');
3264
+ region.className = 'fdy-toast-region';
3265
+ region.setAttribute('aria-live', 'polite');
3266
+ region.setAttribute('aria-atomic', 'false');
3267
+ document.body.appendChild(region);
3268
+ }
3269
+ return region;
3270
+ }
3271
+
3272
+ function dismiss(toast) {
3273
+ if (!toast || toast.dataset.leaving === '1') return;
3274
+ toast.dataset.leaving = '1';
3275
+ toast.classList.add('is-leaving');
3276
+ var remove = function () { if (toast.parentNode) toast.parentNode.removeChild(toast); };
3277
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3278
+ if (reduce) { remove(); return; }
3279
+ var done = false;
3280
+ var finish = function () { if (!done) { done = true; remove(); } };
3281
+ toast.addEventListener('transitionend', finish);
3282
+ setTimeout(finish, 400); // fallback if transitionend never fires
3283
+ }
3284
+
3285
+ function el(tag, className, text) {
3286
+ var node = document.createElement(tag);
3287
+ if (className) node.className = className;
3288
+ if (text != null) node.textContent = text;
3289
+ return node;
3290
+ }
3291
+
3292
+ function toast(opts) {
3293
+ opts = opts || {};
3294
+ var region = ensureRegion();
3295
+ var node = el('div', 'fdy-toast' + (opts.variant ? ' fdy-toast--' + opts.variant : ''));
3296
+ node.setAttribute('role', opts.variant === 'danger' ? 'alert' : 'status');
3297
+
3298
+ var accent = el('span', 'fdy-toast__accent');
3299
+ accent.setAttribute('aria-hidden', 'true');
3300
+ var body = el('div', 'fdy-toast__body');
3301
+ if (opts.title) body.appendChild(el('div', 'fdy-toast__title', opts.title));
3302
+ if (opts.message) body.appendChild(el('div', 'fdy-toast__text', opts.message));
3303
+
3304
+ var close = el('button', 'fdy-toast__close');
3305
+ close.type = 'button';
3306
+ close.setAttribute('aria-label', 'Tutup');
3307
+ close.innerHTML = '&times;';
3308
+ close.addEventListener('click', function () { dismiss(node); });
3309
+
3310
+ node.appendChild(accent);
3311
+ node.appendChild(body);
3312
+ node.appendChild(close);
3313
+ region.appendChild(node);
3314
+
3315
+ var timeout = opts.timeout == null ? 4000 : opts.timeout;
3316
+ if (timeout > 0) setTimeout(function () { dismiss(node); }, timeout);
3317
+ return node;
3318
+ }
3319
+
3320
+ // Pre-create the live region so the FIRST status toast is reliably announced
3321
+ // (some screen readers ignore a live region created in the same tick as its content).
3322
+ if (document.readyState === 'loading') {
3323
+ document.addEventListener('DOMContentLoaded', ensureRegion);
3324
+ } else {
3325
+ ensureRegion();
3326
+ }
3327
+
3328
+ window.Freeday = window.Freeday || {};
3329
+ window.Freeday.toast = toast;
3330
+ })();
3331
+
3332
+ /* Freeday — tree checkbox cascade (optional, zero-dependency).
3333
+ * Turns a [data-fdy-tree] checkbox tree into a selectable one: checking a branch checks
3334
+ * every descendant; a partially-selected branch shows the native :indeterminate state.
3335
+ * Rows are native <input class="fdy-checkbox">, labelled via aria-label — Space toggles.
3336
+ * Selection never toggles the <details> (the checkbox stops click propagation).
3337
+ *
3338
+ * Markup contract:
3339
+ * <ul class="fdy-tree fdy-tree--checkbox" data-fdy-tree>
3340
+ * <li>
3341
+ * <details class="fdy-tree__branch">
3342
+ * <summary>…chevron… <input class="fdy-checkbox fdy-tree__check" type="checkbox" aria-label="…"> …icon… Label</summary>
3343
+ * <ul> …child <li>s (branches or .fdy-tree__leaf leaves)… </ul>
3344
+ * </details>
3345
+ * </li>
3346
+ * <li class="fdy-tree__leaf"><input class="fdy-checkbox fdy-tree__check" type="checkbox" aria-label="…"> …icon… Label</li>
3347
+ * </ul>
3348
+ */
3349
+ (function () {
3350
+ 'use strict';
3351
+
3352
+ // The <li>s directly under a branch's own child list (skip text nodes).
3353
+ function childItems(li) {
3354
+ var ul = li.querySelector(':scope > details > ul');
3355
+ if (!ul) return [];
3356
+ return Array.prototype.filter.call(ul.children, function (n) { return n.tagName === 'LI'; });
3357
+ }
3358
+
3359
+ // The checkbox belonging to a row's own <li> — branch summary or leaf, never a child's.
3360
+ function ownBox(li) {
3361
+ return li.querySelector(':scope > details > summary input[type="checkbox"]')
3362
+ || li.querySelector(':scope > input[type="checkbox"]');
3363
+ }
3364
+
3365
+ function setSubtree(li, checked) {
3366
+ childItems(li).forEach(function (child) {
3367
+ var box = ownBox(child);
3368
+ if (box) { box.checked = checked; box.indeterminate = false; }
3369
+ setSubtree(child, checked);
3370
+ });
3371
+ }
3372
+
3373
+ // Recompute one branch's box from the state of its direct children.
3374
+ function recompute(li) {
3375
+ var box = ownBox(li);
3376
+ var kids = childItems(li).map(ownBox).filter(Boolean);
3377
+ if (!box || !kids.length) return;
3378
+ var allOn = kids.every(function (b) { return b.checked && !b.indeterminate; });
3379
+ var anyOn = kids.some(function (b) { return b.checked || b.indeterminate; });
3380
+ box.checked = allOn;
3381
+ box.indeterminate = anyOn && !allOn;
3382
+ }
3383
+
3384
+ function refreshAncestors(li) {
3385
+ var parent = li.parentElement ? li.parentElement.closest('li') : null;
3386
+ while (parent) {
3387
+ recompute(parent);
3388
+ parent = parent.parentElement ? parent.parentElement.closest('li') : null;
3389
+ }
3390
+ }
3391
+
3392
+ // Post-order pass so pre-checked leaves resolve their branches on load.
3393
+ function initState(li) {
3394
+ childItems(li).forEach(initState);
3395
+ recompute(li);
3396
+ }
3397
+
3398
+ function initTree(tree) {
3399
+ if (tree.dataset.fdyTreeReady === '1') return;
3400
+ tree.dataset.fdyTreeReady = '1';
3401
+ Array.prototype.forEach.call(tree.querySelectorAll('input[type="checkbox"]'), function (box) {
3402
+ // A branch checkbox lives inside <summary>; keep its click from toggling the branch.
3403
+ box.addEventListener('click', function (e) { e.stopPropagation(); });
3404
+ box.addEventListener('change', function () {
3405
+ var li = box.closest('li');
3406
+ setSubtree(li, box.checked);
3407
+ box.indeterminate = false;
3408
+ refreshAncestors(li);
3409
+ });
3410
+ });
3411
+ Array.prototype.filter.call(tree.children, function (n) { return n.tagName === 'LI'; }).forEach(initState);
3412
+ }
3413
+
3414
+ function initAll(context) {
3415
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-tree]'), initTree);
3416
+ }
3417
+
3418
+ if (document.readyState === 'loading') {
3419
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
3420
+ } else {
3421
+ initAll();
3422
+ }
3423
+
3424
+ window.FreedayTree = { init: initTree, initAll: initAll };
3425
+ })();
3426
+
3427
+ /* Freeday — file-upload enhancer (optional, zero-dependency).
3428
+ * A drop target that also opens the file dialog on click / Enter / Space, validates each file
3429
+ * (accept + max size), and renders a per-file row with explicit state. Actual upload is the
3430
+ * consumer's job — this is a controlled reference: wire the emitted event's row API to your
3431
+ * upload call, or set data-fdy-upload-simulate for a demo progress animation.
3432
+ *
3433
+ * Markup contract:
3434
+ * <div class="fdy-dropzone" data-fdy-dropzone role="button" tabindex="0"
3435
+ * data-max-size="10485760" data-filelist="#dz-list" [data-fdy-upload-simulate]
3436
+ * aria-label="Unggah berkas — seret ke sini atau tekan Enter">
3437
+ * <span class="fdy-dropzone__icon">…svg…</span>
3438
+ * <span class="fdy-dropzone__title">…</span>
3439
+ * <span class="fdy-dropzone__hint">…</span>
3440
+ * <input type="file" accept=".pdf,application/pdf" multiple hidden>
3441
+ * </div>
3442
+ * <div class="fdy-filelist" id="dz-list"></div>
3443
+ *
3444
+ * Emits bubbling CustomEvents on the dropzone: "fdy-upload-add" {file, rejected, reason, row}
3445
+ * (row exposes .uploading()/.setProgress(pct)/.done()/.fail(msg)) and "fdy-upload-remove" {file}.
3446
+ */
3447
+ (function () {
3448
+ 'use strict';
3449
+
3450
+ function fmtSize(bytes) {
3451
+ if (bytes < 1024) return bytes + ' B';
3452
+ if (bytes < 1048576) return (bytes / 1024).toFixed(0) + ' KB';
3453
+ return (bytes / 1048576).toFixed(1) + ' MB';
3454
+ }
3455
+
3456
+ var FILE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><path d="M14 2v6h6"></path></svg>';
3457
+ var OK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m8 12 3 3 5-6"></path></svg>';
3458
+ var ERR_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg>';
3459
+
3460
+ function accepts(file, acceptAttr) {
3461
+ if (!acceptAttr) return true;
3462
+ var name = file.name.toLowerCase();
3463
+ var type = (file.type || '').toLowerCase();
3464
+ return acceptAttr.split(',').map(function (s) { return s.trim().toLowerCase(); }).some(function (a) {
3465
+ if (!a) return false;
3466
+ if (a.charAt(0) === '.') return name.slice(-a.length) === a;
3467
+ if (a.slice(-2) === '/*') return type.indexOf(a.slice(0, -1)) === 0;
3468
+ return type === a;
3469
+ });
3470
+ }
3471
+
3472
+ function makeRow(file) {
3473
+ var el = document.createElement('div');
3474
+ el.className = 'fdy-file';
3475
+ var icon = document.createElement('span');
3476
+ icon.className = 'fdy-file__icon';
3477
+ icon.innerHTML = FILE_ICON;
3478
+ var meta = document.createElement('div');
3479
+ meta.className = 'fdy-file__meta';
3480
+ var name = document.createElement('div');
3481
+ name.className = 'fdy-file__name';
3482
+ name.textContent = file.name;
3483
+ var sub = document.createElement('div');
3484
+ sub.className = 'fdy-file__sub';
3485
+ sub.textContent = fmtSize(file.size);
3486
+ meta.appendChild(name);
3487
+ meta.appendChild(sub);
3488
+ var remove = document.createElement('button');
3489
+ remove.type = 'button';
3490
+ remove.className = 'fdy-file__remove';
3491
+ remove.setAttribute('aria-label', 'Hapus ' + file.name);
3492
+ remove.innerHTML = '&times;';
3493
+ remove.addEventListener('click', function () {
3494
+ el.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
3495
+ el.remove();
3496
+ });
3497
+ el.appendChild(icon);
3498
+ el.appendChild(meta);
3499
+ el.appendChild(remove);
3500
+
3501
+ var progressWrap = null, bar = null, progressEl = null;
3502
+ function ensureProgress() {
3503
+ if (progressWrap) return;
3504
+ progressWrap = document.createElement('div');
3505
+ progressWrap.className = 'fdy-file__progress';
3506
+ progressEl = document.createElement('div');
3507
+ progressEl.className = 'fdy-progress';
3508
+ progressEl.setAttribute('role', 'progressbar');
3509
+ progressEl.setAttribute('aria-valuemin', '0');
3510
+ progressEl.setAttribute('aria-valuemax', '100');
3511
+ progressEl.setAttribute('aria-label', 'Progres unggah ' + file.name);
3512
+ bar = document.createElement('div');
3513
+ bar.className = 'fdy-progress__bar';
3514
+ bar.style.width = '0%';
3515
+ progressEl.appendChild(bar);
3516
+ progressWrap.appendChild(progressEl);
3517
+ meta.appendChild(progressWrap);
3518
+ }
3519
+ function dropProgress() {
3520
+ if (progressWrap) { progressWrap.remove(); progressWrap = null; bar = null; progressEl = null; }
3521
+ }
3522
+ return {
3523
+ el: el,
3524
+ uploading: function () {
3525
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
3526
+ icon.innerHTML = FILE_ICON;
3527
+ sub.textContent = fmtSize(file.size) + ' · Mengunggah…';
3528
+ ensureProgress();
3529
+ },
3530
+ setProgress: function (pct) {
3531
+ ensureProgress();
3532
+ var v = Math.max(0, Math.min(100, pct));
3533
+ bar.style.width = v + '%';
3534
+ progressEl.setAttribute('aria-valuenow', String(Math.round(v)));
3535
+ },
3536
+ done: function () {
3537
+ el.classList.add('fdy-file--success');
3538
+ el.classList.remove('fdy-file--error');
3539
+ icon.innerHTML = OK_ICON;
3540
+ sub.textContent = fmtSize(file.size) + ' · Terunggah';
3541
+ dropProgress();
3542
+ },
3543
+ fail: function (msg) {
3544
+ el.classList.add('fdy-file--error');
3545
+ el.classList.remove('fdy-file--success');
3546
+ icon.innerHTML = ERR_ICON;
3547
+ sub.textContent = msg;
3548
+ dropProgress();
3549
+ }
3550
+ };
3551
+ }
3552
+
3553
+ function simulateUpload(row) {
3554
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3555
+ if (reduce) { row.setProgress(100); row.done(); return; }
3556
+ var pct = 0;
3557
+ var id = setInterval(function () {
3558
+ pct += 12 + Math.random() * 18;
3559
+ if (pct >= 100) {
3560
+ row.setProgress(100);
3561
+ clearInterval(id);
3562
+ setTimeout(function () { row.done(); }, 200);
3563
+ } else {
3564
+ row.setProgress(pct);
3565
+ }
3566
+ }, 180);
3567
+ }
3568
+
3569
+ function initDropzone(zone) {
3570
+ if (zone.dataset.fdyUploadReady === '1') return;
3571
+ zone.dataset.fdyUploadReady = '1';
3572
+
3573
+ var input = zone.querySelector('input[type="file"]');
3574
+ var listSel = zone.getAttribute('data-filelist');
3575
+ var list = listSel ? document.querySelector(listSel)
3576
+ : (zone.parentNode ? zone.parentNode.querySelector('.fdy-filelist') : null);
3577
+ var maxSize = parseInt(zone.getAttribute('data-max-size'), 10) || 0;
3578
+ var simulate = zone.hasAttribute('data-fdy-upload-simulate');
3579
+ var acceptAttr = input ? input.getAttribute('accept') : '';
3580
+
3581
+ function openDialog() { if (input) input.click(); }
3582
+
3583
+ zone.addEventListener('click', function (e) { if (e.target !== input) openDialog(); });
3584
+ zone.addEventListener('keydown', function (e) {
3585
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDialog(); }
3586
+ });
3587
+ ['dragenter', 'dragover'].forEach(function (ev) {
3588
+ zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add('is-dragover'); });
3589
+ });
3590
+ ['dragleave', 'dragend'].forEach(function (ev) {
3591
+ zone.addEventListener(ev, function (e) { if (e.target === zone) zone.classList.remove('is-dragover'); });
3592
+ });
3593
+ zone.addEventListener('drop', function (e) {
3594
+ e.preventDefault();
3595
+ zone.classList.remove('is-dragover');
3596
+ handleFiles(e.dataTransfer ? e.dataTransfer.files : null);
3597
+ });
3598
+ if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
3599
+
3600
+ function handleFiles(fileList) {
3601
+ if (!list || !fileList) return;
3602
+ Array.prototype.slice.call(fileList).forEach(function (file) {
3603
+ var reason = null;
3604
+ if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
3605
+ else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
3606
+ var row = makeRow(file);
3607
+ list.appendChild(row.el);
3608
+ if (reason) {
3609
+ row.fail(reason);
3610
+ } else {
3611
+ row.uploading();
3612
+ if (simulate) simulateUpload(row);
3613
+ }
3614
+ zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
3615
+ bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
3616
+ }));
3617
+ });
3618
+ }
3619
+ }
3620
+
3621
+ function initAll(context) {
3622
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
3623
+ }
3624
+
3625
+ if (document.readyState === 'loading') {
3626
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
3627
+ } else {
3628
+ initAll();
3629
+ }
3630
+
3631
+ window.FreedayUpload = { init: initDropzone, initAll: initAll };
3632
+ })();