@fieldia/viewer 0.19.1 → 0.20.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 +18 -0
- package/bundle/fieldia.ar.js +2 -2
- package/bundle/fieldia.ar.js.map +2 -2
- package/bundle/fieldia.choice.js +5 -0
- package/bundle/fieldia.choice.js.map +7 -0
- package/bundle/fieldia.de.js +2 -2
- package/bundle/fieldia.de.js.map +2 -2
- package/bundle/fieldia.filters.js +5 -0
- package/bundle/fieldia.filters.js.map +7 -0
- package/bundle/fieldia.fr.js +2 -2
- package/bundle/fieldia.fr.js.map +2 -2
- package/bundle/fieldia.head.js +5 -0
- package/bundle/fieldia.head.js.map +7 -0
- package/bundle/fieldia.js +44 -44
- package/bundle/fieldia.js.map +4 -4
- package/bundle/fieldia.path.js +5 -0
- package/bundle/fieldia.path.js.map +7 -0
- package/bundle/fieldia.text.js +24 -0
- package/bundle/fieldia.text.js.map +7 -0
- package/package.json +7 -3
- package/src/index.d.mts +2 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +6 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +1 -0
- package/src/index.mjs.map +1 -1
- package/src/lib/button-parts.d.mts +30 -0
- package/src/lib/button-parts.d.ts +30 -0
- package/src/lib/button-parts.js +24 -0
- package/src/lib/button-parts.js.map +1 -0
- package/src/lib/button-parts.mjs +18 -0
- package/src/lib/button-parts.mjs.map +1 -0
- package/src/lib/dialog-window.d.mts +11 -0
- package/src/lib/dialog-window.d.ts +11 -0
- package/src/lib/dialog-window.js +86 -0
- package/src/lib/dialog-window.js.map +1 -0
- package/src/lib/dialog-window.mjs +82 -0
- package/src/lib/dialog-window.mjs.map +1 -0
- package/src/lib/dialog.d.mts +14 -3
- package/src/lib/dialog.d.ts +14 -3
- package/src/lib/dialog.js +84 -14
- package/src/lib/dialog.js.map +1 -1
- package/src/lib/dialog.mjs +85 -15
- package/src/lib/dialog.mjs.map +1 -1
- package/src/lib/filter-bar.d.mts +39 -0
- package/src/lib/filter-bar.d.ts +39 -0
- package/src/lib/filter-bar.js +290 -0
- package/src/lib/filter-bar.js.map +1 -0
- package/src/lib/filter-bar.mjs +287 -0
- package/src/lib/filter-bar.mjs.map +1 -0
- package/src/lib/filter-styles.d.mts +4 -0
- package/src/lib/filter-styles.d.ts +4 -0
- package/src/lib/filter-styles.js +63 -0
- package/src/lib/filter-styles.js.map +1 -0
- package/src/lib/filter-styles.mjs +59 -0
- package/src/lib/filter-styles.mjs.map +1 -0
- package/src/lib/filters-part.d.mts +8 -0
- package/src/lib/filters-part.d.ts +8 -0
- package/src/lib/filters-part.js +14 -0
- package/src/lib/filters-part.js.map +1 -0
- package/src/lib/filters-part.mjs +9 -0
- package/src/lib/filters-part.mjs.map +1 -0
- package/src/lib/group-styles.d.mts +9 -0
- package/src/lib/group-styles.d.ts +9 -0
- package/src/lib/group-styles.js +64 -0
- package/src/lib/group-styles.js.map +1 -0
- package/src/lib/group-styles.mjs +60 -0
- package/src/lib/group-styles.mjs.map +1 -0
- package/src/lib/head-part.d.mts +15 -0
- package/src/lib/head-part.d.ts +15 -0
- package/src/lib/head-part.js +26 -0
- package/src/lib/head-part.js.map +1 -0
- package/src/lib/head-part.mjs +16 -0
- package/src/lib/head-part.mjs.map +1 -0
- package/src/lib/head-styles.d.mts +28 -0
- package/src/lib/head-styles.d.ts +28 -0
- package/src/lib/head-styles.js +320 -0
- package/src/lib/head-styles.js.map +1 -0
- package/src/lib/head-styles.mjs +316 -0
- package/src/lib/head-styles.mjs.map +1 -0
- package/src/lib/labels.d.mts +29 -0
- package/src/lib/labels.d.ts +29 -0
- package/src/lib/labels.js +16 -0
- package/src/lib/labels.js.map +1 -1
- package/src/lib/labels.mjs +16 -0
- package/src/lib/labels.mjs.map +1 -1
- package/src/lib/list-styles.d.mts +1 -1
- package/src/lib/list-styles.d.ts +1 -1
- package/src/lib/list-styles.js +88 -1
- package/src/lib/list-styles.js.map +1 -1
- package/src/lib/list-styles.mjs +88 -1
- package/src/lib/list-styles.mjs.map +1 -1
- package/src/lib/list.d.mts +16 -1
- package/src/lib/list.d.ts +16 -1
- package/src/lib/list.js +281 -51
- package/src/lib/list.js.map +1 -1
- package/src/lib/list.mjs +282 -52
- package/src/lib/list.mjs.map +1 -1
- package/src/lib/locales/ar.js +16 -0
- package/src/lib/locales/ar.js.map +1 -1
- package/src/lib/locales/ar.mjs +16 -0
- package/src/lib/locales/ar.mjs.map +1 -1
- package/src/lib/locales/de.js +16 -0
- package/src/lib/locales/de.js.map +1 -1
- package/src/lib/locales/de.mjs +16 -0
- package/src/lib/locales/de.mjs.map +1 -1
- package/src/lib/locales/fr.js +16 -0
- package/src/lib/locales/fr.js.map +1 -1
- package/src/lib/locales/fr.mjs +16 -0
- package/src/lib/locales/fr.mjs.map +1 -1
- package/src/lib/look.d.mts +1 -1
- package/src/lib/look.d.ts +1 -1
- package/src/lib/look.js +3 -0
- package/src/lib/look.js.map +1 -1
- package/src/lib/look.mjs +3 -0
- package/src/lib/look.mjs.map +1 -1
- package/src/lib/record-bar.d.mts +10 -1
- package/src/lib/record-bar.d.ts +10 -1
- package/src/lib/record-bar.js +15 -7
- package/src/lib/record-bar.js.map +1 -1
- package/src/lib/record-bar.mjs +14 -7
- package/src/lib/record-bar.mjs.map +1 -1
- package/src/lib/record-head.d.mts +58 -0
- package/src/lib/record-head.d.ts +58 -0
- package/src/lib/record-head.js +245 -0
- package/src/lib/record-head.js.map +1 -0
- package/src/lib/record-head.mjs +240 -0
- package/src/lib/record-head.mjs.map +1 -0
- package/src/lib/rich-button.d.mts +12 -0
- package/src/lib/rich-button.d.ts +12 -0
- package/src/lib/rich-button.js +186 -0
- package/src/lib/rich-button.js.map +1 -0
- package/src/lib/rich-button.mjs +183 -0
- package/src/lib/rich-button.mjs.map +1 -0
- package/src/lib/section-head.d.mts +26 -0
- package/src/lib/section-head.d.ts +26 -0
- package/src/lib/section-head.js +65 -0
- package/src/lib/section-head.js.map +1 -0
- package/src/lib/section-head.mjs +56 -0
- package/src/lib/section-head.mjs.map +1 -0
- package/src/lib/section-tools.d.mts +12 -0
- package/src/lib/section-tools.d.ts +12 -0
- package/src/lib/section-tools.js +124 -0
- package/src/lib/section-tools.js.map +1 -0
- package/src/lib/section-tools.mjs +121 -0
- package/src/lib/section-tools.mjs.map +1 -0
- package/src/lib/viewer-parts.d.mts +33 -0
- package/src/lib/viewer-parts.d.ts +33 -0
- package/src/lib/viewer-parts.js +75 -0
- package/src/lib/viewer-parts.js.map +1 -0
- package/src/lib/viewer-parts.mjs +70 -0
- package/src/lib/viewer-parts.mjs.map +1 -0
- package/src/lib/viewer.d.mts +30 -7
- package/src/lib/viewer.d.ts +30 -7
- package/src/lib/viewer.js +317 -97
- package/src/lib/viewer.js.map +1 -1
- package/src/lib/viewer.mjs +319 -99
- package/src/lib/viewer.mjs.map +1 -1
package/src/lib/viewer.js
CHANGED
|
@@ -18,7 +18,10 @@ const value_words_1 = require("./value-words");
|
|
|
18
18
|
const hotkeys_1 = require("./hotkeys");
|
|
19
19
|
const record_bar_1 = require("./record-bar");
|
|
20
20
|
const attachment_preview_1 = require("./attachment-preview");
|
|
21
|
+
const section_head_1 = require("./section-head");
|
|
21
22
|
const record_styles_1 = require("./record-styles");
|
|
23
|
+
const button_parts_1 = require("./button-parts");
|
|
24
|
+
const viewer_parts_1 = require("./viewer-parts");
|
|
22
25
|
const labels_1 = require("./labels");
|
|
23
26
|
const focus_trap_1 = require("./focus-trap");
|
|
24
27
|
var labels_2 = require("./labels");
|
|
@@ -118,6 +121,31 @@ function mountViewer(host, given) {
|
|
|
118
121
|
const words = el('bdi', {}, text);
|
|
119
122
|
return icon ? [icon, words] : [words];
|
|
120
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A tab's face: its icon and words, or its icon alone (its words then its
|
|
126
|
+
* name and tooltip), and the pill of its count after them.
|
|
127
|
+
*/
|
|
128
|
+
function tabFace(tab) {
|
|
129
|
+
const pill = tab.count !== undefined ? [el('span', { class: 'fd-tab-count', 'aria-hidden': 'true' }, '0')] : [];
|
|
130
|
+
if (!tab.iconOnly)
|
|
131
|
+
return [...withIcon(tab.icon, tab.label), ...pill];
|
|
132
|
+
const icon = (0, widgets_1.drawIcon)(doc, tab.icon, options.icons);
|
|
133
|
+
return [...(icon ? [icon] : []), ...pill];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A label's words with its access key's letter underlined, as a work item's
|
|
137
|
+
* "Stat_e_": the first of that letter in them, whatever its case. Its words
|
|
138
|
+
* stay one piece for whatever reads them; without a key, the words alone.
|
|
139
|
+
*/
|
|
140
|
+
function accessKeyed(text, key) {
|
|
141
|
+
const words = keepMarkWithWord(text);
|
|
142
|
+
const at = key ? words.toLocaleLowerCase().indexOf(key) : -1;
|
|
143
|
+
if (at === -1)
|
|
144
|
+
return [words];
|
|
145
|
+
return [el('span', { class: 'fd-label-words' }, words.slice(0, at), el('span', { class: 'fd-access-key' }, words.slice(at, at + 1)), words.slice(at + 1))];
|
|
146
|
+
}
|
|
147
|
+
/** What a group's heading and a help box draw with. */
|
|
148
|
+
const headKit = { doc, labels: widgetLabels, icons: options.icons, withIcon, el };
|
|
121
149
|
const theme = options.theme ?? page.look?.theme;
|
|
122
150
|
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 });
|
|
123
151
|
(0, look_1.applyLook)(root, theme ? { ...page.look, theme } : page.look);
|
|
@@ -154,15 +182,21 @@ function mountViewer(host, given) {
|
|
|
154
182
|
// ---- the parts -------------------------------------------------------
|
|
155
183
|
/** The fields that take the focus as the record opens, in the order they are drawn. */
|
|
156
184
|
const focusers = [];
|
|
185
|
+
/** The fields with an access key, in the order they are drawn: Alt with the key focuses the first shown. */
|
|
186
|
+
const fieldKeys = [];
|
|
157
187
|
/** Each field's face, put on again when the form changes skin: quiet, or in its box. */
|
|
158
188
|
const faces = [];
|
|
159
189
|
/** Folded sections, and how to open each: a problem inside one has to be seen. */
|
|
160
190
|
const folds = new Map();
|
|
161
191
|
/** Every part drawn again from the state of the page shown. */
|
|
162
192
|
const renderNow = () => render(form.getState());
|
|
193
|
+
/** The record's head being drawn: tabs placed in it go to its band. */
|
|
194
|
+
let headNow = null;
|
|
163
195
|
/** Set once the viewer is on the page; set again when it is taken away. */
|
|
164
196
|
let mounted = false;
|
|
165
197
|
let gone = false;
|
|
198
|
+
/** Whether the form has opened a record yet (its `open` event). */
|
|
199
|
+
let opened = false;
|
|
166
200
|
/** The columns a part spans in the grid it sits in; the stylesheet keeps it within the columns there at each width. */
|
|
167
201
|
function spans(element, colspan) {
|
|
168
202
|
if (colspan)
|
|
@@ -191,7 +225,7 @@ function mountViewer(host, given) {
|
|
|
191
225
|
if (node.colspan)
|
|
192
226
|
wrapper.style.setProperty('--fd-span', String(node.colspan));
|
|
193
227
|
const labelText = node.label ?? def.label;
|
|
194
|
-
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id },
|
|
228
|
+
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id }, ...accessKeyed(labelText, node.hotkey));
|
|
195
229
|
// The ✓ of a field filled in right; never on a yes/no box, a table or a file, where it would say nothing.
|
|
196
230
|
const mark = options.showValid && !NO_VALID_MARK.has(def.type) ? (0, widgets_1.drawIcon)(doc, 'check') : null;
|
|
197
231
|
if (mark) {
|
|
@@ -199,9 +233,17 @@ function mountViewer(host, given) {
|
|
|
199
233
|
label.append(mark);
|
|
200
234
|
}
|
|
201
235
|
const widget = (0, widgets_1.createWidget)({ form, name: node.field, field: def, node, id, document: doc, labels: widgetLabels, preferences, locale, dialogs }, options.widgets);
|
|
236
|
+
// Its own tools (a Markdown text's Edit and Done), for a group that holds it alone to put in its heading.
|
|
237
|
+
(0, section_head_1.keepTools)(wrapper, widget);
|
|
202
238
|
// The field that takes the focus as the record opens (Flectra's default_focus): the page's own, not a saved form's inside it.
|
|
203
239
|
if (node.focus && scope.root)
|
|
204
240
|
focusers.push({ wrapper, focus: () => widget.focus() });
|
|
241
|
+
// Its access key: Alt with its letter takes the focus to it, as the letter underlined in its label says.
|
|
242
|
+
if (node.hotkey) {
|
|
243
|
+
fieldKeys.push({ key: node.hotkey, wrapper, focus: () => widget.focus() });
|
|
244
|
+
const control = widget.element.matches(CONTROL) ? widget.element : widget.element.querySelector(CONTROL);
|
|
245
|
+
control?.setAttribute('aria-keyshortcuts', (0, core_1.hotkeyWords)(node.hotkey));
|
|
246
|
+
}
|
|
205
247
|
// A label kept out of sight still names the box; the empty box shows it instead, unless the page gives it words of its own.
|
|
206
248
|
const textBox = widget.element.matches(TEXT_BOX) ? widget.element : widget.element.querySelector(TEXT_BOX);
|
|
207
249
|
if (labelsAt === 'hidden' && !node.placeholder)
|
|
@@ -217,10 +259,11 @@ function mountViewer(host, given) {
|
|
|
217
259
|
// Empty and quiet, it says what to add, unless the page gives it words of its own: "Add story points", or for a
|
|
218
260
|
// question "Your answer". A field of several boxes, each with its own label (an address), says it in none of them.
|
|
219
261
|
const ownWords = !!node.placeholder || labelsAt === 'hidden' || def.type === 'json' || def.type === 'properties';
|
|
220
|
-
|
|
262
|
+
// Unless the page says empty quiet fields show nothing at rest (quietHints false): then its label alone says what it is.
|
|
263
|
+
const hint = page.look?.quietHints === false ? null : quietHint(labelText, locale, labels);
|
|
221
264
|
const restWords = () => (!ownWords && wrapper.hasAttribute('data-quiet') ? hint : drawnPlaceholder);
|
|
222
265
|
// 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);
|
|
266
|
+
const blankWords = node.placeholder || (labelsAt === 'hidden' ? labelText : (hint ?? ''));
|
|
224
267
|
const quietSelect = widget.element instanceof HTMLSelectElement ? widget.element.querySelector(':scope > option[value=""]') : null;
|
|
225
268
|
const richText = widget.element.matches('.fd-richtext') ? widget.element : widget.element.querySelector(':scope > .fd-richtext');
|
|
226
269
|
const stateWords = widget.element.querySelector(':scope > .fd-dot-label');
|
|
@@ -284,7 +327,12 @@ function mountViewer(host, given) {
|
|
|
284
327
|
const icon = (0, widgets_1.drawIcon)(doc, 'lock', options.icons) ?? '';
|
|
285
328
|
lock = reason ? bubbleTip(`${id}-lock`, reason, fill(labels.locked, { label: labelText }), 'fd-lock', icon) : null;
|
|
286
329
|
lockMark = lock?.element ?? el('span', { class: 'fd-lock-wrap' }, el('span', { class: 'fd-lock', 'aria-hidden': 'true' }, icon));
|
|
287
|
-
|
|
330
|
+
// At the value's start, as a work item's Reason: in a record head's band, in the room before the value.
|
|
331
|
+
if (page.look?.lockAt === 'value') {
|
|
332
|
+
widget.element.before(lockMark);
|
|
333
|
+
wrapper.setAttribute('data-lock-at', 'value');
|
|
334
|
+
}
|
|
335
|
+
else if (labelsAt === 'hidden')
|
|
288
336
|
widget.element.after(lockMark);
|
|
289
337
|
else
|
|
290
338
|
label.insertBefore(lockMark, tip && labelsAt !== 'after' ? tip.element : null);
|
|
@@ -329,6 +377,11 @@ function mountViewer(host, given) {
|
|
|
329
377
|
if (!wrapper.contains(event.relatedTarget))
|
|
330
378
|
entered = form.getState().values[node.field];
|
|
331
379
|
});
|
|
380
|
+
// Entered before the record came (a dialog takes the focus at once): what it held is what the record brought.
|
|
381
|
+
cleanups.push(form.on('open', ({ values }) => {
|
|
382
|
+
if (wrapper.contains(doc.activeElement))
|
|
383
|
+
entered = values[node.field];
|
|
384
|
+
}));
|
|
332
385
|
wrapper.addEventListener('keydown', (event) => {
|
|
333
386
|
if (event.key !== 'Escape' || event.defaultPrevented || !wrapper.hasAttribute('data-quiet'))
|
|
334
387
|
return;
|
|
@@ -452,25 +505,38 @@ function mountViewer(host, given) {
|
|
|
452
505
|
watch(() => (0, dom_1.setHidden)(element, form.node(id).invisible));
|
|
453
506
|
}
|
|
454
507
|
function buttonItem(node) {
|
|
508
|
+
// Its icon alone, a toggle, a menu beside it, a look that follows the record's changes: drawn by button-parts.ts.
|
|
509
|
+
const head = (0, button_parts_1.isRichButton)(node) ? viewer_parts_1.headPart.loaded : undefined;
|
|
510
|
+
if (head) {
|
|
511
|
+
const rich = head.richButton({ doc, el, form, labels, icons: options.icons, uid, withIcon, press, say: (message, tone) => actionHost.say(message, tone), watch, cleanups }, node);
|
|
512
|
+
spans(rich, node.colspan);
|
|
513
|
+
return hiddenOn(node, rich);
|
|
514
|
+
}
|
|
455
515
|
const button = el('button', { type: 'button', class: `fd-button fd-button-${node.style ?? 'secondary'}`, 'data-node': node.id }, ...withIcon(node.icon, node.label));
|
|
456
516
|
if (node.hotkey)
|
|
457
517
|
(0, hotkeys_1.hotkeyOn)(button, node.hotkey, node.label);
|
|
458
518
|
spans(button, node.colspan);
|
|
459
|
-
// Its confirmation, then its steps: the form asks, through the viewer.
|
|
460
|
-
button.addEventListener('click', () =>
|
|
519
|
+
// Its confirmation, then its steps: the form asks, through the viewer. Undo with nothing to put back says so.
|
|
520
|
+
button.addEventListener('click', () => {
|
|
521
|
+
if (!(0, button_parts_1.nothingToUndo)(node, form, (message, tone) => actionHost.say(message, tone), labels))
|
|
522
|
+
void press(button, () => form.runAction(node.id));
|
|
523
|
+
});
|
|
461
524
|
hideWhen(button, node.id);
|
|
462
525
|
// A header's button is drawn here too, never through item(): hidden at its widths the same way.
|
|
463
526
|
return hiddenOn(node, button);
|
|
464
527
|
}
|
|
465
528
|
function textItem(node, inline = false) {
|
|
466
529
|
const style = node.style ?? 'paragraph';
|
|
467
|
-
// Its words, with the values of the fields they name.
|
|
530
|
+
// Its words, with the values of the fields they name, and its [words](address) as links.
|
|
468
531
|
const words = (0, value_words_1.valueWords)(doc, page.fields, node.text, locale);
|
|
532
|
+
const shown = (0, section_head_1.wordsWithLinks)(doc, words.nodes);
|
|
469
533
|
const element = inline
|
|
470
|
-
? el('span', { class: `fd-oneline-words fd-text-${style}`, 'data-node': node.id }, ...
|
|
534
|
+
? el('span', { class: `fd-oneline-words fd-text-${style}`, 'data-node': node.id }, ...shown)
|
|
471
535
|
: style === 'alert'
|
|
472
|
-
? el('div', { class: `fd-alert fd-text-alert fd-tone-${node.tone ?? 'info'}`, role: 'status', 'data-node': node.id }, el('span', { class: 'fd-alert-message' }, ...
|
|
473
|
-
:
|
|
536
|
+
? el('div', { class: `fd-alert fd-text-alert fd-tone-${node.tone ?? 'info'}`, role: 'status', 'data-node': node.id }, el('span', { class: 'fd-alert-message' }, ...shown))
|
|
537
|
+
: style === 'help'
|
|
538
|
+
? (0, section_head_1.helpBox)(headKit, node, shown)
|
|
539
|
+
: el(style === 'heading' ? 'h3' : 'p', { class: `fd-text-${style}`, 'data-node': node.id }, ...shown);
|
|
474
540
|
spans(element, node.colspan);
|
|
475
541
|
hideWhen(element, node.id);
|
|
476
542
|
watch((state) => words.update(state.values));
|
|
@@ -571,7 +637,18 @@ function mountViewer(host, given) {
|
|
|
571
637
|
section.style.setProperty('--fd-label-width', `${node.labelWidth}px`);
|
|
572
638
|
const description = node.description ? el('p', { class: 'fd-section-description' }, node.description) : null;
|
|
573
639
|
const content = grid(node.children, plan.at === 'tracks' ? null : node.columns, plan.inner);
|
|
574
|
-
|
|
640
|
+
// Its own column widths and gap; a heading with tools at its end, as a work item's group has.
|
|
641
|
+
(0, section_head_1.applyTracks)(content, node);
|
|
642
|
+
const own = (0, section_head_1.loneFieldTools)(node, content);
|
|
643
|
+
const tools = node.title && ((0, section_head_1.workHeading)(node) || own.length) ? viewer_parts_1.headPart.loaded : undefined;
|
|
644
|
+
if (tools) {
|
|
645
|
+
content.id = uid(`${node.id}-content`);
|
|
646
|
+
const head = tools.sectionHeading(headKit, node, section, content, description, own);
|
|
647
|
+
if (node.collapsible)
|
|
648
|
+
folds.set(section, () => head.show(true));
|
|
649
|
+
section.append(head.legend);
|
|
650
|
+
}
|
|
651
|
+
else if (node.title && node.collapsible) {
|
|
575
652
|
content.id = uid(`${node.id}-content`);
|
|
576
653
|
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));
|
|
577
654
|
let open = node.collapsed !== true;
|
|
@@ -614,7 +691,12 @@ function mountViewer(host, given) {
|
|
|
614
691
|
const parts = node.children.map((tab) => {
|
|
615
692
|
const tabId = uid(`${tab.id}-tab`);
|
|
616
693
|
const panelId = uid(`${tab.id}-panel`);
|
|
617
|
-
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, ...
|
|
694
|
+
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, ...tabFace(tab));
|
|
695
|
+
if (tab.iconOnly) {
|
|
696
|
+
button.classList.add('fd-tab-icon-only');
|
|
697
|
+
button.setAttribute('aria-label', tab.label);
|
|
698
|
+
button.title = tab.label;
|
|
699
|
+
}
|
|
618
700
|
const panel = el('div', { class: 'fd-tabpanel', role: 'tabpanel', id: panelId, 'aria-labelledby': tabId });
|
|
619
701
|
/**
|
|
620
702
|
* A tab's parts are drawn when it is first shown, and kept up to date
|
|
@@ -664,12 +746,36 @@ function mountViewer(host, given) {
|
|
|
664
746
|
// Too long for its room, the strip scrolls; the tab chosen, however, comes into sight in it.
|
|
665
747
|
const strip = (0, tab_strip_1.tabStrip)(list, el);
|
|
666
748
|
cleanups.push(strip.destroy);
|
|
667
|
-
|
|
749
|
+
// Placed in a record's head: the tabs at the end of its band, the panels the sheet's body.
|
|
750
|
+
const inHead = node.place === 'head' && headNow && scope.root ? headNow : null;
|
|
751
|
+
if (inHead) {
|
|
752
|
+
box.classList.add('fd-tabs-in-head');
|
|
753
|
+
inHead.tabs.append(strip.element);
|
|
754
|
+
box.append(...parts.map((p) => p.panel));
|
|
755
|
+
}
|
|
756
|
+
else
|
|
757
|
+
box.append(strip.element, ...parts.map((p) => p.panel));
|
|
758
|
+
// A tab's count in its pill, and in its name: "Links, 0".
|
|
759
|
+
for (const p of parts) {
|
|
760
|
+
const count = p.tab.count;
|
|
761
|
+
if (count === undefined)
|
|
762
|
+
continue;
|
|
763
|
+
const pill = p.button.querySelector('.fd-tab-count');
|
|
764
|
+
watch((state) => {
|
|
765
|
+
const value = state.values[count];
|
|
766
|
+
const n = Array.isArray(value) ? value.length : typeof value === 'number' ? value : Number(value ?? 0) || 0;
|
|
767
|
+
(0, dom_1.setText)(pill, String(n));
|
|
768
|
+
(0, dom_1.setAttr)(p.button, 'aria-label', fill(labels.countOf, { label: p.tab.label, count: n }));
|
|
769
|
+
});
|
|
770
|
+
}
|
|
668
771
|
let shownKey = '';
|
|
669
772
|
let revealed = '';
|
|
670
773
|
watch((state) => {
|
|
671
774
|
const tabsHidden = form.node(node.id).invisible;
|
|
672
775
|
(0, dom_1.setHidden)(box, tabsHidden);
|
|
776
|
+
// In the head, its strip is apart from its box: hidden with it.
|
|
777
|
+
if (inHead)
|
|
778
|
+
(0, dom_1.setHidden)(strip.element, tabsHidden);
|
|
673
779
|
const shown = parts.filter((p) => !form.node(p.tab.id).invisible);
|
|
674
780
|
const keep = picked !== null && shown.some((p) => p.tab.id === picked);
|
|
675
781
|
if (keep)
|
|
@@ -1225,6 +1331,10 @@ function mountViewer(host, given) {
|
|
|
1225
1331
|
return box;
|
|
1226
1332
|
}
|
|
1227
1333
|
function sheetLayout(node) {
|
|
1334
|
+
// A head comes in the head part, waited for before the page is drawn; where it could not come, the sheet is drawn plainly.
|
|
1335
|
+
const part = node.head ? viewer_parts_1.headPart.loaded : undefined;
|
|
1336
|
+
if (part)
|
|
1337
|
+
return headedLayout(node, part);
|
|
1228
1338
|
const pageBox = el('div', { class: 'fd-sheet-page', 'data-node': node.id });
|
|
1229
1339
|
const actions = el('div', { class: 'fd-actions' });
|
|
1230
1340
|
const save = el('button', { type: 'submit', class: 'fd-button fd-button-primary' }, labels.save);
|
|
@@ -1320,41 +1430,7 @@ function mountViewer(host, given) {
|
|
|
1320
1430
|
});
|
|
1321
1431
|
card.append(stats);
|
|
1322
1432
|
}
|
|
1323
|
-
|
|
1324
|
-
// Its words with the values they name, or a field's words while it holds any; its buttons after them, as links unless styled.
|
|
1325
|
-
const words = (0, value_words_1.valueWords)(doc, page.fields, alert.message, locale);
|
|
1326
|
-
const message = el('span', { class: 'fd-alert-message' }, ...words.nodes);
|
|
1327
|
-
let fromField = false;
|
|
1328
|
-
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, message);
|
|
1329
|
-
if (alert.buttons?.length)
|
|
1330
|
-
box.append(el('span', { class: 'fd-alert-actions' }, ...alert.buttons.map((button) => buttonItem({ ...button, style: button.style ?? 'link' }))));
|
|
1331
|
-
updaters.push((state) => {
|
|
1332
|
-
words.update(state.values);
|
|
1333
|
-
const from = alert.messageField ? state.values[alert.messageField] : null;
|
|
1334
|
-
const said = typeof from === 'string' ? from.trim() : '';
|
|
1335
|
-
if (said)
|
|
1336
|
-
(0, dom_1.setText)(message, said);
|
|
1337
|
-
else if (fromField)
|
|
1338
|
-
message.replaceChildren(...words.nodes);
|
|
1339
|
-
fromField = !!said;
|
|
1340
|
-
});
|
|
1341
|
-
if (alert.dismissible) {
|
|
1342
|
-
// Closed, it stays closed while the page is open, whatever its condition does.
|
|
1343
|
-
let dismissed = false;
|
|
1344
|
-
const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': labels.dismiss }, '×');
|
|
1345
|
-
close.addEventListener('click', () => {
|
|
1346
|
-
dismissed = true;
|
|
1347
|
-
box.hidden = true;
|
|
1348
|
-
});
|
|
1349
|
-
box.append(close);
|
|
1350
|
-
updaters.push(() => {
|
|
1351
|
-
box.hidden = dismissed || form.node(alert.id).invisible;
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
else
|
|
1355
|
-
hideWhen(box, alert.id);
|
|
1356
|
-
card.append(box);
|
|
1357
|
-
}
|
|
1433
|
+
card.append(...alertBoxes(node));
|
|
1358
1434
|
if (node.badges?.length) {
|
|
1359
1435
|
const badges = el('div', { class: 'fd-badges' });
|
|
1360
1436
|
for (const badge of node.badges) {
|
|
@@ -1431,6 +1507,95 @@ function mountViewer(host, given) {
|
|
|
1431
1507
|
pageBox.append(...(bar ? [bar] : []), header, layout);
|
|
1432
1508
|
return pageBox;
|
|
1433
1509
|
}
|
|
1510
|
+
/** The sheet's alerts: its words with the values they name, or a field's words while it holds any; its buttons after them, as links unless styled. */
|
|
1511
|
+
function alertBoxes(node) {
|
|
1512
|
+
return (node.alerts ?? []).map((alert) => {
|
|
1513
|
+
const words = (0, value_words_1.valueWords)(doc, page.fields, alert.message, locale);
|
|
1514
|
+
const message = el('span', { class: 'fd-alert-message' }, ...words.nodes);
|
|
1515
|
+
let fromField = false;
|
|
1516
|
+
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, message);
|
|
1517
|
+
if (alert.buttons?.length)
|
|
1518
|
+
box.append(el('span', { class: 'fd-alert-actions' }, ...alert.buttons.map((button) => buttonItem({ ...button, style: button.style ?? 'link' }))));
|
|
1519
|
+
updaters.push((state) => {
|
|
1520
|
+
words.update(state.values);
|
|
1521
|
+
const from = alert.messageField ? state.values[alert.messageField] : null;
|
|
1522
|
+
const said = typeof from === 'string' ? from.trim() : '';
|
|
1523
|
+
if (said)
|
|
1524
|
+
(0, dom_1.setText)(message, said);
|
|
1525
|
+
else if (fromField)
|
|
1526
|
+
message.replaceChildren(...words.nodes);
|
|
1527
|
+
fromField = !!said;
|
|
1528
|
+
});
|
|
1529
|
+
if (alert.dismissible) {
|
|
1530
|
+
// Closed, it stays closed while the page is open, whatever its condition does.
|
|
1531
|
+
let dismissed = false;
|
|
1532
|
+
const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': labels.dismiss }, '×');
|
|
1533
|
+
close.addEventListener('click', () => {
|
|
1534
|
+
dismissed = true;
|
|
1535
|
+
box.hidden = true;
|
|
1536
|
+
});
|
|
1537
|
+
box.append(close);
|
|
1538
|
+
updaters.push(() => {
|
|
1539
|
+
box.hidden = dismissed || form.node(alert.id).invisible;
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
else
|
|
1543
|
+
hideWhen(box, alert.id);
|
|
1544
|
+
return box;
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* A record with a head (`SheetHead`), as a work item: the head — its kind,
|
|
1549
|
+
* title, row, buttons and band, the tabs placed in it — then the body, which
|
|
1550
|
+
* holds its alerts and its parts (a tab's panels, when its tabs are in the
|
|
1551
|
+
* head). The record's breadcrumbs and pager, when the app gives them, over
|
|
1552
|
+
* it; its gear menu is the head's ⋮.
|
|
1553
|
+
*/
|
|
1554
|
+
function headedLayout(node, part) {
|
|
1555
|
+
const pageBox = el('div', { class: 'fd-sheet-page fd-head-page', 'data-node': node.id });
|
|
1556
|
+
const head = part.recordHead({
|
|
1557
|
+
doc,
|
|
1558
|
+
el,
|
|
1559
|
+
form,
|
|
1560
|
+
page,
|
|
1561
|
+
sheet: node,
|
|
1562
|
+
head: node.head,
|
|
1563
|
+
labels,
|
|
1564
|
+
locale,
|
|
1565
|
+
icons: options.icons,
|
|
1566
|
+
uid,
|
|
1567
|
+
fill,
|
|
1568
|
+
withIcon,
|
|
1569
|
+
press,
|
|
1570
|
+
say: (message, tone) => actionHost.say(message, tone),
|
|
1571
|
+
fieldItem,
|
|
1572
|
+
item,
|
|
1573
|
+
buttonItem,
|
|
1574
|
+
watch: (update) => updaters.push(update),
|
|
1575
|
+
cleanups,
|
|
1576
|
+
showTab: (id) => tabs.get(id)?.() ?? false,
|
|
1577
|
+
unfold(part) {
|
|
1578
|
+
for (let folded = part.closest('.fd-section-folded'); folded; folded = folded.parentElement?.closest('.fd-section-folded') ?? null)
|
|
1579
|
+
folds.get(folded)?.();
|
|
1580
|
+
},
|
|
1581
|
+
root: () => root,
|
|
1582
|
+
});
|
|
1583
|
+
// The parts drawn now: tabs placed in the head go to its band.
|
|
1584
|
+
headNow = head;
|
|
1585
|
+
let parts;
|
|
1586
|
+
try {
|
|
1587
|
+
parts = grid(node.children, 1, top(false));
|
|
1588
|
+
}
|
|
1589
|
+
finally {
|
|
1590
|
+
headNow = null;
|
|
1591
|
+
}
|
|
1592
|
+
const body = el('div', { class: 'fd-head-body' }, ...alertBoxes(node), parts);
|
|
1593
|
+
// The side panel beside the body on a wide page; a dialog leaves it to the full page.
|
|
1594
|
+
const side = node.sidePanel ? el('aside', { class: 'fd-side' }, slotItem(node.sidePanel)) : null;
|
|
1595
|
+
const bar = topBar(node.toolbar ? { ...node.toolbar, menu: undefined } : undefined);
|
|
1596
|
+
pageBox.append(...(bar ? [bar] : []), head.element, side ? el('div', { class: 'fd-head-layout' }, body, side) : body);
|
|
1597
|
+
return pageBox;
|
|
1598
|
+
}
|
|
1434
1599
|
/** The sheet header's statusbar: the same widget a form can place anywhere. */
|
|
1435
1600
|
function statusbar(config) {
|
|
1436
1601
|
const def = page.fields[config.field];
|
|
@@ -1453,42 +1618,76 @@ function mountViewer(host, given) {
|
|
|
1453
1618
|
}
|
|
1454
1619
|
let wizardForward = null;
|
|
1455
1620
|
const layout = page.layout;
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1621
|
+
/** A list page's way to ask for its records again (handle.reload). */
|
|
1622
|
+
let reloadList = null;
|
|
1623
|
+
/** And its way to open a record as its row's press does (handle.openRecord). */
|
|
1624
|
+
let openInList = null;
|
|
1625
|
+
/**
|
|
1626
|
+
* The page itself — its layout, head and bar — drawn whole: at once when every part it draws from is in, else once
|
|
1627
|
+
* they have come (or could not, when what they would have drawn is drawn plainly). Nothing of it is shown before, so
|
|
1628
|
+
* nothing moves as a part arrives, and no focus is lost: there was nothing to have it.
|
|
1629
|
+
*/
|
|
1630
|
+
function drawPage() {
|
|
1631
|
+
let body;
|
|
1632
|
+
if (layout.type === 'sheet')
|
|
1633
|
+
body = sheetLayout(layout);
|
|
1634
|
+
else if (layout.type === 'wizard')
|
|
1635
|
+
body = wizardLayout(layout);
|
|
1636
|
+
else if (layout.type === 'tabs')
|
|
1637
|
+
body = sectionsLayout({ id: `${layout.id}-page`, children: [layout] });
|
|
1638
|
+
else if (layout.type === 'list') {
|
|
1639
|
+
const list = (0, list_1.listView)({
|
|
1640
|
+
page,
|
|
1641
|
+
node: layout,
|
|
1642
|
+
form,
|
|
1643
|
+
dataSource: options.dataSource,
|
|
1644
|
+
doc,
|
|
1645
|
+
el,
|
|
1646
|
+
labels,
|
|
1647
|
+
locale,
|
|
1648
|
+
fill,
|
|
1649
|
+
press,
|
|
1650
|
+
withIcon,
|
|
1651
|
+
uid,
|
|
1652
|
+
icons: options.icons,
|
|
1653
|
+
preferences,
|
|
1654
|
+
onOpenRecord: options.onOpenRecord,
|
|
1655
|
+
...(options.listFilter ? { fixedFilter: options.listFilter } : {}),
|
|
1656
|
+
...(options.listViews ? { views: options.listViews } : {}),
|
|
1657
|
+
});
|
|
1658
|
+
cleanups.push(list.destroy);
|
|
1659
|
+
reloadList = list.reload;
|
|
1660
|
+
openInList = list.open;
|
|
1661
|
+
body = list.element;
|
|
1662
|
+
}
|
|
1663
|
+
else
|
|
1664
|
+
body = sectionsLayout(layout);
|
|
1665
|
+
const head = layout.type === 'sheet' ? null : pageHead();
|
|
1666
|
+
// A record of sections or tabs has the bar the app fills — its breadcrumbs and pager — over its title.
|
|
1667
|
+
const bar = layout.type === 'sections' || layout.type === 'tabs' ? topBar(undefined) : null;
|
|
1668
|
+
content.append(...(bar ? [bar] : []), ...(head ? [head] : []), banner, draft, body);
|
|
1669
|
+
pageDrawn = true;
|
|
1670
|
+
}
|
|
1671
|
+
let pageDrawn = false;
|
|
1672
|
+
/** What needs the page drawn — a list's rows to open or ask for again — at once when it is, else once it is. */
|
|
1673
|
+
const afterDrawing = (then) => (pageDrawn ? then() : drawn.then(then));
|
|
1674
|
+
const awaited = (0, viewer_parts_1.partsFor)(page, options.widgets, options.slots).filter((part) => !part.loaded);
|
|
1675
|
+
let drawn = Promise.resolve();
|
|
1676
|
+
if (!awaited.length)
|
|
1677
|
+
drawPage();
|
|
1678
|
+
else {
|
|
1679
|
+
root.setAttribute('aria-busy', 'true');
|
|
1680
|
+
drawn = (0, widgets_1.settleParts)(awaited).then(() => {
|
|
1681
|
+
if (gone)
|
|
1682
|
+
return;
|
|
1683
|
+
drawPage();
|
|
1684
|
+
root.removeAttribute('aria-busy');
|
|
1685
|
+
render(form.getState());
|
|
1686
|
+
// A record that opened meanwhile: its focused field takes the focus now, as it would have then.
|
|
1687
|
+
if (opened)
|
|
1688
|
+
focusAsOpened(true);
|
|
1482
1689
|
});
|
|
1483
|
-
cleanups.push(list.destroy);
|
|
1484
|
-
body = list.element;
|
|
1485
1690
|
}
|
|
1486
|
-
else
|
|
1487
|
-
body = sectionsLayout(layout);
|
|
1488
|
-
const head = layout.type === 'sheet' ? null : pageHead();
|
|
1489
|
-
// A record of sections or tabs has the bar the app fills — its breadcrumbs and pager — over its title.
|
|
1490
|
-
const bar = layout.type === 'sections' || layout.type === 'tabs' ? topBar(undefined) : null;
|
|
1491
|
-
content.append(...(bar ? [bar] : []), ...(head ? [head] : []), banner, draft, body);
|
|
1492
1691
|
root.append(content, done, announcer);
|
|
1493
1692
|
// The status leaves its place beside Save when the page wants it in a corner or across the top.
|
|
1494
1693
|
if (options.saveStatus === 'toast') {
|
|
@@ -1601,6 +1800,14 @@ function mountViewer(host, given) {
|
|
|
1601
1800
|
if (!key || !hotkeysHere(event.target))
|
|
1602
1801
|
return;
|
|
1603
1802
|
const button = [...root.querySelectorAll(`button[data-hotkey="${key}"]`)].find((b) => b.closest('.fd-form') === root && !b.closest('[hidden]') && !b.disabled && !b.hasAttribute('aria-busy'));
|
|
1803
|
+
// A field's access key: the first shown with it takes the focus.
|
|
1804
|
+
const field = button ? undefined : fieldKeys.find((one) => one.key === key && one.wrapper.isConnected && !one.wrapper.closest('[hidden]'));
|
|
1805
|
+
if (field) {
|
|
1806
|
+
event.preventDefault();
|
|
1807
|
+
root.removeAttribute('data-hotkeys');
|
|
1808
|
+
field.focus();
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1604
1811
|
if (!button)
|
|
1605
1812
|
return;
|
|
1606
1813
|
event.preventDefault();
|
|
@@ -1675,22 +1882,24 @@ function mountViewer(host, given) {
|
|
|
1675
1882
|
mounted = true;
|
|
1676
1883
|
// As the record opens, its focused field takes the focus — the first shown — unless the person is busy elsewhere:
|
|
1677
1884
|
// only while nothing has the focus, or, the first time, while it is in this form (a dialog's first box).
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
focusers.find((one) => !one.wrapper.closest('[hidden]'))?.focus();
|
|
1691
|
-
});
|
|
1692
|
-
}));
|
|
1885
|
+
function focusAsOpened(first) {
|
|
1886
|
+
if (!focusers.length)
|
|
1887
|
+
return;
|
|
1888
|
+
queueMicrotask(() => {
|
|
1889
|
+
if (gone || !root.isConnected || root.closest('[hidden]') || (typeof root.checkVisibility === 'function' && !root.checkVisibility()))
|
|
1890
|
+
return;
|
|
1891
|
+
const active = doc.activeElement;
|
|
1892
|
+
const free = !active || active === doc.body || active === doc.documentElement;
|
|
1893
|
+
if (!free && !(first && root.contains(active)))
|
|
1894
|
+
return;
|
|
1895
|
+
focusers.find((one) => !one.wrapper.closest('[hidden]'))?.focus();
|
|
1896
|
+
});
|
|
1693
1897
|
}
|
|
1898
|
+
cleanups.push(form.on('open', () => {
|
|
1899
|
+
const first = !opened;
|
|
1900
|
+
opened = true;
|
|
1901
|
+
focusAsOpened(first);
|
|
1902
|
+
}));
|
|
1694
1903
|
if (form.getState().status === 'idle')
|
|
1695
1904
|
void form.load();
|
|
1696
1905
|
return {
|
|
@@ -1731,6 +1940,14 @@ function mountViewer(host, given) {
|
|
|
1731
1940
|
render(form.getState());
|
|
1732
1941
|
},
|
|
1733
1942
|
isReadonly: () => locked,
|
|
1943
|
+
reload: () => afterDrawing(async () => {
|
|
1944
|
+
if (reloadList)
|
|
1945
|
+
return reloadList();
|
|
1946
|
+
await form.run([{ do: 'reload' }]);
|
|
1947
|
+
}),
|
|
1948
|
+
openRecord: (id, options) => afterDrawing(async () => {
|
|
1949
|
+
await openInList?.(id, options);
|
|
1950
|
+
}),
|
|
1734
1951
|
on: (event, listener) => {
|
|
1735
1952
|
const off = form.on(event, listener);
|
|
1736
1953
|
cleanups.push(off);
|
|
@@ -1738,6 +1955,7 @@ function mountViewer(host, given) {
|
|
|
1738
1955
|
},
|
|
1739
1956
|
setValues: (values) => form.setValues(values),
|
|
1740
1957
|
run: (steps, context) => form.run(steps, context),
|
|
1958
|
+
drawn,
|
|
1741
1959
|
destroy() {
|
|
1742
1960
|
gone = true;
|
|
1743
1961
|
unsubscribe();
|
|
@@ -1749,6 +1967,8 @@ function mountViewer(host, given) {
|
|
|
1749
1967
|
},
|
|
1750
1968
|
};
|
|
1751
1969
|
}
|
|
1970
|
+
/** What a field is worked with: where its access key is said. */
|
|
1971
|
+
const CONTROL = 'input:not([type="hidden"]), select, textarea, button, [contenteditable="true"], [tabindex="0"]';
|
|
1752
1972
|
/** The boxes a label can be shown in, as the words of an empty box. */
|
|
1753
1973
|
const TEXT_BOX = 'input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]), textarea';
|
|
1754
1974
|
/** Nothing in it: no value, empty words, nothing picked. */
|