@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,321 @@
|
|
|
1
|
+
/* Freeday — date picker enhancer (optional, zero-dependency).
|
|
2
|
+
* Builds an input-styled trigger + calendar popover from an empty wrapper.
|
|
3
|
+
* Locale comes from <html lang> (via Intl) — month/weekday/value formatting is not hardcoded.
|
|
4
|
+
*
|
|
5
|
+
* Markup contract:
|
|
6
|
+
* - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Tanggal unggah"
|
|
7
|
+
* data-placeholder="Pilih tanggal" data-min="2026-01-01" data-max="2026-12-31"></div>
|
|
8
|
+
* - Range: <div data-fdy-daterange role="group" aria-label="Rentang tanggal">
|
|
9
|
+
* <div data-fdy-datepicker data-role="from" data-placeholder="Dari"></div>
|
|
10
|
+
* <span class="fdy-daterange__sep">–</span>
|
|
11
|
+
* <div data-fdy-datepicker data-role="to" data-placeholder="Sampai"></div>
|
|
12
|
+
* </div>
|
|
13
|
+
* The range links the two: the end can never precede the start (out-of-range days disable).
|
|
14
|
+
*
|
|
15
|
+
* Emits a bubbling "fdy-datepicker-change" CustomEvent on the wrapper: detail {value, date}.
|
|
16
|
+
* Keyboard in the grid: ←/→ ±1 day, ↑/↓ ±1 week, Home/End week edge, PageUp/Down ±month,
|
|
17
|
+
* Shift+PageUp/Down ±year, Enter/Space select, Esc close (focus returns to the trigger).
|
|
18
|
+
*/
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
var LOCALE = document.documentElement.getAttribute('lang') || 'id';
|
|
23
|
+
var uidSeq = 0;
|
|
24
|
+
function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
|
|
25
|
+
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
26
|
+
function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
27
|
+
function parseISO(s) {
|
|
28
|
+
if (!s) return null;
|
|
29
|
+
var p = String(s).split('-');
|
|
30
|
+
if (p.length !== 3) return null;
|
|
31
|
+
var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
|
|
32
|
+
return isNaN(d.getTime()) ? null : d;
|
|
33
|
+
}
|
|
34
|
+
function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
|
|
35
|
+
function sameDay(a, b) { return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
36
|
+
function addDays(d, n) { var x = new Date(d); x.setDate(x.getDate() + n); return x; }
|
|
37
|
+
function addMonths(d, n) { var x = new Date(d); x.setMonth(x.getMonth() + n); return x; }
|
|
38
|
+
|
|
39
|
+
var monthFmt = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric' });
|
|
40
|
+
var valueFmt = new Intl.DateTimeFormat(LOCALE, { day: '2-digit', month: 'short', year: 'numeric' });
|
|
41
|
+
var dayLabelFmt = new Intl.DateTimeFormat(LOCALE, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
42
|
+
|
|
43
|
+
function weekdayHeaders() {
|
|
44
|
+
var monday = new Date(2024, 0, 1); // a known Monday
|
|
45
|
+
var fmt = new Intl.DateTimeFormat(LOCALE, { weekday: 'short' });
|
|
46
|
+
var out = [];
|
|
47
|
+
for (var i = 0; i < 7; i++) out.push(fmt.format(addDays(monday, i)));
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var CAL_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"></rect><path d="M16 2v4M8 2v4M3 10h18"></path></svg>';
|
|
52
|
+
|
|
53
|
+
function initPicker(wrap) {
|
|
54
|
+
if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
|
|
55
|
+
wrap.dataset.fdyDpReady = '1';
|
|
56
|
+
wrap.classList.add('fdy-datepicker');
|
|
57
|
+
|
|
58
|
+
var placeholder = wrap.getAttribute('data-placeholder') || 'Pilih tanggal';
|
|
59
|
+
var label = wrap.getAttribute('data-label') || 'Tanggal';
|
|
60
|
+
var selected = parseISO(wrap.getAttribute('data-value'));
|
|
61
|
+
var minDate = parseISO(wrap.getAttribute('data-min'));
|
|
62
|
+
var maxDate = parseISO(wrap.getAttribute('data-max'));
|
|
63
|
+
var rangeStart = null, rangeEnd = null; // for in-range shading
|
|
64
|
+
var view = startOfDay(selected || new Date());
|
|
65
|
+
view = new Date(view.getFullYear(), view.getMonth(), 1);
|
|
66
|
+
var focusDate = null;
|
|
67
|
+
|
|
68
|
+
var trigger = document.createElement('button');
|
|
69
|
+
trigger.type = 'button';
|
|
70
|
+
trigger.className = 'fdy-datepicker__trigger';
|
|
71
|
+
trigger.setAttribute('aria-haspopup', 'dialog');
|
|
72
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
73
|
+
trigger.setAttribute('aria-label', label);
|
|
74
|
+
var valueSpan = document.createElement('span');
|
|
75
|
+
valueSpan.className = 'fdy-datepicker__value';
|
|
76
|
+
trigger.appendChild(valueSpan);
|
|
77
|
+
// Icon variants (Freeday convention for JS-built controls): [data-fdy-no-icon] omits the
|
|
78
|
+
// icon; a [data-fdy-icon] child supplies a custom SVG; otherwise the default calendar.
|
|
79
|
+
if (!wrap.hasAttribute('data-fdy-no-icon')) {
|
|
80
|
+
var customIcon = wrap.querySelector('[data-fdy-icon]');
|
|
81
|
+
var icon = document.createElement('span');
|
|
82
|
+
icon.className = 'fdy-datepicker__icon';
|
|
83
|
+
icon.innerHTML = customIcon ? customIcon.innerHTML : CAL_ICON;
|
|
84
|
+
if (customIcon) customIcon.remove();
|
|
85
|
+
trigger.appendChild(icon);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var panel = document.createElement('div');
|
|
89
|
+
panel.className = 'fdy-datepicker__panel';
|
|
90
|
+
panel.setAttribute('role', 'dialog');
|
|
91
|
+
panel.setAttribute('aria-modal', 'false');
|
|
92
|
+
panel.setAttribute('aria-label', label);
|
|
93
|
+
panel.hidden = true;
|
|
94
|
+
|
|
95
|
+
wrap.appendChild(trigger);
|
|
96
|
+
wrap.appendChild(panel);
|
|
97
|
+
|
|
98
|
+
function updateDisplay() {
|
|
99
|
+
if (selected) {
|
|
100
|
+
valueSpan.textContent = valueFmt.format(selected);
|
|
101
|
+
valueSpan.classList.remove('fdy-datepicker__value--placeholder');
|
|
102
|
+
} else {
|
|
103
|
+
valueSpan.textContent = placeholder;
|
|
104
|
+
valueSpan.classList.add('fdy-datepicker__value--placeholder');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isDisabled(d) {
|
|
109
|
+
if (minDate && startOfDay(d).getTime() < startOfDay(minDate).getTime()) return true;
|
|
110
|
+
if (maxDate && startOfDay(d).getTime() > startOfDay(maxDate).getTime()) return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function navButton(glyph, aria, onClick) {
|
|
115
|
+
var b = document.createElement('button');
|
|
116
|
+
b.type = 'button';
|
|
117
|
+
b.className = 'fdy-cal__nav';
|
|
118
|
+
b.textContent = glyph;
|
|
119
|
+
b.setAttribute('aria-label', aria);
|
|
120
|
+
b.addEventListener('click', onClick);
|
|
121
|
+
return b;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function render() {
|
|
125
|
+
panel.innerHTML = '';
|
|
126
|
+
var head = document.createElement('div');
|
|
127
|
+
head.className = 'fdy-cal__head';
|
|
128
|
+
var title = document.createElement('div');
|
|
129
|
+
title.className = 'fdy-cal__title';
|
|
130
|
+
title.id = uid('fdy-cal-title');
|
|
131
|
+
title.textContent = monthFmt.format(view);
|
|
132
|
+
panel.setAttribute('aria-labelledby', title.id);
|
|
133
|
+
head.appendChild(navButton('‹', 'Bulan sebelumnya', function () { view = addMonths(view, -1); render(); }));
|
|
134
|
+
head.appendChild(title);
|
|
135
|
+
head.appendChild(navButton('›', 'Bulan berikutnya', function () { view = addMonths(view, 1); render(); }));
|
|
136
|
+
panel.appendChild(head);
|
|
137
|
+
|
|
138
|
+
var grid = document.createElement('div');
|
|
139
|
+
grid.className = 'fdy-cal__grid';
|
|
140
|
+
grid.setAttribute('role', 'grid');
|
|
141
|
+
grid.setAttribute('aria-labelledby', title.id);
|
|
142
|
+
weekdayHeaders().forEach(function (w) {
|
|
143
|
+
var c = document.createElement('div');
|
|
144
|
+
c.className = 'fdy-cal__dow';
|
|
145
|
+
c.setAttribute('role', 'columnheader');
|
|
146
|
+
c.setAttribute('aria-label', w);
|
|
147
|
+
c.textContent = w;
|
|
148
|
+
grid.appendChild(c);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
var first = new Date(view.getFullYear(), view.getMonth(), 1);
|
|
152
|
+
var offset = (first.getDay() + 6) % 7; // Monday-first
|
|
153
|
+
var startCell = addDays(first, -offset);
|
|
154
|
+
var today = new Date();
|
|
155
|
+
if (!focusDate) focusDate = selected || today;
|
|
156
|
+
// Keep the roving cell inside the visible month so the grid is always keyboard-reachable.
|
|
157
|
+
if (focusDate.getMonth() !== view.getMonth() || focusDate.getFullYear() !== view.getFullYear()) {
|
|
158
|
+
var dim = new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate();
|
|
159
|
+
focusDate = new Date(view.getFullYear(), view.getMonth(), Math.min(focusDate.getDate(), dim));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (var i = 0; i < 42; i++) {
|
|
163
|
+
var d = addDays(startCell, i);
|
|
164
|
+
var btn = document.createElement('button');
|
|
165
|
+
btn.type = 'button';
|
|
166
|
+
btn.className = 'fdy-cal__day';
|
|
167
|
+
btn.setAttribute('role', 'gridcell');
|
|
168
|
+
btn.setAttribute('aria-label', dayLabelFmt.format(d));
|
|
169
|
+
btn.textContent = d.getDate();
|
|
170
|
+
if (d.getMonth() !== view.getMonth()) btn.classList.add('is-outside');
|
|
171
|
+
if (sameDay(d, today)) btn.classList.add('is-today');
|
|
172
|
+
if (selected && sameDay(d, selected)) { btn.classList.add('is-selected'); btn.setAttribute('aria-selected', 'true'); }
|
|
173
|
+
if (rangeStart && rangeEnd) {
|
|
174
|
+
var t = startOfDay(d).getTime();
|
|
175
|
+
if (t > startOfDay(rangeStart).getTime() && t < startOfDay(rangeEnd).getTime()) btn.classList.add('in-range');
|
|
176
|
+
}
|
|
177
|
+
if (isDisabled(d)) btn.disabled = true;
|
|
178
|
+
btn.tabIndex = sameDay(d, focusDate) ? 0 : -1;
|
|
179
|
+
btn._date = d;
|
|
180
|
+
btn.addEventListener('click', (function (dd) { return function () { pick(dd); }; })(d));
|
|
181
|
+
grid.appendChild(btn);
|
|
182
|
+
}
|
|
183
|
+
grid.addEventListener('keydown', onGridKey);
|
|
184
|
+
panel.appendChild(grid);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function focusFocusDate() {
|
|
188
|
+
var cell = panel.querySelector('.fdy-cal__day[tabindex="0"]');
|
|
189
|
+
if (cell) cell.focus();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function moveFocus(next) {
|
|
193
|
+
focusDate = next;
|
|
194
|
+
if (next.getMonth() !== view.getMonth() || next.getFullYear() !== view.getFullYear()) {
|
|
195
|
+
view = new Date(next.getFullYear(), next.getMonth(), 1);
|
|
196
|
+
}
|
|
197
|
+
render();
|
|
198
|
+
focusFocusDate();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onGridKey(e) {
|
|
202
|
+
var handled = true;
|
|
203
|
+
switch (e.key) {
|
|
204
|
+
case 'ArrowLeft': moveFocus(addDays(focusDate, -1)); break;
|
|
205
|
+
case 'ArrowRight': moveFocus(addDays(focusDate, 1)); break;
|
|
206
|
+
case 'ArrowUp': moveFocus(addDays(focusDate, -7)); break;
|
|
207
|
+
case 'ArrowDown': moveFocus(addDays(focusDate, 7)); break;
|
|
208
|
+
case 'Home': moveFocus(addDays(focusDate, -((focusDate.getDay() + 6) % 7))); break;
|
|
209
|
+
case 'End': moveFocus(addDays(focusDate, 6 - ((focusDate.getDay() + 6) % 7))); break;
|
|
210
|
+
case 'PageUp': moveFocus(addMonths(focusDate, e.shiftKey ? -12 : -1)); break;
|
|
211
|
+
case 'PageDown': moveFocus(addMonths(focusDate, e.shiftKey ? 12 : 1)); break;
|
|
212
|
+
case 'Enter':
|
|
213
|
+
case ' ': pick(focusDate); break;
|
|
214
|
+
case 'Escape': close(true); break;
|
|
215
|
+
default: handled = false;
|
|
216
|
+
}
|
|
217
|
+
if (handled) e.preventDefault();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function pick(d) {
|
|
221
|
+
if (isDisabled(d)) return;
|
|
222
|
+
selected = startOfDay(d);
|
|
223
|
+
focusDate = selected;
|
|
224
|
+
updateDisplay();
|
|
225
|
+
wrap.dispatchEvent(new CustomEvent('fdy-datepicker-change', {
|
|
226
|
+
bubbles: true, detail: { value: toISO(selected), date: new Date(selected) }
|
|
227
|
+
}));
|
|
228
|
+
close(true);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
var _pop = null;
|
|
232
|
+
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
233
|
+
function open() {
|
|
234
|
+
if (!panel.hidden) return;
|
|
235
|
+
focusDate = selected || focusDate || new Date();
|
|
236
|
+
view = new Date(focusDate.getFullYear(), focusDate.getMonth(), 1);
|
|
237
|
+
var p = popCtl(); if (p) p.show(); else panel.hidden = false;
|
|
238
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
239
|
+
trigger.classList.add('is-open');
|
|
240
|
+
render();
|
|
241
|
+
focusFocusDate();
|
|
242
|
+
document.addEventListener('click', onDocClick, true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function close(returnFocus) {
|
|
246
|
+
if (panel.hidden) return;
|
|
247
|
+
var p = popCtl(); if (p) p.hide(); else panel.hidden = true;
|
|
248
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
249
|
+
trigger.classList.remove('is-open');
|
|
250
|
+
document.removeEventListener('click', onDocClick, true);
|
|
251
|
+
if (returnFocus === true) trigger.focus();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function onDocClick(e) {
|
|
255
|
+
if (!wrap.contains(e.target)) close(false);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
trigger.addEventListener('click', function () { panel.hidden ? open() : close(true); });
|
|
259
|
+
|
|
260
|
+
updateDisplay();
|
|
261
|
+
|
|
262
|
+
var api = {
|
|
263
|
+
wrap: wrap,
|
|
264
|
+
getValue: function () { return selected ? toISO(selected) : ''; },
|
|
265
|
+
clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
|
|
266
|
+
setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
|
|
267
|
+
setMax: function (iso) { maxDate = parseISO(iso); if (!panel.hidden) render(); },
|
|
268
|
+
setRange: function (fromIso, toIso) {
|
|
269
|
+
rangeStart = parseISO(fromIso);
|
|
270
|
+
rangeEnd = parseISO(toIso);
|
|
271
|
+
if (!panel.hidden) render();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
wrap._fdyDp = api;
|
|
275
|
+
return api;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function initRange(root) {
|
|
279
|
+
if (root.dataset.fdyDrReady === '1') return;
|
|
280
|
+
root.dataset.fdyDrReady = '1';
|
|
281
|
+
var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
|
|
282
|
+
var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
|
|
283
|
+
if (!fromEl || !toEl) return;
|
|
284
|
+
var fromApi = initPicker(fromEl);
|
|
285
|
+
var toApi = initPicker(toEl);
|
|
286
|
+
if (!fromApi || !toApi) return;
|
|
287
|
+
|
|
288
|
+
function sync() {
|
|
289
|
+
var f = fromApi.getValue();
|
|
290
|
+
var t = toApi.getValue();
|
|
291
|
+
toApi.setMin(f || null); // end can't precede start (one-directional, per Foundation)
|
|
292
|
+
fromApi.setRange(f, t); // start stays free — picking a later start clears the end below
|
|
293
|
+
toApi.setRange(f, t);
|
|
294
|
+
}
|
|
295
|
+
fromEl.addEventListener('fdy-datepicker-change', function () {
|
|
296
|
+
var f = fromApi.getValue(), t = toApi.getValue();
|
|
297
|
+
if (f && t && t < f) toApi.clear();
|
|
298
|
+
sync();
|
|
299
|
+
});
|
|
300
|
+
toEl.addEventListener('fdy-datepicker-change', function () {
|
|
301
|
+
var f = fromApi.getValue(), t = toApi.getValue();
|
|
302
|
+
if (f && t && t < f) fromApi.clear();
|
|
303
|
+
sync();
|
|
304
|
+
});
|
|
305
|
+
sync();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function initAll(context) {
|
|
309
|
+
var ctx = context || document;
|
|
310
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-daterange]'), initRange);
|
|
311
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-datepicker]'), initPicker);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (document.readyState === 'loading') {
|
|
315
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
316
|
+
} else {
|
|
317
|
+
initAll();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
window.FreedayDatepicker = { init: initPicker, initAll: initAll };
|
|
321
|
+
})();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* Freeday — datetime picker composer (optional, zero-dependency).
|
|
2
|
+
* Combines a child date picker and time picker into one value. It does not build
|
|
3
|
+
* anything itself — freeday-datepicker.js and freeday-timepicker.js enhance the
|
|
4
|
+
* children; this only listens for their change events and emits a single combined
|
|
5
|
+
* "fdy-datetime-change" (detail {date, time, value}). value is "YYYY-MM-DDTHH:MM"
|
|
6
|
+
* when both parts are set, else "". Icon variants are controlled per child picker
|
|
7
|
+
* (data-fdy-no-icon / <template data-fdy-icon>), so a datetime picker inherits them.
|
|
8
|
+
*
|
|
9
|
+
* Markup: <div data-fdy-datetimepicker>
|
|
10
|
+
* <div data-fdy-datepicker data-value="2026-07-21"></div>
|
|
11
|
+
* <div data-fdy-timepicker data-value="14:30"></div></div>
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
function initDatetime(wrap) {
|
|
17
|
+
if (wrap.dataset.fdyDtReady === '1') return;
|
|
18
|
+
wrap.dataset.fdyDtReady = '1';
|
|
19
|
+
wrap.classList.add('fdy-datetimepicker');
|
|
20
|
+
|
|
21
|
+
var dp = wrap.querySelector('[data-fdy-datepicker]');
|
|
22
|
+
var tp = wrap.querySelector('[data-fdy-timepicker]');
|
|
23
|
+
if (!dp || !tp) return;
|
|
24
|
+
|
|
25
|
+
var date = dp.getAttribute('data-value') || '';
|
|
26
|
+
var time = tp.getAttribute('data-value') || '';
|
|
27
|
+
|
|
28
|
+
function emit() {
|
|
29
|
+
var value = (date && time) ? (date + 'T' + time) : '';
|
|
30
|
+
wrap.dispatchEvent(new CustomEvent('fdy-datetime-change', {
|
|
31
|
+
bubbles: true, detail: { date: date, time: time, value: value }
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Listen for the children's bubbling change events (order-independent — no need
|
|
36
|
+
// for the child enhancers to have run before this composer).
|
|
37
|
+
wrap.addEventListener('fdy-datepicker-change', function (e) { date = e.detail.value; emit(); });
|
|
38
|
+
wrap.addEventListener('fdy-time-select', function (e) { time = e.detail.value; emit(); });
|
|
39
|
+
|
|
40
|
+
var api = {
|
|
41
|
+
wrap: wrap,
|
|
42
|
+
getValue: function () { return (date && time) ? (date + 'T' + time) : ''; }
|
|
43
|
+
};
|
|
44
|
+
wrap._fdyDt = api;
|
|
45
|
+
return api;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function initAll(context) {
|
|
49
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-datetimepicker]'), initDatetime);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (document.readyState === 'loading') {
|
|
53
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
54
|
+
} else {
|
|
55
|
+
initAll();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.FreedayDatetime = { init: initDatetime, initAll: initAll };
|
|
59
|
+
})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Freeday — drawer enhancer (optional, zero-dependency).
|
|
2
|
+
* A trigger [data-fdy-drawer="drawerId"] opens the <dialog class="fdy-drawer" id="drawerId">
|
|
3
|
+
* as a modal side panel (native showModal gives focus-trap, Esc, and return-focus). Clicking
|
|
4
|
+
* the backdrop or any [data-close] inside closes it.
|
|
5
|
+
*/
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
function initDrawer(dialog) {
|
|
10
|
+
if (dialog.dataset.fdyDrawerReady === '1') return;
|
|
11
|
+
dialog.dataset.fdyDrawerReady = '1';
|
|
12
|
+
// Click on the backdrop (target is the dialog box itself) closes it.
|
|
13
|
+
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
|
|
14
|
+
Array.prototype.forEach.call(dialog.querySelectorAll('[data-close]'), function (b) {
|
|
15
|
+
b.addEventListener('click', function () { dialog.close(); });
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function initTriggers(context) {
|
|
20
|
+
var ctx = context || document;
|
|
21
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-drawer]'), function (btn) {
|
|
22
|
+
if (btn.dataset.fdyDrawerBound === '1') return;
|
|
23
|
+
var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
|
|
24
|
+
if (!dialog) return;
|
|
25
|
+
initDrawer(dialog);
|
|
26
|
+
btn.dataset.fdyDrawerBound = '1';
|
|
27
|
+
btn.addEventListener('click', function () {
|
|
28
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
29
|
+
else dialog.setAttribute('open', '');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
// Init any drawer dialogs even without a wired trigger (backdrop/close still work).
|
|
33
|
+
Array.prototype.forEach.call(ctx.querySelectorAll('.fdy-drawer'), initDrawer);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (document.readyState === 'loading') {
|
|
37
|
+
document.addEventListener('DOMContentLoaded', function () { initTriggers(); });
|
|
38
|
+
} else {
|
|
39
|
+
initTriggers();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
window.FreedayDrawer = { init: initTriggers, initAll: initTriggers };
|
|
43
|
+
})();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* Freeday — form validation enhancer (optional, zero-dependency).
|
|
2
|
+
* Wires the native Constraint Validation API to accessible inline errors on any
|
|
3
|
+
* [data-fdy-validate] <form>. Native-first: constraints come from the standard
|
|
4
|
+
* markup attributes (required, type, pattern, min/max, minlength/maxlength, step);
|
|
5
|
+
* this only handles presentation + the accessible plumbing.
|
|
6
|
+
*
|
|
7
|
+
* Per control it toggles aria-invalid (which the CSS mirrors to the --error border)
|
|
8
|
+
* and shows the message in a linked [data-fdy-error] element (auto-created inside
|
|
9
|
+
* the .fdy-field if absent, and wired via aria-describedby).
|
|
10
|
+
*
|
|
11
|
+
* Custom messages via data attributes on the control (first match wins):
|
|
12
|
+
* data-fdy-msg-required | -type | -pattern | -minlength | -maxlength |
|
|
13
|
+
* data-fdy-msg-min | -max | -step | -mismatch, then generic data-fdy-msg.
|
|
14
|
+
* Cross-field match (e.g. confirm password): data-fdy-match="#otherId".
|
|
15
|
+
*
|
|
16
|
+
* Timing: validates on submit (blocks + focuses the first invalid control); after
|
|
17
|
+
* the first submit attempt each field re-validates on input/blur so errors clear
|
|
18
|
+
* live. Add data-fdy-eager to validate on blur from the start.
|
|
19
|
+
*
|
|
20
|
+
* Emits bubbling CustomEvents on the form: "fdy-form-invalid" (detail {invalid:[]})
|
|
21
|
+
* and "fdy-form-valid". Exposes window.FreedayForm.
|
|
22
|
+
*/
|
|
23
|
+
(function () {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
var seq = 0;
|
|
27
|
+
|
|
28
|
+
// ValidityState key -> friendly data-attribute alias.
|
|
29
|
+
var ALIAS = {
|
|
30
|
+
valueMissing: 'required',
|
|
31
|
+
typeMismatch: 'type',
|
|
32
|
+
patternMismatch: 'pattern',
|
|
33
|
+
tooShort: 'minlength',
|
|
34
|
+
tooLong: 'maxlength',
|
|
35
|
+
rangeUnderflow: 'min',
|
|
36
|
+
rangeOverflow: 'max',
|
|
37
|
+
stepMismatch: 'step',
|
|
38
|
+
badInput: 'type',
|
|
39
|
+
customError: 'mismatch'
|
|
40
|
+
};
|
|
41
|
+
var DEFAULTS = {
|
|
42
|
+
required: 'Wajib diisi.',
|
|
43
|
+
type: 'Format tidak valid.',
|
|
44
|
+
pattern: 'Format tidak sesuai.',
|
|
45
|
+
minlength: 'Terlalu pendek.',
|
|
46
|
+
maxlength: 'Terlalu panjang.',
|
|
47
|
+
min: 'Nilai terlalu kecil.',
|
|
48
|
+
max: 'Nilai terlalu besar.',
|
|
49
|
+
step: 'Nilai tidak sesuai kelipatan.',
|
|
50
|
+
mismatch: 'Nilai tidak cocok.'
|
|
51
|
+
};
|
|
52
|
+
var VALIDITY_KEYS = ['valueMissing', 'typeMismatch', 'patternMismatch', 'tooShort', 'tooLong', 'rangeUnderflow', 'rangeOverflow', 'stepMismatch', 'badInput'];
|
|
53
|
+
|
|
54
|
+
function isCandidate(el) {
|
|
55
|
+
if (el.disabled || el.type === 'hidden' || el.type === 'submit' || el.type === 'button' || el.type === 'reset') return false;
|
|
56
|
+
return typeof el.checkValidity === 'function' && el.willValidate;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fieldOf(el) { return el.closest('.fdy-field') || el.parentElement; }
|
|
60
|
+
|
|
61
|
+
function ensureErrorEl(el) {
|
|
62
|
+
var field = fieldOf(el);
|
|
63
|
+
var errEl = field ? field.querySelector('[data-fdy-error]') : null;
|
|
64
|
+
if (!errEl) {
|
|
65
|
+
errEl = document.createElement('p');
|
|
66
|
+
errEl.className = 'fdy-help fdy-help--error';
|
|
67
|
+
errEl.setAttribute('data-fdy-error', '');
|
|
68
|
+
errEl.hidden = true;
|
|
69
|
+
(field || el.parentElement).appendChild(errEl);
|
|
70
|
+
}
|
|
71
|
+
if (!errEl.id) { seq += 1; errEl.id = 'fdy-err-' + seq; }
|
|
72
|
+
// Link the control to its message without clobbering existing describedby.
|
|
73
|
+
var described = (el.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
|
|
74
|
+
if (described.indexOf(errEl.id) === -1) { described.push(errEl.id); el.setAttribute('aria-describedby', described.join(' ')); }
|
|
75
|
+
return errEl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function messageFor(el) {
|
|
79
|
+
// Cross-field match runs first (drives customError via setCustomValidity).
|
|
80
|
+
el.setCustomValidity('');
|
|
81
|
+
var matchSel = el.getAttribute('data-fdy-match');
|
|
82
|
+
if (matchSel) {
|
|
83
|
+
var other = document.querySelector(matchSel);
|
|
84
|
+
if (other && el.value !== other.value) {
|
|
85
|
+
el.setCustomValidity(el.getAttribute('data-fdy-msg-mismatch') || el.getAttribute('data-fdy-msg') || DEFAULTS.mismatch);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (el.validity.valid) return '';
|
|
89
|
+
var alias = el.validity.customError ? 'mismatch' : null;
|
|
90
|
+
if (!alias) {
|
|
91
|
+
for (var i = 0; i < VALIDITY_KEYS.length; i++) {
|
|
92
|
+
if (el.validity[VALIDITY_KEYS[i]]) { alias = ALIAS[VALIDITY_KEYS[i]]; break; }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var custom = alias ? el.getAttribute('data-fdy-msg-' + alias) : null;
|
|
96
|
+
return custom || el.getAttribute('data-fdy-msg') || (alias && DEFAULTS[alias]) || el.validationMessage || 'Tidak valid.';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function paint(el, message) {
|
|
100
|
+
var errEl = ensureErrorEl(el);
|
|
101
|
+
if (message) {
|
|
102
|
+
el.setAttribute('aria-invalid', 'true');
|
|
103
|
+
errEl.textContent = message;
|
|
104
|
+
errEl.hidden = false;
|
|
105
|
+
} else {
|
|
106
|
+
el.removeAttribute('aria-invalid');
|
|
107
|
+
errEl.textContent = '';
|
|
108
|
+
errEl.hidden = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function initForm(form) {
|
|
113
|
+
if (form.dataset.fdyFormReady === '1') return;
|
|
114
|
+
form.dataset.fdyFormReady = '1';
|
|
115
|
+
form.setAttribute('novalidate', '');
|
|
116
|
+
var eager = form.hasAttribute('data-fdy-eager');
|
|
117
|
+
var submitted = false;
|
|
118
|
+
|
|
119
|
+
function controls() {
|
|
120
|
+
return Array.prototype.filter.call(form.elements, isCandidate);
|
|
121
|
+
}
|
|
122
|
+
function validateOne(el) {
|
|
123
|
+
var msg = messageFor(el);
|
|
124
|
+
paint(el, msg);
|
|
125
|
+
return !msg;
|
|
126
|
+
}
|
|
127
|
+
function liveBind(el) {
|
|
128
|
+
if (el.dataset.fdyFieldBound === '1') return;
|
|
129
|
+
el.dataset.fdyFieldBound = '1';
|
|
130
|
+
var live = function () { if (submitted || eager) validateOne(el); };
|
|
131
|
+
el.addEventListener('blur', live, true);
|
|
132
|
+
el.addEventListener('input', function () { if ((submitted || eager) && el.getAttribute('aria-invalid') === 'true') validateOne(el); });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Array.prototype.forEach.call(controls(), liveBind);
|
|
136
|
+
|
|
137
|
+
form.addEventListener('submit', function (e) {
|
|
138
|
+
submitted = true;
|
|
139
|
+
var invalid = [];
|
|
140
|
+
Array.prototype.forEach.call(controls(), function (el) {
|
|
141
|
+
liveBind(el);
|
|
142
|
+
if (!validateOne(el)) invalid.push(el);
|
|
143
|
+
});
|
|
144
|
+
if (invalid.length) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
invalid[0].focus();
|
|
147
|
+
form.dispatchEvent(new CustomEvent('fdy-form-invalid', { bubbles: true, detail: { invalid: invalid } }));
|
|
148
|
+
} else {
|
|
149
|
+
form.dispatchEvent(new CustomEvent('fdy-form-valid', { bubbles: true }));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
var api = {
|
|
154
|
+
form: form,
|
|
155
|
+
validate: function () {
|
|
156
|
+
submitted = true;
|
|
157
|
+
var invalid = [];
|
|
158
|
+
Array.prototype.forEach.call(controls(), function (el) { if (!validateOne(el)) invalid.push(el); });
|
|
159
|
+
return invalid;
|
|
160
|
+
},
|
|
161
|
+
reset: function () {
|
|
162
|
+
submitted = false;
|
|
163
|
+
Array.prototype.forEach.call(controls(), function (el) { paint(el, ''); });
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
form._fdyForm = api;
|
|
167
|
+
return api;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function initAll(context) {
|
|
171
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-validate]'), initForm);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (document.readyState === 'loading') {
|
|
175
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
176
|
+
} else {
|
|
177
|
+
initAll();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
window.FreedayForm = { init: initForm, initAll: initAll };
|
|
181
|
+
})();
|