@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.
- package/CHANGELOG.md +447 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/adapters/blazor/freeday-blazor.js +79 -0
- package/adapters/react/components/FdyAutocomplete.tsx +159 -0
- package/adapters/react/components/FdyCascade.tsx +230 -0
- package/adapters/react/components/FdyCfl.tsx +362 -0
- package/adapters/react/components/FdyChart.tsx +69 -0
- package/adapters/react/components/FdyCombo.tsx +166 -0
- package/adapters/react/components/FdyDateRange.tsx +66 -0
- package/adapters/react/components/FdyDatepicker.tsx +354 -0
- package/adapters/react/index.d.ts +37 -0
- package/adapters/react/index.js +9 -0
- package/adapters/react/tsconfig.json +14 -0
- package/adapters/react/useFreeday.js +44 -0
- package/adapters/react/usePopover.ts +64 -0
- package/adapters/vue/components/FdyAutocomplete.vue +174 -0
- package/adapters/vue/components/FdyCascade.vue +241 -0
- package/adapters/vue/components/FdyCfl.vue +354 -0
- package/adapters/vue/components/FdyChart.vue +89 -0
- package/adapters/vue/components/FdyCombo.vue +232 -0
- package/adapters/vue/components/FdyDateRange.vue +78 -0
- package/adapters/vue/components/FdyDatepicker.vue +333 -0
- package/adapters/vue/index.d.ts +44 -0
- package/adapters/vue/index.js +8 -0
- package/adapters/vue/useFreeday.js +44 -0
- package/adapters/vue/usePopover.ts +78 -0
- package/dist/freeday-autocomplete.js +135 -0
- package/dist/freeday-breakpoint.js +51 -0
- package/dist/freeday-carousel.js +111 -0
- package/dist/freeday-cascade.js +256 -0
- package/dist/freeday-cfl.js +213 -0
- package/dist/freeday-chart.js +403 -0
- package/dist/freeday-chip.js +83 -0
- package/dist/freeday-datepicker.js +321 -0
- package/dist/freeday-datetime.js +59 -0
- package/dist/freeday-drawer.js +43 -0
- package/dist/freeday-form.js +181 -0
- package/dist/freeday-mask.js +114 -0
- package/dist/freeday-menu.js +93 -0
- package/dist/freeday-popover.js +69 -0
- package/dist/freeday-rating.js +50 -0
- package/dist/freeday-select.js +194 -0
- package/dist/freeday-slider.js +34 -0
- package/dist/freeday-stepper.js +90 -0
- package/dist/freeday-table.js +475 -0
- package/dist/freeday-tabs.js +68 -0
- package/dist/freeday-timepicker.js +180 -0
- package/dist/freeday-toast.js +84 -0
- package/dist/freeday-tree.js +94 -0
- package/dist/freeday-upload.js +206 -0
- package/dist/freeday.bundle.css +1352 -0
- package/dist/freeday.css +1007 -0
- package/dist/freeday.js +3632 -0
- package/dist/freeday.tokens.css +343 -0
- package/package.json +72 -0
- package/src/base.css +20 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.css +13 -0
- package/src/components/alert.css +18 -0
- package/src/components/app-shell.css +88 -0
- package/src/components/appbar.css +15 -0
- package/src/components/autocomplete.css +12 -0
- package/src/components/avatar.css +8 -0
- package/src/components/badge.css +27 -0
- package/src/components/breadcrumb.css +9 -0
- package/src/components/breakpoints.css +15 -0
- package/src/components/button.css +58 -0
- package/src/components/card.css +9 -0
- package/src/components/carousel.css +17 -0
- package/src/components/cascade.css +36 -0
- package/src/components/cfl.css +28 -0
- package/src/components/chart.css +55 -0
- package/src/components/chip.css +18 -0
- package/src/components/combo.css +27 -0
- package/src/components/datepicker.css +44 -0
- package/src/components/datetimepicker.css +8 -0
- package/src/components/description-list.css +6 -0
- package/src/components/divider.css +3 -0
- package/src/components/drawer.css +25 -0
- package/src/components/file-upload.css +34 -0
- package/src/components/filterbar.css +25 -0
- package/src/components/form-grid.css +8 -0
- package/src/components/input-group.css +22 -0
- package/src/components/input.css +13 -0
- package/src/components/kbd.css +2 -0
- package/src/components/menu.css +28 -0
- package/src/components/modal.css +22 -0
- package/src/components/pagination.css +8 -0
- package/src/components/progress.css +7 -0
- package/src/components/rating.css +10 -0
- package/src/components/selection.css +25 -0
- package/src/components/skeleton.css +7 -0
- package/src/components/slider.css +12 -0
- package/src/components/spinner.css +6 -0
- package/src/components/states.css +8 -0
- package/src/components/stepper.css +21 -0
- package/src/components/table.css +55 -0
- package/src/components/tabs.css +10 -0
- package/src/components/timeline.css +12 -0
- package/src/components/timepicker.css +22 -0
- package/src/components/toast.css +17 -0
- package/src/components/tooltip.css +6 -0
- package/src/components/tree.css +21 -0
- package/src/freeday-autocomplete.js +135 -0
- package/src/freeday-breakpoint.js +51 -0
- package/src/freeday-carousel.js +111 -0
- package/src/freeday-cascade.js +256 -0
- package/src/freeday-cfl.js +213 -0
- package/src/freeday-chart.js +403 -0
- package/src/freeday-chip.js +83 -0
- package/src/freeday-datepicker.js +321 -0
- package/src/freeday-datetime.js +59 -0
- package/src/freeday-drawer.js +43 -0
- package/src/freeday-form.js +181 -0
- package/src/freeday-mask.js +114 -0
- package/src/freeday-menu.js +93 -0
- package/src/freeday-popover.js +69 -0
- package/src/freeday-rating.js +50 -0
- package/src/freeday-select.js +194 -0
- package/src/freeday-slider.js +34 -0
- package/src/freeday-stepper.js +90 -0
- package/src/freeday-table.js +475 -0
- package/src/freeday-tabs.js +68 -0
- package/src/freeday-timepicker.js +180 -0
- package/src/freeday-toast.js +84 -0
- package/src/freeday-tree.js +94 -0
- package/src/freeday-upload.js +206 -0
- package/tokens/breakpoints.mjs +4 -0
- package/tokens/tokens.json +123 -0
package/dist/freeday.js
ADDED
|
@@ -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 = '×';
|
|
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 = '×';
|
|
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
|
+
})();
|