@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* Freeday, busy overlay (optional, zero-dependency).
|
|
2
|
+
*
|
|
3
|
+
* Freeday.busy({ caption, delay, mark }) block the screen while an operation runs
|
|
4
|
+
* Freeday.idle() release it
|
|
5
|
+
*
|
|
6
|
+
* Imperative on purpose, like Freeday.toast(): a component API invites two instances, and two
|
|
7
|
+
* blocking overlays with two captions is the failure this exists to prevent. A second busy() while
|
|
8
|
+
* one is up REPLACES the caption rather than stacking.
|
|
9
|
+
*
|
|
10
|
+
* caption what is happening. Announced politely, so make it a sentence a reader would want read
|
|
11
|
+
* out, not a spinner label. Omitted, it falls back to the kit default, which a page
|
|
12
|
+
* overrides once with `data-fdy-text-caption` on <html>.
|
|
13
|
+
* delay ms to wait before it appears (default 120; 0 shows immediately). An operation that
|
|
14
|
+
* finishes in 80ms should never flash a scrim — that reads as a glitch, not as progress.
|
|
15
|
+
* mark an Element to use instead of the default spinner. Element only, never an HTML string:
|
|
16
|
+
* a string here would be an injection point in every app that passed user text through.
|
|
17
|
+
*
|
|
18
|
+
* Not a dialog. Interaction is removed with `inert` on everything else, so there is nothing to trap
|
|
19
|
+
* focus against and nothing to dismiss. Focus is parked on the panel and given back on idle(),
|
|
20
|
+
* because the element it was on is inert by then and the browser would otherwise drop it to <body>.
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
var DEFAULT_DELAY = 120;
|
|
26
|
+
|
|
27
|
+
var TEXT = {
|
|
28
|
+
caption: 'Working…'
|
|
29
|
+
};
|
|
30
|
+
/* The overlay has no root of its own to carry an override — it is created, not hydrated — so the
|
|
31
|
+
lookup goes to <html>, the one element every page has before this runs. Kebab-cased for the
|
|
32
|
+
same reason as everywhere else: HTML lowercases attribute names, so a camelCase key could only
|
|
33
|
+
ever be written run-together and the override would fail silently. */
|
|
34
|
+
function textOf(key) {
|
|
35
|
+
var root = document.documentElement;
|
|
36
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
37
|
+
var custom = kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
38
|
+
return custom != null && custom !== '' ? custom : TEXT[key];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var node = null;
|
|
42
|
+
var showTimer = null;
|
|
43
|
+
var inerted = [];
|
|
44
|
+
var returnFocusTo = null;
|
|
45
|
+
|
|
46
|
+
function build() {
|
|
47
|
+
var el = document.createElement('div');
|
|
48
|
+
el.className = 'fdy-busy';
|
|
49
|
+
el.setAttribute('popover', 'manual');
|
|
50
|
+
el.setAttribute('aria-busy', 'true');
|
|
51
|
+
el.tabIndex = -1;
|
|
52
|
+
|
|
53
|
+
var panel = document.createElement('div');
|
|
54
|
+
panel.className = 'fdy-busy__panel';
|
|
55
|
+
|
|
56
|
+
var mark = document.createElement('div');
|
|
57
|
+
mark.className = 'fdy-busy__mark';
|
|
58
|
+
// aria-hidden: the caption below is the message. A second announcement from the spinner's own
|
|
59
|
+
// role="status" would say "busy" twice and name nothing.
|
|
60
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
61
|
+
mark.appendChild(defaultMark());
|
|
62
|
+
|
|
63
|
+
var caption = document.createElement('p');
|
|
64
|
+
caption.className = 'fdy-busy__caption';
|
|
65
|
+
// role="status" rather than a dialog role: this reports a state, it does not ask a question.
|
|
66
|
+
caption.setAttribute('role', 'status');
|
|
67
|
+
|
|
68
|
+
panel.appendChild(mark);
|
|
69
|
+
panel.appendChild(caption);
|
|
70
|
+
el.appendChild(panel);
|
|
71
|
+
return el;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultMark() {
|
|
75
|
+
var spinner = document.createElement('span');
|
|
76
|
+
spinner.className = 'fdy-spinner fdy-spinner--lg';
|
|
77
|
+
return spinner;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** inert everything else, remembering ONLY what we set so an app's own inert is never cleared. */
|
|
81
|
+
function block(on) {
|
|
82
|
+
var i;
|
|
83
|
+
if (on) {
|
|
84
|
+
var kids = document.body.children;
|
|
85
|
+
for (i = 0; i < kids.length; i++) {
|
|
86
|
+
var child = kids[i];
|
|
87
|
+
if (child === node || child.hasAttribute('inert')) continue;
|
|
88
|
+
child.setAttribute('inert', '');
|
|
89
|
+
inerted.push(child);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (i = 0; i < inerted.length; i++) inerted[i].removeAttribute('inert');
|
|
94
|
+
inerted = [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isOpen() {
|
|
98
|
+
return node !== null && node.classList.contains('is-open');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setCaption(text) {
|
|
102
|
+
node.querySelector('.fdy-busy__caption').textContent = text;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setMark(el) {
|
|
106
|
+
var slot = node.querySelector('.fdy-busy__mark');
|
|
107
|
+
while (slot.firstChild) slot.removeChild(slot.firstChild);
|
|
108
|
+
slot.appendChild(el instanceof Element ? el : defaultMark());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function show(opts) {
|
|
112
|
+
showTimer = null;
|
|
113
|
+
setCaption(opts.caption == null ? textOf('caption') : String(opts.caption));
|
|
114
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
115
|
+
|
|
116
|
+
returnFocusTo = document.activeElement;
|
|
117
|
+
document.body.appendChild(node);
|
|
118
|
+
node.classList.add('is-open');
|
|
119
|
+
// Top layer, so it also covers an open <dialog>. Where the API is missing the z-index in
|
|
120
|
+
// busy.css is the fallback; it cannot clear a modal, and that is stated in COMPONENTS.md.
|
|
121
|
+
if (typeof node.showPopover === 'function') {
|
|
122
|
+
try { node.showPopover(); } catch (e) { /* already open, or not connected yet */ }
|
|
123
|
+
}
|
|
124
|
+
block(true);
|
|
125
|
+
node.focus();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function busy(options) {
|
|
129
|
+
var opts = options || {};
|
|
130
|
+
if (node === null) node = build();
|
|
131
|
+
|
|
132
|
+
// Already up: this is a second owner talking. Update what it says, do not stack.
|
|
133
|
+
if (isOpen()) {
|
|
134
|
+
if (opts.caption != null) setCaption(String(opts.caption));
|
|
135
|
+
if (opts.mark !== undefined) setMark(opts.mark);
|
|
136
|
+
return node;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
var delay = opts.delay == null ? DEFAULT_DELAY : Number(opts.delay);
|
|
140
|
+
if (showTimer !== null) clearTimeout(showTimer);
|
|
141
|
+
if (delay > 0) showTimer = setTimeout(function () { show(opts); }, delay);
|
|
142
|
+
else show(opts);
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function idle() {
|
|
147
|
+
// Cancels a pending show too: an operation that beat the delay must leave nothing behind.
|
|
148
|
+
if (showTimer !== null) { clearTimeout(showTimer); showTimer = null; }
|
|
149
|
+
if (node === null || !isOpen()) return;
|
|
150
|
+
|
|
151
|
+
block(false);
|
|
152
|
+
if (typeof node.hidePopover === 'function') {
|
|
153
|
+
try { node.hidePopover(); } catch (e) { /* was never in the top layer */ }
|
|
154
|
+
}
|
|
155
|
+
node.classList.remove('is-open');
|
|
156
|
+
if (node.parentNode !== null) node.parentNode.removeChild(node);
|
|
157
|
+
|
|
158
|
+
// Give focus back to whatever had it, now that its ancestor is no longer inert.
|
|
159
|
+
if (returnFocusTo !== null && typeof returnFocusTo.focus === 'function' && returnFocusTo.isConnected) {
|
|
160
|
+
returnFocusTo.focus();
|
|
161
|
+
}
|
|
162
|
+
returnFocusTo = null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
window.Freeday = window.Freeday || {};
|
|
166
|
+
window.Freeday.busy = busy;
|
|
167
|
+
window.Freeday.idle = idle;
|
|
168
|
+
})();
|
package/dist/freeday-cascade.js
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
* forking this file. Keeping them in ONE table is also what lets a guard prove none is
|
|
61
61
|
* hard-coded further down. */
|
|
62
62
|
var TEXT = {
|
|
63
|
+
label: 'Select',
|
|
64
|
+
placeholder: 'Select…',
|
|
63
65
|
back: 'Back one level',
|
|
64
66
|
submenu: '{label}, submenu'
|
|
65
67
|
};
|
|
@@ -89,8 +91,8 @@
|
|
|
89
91
|
var root = sourceUl ? parse(sourceUl) : [];
|
|
90
92
|
if (sourceUl) sourceUl.remove();
|
|
91
93
|
|
|
92
|
-
var label = wrap.getAttribute('data-label') || '
|
|
93
|
-
var placeholder = wrap.getAttribute('data-placeholder') || '
|
|
94
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
95
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
|
|
94
96
|
var sep = wrap.getAttribute('data-separator') || ' / ';
|
|
95
97
|
|
|
96
98
|
var trigger = document.createElement('button');
|
|
@@ -205,7 +207,44 @@
|
|
|
205
207
|
|
|
206
208
|
var _pop = null;
|
|
207
209
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
210
|
+
|
|
211
|
+
/* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
|
|
212
|
+
`[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
|
|
213
|
+
the control natively had them. Read from the seed at init and settable afterwards, because
|
|
214
|
+
a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
|
|
215
|
+
express a later change any other way. */
|
|
216
|
+
function flagOf(name) {
|
|
217
|
+
var v = wrap.getAttribute('data-' + name);
|
|
218
|
+
return v != null && v !== 'false';
|
|
219
|
+
}
|
|
220
|
+
/* Named `state*` to match the datepicker, where `is*` collided with an older function. */
|
|
221
|
+
var stateDisabled = flagOf('disabled');
|
|
222
|
+
var stateReadonly = flagOf('readonly');
|
|
223
|
+
var stateInvalid = flagOf('invalid');
|
|
224
|
+
/* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
|
|
225
|
+
has to point its label and its error text at, and the raw path had no way to say so. */
|
|
226
|
+
if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
|
|
227
|
+
if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
|
|
228
|
+
|
|
229
|
+
function applyState() {
|
|
230
|
+
trigger.disabled = stateDisabled;
|
|
231
|
+
if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
|
|
232
|
+
else trigger.removeAttribute('aria-readonly');
|
|
233
|
+
if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
|
|
234
|
+
else trigger.removeAttribute('aria-invalid');
|
|
235
|
+
wrap.classList.toggle('fdy-cascade--error', stateInvalid);
|
|
236
|
+
if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
|
|
237
|
+
}
|
|
238
|
+
function setState(next) {
|
|
239
|
+
if (!next) return;
|
|
240
|
+
if (next.disabled != null) stateDisabled = !!next.disabled;
|
|
241
|
+
if (next.readonly != null) stateReadonly = !!next.readonly;
|
|
242
|
+
if (next.invalid != null) stateInvalid = !!next.invalid;
|
|
243
|
+
applyState();
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
function open() {
|
|
247
|
+
if (stateDisabled || stateReadonly) return;
|
|
209
248
|
if (!panel.hidden) return;
|
|
210
249
|
// Re-open at the selected leaf's level for quick re-selection.
|
|
211
250
|
var trail = selectedValue ? pathTo(root, selectedValue, []) : null;
|
|
@@ -260,8 +299,11 @@
|
|
|
260
299
|
valueSpan.classList.add('fdy-cascade__value--placeholder');
|
|
261
300
|
}
|
|
262
301
|
|
|
302
|
+
applyState();
|
|
303
|
+
|
|
263
304
|
var api = {
|
|
264
305
|
wrap: wrap,
|
|
306
|
+
setState: setState,
|
|
265
307
|
getValue: function () { return selectedValue; },
|
|
266
308
|
clear: function () { selectedValue = ''; valueSpan.textContent = placeholder; valueSpan.classList.add('fdy-cascade__value--placeholder'); }
|
|
267
309
|
};
|
|
@@ -282,5 +324,13 @@
|
|
|
282
324
|
initAll();
|
|
283
325
|
}
|
|
284
326
|
|
|
285
|
-
window.FreedayCascade = {
|
|
327
|
+
window.FreedayCascade = {
|
|
328
|
+
init: initCascade,
|
|
329
|
+
initAll: initAll,
|
|
330
|
+
/* Same reason as the datepicker's: a seed rendered once still has to be lockable later. */
|
|
331
|
+
setState: function (root, state) {
|
|
332
|
+
var api = root && root._fdyCascade ? root._fdyCascade : null;
|
|
333
|
+
if (api && api.setState) api.setState(state);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
286
336
|
})();
|
package/dist/freeday-chart.js
CHANGED
|
@@ -29,6 +29,28 @@
|
|
|
29
29
|
|
|
30
30
|
var NS = 'http://www.w3.org/2000/svg';
|
|
31
31
|
|
|
32
|
+
/* User-facing strings. Two of them, and both shipped wrong until 2.2.0: the legend's fallback
|
|
33
|
+
* label read `Seri 1` — Indonesian, three months after 2.0.0 turned every enhancer English —
|
|
34
|
+
* and the donut's centre caption was hard-coded, so no host could rename it. Neither was
|
|
35
|
+
* reachable by the guards: one goes into the DOM through `createTextNode`, the other through
|
|
36
|
+
* `innerHTML`, and both guards look for `textContent` / `setAttribute`. Overridable per element
|
|
37
|
+
* with `data-fdy-text-<key>`, like every other enhancer. */
|
|
38
|
+
var TEXT = {
|
|
39
|
+
series: 'Series {n}',
|
|
40
|
+
total: 'Total'
|
|
41
|
+
};
|
|
42
|
+
function textAttr(root, key) {
|
|
43
|
+
if (!root || !root.getAttribute) return null;
|
|
44
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
45
|
+
return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
46
|
+
}
|
|
47
|
+
function textOf(root, key, vars) {
|
|
48
|
+
var custom = textAttr(root, key);
|
|
49
|
+
var s = custom != null && custom !== '' ? custom : TEXT[key];
|
|
50
|
+
if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
// Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
|
|
33
55
|
// (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
|
|
34
56
|
function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
|
|
@@ -237,7 +259,7 @@
|
|
|
237
259
|
var li = document.createElement('li');
|
|
238
260
|
var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
|
|
239
261
|
li.appendChild(sw);
|
|
240
|
-
li.appendChild(document.createTextNode(s.label || (
|
|
262
|
+
li.appendChild(document.createTextNode(s.label || textOf(el, 'series', { n: si + 1 })));
|
|
241
263
|
legend.appendChild(li);
|
|
242
264
|
});
|
|
243
265
|
el.appendChild(legend);
|
|
@@ -411,8 +433,16 @@
|
|
|
411
433
|
ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
|
|
412
434
|
var center = document.createElement('div'); center.className = 'fdy-donut__center';
|
|
413
435
|
var centerLabel = el.getAttribute('data-fdy-center');
|
|
414
|
-
|
|
415
|
-
|
|
436
|
+
var centerValue = document.createElement('b');
|
|
437
|
+
centerValue.textContent = centerLabel != null ? centerLabel : String(total);
|
|
438
|
+
center.appendChild(centerValue);
|
|
439
|
+
/* Built rather than assigned as innerHTML: the caption is overridable now, and an author's
|
|
440
|
+
string is not markup. */
|
|
441
|
+
if (!centerLabel) {
|
|
442
|
+
var centerCaption = document.createElement('span');
|
|
443
|
+
centerCaption.textContent = textOf(el, 'total');
|
|
444
|
+
center.appendChild(centerCaption);
|
|
445
|
+
}
|
|
416
446
|
ring.appendChild(center);
|
|
417
447
|
var svg = svgEl('svg');
|
|
418
448
|
svg.setAttribute('class', 'fdy-donut__hit');
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Locale comes from <html lang> (via Intl), month/weekday/value formatting is not hardcoded.
|
|
4
4
|
*
|
|
5
5
|
* Markup contract:
|
|
6
|
-
* - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="
|
|
7
|
-
* data-placeholder="
|
|
8
|
-
* - Range: <div data-fdy-daterange role="group" aria-label="
|
|
9
|
-
* <div data-fdy-datepicker data-role="from" data-placeholder="
|
|
6
|
+
* - Single: <div data-fdy-datepicker data-value="2026-07-21" data-label="Upload date"
|
|
7
|
+
* data-placeholder="Choose a date" data-min="2026-01-01" data-max="2026-12-31"></div>
|
|
8
|
+
* - Range: <div data-fdy-daterange role="group" aria-label="Date range">
|
|
9
|
+
* <div data-fdy-datepicker data-role="from" data-placeholder="From"></div>
|
|
10
10
|
* <span class="fdy-daterange__sep">–</span>
|
|
11
|
-
* <div data-fdy-datepicker data-role="to" data-placeholder="
|
|
11
|
+
* <div data-fdy-datepicker data-role="to" data-placeholder="To"></div>
|
|
12
12
|
* </div>
|
|
13
13
|
* The range links the two: the end can never precede the start (out-of-range days disable).
|
|
14
14
|
*
|
|
@@ -24,6 +24,47 @@
|
|
|
24
24
|
weekday names back automatically. The FALLBACK follows the kit's default language, or a
|
|
25
25
|
page without `lang` would read English labels around Indonesian month names. */
|
|
26
26
|
var LOCALE = document.documentElement.getAttribute('lang') || 'en';
|
|
27
|
+
|
|
28
|
+
/* User-facing strings. English by default, and every one overridable per element with
|
|
29
|
+
* `data-fdy-text-<key>`, so a host that speaks another language (an Indonesian app on the raw
|
|
30
|
+
* path, and every Blazor app, whose picker IS this enhancer) supplies its own without forking
|
|
31
|
+
* this file.
|
|
32
|
+
*
|
|
33
|
+
* This table arrived late, in 2.2.0: the ten labels below were written as literals passed to
|
|
34
|
+
* `navButton()` / `titleButton()`, so the guard that proves no enhancer string is hard-coded
|
|
35
|
+
* never saw them — it looks for the line that writes to the DOM, and here that line only ever
|
|
36
|
+
* sees a variable. Month and weekday names are NOT here on purpose: they come from `Intl`
|
|
37
|
+
* through the page's `lang`, which is a better hatch than anything the kit could invent.
|
|
38
|
+
* The `{label}` in the three title strings is the period the button drills into. */
|
|
39
|
+
var TEXT = {
|
|
40
|
+
label: 'Date',
|
|
41
|
+
placeholder: 'Choose a date',
|
|
42
|
+
prevMonth: 'Previous month',
|
|
43
|
+
nextMonth: 'Next month',
|
|
44
|
+
prevYear: 'Previous year',
|
|
45
|
+
nextYear: 'Next year',
|
|
46
|
+
prevYears: 'Previous years',
|
|
47
|
+
nextYears: 'Next years',
|
|
48
|
+
chooseMonth: '{label}, choose month',
|
|
49
|
+
chooseYear: '{label}, choose year',
|
|
50
|
+
backToMonths: '{start} to {end}, back to months'
|
|
51
|
+
};
|
|
52
|
+
/* HTML lowercases attribute names, so a camelCase key like `prevMonth` can only ever be written
|
|
53
|
+
as `data-fdy-text-prevmonth`, while the kebab form anybody would reach for,
|
|
54
|
+
`data-fdy-text-prev-month`, becomes a DIFFERENT attribute the enhancer never reads, and the
|
|
55
|
+
override fails silently. So the key is kebab-cased for the lookup; the run-together spelling
|
|
56
|
+
still resolves. */
|
|
57
|
+
function textAttr(root, key) {
|
|
58
|
+
if (!root || !root.getAttribute) return null;
|
|
59
|
+
var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
|
|
60
|
+
return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
|
|
61
|
+
}
|
|
62
|
+
function textOf(root, key, vars) {
|
|
63
|
+
var custom = textAttr(root, key);
|
|
64
|
+
var s = custom != null && custom !== '' ? custom : TEXT[key];
|
|
65
|
+
if (vars) for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
27
68
|
var uidSeq = 0;
|
|
28
69
|
function uid(p) { uidSeq += 1; return p + '-' + uidSeq; }
|
|
29
70
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
@@ -61,8 +102,8 @@
|
|
|
61
102
|
wrap.dataset.fdyDpReady = '1';
|
|
62
103
|
wrap.classList.add('fdy-datepicker');
|
|
63
104
|
|
|
64
|
-
var placeholder = wrap.getAttribute('data-placeholder') ||
|
|
65
|
-
var label = wrap.getAttribute('data-label') || '
|
|
105
|
+
var placeholder = wrap.getAttribute('data-placeholder') || textOf(wrap, 'placeholder');
|
|
106
|
+
var label = wrap.getAttribute('data-label') || textOf(wrap, 'label');
|
|
66
107
|
var selected = parseISO(wrap.getAttribute('data-value'));
|
|
67
108
|
var minDate = parseISO(wrap.getAttribute('data-min'));
|
|
68
109
|
var maxDate = parseISO(wrap.getAttribute('data-max'));
|
|
@@ -160,16 +201,16 @@
|
|
|
160
201
|
panel.innerHTML = '';
|
|
161
202
|
var head = document.createElement('div');
|
|
162
203
|
head.className = 'fdy-cal__head';
|
|
163
|
-
var title = titleButton(monthFmt.format(view), monthFmt.format(view)
|
|
204
|
+
var title = titleButton(monthFmt.format(view), textOf(wrap, 'chooseMonth', { label: monthFmt.format(view) }), function () {
|
|
164
205
|
mode = 'months';
|
|
165
206
|
focusMonth = view.getMonth();
|
|
166
207
|
render();
|
|
167
208
|
focusMonthCell();
|
|
168
209
|
});
|
|
169
210
|
panel.setAttribute('aria-labelledby', title.id);
|
|
170
|
-
head.appendChild(navButton('‹',
|
|
211
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevMonth'), function () { view = addMonths(view, -1); render(); }));
|
|
171
212
|
head.appendChild(title);
|
|
172
|
-
head.appendChild(navButton('›',
|
|
213
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextMonth'), function () { view = addMonths(view, 1); render(); }));
|
|
173
214
|
panel.appendChild(head);
|
|
174
215
|
|
|
175
216
|
var grid = document.createElement('div');
|
|
@@ -226,16 +267,16 @@
|
|
|
226
267
|
var year = view.getFullYear();
|
|
227
268
|
var head = document.createElement('div');
|
|
228
269
|
head.className = 'fdy-cal__head';
|
|
229
|
-
var title = titleButton(String(year),
|
|
270
|
+
var title = titleButton(String(year), textOf(wrap, 'chooseYear', { label: year }), function () {
|
|
230
271
|
mode = 'years';
|
|
231
272
|
focusYear = year;
|
|
232
273
|
render();
|
|
233
274
|
focusYearCell();
|
|
234
275
|
});
|
|
235
276
|
panel.setAttribute('aria-labelledby', title.id);
|
|
236
|
-
head.appendChild(navButton('‹',
|
|
277
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevYear'), function () { view = addMonths(view, -12); render(); focusMonthCell(); }));
|
|
237
278
|
head.appendChild(title);
|
|
238
|
-
head.appendChild(navButton('›',
|
|
279
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextYear'), function () { view = addMonths(view, 12); render(); focusMonthCell(); }));
|
|
239
280
|
panel.appendChild(head);
|
|
240
281
|
|
|
241
282
|
var grid = document.createElement('div');
|
|
@@ -287,16 +328,16 @@
|
|
|
287
328
|
var end = start + YEARS_PER_PAGE - 1;
|
|
288
329
|
var head = document.createElement('div');
|
|
289
330
|
head.className = 'fdy-cal__head';
|
|
290
|
-
var title = titleButton(start + ' – ' + end,
|
|
331
|
+
var title = titleButton(start + ' – ' + end, textOf(wrap, 'backToMonths', { start: start, end: end }), function () {
|
|
291
332
|
mode = 'months';
|
|
292
333
|
focusMonth = view.getMonth();
|
|
293
334
|
render();
|
|
294
335
|
focusMonthCell();
|
|
295
336
|
});
|
|
296
337
|
panel.setAttribute('aria-labelledby', title.id);
|
|
297
|
-
head.appendChild(navButton('‹',
|
|
338
|
+
head.appendChild(navButton('‹', textOf(wrap, 'prevYears'), function () { moveYearFocus(focusYear - YEARS_PER_PAGE); }));
|
|
298
339
|
head.appendChild(title);
|
|
299
|
-
head.appendChild(navButton('›',
|
|
340
|
+
head.appendChild(navButton('›', textOf(wrap, 'nextYears'), function () { moveYearFocus(focusYear + YEARS_PER_PAGE); }));
|
|
300
341
|
panel.appendChild(head);
|
|
301
342
|
|
|
302
343
|
var grid = document.createElement('div');
|
|
@@ -457,7 +498,46 @@
|
|
|
457
498
|
|
|
458
499
|
var _pop = null;
|
|
459
500
|
function popCtl() { if (_pop === null && window.FreedayPopover) _pop = window.FreedayPopover.attach(panel, trigger); return _pop; }
|
|
501
|
+
|
|
502
|
+
/* The three field states the CSS has always styled (`:disabled`, `[aria-readonly="true"]`,
|
|
503
|
+
`[aria-invalid="true"]`) and this enhancer never set, so only the stacks that re-implement
|
|
504
|
+
the control natively had them. Read from the seed at init and settable afterwards, because
|
|
505
|
+
a host that renders once — every Blazor wrapper does, `ShouldRender => false` — cannot
|
|
506
|
+
express a later change any other way. */
|
|
507
|
+
function flagOf(name) {
|
|
508
|
+
var v = wrap.getAttribute('data-' + name);
|
|
509
|
+
return v != null && v !== 'false';
|
|
510
|
+
}
|
|
511
|
+
/* Named `state*`, not `is*`: this file already has an `isDisabled(date)` deciding whether a
|
|
512
|
+
DAY falls outside min/max, and shadowing it with a boolean made the day grid throw on every
|
|
513
|
+
render — silently, since the panel still opened and only its cells went missing. */
|
|
514
|
+
var stateDisabled = flagOf('disabled');
|
|
515
|
+
var stateReadonly = flagOf('readonly');
|
|
516
|
+
var stateInvalid = flagOf('invalid');
|
|
517
|
+
/* `data-id` and `data-describedby`: the trigger this enhancer BUILDS is the element a form
|
|
518
|
+
has to point its label and its error text at, and the raw path had no way to say so. */
|
|
519
|
+
if (wrap.getAttribute('data-id')) trigger.id = wrap.getAttribute('data-id');
|
|
520
|
+
if (wrap.getAttribute('data-describedby')) trigger.setAttribute('aria-describedby', wrap.getAttribute('data-describedby'));
|
|
521
|
+
|
|
522
|
+
function applyState() {
|
|
523
|
+
trigger.disabled = stateDisabled;
|
|
524
|
+
if (stateReadonly) trigger.setAttribute('aria-readonly', 'true');
|
|
525
|
+
else trigger.removeAttribute('aria-readonly');
|
|
526
|
+
if (stateInvalid) trigger.setAttribute('aria-invalid', 'true');
|
|
527
|
+
else trigger.removeAttribute('aria-invalid');
|
|
528
|
+
wrap.classList.toggle('fdy-datepicker--error', stateInvalid);
|
|
529
|
+
if ((stateDisabled || stateReadonly) && !panel.hidden) close(false);
|
|
530
|
+
}
|
|
531
|
+
function setState(next) {
|
|
532
|
+
if (!next) return;
|
|
533
|
+
if (next.disabled != null) stateDisabled = !!next.disabled;
|
|
534
|
+
if (next.readonly != null) stateReadonly = !!next.readonly;
|
|
535
|
+
if (next.invalid != null) stateInvalid = !!next.invalid;
|
|
536
|
+
applyState();
|
|
537
|
+
}
|
|
538
|
+
|
|
460
539
|
function open() {
|
|
540
|
+
if (stateDisabled || stateReadonly) return;
|
|
461
541
|
if (!panel.hidden) return;
|
|
462
542
|
mode = 'days';
|
|
463
543
|
focusDate = selected || focusDate || new Date();
|
|
@@ -487,8 +567,11 @@
|
|
|
487
567
|
|
|
488
568
|
updateDisplay();
|
|
489
569
|
|
|
570
|
+
applyState();
|
|
571
|
+
|
|
490
572
|
var api = {
|
|
491
573
|
wrap: wrap,
|
|
574
|
+
setState: setState,
|
|
492
575
|
getValue: function () { return selected ? toISO(selected) : ''; },
|
|
493
576
|
clear: function () { selected = null; updateDisplay(); if (!panel.hidden) render(); },
|
|
494
577
|
setMin: function (iso) { minDate = parseISO(iso); if (!panel.hidden) render(); },
|
|
@@ -548,5 +631,14 @@
|
|
|
548
631
|
initAll();
|
|
549
632
|
}
|
|
550
633
|
|
|
551
|
-
window.FreedayDatepicker = {
|
|
634
|
+
window.FreedayDatepicker = {
|
|
635
|
+
init: initPicker,
|
|
636
|
+
initAll: initAll,
|
|
637
|
+
/* A host that rendered its seed once and cannot re-render it (Blazor) still has to be able to
|
|
638
|
+
disable, lock or invalidate the field later. */
|
|
639
|
+
setState: function (root, state) {
|
|
640
|
+
var api = root && root._fdyDp ? root._fdyDp : null;
|
|
641
|
+
if (api && api.setState) api.setState(state);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
552
644
|
})();
|
package/dist/freeday-select.js
CHANGED
|
@@ -210,5 +210,22 @@
|
|
|
210
210
|
if (root && root._fdyCombo) root._fdyCombo.setValue(value);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
/* Beside setValue for the same reason it exists: a host that renders its markup once (every
|
|
214
|
+
Blazor wrapper, `ShouldRender => false`) cannot express a later state change any other way,
|
|
215
|
+
and a parameter that silently stops working after the first render is worse than none. */
|
|
216
|
+
function setState(root, state) {
|
|
217
|
+
var button = root ? root.querySelector('.fdy-combo__button') : null;
|
|
218
|
+
if (!button || !state) return;
|
|
219
|
+
if (state.disabled != null) button.disabled = !!state.disabled;
|
|
220
|
+
if (state.readonly != null) {
|
|
221
|
+
if (state.readonly) button.setAttribute('aria-readonly', 'true');
|
|
222
|
+
else button.removeAttribute('aria-readonly');
|
|
223
|
+
}
|
|
224
|
+
if (state.invalid != null) {
|
|
225
|
+
if (state.invalid) button.setAttribute('aria-invalid', 'true');
|
|
226
|
+
else button.removeAttribute('aria-invalid');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
window.FreedayCombo = { init: initCombo, initAll: initAll, setValue: setValue, setState: setState };
|
|
214
231
|
})();
|
package/dist/freeday-stepper.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* <div class="fdy-step-panel">…</div>… </div>
|
|
12
12
|
* <div class="fdy-step-nav"><button data-fdy-step-prev>…</button>
|
|
13
13
|
* <button data-fdy-step-next>…</button></div></div>
|
|
14
|
-
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step)
|
|
14
|
+
* Emits bubbling "fdy-step-change" {index} and "fdy-step-finish" (Next on the last step), and a
|
|
15
|
+
* cancelable "fdy-step-before-change" {from, to, waitFor} that a guard refuses with
|
|
16
|
+
* preventDefault() or defers by assigning a promise to detail.waitFor.
|
|
15
17
|
*/
|
|
16
18
|
(function () {
|
|
17
19
|
'use strict';
|
|
@@ -90,14 +92,62 @@
|
|
|
90
92
|
render();
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/* Leaving a step is REFUSABLE, because a wizard whose Next cannot be stopped is a wizard that
|
|
96
|
+
* validates nothing. Two ways to refuse, and the second is why an event alone was not enough:
|
|
97
|
+
*
|
|
98
|
+
* sync handler calls ev.preventDefault() — the answer is already known
|
|
99
|
+
* async handler sets ev.detail.waitFor = promise — the answer is a server round-trip away
|
|
100
|
+
*
|
|
101
|
+
* Resolving to `false` refuses; anything else advances, so a handler that forgets to return is
|
|
102
|
+
* not read as a rejection. HOW validity is decided stays entirely with the app: the kit has no
|
|
103
|
+
* opinion about form libraries and this is the line that keeps it that way. */
|
|
104
|
+
var deciding = false;
|
|
105
|
+
|
|
106
|
+
function lock(on) {
|
|
107
|
+
deciding = on;
|
|
108
|
+
var list = root.querySelector('.fdy-stepper');
|
|
109
|
+
if (list) { if (on) list.setAttribute('aria-busy', 'true'); else list.removeAttribute('aria-busy'); }
|
|
110
|
+
if (prevBtn) prevBtn.disabled = on || active === 0;
|
|
111
|
+
if (nextBtn) nextBtn.disabled = on;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function request(to, onAllowed) {
|
|
115
|
+
if (deciding) return;
|
|
116
|
+
var ev = new CustomEvent('fdy-step-before-change', {
|
|
117
|
+
bubbles: true,
|
|
118
|
+
cancelable: true,
|
|
119
|
+
detail: { from: active, to: to, waitFor: null },
|
|
120
|
+
});
|
|
121
|
+
root.dispatchEvent(ev);
|
|
122
|
+
if (ev.defaultPrevented) return;
|
|
123
|
+
|
|
124
|
+
var pending = ev.detail.waitFor;
|
|
125
|
+
if (pending === null || typeof pending.then !== 'function') { onAllowed(); return; }
|
|
126
|
+
|
|
127
|
+
lock(true);
|
|
128
|
+
pending.then(
|
|
129
|
+
function (ok) { lock(false); if (ok !== false) onAllowed(); },
|
|
130
|
+
// A guard that THREW decided nothing, so it must not advance. Staying put with the nav
|
|
131
|
+
// released is the only safe reading; the app's own error handling reports the failure.
|
|
132
|
+
function () { lock(false); },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
if (prevBtn) prevBtn.addEventListener('click', function () { go(active - 1); });
|
|
94
137
|
if (nextBtn) nextBtn.addEventListener('click', function () {
|
|
95
|
-
if (active < steps.length - 1) go(active + 1);
|
|
96
|
-
else root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true }));
|
|
138
|
+
if (active < steps.length - 1) request(active + 1, function () { go(active + 1); });
|
|
139
|
+
else request(active + 1, function () { root.dispatchEvent(new CustomEvent('fdy-step-finish', { bubbles: true })); });
|
|
97
140
|
});
|
|
98
141
|
steps.forEach(function (s, i) {
|
|
99
142
|
var btn = s.querySelector('.fdy-step__btn');
|
|
100
|
-
if (btn)
|
|
143
|
+
if (!btn) return;
|
|
144
|
+
btn.addEventListener('click', function () {
|
|
145
|
+
if (i > maxReached) return;
|
|
146
|
+
// Going BACK is always allowed — nothing is being committed. Jumping forward to a step
|
|
147
|
+
// already reached still leaves the current one behind, so it asks the guard like Next does.
|
|
148
|
+
if (i <= active) go(i);
|
|
149
|
+
else request(i, function () { go(i); });
|
|
150
|
+
});
|
|
101
151
|
});
|
|
102
152
|
|
|
103
153
|
render();
|
package/dist/freeday-table.js
CHANGED
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
filterText: 'Contains text',
|
|
48
48
|
filterTextPlaceholder: 'Contains…',
|
|
49
49
|
filterEnum: 'Show values',
|
|
50
|
+
filterMin: 'Min',
|
|
51
|
+
filterMax: 'Max',
|
|
50
52
|
filterRange: 'Value range',
|
|
51
53
|
reset: 'Reset',
|
|
52
54
|
close: 'Close',
|
|
@@ -319,8 +321,8 @@
|
|
|
319
321
|
pop.appendChild(filterTitle(textOf(root, 'filterRange')));
|
|
320
322
|
var range = document.createElement('div');
|
|
321
323
|
range.className = 'fdy-filter__range';
|
|
322
|
-
var minI = numberInput('
|
|
323
|
-
var maxI = numberInput('
|
|
324
|
+
var minI = numberInput(textOf(root, 'filterMin'), f.min);
|
|
325
|
+
var maxI = numberInput(textOf(root, 'filterMax'), f.max);
|
|
324
326
|
var applyRange = function () {
|
|
325
327
|
f.min = minI.value !== '' ? parseNum(minI.value) : null;
|
|
326
328
|
f.max = maxI.value !== '' ? parseNum(maxI.value) : null;
|