@cahyo-dimas/freeday 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/adapters/blazor/freeday-blazor.js +79 -0
  5. package/adapters/react/components/FdyAutocomplete.tsx +159 -0
  6. package/adapters/react/components/FdyCascade.tsx +230 -0
  7. package/adapters/react/components/FdyCfl.tsx +362 -0
  8. package/adapters/react/components/FdyChart.tsx +69 -0
  9. package/adapters/react/components/FdyCombo.tsx +166 -0
  10. package/adapters/react/components/FdyDateRange.tsx +66 -0
  11. package/adapters/react/components/FdyDatepicker.tsx +354 -0
  12. package/adapters/react/index.d.ts +37 -0
  13. package/adapters/react/index.js +9 -0
  14. package/adapters/react/tsconfig.json +14 -0
  15. package/adapters/react/useFreeday.js +44 -0
  16. package/adapters/react/usePopover.ts +64 -0
  17. package/adapters/vue/components/FdyAutocomplete.vue +174 -0
  18. package/adapters/vue/components/FdyCascade.vue +241 -0
  19. package/adapters/vue/components/FdyCfl.vue +354 -0
  20. package/adapters/vue/components/FdyChart.vue +89 -0
  21. package/adapters/vue/components/FdyCombo.vue +232 -0
  22. package/adapters/vue/components/FdyDateRange.vue +78 -0
  23. package/adapters/vue/components/FdyDatepicker.vue +333 -0
  24. package/adapters/vue/index.d.ts +44 -0
  25. package/adapters/vue/index.js +8 -0
  26. package/adapters/vue/useFreeday.js +44 -0
  27. package/adapters/vue/usePopover.ts +78 -0
  28. package/dist/freeday-autocomplete.js +135 -0
  29. package/dist/freeday-breakpoint.js +51 -0
  30. package/dist/freeday-carousel.js +111 -0
  31. package/dist/freeday-cascade.js +256 -0
  32. package/dist/freeday-cfl.js +213 -0
  33. package/dist/freeday-chart.js +403 -0
  34. package/dist/freeday-chip.js +83 -0
  35. package/dist/freeday-datepicker.js +321 -0
  36. package/dist/freeday-datetime.js +59 -0
  37. package/dist/freeday-drawer.js +43 -0
  38. package/dist/freeday-form.js +181 -0
  39. package/dist/freeday-mask.js +114 -0
  40. package/dist/freeday-menu.js +93 -0
  41. package/dist/freeday-popover.js +69 -0
  42. package/dist/freeday-rating.js +50 -0
  43. package/dist/freeday-select.js +194 -0
  44. package/dist/freeday-slider.js +34 -0
  45. package/dist/freeday-stepper.js +90 -0
  46. package/dist/freeday-table.js +475 -0
  47. package/dist/freeday-tabs.js +68 -0
  48. package/dist/freeday-timepicker.js +180 -0
  49. package/dist/freeday-toast.js +84 -0
  50. package/dist/freeday-tree.js +94 -0
  51. package/dist/freeday-upload.js +206 -0
  52. package/dist/freeday.bundle.css +1352 -0
  53. package/dist/freeday.css +1007 -0
  54. package/dist/freeday.js +3632 -0
  55. package/dist/freeday.tokens.css +343 -0
  56. package/package.json +72 -0
  57. package/src/base.css +20 -0
  58. package/src/components/.gitkeep +0 -0
  59. package/src/components/accordion.css +13 -0
  60. package/src/components/alert.css +18 -0
  61. package/src/components/app-shell.css +88 -0
  62. package/src/components/appbar.css +15 -0
  63. package/src/components/autocomplete.css +12 -0
  64. package/src/components/avatar.css +8 -0
  65. package/src/components/badge.css +27 -0
  66. package/src/components/breadcrumb.css +9 -0
  67. package/src/components/breakpoints.css +15 -0
  68. package/src/components/button.css +58 -0
  69. package/src/components/card.css +9 -0
  70. package/src/components/carousel.css +17 -0
  71. package/src/components/cascade.css +36 -0
  72. package/src/components/cfl.css +28 -0
  73. package/src/components/chart.css +55 -0
  74. package/src/components/chip.css +18 -0
  75. package/src/components/combo.css +27 -0
  76. package/src/components/datepicker.css +44 -0
  77. package/src/components/datetimepicker.css +8 -0
  78. package/src/components/description-list.css +6 -0
  79. package/src/components/divider.css +3 -0
  80. package/src/components/drawer.css +25 -0
  81. package/src/components/file-upload.css +34 -0
  82. package/src/components/filterbar.css +25 -0
  83. package/src/components/form-grid.css +8 -0
  84. package/src/components/input-group.css +22 -0
  85. package/src/components/input.css +13 -0
  86. package/src/components/kbd.css +2 -0
  87. package/src/components/menu.css +28 -0
  88. package/src/components/modal.css +22 -0
  89. package/src/components/pagination.css +8 -0
  90. package/src/components/progress.css +7 -0
  91. package/src/components/rating.css +10 -0
  92. package/src/components/selection.css +25 -0
  93. package/src/components/skeleton.css +7 -0
  94. package/src/components/slider.css +12 -0
  95. package/src/components/spinner.css +6 -0
  96. package/src/components/states.css +8 -0
  97. package/src/components/stepper.css +21 -0
  98. package/src/components/table.css +55 -0
  99. package/src/components/tabs.css +10 -0
  100. package/src/components/timeline.css +12 -0
  101. package/src/components/timepicker.css +22 -0
  102. package/src/components/toast.css +17 -0
  103. package/src/components/tooltip.css +6 -0
  104. package/src/components/tree.css +21 -0
  105. package/src/freeday-autocomplete.js +135 -0
  106. package/src/freeday-breakpoint.js +51 -0
  107. package/src/freeday-carousel.js +111 -0
  108. package/src/freeday-cascade.js +256 -0
  109. package/src/freeday-cfl.js +213 -0
  110. package/src/freeday-chart.js +403 -0
  111. package/src/freeday-chip.js +83 -0
  112. package/src/freeday-datepicker.js +321 -0
  113. package/src/freeday-datetime.js +59 -0
  114. package/src/freeday-drawer.js +43 -0
  115. package/src/freeday-form.js +181 -0
  116. package/src/freeday-mask.js +114 -0
  117. package/src/freeday-menu.js +93 -0
  118. package/src/freeday-popover.js +69 -0
  119. package/src/freeday-rating.js +50 -0
  120. package/src/freeday-select.js +194 -0
  121. package/src/freeday-slider.js +34 -0
  122. package/src/freeday-stepper.js +90 -0
  123. package/src/freeday-table.js +475 -0
  124. package/src/freeday-tabs.js +68 -0
  125. package/src/freeday-timepicker.js +180 -0
  126. package/src/freeday-toast.js +84 -0
  127. package/src/freeday-tree.js +94 -0
  128. package/src/freeday-upload.js +206 -0
  129. package/tokens/breakpoints.mjs +4 -0
  130. package/tokens/tokens.json +123 -0
@@ -0,0 +1,180 @@
1
+ /* Freeday — time picker enhancer (optional, zero-dependency).
2
+ * Builds an input-styled trigger + scrollable time-list popover (WAI-ARIA listbox)
3
+ * from an empty [data-fdy-timepicker] wrapper. 24h HH:MM. Options run from
4
+ * data-min (default 00:00) to data-max (default 23:59) every data-step minutes
5
+ * (default 30). Attributes: data-value, data-label, data-placeholder.
6
+ *
7
+ * Icon variants (Freeday convention, same as the date picker): [data-fdy-no-icon]
8
+ * omits the icon; a [data-fdy-icon] child supplies a custom SVG; otherwise a clock.
9
+ *
10
+ * Keyboard: trigger opens on click / Enter / Space / ArrowDown; in the list,
11
+ * Up/Down move, Home/End jump, Enter/Space select, Esc closes (focus returns to
12
+ * the trigger). Emits a bubbling "fdy-time-select" CustomEvent (detail {value}).
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ var seq = 0;
18
+ 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>';
19
+
20
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
21
+ function valid(s) { return /^\d{1,2}:\d{2}$/.test(s || ''); }
22
+ function toMin(s) { var p = s.split(':'); return (+p[0]) * 60 + (+p[1]); }
23
+ function fromMin(m) { return pad(Math.floor(m / 60)) + ':' + pad(m % 60); }
24
+ function norm(s) { return valid(s) ? fromMin(toMin(s)) : ''; }
25
+
26
+ function initTime(wrap) {
27
+ if (wrap.dataset.fdyTpReady === '1') return;
28
+ wrap.dataset.fdyTpReady = '1';
29
+ wrap.classList.add('fdy-timepicker');
30
+
31
+ var label = wrap.getAttribute('data-label') || 'Pilih waktu';
32
+ var placeholder = wrap.getAttribute('data-placeholder') || '--:--';
33
+ var step = Math.max(1, parseInt(wrap.getAttribute('data-step') || '30', 10));
34
+ var minM = valid(wrap.getAttribute('data-min')) ? toMin(wrap.getAttribute('data-min')) : 0;
35
+ var maxM = valid(wrap.getAttribute('data-max')) ? toMin(wrap.getAttribute('data-max')) : 23 * 60 + 59;
36
+ var value = norm(wrap.getAttribute('data-value'));
37
+
38
+ var trigger = document.createElement('button');
39
+ trigger.type = 'button';
40
+ trigger.className = 'fdy-timepicker__trigger';
41
+ trigger.setAttribute('aria-haspopup', 'listbox');
42
+ trigger.setAttribute('aria-expanded', 'false');
43
+ trigger.setAttribute('aria-label', label);
44
+ var valueSpan = document.createElement('span');
45
+ valueSpan.className = 'fdy-timepicker__value';
46
+ trigger.appendChild(valueSpan);
47
+ if (!wrap.hasAttribute('data-fdy-no-icon')) {
48
+ var customIcon = wrap.querySelector('[data-fdy-icon]');
49
+ var icon = document.createElement('span');
50
+ icon.className = 'fdy-timepicker__icon';
51
+ icon.innerHTML = customIcon ? customIcon.innerHTML : CLOCK;
52
+ if (customIcon) customIcon.remove();
53
+ trigger.appendChild(icon);
54
+ }
55
+
56
+ var panel = document.createElement('ul');
57
+ panel.className = 'fdy-timepicker__panel';
58
+ panel.setAttribute('role', 'listbox');
59
+ panel.setAttribute('aria-label', label);
60
+ panel.tabIndex = -1;
61
+ panel.hidden = true;
62
+
63
+ var opts = [];
64
+ for (var m = minM; m <= maxM; m += step) {
65
+ var t = fromMin(m);
66
+ var li = document.createElement('li');
67
+ seq += 1; li.id = 'fdy-tp-opt-' + seq;
68
+ li.className = 'fdy-timepicker__opt';
69
+ li.setAttribute('role', 'option');
70
+ li.setAttribute('data-value', t);
71
+ li.setAttribute('aria-selected', t === value ? 'true' : 'false');
72
+ li.textContent = t;
73
+ panel.appendChild(li);
74
+ opts.push(li);
75
+ }
76
+
77
+ wrap.appendChild(trigger);
78
+ wrap.appendChild(panel);
79
+
80
+ var active = -1;
81
+
82
+ function updateDisplay() {
83
+ if (value) {
84
+ valueSpan.textContent = value;
85
+ valueSpan.classList.remove('fdy-timepicker__value--placeholder');
86
+ } else {
87
+ valueSpan.textContent = placeholder;
88
+ valueSpan.classList.add('fdy-timepicker__value--placeholder');
89
+ }
90
+ }
91
+ function indexOfValue() {
92
+ for (var i = 0; i < opts.length; i++) { if (opts[i].getAttribute('data-value') === value) return i; }
93
+ return -1;
94
+ }
95
+ function setActive(i) {
96
+ if (active >= 0 && opts[active]) opts[active].classList.remove('is-active');
97
+ active = i;
98
+ if (i >= 0 && opts[i]) {
99
+ opts[i].classList.add('is-active');
100
+ panel.setAttribute('aria-activedescendant', opts[i].id);
101
+ opts[i].scrollIntoView({ block: 'nearest' });
102
+ } else {
103
+ panel.removeAttribute('aria-activedescendant');
104
+ }
105
+ }
106
+ var _pop = null;
107
+ function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
108
+ function open() {
109
+ if (!panel.hidden) return;
110
+ var p = popCtl(); if (p) p.show(); else panel.hidden = false;
111
+ trigger.setAttribute('aria-expanded', 'true');
112
+ trigger.classList.add('is-open');
113
+ var i = indexOfValue();
114
+ setActive(i >= 0 ? i : 0);
115
+ panel.focus();
116
+ document.addEventListener('click', onDoc, true);
117
+ }
118
+ function close(returnFocus) {
119
+ if (panel.hidden) return;
120
+ var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
121
+ trigger.setAttribute('aria-expanded', 'false');
122
+ trigger.classList.remove('is-open');
123
+ panel.removeAttribute('aria-activedescendant');
124
+ document.removeEventListener('click', onDoc, true);
125
+ if (returnFocus === true) trigger.focus();
126
+ }
127
+ function onDoc(e) { if (!wrap.contains(e.target)) close(false); }
128
+ function choose(i) {
129
+ if (i < 0 || i >= opts.length) return;
130
+ value = opts[i].getAttribute('data-value');
131
+ for (var j = 0; j < opts.length; j++) opts[j].setAttribute('aria-selected', j === i ? 'true' : 'false');
132
+ updateDisplay();
133
+ wrap.dispatchEvent(new CustomEvent('fdy-time-select', { bubbles: true, detail: { value: value } }));
134
+ close(true);
135
+ }
136
+
137
+ trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
138
+ trigger.addEventListener('keydown', function (e) {
139
+ if (e.key === 'ArrowDown') { e.preventDefault(); open(); }
140
+ });
141
+ panel.addEventListener('keydown', function (e) {
142
+ switch (e.key) {
143
+ case 'ArrowDown': e.preventDefault(); setActive(Math.min(opts.length - 1, active + 1)); break;
144
+ case 'ArrowUp': e.preventDefault(); setActive(Math.max(0, active - 1)); break;
145
+ case 'Home': e.preventDefault(); setActive(0); break;
146
+ case 'End': e.preventDefault(); setActive(opts.length - 1); break;
147
+ case 'Enter': case ' ': e.preventDefault(); choose(active); break;
148
+ case 'Escape': e.preventDefault(); close(true); break;
149
+ case 'Tab': close(false); break;
150
+ default: break;
151
+ }
152
+ });
153
+ panel.addEventListener('click', function (e) {
154
+ var li = e.target.closest('[role="option"]');
155
+ if (li) choose(opts.indexOf(li));
156
+ });
157
+
158
+ updateDisplay();
159
+
160
+ var api = {
161
+ wrap: wrap,
162
+ getValue: function () { return value; },
163
+ clear: function () { value = ''; for (var i = 0; i < opts.length; i++) opts[i].setAttribute('aria-selected', 'false'); updateDisplay(); }
164
+ };
165
+ wrap._fdyTp = api;
166
+ return api;
167
+ }
168
+
169
+ function initAll(context) {
170
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-timepicker]'), initTime);
171
+ }
172
+
173
+ if (document.readyState === 'loading') {
174
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
175
+ } else {
176
+ initAll();
177
+ }
178
+
179
+ window.FreedayTimepicker = { init: initTime, initAll: initAll };
180
+ })();
@@ -0,0 +1,84 @@
1
+ /* Freeday — toast API (optional, zero-dependency).
2
+ * Freeday.toast({ variant, title, message, timeout }) shows a transient
3
+ * notification in a live region (created on first use). Returns the element.
4
+ * variant: 'success' | 'warning' | 'danger' | 'info' (default: neutral)
5
+ * timeout: ms before auto-dismiss (default 4000; 0 = sticky)
6
+ * Danger toasts use role="alert"; others role="status".
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ var region = null;
12
+
13
+ function ensureRegion() {
14
+ if (region && document.body.contains(region)) return region;
15
+ region = document.querySelector('.fdy-toast-region');
16
+ if (!region) {
17
+ region = document.createElement('div');
18
+ region.className = 'fdy-toast-region';
19
+ region.setAttribute('aria-live', 'polite');
20
+ region.setAttribute('aria-atomic', 'false');
21
+ document.body.appendChild(region);
22
+ }
23
+ return region;
24
+ }
25
+
26
+ function dismiss(toast) {
27
+ if (!toast || toast.dataset.leaving === '1') return;
28
+ toast.dataset.leaving = '1';
29
+ toast.classList.add('is-leaving');
30
+ var remove = function () { if (toast.parentNode) toast.parentNode.removeChild(toast); };
31
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
32
+ if (reduce) { remove(); return; }
33
+ var done = false;
34
+ var finish = function () { if (!done) { done = true; remove(); } };
35
+ toast.addEventListener('transitionend', finish);
36
+ setTimeout(finish, 400); // fallback if transitionend never fires
37
+ }
38
+
39
+ function el(tag, className, text) {
40
+ var node = document.createElement(tag);
41
+ if (className) node.className = className;
42
+ if (text != null) node.textContent = text;
43
+ return node;
44
+ }
45
+
46
+ function toast(opts) {
47
+ opts = opts || {};
48
+ var region = ensureRegion();
49
+ var node = el('div', 'fdy-toast' + (opts.variant ? ' fdy-toast--' + opts.variant : ''));
50
+ node.setAttribute('role', opts.variant === 'danger' ? 'alert' : 'status');
51
+
52
+ var accent = el('span', 'fdy-toast__accent');
53
+ accent.setAttribute('aria-hidden', 'true');
54
+ var body = el('div', 'fdy-toast__body');
55
+ if (opts.title) body.appendChild(el('div', 'fdy-toast__title', opts.title));
56
+ if (opts.message) body.appendChild(el('div', 'fdy-toast__text', opts.message));
57
+
58
+ var close = el('button', 'fdy-toast__close');
59
+ close.type = 'button';
60
+ close.setAttribute('aria-label', 'Tutup');
61
+ close.innerHTML = '&times;';
62
+ close.addEventListener('click', function () { dismiss(node); });
63
+
64
+ node.appendChild(accent);
65
+ node.appendChild(body);
66
+ node.appendChild(close);
67
+ region.appendChild(node);
68
+
69
+ var timeout = opts.timeout == null ? 4000 : opts.timeout;
70
+ if (timeout > 0) setTimeout(function () { dismiss(node); }, timeout);
71
+ return node;
72
+ }
73
+
74
+ // Pre-create the live region so the FIRST status toast is reliably announced
75
+ // (some screen readers ignore a live region created in the same tick as its content).
76
+ if (document.readyState === 'loading') {
77
+ document.addEventListener('DOMContentLoaded', ensureRegion);
78
+ } else {
79
+ ensureRegion();
80
+ }
81
+
82
+ window.Freeday = window.Freeday || {};
83
+ window.Freeday.toast = toast;
84
+ })();
@@ -0,0 +1,94 @@
1
+ /* Freeday — tree checkbox cascade (optional, zero-dependency).
2
+ * Turns a [data-fdy-tree] checkbox tree into a selectable one: checking a branch checks
3
+ * every descendant; a partially-selected branch shows the native :indeterminate state.
4
+ * Rows are native <input class="fdy-checkbox">, labelled via aria-label — Space toggles.
5
+ * Selection never toggles the <details> (the checkbox stops click propagation).
6
+ *
7
+ * Markup contract:
8
+ * <ul class="fdy-tree fdy-tree--checkbox" data-fdy-tree>
9
+ * <li>
10
+ * <details class="fdy-tree__branch">
11
+ * <summary>…chevron… <input class="fdy-checkbox fdy-tree__check" type="checkbox" aria-label="…"> …icon… Label</summary>
12
+ * <ul> …child <li>s (branches or .fdy-tree__leaf leaves)… </ul>
13
+ * </details>
14
+ * </li>
15
+ * <li class="fdy-tree__leaf"><input class="fdy-checkbox fdy-tree__check" type="checkbox" aria-label="…"> …icon… Label</li>
16
+ * </ul>
17
+ */
18
+ (function () {
19
+ 'use strict';
20
+
21
+ // The <li>s directly under a branch's own child list (skip text nodes).
22
+ function childItems(li) {
23
+ var ul = li.querySelector(':scope > details > ul');
24
+ if (!ul) return [];
25
+ return Array.prototype.filter.call(ul.children, function (n) { return n.tagName === 'LI'; });
26
+ }
27
+
28
+ // The checkbox belonging to a row's own <li> — branch summary or leaf, never a child's.
29
+ function ownBox(li) {
30
+ return li.querySelector(':scope > details > summary input[type="checkbox"]')
31
+ || li.querySelector(':scope > input[type="checkbox"]');
32
+ }
33
+
34
+ function setSubtree(li, checked) {
35
+ childItems(li).forEach(function (child) {
36
+ var box = ownBox(child);
37
+ if (box) { box.checked = checked; box.indeterminate = false; }
38
+ setSubtree(child, checked);
39
+ });
40
+ }
41
+
42
+ // Recompute one branch's box from the state of its direct children.
43
+ function recompute(li) {
44
+ var box = ownBox(li);
45
+ var kids = childItems(li).map(ownBox).filter(Boolean);
46
+ if (!box || !kids.length) return;
47
+ var allOn = kids.every(function (b) { return b.checked && !b.indeterminate; });
48
+ var anyOn = kids.some(function (b) { return b.checked || b.indeterminate; });
49
+ box.checked = allOn;
50
+ box.indeterminate = anyOn && !allOn;
51
+ }
52
+
53
+ function refreshAncestors(li) {
54
+ var parent = li.parentElement ? li.parentElement.closest('li') : null;
55
+ while (parent) {
56
+ recompute(parent);
57
+ parent = parent.parentElement ? parent.parentElement.closest('li') : null;
58
+ }
59
+ }
60
+
61
+ // Post-order pass so pre-checked leaves resolve their branches on load.
62
+ function initState(li) {
63
+ childItems(li).forEach(initState);
64
+ recompute(li);
65
+ }
66
+
67
+ function initTree(tree) {
68
+ if (tree.dataset.fdyTreeReady === '1') return;
69
+ tree.dataset.fdyTreeReady = '1';
70
+ Array.prototype.forEach.call(tree.querySelectorAll('input[type="checkbox"]'), function (box) {
71
+ // A branch checkbox lives inside <summary>; keep its click from toggling the branch.
72
+ box.addEventListener('click', function (e) { e.stopPropagation(); });
73
+ box.addEventListener('change', function () {
74
+ var li = box.closest('li');
75
+ setSubtree(li, box.checked);
76
+ box.indeterminate = false;
77
+ refreshAncestors(li);
78
+ });
79
+ });
80
+ Array.prototype.filter.call(tree.children, function (n) { return n.tagName === 'LI'; }).forEach(initState);
81
+ }
82
+
83
+ function initAll(context) {
84
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-tree]'), initTree);
85
+ }
86
+
87
+ if (document.readyState === 'loading') {
88
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
89
+ } else {
90
+ initAll();
91
+ }
92
+
93
+ window.FreedayTree = { init: initTree, initAll: initAll };
94
+ })();
@@ -0,0 +1,206 @@
1
+ /* Freeday — file-upload enhancer (optional, zero-dependency).
2
+ * A drop target that also opens the file dialog on click / Enter / Space, validates each file
3
+ * (accept + max size), and renders a per-file row with explicit state. Actual upload is the
4
+ * consumer's job — this is a controlled reference: wire the emitted event's row API to your
5
+ * upload call, or set data-fdy-upload-simulate for a demo progress animation.
6
+ *
7
+ * Markup contract:
8
+ * <div class="fdy-dropzone" data-fdy-dropzone role="button" tabindex="0"
9
+ * data-max-size="10485760" data-filelist="#dz-list" [data-fdy-upload-simulate]
10
+ * aria-label="Unggah berkas — seret ke sini atau tekan Enter">
11
+ * <span class="fdy-dropzone__icon">…svg…</span>
12
+ * <span class="fdy-dropzone__title">…</span>
13
+ * <span class="fdy-dropzone__hint">…</span>
14
+ * <input type="file" accept=".pdf,application/pdf" multiple hidden>
15
+ * </div>
16
+ * <div class="fdy-filelist" id="dz-list"></div>
17
+ *
18
+ * Emits bubbling CustomEvents on the dropzone: "fdy-upload-add" {file, rejected, reason, row}
19
+ * (row exposes .uploading()/.setProgress(pct)/.done()/.fail(msg)) and "fdy-upload-remove" {file}.
20
+ */
21
+ (function () {
22
+ 'use strict';
23
+
24
+ function fmtSize(bytes) {
25
+ if (bytes < 1024) return bytes + ' B';
26
+ if (bytes < 1048576) return (bytes / 1024).toFixed(0) + ' KB';
27
+ return (bytes / 1048576).toFixed(1) + ' MB';
28
+ }
29
+
30
+ 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>';
31
+ 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>';
32
+ 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>';
33
+
34
+ function accepts(file, acceptAttr) {
35
+ if (!acceptAttr) return true;
36
+ var name = file.name.toLowerCase();
37
+ var type = (file.type || '').toLowerCase();
38
+ return acceptAttr.split(',').map(function (s) { return s.trim().toLowerCase(); }).some(function (a) {
39
+ if (!a) return false;
40
+ if (a.charAt(0) === '.') return name.slice(-a.length) === a;
41
+ if (a.slice(-2) === '/*') return type.indexOf(a.slice(0, -1)) === 0;
42
+ return type === a;
43
+ });
44
+ }
45
+
46
+ function makeRow(file) {
47
+ var el = document.createElement('div');
48
+ el.className = 'fdy-file';
49
+ var icon = document.createElement('span');
50
+ icon.className = 'fdy-file__icon';
51
+ icon.innerHTML = FILE_ICON;
52
+ var meta = document.createElement('div');
53
+ meta.className = 'fdy-file__meta';
54
+ var name = document.createElement('div');
55
+ name.className = 'fdy-file__name';
56
+ name.textContent = file.name;
57
+ var sub = document.createElement('div');
58
+ sub.className = 'fdy-file__sub';
59
+ sub.textContent = fmtSize(file.size);
60
+ meta.appendChild(name);
61
+ meta.appendChild(sub);
62
+ var remove = document.createElement('button');
63
+ remove.type = 'button';
64
+ remove.className = 'fdy-file__remove';
65
+ remove.setAttribute('aria-label', 'Hapus ' + file.name);
66
+ remove.innerHTML = '&times;';
67
+ remove.addEventListener('click', function () {
68
+ el.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
69
+ el.remove();
70
+ });
71
+ el.appendChild(icon);
72
+ el.appendChild(meta);
73
+ el.appendChild(remove);
74
+
75
+ var progressWrap = null, bar = null, progressEl = null;
76
+ function ensureProgress() {
77
+ if (progressWrap) return;
78
+ progressWrap = document.createElement('div');
79
+ progressWrap.className = 'fdy-file__progress';
80
+ progressEl = document.createElement('div');
81
+ progressEl.className = 'fdy-progress';
82
+ progressEl.setAttribute('role', 'progressbar');
83
+ progressEl.setAttribute('aria-valuemin', '0');
84
+ progressEl.setAttribute('aria-valuemax', '100');
85
+ progressEl.setAttribute('aria-label', 'Progres unggah ' + file.name);
86
+ bar = document.createElement('div');
87
+ bar.className = 'fdy-progress__bar';
88
+ bar.style.width = '0%';
89
+ progressEl.appendChild(bar);
90
+ progressWrap.appendChild(progressEl);
91
+ meta.appendChild(progressWrap);
92
+ }
93
+ function dropProgress() {
94
+ if (progressWrap) { progressWrap.remove(); progressWrap = null; bar = null; progressEl = null; }
95
+ }
96
+ return {
97
+ el: el,
98
+ uploading: function () {
99
+ el.classList.remove('fdy-file--error', 'fdy-file--success');
100
+ icon.innerHTML = FILE_ICON;
101
+ sub.textContent = fmtSize(file.size) + ' · Mengunggah…';
102
+ ensureProgress();
103
+ },
104
+ setProgress: function (pct) {
105
+ ensureProgress();
106
+ var v = Math.max(0, Math.min(100, pct));
107
+ bar.style.width = v + '%';
108
+ progressEl.setAttribute('aria-valuenow', String(Math.round(v)));
109
+ },
110
+ done: function () {
111
+ el.classList.add('fdy-file--success');
112
+ el.classList.remove('fdy-file--error');
113
+ icon.innerHTML = OK_ICON;
114
+ sub.textContent = fmtSize(file.size) + ' · Terunggah';
115
+ dropProgress();
116
+ },
117
+ fail: function (msg) {
118
+ el.classList.add('fdy-file--error');
119
+ el.classList.remove('fdy-file--success');
120
+ icon.innerHTML = ERR_ICON;
121
+ sub.textContent = msg;
122
+ dropProgress();
123
+ }
124
+ };
125
+ }
126
+
127
+ function simulateUpload(row) {
128
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
129
+ if (reduce) { row.setProgress(100); row.done(); return; }
130
+ var pct = 0;
131
+ var id = setInterval(function () {
132
+ pct += 12 + Math.random() * 18;
133
+ if (pct >= 100) {
134
+ row.setProgress(100);
135
+ clearInterval(id);
136
+ setTimeout(function () { row.done(); }, 200);
137
+ } else {
138
+ row.setProgress(pct);
139
+ }
140
+ }, 180);
141
+ }
142
+
143
+ function initDropzone(zone) {
144
+ if (zone.dataset.fdyUploadReady === '1') return;
145
+ zone.dataset.fdyUploadReady = '1';
146
+
147
+ var input = zone.querySelector('input[type="file"]');
148
+ var listSel = zone.getAttribute('data-filelist');
149
+ var list = listSel ? document.querySelector(listSel)
150
+ : (zone.parentNode ? zone.parentNode.querySelector('.fdy-filelist') : null);
151
+ var maxSize = parseInt(zone.getAttribute('data-max-size'), 10) || 0;
152
+ var simulate = zone.hasAttribute('data-fdy-upload-simulate');
153
+ var acceptAttr = input ? input.getAttribute('accept') : '';
154
+
155
+ function openDialog() { if (input) input.click(); }
156
+
157
+ zone.addEventListener('click', function (e) { if (e.target !== input) openDialog(); });
158
+ zone.addEventListener('keydown', function (e) {
159
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDialog(); }
160
+ });
161
+ ['dragenter', 'dragover'].forEach(function (ev) {
162
+ zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add('is-dragover'); });
163
+ });
164
+ ['dragleave', 'dragend'].forEach(function (ev) {
165
+ zone.addEventListener(ev, function (e) { if (e.target === zone) zone.classList.remove('is-dragover'); });
166
+ });
167
+ zone.addEventListener('drop', function (e) {
168
+ e.preventDefault();
169
+ zone.classList.remove('is-dragover');
170
+ handleFiles(e.dataTransfer ? e.dataTransfer.files : null);
171
+ });
172
+ if (input) input.addEventListener('change', function () { handleFiles(input.files); input.value = ''; });
173
+
174
+ function handleFiles(fileList) {
175
+ if (!list || !fileList) return;
176
+ Array.prototype.slice.call(fileList).forEach(function (file) {
177
+ var reason = null;
178
+ if (!accepts(file, acceptAttr)) reason = 'Tipe berkas tidak didukung.';
179
+ else if (maxSize && file.size > maxSize) reason = 'Ukuran melebihi batas (' + fmtSize(maxSize) + ').';
180
+ var row = makeRow(file);
181
+ list.appendChild(row.el);
182
+ if (reason) {
183
+ row.fail(reason);
184
+ } else {
185
+ row.uploading();
186
+ if (simulate) simulateUpload(row);
187
+ }
188
+ zone.dispatchEvent(new CustomEvent('fdy-upload-add', {
189
+ bubbles: true, detail: { file: file, rejected: !!reason, reason: reason, row: row }
190
+ }));
191
+ });
192
+ }
193
+ }
194
+
195
+ function initAll(context) {
196
+ Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-dropzone]'), initDropzone);
197
+ }
198
+
199
+ if (document.readyState === 'loading') {
200
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
201
+ } else {
202
+ initAll();
203
+ }
204
+
205
+ window.FreedayUpload = { init: initDropzone, initAll: initAll };
206
+ })();