@fieldia/viewer 0.9.1 → 0.10.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/README.md +20 -1
- package/bundle/fieldia.ar.js +3 -0
- package/bundle/fieldia.ar.js.map +7 -0
- package/bundle/fieldia.de.js +3 -0
- package/bundle/fieldia.de.js.map +7 -0
- package/bundle/fieldia.fr.js +3 -0
- package/bundle/fieldia.fr.js.map +7 -0
- package/bundle/fieldia.js +4 -4
- package/bundle/fieldia.js.map +4 -4
- package/package.json +6 -3
- package/src/index.d.mts +4 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -1
- package/src/index.mjs +4 -0
- package/src/index.mjs.map +1 -1
- package/src/lib/add-language.d.mts +20 -0
- package/src/lib/add-language.d.ts +20 -0
- package/src/lib/add-language.js +19 -0
- package/src/lib/add-language.js.map +1 -0
- package/src/lib/add-language.mjs +16 -0
- package/src/lib/add-language.mjs.map +1 -0
- package/src/lib/dialog.d.mts +7 -2
- package/src/lib/dialog.d.ts +7 -2
- package/src/lib/dialog.js +16 -21
- package/src/lib/dialog.js.map +1 -1
- package/src/lib/dialog.mjs +16 -21
- package/src/lib/dialog.mjs.map +1 -1
- package/src/lib/dom.d.mts +10 -0
- package/src/lib/dom.d.ts +10 -0
- package/src/lib/dom.js +26 -0
- package/src/lib/dom.js.map +1 -1
- package/src/lib/dom.mjs +23 -1
- package/src/lib/dom.mjs.map +1 -1
- package/src/lib/focus-trap.d.mts +2 -0
- package/src/lib/focus-trap.d.ts +2 -0
- package/src/lib/focus-trap.js +7 -0
- package/src/lib/focus-trap.js.map +1 -0
- package/src/lib/focus-trap.mjs +3 -0
- package/src/lib/focus-trap.mjs.map +1 -0
- package/src/lib/labels.d.mts +13 -0
- package/src/lib/labels.d.ts +13 -0
- package/src/lib/labels.js +103 -277
- package/src/lib/labels.js.map +1 -1
- package/src/lib/labels.mjs +102 -277
- package/src/lib/labels.mjs.map +1 -1
- package/src/lib/list-styles.d.mts +1 -1
- package/src/lib/list-styles.d.ts +1 -1
- package/src/lib/list-styles.js +3 -2
- package/src/lib/list-styles.js.map +1 -1
- package/src/lib/list-styles.mjs +3 -2
- package/src/lib/list-styles.mjs.map +1 -1
- package/src/lib/locales/all.d.mts +8 -0
- package/src/lib/locales/all.d.ts +8 -0
- package/src/lib/locales/all.js +13 -0
- package/src/lib/locales/all.js.map +1 -0
- package/src/lib/locales/all.mjs +10 -0
- package/src/lib/locales/all.mjs.map +1 -0
- package/src/lib/locales/ar.d.mts +3 -0
- package/src/lib/locales/ar.d.ts +3 -0
- package/src/lib/locales/ar.js +74 -0
- package/src/lib/locales/ar.js.map +1 -0
- package/src/lib/locales/ar.mjs +71 -0
- package/src/lib/locales/ar.mjs.map +1 -0
- package/src/lib/locales/de.d.mts +3 -0
- package/src/lib/locales/de.d.ts +3 -0
- package/src/lib/locales/de.js +74 -0
- package/src/lib/locales/de.js.map +1 -0
- package/src/lib/locales/de.mjs +71 -0
- package/src/lib/locales/de.mjs.map +1 -0
- package/src/lib/locales/fr.d.mts +3 -0
- package/src/lib/locales/fr.d.ts +3 -0
- package/src/lib/locales/fr.js +74 -0
- package/src/lib/locales/fr.js.map +1 -0
- package/src/lib/locales/fr.mjs +71 -0
- package/src/lib/locales/fr.mjs.map +1 -0
- package/src/lib/look.d.mts +21 -0
- package/src/lib/look.d.ts +21 -0
- package/src/lib/look.js +84 -0
- package/src/lib/look.js.map +1 -0
- package/src/lib/look.mjs +79 -0
- package/src/lib/look.mjs.map +1 -0
- package/src/lib/place.d.mts +35 -0
- package/src/lib/place.d.ts +35 -0
- package/src/lib/place.js +37 -0
- package/src/lib/place.js.map +1 -0
- package/src/lib/place.mjs +33 -0
- package/src/lib/place.mjs.map +1 -0
- package/src/lib/related.js +2 -1
- package/src/lib/related.js.map +1 -1
- package/src/lib/related.mjs +2 -1
- package/src/lib/related.mjs.map +1 -1
- package/src/lib/viewer.d.mts +10 -2
- package/src/lib/viewer.d.ts +10 -2
- package/src/lib/viewer.js +160 -57
- package/src/lib/viewer.js.map +1 -1
- package/src/lib/viewer.mjs +162 -59
- package/src/lib/viewer.mjs.map +1 -1
package/src/lib/viewer.js
CHANGED
|
@@ -6,7 +6,11 @@ const core_1 = require("@fieldia/core");
|
|
|
6
6
|
const widgets_1 = require("@fieldia/widgets");
|
|
7
7
|
const related_1 = require("./related");
|
|
8
8
|
const list_1 = require("./list");
|
|
9
|
+
const look_1 = require("./look");
|
|
10
|
+
const place_1 = require("./place");
|
|
11
|
+
const dom_1 = require("./dom");
|
|
9
12
|
const labels_1 = require("./labels");
|
|
13
|
+
const focus_trap_1 = require("./focus-trap");
|
|
10
14
|
var labels_2 = require("./labels");
|
|
11
15
|
Object.defineProperty(exports, "VIEWER_LABELS", { enumerable: true, get: function () { return labels_2.VIEWER_LABELS; } });
|
|
12
16
|
Object.defineProperty(exports, "DEFAULT_LABELS", { enumerable: true, get: function () { return labels_2.DEFAULT_LABELS; } });
|
|
@@ -23,15 +27,18 @@ function mountViewer(host, options) {
|
|
|
23
27
|
if (!checked.ok) {
|
|
24
28
|
throw new Error(`This page cannot be shown:\n${checked.issues.map((i) => ` ${i.path}: ${i.message}`).join('\n')}`);
|
|
25
29
|
}
|
|
26
|
-
|
|
27
|
-
const
|
|
30
|
+
// The page's own words in its language first; the app's translator over what is left.
|
|
31
|
+
const tag = options.locale ?? checked.page.language;
|
|
32
|
+
const localized = tag ? (0, core_1.localizePage)(checked.page, tag) : checked.page;
|
|
33
|
+
const page = options.translate ? (0, core_1.translatePage)(localized, options.translate) : localized;
|
|
34
|
+
const locale = (0, labels_1.ownLocale)(tag);
|
|
28
35
|
const preferences = options.preferences ?? (0, widgets_1.browserPreferences)();
|
|
29
36
|
const dialogs = (0, related_1.pageDialogs)(options);
|
|
30
37
|
const ownsForm = !options.form;
|
|
31
38
|
const form = options.form ?? (0, core_1.createForm)({ ...options, page, messages: options.messages ?? core_1.MESSAGES[locale] });
|
|
32
39
|
const labels = { ...labels_1.VIEWER_LABELS[locale], ...options.labels };
|
|
33
40
|
const widgetLabels = widgets_1.WIDGET_LABELS[locale];
|
|
34
|
-
const dir = options.dir ?? (
|
|
41
|
+
const dir = options.dir ?? (tag && (0, core_1.isRightToLeft)(tag) ? 'rtl' : undefined);
|
|
35
42
|
const prefix = `fd${++mounts}`;
|
|
36
43
|
const updaters = [];
|
|
37
44
|
const cleanups = [];
|
|
@@ -46,21 +53,30 @@ function mountViewer(host, options) {
|
|
|
46
53
|
node.append(...children);
|
|
47
54
|
return node;
|
|
48
55
|
}
|
|
49
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Words with the icon a page names before them, when there is such an icon.
|
|
58
|
+
* The words sit in a <bdi>: left as written on a page of the other direction,
|
|
59
|
+
* they still run their own way, the icon still before them.
|
|
60
|
+
*/
|
|
50
61
|
function withIcon(name, text) {
|
|
51
62
|
const icon = (0, widgets_1.drawIcon)(doc, name, options.icons);
|
|
52
|
-
|
|
63
|
+
const words = el('bdi', {}, text);
|
|
64
|
+
return icon ? [icon, words] : [words];
|
|
53
65
|
}
|
|
54
|
-
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': options.skin ?? 'underline', dir, lang:
|
|
66
|
+
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': options.skin ?? 'underline', dir, lang: tag, 'data-max-width': page.maxWidth });
|
|
67
|
+
(0, look_1.applyLook)(root, page.look);
|
|
55
68
|
const confirm = options.confirm ?? dialogConfirm;
|
|
56
69
|
// ---- the parts -------------------------------------------------------
|
|
57
|
-
|
|
70
|
+
/** A field with its label, help and messages. `labels` is where labels sit where it is put; the title's own fields take none. */
|
|
71
|
+
function fieldItem(node, labels) {
|
|
58
72
|
const def = page.fields[node.field];
|
|
59
73
|
const id = uid(node.id);
|
|
60
|
-
const
|
|
74
|
+
const labelsAt = (0, place_1.labelPlace)(node, def.type, labels);
|
|
75
|
+
const wrapper = el('div', { class: 'fd-field', 'data-node': node.id, 'data-field': node.field, 'data-type': def.type, 'data-labels': labelsAt });
|
|
61
76
|
if (node.colspan)
|
|
62
77
|
wrapper.style.setProperty('--fd-span', String(node.colspan));
|
|
63
|
-
const
|
|
78
|
+
const labelText = node.label ?? def.label;
|
|
79
|
+
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id }, labelText);
|
|
64
80
|
// The ✓ of a field filled in right; never on a yes/no box, a table or a file, where it would say nothing.
|
|
65
81
|
const mark = options.showValid && !NO_VALID_MARK.has(def.type) ? (0, widgets_1.drawIcon)(doc, 'check') : null;
|
|
66
82
|
if (mark) {
|
|
@@ -68,33 +84,56 @@ function mountViewer(host, options) {
|
|
|
68
84
|
label.append(mark);
|
|
69
85
|
}
|
|
70
86
|
const widget = (0, widgets_1.createWidget)({ form, name: node.field, field: def, node, id, document: doc, labels: widgetLabels, preferences, locale, dialogs }, options.widgets);
|
|
87
|
+
// A label kept out of sight still names the box; the empty box shows it instead, unless the page gives it words of its own.
|
|
88
|
+
if (labelsAt === 'hidden' && !node.placeholder) {
|
|
89
|
+
const box = widget.element.matches(TEXT_BOX) ? widget.element : widget.element.querySelector(TEXT_BOX);
|
|
90
|
+
box?.setAttribute('placeholder', labelText);
|
|
91
|
+
}
|
|
71
92
|
if (!['INPUT', 'SELECT', 'TEXTAREA'].includes(widget.element.tagName)) {
|
|
72
93
|
// `for` stays: a custom field that puts the id on its own input is
|
|
73
94
|
// labelled natively. Only a wrapper with a role (a radio group, say) may
|
|
74
95
|
// carry a name; ARIA forbids naming a plain div or span.
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
// A group inside a box of its own (a radio group with "Clear selection" under it) is named too.
|
|
97
|
+
const named = widget.element.getAttribute('role') ? widget.element : widget.element.querySelector(`[id="${id}"][role]`);
|
|
98
|
+
named?.setAttribute('aria-labelledby', label.id);
|
|
77
99
|
label.addEventListener('click', () => widget.focus());
|
|
78
100
|
}
|
|
79
101
|
const helpText = node.help ?? def.help;
|
|
80
102
|
const help = helpText ? el('div', { class: 'fd-help', id: `${id}-help` }, helpText) : null;
|
|
81
103
|
// Not an alert of its own: a refused save is announced once, naming every field to look at.
|
|
82
104
|
const error = el('div', { class: 'fd-error', id: `${id}-error`, hidden: '' });
|
|
83
|
-
// A warning from the data source's onchange
|
|
84
|
-
const warning = el('div', { class: 'fd-warning', role: 'status', hidden: '' });
|
|
85
|
-
wrapper.append(label, widget.element, ...(help ? [help] : []), error, warning);
|
|
105
|
+
// A warning from an answer rule, and one from the data source's onchange beside the field whose change brought it.
|
|
106
|
+
const warning = el('div', { class: 'fd-warning', id: `${id}-warning`, role: 'status', hidden: '' });
|
|
107
|
+
wrapper.append(...(labelsAt === 'after' ? [widget.element, label] : [label, widget.element]), ...(help ? [help] : []), error, warning);
|
|
86
108
|
if (widget.destroy)
|
|
87
109
|
cleanups.push(() => widget.destroy?.());
|
|
88
|
-
|
|
110
|
+
// An answer rule's warning waits until the person leaves the field: no advice
|
|
111
|
+
// mid-word. One already shown stays while they put it right, and goes once they have.
|
|
112
|
+
let typing = false;
|
|
113
|
+
let advice = '';
|
|
114
|
+
wrapper.addEventListener('focusin', () => (typing = true));
|
|
115
|
+
wrapper.addEventListener('focusout', (event) => {
|
|
116
|
+
if (wrapper.contains(event.relatedTarget))
|
|
117
|
+
return;
|
|
118
|
+
typing = false;
|
|
119
|
+
// Not at once: focus also leaves when a redraw removes the focused part
|
|
120
|
+
// (a deleted line's button), and that redraw must finish first.
|
|
121
|
+
queueMicrotask(() => update(form.getState()));
|
|
122
|
+
});
|
|
123
|
+
const update = (state) => {
|
|
89
124
|
const shown = form.node(node.id);
|
|
90
|
-
wrapper
|
|
125
|
+
(0, dom_1.setHidden)(wrapper, shown.invisible);
|
|
91
126
|
wrapper.classList.toggle('fd-required', shown.required);
|
|
92
127
|
const message = state.errors[node.field];
|
|
93
|
-
error
|
|
94
|
-
error
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
(0, dom_1.setHidden)(error, !message);
|
|
129
|
+
(0, dom_1.setText)(error, message ?? '');
|
|
130
|
+
const now = state.warnings[node.field] ?? '';
|
|
131
|
+
if (!typing || !now)
|
|
132
|
+
advice = now;
|
|
133
|
+
// An error shown says enough: the rule's warning gives way to it.
|
|
134
|
+
const warned = [message ? '' : advice, state.warning && state.warningField === node.field ? state.warning : ''].filter(Boolean).join(' ');
|
|
135
|
+
(0, dom_1.setHidden)(warning, !warned);
|
|
136
|
+
(0, dom_1.setText)(warning, warned);
|
|
98
137
|
if (mark) {
|
|
99
138
|
const value = state.values[node.field];
|
|
100
139
|
const filled = value !== null && value !== undefined && value !== '' && !(Array.isArray(value) && !value.length);
|
|
@@ -108,18 +147,18 @@ function mountViewer(host, options) {
|
|
|
108
147
|
readonly: shown.readonly || locked,
|
|
109
148
|
required: shown.required,
|
|
110
149
|
invalid: !!message,
|
|
111
|
-
describedBy: [help?.id, message ? error.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
150
|
+
describedBy: [help?.id, message ? error.id : undefined, warned ? warning.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
112
151
|
});
|
|
113
|
-
}
|
|
152
|
+
};
|
|
153
|
+
updaters.push(update);
|
|
114
154
|
return wrapper;
|
|
115
155
|
}
|
|
116
156
|
function hideWhen(element, id) {
|
|
117
|
-
updaters.push(() =>
|
|
118
|
-
element.hidden = form.node(id).invisible;
|
|
119
|
-
});
|
|
157
|
+
updaters.push(() => (0, dom_1.setHidden)(element, form.node(id).invisible));
|
|
120
158
|
}
|
|
121
159
|
function buttonItem(node) {
|
|
122
160
|
const button = el('button', { type: 'button', class: `fd-button fd-button-${node.style ?? 'secondary'}`, 'data-node': node.id }, ...withIcon(node.icon, node.label));
|
|
161
|
+
spans(button, node.colspan);
|
|
123
162
|
button.addEventListener('click', async () => {
|
|
124
163
|
if (node.confirm && !(await confirm(node.confirm)))
|
|
125
164
|
return;
|
|
@@ -131,6 +170,7 @@ function mountViewer(host, options) {
|
|
|
131
170
|
function textItem(node) {
|
|
132
171
|
const style = node.style ?? 'paragraph';
|
|
133
172
|
const element = el(style === 'heading' ? 'h3' : 'p', { class: `fd-text-${style}`, 'data-node': node.id }, node.text);
|
|
173
|
+
spans(element, node.colspan);
|
|
134
174
|
hideWhen(element, node.id);
|
|
135
175
|
return element;
|
|
136
176
|
}
|
|
@@ -142,28 +182,52 @@ function mountViewer(host, options) {
|
|
|
142
182
|
hideWhen(element, node.id);
|
|
143
183
|
return element;
|
|
144
184
|
}
|
|
145
|
-
|
|
185
|
+
/** The columns a part spans in the grid it sits in; the stylesheet keeps it within the columns there at each width. */
|
|
186
|
+
function spans(element, colspan) {
|
|
187
|
+
if (colspan)
|
|
188
|
+
element.style.setProperty('--fd-span', String(colspan));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A grid of parts. With `columns` null, it has none of its own: it lays its
|
|
192
|
+
* parts on the columns of the grid round it (an arrangement on its tracks).
|
|
193
|
+
*/
|
|
194
|
+
function grid(children, columns, place) {
|
|
146
195
|
const box = el('div', { class: 'fd-grid' });
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
if (columns !== null) {
|
|
197
|
+
box.style.setProperty('--fd-columns', String((0, core_1.wideColumns)(columns)));
|
|
198
|
+
// Counts given for the narrower widths replace the skin's own stacking there.
|
|
199
|
+
if (typeof columns === 'object') {
|
|
200
|
+
for (const width of ['medium', 'narrow']) {
|
|
201
|
+
const count = columns[width];
|
|
202
|
+
if (count === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
box.setAttribute(`data-columns-${width}`, String(count));
|
|
205
|
+
box.style.setProperty(`--fd-columns-${width}`, String(count));
|
|
206
|
+
}
|
|
156
207
|
}
|
|
157
208
|
}
|
|
158
|
-
box.append(...children.map(item));
|
|
209
|
+
box.append(...children.map((child) => item(child, place)));
|
|
159
210
|
return box;
|
|
160
211
|
}
|
|
212
|
+
/** Where the parts of a page, a tab or a step sit: one column, the page's labels. */
|
|
213
|
+
const top = (onPage) => ({ columns: 1, onPage, labels: page.look?.labels });
|
|
161
214
|
/** Folded sections, and how to open each: a problem inside one has to be seen. */
|
|
162
215
|
const folds = new Map();
|
|
163
|
-
function sectionItem(node) {
|
|
164
|
-
const
|
|
216
|
+
function sectionItem(node, place) {
|
|
217
|
+
const plan = (0, place_1.planSection)(node, place);
|
|
218
|
+
// An arrangement is no group to name, and a fieldset cannot lay its parts on the columns round it.
|
|
219
|
+
const section = el(plan.arrangement ? 'div' : 'fieldset', {
|
|
220
|
+
class: 'fd-section',
|
|
221
|
+
'data-node': node.id,
|
|
222
|
+
'data-style': plan.style,
|
|
223
|
+
'data-place': plan.at,
|
|
224
|
+
'data-on-page': plan.style === 'card' && place.onPage ? '' : undefined,
|
|
225
|
+
});
|
|
226
|
+
spans(section, node.colspan);
|
|
227
|
+
if (node.labelWidth)
|
|
228
|
+
section.style.setProperty('--fd-label-width', `${node.labelWidth}px`);
|
|
165
229
|
const description = node.description ? el('p', { class: 'fd-section-description' }, node.description) : null;
|
|
166
|
-
const content = grid(node.children, node.columns);
|
|
230
|
+
const content = grid(node.children, plan.at === 'tracks' ? null : node.columns, plan.inner);
|
|
167
231
|
if (node.title && node.collapsible) {
|
|
168
232
|
content.id = uid(`${node.id}-content`);
|
|
169
233
|
const toggle = el('button', { type: 'button', class: 'fd-section-toggle', 'aria-controls': content.id }, el('span', { class: 'fd-section-chevron', 'aria-hidden': 'true' }), ...withIcon(node.icon, node.title));
|
|
@@ -190,8 +254,9 @@ function mountViewer(host, options) {
|
|
|
190
254
|
hideWhen(section, node.id);
|
|
191
255
|
return section;
|
|
192
256
|
}
|
|
193
|
-
function tabsItem(node) {
|
|
257
|
+
function tabsItem(node, place) {
|
|
194
258
|
const box = el('div', { class: 'fd-tabs', 'data-node': node.id });
|
|
259
|
+
spans(box, node.colspan);
|
|
195
260
|
const list = el('div', { class: 'fd-tablist', role: 'tablist' });
|
|
196
261
|
// The tab someone picked. Until they pick, the first visible tab is open —
|
|
197
262
|
// a tab hidden while the record loads must not leave its neighbour open.
|
|
@@ -201,7 +266,8 @@ function mountViewer(host, options) {
|
|
|
201
266
|
const tabId = uid(`${tab.id}-tab`);
|
|
202
267
|
const panelId = uid(`${tab.id}-panel`);
|
|
203
268
|
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, ...withIcon(tab.icon, tab.label));
|
|
204
|
-
|
|
269
|
+
// A tab's parts sit where the tabs do: on the page, or in the box round them.
|
|
270
|
+
const panel = el('div', { class: 'fd-tabpanel', role: 'tabpanel', id: panelId, 'aria-labelledby': tabId }, grid(tab.children, 1, { columns: 1, onPage: place.onPage, labels: place.labels }));
|
|
205
271
|
button.addEventListener('click', () => {
|
|
206
272
|
picked = active = tab.id;
|
|
207
273
|
render(form.getState());
|
|
@@ -241,10 +307,10 @@ function mountViewer(host, options) {
|
|
|
241
307
|
});
|
|
242
308
|
return box;
|
|
243
309
|
}
|
|
244
|
-
function item(node) {
|
|
310
|
+
function item(node, place) {
|
|
245
311
|
switch (node.type) {
|
|
246
312
|
case 'field':
|
|
247
|
-
return fieldItem(node);
|
|
313
|
+
return fieldItem(node, place.labels);
|
|
248
314
|
case 'button':
|
|
249
315
|
return buttonItem(node);
|
|
250
316
|
case 'text':
|
|
@@ -252,11 +318,35 @@ function mountViewer(host, options) {
|
|
|
252
318
|
case 'slot':
|
|
253
319
|
return slotItem(node);
|
|
254
320
|
case 'section':
|
|
255
|
-
return sectionItem(node);
|
|
321
|
+
return sectionItem(node, place);
|
|
256
322
|
case 'tabs':
|
|
257
|
-
return tabsItem(node);
|
|
323
|
+
return tabsItem(node, place);
|
|
324
|
+
case 'divider':
|
|
325
|
+
case 'spacer':
|
|
326
|
+
case 'image':
|
|
327
|
+
return blockItem(node);
|
|
258
328
|
}
|
|
259
329
|
}
|
|
330
|
+
/** A line across the row, empty room, or a picture: shown and hidden by its rule like any part. */
|
|
331
|
+
function blockItem(node) {
|
|
332
|
+
const element = node.type === 'divider' ? el('hr', { class: 'fd-divider', 'data-node': node.id })
|
|
333
|
+
: node.type === 'spacer' ? el('div', { class: 'fd-block fd-spacer', 'aria-hidden': 'true', 'data-node': node.id })
|
|
334
|
+
: figure(node);
|
|
335
|
+
if (node.type !== 'divider' && node.colspan)
|
|
336
|
+
element.style.setProperty('--fd-span', String(node.colspan));
|
|
337
|
+
hideWhen(element, node.id);
|
|
338
|
+
return element;
|
|
339
|
+
}
|
|
340
|
+
/** A picture as wide and where its page says, opening its link in a new tab — a web or mail address only — its caption under it. */
|
|
341
|
+
function figure(node) {
|
|
342
|
+
const image = el('img', { class: 'fd-image', src: node.src, alt: node.alt, loading: 'lazy' });
|
|
343
|
+
const href = node.href && /^(https?:\/\/|mailto:)/i.test(node.href) ? node.href : undefined;
|
|
344
|
+
const element = el('figure', { class: 'fd-block fd-figure', 'data-node': node.id, 'data-align': node.align }, href ? el('a', { href, target: '_blank', rel: 'noopener noreferrer' }, image) : image, ...(node.caption ? [el('figcaption', { class: 'fd-caption' }, node.caption)] : []));
|
|
345
|
+
const width = typeof node.width === 'number' ? `${node.width}px` : node.width && { small: '160px', medium: '320px', large: '480px', full: '100%' }[node.width];
|
|
346
|
+
if (width)
|
|
347
|
+
element.style.setProperty('--fd-image-width', width);
|
|
348
|
+
return element;
|
|
349
|
+
}
|
|
260
350
|
// ---- shared chrome ----------------------------------------------------------
|
|
261
351
|
/** The whole form locked: every field read-only, no Save; buttons still run. */
|
|
262
352
|
let locked = options.readonly === true;
|
|
@@ -316,8 +406,10 @@ function mountViewer(host, options) {
|
|
|
316
406
|
: state.status === 'error' && problem ? (problem.kind === 'fields' ? checkText(state) : problem.kind === 'other' ? problem.message : notDone())
|
|
317
407
|
: state.status === 'error' ? state.error ?? ''
|
|
318
408
|
: '';
|
|
319
|
-
|
|
320
|
-
|
|
409
|
+
// A problem is said once, at once, by the announcer (or the banner, or a dialog): the status shows it and keeps quiet.
|
|
410
|
+
(0, dom_1.setAttr)(statusText, 'aria-live', state.status === 'error' && problem ? 'off' : 'polite');
|
|
411
|
+
(0, dom_1.setText)(statusText, text);
|
|
412
|
+
(0, dom_1.setHidden)(retry, !(state.status === 'error' && problem?.kind === 'other'));
|
|
321
413
|
statusText.classList.toggle('fd-status-error', state.status === 'error');
|
|
322
414
|
statusText.classList.toggle('fd-status-saved', state.status === 'saved');
|
|
323
415
|
// A toast shows while there is something to say, and lets "Saved" go after a moment.
|
|
@@ -362,7 +454,7 @@ function mountViewer(host, options) {
|
|
|
362
454
|
discardDraft.addEventListener('click', () => form.discardDraft());
|
|
363
455
|
draft.append(draftText, restore, discardDraft);
|
|
364
456
|
updaters.push((state) => {
|
|
365
|
-
draft
|
|
457
|
+
(0, dom_1.setHidden)(draft, !state.draft);
|
|
366
458
|
if (state.draft)
|
|
367
459
|
draftText.textContent = fill(labels.draftFound, { time: new Date(state.draft.savedAt).toLocaleString() });
|
|
368
460
|
});
|
|
@@ -414,7 +506,7 @@ function mountViewer(host, options) {
|
|
|
414
506
|
}
|
|
415
507
|
// ---- the four page layouts ---------------------------------------------------
|
|
416
508
|
function sectionsLayout(node) {
|
|
417
|
-
const box = el('div', { class: 'fd-sections', 'data-node': node.id }, ...node.children.map(item));
|
|
509
|
+
const box = el('div', { class: 'fd-sections', 'data-node': node.id }, ...node.children.map((child) => item(child, top(true))));
|
|
418
510
|
const action = el('button', { type: 'submit', class: 'fd-button fd-button-primary' }, page.data.kind === 'responses' ? labels.submit : labels.save);
|
|
419
511
|
const actions = el('div', { class: 'fd-actions fd-actions-end' }, status, action);
|
|
420
512
|
if (page.data.kind === 'record') {
|
|
@@ -424,12 +516,12 @@ function mountViewer(host, options) {
|
|
|
424
516
|
actions.insertBefore(discard, action);
|
|
425
517
|
updaters.push((state) => {
|
|
426
518
|
const dirty = state.dirty.length > 0 && state.status !== 'saving' && !locked;
|
|
427
|
-
action
|
|
428
|
-
discard
|
|
519
|
+
(0, dom_1.setHidden)(action, !dirty);
|
|
520
|
+
(0, dom_1.setHidden)(discard, !dirty);
|
|
429
521
|
});
|
|
430
522
|
}
|
|
431
523
|
else
|
|
432
|
-
updaters.push(() => (action
|
|
524
|
+
updaters.push(() => (0, dom_1.setHidden)(action, locked));
|
|
433
525
|
if (editSwitch)
|
|
434
526
|
actions.prepend(editSwitch);
|
|
435
527
|
if (options.showActions === false)
|
|
@@ -467,7 +559,8 @@ function mountViewer(host, options) {
|
|
|
467
559
|
panel.append(el('h2', { class: 'fd-step-title', id: uid(`${step.id}-title`) }, ...withIcon(step.icon, step.label)));
|
|
468
560
|
if (step.description)
|
|
469
561
|
panel.append(el('p', { class: 'fd-section-description' }, step.description));
|
|
470
|
-
|
|
562
|
+
// The step is the box: a card in it draws none of its own.
|
|
563
|
+
panel.append(grid(step.children, 1, top(false)));
|
|
471
564
|
box.append(panel);
|
|
472
565
|
return { step, panel };
|
|
473
566
|
});
|
|
@@ -610,12 +703,12 @@ function mountViewer(host, options) {
|
|
|
610
703
|
if (node.title) {
|
|
611
704
|
const title = el('div', { class: 'fd-title' });
|
|
612
705
|
if (node.title.above?.length)
|
|
613
|
-
title.append(el('div', { class: 'fd-title-above' }, ...node.title.above.map(fieldItem)));
|
|
706
|
+
title.append(el('div', { class: 'fd-title-above' }, ...node.title.above.map((part) => fieldItem(part))));
|
|
614
707
|
title.append(fieldItem({ type: 'field', id: '#title', field: node.title.field, placeholder: node.title.placeholder }));
|
|
615
708
|
if (node.title.subtitleField)
|
|
616
709
|
title.append(fieldItem({ type: 'field', id: '#subtitle', field: node.title.subtitleField }));
|
|
617
710
|
if (node.title.below?.length)
|
|
618
|
-
title.append(el('div', { class: 'fd-title-below' }, ...node.title.below.map(fieldItem)));
|
|
711
|
+
title.append(el('div', { class: 'fd-title-below' }, ...node.title.below.map((part) => fieldItem(part))));
|
|
619
712
|
const row = el('div', { class: 'fd-title-row' }, title);
|
|
620
713
|
if (node.title.avatarField) {
|
|
621
714
|
const avatar = fieldItem({ type: 'field', id: '#avatar', field: node.title.avatarField });
|
|
@@ -626,7 +719,7 @@ function mountViewer(host, options) {
|
|
|
626
719
|
}
|
|
627
720
|
if (node.statusbar && underTitle)
|
|
628
721
|
card.append(el('div', { class: 'fd-title-statusbar' }, statusbar(node.statusbar)));
|
|
629
|
-
card.append(grid(node.children));
|
|
722
|
+
card.append(grid(node.children, 1, top(false)));
|
|
630
723
|
if (atFoot && options.showActions !== false) {
|
|
631
724
|
card.append(foot);
|
|
632
725
|
updaters.push((state) => {
|
|
@@ -769,8 +862,14 @@ function mountViewer(host, options) {
|
|
|
769
862
|
const ok = el('button', { type: 'button', class: 'fd-button fd-button-primary' }, labels.ok);
|
|
770
863
|
const dialog = el('div', { class: 'fd-dialog', role: 'alertdialog', 'aria-modal': 'true', 'aria-labelledby': text.id }, text, el('div', { class: 'fd-actions fd-actions-end' }, ...(withCancel ? [cancel, ok] : [ok])));
|
|
771
864
|
const backdrop = el('div', { class: 'fd-dialog-backdrop' }, dialog);
|
|
865
|
+
const doc = root.ownerDocument;
|
|
866
|
+
const opener = doc.activeElement;
|
|
772
867
|
const close = (answer) => {
|
|
773
868
|
backdrop.remove();
|
|
869
|
+
// Back to what had focus when it asked; or, when that let go of it — Save hides while it saves — to Save, shown again.
|
|
870
|
+
const shown = (e) => !!e && e !== doc.body && e.isConnected && !e.closest('[hidden]');
|
|
871
|
+
const back = shown(opener) ? opener : root.querySelector('button[type="submit"]:not([hidden])');
|
|
872
|
+
back?.focus();
|
|
774
873
|
resolve(answer);
|
|
775
874
|
};
|
|
776
875
|
cancel.addEventListener('click', () => close(false));
|
|
@@ -778,6 +877,8 @@ function mountViewer(host, options) {
|
|
|
778
877
|
backdrop.addEventListener('keydown', (event) => {
|
|
779
878
|
if (event.key === 'Escape')
|
|
780
879
|
close(false);
|
|
880
|
+
else
|
|
881
|
+
(0, focus_trap_1.keepTabIn)(dialog, event);
|
|
781
882
|
});
|
|
782
883
|
root.append(backdrop);
|
|
783
884
|
ok.focus();
|
|
@@ -827,6 +928,8 @@ function mountViewer(host, options) {
|
|
|
827
928
|
},
|
|
828
929
|
};
|
|
829
930
|
}
|
|
931
|
+
/** The boxes a label can be shown in, as the words of an empty box. */
|
|
932
|
+
const TEXT_BOX = 'input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]), textarea';
|
|
830
933
|
/** Fields whose ✓ would say nothing: a yes/no box is never wrong, a table or a file has its own look. */
|
|
831
934
|
const NO_VALID_MARK = new Set(['boolean', 'one2many', 'binary', 'image', 'html', 'json', 'properties']);
|
|
832
935
|
//# sourceMappingURL=viewer.js.map
|