@cahyo-dimas/freeday 1.18.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/COMPONENTS.md +775 -0
  3. package/README.id.md +11 -1
  4. package/README.md +18 -2
  5. package/USAGE.md +55 -11
  6. package/adapters/blazor/FdyTable.razor.cs +66 -7
  7. package/adapters/blazor/TableTypes.cs +6 -0
  8. package/adapters/react/components/FdyTable.tsx +32 -8
  9. package/adapters/vue/components/FdyTable.vue +30 -6
  10. package/dist/freeday.bundle.css +91 -9
  11. package/dist/freeday.css +75 -6
  12. package/dist/freeday.tokens.css +16 -3
  13. package/docs/agent-onboarding.md +153 -0
  14. package/docs/getting-started.md +456 -0
  15. package/docs/integrations.md +321 -0
  16. package/docs/reference-screen.html +461 -0
  17. package/package.json +10 -4
  18. package/src/base.css +15 -0
  19. package/src/components/accordion.css +5 -1
  20. package/src/components/card.css +7 -1
  21. package/src/components/carousel.css +4 -1
  22. package/src/components/list.css +36 -0
  23. package/src/components/table.css +5 -2
  24. package/src/components/tabs.css +2 -1
  25. package/tokens/breakpoints.d.ts +3 -0
  26. package/tokens/breakpoints.mjs +8 -1
  27. package/src/components/.gitkeep +0 -0
  28. package/src/freeday-autocomplete.js +0 -135
  29. package/src/freeday-breakpoint.js +0 -51
  30. package/src/freeday-carousel.js +0 -111
  31. package/src/freeday-cascade.js +0 -256
  32. package/src/freeday-cfl.js +0 -213
  33. package/src/freeday-chart.js +0 -429
  34. package/src/freeday-chip.js +0 -83
  35. package/src/freeday-datepicker.js +0 -321
  36. package/src/freeday-datetime.js +0 -83
  37. package/src/freeday-drawer.js +0 -43
  38. package/src/freeday-form.js +0 -181
  39. package/src/freeday-mask.js +0 -114
  40. package/src/freeday-menu.js +0 -93
  41. package/src/freeday-popover.js +0 -69
  42. package/src/freeday-rating.js +0 -50
  43. package/src/freeday-select.js +0 -218
  44. package/src/freeday-slider.js +0 -34
  45. package/src/freeday-stepper.js +0 -90
  46. package/src/freeday-table.js +0 -475
  47. package/src/freeday-tabs.js +0 -68
  48. package/src/freeday-timepicker.js +0 -180
  49. package/src/freeday-toast.js +0 -105
  50. package/src/freeday-tree.js +0 -94
  51. package/src/freeday-upload.js +0 -206
@@ -1,5 +1,8 @@
1
1
  /* Freeday — Table */
2
- .fdy-table-wrap{overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
2
+ /* position:relative on every scroller/clipper — it makes the box the containing block for its own
3
+ absolutely positioned content (.fdy-visually-hidden above all), which `overflow` alone does NOT
4
+ clip. Without it a hidden label in a wide table scrolls the whole page. See base.css. */
5
+ .fdy-table-wrap{position:relative;overflow-x:auto;border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);}
3
6
  .fdy-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
4
7
  .fdy-table caption{text-align:left;padding:var(--space-3) var(--space-4);font-weight:var(--weight-semibold);color:var(--color-text);}
5
8
  .fdy-table th{text-align:left;padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:var(--weight-semibold);text-transform:uppercase;letter-spacing:var(--tracking-wide);color:var(--color-text-muted);background:var(--color-surface-2);border-bottom:var(--bw) solid var(--color-border);white-space:nowrap;}
@@ -36,7 +39,7 @@
36
39
 
37
40
  /* Data-table shell (toolbar + scroll + footer stay put while the table scrolls) */
38
41
  .fdy-datatable{border:var(--bw) solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);background:var(--color-surface);overflow:hidden;}
39
- .fdy-table-scroll{overflow-x:auto;}
42
+ .fdy-table-scroll{position:relative;overflow-x:auto;}
40
43
  .fdy-table-toolbar{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;padding:var(--space-3) var(--space-4);border-bottom:var(--bw) solid var(--color-border);}
41
44
  .fdy-table-toolbar__search{max-width:18rem;}
42
45
  .fdy-table-toolbar__spacer{flex:1;}
@@ -1,5 +1,6 @@
1
1
  /* Freeday — Tabs (WAI-ARIA APG) */
2
- .fdy-tabs__list{display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
2
+ /* position:relative — containing block for out-of-flow content in the tabs (see base.css). */
3
+ .fdy-tabs__list{position:relative;display:flex;gap:var(--space-1);border-bottom:var(--bw) solid var(--color-border);overflow-x:auto;}
3
4
  .fdy-tabs__tab{appearance:none;border:0;background:transparent;cursor:pointer;white-space:nowrap;font-family:var(--font-body);font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--color-text-muted);padding:var(--space-3) var(--space-4);border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--dur-fast) var(--ease-standard),border-color var(--dur-fast) var(--ease-standard);}
4
5
  .fdy-tabs__tab:hover{color:var(--color-text);}
5
6
  .fdy-tabs__tab:disabled,.fdy-tabs__tab[aria-disabled="true"]{opacity:.5;cursor:not-allowed;color:var(--color-text-muted);}
@@ -1,6 +1,9 @@
1
1
  // Types for tokens/breakpoints.mjs — the JS-side breakpoint scale in px
2
2
  // (mirrors the sm/md/lg/xl utilities in src/components/breakpoints.css).
3
3
  export declare const breakpoints: {
4
+ /** Width at which .fdy-app's sidebar turns from off-canvas drawer into a static column.
5
+ * Not part of the sm/md/lg/xl ramp — it mirrors app-shell.css and nothing else. */
6
+ nav: number;
4
7
  sm: number;
5
8
  md: number;
6
9
  lg: number;
@@ -1,4 +1,11 @@
1
1
  // Freeday breakpoint scale in px — mirrors src/components/breakpoints.css utilities
2
2
  // (sm/md/lg/xl). CSS @media can't read custom properties, so this is the JS-side
3
3
  // source of the scale for matchMedia / app @media alignment.
4
- export const breakpoints = { sm: 600, md: 960, lg: 1280, xl: 1920 };
4
+ //
5
+ // `nav` is not part of the sm/md/lg/xl ramp: it is the width at which .fdy-app switches the
6
+ // sidebar from an off-canvas drawer (below) to a static column (at and above), hard-wired in
7
+ // src/components/app-shell.css. Anything that has to agree with the shell — a matchMedia guard
8
+ // deciding whether the nav toggle sets --nav-open or --nav-collapsed, a utility-framework
9
+ // variant — must use THIS number, not md. Aligning such code to md (960) leaves 721–959px
10
+ // broken: the sidebar is already static while the script still thinks it is an overlay.
11
+ export const breakpoints = { nav: 721, sm: 600, md: 960, lg: 1280, xl: 1920 };
File without changes
@@ -1,135 +0,0 @@
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
- })();
@@ -1,51 +0,0 @@
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
- })();
@@ -1,111 +0,0 @@
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
- })();
@@ -1,256 +0,0 @@
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
- })();