@fieldia/viewer 0.18.0 → 0.19.1
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 +1 -1
- package/bundle/fieldia.ar.js +2 -2
- package/bundle/fieldia.ar.js.map +2 -2
- package/bundle/fieldia.de.js +2 -2
- package/bundle/fieldia.de.js.map +2 -2
- package/bundle/fieldia.fr.js +2 -2
- package/bundle/fieldia.fr.js.map +2 -2
- package/bundle/fieldia.js +16 -16
- package/bundle/fieldia.js.map +4 -4
- package/package.json +3 -3
- package/src/lib/dialog.js +2 -2
- package/src/lib/dialog.js.map +1 -1
- package/src/lib/dialog.mjs +2 -2
- package/src/lib/dialog.mjs.map +1 -1
- package/src/lib/labels.d.mts +8 -0
- package/src/lib/labels.d.ts +8 -0
- package/src/lib/labels.js +3 -0
- package/src/lib/labels.js.map +1 -1
- package/src/lib/labels.mjs +3 -0
- package/src/lib/labels.mjs.map +1 -1
- package/src/lib/list.js +2 -5
- package/src/lib/list.js.map +1 -1
- package/src/lib/list.mjs +3 -6
- package/src/lib/list.mjs.map +1 -1
- package/src/lib/locales/ar.js +3 -0
- package/src/lib/locales/ar.js.map +1 -1
- package/src/lib/locales/ar.mjs +3 -0
- package/src/lib/locales/ar.mjs.map +1 -1
- package/src/lib/locales/de.js +3 -0
- package/src/lib/locales/de.js.map +1 -1
- package/src/lib/locales/de.mjs +3 -0
- package/src/lib/locales/de.mjs.map +1 -1
- package/src/lib/locales/fr.js +3 -0
- package/src/lib/locales/fr.js.map +1 -1
- package/src/lib/locales/fr.mjs +3 -0
- package/src/lib/locales/fr.mjs.map +1 -1
- package/src/lib/look.d.mts +3 -3
- package/src/lib/look.d.ts +3 -3
- package/src/lib/look.js +1 -1
- package/src/lib/look.js.map +1 -1
- package/src/lib/look.mjs +1 -1
- package/src/lib/look.mjs.map +1 -1
- package/src/lib/place.d.mts +4 -0
- package/src/lib/place.d.ts +4 -0
- package/src/lib/place.js +2 -0
- package/src/lib/place.js.map +1 -1
- package/src/lib/place.mjs +2 -0
- package/src/lib/place.mjs.map +1 -1
- package/src/lib/viewer.d.mts +13 -3
- package/src/lib/viewer.d.ts +13 -3
- package/src/lib/viewer.js +147 -13
- package/src/lib/viewer.js.map +1 -1
- package/src/lib/viewer.mjs +147 -14
- package/src/lib/viewer.mjs.map +1 -1
package/src/lib/viewer.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createForm, checkPage, isRightToLeft, localizePage, translatePage, wideColumns, MESSAGES, } from '@fieldia/core';
|
|
2
|
-
import { browserPreferences, createWidget, displayValue, drawIcon, installStyles, readsAsText, readText, WIDGET_LABELS } from '@fieldia/widgets';
|
|
2
|
+
import { browserPreferences, createWidget, displayValue, drawIcon, installStyles, readsAsText, readText, wearsQuiet, WIDGET_LABELS } from '@fieldia/widgets';
|
|
3
3
|
import { findPage, pageDialogs } from './related.mjs';
|
|
4
4
|
import { listView } from './list.mjs';
|
|
5
5
|
import { applyLook, skinFor, wearTokens } from './look.mjs';
|
|
@@ -111,7 +111,7 @@ export function mountViewer(host, given) {
|
|
|
111
111
|
return icon ? [icon, words] : [words];
|
|
112
112
|
}
|
|
113
113
|
const theme = options.theme ?? page.look?.theme;
|
|
114
|
-
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': skinFor(options.skin, theme), dir, lang: tag, 'data-max-width': page.maxWidth });
|
|
114
|
+
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': skinFor(options.skin ?? page.look?.skin, theme), dir, lang: tag, 'data-max-width': page.maxWidth });
|
|
115
115
|
applyLook(root, theme ? { ...page.look, theme } : page.look);
|
|
116
116
|
wearTokens(root, options.tokens);
|
|
117
117
|
const confirm = options.confirm ?? dialogConfirm;
|
|
@@ -146,6 +146,8 @@ export function mountViewer(host, given) {
|
|
|
146
146
|
// ---- the parts -------------------------------------------------------
|
|
147
147
|
/** The fields that take the focus as the record opens, in the order they are drawn. */
|
|
148
148
|
const focusers = [];
|
|
149
|
+
/** Each field's face, put on again when the form changes skin: quiet, or in its box. */
|
|
150
|
+
const faces = [];
|
|
149
151
|
/** Folded sections, and how to open each: a problem inside one has to be seen. */
|
|
150
152
|
const folds = new Map();
|
|
151
153
|
/** Every part drawn again from the state of the page shown. */
|
|
@@ -169,8 +171,11 @@ export function mountViewer(host, given) {
|
|
|
169
171
|
const uid = (id) => ownUid(scope.prefix + id);
|
|
170
172
|
/** A part's updater, handed the state of the part's own form. */
|
|
171
173
|
const watch = (update) => sink.push(scope.root ? update : () => update(form.getState()));
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
+
/**
|
|
175
|
+
* A field with its label, help and messages. `place` is where labels sit where it is put; the title's own fields take none.
|
|
176
|
+
* `around` says what the groups round it say of the quiet look: quiet or boxed, and why their fields are read-only.
|
|
177
|
+
*/
|
|
178
|
+
function fieldItem(node, place, around = {}) {
|
|
174
179
|
const def = page.fields[node.field];
|
|
175
180
|
const id = uid(node.id);
|
|
176
181
|
const labelsAt = labelPlace(node, def.type, place);
|
|
@@ -196,6 +201,39 @@ export function mountViewer(host, given) {
|
|
|
196
201
|
// Words chosen by a condition (placeholderWhen), else what the box showed as it was drawn.
|
|
197
202
|
const drawnPlaceholder = textBox?.getAttribute('placeholder') ?? null;
|
|
198
203
|
const chosenPlaceholder = node.placeholderWhen?.length ? textBox : null;
|
|
204
|
+
// The quiet look: its own word, else its group's, else the form's skin. In a quiet place its label is quiet; its value
|
|
205
|
+
// wears the look (data-quiet) unless its widget keeps its own face.
|
|
206
|
+
const quietOwn = node.quiet ?? around.quiet;
|
|
207
|
+
const wears = wearsQuiet(def, node);
|
|
208
|
+
const quietPlace = () => quietOwn ?? root.getAttribute('data-fd-skin') === 'quiet';
|
|
209
|
+
// Empty and quiet, it says what to add, unless the page gives it words of its own: "Add story points", or for a
|
|
210
|
+
// question "Your answer". A field of several boxes, each with its own label (an address), says it in none of them.
|
|
211
|
+
const ownWords = !!node.placeholder || labelsAt === 'hidden' || def.type === 'json' || def.type === 'properties';
|
|
212
|
+
const hint = quietHint(labelText, locale, labels);
|
|
213
|
+
const restWords = () => (!ownWords && wrapper.hasAttribute('data-quiet') ? hint : drawnPlaceholder);
|
|
214
|
+
// Where no box can hold a placeholder (a choice's blank, a state's words), the page's own words come first.
|
|
215
|
+
const blankWords = node.placeholder || (labelsAt === 'hidden' ? labelText : hint);
|
|
216
|
+
const quietSelect = widget.element instanceof HTMLSelectElement ? widget.element.querySelector(':scope > option[value=""]') : null;
|
|
217
|
+
const richText = widget.element.matches('.fd-richtext') ? widget.element : widget.element.querySelector(':scope > .fd-richtext');
|
|
218
|
+
const stateWords = widget.element.querySelector(':scope > .fd-dot-label');
|
|
219
|
+
const face = () => {
|
|
220
|
+
const place = quietPlace();
|
|
221
|
+
const quiet = wears && place;
|
|
222
|
+
wrapper.toggleAttribute('data-in-quiet', place);
|
|
223
|
+
wrapper.toggleAttribute('data-quiet', quiet);
|
|
224
|
+
if (textBox && !chosenPlaceholder)
|
|
225
|
+
setAttr(textBox, 'placeholder', restWords());
|
|
226
|
+
if (quietSelect)
|
|
227
|
+
quietSelect.textContent = quiet ? blankWords : '';
|
|
228
|
+
if (richText && !node.placeholder) {
|
|
229
|
+
setAttr(richText, 'data-placeholder', quiet ? hint : null);
|
|
230
|
+
setAttr(richText, 'aria-placeholder', quiet ? hint : null);
|
|
231
|
+
}
|
|
232
|
+
if (stateWords)
|
|
233
|
+
setAttr(stateWords, 'data-hint', quiet ? blankWords : null);
|
|
234
|
+
};
|
|
235
|
+
faces.push(face);
|
|
236
|
+
face();
|
|
199
237
|
if (!['INPUT', 'SELECT', 'TEXTAREA'].includes(widget.element.tagName)) {
|
|
200
238
|
// `for` stays: a custom field that puts the id on its own input is
|
|
201
239
|
// labelled natively. Only a wrapper with a role (a radio group, say) may
|
|
@@ -205,7 +243,7 @@ export function mountViewer(host, given) {
|
|
|
205
243
|
named?.setAttribute('aria-labelledby', label.id);
|
|
206
244
|
// Not a press on its (?) or the help bubble beside it: they show the help, and must not open a picker's list.
|
|
207
245
|
label.addEventListener('click', (event) => {
|
|
208
|
-
if (!event.target.closest?.('.fd-help-tip-wrap'))
|
|
246
|
+
if (!event.target.closest?.('.fd-help-tip-wrap, .fd-lock-wrap'))
|
|
209
247
|
widget.focus();
|
|
210
248
|
});
|
|
211
249
|
}
|
|
@@ -222,6 +260,33 @@ export function mountViewer(host, given) {
|
|
|
222
260
|
label.append(tip.element);
|
|
223
261
|
label.dir = 'auto';
|
|
224
262
|
}
|
|
263
|
+
// A rule keeping it read-only: a lock by its label (after the box when the label is out of sight) in the quiet look,
|
|
264
|
+
// and in any skin when the page says why — why, it says in a bubble, as the (?) does, pointed at or focused. Made
|
|
265
|
+
// the first time it is needed: a field never locked has none.
|
|
266
|
+
const reason = node.readonlyReason ?? around.reason;
|
|
267
|
+
// Read-only by its nature — worked out from others, or always read-only where the page puts it — a value is shown,
|
|
268
|
+
// not locked: it has a lock only when the page says why.
|
|
269
|
+
const fixed = node.readonly === true || def.readonly === true || def.compute !== undefined;
|
|
270
|
+
let lock = null;
|
|
271
|
+
let lockMark = null;
|
|
272
|
+
let lockedNow = false;
|
|
273
|
+
const lockFor = () => {
|
|
274
|
+
if (lockMark)
|
|
275
|
+
return lockMark;
|
|
276
|
+
const icon = drawIcon(doc, 'lock', options.icons) ?? '';
|
|
277
|
+
lock = reason ? bubbleTip(`${id}-lock`, reason, fill(labels.locked, { label: labelText }), 'fd-lock', icon) : null;
|
|
278
|
+
lockMark = lock?.element ?? el('span', { class: 'fd-lock-wrap' }, el('span', { class: 'fd-lock', 'aria-hidden': 'true' }, icon));
|
|
279
|
+
if (labelsAt === 'hidden')
|
|
280
|
+
widget.element.after(lockMark);
|
|
281
|
+
else
|
|
282
|
+
label.insertBefore(lockMark, tip && labelsAt !== 'after' ? tip.element : null);
|
|
283
|
+
return lockMark;
|
|
284
|
+
};
|
|
285
|
+
// Pointing at a locked quiet value, or focusing it, says why too.
|
|
286
|
+
const point = (on) => lock?.point(on && lockedNow && wrapper.hasAttribute('data-in-quiet'));
|
|
287
|
+
if (reason)
|
|
288
|
+
for (const [event, on] of [['mouseenter', true], ['mouseleave', false], ['focusin', true], ['focusout', false]])
|
|
289
|
+
widget.element.addEventListener(event, () => point(on));
|
|
225
290
|
// Not an alert of its own: a refused save is announced once, naming every field to look at.
|
|
226
291
|
const error = el('div', { class: 'fd-error', id: `${id}-error`, hidden: '' });
|
|
227
292
|
// A warning from an answer rule, and one from the data source's onchange beside the field whose change brought it.
|
|
@@ -231,6 +296,9 @@ export function mountViewer(host, given) {
|
|
|
231
296
|
const asText = page.look?.readonlyShown === 'text' && readsAsText(def, node) ? readText({ document: doc, field: def, node, id, locale, labels: widgetLabels, dialogs }) : null;
|
|
232
297
|
if (asText)
|
|
233
298
|
widget.element.after(asText.element);
|
|
299
|
+
if (asText && reason)
|
|
300
|
+
for (const [event, on] of [['mouseenter', true], ['mouseleave', false]])
|
|
301
|
+
asText.element.addEventListener(event, () => point(on));
|
|
234
302
|
if (widget.destroy)
|
|
235
303
|
cleanups.push(() => widget.destroy?.());
|
|
236
304
|
// An answer rule's warning waits until the person leaves the field: no advice
|
|
@@ -246,6 +314,22 @@ export function mountViewer(host, given) {
|
|
|
246
314
|
// (a deleted line's button), and that redraw must finish first.
|
|
247
315
|
queueMicrotask(() => update(form.getState()));
|
|
248
316
|
});
|
|
317
|
+
// Quiet, Escape puts back the value the field had as it was entered — once its own list or calendar, if open, has
|
|
318
|
+
// taken the key — and only then is it left for the dialog round it. Leaving keeps the change, for Save.
|
|
319
|
+
let entered;
|
|
320
|
+
wrapper.addEventListener('focusin', (event) => {
|
|
321
|
+
if (!wrapper.contains(event.relatedTarget))
|
|
322
|
+
entered = form.getState().values[node.field];
|
|
323
|
+
});
|
|
324
|
+
wrapper.addEventListener('keydown', (event) => {
|
|
325
|
+
if (event.key !== 'Escape' || event.defaultPrevented || !wrapper.hasAttribute('data-quiet'))
|
|
326
|
+
return;
|
|
327
|
+
if (JSON.stringify(form.getState().values[node.field] ?? null) === JSON.stringify(entered ?? null))
|
|
328
|
+
return;
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
event.stopPropagation();
|
|
331
|
+
form.setValue(node.field, (entered ?? null));
|
|
332
|
+
});
|
|
249
333
|
const toned = node.tones !== undefined || node.bold !== undefined;
|
|
250
334
|
const update = (state) => {
|
|
251
335
|
const shown = form.node(node.id);
|
|
@@ -258,7 +342,7 @@ export function mountViewer(host, given) {
|
|
|
258
342
|
wrapper.classList.toggle('fd-value-bold', look.bold);
|
|
259
343
|
}
|
|
260
344
|
if (chosenPlaceholder)
|
|
261
|
-
setAttr(chosenPlaceholder, 'placeholder', form.placeholder(node.id) ??
|
|
345
|
+
setAttr(chosenPlaceholder, 'placeholder', form.placeholder(node.id) ?? restWords());
|
|
262
346
|
const message = state.errors[node.field];
|
|
263
347
|
setHidden(error, !message);
|
|
264
348
|
setText(error, message ?? '');
|
|
@@ -277,6 +361,16 @@ export function mountViewer(host, given) {
|
|
|
277
361
|
mark.toggleAttribute('hidden', !valid);
|
|
278
362
|
}
|
|
279
363
|
const readonly = shown.readonly || locked || scope.locked();
|
|
364
|
+
// Locked by a rule, not by the whole form being read: its lock shows, in the quiet look or with its reason, and its words are muted.
|
|
365
|
+
const quiet = wrapper.hasAttribute('data-quiet');
|
|
366
|
+
lockedNow = readonly && !locked;
|
|
367
|
+
const showsLock = lockedNow && (!!reason || (wrapper.hasAttribute('data-in-quiet') && !fixed));
|
|
368
|
+
if (showsLock || lockMark)
|
|
369
|
+
setHidden(lockFor(), !showsLock);
|
|
370
|
+
if (!lockedNow)
|
|
371
|
+
point(false);
|
|
372
|
+
setAttr(wrapper, 'data-locked', quiet && showsLock ? '' : null);
|
|
373
|
+
setAttr(wrapper, 'data-empty', quiet && isBlank(state.values[node.field]) ? '' : null);
|
|
280
374
|
if (asText) {
|
|
281
375
|
setHidden(widget.element, readonly);
|
|
282
376
|
setHidden(asText.element, !readonly);
|
|
@@ -290,7 +384,7 @@ export function mountViewer(host, given) {
|
|
|
290
384
|
readonly,
|
|
291
385
|
required: shown.required,
|
|
292
386
|
invalid: !!message,
|
|
293
|
-
describedBy: [help?.id ?? tip?.bubble.id, message ? error.id : undefined, warned ? warning.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
387
|
+
describedBy: [help?.id ?? tip?.bubble.id, lock && lockedNow ? lock.bubble.id : undefined, message ? error.id : undefined, warned ? warning.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
294
388
|
});
|
|
295
389
|
};
|
|
296
390
|
watch(update);
|
|
@@ -298,8 +392,18 @@ export function mountViewer(host, given) {
|
|
|
298
392
|
}
|
|
299
393
|
/** A (?) that shows a field's help in a bubble: on hover, on focus, or kept open by a press — a tap — and gone with Escape. */
|
|
300
394
|
function helpTip(bubbleId, text, name) {
|
|
395
|
+
return bubbleTip(bubbleId, text, name, 'fd-help-tip', '?');
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* A small button with words in a bubble — a field's help behind its (?), why it is locked behind its lock — shown
|
|
399
|
+
* on hover, on focus, or kept open by a press, and gone with Escape. `point` shows it from elsewhere: its value pointed at.
|
|
400
|
+
*/
|
|
401
|
+
function bubbleTip(bubbleId, text, name, kind, face) {
|
|
301
402
|
const bubble = el('span', { class: 'fd-help-bubble', role: 'tooltip', id: bubbleId, hidden: '' }, text);
|
|
302
|
-
const button = el('button', { type: 'button', class:
|
|
403
|
+
const button = el('button', { type: 'button', class: kind, 'aria-label': name, 'aria-expanded': 'false', 'aria-controls': bubbleId }, face);
|
|
404
|
+
// A lock's reason is said as it is reached; the help waits to be asked for.
|
|
405
|
+
if (kind === 'fd-lock')
|
|
406
|
+
button.setAttribute('aria-describedby', bubbleId);
|
|
303
407
|
let kept = false;
|
|
304
408
|
const show = (open) => {
|
|
305
409
|
bubble.hidden = !open;
|
|
@@ -324,7 +428,7 @@ export function mountViewer(host, given) {
|
|
|
324
428
|
kept = false;
|
|
325
429
|
show(false);
|
|
326
430
|
});
|
|
327
|
-
return { element: el('span', { class: 'fd-help-tip-wrap' }, button, bubble), bubble };
|
|
431
|
+
return { element: el('span', { class: kind === 'fd-lock' ? 'fd-lock-wrap' : 'fd-help-tip-wrap' }, button, bubble), bubble, point: (on) => show(on || kept) };
|
|
328
432
|
}
|
|
329
433
|
/** A bubble hangs from its (?): near the edge of a phone's screen it runs past it, so it is moved back in. */
|
|
330
434
|
function keepOnScreen(bubble) {
|
|
@@ -423,7 +527,7 @@ export function mountViewer(host, given) {
|
|
|
423
527
|
const line = el('div', { class: 'fd-field fd-oneline', 'data-node': node.id, 'data-style': 'inline', 'data-labels': place.labels });
|
|
424
528
|
spans(line, node.colspan);
|
|
425
529
|
const row = el('div', { class: 'fd-oneline-row', role: 'group' }, ...onelineRuns(node.children).map((run) => {
|
|
426
|
-
const parts = run.map((child) => item(child, { columns: 1, onPage: false, inline: true }));
|
|
530
|
+
const parts = run.map((child) => item(child, { columns: 1, onPage: false, inline: true, quiet: node.quiet ?? place.quiet, reason: node.readonlyReason ?? place.reason }));
|
|
427
531
|
return parts.length === 1 ? parts[0] : el('span', { class: 'fd-oneline-pair' }, ...parts);
|
|
428
532
|
}));
|
|
429
533
|
if (node.title) {
|
|
@@ -433,6 +537,10 @@ export function mountViewer(host, given) {
|
|
|
433
537
|
}
|
|
434
538
|
line.append(row);
|
|
435
539
|
hideWhen(line, node.id);
|
|
540
|
+
// In a quiet place the line's label is quiet, as its fields' are.
|
|
541
|
+
const face = () => line.toggleAttribute('data-in-quiet', node.quiet ?? place.quiet ?? root.getAttribute('data-fd-skin') === 'quiet');
|
|
542
|
+
faces.push(face);
|
|
543
|
+
face();
|
|
436
544
|
return line;
|
|
437
545
|
}
|
|
438
546
|
function sectionItem(node, place) {
|
|
@@ -514,7 +622,7 @@ export function mountViewer(host, given) {
|
|
|
514
622
|
sink = own;
|
|
515
623
|
try {
|
|
516
624
|
// A tab's parts sit where the tabs do: on the page, or in the box round them.
|
|
517
|
-
panel.append(grid(tab.children, 1, { columns: 1, onPage: place.onPage, labels: place.labels }));
|
|
625
|
+
panel.append(grid(tab.children, 1, { columns: 1, onPage: place.onPage, labels: place.labels, quiet: place.quiet, reason: place.reason }));
|
|
518
626
|
}
|
|
519
627
|
finally {
|
|
520
628
|
sink = outer;
|
|
@@ -610,7 +718,7 @@ export function mountViewer(host, given) {
|
|
|
610
718
|
function partItem(node, place) {
|
|
611
719
|
switch (node.type) {
|
|
612
720
|
case 'field':
|
|
613
|
-
return fieldItem(node, place.labels);
|
|
721
|
+
return fieldItem(node, place.labels, place);
|
|
614
722
|
case 'button':
|
|
615
723
|
return buttonItem(node);
|
|
616
724
|
case 'text':
|
|
@@ -731,7 +839,7 @@ export function mountViewer(host, given) {
|
|
|
731
839
|
});
|
|
732
840
|
const layout = saved.layout;
|
|
733
841
|
// Its parts sit in the part's box: a card there draws none of its own.
|
|
734
|
-
note.replaceWith(own.grid(layout.type === 'tabs' ? [layout] : layout.children, 1, { columns: 1, onPage: false, labels: place.labels }));
|
|
842
|
+
note.replaceWith(own.grid(layout.type === 'tabs' ? [layout] : layout.children, 1, { columns: 1, onPage: false, labels: place.labels, quiet: place.quiet }));
|
|
735
843
|
box.removeAttribute('aria-busy');
|
|
736
844
|
cleanups.push(inner.subscribe(renderNow));
|
|
737
845
|
if (mounted)
|
|
@@ -1549,6 +1657,13 @@ export function mountViewer(host, given) {
|
|
|
1549
1657
|
actionHost.say(result.message ?? '', result.reason === 'app' ? 'danger' : 'warning');
|
|
1550
1658
|
}));
|
|
1551
1659
|
render(form.getState());
|
|
1660
|
+
/** The form in another skin: each field's face put on again, and drawn again from the state. */
|
|
1661
|
+
function refaced() {
|
|
1662
|
+
for (const face of faces)
|
|
1663
|
+
face();
|
|
1664
|
+
if (mounted)
|
|
1665
|
+
renderNow();
|
|
1666
|
+
}
|
|
1552
1667
|
mounted = true;
|
|
1553
1668
|
// As the record opens, its focused field takes the focus — the first shown — unless the person is busy elsewhere:
|
|
1554
1669
|
// only while nothing has the focus, or, the first time, while it is in this form (a dialog's first box).
|
|
@@ -1575,13 +1690,15 @@ export function mountViewer(host, given) {
|
|
|
1575
1690
|
element: root,
|
|
1576
1691
|
setSkin(skin) {
|
|
1577
1692
|
root.setAttribute('data-fd-skin', skin);
|
|
1693
|
+
refaced();
|
|
1578
1694
|
},
|
|
1579
1695
|
setTheme(next) {
|
|
1580
1696
|
if (next)
|
|
1581
1697
|
root.setAttribute('data-fd-theme', next);
|
|
1582
1698
|
else
|
|
1583
1699
|
root.removeAttribute('data-fd-theme');
|
|
1584
|
-
root.setAttribute('data-fd-skin', skinFor(options.skin, next ?? undefined));
|
|
1700
|
+
root.setAttribute('data-fd-skin', skinFor(options.skin ?? page.look?.skin, next ?? undefined));
|
|
1701
|
+
refaced();
|
|
1585
1702
|
},
|
|
1586
1703
|
setTokens(next) {
|
|
1587
1704
|
options.tokens = next ?? undefined;
|
|
@@ -1626,6 +1743,22 @@ export function mountViewer(host, given) {
|
|
|
1626
1743
|
}
|
|
1627
1744
|
/** The boxes a label can be shown in, as the words of an empty box. */
|
|
1628
1745
|
const TEXT_BOX = 'input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]), textarea';
|
|
1746
|
+
/** Nothing in it: no value, empty words, nothing picked. */
|
|
1747
|
+
const isBlank = (value) => value === null || value === undefined || value === '' || (Array.isArray(value) && !value.length);
|
|
1748
|
+
/**
|
|
1749
|
+
* A label inside a sentence, as "Add story points" says it: in English and French its first letter small, unless the
|
|
1750
|
+
* word is a name such as "VAT"; German nouns, and Arabic, as written.
|
|
1751
|
+
*/
|
|
1752
|
+
function inSentence(label, locale) {
|
|
1753
|
+
return (locale === 'en' || locale === 'fr') && /^\p{Lu}\p{Ll}/u.test(label) ? label[0].toLocaleLowerCase(locale) + label.slice(1) : label;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* What an empty field in the quiet look says, by its label, in the viewer's
|
|
1757
|
+
* words for `locale`: "Add story points", or for a question "Your answer".
|
|
1758
|
+
*/
|
|
1759
|
+
export function quietHint(label, locale, labels = VIEWER_LABELS[locale]) {
|
|
1760
|
+
return /[?؟]\s*$/.test(label) ? labels.quietAnswer : labels.quietHint.replace('{label}', inSentence(label, locale));
|
|
1761
|
+
}
|
|
1629
1762
|
/** Fields whose ✓ would say nothing: a yes/no box is never wrong, a table or a file has its own look. */
|
|
1630
1763
|
const NO_VALID_MARK = new Set(['boolean', 'one2many', 'binary', 'image', 'html', 'json', 'properties']);
|
|
1631
1764
|
/**
|