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