@fieldia/viewer 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/fieldia.js +112 -67
- package/bundle/fieldia.js.map +4 -4
- package/package.json +3 -3
- package/src/lib/dialog.js +10 -0
- package/src/lib/dialog.js.map +1 -1
- package/src/lib/dialog.mjs +10 -0
- package/src/lib/dialog.mjs.map +1 -1
- package/src/lib/labels.d.mts +43 -0
- package/src/lib/labels.d.ts +43 -0
- package/src/lib/labels.js +128 -0
- package/src/lib/labels.js.map +1 -0
- package/src/lib/labels.mjs +125 -0
- package/src/lib/labels.mjs.map +1 -0
- package/src/lib/related.d.mts +7 -0
- package/src/lib/related.d.ts +7 -0
- package/src/lib/related.js +88 -0
- package/src/lib/related.js.map +1 -0
- package/src/lib/related.mjs +84 -0
- package/src/lib/related.mjs.map +1 -0
- package/src/lib/viewer.d.mts +33 -26
- package/src/lib/viewer.d.ts +33 -26
- package/src/lib/viewer.js +325 -200
- package/src/lib/viewer.js.map +1 -1
- package/src/lib/viewer.mjs +323 -200
- package/src/lib/viewer.mjs.map +1 -1
package/src/lib/viewer.mjs
CHANGED
|
@@ -1,90 +1,8 @@
|
|
|
1
|
-
import { createForm,
|
|
2
|
-
import { browserPreferences, createWidget, installStyles, WIDGET_LABELS } from '@fieldia/widgets';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
save: 'Save',
|
|
7
|
-
discard: 'Discard',
|
|
8
|
-
saving: 'Saving…',
|
|
9
|
-
saved: 'Saved',
|
|
10
|
-
submit: 'Submit',
|
|
11
|
-
next: 'Next',
|
|
12
|
-
back: 'Back',
|
|
13
|
-
stepOf: 'Step {n} of {total}',
|
|
14
|
-
submitted: 'Thank you. Your answers were sent.',
|
|
15
|
-
submitAnother: 'Submit another response',
|
|
16
|
-
draftFound: 'You have unsaved answers from {time}.',
|
|
17
|
-
restore: 'Restore',
|
|
18
|
-
discardDraft: 'Discard',
|
|
19
|
-
ok: 'OK',
|
|
20
|
-
cancel: 'Cancel',
|
|
21
|
-
loading: 'Loading…',
|
|
22
|
-
saveClose: 'Save & Close',
|
|
23
|
-
close: 'Close',
|
|
24
|
-
},
|
|
25
|
-
ar: {
|
|
26
|
-
save: 'حفظ',
|
|
27
|
-
discard: 'تجاهل',
|
|
28
|
-
saving: 'جارٍ الحفظ…',
|
|
29
|
-
saved: 'تم الحفظ',
|
|
30
|
-
submit: 'إرسال',
|
|
31
|
-
next: 'التالي',
|
|
32
|
-
back: 'رجوع',
|
|
33
|
-
stepOf: 'الخطوة {n} من {total}',
|
|
34
|
-
submitted: 'شكرًا لك. تم إرسال إجاباتك.',
|
|
35
|
-
submitAnother: 'إرسال إجابة أخرى',
|
|
36
|
-
draftFound: 'لديك إجابات غير محفوظة من {time}.',
|
|
37
|
-
restore: 'استعادة',
|
|
38
|
-
discardDraft: 'تجاهل',
|
|
39
|
-
ok: 'موافق',
|
|
40
|
-
cancel: 'إلغاء',
|
|
41
|
-
loading: 'جارٍ التحميل…',
|
|
42
|
-
saveClose: 'حفظ وإغلاق',
|
|
43
|
-
close: 'إغلاق',
|
|
44
|
-
},
|
|
45
|
-
de: {
|
|
46
|
-
save: 'Speichern',
|
|
47
|
-
discard: 'Verwerfen',
|
|
48
|
-
saving: 'Wird gespeichert…',
|
|
49
|
-
saved: 'Gespeichert',
|
|
50
|
-
submit: 'Absenden',
|
|
51
|
-
next: 'Weiter',
|
|
52
|
-
back: 'Zurück',
|
|
53
|
-
stepOf: 'Schritt {n} von {total}',
|
|
54
|
-
submitted: 'Vielen Dank. Ihre Antworten wurden gesendet.',
|
|
55
|
-
submitAnother: 'Weitere Antwort senden',
|
|
56
|
-
draftFound: 'Sie haben ungespeicherte Antworten vom {time}.',
|
|
57
|
-
restore: 'Wiederherstellen',
|
|
58
|
-
discardDraft: 'Verwerfen',
|
|
59
|
-
ok: 'OK',
|
|
60
|
-
cancel: 'Abbrechen',
|
|
61
|
-
loading: 'Wird geladen…',
|
|
62
|
-
saveClose: 'Speichern und schließen',
|
|
63
|
-
close: 'Schließen',
|
|
64
|
-
},
|
|
65
|
-
fr: {
|
|
66
|
-
save: 'Enregistrer',
|
|
67
|
-
discard: 'Annuler les modifications',
|
|
68
|
-
saving: 'Enregistrement…',
|
|
69
|
-
saved: 'Enregistré',
|
|
70
|
-
submit: 'Envoyer',
|
|
71
|
-
next: 'Suivant',
|
|
72
|
-
back: 'Retour',
|
|
73
|
-
stepOf: 'Étape {n} sur {total}',
|
|
74
|
-
submitted: 'Merci. Vos réponses ont été envoyées.',
|
|
75
|
-
submitAnother: 'Envoyer une autre réponse',
|
|
76
|
-
draftFound: 'Vous avez des réponses non enregistrées du {time}.',
|
|
77
|
-
restore: 'Restaurer',
|
|
78
|
-
discardDraft: 'Ignorer',
|
|
79
|
-
ok: 'OK',
|
|
80
|
-
cancel: 'Annuler',
|
|
81
|
-
loading: 'Chargement…',
|
|
82
|
-
saveClose: 'Enregistrer et fermer',
|
|
83
|
-
close: 'Fermer',
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
/** English, kept under its old name. */
|
|
87
|
-
export const DEFAULT_LABELS = VIEWER_LABELS.en;
|
|
1
|
+
import { createForm, checkPage, translatePage, wideColumns, MESSAGES, } from '@fieldia/core';
|
|
2
|
+
import { browserPreferences, createWidget, drawIcon, installStyles, WIDGET_LABELS } from '@fieldia/widgets';
|
|
3
|
+
import { pageDialogs } from './related.mjs';
|
|
4
|
+
import { VIEWER_LABELS } from './labels.mjs';
|
|
5
|
+
export { VIEWER_LABELS, DEFAULT_LABELS } from './labels.mjs';
|
|
88
6
|
let mounts = 0;
|
|
89
7
|
/**
|
|
90
8
|
* Render a page into `host` and keep it in step with its form. Plain DOM: the
|
|
@@ -93,11 +11,12 @@ let mounts = 0;
|
|
|
93
11
|
*/
|
|
94
12
|
export function mountViewer(host, options) {
|
|
95
13
|
const doc = host.ownerDocument;
|
|
96
|
-
|
|
14
|
+
// The quick check of names and conditions: the full one, with its validation library, belongs where pages are made.
|
|
15
|
+
const checked = checkPage(options.page);
|
|
97
16
|
if (!checked.ok) {
|
|
98
17
|
throw new Error(`This page cannot be shown:\n${checked.issues.map((i) => ` ${i.path}: ${i.message}`).join('\n')}`);
|
|
99
18
|
}
|
|
100
|
-
const page = checked.page;
|
|
19
|
+
const page = options.translate ? translatePage(checked.page, options.translate) : checked.page;
|
|
101
20
|
const locale = options.locale ?? 'en';
|
|
102
21
|
const preferences = options.preferences ?? browserPreferences();
|
|
103
22
|
const dialogs = pageDialogs(options);
|
|
@@ -120,7 +39,12 @@ export function mountViewer(host, options) {
|
|
|
120
39
|
node.append(...children);
|
|
121
40
|
return node;
|
|
122
41
|
}
|
|
123
|
-
|
|
42
|
+
/** Words with the icon a page names before them, when there is such an icon. */
|
|
43
|
+
function withIcon(name, text) {
|
|
44
|
+
const icon = drawIcon(doc, name, options.icons);
|
|
45
|
+
return icon ? [icon, text] : [text];
|
|
46
|
+
}
|
|
47
|
+
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': options.skin ?? 'underline', dir, lang: options.locale, 'data-max-width': page.maxWidth });
|
|
124
48
|
const confirm = options.confirm ?? dialogConfirm;
|
|
125
49
|
// ---- the parts -------------------------------------------------------
|
|
126
50
|
function fieldItem(node) {
|
|
@@ -130,6 +54,12 @@ export function mountViewer(host, options) {
|
|
|
130
54
|
if (node.colspan)
|
|
131
55
|
wrapper.style.setProperty('--fd-span', String(node.colspan));
|
|
132
56
|
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id }, node.label ?? def.label);
|
|
57
|
+
// The ✓ of a field filled in right; never on a yes/no box, a table or a file, where it would say nothing.
|
|
58
|
+
const mark = options.showValid && !NO_VALID_MARK.has(def.type) ? drawIcon(doc, 'check') : null;
|
|
59
|
+
if (mark) {
|
|
60
|
+
mark.classList.add('fd-valid-mark');
|
|
61
|
+
label.append(mark);
|
|
62
|
+
}
|
|
133
63
|
const widget = createWidget({ form, name: node.field, field: def, node, id, document: doc, labels: widgetLabels, preferences, locale, dialogs }, options.widgets);
|
|
134
64
|
if (!['INPUT', 'SELECT', 'TEXTAREA'].includes(widget.element.tagName)) {
|
|
135
65
|
// `for` stays: a custom field that puts the id on its own input is
|
|
@@ -141,7 +71,8 @@ export function mountViewer(host, options) {
|
|
|
141
71
|
}
|
|
142
72
|
const helpText = node.help ?? def.help;
|
|
143
73
|
const help = helpText ? el('div', { class: 'fd-help', id: `${id}-help` }, helpText) : null;
|
|
144
|
-
|
|
74
|
+
// Not an alert of its own: a refused save is announced once, naming every field to look at.
|
|
75
|
+
const error = el('div', { class: 'fd-error', id: `${id}-error`, hidden: '' });
|
|
145
76
|
// A warning from the data source's onchange, beside the field whose change brought it.
|
|
146
77
|
const warning = el('div', { class: 'fd-warning', role: 'status', hidden: '' });
|
|
147
78
|
wrapper.append(label, widget.element, ...(help ? [help] : []), error, warning);
|
|
@@ -157,10 +88,17 @@ export function mountViewer(host, options) {
|
|
|
157
88
|
const warned = state.warning && state.warningField === node.field ? state.warning : '';
|
|
158
89
|
warning.hidden = !warned;
|
|
159
90
|
warning.textContent = warned;
|
|
91
|
+
if (mark) {
|
|
92
|
+
const value = state.values[node.field];
|
|
93
|
+
const filled = value !== null && value !== undefined && value !== '' && !(Array.isArray(value) && !value.length);
|
|
94
|
+
const valid = !shown.readonly && !message && filled && state.dirty.includes(node.field) && form.problem(node.field) === null;
|
|
95
|
+
wrapper.classList.toggle('fd-valid', valid);
|
|
96
|
+
mark.toggleAttribute('hidden', !valid);
|
|
97
|
+
}
|
|
160
98
|
widget.update({
|
|
161
99
|
value: state.values[node.field],
|
|
162
100
|
values: state.values,
|
|
163
|
-
readonly: shown.readonly,
|
|
101
|
+
readonly: shown.readonly || locked,
|
|
164
102
|
required: shown.required,
|
|
165
103
|
invalid: !!message,
|
|
166
104
|
describedBy: [help?.id, message ? error.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
@@ -174,7 +112,7 @@ export function mountViewer(host, options) {
|
|
|
174
112
|
});
|
|
175
113
|
}
|
|
176
114
|
function buttonItem(node) {
|
|
177
|
-
const button = el('button', { type: 'button', class: `fd-button fd-button-${node.style ?? 'secondary'}`, 'data-node': node.id }, node.label);
|
|
115
|
+
const button = el('button', { type: 'button', class: `fd-button fd-button-${node.style ?? 'secondary'}`, 'data-node': node.id }, ...withIcon(node.icon, node.label));
|
|
178
116
|
button.addEventListener('click', async () => {
|
|
179
117
|
if (node.confirm && !(await confirm(node.confirm)))
|
|
180
118
|
return;
|
|
@@ -199,7 +137,17 @@ export function mountViewer(host, options) {
|
|
|
199
137
|
}
|
|
200
138
|
function grid(children, columns = 1) {
|
|
201
139
|
const box = el('div', { class: 'fd-grid' });
|
|
202
|
-
box.style.setProperty('--fd-columns', String(columns));
|
|
140
|
+
box.style.setProperty('--fd-columns', String(wideColumns(columns)));
|
|
141
|
+
// Counts given for the narrower widths replace the skin's own stacking there.
|
|
142
|
+
if (typeof columns === 'object') {
|
|
143
|
+
for (const width of ['medium', 'narrow']) {
|
|
144
|
+
const count = columns[width];
|
|
145
|
+
if (count === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
box.setAttribute(`data-columns-${width}`, String(count));
|
|
148
|
+
box.style.setProperty(`--fd-columns-${width}`, String(count));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
203
151
|
box.append(...children.map(item));
|
|
204
152
|
return box;
|
|
205
153
|
}
|
|
@@ -208,10 +156,10 @@ export function mountViewer(host, options) {
|
|
|
208
156
|
function sectionItem(node) {
|
|
209
157
|
const section = el('fieldset', { class: 'fd-section', 'data-node': node.id });
|
|
210
158
|
const description = node.description ? el('p', { class: 'fd-section-description' }, node.description) : null;
|
|
211
|
-
const content = grid(node.children, node.columns
|
|
159
|
+
const content = grid(node.children, node.columns);
|
|
212
160
|
if (node.title && node.collapsible) {
|
|
213
161
|
content.id = uid(`${node.id}-content`);
|
|
214
|
-
const toggle = el('button', { type: 'button', class: 'fd-section-toggle', 'aria-controls': content.id }, el('span', { class: 'fd-section-chevron', 'aria-hidden': 'true' }), node.title);
|
|
162
|
+
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));
|
|
215
163
|
let open = node.collapsed !== true;
|
|
216
164
|
const show = (next) => {
|
|
217
165
|
open = next;
|
|
@@ -227,7 +175,7 @@ export function mountViewer(host, options) {
|
|
|
227
175
|
section.append(el('legend', { class: 'fd-section-title' }, toggle));
|
|
228
176
|
}
|
|
229
177
|
else if (node.title) {
|
|
230
|
-
section.append(el('legend', { class: 'fd-section-title' }, node.title));
|
|
178
|
+
section.append(el('legend', { class: 'fd-section-title' }, ...withIcon(node.icon, node.title)));
|
|
231
179
|
}
|
|
232
180
|
if (description)
|
|
233
181
|
section.append(description);
|
|
@@ -245,7 +193,7 @@ export function mountViewer(host, options) {
|
|
|
245
193
|
const parts = node.children.map((tab) => {
|
|
246
194
|
const tabId = uid(`${tab.id}-tab`);
|
|
247
195
|
const panelId = uid(`${tab.id}-panel`);
|
|
248
|
-
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, tab.label);
|
|
196
|
+
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));
|
|
249
197
|
const panel = el('div', { class: 'fd-tabpanel', role: 'tabpanel', id: panelId, 'aria-labelledby': tabId }, grid(tab.children));
|
|
250
198
|
button.addEventListener('click', () => {
|
|
251
199
|
picked = active = tab.id;
|
|
@@ -303,12 +251,101 @@ export function mountViewer(host, options) {
|
|
|
303
251
|
}
|
|
304
252
|
}
|
|
305
253
|
// ---- shared chrome ----------------------------------------------------------
|
|
306
|
-
|
|
254
|
+
/** The whole form locked: every field read-only, no Save; buttons still run. */
|
|
255
|
+
let locked = options.readonly === true;
|
|
256
|
+
// Edit unlocks; Done saves what changed first, and locks only once that is saved.
|
|
257
|
+
const editSwitch = options.editSwitch ? el('button', { type: 'button', class: 'fd-button fd-edit-switch' }) : null;
|
|
258
|
+
editSwitch?.addEventListener('click', async () => {
|
|
259
|
+
if (!locked) {
|
|
260
|
+
if (form.getState().dirty.length) {
|
|
261
|
+
const saved = await form.save();
|
|
262
|
+
if (!saved) {
|
|
263
|
+
if (!form.getState().saveProblem)
|
|
264
|
+
announce(checkText(form.getState()));
|
|
265
|
+
focusFirstProblem();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
locked = true;
|
|
270
|
+
}
|
|
271
|
+
else
|
|
272
|
+
locked = false;
|
|
273
|
+
root.toggleAttribute('data-readonly', locked);
|
|
274
|
+
render(form.getState());
|
|
275
|
+
});
|
|
276
|
+
root.toggleAttribute('data-readonly', locked);
|
|
277
|
+
updaters.push(() => {
|
|
278
|
+
if (!editSwitch)
|
|
279
|
+
return;
|
|
280
|
+
editSwitch.textContent = locked ? labels.edit : labels.done;
|
|
281
|
+
editSwitch.classList.toggle('fd-button-primary', locked);
|
|
282
|
+
});
|
|
283
|
+
// The status says its words alone, so a screen reader hears them; Retry sits beside it.
|
|
284
|
+
const statusText = el('div', { class: 'fd-status', role: 'status', 'aria-live': 'polite' });
|
|
285
|
+
const retry = el('button', { type: 'button', class: 'fd-button fd-button-link fd-retry', hidden: '' }, labels.retry);
|
|
286
|
+
retry.addEventListener('click', () => void submitOrSave());
|
|
287
|
+
const status = el('div', { class: 'fd-status-box' }, statusText, retry);
|
|
288
|
+
const notDone = () => (page.data.kind === 'responses' ? labels.notSent : labels.notSaved);
|
|
289
|
+
/** "Not saved. Check: Email, Phone": the fields with a problem, in the order they show. */
|
|
290
|
+
function checkText(state) {
|
|
291
|
+
const names = [];
|
|
292
|
+
for (const key of Object.keys(state.errors)) {
|
|
293
|
+
const field = key.split('.')[0];
|
|
294
|
+
const shown = root.querySelector(`.fd-field[data-field="${field}"]:not([hidden]) > .fd-label`);
|
|
295
|
+
const name = shown?.firstChild?.textContent ?? page.fields[field]?.label ?? field;
|
|
296
|
+
if (!names.includes(name))
|
|
297
|
+
names.push(name);
|
|
298
|
+
}
|
|
299
|
+
const inOrder = [...root.querySelectorAll('.fd-field[data-field] > .fd-label')].map((label) => label.firstChild?.textContent ?? '');
|
|
300
|
+
names.sort((a, b) => (inOrder.indexOf(a) + 1 || Infinity) - (inOrder.indexOf(b) + 1 || Infinity));
|
|
301
|
+
return names.length ? fill(labels.checkFields, { what: notDone(), fields: names.join(', ') }) : notDone();
|
|
302
|
+
}
|
|
303
|
+
let quietTimer;
|
|
304
|
+
updaters.push((state) => {
|
|
305
|
+
const problem = state.saveProblem;
|
|
306
|
+
const text = state.status === 'saving' ? labels.saving
|
|
307
|
+
: state.status === 'saved' ? labels.saved
|
|
308
|
+
: state.status === 'loading' ? labels.loading
|
|
309
|
+
: state.status === 'error' && problem ? (problem.kind === 'fields' ? checkText(state) : problem.kind === 'other' ? problem.message : notDone())
|
|
310
|
+
: state.status === 'error' ? state.error ?? ''
|
|
311
|
+
: '';
|
|
312
|
+
statusText.textContent = text;
|
|
313
|
+
retry.hidden = !(state.status === 'error' && problem?.kind === 'other');
|
|
314
|
+
statusText.classList.toggle('fd-status-error', state.status === 'error');
|
|
315
|
+
statusText.classList.toggle('fd-status-saved', state.status === 'saved');
|
|
316
|
+
// A toast shows while there is something to say, and lets "Saved" go after a moment.
|
|
317
|
+
if (options.saveStatus === 'toast') {
|
|
318
|
+
clearTimeout(quietTimer);
|
|
319
|
+
status.hidden = !text;
|
|
320
|
+
if (state.status === 'saved')
|
|
321
|
+
quietTimer = setTimeout(() => (status.hidden = true), 2500);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
// A save the network failed: a banner over the page, with Retry.
|
|
325
|
+
const bannerRetry = el('button', { type: 'button', class: 'fd-button fd-retry' }, labels.retry);
|
|
326
|
+
bannerRetry.addEventListener('click', () => void submitOrSave());
|
|
327
|
+
const banner = el('div', { class: 'fd-banner fd-tone-warning', role: 'alert', hidden: '' }, el('span', { class: 'fd-banner-text' }, labels.offline), bannerRetry);
|
|
328
|
+
/** Said at once to a screen reader: why a save or send did not go through. */
|
|
329
|
+
const announcer = el('div', { class: 'fd-announce', role: 'alert', 'aria-live': 'assertive' });
|
|
330
|
+
function announce(text) {
|
|
331
|
+
announcer.textContent = '';
|
|
332
|
+
setTimeout(() => (announcer.textContent = text), 0);
|
|
333
|
+
}
|
|
334
|
+
// Each new reason a save was refused, from Save or from autosave, is shown where it belongs.
|
|
335
|
+
let seenProblem = null;
|
|
307
336
|
updaters.push((state) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
337
|
+
if (state.saveProblem === seenProblem)
|
|
338
|
+
return;
|
|
339
|
+
seenProblem = state.saveProblem;
|
|
340
|
+
banner.hidden = seenProblem?.kind !== 'network';
|
|
341
|
+
if (!seenProblem)
|
|
342
|
+
return;
|
|
343
|
+
if (seenProblem.kind === 'rule')
|
|
344
|
+
void dialogConfirm(seenProblem.message, false);
|
|
345
|
+
else if (seenProblem.kind === 'fields')
|
|
346
|
+
announce(checkText(state));
|
|
347
|
+
else if (seenProblem.kind === 'other')
|
|
348
|
+
announce(seenProblem.message);
|
|
312
349
|
});
|
|
313
350
|
const draft = el('div', { class: 'fd-draft', hidden: '' });
|
|
314
351
|
const draftText = el('span');
|
|
@@ -342,6 +379,9 @@ export function mountViewer(host, options) {
|
|
|
342
379
|
async function submitOrSave() {
|
|
343
380
|
const saved = await form.save();
|
|
344
381
|
if (!saved) {
|
|
382
|
+
// Stopped by the page's own checks: say which fields; a refused save says so above.
|
|
383
|
+
if (!form.getState().saveProblem)
|
|
384
|
+
announce(checkText(form.getState()));
|
|
345
385
|
focusFirstProblem();
|
|
346
386
|
return;
|
|
347
387
|
}
|
|
@@ -376,34 +416,66 @@ export function mountViewer(host, options) {
|
|
|
376
416
|
discard.addEventListener('click', () => form.reset());
|
|
377
417
|
actions.insertBefore(discard, action);
|
|
378
418
|
updaters.push((state) => {
|
|
379
|
-
const dirty = state.dirty.length > 0 && state.status !== 'saving';
|
|
419
|
+
const dirty = state.dirty.length > 0 && state.status !== 'saving' && !locked;
|
|
380
420
|
action.hidden = !dirty;
|
|
381
421
|
discard.hidden = !dirty;
|
|
382
422
|
});
|
|
383
423
|
}
|
|
384
|
-
|
|
424
|
+
else
|
|
425
|
+
updaters.push(() => (action.hidden = locked));
|
|
426
|
+
if (editSwitch)
|
|
427
|
+
actions.prepend(editSwitch);
|
|
428
|
+
if (options.showActions === false)
|
|
429
|
+
return el('div', {}, box);
|
|
430
|
+
if (page.actionsPosition === 'top') {
|
|
431
|
+
actions.classList.add('fd-actions-top');
|
|
432
|
+
return el('div', {}, actions, box);
|
|
433
|
+
}
|
|
434
|
+
return el('div', {}, box, actions);
|
|
385
435
|
}
|
|
386
436
|
function wizardLayout(node) {
|
|
387
437
|
const box = el('div', { class: 'fd-wizard', 'data-node': node.id });
|
|
388
438
|
const progressText = el('div', { class: 'fd-progress-text' });
|
|
389
439
|
const bar = el('span');
|
|
390
440
|
box.append(el('div', { class: 'fd-progress' }, progressText, el('div', { class: 'fd-progress-bar' }, bar)));
|
|
441
|
+
// A clickable wizard lists its steps: back to any, forward past those that are complete.
|
|
442
|
+
const list = node.clickable ? el('ol', { class: 'fd-steps-list' }) : null;
|
|
443
|
+
const stepButtons = new Map();
|
|
444
|
+
if (list) {
|
|
445
|
+
for (const step of node.children) {
|
|
446
|
+
const go = el('button', { type: 'button', class: 'fd-step-link' }, ...withIcon(step.icon, step.label));
|
|
447
|
+
go.addEventListener('click', () => {
|
|
448
|
+
if (form.goTo(step.id))
|
|
449
|
+
focusStep();
|
|
450
|
+
else
|
|
451
|
+
focusFirstProblem();
|
|
452
|
+
});
|
|
453
|
+
stepButtons.set(step.id, go);
|
|
454
|
+
list.append(el('li', {}, go));
|
|
455
|
+
}
|
|
456
|
+
box.append(el('nav', { class: 'fd-steps', 'aria-label': labels.steps }, list));
|
|
457
|
+
}
|
|
391
458
|
const panels = node.children.map((step) => {
|
|
392
459
|
const panel = el('section', { class: 'fd-step', 'data-node': step.id, 'aria-labelledby': uid(`${step.id}-title`) });
|
|
393
|
-
panel.append(el('h2', { class: 'fd-step-title', id: uid(`${step.id}-title`) }, step.label));
|
|
460
|
+
panel.append(el('h2', { class: 'fd-step-title', id: uid(`${step.id}-title`) }, ...withIcon(step.icon, step.label)));
|
|
394
461
|
if (step.description)
|
|
395
462
|
panel.append(el('p', { class: 'fd-section-description' }, step.description));
|
|
396
463
|
panel.append(grid(step.children));
|
|
397
464
|
box.append(panel);
|
|
398
465
|
return { step, panel };
|
|
399
466
|
});
|
|
400
|
-
const back = el('button', { type: 'button', class: 'fd-button' }, labels.back);
|
|
467
|
+
const back = el('button', { type: 'button', class: 'fd-button' }, node.backLabel ?? labels.back);
|
|
468
|
+
const skip = el('button', { type: 'button', class: 'fd-button fd-button-link' }, labels.skip);
|
|
401
469
|
const forward = el('button', { type: 'submit', class: 'fd-button fd-button-primary' });
|
|
402
|
-
box.append(el('div', { class: 'fd-wizard-nav' }, back, el('span', { class: 'fd-spacer' }), status, forward));
|
|
470
|
+
box.append(el('div', { class: 'fd-wizard-nav' }, back, el('span', { class: 'fd-spacer' }), status, skip, forward));
|
|
403
471
|
back.addEventListener('click', () => {
|
|
404
472
|
if (form.back())
|
|
405
473
|
focusStep();
|
|
406
474
|
});
|
|
475
|
+
skip.addEventListener('click', () => {
|
|
476
|
+
if (form.skip())
|
|
477
|
+
focusStep();
|
|
478
|
+
});
|
|
407
479
|
const isLast = () => {
|
|
408
480
|
const steps = form.steps();
|
|
409
481
|
return steps.indexOf(form.getState().step ?? '') === steps.length - 1;
|
|
@@ -427,8 +499,20 @@ export function mountViewer(host, options) {
|
|
|
427
499
|
bar.style.width = `${((index + 1) / Math.max(1, steps.length)) * 100}%`;
|
|
428
500
|
for (const { step, panel } of panels)
|
|
429
501
|
panel.hidden = step.id !== state.step;
|
|
502
|
+
const last = index === steps.length - 1;
|
|
430
503
|
back.hidden = index === 0;
|
|
431
|
-
|
|
504
|
+
skip.hidden = last || !node.children.find((step) => step.id === state.step)?.optional;
|
|
505
|
+
forward.textContent = last ? node.finishLabel ?? labels.submit : node.nextLabel ?? labels.next;
|
|
506
|
+
for (const [id, go] of stepButtons) {
|
|
507
|
+
const at = steps.indexOf(id);
|
|
508
|
+
go.parentElement.hidden = at === -1;
|
|
509
|
+
go.classList.toggle('fd-step-done', at !== -1 && at < index);
|
|
510
|
+
go.classList.toggle('fd-step-skipped', state.skipped.includes(id));
|
|
511
|
+
if (id === state.step)
|
|
512
|
+
go.setAttribute('aria-current', 'step');
|
|
513
|
+
else
|
|
514
|
+
go.removeAttribute('aria-current');
|
|
515
|
+
}
|
|
432
516
|
});
|
|
433
517
|
return box;
|
|
434
518
|
}
|
|
@@ -438,17 +522,28 @@ export function mountViewer(host, options) {
|
|
|
438
522
|
const save = el('button', { type: 'submit', class: 'fd-button fd-button-primary' }, labels.save);
|
|
439
523
|
const discard = el('button', { type: 'button', class: 'fd-button' }, labels.discard);
|
|
440
524
|
discard.addEventListener('click', () => form.reset());
|
|
525
|
+
// Save and Discard lead the header bar, or close the sheet when the page puts them at its foot.
|
|
526
|
+
const atFoot = page.actionsPosition === 'bottom';
|
|
527
|
+
const foot = el('div', { class: 'fd-actions fd-actions-end fd-sheet-foot' });
|
|
441
528
|
if (options.showActions === false)
|
|
442
529
|
actions.append(...(node.buttons ?? []).map(buttonItem));
|
|
530
|
+
else if (atFoot) {
|
|
531
|
+
actions.append(...(node.buttons ?? []).map(buttonItem));
|
|
532
|
+
foot.append(status, discard, save);
|
|
533
|
+
}
|
|
443
534
|
else
|
|
444
535
|
actions.append(save, discard, ...(node.buttons ?? []).map(buttonItem), status);
|
|
536
|
+
// The Edit switch leads the header bar, before Save and the record's own buttons.
|
|
537
|
+
if (editSwitch && options.showActions !== false)
|
|
538
|
+
actions.prepend(editSwitch);
|
|
445
539
|
updaters.push((state) => {
|
|
446
|
-
const dirty = state.dirty.length > 0 && state.status !== 'saving';
|
|
540
|
+
const dirty = state.dirty.length > 0 && state.status !== 'saving' && !locked;
|
|
447
541
|
save.hidden = !dirty;
|
|
448
542
|
discard.hidden = !dirty;
|
|
449
543
|
});
|
|
450
544
|
const header = el('div', { class: 'fd-header' }, actions);
|
|
451
|
-
|
|
545
|
+
const underTitle = node.statusbar?.position === 'title';
|
|
546
|
+
if (node.statusbar && !underTitle)
|
|
452
547
|
header.append(statusbar(node.statusbar));
|
|
453
548
|
const card = el('div', { class: 'fd-card' });
|
|
454
549
|
if (node.ribbon) {
|
|
@@ -461,7 +556,9 @@ export function mountViewer(host, options) {
|
|
|
461
556
|
const stats = el('div', { class: 'fd-stats' });
|
|
462
557
|
for (const stat of node.statButtons) {
|
|
463
558
|
const value = el('span', { class: 'fd-stat-value' });
|
|
464
|
-
const
|
|
559
|
+
const icon = drawIcon(doc, stat.icon, options.icons);
|
|
560
|
+
const words = el('span', { class: 'fd-stat-words' }, value, el('span', { class: 'fd-stat-label' }, stat.label));
|
|
561
|
+
const button = el('button', { type: 'button', class: 'fd-stat', 'data-node': stat.id }, ...(icon ? [icon, words] : [words]));
|
|
465
562
|
button.addEventListener('click', () => void form.runAction(stat.id));
|
|
466
563
|
updaters.push((state) => {
|
|
467
564
|
button.hidden = form.node(stat.id).invisible;
|
|
@@ -473,15 +570,45 @@ export function mountViewer(host, options) {
|
|
|
473
570
|
card.append(stats);
|
|
474
571
|
}
|
|
475
572
|
for (const alert of node.alerts ?? []) {
|
|
476
|
-
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, alert.message);
|
|
477
|
-
|
|
573
|
+
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, el('span', { class: 'fd-alert-message' }, alert.message));
|
|
574
|
+
if (alert.dismissible) {
|
|
575
|
+
// Closed, it stays closed while the page is open, whatever its condition does.
|
|
576
|
+
let dismissed = false;
|
|
577
|
+
const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': labels.dismiss }, '×');
|
|
578
|
+
close.addEventListener('click', () => {
|
|
579
|
+
dismissed = true;
|
|
580
|
+
box.hidden = true;
|
|
581
|
+
});
|
|
582
|
+
box.append(close);
|
|
583
|
+
updaters.push(() => {
|
|
584
|
+
box.hidden = dismissed || form.node(alert.id).invisible;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
else
|
|
588
|
+
hideWhen(box, alert.id);
|
|
478
589
|
card.append(box);
|
|
479
590
|
}
|
|
591
|
+
if (node.badges?.length) {
|
|
592
|
+
const badges = el('div', { class: 'fd-badges' });
|
|
593
|
+
for (const badge of node.badges) {
|
|
594
|
+
const chip = el('span', { class: `fd-badge fd-tone-${badge.tone ?? 'muted'}`, 'data-node': badge.id }, ...withIcon(badge.icon, badge.label));
|
|
595
|
+
hideWhen(chip, badge.id);
|
|
596
|
+
badges.append(chip);
|
|
597
|
+
}
|
|
598
|
+
updaters.push(() => {
|
|
599
|
+
badges.hidden = [...badges.children].every((chip) => chip.hidden);
|
|
600
|
+
});
|
|
601
|
+
card.append(badges);
|
|
602
|
+
}
|
|
480
603
|
if (node.title) {
|
|
481
604
|
const title = el('div', { class: 'fd-title' });
|
|
605
|
+
if (node.title.above?.length)
|
|
606
|
+
title.append(el('div', { class: 'fd-title-above' }, ...node.title.above.map(fieldItem)));
|
|
482
607
|
title.append(fieldItem({ type: 'field', id: '#title', field: node.title.field, placeholder: node.title.placeholder }));
|
|
483
608
|
if (node.title.subtitleField)
|
|
484
609
|
title.append(fieldItem({ type: 'field', id: '#subtitle', field: node.title.subtitleField }));
|
|
610
|
+
if (node.title.below?.length)
|
|
611
|
+
title.append(el('div', { class: 'fd-title-below' }, ...node.title.below.map(fieldItem)));
|
|
485
612
|
const row = el('div', { class: 'fd-title-row' }, title);
|
|
486
613
|
if (node.title.avatarField) {
|
|
487
614
|
const avatar = fieldItem({ type: 'field', id: '#avatar', field: node.title.avatarField });
|
|
@@ -490,7 +617,15 @@ export function mountViewer(host, options) {
|
|
|
490
617
|
}
|
|
491
618
|
card.append(row);
|
|
492
619
|
}
|
|
620
|
+
if (node.statusbar && underTitle)
|
|
621
|
+
card.append(el('div', { class: 'fd-title-statusbar' }, statusbar(node.statusbar)));
|
|
493
622
|
card.append(grid(node.children));
|
|
623
|
+
if (atFoot && options.showActions !== false) {
|
|
624
|
+
card.append(foot);
|
|
625
|
+
updaters.push((state) => {
|
|
626
|
+
foot.hidden = state.dirty.length === 0 && !statusText.textContent;
|
|
627
|
+
});
|
|
628
|
+
}
|
|
494
629
|
const layout = el('div', { class: 'fd-sheet-layout' }, card);
|
|
495
630
|
if (node.sidePanel) {
|
|
496
631
|
layout.classList.add('fd-has-side');
|
|
@@ -507,7 +642,7 @@ export function mountViewer(host, options) {
|
|
|
507
642
|
barOptions['visibleStates'] = config.visibleStates;
|
|
508
643
|
const node = { type: 'field', id: '#statusbar', field: config.field, widget: 'statusbar', options: barOptions };
|
|
509
644
|
const widget = createWidget({ form, name: config.field, field: def, node, id: uid('statusbar'), document: doc, labels: widgetLabels, preferences, locale, dialogs }, options.widgets);
|
|
510
|
-
updaters.push((state) => widget.update({ value: state.values[config.field], values: state.values, readonly: form.node('#statusbar').readonly, required: false, invalid: false }));
|
|
645
|
+
updaters.push((state) => widget.update({ value: state.values[config.field], values: state.values, readonly: form.node('#statusbar').readonly || locked, required: false, invalid: false }));
|
|
511
646
|
return widget.element;
|
|
512
647
|
}
|
|
513
648
|
let wizardForward = null;
|
|
@@ -522,8 +657,17 @@ export function mountViewer(host, options) {
|
|
|
522
657
|
else
|
|
523
658
|
body = sectionsLayout(layout);
|
|
524
659
|
const head = layout.type === 'sheet' ? null : pageHead();
|
|
525
|
-
content.append(...(head ? [head] : []), draft, body);
|
|
526
|
-
root.append(content, done);
|
|
660
|
+
content.append(...(head ? [head] : []), banner, draft, body);
|
|
661
|
+
root.append(content, done, announcer);
|
|
662
|
+
// The status leaves its place beside Save when the page wants it in a corner or across the top.
|
|
663
|
+
if (options.saveStatus === 'toast') {
|
|
664
|
+
status.classList.add('fd-toast');
|
|
665
|
+
root.append(status);
|
|
666
|
+
}
|
|
667
|
+
else if (options.saveStatus === 'bar') {
|
|
668
|
+
status.classList.add('fd-status-bar');
|
|
669
|
+
content.prepend(status);
|
|
670
|
+
}
|
|
527
671
|
root.addEventListener('submit', (event) => {
|
|
528
672
|
event.preventDefault();
|
|
529
673
|
if (wizardForward)
|
|
@@ -531,13 +675,65 @@ export function mountViewer(host, options) {
|
|
|
531
675
|
else
|
|
532
676
|
void submitOrSave();
|
|
533
677
|
});
|
|
678
|
+
// ---- keys -------------------------------------------------------------------
|
|
679
|
+
/** Text typed and not yet taken, such as a tag without its Enter, is taken now. */
|
|
680
|
+
function commitTyping() {
|
|
681
|
+
const active = doc.activeElement;
|
|
682
|
+
if (active && root.contains(active) && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) {
|
|
683
|
+
active.dispatchEvent(new Event('change', { bubbles: true }));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** The fields Enter can move between: on this page, shown, with a box to type or choose in, outside tables of lines. */
|
|
687
|
+
function nextField(from) {
|
|
688
|
+
const fields = [...root.querySelectorAll('.fd-field')].filter((field) => !field.closest('[hidden], [data-type="one2many"] .fd-field, .fd-form-dialog') && !field.parentElement?.closest('[data-type="one2many"]'));
|
|
689
|
+
const here = from.closest('.fd-field');
|
|
690
|
+
const after = fields.slice(fields.indexOf(here) + 1);
|
|
691
|
+
for (const field of after) {
|
|
692
|
+
const control = field.querySelector('input:not([type="hidden"]):not([readonly]):not([disabled]), select:not([disabled]), textarea:not([readonly]), [contenteditable="true"]');
|
|
693
|
+
if (control)
|
|
694
|
+
return control;
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
root.addEventListener('keydown', (event) => {
|
|
699
|
+
if (event.isComposing || event.key !== 'Enter')
|
|
700
|
+
return;
|
|
701
|
+
const keys = options.keys ?? {};
|
|
702
|
+
if (event.ctrlKey || event.metaKey) {
|
|
703
|
+
// Saves even after a field used the Enter (a tag box adding its tag); the grid keeps its own keys.
|
|
704
|
+
if (keys.saveWithCtrlEnter === false || options.showActions === false)
|
|
705
|
+
return;
|
|
706
|
+
if (event.target.closest('.ag-root-wrapper'))
|
|
707
|
+
return;
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
commitTyping();
|
|
710
|
+
if (wizardForward)
|
|
711
|
+
void wizardForward();
|
|
712
|
+
else
|
|
713
|
+
void submitOrSave();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// A plain Enter a field used itself, such as picking from an open list, is the field's.
|
|
717
|
+
if (event.defaultPrevented || !keys.enterMovesToNext || event.shiftKey || event.altKey)
|
|
718
|
+
return;
|
|
719
|
+
const from = event.target;
|
|
720
|
+
if (from.tagName !== 'INPUT' && from.tagName !== 'SELECT')
|
|
721
|
+
return;
|
|
722
|
+
if (from.closest('[data-type="one2many"]'))
|
|
723
|
+
return;
|
|
724
|
+
if (['button', 'submit', 'reset', 'file'].includes(from.type))
|
|
725
|
+
return;
|
|
726
|
+
event.preventDefault();
|
|
727
|
+
nextField(from)?.focus();
|
|
728
|
+
});
|
|
534
729
|
// ---- confirmation dialog -----------------------------------------------------
|
|
535
|
-
|
|
730
|
+
/** A question with Cancel and OK, or with `withCancel` false a notice with OK alone. */
|
|
731
|
+
function dialogConfirm(message, withCancel = true) {
|
|
536
732
|
return new Promise((resolve) => {
|
|
537
733
|
const text = el('p', { id: `${prefix}-confirm` }, message);
|
|
538
734
|
const cancel = el('button', { type: 'button', class: 'fd-button' }, labels.cancel);
|
|
539
735
|
const ok = el('button', { type: 'button', class: 'fd-button fd-button-primary' }, labels.ok);
|
|
540
|
-
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' }, cancel, ok));
|
|
736
|
+
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])));
|
|
541
737
|
const backdrop = el('div', { class: 'fd-dialog-backdrop' }, dialog);
|
|
542
738
|
const close = (answer) => {
|
|
543
739
|
backdrop.remove();
|
|
@@ -581,6 +777,12 @@ export function mountViewer(host, options) {
|
|
|
581
777
|
focusFirstProblem();
|
|
582
778
|
return valid;
|
|
583
779
|
},
|
|
780
|
+
setReadonly(readonly) {
|
|
781
|
+
locked = readonly;
|
|
782
|
+
root.toggleAttribute('data-readonly', locked);
|
|
783
|
+
render(form.getState());
|
|
784
|
+
},
|
|
785
|
+
isReadonly: () => locked,
|
|
584
786
|
destroy() {
|
|
585
787
|
unsubscribe();
|
|
586
788
|
for (const cleanup of cleanups)
|
|
@@ -591,85 +793,6 @@ export function mountViewer(host, options) {
|
|
|
591
793
|
},
|
|
592
794
|
};
|
|
593
795
|
}
|
|
594
|
-
/**
|
|
595
|
-
|
|
596
|
-
if (page.layout.type === 'sheet' && page.layout.title?.field)
|
|
597
|
-
return page.layout.title.field;
|
|
598
|
-
if (page.fields['name'])
|
|
599
|
-
return 'name';
|
|
600
|
-
return Object.entries(page.fields).find(([, def]) => def.type === 'char')?.[0] ?? null;
|
|
601
|
-
}
|
|
602
|
-
/** A form made of fields, for values edited in a dialog (a line, for one). */
|
|
603
|
-
function valuesPage(fields, title, readonly) {
|
|
604
|
-
const shown = Object.fromEntries(Object.entries(fields).map(([name, def]) => [name, readonly ? { ...def, readonly: true } : def]));
|
|
605
|
-
return {
|
|
606
|
-
fieldia: '0.1',
|
|
607
|
-
id: 'values',
|
|
608
|
-
title,
|
|
609
|
-
data: { kind: 'record', model: 'values' },
|
|
610
|
-
fields: shown,
|
|
611
|
-
layout: {
|
|
612
|
-
type: 'sections',
|
|
613
|
-
id: 'values',
|
|
614
|
-
children: [{ type: 'section', id: 'values-section', columns: 2, children: Object.keys(shown).map((name) => ({ type: 'field', id: `values-${name}`, field: name })) }],
|
|
615
|
-
},
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
/** Only finding and making linked records: values edited in a dialog are no record of the source's to load, save or recalculate. */
|
|
619
|
-
function lookupsOf(source) {
|
|
620
|
-
if (!source)
|
|
621
|
-
return undefined;
|
|
622
|
-
return {
|
|
623
|
-
...(source.search ? { search: (request) => source.search(request) } : {}),
|
|
624
|
-
...(source.create ? { create: (request) => source.create(request) } : {}),
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
/** The dialogs a page's widgets may open, made from the viewer's own options. */
|
|
628
|
-
function pageDialogs(options) {
|
|
629
|
-
const pageFor = (model) => (typeof options.relatedPages === 'function' ? options.relatedPages(model) : options.relatedPages?.[model]) ?? null;
|
|
630
|
-
// A dialog's page looks and reads like the page that opened it, and can open dialogs of its own.
|
|
631
|
-
const shared = {
|
|
632
|
-
locale: options.locale,
|
|
633
|
-
skin: options.skin,
|
|
634
|
-
dir: options.dir,
|
|
635
|
-
widgets: options.widgets,
|
|
636
|
-
preferences: options.preferences,
|
|
637
|
-
relatedPages: options.relatedPages,
|
|
638
|
-
};
|
|
639
|
-
return {
|
|
640
|
-
canOpen: (model) => pageFor(model) !== null,
|
|
641
|
-
async openRecord(model, request) {
|
|
642
|
-
const related = pageFor(model);
|
|
643
|
-
if (!related)
|
|
644
|
-
return null;
|
|
645
|
-
const nameField = nameFieldOf(related);
|
|
646
|
-
const result = await openFormDialog({
|
|
647
|
-
...shared,
|
|
648
|
-
page: related,
|
|
649
|
-
dataSource: options.dataSource,
|
|
650
|
-
recordId: request.recordId ?? null,
|
|
651
|
-
values: request.name && nameField ? { [nameField]: request.name } : undefined,
|
|
652
|
-
title: request.title,
|
|
653
|
-
size: 'large',
|
|
654
|
-
});
|
|
655
|
-
if (!result.saved || result.recordId === null)
|
|
656
|
-
return null;
|
|
657
|
-
const name = nameField ? result.values[nameField] : null;
|
|
658
|
-
return { id: result.recordId, label: typeof name === 'string' && name ? name : request.name ?? request.title };
|
|
659
|
-
},
|
|
660
|
-
searchMore: (request) => openSearchDialog({ title: request.title, search: request.search, locale: options.locale, skin: options.skin, dir: options.dir }),
|
|
661
|
-
async editValues(request) {
|
|
662
|
-
const result = await openFormDialog({
|
|
663
|
-
...shared,
|
|
664
|
-
page: valuesPage(request.fields, request.title, request.readonly === true),
|
|
665
|
-
dataSource: lookupsOf(options.dataSource),
|
|
666
|
-
values: request.values,
|
|
667
|
-
title: request.title,
|
|
668
|
-
mode: 'values',
|
|
669
|
-
...(request.recompute ? { recompute: request.recompute } : {}),
|
|
670
|
-
});
|
|
671
|
-
return result.saved ? result.values : null;
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
}
|
|
796
|
+
/** Fields whose ✓ would say nothing: a yes/no box is never wrong, a table or a file has its own look. */
|
|
797
|
+
const NO_VALID_MARK = new Set(['boolean', 'one2many', 'binary', 'image', 'html', 'json', 'properties']);
|
|
675
798
|
//# sourceMappingURL=viewer.mjs.map
|