@fieldia/viewer 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_LABELS = exports.VIEWER_LABELS = void 0;
|
|
4
4
|
exports.mountViewer = mountViewer;
|
|
5
|
+
exports.quietHint = quietHint;
|
|
5
6
|
exports.onelineRuns = onelineRuns;
|
|
6
7
|
const core_1 = require("@fieldia/core");
|
|
7
8
|
const widgets_1 = require("@fieldia/widgets");
|
|
@@ -118,7 +119,7 @@ function mountViewer(host, given) {
|
|
|
118
119
|
return icon ? [icon, words] : [words];
|
|
119
120
|
}
|
|
120
121
|
const theme = options.theme ?? page.look?.theme;
|
|
121
|
-
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': (0, look_1.skinFor)(options.skin, theme), dir, lang: tag, 'data-max-width': page.maxWidth });
|
|
122
|
+
const root = el('form', { class: 'fd-form', novalidate: '', 'data-fd-skin': (0, look_1.skinFor)(options.skin ?? page.look?.skin, theme), dir, lang: tag, 'data-max-width': page.maxWidth });
|
|
122
123
|
(0, look_1.applyLook)(root, theme ? { ...page.look, theme } : page.look);
|
|
123
124
|
(0, look_1.wearTokens)(root, options.tokens);
|
|
124
125
|
const confirm = options.confirm ?? dialogConfirm;
|
|
@@ -153,6 +154,8 @@ function mountViewer(host, given) {
|
|
|
153
154
|
// ---- the parts -------------------------------------------------------
|
|
154
155
|
/** The fields that take the focus as the record opens, in the order they are drawn. */
|
|
155
156
|
const focusers = [];
|
|
157
|
+
/** Each field's face, put on again when the form changes skin: quiet, or in its box. */
|
|
158
|
+
const faces = [];
|
|
156
159
|
/** Folded sections, and how to open each: a problem inside one has to be seen. */
|
|
157
160
|
const folds = new Map();
|
|
158
161
|
/** Every part drawn again from the state of the page shown. */
|
|
@@ -176,8 +179,11 @@ function mountViewer(host, given) {
|
|
|
176
179
|
const uid = (id) => ownUid(scope.prefix + id);
|
|
177
180
|
/** A part's updater, handed the state of the part's own form. */
|
|
178
181
|
const watch = (update) => sink.push(scope.root ? update : () => update(form.getState()));
|
|
179
|
-
/**
|
|
180
|
-
|
|
182
|
+
/**
|
|
183
|
+
* A field with its label, help and messages. `place` is where labels sit where it is put; the title's own fields take none.
|
|
184
|
+
* `around` says what the groups round it say of the quiet look: quiet or boxed, and why their fields are read-only.
|
|
185
|
+
*/
|
|
186
|
+
function fieldItem(node, place, around = {}) {
|
|
181
187
|
const def = page.fields[node.field];
|
|
182
188
|
const id = uid(node.id);
|
|
183
189
|
const labelsAt = (0, place_1.labelPlace)(node, def.type, place);
|
|
@@ -203,6 +209,39 @@ function mountViewer(host, given) {
|
|
|
203
209
|
// Words chosen by a condition (placeholderWhen), else what the box showed as it was drawn.
|
|
204
210
|
const drawnPlaceholder = textBox?.getAttribute('placeholder') ?? null;
|
|
205
211
|
const chosenPlaceholder = node.placeholderWhen?.length ? textBox : null;
|
|
212
|
+
// 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
|
|
213
|
+
// wears the look (data-quiet) unless its widget keeps its own face.
|
|
214
|
+
const quietOwn = node.quiet ?? around.quiet;
|
|
215
|
+
const wears = (0, widgets_1.wearsQuiet)(def, node);
|
|
216
|
+
const quietPlace = () => quietOwn ?? root.getAttribute('data-fd-skin') === 'quiet';
|
|
217
|
+
// Empty and quiet, it says what to add, unless the page gives it words of its own: "Add story points", or for a
|
|
218
|
+
// question "Your answer". A field of several boxes, each with its own label (an address), says it in none of them.
|
|
219
|
+
const ownWords = !!node.placeholder || labelsAt === 'hidden' || def.type === 'json' || def.type === 'properties';
|
|
220
|
+
const hint = quietHint(labelText, locale, labels);
|
|
221
|
+
const restWords = () => (!ownWords && wrapper.hasAttribute('data-quiet') ? hint : drawnPlaceholder);
|
|
222
|
+
// Where no box can hold a placeholder (a choice's blank, a state's words), the page's own words come first.
|
|
223
|
+
const blankWords = node.placeholder || (labelsAt === 'hidden' ? labelText : hint);
|
|
224
|
+
const quietSelect = widget.element instanceof HTMLSelectElement ? widget.element.querySelector(':scope > option[value=""]') : null;
|
|
225
|
+
const richText = widget.element.matches('.fd-richtext') ? widget.element : widget.element.querySelector(':scope > .fd-richtext');
|
|
226
|
+
const stateWords = widget.element.querySelector(':scope > .fd-dot-label');
|
|
227
|
+
const face = () => {
|
|
228
|
+
const place = quietPlace();
|
|
229
|
+
const quiet = wears && place;
|
|
230
|
+
wrapper.toggleAttribute('data-in-quiet', place);
|
|
231
|
+
wrapper.toggleAttribute('data-quiet', quiet);
|
|
232
|
+
if (textBox && !chosenPlaceholder)
|
|
233
|
+
(0, dom_1.setAttr)(textBox, 'placeholder', restWords());
|
|
234
|
+
if (quietSelect)
|
|
235
|
+
quietSelect.textContent = quiet ? blankWords : '';
|
|
236
|
+
if (richText && !node.placeholder) {
|
|
237
|
+
(0, dom_1.setAttr)(richText, 'data-placeholder', quiet ? hint : null);
|
|
238
|
+
(0, dom_1.setAttr)(richText, 'aria-placeholder', quiet ? hint : null);
|
|
239
|
+
}
|
|
240
|
+
if (stateWords)
|
|
241
|
+
(0, dom_1.setAttr)(stateWords, 'data-hint', quiet ? blankWords : null);
|
|
242
|
+
};
|
|
243
|
+
faces.push(face);
|
|
244
|
+
face();
|
|
206
245
|
if (!['INPUT', 'SELECT', 'TEXTAREA'].includes(widget.element.tagName)) {
|
|
207
246
|
// `for` stays: a custom field that puts the id on its own input is
|
|
208
247
|
// labelled natively. Only a wrapper with a role (a radio group, say) may
|
|
@@ -212,7 +251,7 @@ function mountViewer(host, given) {
|
|
|
212
251
|
named?.setAttribute('aria-labelledby', label.id);
|
|
213
252
|
// Not a press on its (?) or the help bubble beside it: they show the help, and must not open a picker's list.
|
|
214
253
|
label.addEventListener('click', (event) => {
|
|
215
|
-
if (!event.target.closest?.('.fd-help-tip-wrap'))
|
|
254
|
+
if (!event.target.closest?.('.fd-help-tip-wrap, .fd-lock-wrap'))
|
|
216
255
|
widget.focus();
|
|
217
256
|
});
|
|
218
257
|
}
|
|
@@ -229,6 +268,33 @@ function mountViewer(host, given) {
|
|
|
229
268
|
label.append(tip.element);
|
|
230
269
|
label.dir = 'auto';
|
|
231
270
|
}
|
|
271
|
+
// 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,
|
|
272
|
+
// and in any skin when the page says why — why, it says in a bubble, as the (?) does, pointed at or focused. Made
|
|
273
|
+
// the first time it is needed: a field never locked has none.
|
|
274
|
+
const reason = node.readonlyReason ?? around.reason;
|
|
275
|
+
// Read-only by its nature — worked out from others, or always read-only where the page puts it — a value is shown,
|
|
276
|
+
// not locked: it has a lock only when the page says why.
|
|
277
|
+
const fixed = node.readonly === true || def.readonly === true || def.compute !== undefined;
|
|
278
|
+
let lock = null;
|
|
279
|
+
let lockMark = null;
|
|
280
|
+
let lockedNow = false;
|
|
281
|
+
const lockFor = () => {
|
|
282
|
+
if (lockMark)
|
|
283
|
+
return lockMark;
|
|
284
|
+
const icon = (0, widgets_1.drawIcon)(doc, 'lock', options.icons) ?? '';
|
|
285
|
+
lock = reason ? bubbleTip(`${id}-lock`, reason, fill(labels.locked, { label: labelText }), 'fd-lock', icon) : null;
|
|
286
|
+
lockMark = lock?.element ?? el('span', { class: 'fd-lock-wrap' }, el('span', { class: 'fd-lock', 'aria-hidden': 'true' }, icon));
|
|
287
|
+
if (labelsAt === 'hidden')
|
|
288
|
+
widget.element.after(lockMark);
|
|
289
|
+
else
|
|
290
|
+
label.insertBefore(lockMark, tip && labelsAt !== 'after' ? tip.element : null);
|
|
291
|
+
return lockMark;
|
|
292
|
+
};
|
|
293
|
+
// Pointing at a locked quiet value, or focusing it, says why too.
|
|
294
|
+
const point = (on) => lock?.point(on && lockedNow && wrapper.hasAttribute('data-in-quiet'));
|
|
295
|
+
if (reason)
|
|
296
|
+
for (const [event, on] of [['mouseenter', true], ['mouseleave', false], ['focusin', true], ['focusout', false]])
|
|
297
|
+
widget.element.addEventListener(event, () => point(on));
|
|
232
298
|
// Not an alert of its own: a refused save is announced once, naming every field to look at.
|
|
233
299
|
const error = el('div', { class: 'fd-error', id: `${id}-error`, hidden: '' });
|
|
234
300
|
// A warning from an answer rule, and one from the data source's onchange beside the field whose change brought it.
|
|
@@ -238,6 +304,9 @@ function mountViewer(host, given) {
|
|
|
238
304
|
const asText = page.look?.readonlyShown === 'text' && (0, widgets_1.readsAsText)(def, node) ? (0, widgets_1.readText)({ document: doc, field: def, node, id, locale, labels: widgetLabels, dialogs }) : null;
|
|
239
305
|
if (asText)
|
|
240
306
|
widget.element.after(asText.element);
|
|
307
|
+
if (asText && reason)
|
|
308
|
+
for (const [event, on] of [['mouseenter', true], ['mouseleave', false]])
|
|
309
|
+
asText.element.addEventListener(event, () => point(on));
|
|
241
310
|
if (widget.destroy)
|
|
242
311
|
cleanups.push(() => widget.destroy?.());
|
|
243
312
|
// An answer rule's warning waits until the person leaves the field: no advice
|
|
@@ -253,6 +322,22 @@ function mountViewer(host, given) {
|
|
|
253
322
|
// (a deleted line's button), and that redraw must finish first.
|
|
254
323
|
queueMicrotask(() => update(form.getState()));
|
|
255
324
|
});
|
|
325
|
+
// Quiet, Escape puts back the value the field had as it was entered — once its own list or calendar, if open, has
|
|
326
|
+
// taken the key — and only then is it left for the dialog round it. Leaving keeps the change, for Save.
|
|
327
|
+
let entered;
|
|
328
|
+
wrapper.addEventListener('focusin', (event) => {
|
|
329
|
+
if (!wrapper.contains(event.relatedTarget))
|
|
330
|
+
entered = form.getState().values[node.field];
|
|
331
|
+
});
|
|
332
|
+
wrapper.addEventListener('keydown', (event) => {
|
|
333
|
+
if (event.key !== 'Escape' || event.defaultPrevented || !wrapper.hasAttribute('data-quiet'))
|
|
334
|
+
return;
|
|
335
|
+
if (JSON.stringify(form.getState().values[node.field] ?? null) === JSON.stringify(entered ?? null))
|
|
336
|
+
return;
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
event.stopPropagation();
|
|
339
|
+
form.setValue(node.field, (entered ?? null));
|
|
340
|
+
});
|
|
256
341
|
const toned = node.tones !== undefined || node.bold !== undefined;
|
|
257
342
|
const update = (state) => {
|
|
258
343
|
const shown = form.node(node.id);
|
|
@@ -265,7 +350,7 @@ function mountViewer(host, given) {
|
|
|
265
350
|
wrapper.classList.toggle('fd-value-bold', look.bold);
|
|
266
351
|
}
|
|
267
352
|
if (chosenPlaceholder)
|
|
268
|
-
(0, dom_1.setAttr)(chosenPlaceholder, 'placeholder', form.placeholder(node.id) ??
|
|
353
|
+
(0, dom_1.setAttr)(chosenPlaceholder, 'placeholder', form.placeholder(node.id) ?? restWords());
|
|
269
354
|
const message = state.errors[node.field];
|
|
270
355
|
(0, dom_1.setHidden)(error, !message);
|
|
271
356
|
(0, dom_1.setText)(error, message ?? '');
|
|
@@ -284,6 +369,16 @@ function mountViewer(host, given) {
|
|
|
284
369
|
mark.toggleAttribute('hidden', !valid);
|
|
285
370
|
}
|
|
286
371
|
const readonly = shown.readonly || locked || scope.locked();
|
|
372
|
+
// 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.
|
|
373
|
+
const quiet = wrapper.hasAttribute('data-quiet');
|
|
374
|
+
lockedNow = readonly && !locked;
|
|
375
|
+
const showsLock = lockedNow && (!!reason || (wrapper.hasAttribute('data-in-quiet') && !fixed));
|
|
376
|
+
if (showsLock || lockMark)
|
|
377
|
+
(0, dom_1.setHidden)(lockFor(), !showsLock);
|
|
378
|
+
if (!lockedNow)
|
|
379
|
+
point(false);
|
|
380
|
+
(0, dom_1.setAttr)(wrapper, 'data-locked', quiet && showsLock ? '' : null);
|
|
381
|
+
(0, dom_1.setAttr)(wrapper, 'data-empty', quiet && isBlank(state.values[node.field]) ? '' : null);
|
|
287
382
|
if (asText) {
|
|
288
383
|
(0, dom_1.setHidden)(widget.element, readonly);
|
|
289
384
|
(0, dom_1.setHidden)(asText.element, !readonly);
|
|
@@ -297,7 +392,7 @@ function mountViewer(host, given) {
|
|
|
297
392
|
readonly,
|
|
298
393
|
required: shown.required,
|
|
299
394
|
invalid: !!message,
|
|
300
|
-
describedBy: [help?.id ?? tip?.bubble.id, message ? error.id : undefined, warned ? warning.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
395
|
+
describedBy: [help?.id ?? tip?.bubble.id, lock && lockedNow ? lock.bubble.id : undefined, message ? error.id : undefined, warned ? warning.id : undefined].filter(Boolean).join(' ') || undefined,
|
|
301
396
|
});
|
|
302
397
|
};
|
|
303
398
|
watch(update);
|
|
@@ -305,8 +400,18 @@ function mountViewer(host, given) {
|
|
|
305
400
|
}
|
|
306
401
|
/** 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. */
|
|
307
402
|
function helpTip(bubbleId, text, name) {
|
|
403
|
+
return bubbleTip(bubbleId, text, name, 'fd-help-tip', '?');
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* A small button with words in a bubble — a field's help behind its (?), why it is locked behind its lock — shown
|
|
407
|
+
* on hover, on focus, or kept open by a press, and gone with Escape. `point` shows it from elsewhere: its value pointed at.
|
|
408
|
+
*/
|
|
409
|
+
function bubbleTip(bubbleId, text, name, kind, face) {
|
|
308
410
|
const bubble = el('span', { class: 'fd-help-bubble', role: 'tooltip', id: bubbleId, hidden: '' }, text);
|
|
309
|
-
const button = el('button', { type: 'button', class:
|
|
411
|
+
const button = el('button', { type: 'button', class: kind, 'aria-label': name, 'aria-expanded': 'false', 'aria-controls': bubbleId }, face);
|
|
412
|
+
// A lock's reason is said as it is reached; the help waits to be asked for.
|
|
413
|
+
if (kind === 'fd-lock')
|
|
414
|
+
button.setAttribute('aria-describedby', bubbleId);
|
|
310
415
|
let kept = false;
|
|
311
416
|
const show = (open) => {
|
|
312
417
|
bubble.hidden = !open;
|
|
@@ -331,7 +436,7 @@ function mountViewer(host, given) {
|
|
|
331
436
|
kept = false;
|
|
332
437
|
show(false);
|
|
333
438
|
});
|
|
334
|
-
return { element: el('span', { class: 'fd-help-tip-wrap' }, button, bubble), bubble };
|
|
439
|
+
return { element: el('span', { class: kind === 'fd-lock' ? 'fd-lock-wrap' : 'fd-help-tip-wrap' }, button, bubble), bubble, point: (on) => show(on || kept) };
|
|
335
440
|
}
|
|
336
441
|
/** A bubble hangs from its (?): near the edge of a phone's screen it runs past it, so it is moved back in. */
|
|
337
442
|
function keepOnScreen(bubble) {
|
|
@@ -430,7 +535,7 @@ function mountViewer(host, given) {
|
|
|
430
535
|
const line = el('div', { class: 'fd-field fd-oneline', 'data-node': node.id, 'data-style': 'inline', 'data-labels': place.labels });
|
|
431
536
|
spans(line, node.colspan);
|
|
432
537
|
const row = el('div', { class: 'fd-oneline-row', role: 'group' }, ...onelineRuns(node.children).map((run) => {
|
|
433
|
-
const parts = run.map((child) => item(child, { columns: 1, onPage: false, inline: true }));
|
|
538
|
+
const parts = run.map((child) => item(child, { columns: 1, onPage: false, inline: true, quiet: node.quiet ?? place.quiet, reason: node.readonlyReason ?? place.reason }));
|
|
434
539
|
return parts.length === 1 ? parts[0] : el('span', { class: 'fd-oneline-pair' }, ...parts);
|
|
435
540
|
}));
|
|
436
541
|
if (node.title) {
|
|
@@ -440,6 +545,10 @@ function mountViewer(host, given) {
|
|
|
440
545
|
}
|
|
441
546
|
line.append(row);
|
|
442
547
|
hideWhen(line, node.id);
|
|
548
|
+
// In a quiet place the line's label is quiet, as its fields' are.
|
|
549
|
+
const face = () => line.toggleAttribute('data-in-quiet', node.quiet ?? place.quiet ?? root.getAttribute('data-fd-skin') === 'quiet');
|
|
550
|
+
faces.push(face);
|
|
551
|
+
face();
|
|
443
552
|
return line;
|
|
444
553
|
}
|
|
445
554
|
function sectionItem(node, place) {
|
|
@@ -521,7 +630,7 @@ function mountViewer(host, given) {
|
|
|
521
630
|
sink = own;
|
|
522
631
|
try {
|
|
523
632
|
// A tab's parts sit where the tabs do: on the page, or in the box round them.
|
|
524
|
-
panel.append(grid(tab.children, 1, { columns: 1, onPage: place.onPage, labels: place.labels }));
|
|
633
|
+
panel.append(grid(tab.children, 1, { columns: 1, onPage: place.onPage, labels: place.labels, quiet: place.quiet, reason: place.reason }));
|
|
525
634
|
}
|
|
526
635
|
finally {
|
|
527
636
|
sink = outer;
|
|
@@ -617,7 +726,7 @@ function mountViewer(host, given) {
|
|
|
617
726
|
function partItem(node, place) {
|
|
618
727
|
switch (node.type) {
|
|
619
728
|
case 'field':
|
|
620
|
-
return fieldItem(node, place.labels);
|
|
729
|
+
return fieldItem(node, place.labels, place);
|
|
621
730
|
case 'button':
|
|
622
731
|
return buttonItem(node);
|
|
623
732
|
case 'text':
|
|
@@ -738,7 +847,7 @@ function mountViewer(host, given) {
|
|
|
738
847
|
});
|
|
739
848
|
const layout = saved.layout;
|
|
740
849
|
// Its parts sit in the part's box: a card there draws none of its own.
|
|
741
|
-
note.replaceWith(own.grid(layout.type === 'tabs' ? [layout] : layout.children, 1, { columns: 1, onPage: false, labels: place.labels }));
|
|
850
|
+
note.replaceWith(own.grid(layout.type === 'tabs' ? [layout] : layout.children, 1, { columns: 1, onPage: false, labels: place.labels, quiet: place.quiet }));
|
|
742
851
|
box.removeAttribute('aria-busy');
|
|
743
852
|
cleanups.push(inner.subscribe(renderNow));
|
|
744
853
|
if (mounted)
|
|
@@ -1556,6 +1665,13 @@ function mountViewer(host, given) {
|
|
|
1556
1665
|
actionHost.say(result.message ?? '', result.reason === 'app' ? 'danger' : 'warning');
|
|
1557
1666
|
}));
|
|
1558
1667
|
render(form.getState());
|
|
1668
|
+
/** The form in another skin: each field's face put on again, and drawn again from the state. */
|
|
1669
|
+
function refaced() {
|
|
1670
|
+
for (const face of faces)
|
|
1671
|
+
face();
|
|
1672
|
+
if (mounted)
|
|
1673
|
+
renderNow();
|
|
1674
|
+
}
|
|
1559
1675
|
mounted = true;
|
|
1560
1676
|
// As the record opens, its focused field takes the focus — the first shown — unless the person is busy elsewhere:
|
|
1561
1677
|
// only while nothing has the focus, or, the first time, while it is in this form (a dialog's first box).
|
|
@@ -1582,13 +1698,15 @@ function mountViewer(host, given) {
|
|
|
1582
1698
|
element: root,
|
|
1583
1699
|
setSkin(skin) {
|
|
1584
1700
|
root.setAttribute('data-fd-skin', skin);
|
|
1701
|
+
refaced();
|
|
1585
1702
|
},
|
|
1586
1703
|
setTheme(next) {
|
|
1587
1704
|
if (next)
|
|
1588
1705
|
root.setAttribute('data-fd-theme', next);
|
|
1589
1706
|
else
|
|
1590
1707
|
root.removeAttribute('data-fd-theme');
|
|
1591
|
-
root.setAttribute('data-fd-skin', (0, look_1.skinFor)(options.skin, next ?? undefined));
|
|
1708
|
+
root.setAttribute('data-fd-skin', (0, look_1.skinFor)(options.skin ?? page.look?.skin, next ?? undefined));
|
|
1709
|
+
refaced();
|
|
1592
1710
|
},
|
|
1593
1711
|
setTokens(next) {
|
|
1594
1712
|
options.tokens = next ?? undefined;
|
|
@@ -1633,6 +1751,22 @@ function mountViewer(host, given) {
|
|
|
1633
1751
|
}
|
|
1634
1752
|
/** The boxes a label can be shown in, as the words of an empty box. */
|
|
1635
1753
|
const TEXT_BOX = 'input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]), textarea';
|
|
1754
|
+
/** Nothing in it: no value, empty words, nothing picked. */
|
|
1755
|
+
const isBlank = (value) => value === null || value === undefined || value === '' || (Array.isArray(value) && !value.length);
|
|
1756
|
+
/**
|
|
1757
|
+
* A label inside a sentence, as "Add story points" says it: in English and French its first letter small, unless the
|
|
1758
|
+
* word is a name such as "VAT"; German nouns, and Arabic, as written.
|
|
1759
|
+
*/
|
|
1760
|
+
function inSentence(label, locale) {
|
|
1761
|
+
return (locale === 'en' || locale === 'fr') && /^\p{Lu}\p{Ll}/u.test(label) ? label[0].toLocaleLowerCase(locale) + label.slice(1) : label;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* What an empty field in the quiet look says, by its label, in the viewer's
|
|
1765
|
+
* words for `locale`: "Add story points", or for a question "Your answer".
|
|
1766
|
+
*/
|
|
1767
|
+
function quietHint(label, locale, labels = labels_1.VIEWER_LABELS[locale]) {
|
|
1768
|
+
return /[?؟]\s*$/.test(label) ? labels.quietAnswer : labels.quietHint.replace('{label}', inSentence(label, locale));
|
|
1769
|
+
}
|
|
1636
1770
|
/** Fields whose ✓ would say nothing: a yes/no box is never wrong, a table or a file has its own look. */
|
|
1637
1771
|
const NO_VALID_MARK = new Set(['boolean', 'one2many', 'binary', 'image', 'html', 'json', 'properties']);
|
|
1638
1772
|
/**
|