@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Freeday — autocomplete enhancer (optional, zero-dependency).
|
|
2
|
+
* Editable combobox (WAI-ARIA APG): a text input filters a role="listbox" of role="option"
|
|
3
|
+
* items as you type; select fills the input. Auto-inits [data-fdy-autocomplete]. Options are
|
|
4
|
+
* static markup filtered client-side — in an app, fetch/filter server-side and re-render options.
|
|
5
|
+
*
|
|
6
|
+
* Markup: <div data-fdy-autocomplete class="fdy-autocomplete">
|
|
7
|
+
* <input class="fdy-input" role="combobox" aria-expanded="false" aria-autocomplete="list"
|
|
8
|
+
* aria-controls="listId" autocomplete="off">
|
|
9
|
+
* <ul class="fdy-autocomplete__listbox" id="listId" role="listbox" hidden>
|
|
10
|
+
* <li class="fdy-autocomplete__option" role="option">…</li>…
|
|
11
|
+
* <li class="fdy-autocomplete__empty" hidden>Tak ada hasil</li></ul></div>
|
|
12
|
+
* Emits a bubbling "fdy-autocomplete-select" CustomEvent (detail {value, option}).
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var seq = 0;
|
|
18
|
+
|
|
19
|
+
function initAutocomplete(root) {
|
|
20
|
+
if (root.dataset.fdyAcReady === '1') return;
|
|
21
|
+
root.dataset.fdyAcReady = '1';
|
|
22
|
+
|
|
23
|
+
var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
|
|
24
|
+
var listbox = root.querySelector('[role="listbox"]');
|
|
25
|
+
if (!input || !listbox) return;
|
|
26
|
+
var options = Array.prototype.slice.call(listbox.querySelectorAll('[role="option"]'));
|
|
27
|
+
var emptyEl = listbox.querySelector('.fdy-autocomplete__empty');
|
|
28
|
+
var active = -1;
|
|
29
|
+
|
|
30
|
+
options.forEach(function (o) {
|
|
31
|
+
if (!o.id) { seq += 1; o.id = 'fdy-ac-opt-' + seq; }
|
|
32
|
+
o.dataset.label = o.textContent.trim();
|
|
33
|
+
});
|
|
34
|
+
listbox.hidden = true;
|
|
35
|
+
input.setAttribute('aria-expanded', 'false');
|
|
36
|
+
|
|
37
|
+
function visible() { return options.filter(function (o) { return !o.hidden; }); }
|
|
38
|
+
function clearHighlight() { options.forEach(function (o) { o.classList.remove('is-highlighted'); }); }
|
|
39
|
+
|
|
40
|
+
var _pop = null;
|
|
41
|
+
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, input); return _pop; }
|
|
42
|
+
function open() {
|
|
43
|
+
if (!listbox.hidden) return;
|
|
44
|
+
var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
|
|
45
|
+
input.setAttribute('aria-expanded', 'true');
|
|
46
|
+
document.addEventListener('click', onDocClick, true);
|
|
47
|
+
}
|
|
48
|
+
function close() {
|
|
49
|
+
if (listbox.hidden) return;
|
|
50
|
+
var p = popCtl(); if (p) p.hide(); else listbox.hidden = true;
|
|
51
|
+
input.setAttribute('aria-expanded', 'false');
|
|
52
|
+
input.removeAttribute('aria-activedescendant');
|
|
53
|
+
active = -1;
|
|
54
|
+
clearHighlight();
|
|
55
|
+
document.removeEventListener('click', onDocClick, true);
|
|
56
|
+
}
|
|
57
|
+
function onDocClick(e) { if (!root.contains(e.target)) close(); }
|
|
58
|
+
|
|
59
|
+
function highlight(i) {
|
|
60
|
+
var vis = visible();
|
|
61
|
+
clearHighlight();
|
|
62
|
+
if (!vis.length || i < 0 || i >= vis.length) { active = -1; input.removeAttribute('aria-activedescendant'); return; }
|
|
63
|
+
active = i;
|
|
64
|
+
vis[i].classList.add('is-highlighted');
|
|
65
|
+
input.setAttribute('aria-activedescendant', vis[i].id);
|
|
66
|
+
vis[i].scrollIntoView({ block: 'nearest' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyFilter() {
|
|
70
|
+
var q = input.value.trim().toLowerCase();
|
|
71
|
+
var shown = 0;
|
|
72
|
+
options.forEach(function (o) {
|
|
73
|
+
var match = !q || o.dataset.label.toLowerCase().indexOf(q) !== -1;
|
|
74
|
+
o.hidden = !match;
|
|
75
|
+
if (match) shown++;
|
|
76
|
+
});
|
|
77
|
+
if (emptyEl) emptyEl.hidden = shown !== 0;
|
|
78
|
+
active = -1;
|
|
79
|
+
input.removeAttribute('aria-activedescendant');
|
|
80
|
+
clearHighlight();
|
|
81
|
+
open();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function choose(opt) {
|
|
85
|
+
input.value = opt.dataset.label;
|
|
86
|
+
options.forEach(function (o) { o.setAttribute('aria-selected', o === opt ? 'true' : 'false'); });
|
|
87
|
+
close();
|
|
88
|
+
root.dispatchEvent(new CustomEvent('fdy-autocomplete-select', { bubbles: true, detail: { value: opt.dataset.label, option: opt } }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
input.addEventListener('input', applyFilter);
|
|
92
|
+
input.addEventListener('focus', function () { applyFilter(); });
|
|
93
|
+
input.addEventListener('keydown', function (e) {
|
|
94
|
+
var vis = visible();
|
|
95
|
+
switch (e.key) {
|
|
96
|
+
case 'ArrowDown':
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (listbox.hidden) open();
|
|
99
|
+
highlight(active + 1 < vis.length ? active + 1 : 0);
|
|
100
|
+
break;
|
|
101
|
+
case 'ArrowUp':
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
if (listbox.hidden) open();
|
|
104
|
+
highlight(active - 1 >= 0 ? active - 1 : vis.length - 1);
|
|
105
|
+
break;
|
|
106
|
+
case 'Enter':
|
|
107
|
+
if (!listbox.hidden && active >= 0) { e.preventDefault(); choose(vis[active]); }
|
|
108
|
+
break;
|
|
109
|
+
case 'Escape':
|
|
110
|
+
if (!listbox.hidden) { e.preventDefault(); close(); }
|
|
111
|
+
break;
|
|
112
|
+
case 'Tab':
|
|
113
|
+
close();
|
|
114
|
+
break;
|
|
115
|
+
default: break;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
listbox.addEventListener('click', function (e) {
|
|
119
|
+
var opt = e.target.closest('[role="option"]');
|
|
120
|
+
if (opt && !opt.hidden) choose(opt);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function initAll(context) {
|
|
125
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-autocomplete]'), initAutocomplete);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (document.readyState === 'loading') {
|
|
129
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
130
|
+
} else {
|
|
131
|
+
initAll();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll };
|
|
135
|
+
})();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Freeday — breakpoint provider (optional, zero-dependency).
|
|
2
|
+
* The MudBreakpointProvider equivalent for JS consumers: stamps the current breakpoint on
|
|
3
|
+
* <html data-breakpoint="xs|sm|md|lg|xl"> and emits a bubbling "fdy-breakpoint-change"
|
|
4
|
+
* CustomEvent (detail {breakpoint}) whenever it changes. Also fills any element with
|
|
5
|
+
* [data-fdy-breakpoint-label] with the current name. Read it live via FreedayBreakpoint.get().
|
|
6
|
+
* Scale (min-width, px): sm 600 · md 960 · lg 1280 · xl 1920 (matches breakpoints.css).
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
var STEPS = [['xl', 1920], ['lg', 1280], ['md', 960], ['sm', 600], ['xs', 0]];
|
|
12
|
+
|
|
13
|
+
function current() {
|
|
14
|
+
var w = window.innerWidth;
|
|
15
|
+
for (var i = 0; i < STEPS.length; i++) {
|
|
16
|
+
if (w >= STEPS[i][1]) return STEPS[i][0];
|
|
17
|
+
}
|
|
18
|
+
return 'xs';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var root = document.documentElement;
|
|
22
|
+
var last = null;
|
|
23
|
+
|
|
24
|
+
function update() {
|
|
25
|
+
var bp = current();
|
|
26
|
+
if (bp === last) return;
|
|
27
|
+
last = bp;
|
|
28
|
+
root.setAttribute('data-breakpoint', bp);
|
|
29
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
|
|
30
|
+
el.textContent = bp;
|
|
31
|
+
});
|
|
32
|
+
root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener('resize', update, { passive: true });
|
|
36
|
+
if (document.readyState === 'loading') {
|
|
37
|
+
document.addEventListener('DOMContentLoaded', update);
|
|
38
|
+
} else {
|
|
39
|
+
update();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
window.FreedayBreakpoint = {
|
|
43
|
+
get: function () { return last || current(); },
|
|
44
|
+
current: current,
|
|
45
|
+
// true if the current breakpoint is `name` or wider (e.g. is('md') → md, lg, or xl)
|
|
46
|
+
isUp: function (name) {
|
|
47
|
+
var order = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
48
|
+
return order.indexOf(this.get()) >= order.indexOf(name);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Freeday — carousel enhancer (optional, zero-dependency).
|
|
2
|
+
* Scroll-snap slider with prev/next arrows, generated dot indicators, keyboard (←/→),
|
|
3
|
+
* and optional autoplay (data-fdy-autoplay="4000", pauses on hover/focus). Auto-inits
|
|
4
|
+
* [data-fdy-carousel].
|
|
5
|
+
*
|
|
6
|
+
* Markup: <div class="fdy-carousel" data-fdy-carousel role="region"
|
|
7
|
+
* aria-roledescription="carousel" aria-label="…" tabindex="0">
|
|
8
|
+
* <button class="fdy-carousel__arrow fdy-carousel__arrow--prev" aria-label="Sebelumnya">…</button>
|
|
9
|
+
* <div class="fdy-carousel__viewport">
|
|
10
|
+
* <div class="fdy-carousel__slide" role="group" aria-roledescription="slide">…</div>…
|
|
11
|
+
* </div>
|
|
12
|
+
* <button class="fdy-carousel__arrow fdy-carousel__arrow--next" aria-label="Berikutnya">…</button>
|
|
13
|
+
* <div class="fdy-carousel__dots"></div> <!-- dots generated here -->
|
|
14
|
+
* </div>
|
|
15
|
+
* Emits a bubbling "fdy-carousel-change" CustomEvent (detail {index}).
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
function initCarousel(root) {
|
|
21
|
+
if (root.dataset.fdyCarouselReady === '1') return;
|
|
22
|
+
root.dataset.fdyCarouselReady = '1';
|
|
23
|
+
|
|
24
|
+
var viewport = root.querySelector('.fdy-carousel__viewport');
|
|
25
|
+
var slides = Array.prototype.slice.call(root.querySelectorAll('.fdy-carousel__slide'));
|
|
26
|
+
if (!viewport || !slides.length) return;
|
|
27
|
+
var prev = root.querySelector('.fdy-carousel__arrow--prev');
|
|
28
|
+
var next = root.querySelector('.fdy-carousel__arrow--next');
|
|
29
|
+
var dotsWrap = root.querySelector('.fdy-carousel__dots');
|
|
30
|
+
var index = 0;
|
|
31
|
+
|
|
32
|
+
slides.forEach(function (s, i) {
|
|
33
|
+
if (!s.hasAttribute('aria-label')) s.setAttribute('aria-label', (i + 1) + ' dari ' + slides.length);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
var dots = [];
|
|
37
|
+
if (dotsWrap) {
|
|
38
|
+
slides.forEach(function (s, i) {
|
|
39
|
+
var b = document.createElement('button');
|
|
40
|
+
b.type = 'button';
|
|
41
|
+
b.className = 'fdy-carousel__dot';
|
|
42
|
+
b.setAttribute('aria-label', 'Slide ' + (i + 1));
|
|
43
|
+
b.addEventListener('click', function () { goTo(i); });
|
|
44
|
+
dotsWrap.appendChild(b);
|
|
45
|
+
dots.push(b);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clamp(i) { return Math.max(0, Math.min(slides.length - 1, i)); }
|
|
50
|
+
|
|
51
|
+
function sync() {
|
|
52
|
+
dots.forEach(function (d, i) { d.setAttribute('aria-current', i === index ? 'true' : 'false'); });
|
|
53
|
+
if (prev) prev.disabled = index === 0;
|
|
54
|
+
if (next) next.disabled = index === slides.length - 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function goTo(i) {
|
|
58
|
+
index = clamp(i);
|
|
59
|
+
viewport.scrollTo({ left: index * viewport.clientWidth, behavior: 'smooth' });
|
|
60
|
+
sync();
|
|
61
|
+
root.dispatchEvent(new CustomEvent('fdy-carousel-change', { bubbles: true, detail: { index: index } }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (prev) prev.addEventListener('click', function () { goTo(index - 1); });
|
|
65
|
+
if (next) next.addEventListener('click', function () { goTo(index + 1); });
|
|
66
|
+
root.addEventListener('keydown', function (e) {
|
|
67
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(index - 1); }
|
|
68
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); goTo(index + 1); }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Keep index in sync when the user swipes/scrolls the viewport directly.
|
|
72
|
+
var ticking = false;
|
|
73
|
+
viewport.addEventListener('scroll', function () {
|
|
74
|
+
if (ticking) return;
|
|
75
|
+
ticking = true;
|
|
76
|
+
requestAnimationFrame(function () {
|
|
77
|
+
ticking = false;
|
|
78
|
+
var w = viewport.clientWidth || 1;
|
|
79
|
+
var i = clamp(Math.round(viewport.scrollLeft / w));
|
|
80
|
+
if (i !== index) { index = i; sync(); }
|
|
81
|
+
});
|
|
82
|
+
}, { passive: true });
|
|
83
|
+
|
|
84
|
+
var autoplay = parseInt(root.getAttribute('data-fdy-autoplay'), 10);
|
|
85
|
+
if (autoplay > 0) {
|
|
86
|
+
var timer = null;
|
|
87
|
+
var start = function () { if (!timer) timer = setInterval(function () { goTo(index >= slides.length - 1 ? 0 : index + 1); }, autoplay); };
|
|
88
|
+
var stop = function () { if (timer) { clearInterval(timer); timer = null; } };
|
|
89
|
+
root.addEventListener('mouseenter', stop);
|
|
90
|
+
root.addEventListener('mouseleave', start);
|
|
91
|
+
root.addEventListener('focusin', stop);
|
|
92
|
+
root.addEventListener('focusout', start);
|
|
93
|
+
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
94
|
+
if (!reduce) start();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
sync();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function initAll(context) {
|
|
101
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-carousel]'), initCarousel);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (document.readyState === 'loading') {
|
|
105
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
106
|
+
} else {
|
|
107
|
+
initAll();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
window.FreedayCarousel = { init: initCarousel, initAll: initAll };
|
|
111
|
+
})();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/* Freeday — cascade select enhancer (optional, zero-dependency).
|
|
2
|
+
* Turns a [data-fdy-cascade] wrapper into a hierarchical drill-down picker. The
|
|
3
|
+
* data model is a hidden nested <ul> inside the wrapper: a <li> with a child <ul>
|
|
4
|
+
* is a branch (drills in), a <li> without is a leaf (selects). Each <li> carries
|
|
5
|
+
* its value in data-value; its own text (before any nested <ul>) is the label.
|
|
6
|
+
*
|
|
7
|
+
* The popover shows one level at a time (a listbox); a branch drills in, the back
|
|
8
|
+
* control ascends, a leaf selects and stores the full path as the value.
|
|
9
|
+
* Attributes: data-label, data-placeholder, data-value (pre-select a leaf value),
|
|
10
|
+
* data-separator (path separator in the display, default " / ").
|
|
11
|
+
*
|
|
12
|
+
* Keyboard: trigger opens on click / Enter / Space / ArrowDown. In the list:
|
|
13
|
+
* Up/Down move, Home/End jump, ArrowRight/Enter/Space drill into a branch or
|
|
14
|
+
* select a leaf, ArrowLeft/Backspace go up a level, Esc closes (focus returns to
|
|
15
|
+
* the trigger). Emits a bubbling "fdy-cascade-change" (detail {value, path, labels}).
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var seq = 0;
|
|
21
|
+
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>';
|
|
22
|
+
|
|
23
|
+
// Own text of an <li>, excluding any nested <ul>.
|
|
24
|
+
function ownLabel(li) {
|
|
25
|
+
var s = '';
|
|
26
|
+
Array.prototype.forEach.call(li.childNodes, function (n) {
|
|
27
|
+
if (n.nodeType === 3) s += n.textContent;
|
|
28
|
+
else if (n.nodeType === 1 && n.tagName !== 'UL') s += n.textContent;
|
|
29
|
+
});
|
|
30
|
+
return s.trim();
|
|
31
|
+
}
|
|
32
|
+
function parse(ul) {
|
|
33
|
+
var nodes = [];
|
|
34
|
+
Array.prototype.forEach.call(ul.children, function (li) {
|
|
35
|
+
if (li.tagName !== 'LI') return;
|
|
36
|
+
var childUl = li.querySelector(':scope > ul');
|
|
37
|
+
nodes.push({
|
|
38
|
+
label: ownLabel(li),
|
|
39
|
+
value: li.getAttribute('data-value') || ownLabel(li),
|
|
40
|
+
children: childUl ? parse(childUl) : null
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return nodes;
|
|
44
|
+
}
|
|
45
|
+
// Depth-first search for the stack of labels leading to a leaf value.
|
|
46
|
+
function pathTo(nodes, value, trail) {
|
|
47
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
48
|
+
var n = nodes[i];
|
|
49
|
+
var here = trail.concat([n]);
|
|
50
|
+
if (!n.children && n.value === value) return here;
|
|
51
|
+
if (n.children) { var found = pathTo(n.children, value, here); if (found) return found; }
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function initCascade(wrap) {
|
|
57
|
+
if (wrap.dataset.fdyCascadeReady === '1') return;
|
|
58
|
+
wrap.dataset.fdyCascadeReady = '1';
|
|
59
|
+
wrap.classList.add('fdy-cascade');
|
|
60
|
+
|
|
61
|
+
var sourceUl = wrap.querySelector('ul');
|
|
62
|
+
var root = sourceUl ? parse(sourceUl) : [];
|
|
63
|
+
if (sourceUl) sourceUl.remove();
|
|
64
|
+
|
|
65
|
+
var label = wrap.getAttribute('data-label') || 'Pilih';
|
|
66
|
+
var placeholder = wrap.getAttribute('data-placeholder') || 'Pilih…';
|
|
67
|
+
var sep = wrap.getAttribute('data-separator') || ' / ';
|
|
68
|
+
|
|
69
|
+
var trigger = document.createElement('button');
|
|
70
|
+
trigger.type = 'button';
|
|
71
|
+
trigger.className = 'fdy-cascade__trigger';
|
|
72
|
+
trigger.setAttribute('aria-haspopup', 'true');
|
|
73
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
74
|
+
trigger.setAttribute('aria-label', label);
|
|
75
|
+
var valueSpan = document.createElement('span');
|
|
76
|
+
valueSpan.className = 'fdy-cascade__value';
|
|
77
|
+
trigger.appendChild(valueSpan);
|
|
78
|
+
|
|
79
|
+
var panel = document.createElement('div');
|
|
80
|
+
panel.className = 'fdy-cascade__panel';
|
|
81
|
+
panel.hidden = true;
|
|
82
|
+
var head = document.createElement('div');
|
|
83
|
+
head.className = 'fdy-cascade__head';
|
|
84
|
+
var back = document.createElement('button');
|
|
85
|
+
back.type = 'button';
|
|
86
|
+
back.className = 'fdy-cascade__back';
|
|
87
|
+
back.setAttribute('aria-label', 'Kembali satu tingkat');
|
|
88
|
+
back.innerHTML = BACK;
|
|
89
|
+
back.hidden = true;
|
|
90
|
+
var crumb = document.createElement('span');
|
|
91
|
+
crumb.className = 'fdy-cascade__crumb';
|
|
92
|
+
crumb.setAttribute('aria-live', 'polite');
|
|
93
|
+
head.appendChild(back);
|
|
94
|
+
head.appendChild(crumb);
|
|
95
|
+
var list = document.createElement('ul');
|
|
96
|
+
seq += 1;
|
|
97
|
+
list.className = 'fdy-cascade__list';
|
|
98
|
+
list.id = 'fdy-cascade-list-' + seq;
|
|
99
|
+
list.setAttribute('role', 'listbox');
|
|
100
|
+
list.setAttribute('aria-label', label);
|
|
101
|
+
list.tabIndex = -1;
|
|
102
|
+
panel.appendChild(head);
|
|
103
|
+
panel.appendChild(list);
|
|
104
|
+
|
|
105
|
+
wrap.appendChild(trigger);
|
|
106
|
+
wrap.appendChild(panel);
|
|
107
|
+
|
|
108
|
+
var stack = []; // branch nodes drilled through (context)
|
|
109
|
+
var current = root; // nodes at the visible level
|
|
110
|
+
var opts = []; // rendered <li> elements
|
|
111
|
+
var active = -1;
|
|
112
|
+
var selectedValue = wrap.getAttribute('data-value') || '';
|
|
113
|
+
|
|
114
|
+
function render() {
|
|
115
|
+
list.innerHTML = '';
|
|
116
|
+
opts = [];
|
|
117
|
+
back.hidden = stack.length === 0;
|
|
118
|
+
crumb.textContent = stack.length ? stack.map(function (n) { return n.label; }).join(sep) : label;
|
|
119
|
+
current.forEach(function (node, i) {
|
|
120
|
+
var li = document.createElement('li');
|
|
121
|
+
seq += 1; li.id = 'fdy-cascade-opt-' + seq;
|
|
122
|
+
li.className = 'fdy-cascade__opt';
|
|
123
|
+
li.setAttribute('role', 'option');
|
|
124
|
+
li.setAttribute('data-index', String(i));
|
|
125
|
+
var isBranch = !!node.children;
|
|
126
|
+
li.setAttribute('aria-selected', (!isBranch && node.value === selectedValue) ? 'true' : 'false');
|
|
127
|
+
if (isBranch) li.setAttribute('aria-label', node.label + ', submenu');
|
|
128
|
+
var lbl = document.createElement('span');
|
|
129
|
+
lbl.className = 'fdy-cascade__opt-label';
|
|
130
|
+
lbl.textContent = node.label;
|
|
131
|
+
li.appendChild(lbl);
|
|
132
|
+
if (isBranch) {
|
|
133
|
+
var arrow = document.createElement('span');
|
|
134
|
+
arrow.className = 'fdy-cascade__opt-arrow';
|
|
135
|
+
arrow.setAttribute('aria-hidden', 'true');
|
|
136
|
+
li.appendChild(arrow);
|
|
137
|
+
}
|
|
138
|
+
list.appendChild(li);
|
|
139
|
+
opts.push(li);
|
|
140
|
+
});
|
|
141
|
+
setActive(0);
|
|
142
|
+
}
|
|
143
|
+
function setActive(i) {
|
|
144
|
+
if (active >= 0 && opts[active]) opts[active].classList.remove('is-active');
|
|
145
|
+
active = Math.max(-1, Math.min(opts.length - 1, i));
|
|
146
|
+
if (active >= 0) {
|
|
147
|
+
opts[active].classList.add('is-active');
|
|
148
|
+
list.setAttribute('aria-activedescendant', opts[active].id);
|
|
149
|
+
opts[active].scrollIntoView({ block: 'nearest' });
|
|
150
|
+
} else {
|
|
151
|
+
list.removeAttribute('aria-activedescendant');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function drill(i) {
|
|
155
|
+
var node = current[i];
|
|
156
|
+
if (!node || !node.children) return;
|
|
157
|
+
stack.push(node);
|
|
158
|
+
current = node.children;
|
|
159
|
+
render();
|
|
160
|
+
}
|
|
161
|
+
function ascend() {
|
|
162
|
+
if (!stack.length) return;
|
|
163
|
+
stack.pop();
|
|
164
|
+
current = stack.length ? stack[stack.length - 1].children : root;
|
|
165
|
+
render();
|
|
166
|
+
}
|
|
167
|
+
function activate(i) {
|
|
168
|
+
var node = current[i];
|
|
169
|
+
if (!node) return;
|
|
170
|
+
if (node.children) { drill(i); return; }
|
|
171
|
+
selectedValue = node.value;
|
|
172
|
+
var labels = stack.map(function (n) { return n.label; }).concat([node.label]);
|
|
173
|
+
valueSpan.textContent = labels.join(sep);
|
|
174
|
+
valueSpan.classList.remove('fdy-cascade__value--placeholder');
|
|
175
|
+
wrap.dispatchEvent(new CustomEvent('fdy-cascade-change', { bubbles: true, detail: { value: node.value, path: labels.join(sep), labels: labels } }));
|
|
176
|
+
close(true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var _pop = null;
|
|
180
|
+
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
181
|
+
function open() {
|
|
182
|
+
if (!panel.hidden) return;
|
|
183
|
+
// Re-open at the selected leaf's level for quick re-selection.
|
|
184
|
+
var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
|
|
185
|
+
if (trail && trail.length > 1) { stack = trail.slice(0, -1); current = stack[stack.length - 1].children; }
|
|
186
|
+
else { stack = []; current = root; }
|
|
187
|
+
var p = popCtl(); if (p) p.show(); else panel.hidden = false;
|
|
188
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
189
|
+
trigger.classList.add('is-open');
|
|
190
|
+
render();
|
|
191
|
+
list.focus();
|
|
192
|
+
document.addEventListener('click', onDoc, true);
|
|
193
|
+
}
|
|
194
|
+
function close(returnFocus) {
|
|
195
|
+
if (panel.hidden) return;
|
|
196
|
+
var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
|
|
197
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
198
|
+
trigger.classList.remove('is-open');
|
|
199
|
+
document.removeEventListener('click', onDoc, true);
|
|
200
|
+
if (returnFocus === true) trigger.focus();
|
|
201
|
+
}
|
|
202
|
+
function onDoc(e) { if (!wrap.contains(e.target)) close(false); }
|
|
203
|
+
|
|
204
|
+
trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
|
|
205
|
+
trigger.addEventListener('keydown', function (e) {
|
|
206
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
|
|
207
|
+
});
|
|
208
|
+
back.addEventListener('click', function () { ascend(); list.focus(); });
|
|
209
|
+
list.addEventListener('keydown', function (e) {
|
|
210
|
+
switch (e.key) {
|
|
211
|
+
case 'ArrowDown': e.preventDefault(); setActive(active + 1); break;
|
|
212
|
+
case 'ArrowUp': e.preventDefault(); setActive(active - 1); break;
|
|
213
|
+
case 'Home': e.preventDefault(); setActive(0); break;
|
|
214
|
+
case 'End': e.preventDefault(); setActive(opts.length - 1); break;
|
|
215
|
+
case 'ArrowRight': case 'Enter': case ' ': e.preventDefault(); if (active >= 0) activate(active); break;
|
|
216
|
+
case 'ArrowLeft': case 'Backspace': e.preventDefault(); ascend(); break;
|
|
217
|
+
case 'Escape': e.preventDefault(); close(true); break;
|
|
218
|
+
case 'Tab': close(false); break;
|
|
219
|
+
default: break;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
list.addEventListener('click', function (e) {
|
|
223
|
+
var li = e.target.closest('[role="option"]');
|
|
224
|
+
if (li) activate(opts.indexOf(li));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Initial display: pre-selected leaf shows its full path, else placeholder.
|
|
228
|
+
var initTrail = selectedValue ? pathTo(root, selectedValue, []) : null;
|
|
229
|
+
if (initTrail) {
|
|
230
|
+
valueSpan.textContent = initTrail.map(function (n) { return n.label; }).join(sep);
|
|
231
|
+
} else {
|
|
232
|
+
valueSpan.textContent = placeholder;
|
|
233
|
+
valueSpan.classList.add('fdy-cascade__value--placeholder');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
var api = {
|
|
237
|
+
wrap: wrap,
|
|
238
|
+
getValue: function () { return selectedValue; },
|
|
239
|
+
clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
|
|
240
|
+
};
|
|
241
|
+
wrap._fdyCascade = api;
|
|
242
|
+
return api;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function initAll(context) {
|
|
246
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-cascade]'), initCascade);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (document.readyState === 'loading') {
|
|
250
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
251
|
+
} else {
|
|
252
|
+
initAll();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
window.FreedayCascade = { init: initCascade, initAll: initAll };
|
|
256
|
+
})();
|