@cahyo-dimas/freeday 2.1.0 → 3.0.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 +244 -0
- package/COMPONENTS.md +191 -18
- package/README.id.md +1 -1
- package/README.md +1 -1
- package/USAGE.md +1 -1
- package/adapters/blazor/FdyAppShell.razor +1 -1
- package/adapters/blazor/FdyAppShell.razor.cs +35 -0
- package/adapters/blazor/FdyAutocomplete.razor +4 -1
- package/adapters/blazor/FdyAutocomplete.razor.cs +26 -0
- package/adapters/blazor/FdyCascade.razor +6 -1
- package/adapters/blazor/FdyCascade.razor.cs +24 -0
- package/adapters/blazor/FdyCombo.razor +1 -0
- package/adapters/blazor/FdyCombo.razor.cs +13 -0
- package/adapters/blazor/FdyDatepicker.razor +15 -1
- package/adapters/blazor/FdyDatepicker.razor.cs +46 -0
- package/adapters/blazor/FdyTable.razor +44 -4
- package/adapters/blazor/FdyTable.razor.cs +110 -0
- package/adapters/blazor/freeday-blazor.js +8 -0
- package/adapters/react/components/FdyAppShell.tsx +52 -11
- package/adapters/react/components/FdyDrawer.tsx +3 -1
- package/adapters/react/components/FdyModal.tsx +3 -1
- package/adapters/react/components/FdyTable.tsx +123 -2
- package/adapters/vue/components/FdyAppShell.vue +41 -11
- package/adapters/vue/components/FdyDrawer.vue +4 -1
- package/adapters/vue/components/FdyModal.vue +4 -1
- package/adapters/vue/components/FdyTable.vue +125 -4
- package/dist/freeday-app-shell.js +23 -5
- package/dist/freeday-autocomplete.js +17 -1
- package/dist/freeday-busy.js +168 -0
- package/dist/freeday-cascade.js +53 -3
- package/dist/freeday-chart.js +33 -3
- package/dist/freeday-datepicker.js +109 -17
- package/dist/freeday-select.js +18 -1
- package/dist/freeday-stepper.js +54 -4
- package/dist/freeday-table.js +4 -2
- package/dist/freeday-timepicker.js +19 -1
- package/dist/freeday.bundle.css +616 -39
- package/dist/freeday.css +93 -11
- package/dist/freeday.js +499 -37
- package/dist/freeday.tokens.css +523 -28
- package/docs/agent-onboarding.md +4 -0
- package/docs/getting-started.md +1 -1
- package/package.json +4 -3
- package/src/components/app-shell.css +29 -5
- package/src/components/appbar.css +2 -2
- package/src/components/busy.css +33 -0
- package/src/components/card.css +1 -1
- package/src/components/drawer.css +1 -1
- package/src/components/menu.css +1 -1
- package/src/components/modal.css +1 -1
- package/src/components/stepper.css +11 -0
- package/src/components/table.css +13 -0
- package/tokens/tokens.json +54 -10
package/dist/freeday.js
CHANGED
|
@@ -62,7 +62,13 @@
|
|
|
62
62
|
|
|
63
63
|
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
64
64
|
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
65
|
-
|
|
65
|
+
/* Whether the nav FLOATS. Two ways to be true, and only one of them is the viewport: below the
|
|
66
|
+
breakpoint it is off-canvas by definition, and above it `--nav-overlay` says the app chose to
|
|
67
|
+
float a nav that could have been a column. Everything downstream — which class means visible,
|
|
68
|
+
what the toggle does, whether the content goes inert — asks this instead of the media query,
|
|
69
|
+
so overlay mode reuses the drawer's whole code path rather than growing a second one. */
|
|
70
|
+
function isOverlayMode() { return !mqWide.matches || app.classList.contains('fdy-app--nav-overlay'); }
|
|
71
|
+
function navVisible() { return isOverlayMode() ? isOverlayOpen() : !isCollapsed(); }
|
|
66
72
|
|
|
67
73
|
/* aria-expanded answers "is the nav showing?" in BOTH modes, the two state classes are the
|
|
68
74
|
kit's business, not the reader's. */
|
|
@@ -70,7 +76,7 @@
|
|
|
70
76
|
var visible = navVisible();
|
|
71
77
|
toggle.setAttribute('aria-expanded', String(visible));
|
|
72
78
|
setInert(sidebar, !visible);
|
|
73
|
-
setInert(content,
|
|
79
|
+
setInert(content, isOverlayMode() && visible);
|
|
74
80
|
/* Announce only real changes. The first sync() runs at init to describe the state the markup
|
|
75
81
|
arrived in, which is not something a host asked for and must not look like one. */
|
|
76
82
|
if (lastVisible !== null && visible !== lastVisible) {
|
|
@@ -108,7 +114,7 @@
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
toggle.addEventListener('click', function () {
|
|
111
|
-
if (
|
|
117
|
+
if (!isOverlayMode()) {
|
|
112
118
|
app.classList.toggle('fdy-app--nav-collapsed');
|
|
113
119
|
sync();
|
|
114
120
|
} else if (isOverlayOpen()) {
|
|
@@ -156,7 +162,10 @@
|
|
|
156
162
|
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
157
163
|
can no longer be clicked or read. */
|
|
158
164
|
mqWide.addEventListener('change', function () {
|
|
159
|
-
|
|
165
|
+
/* Only when the panel stops floating. In overlay MODE it floats at every width, so widening
|
|
166
|
+
must leave an open panel exactly as it is — closing it there would be the shell overruling
|
|
167
|
+
a reader who never asked for anything. */
|
|
168
|
+
if (mqWide.matches && !isOverlayMode() && isOverlayOpen()) close(false);
|
|
160
169
|
else sync();
|
|
161
170
|
});
|
|
162
171
|
|
|
@@ -166,9 +175,14 @@
|
|
|
166
175
|
own state needs to drive this without reaching for the class names the kit reserves. */
|
|
167
176
|
app._fdyAppShell = {
|
|
168
177
|
isVisible: navVisible,
|
|
178
|
+
/* Re-read the DOM and reconcile `inert` + `aria-expanded`. Needed when something OUTSIDE the
|
|
179
|
+
enhancer changes what the state classes mean — switching `--nav-overlay` on or off does
|
|
180
|
+
exactly that, because it moves the answer to "is the nav visible?" from `--nav-collapsed`
|
|
181
|
+
to `--nav-open`. Without this a mode switch leaves a visible sidebar marked inert. */
|
|
182
|
+
refresh: sync,
|
|
169
183
|
setVisible: function (visible) {
|
|
170
184
|
if (visible === navVisible()) return;
|
|
171
|
-
if (
|
|
185
|
+
if (!isOverlayMode()) {
|
|
172
186
|
app.classList.toggle('fdy-app--nav-collapsed', !visible);
|
|
173
187
|
sync();
|
|
174
188
|
} else if (visible) {
|
|
@@ -205,6 +219,10 @@
|
|
|
205
219
|
isVisible: function (root) {
|
|
206
220
|
return !!(root && root._fdyAppShell && root._fdyAppShell.isVisible());
|
|
207
221
|
},
|
|
222
|
+
/* Call after changing `--nav-overlay` from outside; see the note on the handle above. */
|
|
223
|
+
refresh: function (root) {
|
|
224
|
+
if (root && root._fdyAppShell) root._fdyAppShell.refresh();
|
|
225
|
+
},
|
|
208
226
|
};
|
|
209
227
|
})();
|
|
210
228
|
|
|
@@ -250,6 +268,9 @@
|
|
|
250
268
|
var _pop = null;
|
|
251
269
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(listbox, input); return _pop; }
|
|
252
270
|
function open() {
|
|
271
|
+
/* The input carries these natively; a disabled one fires nothing, but a READONLY one still
|
|
272
|
+
takes focus and clicks, and the list used to open over a field nobody can edit. */
|
|
273
|
+
if (input.readOnly || input.disabled) return;
|
|
253
274
|
if (!listbox.hidden) return;
|
|
254
275
|
var p = popCtl(); if (p) p.show(); else listbox.hidden = false;
|
|
255
276
|
input.setAttribute('aria-expanded', 'true');
|
|
@@ -344,7 +365,20 @@
|
|
|
344
365
|
initAll();
|
|
345
366
|
}
|
|
346
367
|
|
|
347
|
-
|
|
368
|
+
/* Same contract as the combo's: the states live on the input natively, and a host that stops
|
|
369
|
+
re-rendering after hydration needs a way to change them afterwards. */
|
|
370
|
+
function setState(root, state) {
|
|
371
|
+
var input = root ? root.querySelector('input') : null;
|
|
372
|
+
if (!input || !state) return;
|
|
373
|
+
if (state.disabled != null) input.disabled = !!state.disabled;
|
|
374
|
+
if (state.readonly != null) input.readOnly = !!state.readonly;
|
|
375
|
+
if (state.invalid != null) {
|
|
376
|
+
if (state.invalid) input.setAttribute('aria-invalid', 'true');
|
|
377
|
+
else input.removeAttribute('aria-invalid');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
window.FreedayAutocomplete = { init: initAutocomplete, initAll: initAll, setState: setState };
|
|
348
382
|
})();
|
|
349
383
|
|
|
350
384
|
/* Freeday, breakpoint provider (optional, zero-dependency).
|
|
@@ -399,6 +433,175 @@
|
|
|
399
433
|
};
|
|
400
434
|
})();
|
|
401
435
|
|
|
436
|
+
/* Freeday, busy overlay (optional, zero-dependency).
|
|
437
|
+
*
|
|
438
|
+
* Freeday.busy({ caption, delay, mark }) block the screen while an operation runs
|
|
439
|
+
* Freeday.idle() release it
|
|
440
|
+
*
|
|
441
|
+
* Imperative on purpose, like Freeday.toast(): a component API invites two instances, and two
|
|
442
|
+
* blocking overlays with two captions is the failure this exists to prevent. A second busy() while
|
|
443
|
+
* one is up REPLACES the caption rather than stacking.
|
|
444
|
+
*
|
|
445
|
+
* caption what is happening. Announced politely, so make it a sentence a reader would want read
|
|
446
|
+
* out, not a spinner label. Omitted, it falls back to the kit default, which a page
|
|
447
|
+
* overrides once with `data-fdy-text-caption` on <html>.
|
|
448
|
+
* delay ms to wait before it appears (default 120; 0 shows immediately). An operation that
|
|
449
|
+
* finishes in 80ms should never flash a scrim — that reads as a glitch, not as progress.
|
|
450
|
+
* mark an Element to use instead of the default spinner. Element only, never an HTML string:
|
|
451
|
+
* a string here would be an injection point in every app that passed user text through.
|
|
452
|
+
*
|
|
453
|
+
* Not a dialog. Interaction is removed with `inert` on everything else, so there is nothing to trap
|
|
454
|
+
* focus against and nothing to dismiss. Focus is parked on the panel and given back on idle(),
|
|
455
|
+
* because the element it was on is inert by then and the browser would otherwise drop it to <body>.
|
|
456
|
+
*/
|
|
457
|
+
(function () {
|
|
458
|
+
'use strict';
|
|
459
|
+
|
|
460
|
+
var DEFAULT_DELAY = 120;
|
|
461
|
+
|
|
462
|
+
var TEXT = {
|
|
463
|
+
caption: 'Working…'
|
|
464
|
+
};
|
|
465
|
+
/* The overlay has no root of its own to carry an override — it is created, not hydrated — so the
|
|
466
|
+
lookup goes to <html>, the one element every page has before this runs. Kebab-cased for the
|
|
467
|
+
same reason as everywhere else: HTML lowercases attribute names, so a camelCase key could only
|
|
468
|
+
ever be written run-together and the override would fail silently. */
|
|
469
|
+
function textOf(key) {
|
|
470
|
+
var root = document.documentElement;
|
|
471
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
472
|
+
var custom = kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
473
|
+
return custom != null && custom !== '' ? custom : TEXT[key];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
var node = null;
|
|
477
|
+
var showTimer = null;
|
|
478
|
+
var inerted = [];
|
|
479
|
+
var returnFocusTo = null;
|
|
480
|
+
|
|
481
|
+
function build() {
|
|
482
|
+
var el = document.createElement('div');
|
|
483
|
+
el.className = 'fdy-busy';
|
|
484
|
+
el.setAttribute('popover', 'manual');
|
|
485
|
+
el.setAttribute('aria-busy', 'true');
|
|
486
|
+
el.tabIndex = -1;
|
|
487
|
+
|
|
488
|
+
var panel = document.createElement('div');
|
|
489
|
+
panel.className = 'fdy-busy__panel';
|
|
490
|
+
|
|
491
|
+
var mark = document.createElement('div');
|
|
492
|
+
mark.className = 'fdy-busy__mark';
|
|
493
|
+
// aria-hidden: the caption below is the message. A second announcement from the spinner's own
|
|
494
|
+
// role="status" would say "busy" twice and name nothing.
|
|
495
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
496
|
+
mark.appendChild(defaultMark());
|
|
497
|
+
|
|
498
|
+
var caption = document.createElement('p');
|
|
499
|
+
caption.className = 'fdy-busy__caption';
|
|
500
|
+
// role="status" rather than a dialog role: this reports a state, it does not ask a question.
|
|
501
|
+
caption.setAttribute('role', 'status');
|
|
502
|
+
|
|
503
|
+
panel.appendChild(mark);
|
|
504
|
+
panel.appendChild(caption);
|
|
505
|
+
el.appendChild(panel);
|
|
506
|
+
return el;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function defaultMark() {
|
|
510
|
+
var spinner = document.createElement('span');
|
|
511
|
+
spinner.className = 'fdy-spinner fdy-spinner--lg';
|
|
512
|
+
return spinner;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** inert everything else, remembering ONLY what we set so an app's own inert is never cleared. */
|
|
516
|
+
function block(on) {
|
|
517
|
+
var i;
|
|
518
|
+
if (on) {
|
|
519
|
+
var kids = document.body.children;
|
|
520
|
+
for (i = 0; i < kids.length; i++) {
|
|
521
|
+
var child = kids[i];
|
|
522
|
+
if (child === node || child.hasAttribute('inert')) continue;
|
|
523
|
+
child.setAttribute('inert', '');
|
|
524
|
+
inerted.push(child);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
for (i = 0; i < inerted.length; i++) inerted[i].removeAttribute('inert');
|
|
529
|
+
inerted = [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function isOpen() {
|
|
533
|
+
return node !== null && node.classList.contains('is-open');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function setCaption(text) {
|
|
537
|
+
node.querySelector('.fdy-busy__caption').textContent = text;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setMark(el) {
|
|
541
|
+
var slot = node.querySelector('.fdy-busy__mark');
|
|
542
|
+
while (slot.firstChild) slot.removeChild(slot.firstChild);
|
|
543
|
+
slot.appendChild(el instanceof Element ? el : defaultMark());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function show(opts) {
|
|
547
|
+
showTimer = null;
|
|
548
|
+
setCaption(opts.caption == null ? textOf('caption') : String(opts.caption));
|
|
549
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
550
|
+
|
|
551
|
+
returnFocusTo = document.activeElement;
|
|
552
|
+
document.body.appendChild(node);
|
|
553
|
+
node.classList.add('is-open');
|
|
554
|
+
// Top layer, so it also covers an open <dialog>. Where the API is missing the z-index in
|
|
555
|
+
// busy.css is the fallback; it cannot clear a modal, and that is stated in COMPONENTS.md.
|
|
556
|
+
if (typeof node.showPopover === 'function') {
|
|
557
|
+
try { node.showPopover(); } catch (e) { /* already open, or not connected yet */ }
|
|
558
|
+
}
|
|
559
|
+
block(true);
|
|
560
|
+
node.focus();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function busy(options) {
|
|
564
|
+
var opts = options || {};
|
|
565
|
+
if (node === null) node = build();
|
|
566
|
+
|
|
567
|
+
// Already up: this is a second owner talking. Update what it says, do not stack.
|
|
568
|
+
if (isOpen()) {
|
|
569
|
+
if (opts.caption != null) setCaption(String(opts.caption));
|
|
570
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
571
|
+
return node;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var delay = opts.delay == null ? DEFAULT_DELAY : Number(opts.delay);
|
|
575
|
+
if (showTimer !== null) clearTimeout(showTimer);
|
|
576
|
+
if (delay > 0) showTimer = setTimeout(function () { show(opts); }, delay);
|
|
577
|
+
else show(opts);
|
|
578
|
+
return node;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function idle() {
|
|
582
|
+
// Cancels a pending show too: an operation that beat the delay must leave nothing behind.
|
|
583
|
+
if (showTimer !== null) { clearTimeout(showTimer); showTimer = null; }
|
|
584
|
+
if (node === null || !isOpen()) return;
|
|
585
|
+
|
|
586
|
+
block(false);
|
|
587
|
+
if (typeof node.hidePopover === 'function') {
|
|
588
|
+
try { node.hidePopover(); } catch (e) { /* was never in the top layer */ }
|
|
589
|
+
}
|
|
590
|
+
node.classList.remove('is-open');
|
|
591
|
+
if (node.parentNode !== null) node.parentNode.removeChild(node);
|
|
592
|
+
|
|
593
|
+
// Give focus back to whatever had it, now that its ancestor is no longer inert.
|
|
594
|
+
if (returnFocusTo !== null && typeof returnFocusTo.focus === 'function' && returnFocusTo.isConnected) {
|
|
595
|
+
returnFocusTo.focus();
|
|
596
|
+
}
|
|
597
|
+
returnFocusTo = null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
window.Freeday = window.Freeday || {};
|
|
601
|
+
window.Freeday.busy = busy;
|
|
602
|
+
window.Freeday.idle = idle;
|
|
603
|
+
})();
|
|
604
|
+
|
|
402
605
|
/* Freeday, carousel enhancer (optional, zero-dependency).
|
|
403
606
|
* Scroll-snap slider with prev/next arrows, generated dot indicators, keyboard (←/→),
|
|
404
607
|
* and optional autoplay (data-fdy-autoplay="4000", pauses on hover/focus). Auto-inits
|
|
@@ -603,6 +806,8 @@
|
|
|
603
806
|
* forking this file. Keeping them in ONE table is also what lets a guard prove none is
|
|
604
807
|
* hard-coded further down. */
|
|
605
808
|
var TEXT = {
|
|
809
|
+
label: 'Select',
|
|
810
|
+
placeholder: 'Select…',
|
|
606
811
|
back: 'Back one level',
|
|
607
812
|
submenu: '{label}, submenu'
|
|
608
813
|
};
|
|
@@ -632,8 +837,8 @@
|
|
|
632
837
|
var root = sourceUl ? parse(sourceUl) : [];
|
|
633
838
|
if (sourceUl) sourceUl.remove();
|
|
634
839
|
|
|
635
|
-
var label = wrap.getAttribute('data-label') || '
|
|
636
|
-
var placeholder = wrap.getAttribute('data-placeholder') || '
|
|
840
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
841
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
|
|
637
842
|
var sep = wrap.getAttribute('data-separator') || ' / ';
|
|
638
843
|
|
|
639
844
|
var trigger = document.createElement('button');
|
|
@@ -748,7 +953,44 @@
|
|
|
748
953
|
|
|
749
954
|
var _pop = null;
|
|
750
955
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
956
|
+
|
|
957
|
+
/* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
|
|
958
|
+
`[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
|
|
959
|
+
the control natively had them. Read from the seed at init and settable afterwards, because
|
|
960
|
+
a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
|
|
961
|
+
express a later change any other way. */
|
|
962
|
+
function flagOf(name) {
|
|
963
|
+
var v = wrap.getAttribute('data-' + name);
|
|
964
|
+
return v != null && v !== 'false';
|
|
965
|
+
}
|
|
966
|
+
/* Named `state*` to match the datepicker, where `is*` collided with an older function. */
|
|
967
|
+
var stateDisabled = flagOf('disabled');
|
|
968
|
+
var stateReadonly = flagOf('readonly');
|
|
969
|
+
var stateInvalid = flagOf('invalid');
|
|
970
|
+
/* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
|
|
971
|
+
has to point its label and its error text at, and the raw path had no way to say so. */
|
|
972
|
+
if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
|
|
973
|
+
if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
|
|
974
|
+
|
|
975
|
+
function applyState() {
|
|
976
|
+
trigger.disabled = stateDisabled;
|
|
977
|
+
if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
|
|
978
|
+
else trigger.removeAttribute('aria-readonly');
|
|
979
|
+
if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
|
|
980
|
+
else trigger.removeAttribute('aria-invalid');
|
|
981
|
+
wrap.classList.toggle('fdy-cascade--error', stateInvalid);
|
|
982
|
+
if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
|
|
983
|
+
}
|
|
984
|
+
function setState(next) {
|
|
985
|
+
if (!next) return;
|
|
986
|
+
if (next.disabled != null) stateDisabled = !!next.disabled;
|
|
987
|
+
if (next.readonly != null) stateReadonly = !!next.readonly;
|
|
988
|
+
if (next.invalid != null) stateInvalid = !!next.invalid;
|
|
989
|
+
applyState();
|
|
990
|
+
}
|
|
991
|
+
|
|
751
992
|
function open() {
|
|
993
|
+
if (stateDisabled || stateReadonly) return;
|
|
752
994
|
if (!panel.hidden) return;
|
|
753
995
|
// Re-open at the selected leaf's level for quick re-selection.
|
|
754
996
|
var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
|
|
@@ -803,8 +1045,11 @@
|
|
|
803
1045
|
valueSpan.classList.add('fdy-cascade__value--placeholder');
|
|
804
1046
|
}
|
|
805
1047
|
|
|
1048
|
+
applyState();
|
|
1049
|
+
|
|
806
1050
|
var api = {
|
|
807
1051
|
wrap: wrap,
|
|
1052
|
+
setState: setState,
|
|
808
1053
|
getValue: function () { return selectedValue; },
|
|
809
1054
|
clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
|
|
810
1055
|
};
|
|
@@ -825,7 +1070,15 @@
|
|
|
825
1070
|
initAll();
|
|
826
1071
|
}
|
|
827
1072
|
|
|
828
|
-
window.FreedayCascade = {
|
|
1073
|
+
window.FreedayCascade = {
|
|
1074
|
+
init: initCascade,
|
|
1075
|
+
initAll: initAll,
|
|
1076
|
+
/* Same reason as the datepicker's: a seed rendered once still has to be lockable later. */
|
|
1077
|
+
setState: function (root, state) {
|
|
1078
|
+
var api = root && root._fdyCascade ? root._fdyCascade : null;
|
|
1079
|
+
if (api && api.setState) api.setState(state);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
829
1082
|
})();
|
|
830
1083
|
|
|
831
1084
|
/* Freeday, choose-from-list enhancer (optional, zero-dependency).
|
|
@@ -1104,6 +1357,28 @@
|
|
|
1104
1357
|
|
|
1105
1358
|
var NS = 'http://www.w3.org/2000/svg';
|
|
1106
1359
|
|
|
1360
|
+
/* User-facing strings. Two of them, and both shipped wrong until 2.2.0: the legend's fallback
|
|
1361
|
+
* label read `Seri 1` — Indonesian, three months after 2.0.0 turned every enhancer English —
|
|
1362
|
+
* and the donut's centre caption was hard-coded, so no host could rename it. Neither was
|
|
1363
|
+
* reachable by the guards: one goes into the DOM through `createTextNode`, the other through
|
|
1364
|
+
* `innerHTML`, and both guards look for `textContent` / `setAttribute`. Overridable per element
|
|
1365
|
+
* with `data-fdy-text-<key>`, like every other enhancer. */
|
|
1366
|
+
var TEXT = {
|
|
1367
|
+
series: 'Series {n}',
|
|
1368
|
+
total: 'Total'
|
|
1369
|
+
};
|
|
1370
|
+
function textAttr(root, key) {
|
|
1371
|
+
if (!root || !root.getAttribute) return null;
|
|
1372
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
1373
|
+
return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
1374
|
+
}
|
|
1375
|
+
function textOf(root, key, vars) {
|
|
1376
|
+
var custom = textAttr(root, key);
|
|
1377
|
+
var s = custom != null && custom !== '' ? custom : TEXT[key];
|
|
1378
|
+
if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
|
|
1379
|
+
return s;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1107
1382
|
// Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
|
|
1108
1383
|
// (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
|
|
1109
1384
|
function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
|
|
@@ -1312,7 +1587,7 @@
|
|
|
1312
1587
|
var li = document.createElement('li');
|
|
1313
1588
|
var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
|
|
1314
1589
|
li.appendChild(sw);
|
|
1315
|
-
li.appendChild(document.createTextNode(s.label || (
|
|
1590
|
+
li.appendChild(document.createTextNode(s.label || textOf(el, 'series', { n: si + 1 })));
|
|
1316
1591
|
legend.appendChild(li);
|
|
1317
1592
|
});
|
|
1318
1593
|
el.appendChild(legend);
|
|
@@ -1486,8 +1761,16 @@
|
|
|
1486
1761
|
ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
|
|
1487
1762
|
var center = document.createElement('div'); center.className = 'fdy-donut__center';
|
|
1488
1763
|
var centerLabel = el.getAttribute('data-fdy-center');
|
|
1489
|
-
|
|
1490
|
-
|
|
1764
|
+
var centerValue = document.createElement('b');
|
|
1765
|
+
centerValue.textContent = centerLabel != null ? centerLabel : String(total);
|
|
1766
|
+
center.appendChild(centerValue);
|
|
1767
|
+
/* Built rather than assigned as innerHTML: the caption is overridable now, and an author's
|
|
1768
|
+
string is not markup. */
|
|
1769
|
+
if (!centerLabel) {
|
|
1770
|
+
var centerCaption = document.createElement('span');
|
|
1771
|
+
centerCaption.textContent = textOf(el, 'total');
|
|
1772
|
+
center.appendChild(centerCaption);
|
|
1773
|
+
}
|
|
1491
1774
|
ring.appendChild(center);
|
|
1492
1775
|
var svg = svgEl('svg');
|
|
1493
1776
|
svg.setAttribute('class', 'fdy-donut__hit');
|
|
@@ -1646,12 +1929,12 @@
|
|
|
1646
1929
|
* Locale comes from <html lang> (via Intl), month/weekday/value formatting is not hardcoded.
|
|
1647
1930
|
*
|
|
1648
1931
|
* Markup contract:
|
|
1649
|
-
* - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="
|
|
1650
|
-
* data-placeholder="
|
|
1651
|
-
* - Range: <div data-fdy-daterange role="group" aria-label="
|
|
1652
|
-
* <div data-fdy-datepicker data-role="from" data-placeholder="
|
|
1932
|
+
* - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Upload date"
|
|
1933
|
+
* data-placeholder="Choose a date" data-min="2026-01-01" data-max="2026-12-31"></div>
|
|
1934
|
+
* - Range: <div data-fdy-daterange role="group" aria-label="Date range">
|
|
1935
|
+
* <div data-fdy-datepicker data-role="from" data-placeholder="From"></div>
|
|
1653
1936
|
* <span class="fdy-daterange__sep">–</span>
|
|
1654
|
-
* <div data-fdy-datepicker data-role="to" data-placeholder="
|
|
1937
|
+
* <div data-fdy-datepicker data-role="to" data-placeholder="To"></div>
|
|
1655
1938
|
* </div>
|
|
1656
1939
|
* The range links the two: the end can never precede the start (out-of-range days disable).
|
|
1657
1940
|
*
|
|
@@ -1667,6 +1950,47 @@
|
|
|
1667
1950
|
weekday names back automatically. The FALLBACK follows the kit's default language, or a
|
|
1668
1951
|
page without `lang` would read English labels around Indonesian month names. */
|
|
1669
1952
|
var LOCALE = document.documentElement.getAttribute('lang') || 'en';
|
|
1953
|
+
|
|
1954
|
+
/* User-facing strings. English by default, and every one overridable per element with
|
|
1955
|
+
* `data-fdy-text-<key>`, so a host that speaks another language (an Indonesian app on the raw
|
|
1956
|
+
* path, and every Blazor app, whose picker IS this enhancer) supplies its own without forking
|
|
1957
|
+
* this file.
|
|
1958
|
+
*
|
|
1959
|
+
* This table arrived late, in 2.2.0: the ten labels below were written as literals passed to
|
|
1960
|
+
* `navButton()` / `titleButton()`, so the guard that proves no enhancer string is hard-coded
|
|
1961
|
+
* never saw them — it looks for the line that writes to the DOM, and here that line only ever
|
|
1962
|
+
* sees a variable. Month and weekday names are NOT here on purpose: they come from `Intl`
|
|
1963
|
+
* through the page's `lang`, which is a better hatch than anything the kit could invent.
|
|
1964
|
+
* The `{label}` in the three title strings is the period the button drills into. */
|
|
1965
|
+
var TEXT = {
|
|
1966
|
+
label: 'Date',
|
|
1967
|
+
placeholder: 'Choose a date',
|
|
1968
|
+
prevMonth: 'Previous month',
|
|
1969
|
+
nextMonth: 'Next month',
|
|
1970
|
+
prevYear: 'Previous year',
|
|
1971
|
+
nextYear: 'Next year',
|
|
1972
|
+
prevYears: 'Previous years',
|
|
1973
|
+
nextYears: 'Next years',
|
|
1974
|
+
chooseMonth: '{label}, choose month',
|
|
1975
|
+
chooseYear: '{label}, choose year',
|
|
1976
|
+
backToMonths: '{start} to {end}, back to months'
|
|
1977
|
+
};
|
|
1978
|
+
/* HTML lowercases attribute names, so a camelCase key like `prevMonth` can only ever be written
|
|
1979
|
+
as `data-fdy-text-prevmonth`, while the kebab form anybody would reach for,
|
|
1980
|
+
`data-fdy-text-prev-month`, becomes a DIFFERENT attribute the enhancer never reads, and the
|
|
1981
|
+
override fails silently. So the key is kebab-cased for the lookup; the run-together spelling
|
|
1982
|
+
still resolves. */
|
|
1983
|
+
function textAttr(root, key) {
|
|
1984
|
+
if (!root || !root.getAttribute) return null;
|
|
1985
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
1986
|
+
return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
1987
|
+
}
|
|
1988
|
+
function textOf(root, key, vars) {
|
|
1989
|
+
var custom = textAttr(root, key);
|
|
1990
|
+
var s = custom != null && custom !== '' ? custom : TEXT[key];
|
|
1991
|
+
if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
|
|
1992
|
+
return s;
|
|
1993
|
+
}
|
|
1670
1994
|
var uidSeq = 0;
|
|
1671
1995
|
function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
|
|
1672
1996
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
@@ -1704,8 +2028,8 @@
|
|
|
1704
2028
|
wrap.dataset.fdyDpReady = '1';
|
|
1705
2029
|
wrap.classList.add('fdy-datepicker');
|
|
1706
2030
|
|
|
1707
|
-
var placeholder = wrap.getAttribute('data-placeholder') ||
|
|
1708
|
-
var label = wrap.getAttribute('data-label') || '
|
|
2031
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
|
|
2032
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
1709
2033
|
var selected = parseISO(wrap.getAttribute('data-value'));
|
|
1710
2034
|
var minDate = parseISO(wrap.getAttribute('data-min'));
|
|
1711
2035
|
var maxDate = parseISO(wrap.getAttribute('data-max'));
|
|
@@ -1803,16 +2127,16 @@
|
|
|
1803
2127
|
panel.innerHTML = '';
|
|
1804
2128
|
var head = document.createElement('div');
|
|
1805
2129
|
head.className = 'fdy-cal__head';
|
|
1806
|
-
var title = titleButton(monthFmt.format(view), monthFmt.format(view)
|
|
2130
|
+
var title = titleButton(monthFmt.format(view), textOf(wrap, 'chooseMonth', { label: monthFmt.format(view) }), function () {
|
|
1807
2131
|
mode = 'months';
|
|
1808
2132
|
focusMonth = view.getMonth();
|
|
1809
2133
|
render();
|
|
1810
2134
|
focusMonthCell();
|
|
1811
2135
|
});
|
|
1812
2136
|
panel.setAttribute('aria-labelledby', title.id);
|
|
1813
|
-
head.appendChild(navButton('‹',
|
|
2137
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevMonth'), function () { view = addMonths(view, -1); render(); }));
|
|
1814
2138
|
head.appendChild(title);
|
|
1815
|
-
head.appendChild(navButton('›',
|
|
2139
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextMonth'), function () { view = addMonths(view, 1); render(); }));
|
|
1816
2140
|
panel.appendChild(head);
|
|
1817
2141
|
|
|
1818
2142
|
var grid = document.createElement('div');
|
|
@@ -1869,16 +2193,16 @@
|
|
|
1869
2193
|
var year = view.getFullYear();
|
|
1870
2194
|
var head = document.createElement('div');
|
|
1871
2195
|
head.className = 'fdy-cal__head';
|
|
1872
|
-
var title = titleButton(String(year),
|
|
2196
|
+
var title = titleButton(String(year), textOf(wrap, 'chooseYear', { label: year }), function () {
|
|
1873
2197
|
mode = 'years';
|
|
1874
2198
|
focusYear = year;
|
|
1875
2199
|
render();
|
|
1876
2200
|
focusYearCell();
|
|
1877
2201
|
});
|
|
1878
2202
|
panel.setAttribute('aria-labelledby', title.id);
|
|
1879
|
-
head.appendChild(navButton('‹',
|
|
2203
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevYear'), function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
|
|
1880
2204
|
head.appendChild(title);
|
|
1881
|
-
head.appendChild(navButton('›',
|
|
2205
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextYear'), function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
|
|
1882
2206
|
panel.appendChild(head);
|
|
1883
2207
|
|
|
1884
2208
|
var grid = document.createElement('div');
|
|
@@ -1930,16 +2254,16 @@
|
|
|
1930
2254
|
var end = start + YEARS_PER_PAGE - 1;
|
|
1931
2255
|
var head = document.createElement('div');
|
|
1932
2256
|
head.className = 'fdy-cal__head';
|
|
1933
|
-
var title = titleButton(start + ' – ' + end,
|
|
2257
|
+
var title = titleButton(start + ' – ' + end, textOf(wrap, 'backToMonths', { start: start, end: end }), function () {
|
|
1934
2258
|
mode = 'months';
|
|
1935
2259
|
focusMonth = view.getMonth();
|
|
1936
2260
|
render();
|
|
1937
2261
|
focusMonthCell();
|
|
1938
2262
|
});
|
|
1939
2263
|
panel.setAttribute('aria-labelledby', title.id);
|
|
1940
|
-
head.appendChild(navButton('‹',
|
|
2264
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevYears'), function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
|
|
1941
2265
|
head.appendChild(title);
|
|
1942
|
-
head.appendChild(navButton('›',
|
|
2266
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextYears'), function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
|
|
1943
2267
|
panel.appendChild(head);
|
|
1944
2268
|
|
|
1945
2269
|
var grid = document.createElement('div');
|
|
@@ -2100,7 +2424,46 @@
|
|
|
2100
2424
|
|
|
2101
2425
|
var _pop = null;
|
|
2102
2426
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
2427
|
+
|
|
2428
|
+
/* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
|
|
2429
|
+
`[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
|
|
2430
|
+
the control natively had them. Read from the seed at init and settable afterwards, because
|
|
2431
|
+
a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
|
|
2432
|
+
express a later change any other way. */
|
|
2433
|
+
function flagOf(name) {
|
|
2434
|
+
var v = wrap.getAttribute('data-' + name);
|
|
2435
|
+
return v != null && v !== 'false';
|
|
2436
|
+
}
|
|
2437
|
+
/* Named `state*`, not `is*`: this file already has an `isDisabled(date)` deciding whether a
|
|
2438
|
+
DAY falls outside min/max, and shadowing it with a boolean made the day grid throw on every
|
|
2439
|
+
render — silently, since the panel still opened and only its cells went missing. */
|
|
2440
|
+
var stateDisabled = flagOf('disabled');
|
|
2441
|
+
var stateReadonly = flagOf('readonly');
|
|
2442
|
+
var stateInvalid = flagOf('invalid');
|
|
2443
|
+
/* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
|
|
2444
|
+
has to point its label and its error text at, and the raw path had no way to say so. */
|
|
2445
|
+
if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
|
|
2446
|
+
if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
|
|
2447
|
+
|
|
2448
|
+
function applyState() {
|
|
2449
|
+
trigger.disabled = stateDisabled;
|
|
2450
|
+
if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
|
|
2451
|
+
else trigger.removeAttribute('aria-readonly');
|
|
2452
|
+
if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
|
|
2453
|
+
else trigger.removeAttribute('aria-invalid');
|
|
2454
|
+
wrap.classList.toggle('fdy-datepicker--error', stateInvalid);
|
|
2455
|
+
if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
|
|
2456
|
+
}
|
|
2457
|
+
function setState(next) {
|
|
2458
|
+
if (!next) return;
|
|
2459
|
+
if (next.disabled != null) stateDisabled = !!next.disabled;
|
|
2460
|
+
if (next.readonly != null) stateReadonly = !!next.readonly;
|
|
2461
|
+
if (next.invalid != null) stateInvalid = !!next.invalid;
|
|
2462
|
+
applyState();
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2103
2465
|
function open() {
|
|
2466
|
+
if (stateDisabled || stateReadonly) return;
|
|
2104
2467
|
if (!panel.hidden) return;
|
|
2105
2468
|
mode = 'days';
|
|
2106
2469
|
focusDate = selected || focusDate || new Date();
|
|
@@ -2130,8 +2493,11 @@
|
|
|
2130
2493
|
|
|
2131
2494
|
updateDisplay();
|
|
2132
2495
|
|
|
2496
|
+
applyState();
|
|
2497
|
+
|
|
2133
2498
|
var api = {
|
|
2134
2499
|
wrap: wrap,
|
|
2500
|
+
setState: setState,
|
|
2135
2501
|
getValue: function () { return selected ? toISO(selected) : ''; },
|
|
2136
2502
|
clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
|
|
2137
2503
|
setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
|
|
@@ -2191,7 +2557,16 @@
|
|
|
2191
2557
|
initAll();
|
|
2192
2558
|
}
|
|
2193
2559
|
|
|
2194
|
-
window.FreedayDatepicker = {
|
|
2560
|
+
window.FreedayDatepicker = {
|
|
2561
|
+
init: initPicker,
|
|
2562
|
+
initAll: initAll,
|
|
2563
|
+
/* A host that rendered its seed once and cannot re-render it (Blazor) still has to be able to
|
|
2564
|
+
disable, lock or invalidate the field later. */
|
|
2565
|
+
setState: function (root, state) {
|
|
2566
|
+
var api = root && root._fdyDp ? root._fdyDp : null;
|
|
2567
|
+
if (api && api.setState) api.setState(state);
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2195
2570
|
})();
|
|
2196
2571
|
|
|
2197
2572
|
/* Freeday, datetime picker composer (optional, zero-dependency).
|
|
@@ -3215,7 +3590,24 @@
|
|
|
3215
3590
|
if (root && root._fdyCombo) root._fdyCombo.setValue(value);
|
|
3216
3591
|
}
|
|
3217
3592
|
|
|
3218
|
-
|
|
3593
|
+
/* Beside setValue for the same reason it exists: a host that renders its markup once (every
|
|
3594
|
+
Blazor wrapper, `ShouldRender => false`) cannot express a later state change any other way,
|
|
3595
|
+
and a parameter that silently stops working after the first render is worse than none. */
|
|
3596
|
+
function setState(root, state) {
|
|
3597
|
+
var button = root ? root.querySelector('.fdy-combo__button') : null;
|
|
3598
|
+
if (!button || !state) return;
|
|
3599
|
+
if (state.disabled != null) button.disabled = !!state.disabled;
|
|
3600
|
+
if (state.readonly != null) {
|
|
3601
|
+
if (state.readonly) button.setAttribute('aria-readonly', 'true');
|
|
3602
|
+
else button.removeAttribute('aria-readonly');
|
|
3603
|
+
}
|
|
3604
|
+
if (state.invalid != null) {
|
|
3605
|
+
if (state.invalid) button.setAttribute('aria-invalid', 'true');
|
|
3606
|
+
else button.removeAttribute('aria-invalid');
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue, setState: setState };
|
|
3219
3611
|
})();
|
|
3220
3612
|
|
|
3221
3613
|
/* Freeday, slider value binding (optional, zero-dependency).
|
|
@@ -3269,7 +3661,9 @@
|
|
|
3269
3661
|
* <div class="fdy-step-panel">…</div>… </div>
|
|
3270
3662
|
* <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
|
|
3271
3663
|
* <button data-fdy-step-next>…</button></div></div>
|
|
3272
|
-
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step)
|
|
3664
|
+
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step), and a
|
|
3665
|
+
* cancelable "fdy-step-before-change" {from, to, waitFor} that a guard refuses with
|
|
3666
|
+
* preventDefault() or defers by assigning a promise to detail.waitFor.
|
|
3273
3667
|
*/
|
|
3274
3668
|
(function () {
|
|
3275
3669
|
'use strict';
|
|
@@ -3348,14 +3742,62 @@
|
|
|
3348
3742
|
render();
|
|
3349
3743
|
}
|
|
3350
3744
|
|
|
3745
|
+
/* Leaving a step is REFUSABLE, because a wizard whose Next cannot be stopped is a wizard that
|
|
3746
|
+
* validates nothing. Two ways to refuse, and the second is why an event alone was not enough:
|
|
3747
|
+
*
|
|
3748
|
+
* sync handler calls ev.preventDefault() — the answer is already known
|
|
3749
|
+
* async handler sets ev.detail.waitFor = promise — the answer is a server round-trip away
|
|
3750
|
+
*
|
|
3751
|
+
* Resolving to `false` refuses; anything else advances, so a handler that forgets to return is
|
|
3752
|
+
* not read as a rejection. HOW validity is decided stays entirely with the app: the kit has no
|
|
3753
|
+
* opinion about form libraries and this is the line that keeps it that way. */
|
|
3754
|
+
var deciding = false;
|
|
3755
|
+
|
|
3756
|
+
function lock(on) {
|
|
3757
|
+
deciding = on;
|
|
3758
|
+
var list = root.querySelector('.fdy-stepper');
|
|
3759
|
+
if (list) { if (on) list.setAttribute('aria-busy', 'true'); else list.removeAttribute('aria-busy'); }
|
|
3760
|
+
if (prevBtn) prevBtn.disabled = on || active === 0;
|
|
3761
|
+
if (nextBtn) nextBtn.disabled = on;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function request(to, onAllowed) {
|
|
3765
|
+
if (deciding) return;
|
|
3766
|
+
var ev = new CustomEvent('fdy-step-before-change', {
|
|
3767
|
+
bubbles: true,
|
|
3768
|
+
cancelable: true,
|
|
3769
|
+
detail: { from: active, to: to, waitFor: null },
|
|
3770
|
+
});
|
|
3771
|
+
root.dispatchEvent(ev);
|
|
3772
|
+
if (ev.defaultPrevented) return;
|
|
3773
|
+
|
|
3774
|
+
var pending = ev.detail.waitFor;
|
|
3775
|
+
if (pending === null || typeof pending.then !== 'function') { onAllowed(); return; }
|
|
3776
|
+
|
|
3777
|
+
lock(true);
|
|
3778
|
+
pending.then(
|
|
3779
|
+
function (ok) { lock(false); if (ok !== false) onAllowed(); },
|
|
3780
|
+
// A guard that THREW decided nothing, so it must not advance. Staying put with the nav
|
|
3781
|
+
// released is the only safe reading; the app's own error handling reports the failure.
|
|
3782
|
+
function () { lock(false); },
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3351
3786
|
if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
|
|
3352
3787
|
if (nextBtn) nextBtn.addEventListener('click', function () {
|
|
3353
|
-
if (active < steps.length - 1) go(active + 1);
|
|
3354
|
-
else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
|
|
3788
|
+
if (active < steps.length - 1) request(active + 1, function () { go(active + 1); });
|
|
3789
|
+
else request(active + 1, function () { root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true })); });
|
|
3355
3790
|
});
|
|
3356
3791
|
steps.forEach(function (s, i) {
|
|
3357
3792
|
var btn = s.querySelector('.fdy-step__btn');
|
|
3358
|
-
if (btn)
|
|
3793
|
+
if (!btn) return;
|
|
3794
|
+
btn.addEventListener('click', function () {
|
|
3795
|
+
if (i > maxReached) return;
|
|
3796
|
+
// Going BACK is always allowed — nothing is being committed. Jumping forward to a step
|
|
3797
|
+
// already reached still leaves the current one behind, so it asks the guard like Next does.
|
|
3798
|
+
if (i <= active) go(i);
|
|
3799
|
+
else request(i, function () { go(i); });
|
|
3800
|
+
});
|
|
3359
3801
|
});
|
|
3360
3802
|
|
|
3361
3803
|
render();
|
|
@@ -3426,6 +3868,8 @@
|
|
|
3426
3868
|
filterText: 'Contains text',
|
|
3427
3869
|
filterTextPlaceholder: 'Contains…',
|
|
3428
3870
|
filterEnum: 'Show values',
|
|
3871
|
+
filterMin: 'Min',
|
|
3872
|
+
filterMax: 'Max',
|
|
3429
3873
|
filterRange: 'Value range',
|
|
3430
3874
|
reset: 'Reset',
|
|
3431
3875
|
close: 'Close',
|
|
@@ -3698,8 +4142,8 @@
|
|
|
3698
4142
|
pop.appendChild(filterTitle(textOf(root, 'filterRange')));
|
|
3699
4143
|
var range = document.createElement('div');
|
|
3700
4144
|
range.className = 'fdy-filter__range';
|
|
3701
|
-
var minI = numberInput('
|
|
3702
|
-
var maxI = numberInput('
|
|
4145
|
+
var minI = numberInput(textOf(root, 'filterMin'), f.min);
|
|
4146
|
+
var maxI = numberInput(textOf(root, 'filterMax'), f.max);
|
|
3703
4147
|
var applyRange = function () {
|
|
3704
4148
|
f.min = minI.value !== '' ? parseNum(minI.value) : null;
|
|
3705
4149
|
f.max = maxI.value !== '' ? parseNum(maxI.value) : null;
|
|
@@ -3998,6 +4442,24 @@
|
|
|
3998
4442
|
(function () {
|
|
3999
4443
|
'use strict';
|
|
4000
4444
|
|
|
4445
|
+
/* User-facing strings, overridable per element with `data-fdy-text-<key>`. One entry, and it
|
|
4446
|
+
* still needed the table: it reached the DOM as an argument to a helper, which is how it sat
|
|
4447
|
+
* outside every string guard until 2.2.0. */
|
|
4448
|
+
var TEXT = {
|
|
4449
|
+
label: 'Choose a time'
|
|
4450
|
+
};
|
|
4451
|
+
function textAttr(root, key) {
|
|
4452
|
+
if (!root || !root.getAttribute) return null;
|
|
4453
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
4454
|
+
return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
4455
|
+
}
|
|
4456
|
+
function textOf(root, key, vars) {
|
|
4457
|
+
var custom = textAttr(root, key);
|
|
4458
|
+
var s = custom != null && custom !== '' ? custom : TEXT[key];
|
|
4459
|
+
if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
|
|
4460
|
+
return s;
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4001
4463
|
var seq = 0;
|
|
4002
4464
|
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>';
|
|
4003
4465
|
|
|
@@ -4012,7 +4474,7 @@
|
|
|
4012
4474
|
wrap.dataset.fdyTpReady = '1';
|
|
4013
4475
|
wrap.classList.add('fdy-timepicker');
|
|
4014
4476
|
|
|
4015
|
-
var label = wrap.getAttribute('data-label') ||
|
|
4477
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
4016
4478
|
var placeholder = wrap.getAttribute('data-placeholder') || '--:--';
|
|
4017
4479
|
var step = Math.max(1, parseInt(wrap.getAttribute('data-step') || '30', 10));
|
|
4018
4480
|
var minM = valid(wrap.getAttribute('data-min')) ? toMin(wrap.getAttribute('data-min')) : 0;
|