@cahyo-dimas/freeday 2.2.0 → 3.1.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 +251 -0
- package/COMPONENTS.md +183 -7
- 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/FdyTable.razor +44 -4
- package/adapters/blazor/FdyTable.razor.cs +110 -0
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/freeday-blazor.js +8 -0
- package/adapters/react/components/FdyAppShell.tsx +52 -11
- package/adapters/react/components/FdyTable.tsx +123 -2
- package/adapters/react/components/FdyTableFooter.tsx +1 -1
- package/adapters/vue/components/FdyAppShell.vue +41 -11
- package/adapters/vue/components/FdyTable.vue +125 -4
- package/adapters/vue/components/FdyTableFooter.vue +1 -1
- package/dist/freeday-app-shell.js +23 -5
- package/dist/freeday-busy.js +168 -0
- package/dist/freeday-stepper.js +54 -4
- package/dist/freeday-table.js +7 -0
- package/dist/freeday.bundle.css +652 -43
- package/dist/freeday.css +129 -15
- package/dist/freeday.js +253 -9
- 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 +42 -0
- package/src/components/card.css +1 -1
- package/src/components/composition.css +15 -2
- package/src/components/drawer.css +1 -1
- package/src/components/filterbar.css +3 -2
- package/src/components/input.css +9 -0
- 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
|
@@ -76,6 +76,23 @@ const props = withDefaults(defineProps<{
|
|
|
76
76
|
rowClass?: (row: Row) => string | undefined;
|
|
77
77
|
/** Controlled: row keys whose `row-detail` slot is shown as a full-width row beneath them. */
|
|
78
78
|
expandedKeys?: ReadonlyArray<string | number>;
|
|
79
|
+
/** Zebra-stripe the body rows. */
|
|
80
|
+
striped?: boolean;
|
|
81
|
+
/** Render the checkbox column and the bulk bar. */
|
|
82
|
+
selectable?: boolean;
|
|
83
|
+
/** Controlled selection, as `rowKey` values. Provide to own it; omit for internal (the column
|
|
84
|
+
* still works with nothing wired). */
|
|
85
|
+
selectedKeys?: ReadonlyArray<string | number>;
|
|
86
|
+
/** Bulk-bar count, `{n}` substituted. Default `{n} selected`. */
|
|
87
|
+
selectedText?: string;
|
|
88
|
+
/** Bulk-bar clear button. Default `Clear`. */
|
|
89
|
+
clearSelectionText?: string;
|
|
90
|
+
/** Accessible name of the header checkbox. Default `Select all rows on this page`. */
|
|
91
|
+
selectAllLabel?: string;
|
|
92
|
+
/** Accessible name of each row checkbox. Default `Select row`. */
|
|
93
|
+
selectRowLabel?: string;
|
|
94
|
+
/** Accessible name of the bulk bar region. Default `Bulk actions`. */
|
|
95
|
+
bulkLabel?: string;
|
|
79
96
|
}>(), { pager: true });
|
|
80
97
|
|
|
81
98
|
const emit = defineEmits<{
|
|
@@ -90,6 +107,9 @@ const emit = defineEmits<{
|
|
|
90
107
|
'update:pageSize': [size: number];
|
|
91
108
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
92
109
|
'row-activate': [row: Row];
|
|
110
|
+
/** Selection changed, as `rowKey` values. Fires in both modes, so a screen can watch the
|
|
111
|
+
* selection without owning it. */
|
|
112
|
+
'update:selectedKeys': [keys: Array<string | number>];
|
|
93
113
|
/** The processed page of rows (after filter/sort/paginate) plus the total row count, fires in
|
|
94
114
|
* BOTH modes whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
95
115
|
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
@@ -262,6 +282,65 @@ function onRowKeydown(e: KeyboardEvent, row: Row): void {
|
|
|
262
282
|
function isExpanded(row: Row): boolean {
|
|
263
283
|
return props.expandedKeys?.includes(props.rowKey(row)) === true;
|
|
264
284
|
}
|
|
285
|
+
|
|
286
|
+
/* Selection is keyed by `rowKey`, exactly as `expandedKeys` is, and for the same reason: a key
|
|
287
|
+
* survives the re-fetch that replaces every row object, an object identity does not. Controlled when
|
|
288
|
+
* `selectedKeys` is provided, internal otherwise, so the column works with nothing wired. */
|
|
289
|
+
const internalSelectedKeys: Ref<Array<string | number>> = ref([]);
|
|
290
|
+
const selectionControlled: ComputedRef<boolean> = computed((): boolean => props.selectedKeys !== undefined);
|
|
291
|
+
const effectiveSelectedKeys: ComputedRef<ReadonlyArray<string | number>> = computed(
|
|
292
|
+
(): ReadonlyArray<string | number> =>
|
|
293
|
+
selectionControlled.value ? (props.selectedKeys as ReadonlyArray<string | number>) : internalSelectedKeys.value,
|
|
294
|
+
);
|
|
295
|
+
const selectedCount: ComputedRef<number> = computed((): number => effectiveSelectedKeys.value.length);
|
|
296
|
+
/* The select-all box acts on the CURRENT PAGE, not on every filtered row: a header checkbox that
|
|
297
|
+
* silently selects rows the reader cannot see is how bulk deletes go wrong. Keys picked on other
|
|
298
|
+
* pages are preserved rather than dropped, so paging away and back does not lose them. */
|
|
299
|
+
const pageKeys: ComputedRef<Array<string | number>> = computed((): Array<string | number> =>
|
|
300
|
+
displayRows.value.map((row: Row): string | number => props.rowKey(row)),
|
|
301
|
+
);
|
|
302
|
+
const allPageSelected: ComputedRef<boolean> = computed((): boolean =>
|
|
303
|
+
pageKeys.value.length > 0 && pageKeys.value.every((k: string | number): boolean => effectiveSelectedKeys.value.includes(k)),
|
|
304
|
+
);
|
|
305
|
+
const somePageSelected: ComputedRef<boolean> = computed((): boolean =>
|
|
306
|
+
!allPageSelected.value && pageKeys.value.some((k: string | number): boolean => effectiveSelectedKeys.value.includes(k)),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
/* Always emits, controlled or not — the same call the `update:pageSize` control makes, and for the
|
|
310
|
+
* same reason: a screen that only wants to WATCH the selection (a summary line, an export button)
|
|
311
|
+
* should not have to take ownership of it to hear about it. */
|
|
312
|
+
function setSelection(keys: Array<string | number>): void {
|
|
313
|
+
if (!selectionControlled.value) internalSelectedKeys.value = keys;
|
|
314
|
+
emit('update:selectedKeys', keys);
|
|
315
|
+
}
|
|
316
|
+
function isSelected(row: Row): boolean {
|
|
317
|
+
return effectiveSelectedKeys.value.includes(props.rowKey(row));
|
|
318
|
+
}
|
|
319
|
+
function toggleRow(row: Row, checked: boolean): void {
|
|
320
|
+
const key: string | number = props.rowKey(row);
|
|
321
|
+
const next: Array<string | number> = effectiveSelectedKeys.value.filter((k: string | number): boolean => k !== key);
|
|
322
|
+
if (checked) next.push(key);
|
|
323
|
+
setSelection(next);
|
|
324
|
+
}
|
|
325
|
+
function toggleAllOnPage(checked: boolean): void {
|
|
326
|
+
const onPage: Set<string | number> = new Set(pageKeys.value);
|
|
327
|
+
const offPage: Array<string | number> = effectiveSelectedKeys.value.filter(
|
|
328
|
+
(k: string | number): boolean => !onPage.has(k),
|
|
329
|
+
);
|
|
330
|
+
setSelection(checked ? offPage.concat(pageKeys.value) : offPage);
|
|
331
|
+
}
|
|
332
|
+
function clearSelection(): void {
|
|
333
|
+
setSelection([]);
|
|
334
|
+
}
|
|
335
|
+
function selectedLabel(n: number): string {
|
|
336
|
+
return (props.selectedText ?? '{n} selected').replace('{n}', String(n));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* The checkbox column widens every full-width row (loading, empty, row detail) by one. Deriving it
|
|
340
|
+
* once is what keeps a later column change from leaving one of the three behind. */
|
|
341
|
+
const colSpan: ComputedRef<number> = computed((): number =>
|
|
342
|
+
props.columns.length + (props.selectable === true ? 1 : 0),
|
|
343
|
+
);
|
|
265
344
|
</script>
|
|
266
345
|
|
|
267
346
|
<template>
|
|
@@ -270,10 +349,38 @@ function isExpanded(row: Row): boolean {
|
|
|
270
349
|
<slot name="toolbar" />
|
|
271
350
|
</div>
|
|
272
351
|
|
|
352
|
+
<div
|
|
353
|
+
v-if="selectable"
|
|
354
|
+
class="fdy-table-bulkbar"
|
|
355
|
+
:hidden="selectedCount === 0"
|
|
356
|
+
role="region"
|
|
357
|
+
:aria-label="bulkLabel ?? 'Bulk actions'"
|
|
358
|
+
>
|
|
359
|
+
<span class="fdy-table-bulkbar__count" aria-live="polite">{{ selectedLabel(selectedCount) }}</span>
|
|
360
|
+
<span class="fdy-table-bulkbar__spacer"></span>
|
|
361
|
+
<div class="fdy-table-bulkbar__actions">
|
|
362
|
+
<slot name="bulk-actions" :keys="effectiveSelectedKeys" :clear="clearSelection" />
|
|
363
|
+
<button type="button" class="fdy-btn fdy-btn--ghost fdy-btn--sm" @click="clearSelection">
|
|
364
|
+
{{ clearSelectionText ?? 'Clear' }}
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
273
369
|
<div class="fdy-table-scroll">
|
|
274
|
-
<table class="fdy-table" :aria-label="ariaLabel">
|
|
370
|
+
<table class="fdy-table" :class="{ 'fdy-table--striped': striped }" :aria-label="ariaLabel">
|
|
275
371
|
<thead>
|
|
276
372
|
<tr>
|
|
373
|
+
<th v-if="selectable" class="fdy-table__selcol" scope="col">
|
|
374
|
+
<input
|
|
375
|
+
type="checkbox"
|
|
376
|
+
class="fdy-checkbox"
|
|
377
|
+
data-fdy-select-all
|
|
378
|
+
:checked="allPageSelected"
|
|
379
|
+
:indeterminate.prop="somePageSelected"
|
|
380
|
+
:aria-label="selectAllLabel ?? 'Select all rows on this page'"
|
|
381
|
+
@change="toggleAllOnPage(($event.target as HTMLInputElement).checked)"
|
|
382
|
+
>
|
|
383
|
+
</th>
|
|
277
384
|
<th v-for="col in columns" :key="col.key" scope="col" :style="alignStyle(col)" :aria-sort="ariaSortOf(col)">
|
|
278
385
|
<button
|
|
279
386
|
v-if="col.sortable"
|
|
@@ -295,10 +402,10 @@ function isExpanded(row: Row): boolean {
|
|
|
295
402
|
</thead>
|
|
296
403
|
<tbody>
|
|
297
404
|
<tr v-if="loading">
|
|
298
|
-
<td :colspan="
|
|
405
|
+
<td :colspan="colSpan" class="fdy-table__state" role="status">Loading…</td>
|
|
299
406
|
</tr>
|
|
300
407
|
<tr v-else-if="displayRows.length === 0">
|
|
301
|
-
<td :colspan="
|
|
408
|
+
<td :colspan="colSpan" class="fdy-table__state">
|
|
302
409
|
<slot name="empty">{{ emptyText ?? 'No data' }}</slot>
|
|
303
410
|
</td>
|
|
304
411
|
</tr>
|
|
@@ -308,15 +415,29 @@ function isExpanded(row: Row): boolean {
|
|
|
308
415
|
:class="rowClasses(row)"
|
|
309
416
|
:tabindex="rowActivatable ? 0 : undefined"
|
|
310
417
|
:aria-expanded="$slots['row-detail'] ? (isExpanded(row) ? 'true' : 'false') : undefined"
|
|
418
|
+
:aria-selected="selectable ? (isSelected(row) ? 'true' : 'false') : undefined"
|
|
311
419
|
@click="onRowClick(row)"
|
|
312
420
|
@keydown="onRowKeydown($event, row)"
|
|
313
421
|
>
|
|
422
|
+
<td v-if="selectable" class="fdy-table__selcol">
|
|
423
|
+
<!-- `.stop`: without it, ticking a checkbox in an activatable row also fires
|
|
424
|
+
`row-activate`, so selecting a row would navigate away from it. -->
|
|
425
|
+
<input
|
|
426
|
+
type="checkbox"
|
|
427
|
+
class="fdy-checkbox"
|
|
428
|
+
data-fdy-row-select
|
|
429
|
+
:checked="isSelected(row)"
|
|
430
|
+
:aria-label="selectRowLabel ?? 'Select row'"
|
|
431
|
+
@click.stop
|
|
432
|
+
@change="toggleRow(row, ($event.target as HTMLInputElement).checked)"
|
|
433
|
+
>
|
|
434
|
+
</td>
|
|
314
435
|
<td v-for="col in columns" :key="col.key" :class="cellClass(col)" :style="alignStyle(col)">
|
|
315
436
|
<slot :name="`cell-${col.key}`" :row="row" :value="cellValue(row, col)">{{ cellText(row, col) }}</slot>
|
|
316
437
|
</td>
|
|
317
438
|
</tr>
|
|
318
439
|
<tr v-if="$slots['row-detail'] && isExpanded(row)" class="fdy-table__detailrow">
|
|
319
|
-
<td :colspan="
|
|
440
|
+
<td :colspan="colSpan"><slot name="row-detail" :row="row" /></td>
|
|
320
441
|
</tr>
|
|
321
442
|
</template>
|
|
322
443
|
</template>
|
|
@@ -60,7 +60,13 @@
|
|
|
60
60
|
|
|
61
61
|
function isOverlayOpen() { return app.classList.contains('fdy-app--nav-open'); }
|
|
62
62
|
function isCollapsed() { return app.classList.contains('fdy-app--nav-collapsed'); }
|
|
63
|
-
|
|
63
|
+
/* Whether the nav FLOATS. Two ways to be true, and only one of them is the viewport: below the
|
|
64
|
+
breakpoint it is off-canvas by definition, and above it `--nav-overlay` says the app chose to
|
|
65
|
+
float a nav that could have been a column. Everything downstream — which class means visible,
|
|
66
|
+
what the toggle does, whether the content goes inert — asks this instead of the media query,
|
|
67
|
+
so overlay mode reuses the drawer's whole code path rather than growing a second one. */
|
|
68
|
+
function isOverlayMode() { return !mqWide.matches || app.classList.contains('fdy-app--nav-overlay'); }
|
|
69
|
+
function navVisible() { return isOverlayMode() ? isOverlayOpen() : !isCollapsed(); }
|
|
64
70
|
|
|
65
71
|
/* aria-expanded answers "is the nav showing?" in BOTH modes, the two state classes are the
|
|
66
72
|
kit's business, not the reader's. */
|
|
@@ -68,7 +74,7 @@
|
|
|
68
74
|
var visible = navVisible();
|
|
69
75
|
toggle.setAttribute('aria-expanded', String(visible));
|
|
70
76
|
setInert(sidebar, !visible);
|
|
71
|
-
setInert(content,
|
|
77
|
+
setInert(content, isOverlayMode() && visible);
|
|
72
78
|
/* Announce only real changes. The first sync() runs at init to describe the state the markup
|
|
73
79
|
arrived in, which is not something a host asked for and must not look like one. */
|
|
74
80
|
if (lastVisible !== null && visible !== lastVisible) {
|
|
@@ -106,7 +112,7 @@
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
toggle.addEventListener('click', function () {
|
|
109
|
-
if (
|
|
115
|
+
if (!isOverlayMode()) {
|
|
110
116
|
app.classList.toggle('fdy-app--nav-collapsed');
|
|
111
117
|
sync();
|
|
112
118
|
} else if (isOverlayOpen()) {
|
|
@@ -154,7 +160,10 @@
|
|
|
154
160
|
content inert forever: the panel becomes a static column again, and the page it is covering
|
|
155
161
|
can no longer be clicked or read. */
|
|
156
162
|
mqWide.addEventListener('change', function () {
|
|
157
|
-
|
|
163
|
+
/* Only when the panel stops floating. In overlay MODE it floats at every width, so widening
|
|
164
|
+
must leave an open panel exactly as it is — closing it there would be the shell overruling
|
|
165
|
+
a reader who never asked for anything. */
|
|
166
|
+
if (mqWide.matches && !isOverlayMode() && isOverlayOpen()) close(false);
|
|
158
167
|
else sync();
|
|
159
168
|
});
|
|
160
169
|
|
|
@@ -164,9 +173,14 @@
|
|
|
164
173
|
own state needs to drive this without reaching for the class names the kit reserves. */
|
|
165
174
|
app._fdyAppShell = {
|
|
166
175
|
isVisible: navVisible,
|
|
176
|
+
/* Re-read the DOM and reconcile `inert` + `aria-expanded`. Needed when something OUTSIDE the
|
|
177
|
+
enhancer changes what the state classes mean — switching `--nav-overlay` on or off does
|
|
178
|
+
exactly that, because it moves the answer to "is the nav visible?" from `--nav-collapsed`
|
|
179
|
+
to `--nav-open`. Without this a mode switch leaves a visible sidebar marked inert. */
|
|
180
|
+
refresh: sync,
|
|
167
181
|
setVisible: function (visible) {
|
|
168
182
|
if (visible === navVisible()) return;
|
|
169
|
-
if (
|
|
183
|
+
if (!isOverlayMode()) {
|
|
170
184
|
app.classList.toggle('fdy-app--nav-collapsed', !visible);
|
|
171
185
|
sync();
|
|
172
186
|
} else if (visible) {
|
|
@@ -203,5 +217,9 @@
|
|
|
203
217
|
isVisible: function (root) {
|
|
204
218
|
return !!(root && root._fdyAppShell && root._fdyAppShell.isVisible());
|
|
205
219
|
},
|
|
220
|
+
/* Call after changing `--nav-overlay` from outside; see the note on the handle above. */
|
|
221
|
+
refresh: function (root) {
|
|
222
|
+
if (root && root._fdyAppShell) root._fdyAppShell.refresh();
|
|
223
|
+
},
|
|
206
224
|
};
|
|
207
225
|
})();
|
|
@@ -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-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
|
@@ -84,6 +84,13 @@
|
|
|
84
84
|
var countEl = root.querySelector('[data-fdy-table-count]');
|
|
85
85
|
var infoEl = root.querySelector('[data-fdy-table-info]');
|
|
86
86
|
var pagerEl = root.querySelector('[data-fdy-table-pagination]');
|
|
87
|
+
/* COMPONENTS.md documents the wrapper as <nav class="fdy-pagination" data-fdy-table-pagination>
|
|
88
|
+
and nothing in the kit ever wrote that class, here or in the three typed footers (#050 §2).
|
|
89
|
+
It carries no rule (NEXT-UP #9), so nothing rendered differently, but a consumer selector or
|
|
90
|
+
an e2e assertion on the block passed against hand-written markup and failed against the kit's
|
|
91
|
+
own output, and the raw and typed paths are supposed to be interchangeable. Set once at init,
|
|
92
|
+
not per render: the wrapper outlives every pager we build into it. */
|
|
93
|
+
if (pagerEl) pagerEl.classList.add('fdy-pagination');
|
|
87
94
|
var pageSizeEl = root.querySelector('[data-fdy-table-page-size]');
|
|
88
95
|
var selectAll = root.querySelector('[data-fdy-select-all]');
|
|
89
96
|
var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 0; // 0 = no pagination
|