@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createForm, checkPage, isRightToLeft, localizePage, translatePage, wideColumns, MESSAGES, } from '@fieldia/core';
|
|
2
|
-
import { browserPreferences, createWidget, displayValue, drawIcon, installStyles, readsAsText, readText, wearsQuiet, WIDGET_LABELS } from '@fieldia/widgets';
|
|
1
|
+
import { createForm, checkPage, isRightToLeft, localizePage, translatePage, wideColumns, MESSAGES, hotkeyWords, } from '@fieldia/core';
|
|
2
|
+
import { browserPreferences, createWidget, displayValue, drawIcon, installStyles, readsAsText, readText, settleParts, 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';
|
|
@@ -12,7 +12,10 @@ import { valueWords } from './value-words.mjs';
|
|
|
12
12
|
import { hotkeyOf, hotkeyOn } from './hotkeys.mjs';
|
|
13
13
|
import { recordBar } from './record-bar.mjs';
|
|
14
14
|
import { attachmentPreview } from './attachment-preview.mjs';
|
|
15
|
+
import { applyTracks, helpBox, keepTools, loneFieldTools, wordsWithLinks, workHeading } from './section-head.mjs';
|
|
15
16
|
import { installRecordStyles } from './record-styles.mjs';
|
|
17
|
+
import { isRichButton, nothingToUndo } from './button-parts.mjs';
|
|
18
|
+
import { headPart, partsFor } from './viewer-parts.mjs';
|
|
16
19
|
import { ownLocale, VIEWER_LABELS } from './labels.mjs';
|
|
17
20
|
import { keepTabIn } from './focus-trap.mjs';
|
|
18
21
|
export { VIEWER_LABELS, DEFAULT_LABELS } from './labels.mjs';
|
|
@@ -110,6 +113,31 @@ export function mountViewer(host, given) {
|
|
|
110
113
|
const words = el('bdi', {}, text);
|
|
111
114
|
return icon ? [icon, words] : [words];
|
|
112
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A tab's face: its icon and words, or its icon alone (its words then its
|
|
118
|
+
* name and tooltip), and the pill of its count after them.
|
|
119
|
+
*/
|
|
120
|
+
function tabFace(tab) {
|
|
121
|
+
const pill = tab.count !== undefined ? [el('span', { class: 'fd-tab-count', 'aria-hidden': 'true' }, '0')] : [];
|
|
122
|
+
if (!tab.iconOnly)
|
|
123
|
+
return [...withIcon(tab.icon, tab.label), ...pill];
|
|
124
|
+
const icon = drawIcon(doc, tab.icon, options.icons);
|
|
125
|
+
return [...(icon ? [icon] : []), ...pill];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A label's words with its access key's letter underlined, as a work item's
|
|
129
|
+
* "Stat_e_": the first of that letter in them, whatever its case. Its words
|
|
130
|
+
* stay one piece for whatever reads them; without a key, the words alone.
|
|
131
|
+
*/
|
|
132
|
+
function accessKeyed(text, key) {
|
|
133
|
+
const words = keepMarkWithWord(text);
|
|
134
|
+
const at = key ? words.toLocaleLowerCase().indexOf(key) : -1;
|
|
135
|
+
if (at === -1)
|
|
136
|
+
return [words];
|
|
137
|
+
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))];
|
|
138
|
+
}
|
|
139
|
+
/** What a group's heading and a help box draw with. */
|
|
140
|
+
const headKit = { doc, labels: widgetLabels, icons: options.icons, withIcon, el };
|
|
113
141
|
const theme = options.theme ?? page.look?.theme;
|
|
114
142
|
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
143
|
applyLook(root, theme ? { ...page.look, theme } : page.look);
|
|
@@ -146,15 +174,21 @@ export function mountViewer(host, given) {
|
|
|
146
174
|
// ---- the parts -------------------------------------------------------
|
|
147
175
|
/** The fields that take the focus as the record opens, in the order they are drawn. */
|
|
148
176
|
const focusers = [];
|
|
177
|
+
/** The fields with an access key, in the order they are drawn: Alt with the key focuses the first shown. */
|
|
178
|
+
const fieldKeys = [];
|
|
149
179
|
/** Each field's face, put on again when the form changes skin: quiet, or in its box. */
|
|
150
180
|
const faces = [];
|
|
151
181
|
/** Folded sections, and how to open each: a problem inside one has to be seen. */
|
|
152
182
|
const folds = new Map();
|
|
153
183
|
/** Every part drawn again from the state of the page shown. */
|
|
154
184
|
const renderNow = () => render(form.getState());
|
|
185
|
+
/** The record's head being drawn: tabs placed in it go to its band. */
|
|
186
|
+
let headNow = null;
|
|
155
187
|
/** Set once the viewer is on the page; set again when it is taken away. */
|
|
156
188
|
let mounted = false;
|
|
157
189
|
let gone = false;
|
|
190
|
+
/** Whether the form has opened a record yet (its `open` event). */
|
|
191
|
+
let opened = false;
|
|
158
192
|
/** The columns a part spans in the grid it sits in; the stylesheet keeps it within the columns there at each width. */
|
|
159
193
|
function spans(element, colspan) {
|
|
160
194
|
if (colspan)
|
|
@@ -183,7 +217,7 @@ export function mountViewer(host, given) {
|
|
|
183
217
|
if (node.colspan)
|
|
184
218
|
wrapper.style.setProperty('--fd-span', String(node.colspan));
|
|
185
219
|
const labelText = node.label ?? def.label;
|
|
186
|
-
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id },
|
|
220
|
+
const label = el('label', { class: 'fd-label', id: `${id}-label`, for: id }, ...accessKeyed(labelText, node.hotkey));
|
|
187
221
|
// The ✓ of a field filled in right; never on a yes/no box, a table or a file, where it would say nothing.
|
|
188
222
|
const mark = options.showValid && !NO_VALID_MARK.has(def.type) ? drawIcon(doc, 'check') : null;
|
|
189
223
|
if (mark) {
|
|
@@ -191,9 +225,17 @@ export function mountViewer(host, given) {
|
|
|
191
225
|
label.append(mark);
|
|
192
226
|
}
|
|
193
227
|
const widget = createWidget({ form, name: node.field, field: def, node, id, document: doc, labels: widgetLabels, preferences, locale, dialogs }, options.widgets);
|
|
228
|
+
// Its own tools (a Markdown text's Edit and Done), for a group that holds it alone to put in its heading.
|
|
229
|
+
keepTools(wrapper, widget);
|
|
194
230
|
// 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.
|
|
195
231
|
if (node.focus && scope.root)
|
|
196
232
|
focusers.push({ wrapper, focus: () => widget.focus() });
|
|
233
|
+
// Its access key: Alt with its letter takes the focus to it, as the letter underlined in its label says.
|
|
234
|
+
if (node.hotkey) {
|
|
235
|
+
fieldKeys.push({ key: node.hotkey, wrapper, focus: () => widget.focus() });
|
|
236
|
+
const control = widget.element.matches(CONTROL) ? widget.element : widget.element.querySelector(CONTROL);
|
|
237
|
+
control?.setAttribute('aria-keyshortcuts', hotkeyWords(node.hotkey));
|
|
238
|
+
}
|
|
197
239
|
// 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.
|
|
198
240
|
const textBox = widget.element.matches(TEXT_BOX) ? widget.element : widget.element.querySelector(TEXT_BOX);
|
|
199
241
|
if (labelsAt === 'hidden' && !node.placeholder)
|
|
@@ -209,10 +251,11 @@ export function mountViewer(host, given) {
|
|
|
209
251
|
// Empty and quiet, it says what to add, unless the page gives it words of its own: "Add story points", or for a
|
|
210
252
|
// question "Your answer". A field of several boxes, each with its own label (an address), says it in none of them.
|
|
211
253
|
const ownWords = !!node.placeholder || labelsAt === 'hidden' || def.type === 'json' || def.type === 'properties';
|
|
212
|
-
|
|
254
|
+
// Unless the page says empty quiet fields show nothing at rest (quietHints false): then its label alone says what it is.
|
|
255
|
+
const hint = page.look?.quietHints === false ? null : quietHint(labelText, locale, labels);
|
|
213
256
|
const restWords = () => (!ownWords && wrapper.hasAttribute('data-quiet') ? hint : drawnPlaceholder);
|
|
214
257
|
// 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);
|
|
258
|
+
const blankWords = node.placeholder || (labelsAt === 'hidden' ? labelText : (hint ?? ''));
|
|
216
259
|
const quietSelect = widget.element instanceof HTMLSelectElement ? widget.element.querySelector(':scope > option[value=""]') : null;
|
|
217
260
|
const richText = widget.element.matches('.fd-richtext') ? widget.element : widget.element.querySelector(':scope > .fd-richtext');
|
|
218
261
|
const stateWords = widget.element.querySelector(':scope > .fd-dot-label');
|
|
@@ -276,7 +319,12 @@ export function mountViewer(host, given) {
|
|
|
276
319
|
const icon = drawIcon(doc, 'lock', options.icons) ?? '';
|
|
277
320
|
lock = reason ? bubbleTip(`${id}-lock`, reason, fill(labels.locked, { label: labelText }), 'fd-lock', icon) : null;
|
|
278
321
|
lockMark = lock?.element ?? el('span', { class: 'fd-lock-wrap' }, el('span', { class: 'fd-lock', 'aria-hidden': 'true' }, icon));
|
|
279
|
-
|
|
322
|
+
// At the value's start, as a work item's Reason: in a record head's band, in the room before the value.
|
|
323
|
+
if (page.look?.lockAt === 'value') {
|
|
324
|
+
widget.element.before(lockMark);
|
|
325
|
+
wrapper.setAttribute('data-lock-at', 'value');
|
|
326
|
+
}
|
|
327
|
+
else if (labelsAt === 'hidden')
|
|
280
328
|
widget.element.after(lockMark);
|
|
281
329
|
else
|
|
282
330
|
label.insertBefore(lockMark, tip && labelsAt !== 'after' ? tip.element : null);
|
|
@@ -321,6 +369,11 @@ export function mountViewer(host, given) {
|
|
|
321
369
|
if (!wrapper.contains(event.relatedTarget))
|
|
322
370
|
entered = form.getState().values[node.field];
|
|
323
371
|
});
|
|
372
|
+
// Entered before the record came (a dialog takes the focus at once): what it held is what the record brought.
|
|
373
|
+
cleanups.push(form.on('open', ({ values }) => {
|
|
374
|
+
if (wrapper.contains(doc.activeElement))
|
|
375
|
+
entered = values[node.field];
|
|
376
|
+
}));
|
|
324
377
|
wrapper.addEventListener('keydown', (event) => {
|
|
325
378
|
if (event.key !== 'Escape' || event.defaultPrevented || !wrapper.hasAttribute('data-quiet'))
|
|
326
379
|
return;
|
|
@@ -444,25 +497,38 @@ export function mountViewer(host, given) {
|
|
|
444
497
|
watch(() => setHidden(element, form.node(id).invisible));
|
|
445
498
|
}
|
|
446
499
|
function buttonItem(node) {
|
|
500
|
+
// Its icon alone, a toggle, a menu beside it, a look that follows the record's changes: drawn by button-parts.ts.
|
|
501
|
+
const head = isRichButton(node) ? headPart.loaded : undefined;
|
|
502
|
+
if (head) {
|
|
503
|
+
const rich = head.richButton({ doc, el, form, labels, icons: options.icons, uid, withIcon, press, say: (message, tone) => actionHost.say(message, tone), watch, cleanups }, node);
|
|
504
|
+
spans(rich, node.colspan);
|
|
505
|
+
return hiddenOn(node, rich);
|
|
506
|
+
}
|
|
447
507
|
const button = el('button', { type: 'button', class: `fd-button fd-button-${node.style ?? 'secondary'}`, 'data-node': node.id }, ...withIcon(node.icon, node.label));
|
|
448
508
|
if (node.hotkey)
|
|
449
509
|
hotkeyOn(button, node.hotkey, node.label);
|
|
450
510
|
spans(button, node.colspan);
|
|
451
|
-
// Its confirmation, then its steps: the form asks, through the viewer.
|
|
452
|
-
button.addEventListener('click', () =>
|
|
511
|
+
// Its confirmation, then its steps: the form asks, through the viewer. Undo with nothing to put back says so.
|
|
512
|
+
button.addEventListener('click', () => {
|
|
513
|
+
if (!nothingToUndo(node, form, (message, tone) => actionHost.say(message, tone), labels))
|
|
514
|
+
void press(button, () => form.runAction(node.id));
|
|
515
|
+
});
|
|
453
516
|
hideWhen(button, node.id);
|
|
454
517
|
// A header's button is drawn here too, never through item(): hidden at its widths the same way.
|
|
455
518
|
return hiddenOn(node, button);
|
|
456
519
|
}
|
|
457
520
|
function textItem(node, inline = false) {
|
|
458
521
|
const style = node.style ?? 'paragraph';
|
|
459
|
-
// Its words, with the values of the fields they name.
|
|
522
|
+
// Its words, with the values of the fields they name, and its [words](address) as links.
|
|
460
523
|
const words = valueWords(doc, page.fields, node.text, locale);
|
|
524
|
+
const shown = wordsWithLinks(doc, words.nodes);
|
|
461
525
|
const element = inline
|
|
462
|
-
? el('span', { class: `fd-oneline-words fd-text-${style}`, 'data-node': node.id }, ...
|
|
526
|
+
? el('span', { class: `fd-oneline-words fd-text-${style}`, 'data-node': node.id }, ...shown)
|
|
463
527
|
: style === 'alert'
|
|
464
|
-
? 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' }, ...
|
|
465
|
-
:
|
|
528
|
+
? 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))
|
|
529
|
+
: style === 'help'
|
|
530
|
+
? helpBox(headKit, node, shown)
|
|
531
|
+
: el(style === 'heading' ? 'h3' : 'p', { class: `fd-text-${style}`, 'data-node': node.id }, ...shown);
|
|
466
532
|
spans(element, node.colspan);
|
|
467
533
|
hideWhen(element, node.id);
|
|
468
534
|
watch((state) => words.update(state.values));
|
|
@@ -563,7 +629,18 @@ export function mountViewer(host, given) {
|
|
|
563
629
|
section.style.setProperty('--fd-label-width', `${node.labelWidth}px`);
|
|
564
630
|
const description = node.description ? el('p', { class: 'fd-section-description' }, node.description) : null;
|
|
565
631
|
const content = grid(node.children, plan.at === 'tracks' ? null : node.columns, plan.inner);
|
|
566
|
-
|
|
632
|
+
// Its own column widths and gap; a heading with tools at its end, as a work item's group has.
|
|
633
|
+
applyTracks(content, node);
|
|
634
|
+
const own = loneFieldTools(node, content);
|
|
635
|
+
const tools = node.title && (workHeading(node) || own.length) ? headPart.loaded : undefined;
|
|
636
|
+
if (tools) {
|
|
637
|
+
content.id = uid(`${node.id}-content`);
|
|
638
|
+
const head = tools.sectionHeading(headKit, node, section, content, description, own);
|
|
639
|
+
if (node.collapsible)
|
|
640
|
+
folds.set(section, () => head.show(true));
|
|
641
|
+
section.append(head.legend);
|
|
642
|
+
}
|
|
643
|
+
else if (node.title && node.collapsible) {
|
|
567
644
|
content.id = uid(`${node.id}-content`);
|
|
568
645
|
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));
|
|
569
646
|
let open = node.collapsed !== true;
|
|
@@ -606,7 +683,12 @@ export function mountViewer(host, given) {
|
|
|
606
683
|
const parts = node.children.map((tab) => {
|
|
607
684
|
const tabId = uid(`${tab.id}-tab`);
|
|
608
685
|
const panelId = uid(`${tab.id}-panel`);
|
|
609
|
-
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, ...
|
|
686
|
+
const button = el('button', { type: 'button', class: 'fd-tab', role: 'tab', id: tabId, 'aria-controls': panelId, 'data-node': tab.id }, ...tabFace(tab));
|
|
687
|
+
if (tab.iconOnly) {
|
|
688
|
+
button.classList.add('fd-tab-icon-only');
|
|
689
|
+
button.setAttribute('aria-label', tab.label);
|
|
690
|
+
button.title = tab.label;
|
|
691
|
+
}
|
|
610
692
|
const panel = el('div', { class: 'fd-tabpanel', role: 'tabpanel', id: panelId, 'aria-labelledby': tabId });
|
|
611
693
|
/**
|
|
612
694
|
* A tab's parts are drawn when it is first shown, and kept up to date
|
|
@@ -656,12 +738,36 @@ export function mountViewer(host, given) {
|
|
|
656
738
|
// Too long for its room, the strip scrolls; the tab chosen, however, comes into sight in it.
|
|
657
739
|
const strip = tabStrip(list, el);
|
|
658
740
|
cleanups.push(strip.destroy);
|
|
659
|
-
|
|
741
|
+
// Placed in a record's head: the tabs at the end of its band, the panels the sheet's body.
|
|
742
|
+
const inHead = node.place === 'head' && headNow && scope.root ? headNow : null;
|
|
743
|
+
if (inHead) {
|
|
744
|
+
box.classList.add('fd-tabs-in-head');
|
|
745
|
+
inHead.tabs.append(strip.element);
|
|
746
|
+
box.append(...parts.map((p) => p.panel));
|
|
747
|
+
}
|
|
748
|
+
else
|
|
749
|
+
box.append(strip.element, ...parts.map((p) => p.panel));
|
|
750
|
+
// A tab's count in its pill, and in its name: "Links, 0".
|
|
751
|
+
for (const p of parts) {
|
|
752
|
+
const count = p.tab.count;
|
|
753
|
+
if (count === undefined)
|
|
754
|
+
continue;
|
|
755
|
+
const pill = p.button.querySelector('.fd-tab-count');
|
|
756
|
+
watch((state) => {
|
|
757
|
+
const value = state.values[count];
|
|
758
|
+
const n = Array.isArray(value) ? value.length : typeof value === 'number' ? value : Number(value ?? 0) || 0;
|
|
759
|
+
setText(pill, String(n));
|
|
760
|
+
setAttr(p.button, 'aria-label', fill(labels.countOf, { label: p.tab.label, count: n }));
|
|
761
|
+
});
|
|
762
|
+
}
|
|
660
763
|
let shownKey = '';
|
|
661
764
|
let revealed = '';
|
|
662
765
|
watch((state) => {
|
|
663
766
|
const tabsHidden = form.node(node.id).invisible;
|
|
664
767
|
setHidden(box, tabsHidden);
|
|
768
|
+
// In the head, its strip is apart from its box: hidden with it.
|
|
769
|
+
if (inHead)
|
|
770
|
+
setHidden(strip.element, tabsHidden);
|
|
665
771
|
const shown = parts.filter((p) => !form.node(p.tab.id).invisible);
|
|
666
772
|
const keep = picked !== null && shown.some((p) => p.tab.id === picked);
|
|
667
773
|
if (keep)
|
|
@@ -1217,6 +1323,10 @@ export function mountViewer(host, given) {
|
|
|
1217
1323
|
return box;
|
|
1218
1324
|
}
|
|
1219
1325
|
function sheetLayout(node) {
|
|
1326
|
+
// A head comes in the head part, waited for before the page is drawn; where it could not come, the sheet is drawn plainly.
|
|
1327
|
+
const part = node.head ? headPart.loaded : undefined;
|
|
1328
|
+
if (part)
|
|
1329
|
+
return headedLayout(node, part);
|
|
1220
1330
|
const pageBox = el('div', { class: 'fd-sheet-page', 'data-node': node.id });
|
|
1221
1331
|
const actions = el('div', { class: 'fd-actions' });
|
|
1222
1332
|
const save = el('button', { type: 'submit', class: 'fd-button fd-button-primary' }, labels.save);
|
|
@@ -1312,41 +1422,7 @@ export function mountViewer(host, given) {
|
|
|
1312
1422
|
});
|
|
1313
1423
|
card.append(stats);
|
|
1314
1424
|
}
|
|
1315
|
-
|
|
1316
|
-
// Its words with the values they name, or a field's words while it holds any; its buttons after them, as links unless styled.
|
|
1317
|
-
const words = valueWords(doc, page.fields, alert.message, locale);
|
|
1318
|
-
const message = el('span', { class: 'fd-alert-message' }, ...words.nodes);
|
|
1319
|
-
let fromField = false;
|
|
1320
|
-
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, message);
|
|
1321
|
-
if (alert.buttons?.length)
|
|
1322
|
-
box.append(el('span', { class: 'fd-alert-actions' }, ...alert.buttons.map((button) => buttonItem({ ...button, style: button.style ?? 'link' }))));
|
|
1323
|
-
updaters.push((state) => {
|
|
1324
|
-
words.update(state.values);
|
|
1325
|
-
const from = alert.messageField ? state.values[alert.messageField] : null;
|
|
1326
|
-
const said = typeof from === 'string' ? from.trim() : '';
|
|
1327
|
-
if (said)
|
|
1328
|
-
setText(message, said);
|
|
1329
|
-
else if (fromField)
|
|
1330
|
-
message.replaceChildren(...words.nodes);
|
|
1331
|
-
fromField = !!said;
|
|
1332
|
-
});
|
|
1333
|
-
if (alert.dismissible) {
|
|
1334
|
-
// Closed, it stays closed while the page is open, whatever its condition does.
|
|
1335
|
-
let dismissed = false;
|
|
1336
|
-
const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': labels.dismiss }, '×');
|
|
1337
|
-
close.addEventListener('click', () => {
|
|
1338
|
-
dismissed = true;
|
|
1339
|
-
box.hidden = true;
|
|
1340
|
-
});
|
|
1341
|
-
box.append(close);
|
|
1342
|
-
updaters.push(() => {
|
|
1343
|
-
box.hidden = dismissed || form.node(alert.id).invisible;
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
else
|
|
1347
|
-
hideWhen(box, alert.id);
|
|
1348
|
-
card.append(box);
|
|
1349
|
-
}
|
|
1425
|
+
card.append(...alertBoxes(node));
|
|
1350
1426
|
if (node.badges?.length) {
|
|
1351
1427
|
const badges = el('div', { class: 'fd-badges' });
|
|
1352
1428
|
for (const badge of node.badges) {
|
|
@@ -1423,6 +1499,95 @@ export function mountViewer(host, given) {
|
|
|
1423
1499
|
pageBox.append(...(bar ? [bar] : []), header, layout);
|
|
1424
1500
|
return pageBox;
|
|
1425
1501
|
}
|
|
1502
|
+
/** 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. */
|
|
1503
|
+
function alertBoxes(node) {
|
|
1504
|
+
return (node.alerts ?? []).map((alert) => {
|
|
1505
|
+
const words = valueWords(doc, page.fields, alert.message, locale);
|
|
1506
|
+
const message = el('span', { class: 'fd-alert-message' }, ...words.nodes);
|
|
1507
|
+
let fromField = false;
|
|
1508
|
+
const box = el('div', { class: `fd-alert fd-tone-${alert.tone ?? 'info'}`, role: 'status', 'data-node': alert.id }, message);
|
|
1509
|
+
if (alert.buttons?.length)
|
|
1510
|
+
box.append(el('span', { class: 'fd-alert-actions' }, ...alert.buttons.map((button) => buttonItem({ ...button, style: button.style ?? 'link' }))));
|
|
1511
|
+
updaters.push((state) => {
|
|
1512
|
+
words.update(state.values);
|
|
1513
|
+
const from = alert.messageField ? state.values[alert.messageField] : null;
|
|
1514
|
+
const said = typeof from === 'string' ? from.trim() : '';
|
|
1515
|
+
if (said)
|
|
1516
|
+
setText(message, said);
|
|
1517
|
+
else if (fromField)
|
|
1518
|
+
message.replaceChildren(...words.nodes);
|
|
1519
|
+
fromField = !!said;
|
|
1520
|
+
});
|
|
1521
|
+
if (alert.dismissible) {
|
|
1522
|
+
// Closed, it stays closed while the page is open, whatever its condition does.
|
|
1523
|
+
let dismissed = false;
|
|
1524
|
+
const close = el('button', { type: 'button', class: 'fd-alert-close', 'aria-label': labels.dismiss }, '×');
|
|
1525
|
+
close.addEventListener('click', () => {
|
|
1526
|
+
dismissed = true;
|
|
1527
|
+
box.hidden = true;
|
|
1528
|
+
});
|
|
1529
|
+
box.append(close);
|
|
1530
|
+
updaters.push(() => {
|
|
1531
|
+
box.hidden = dismissed || form.node(alert.id).invisible;
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
else
|
|
1535
|
+
hideWhen(box, alert.id);
|
|
1536
|
+
return box;
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* A record with a head (`SheetHead`), as a work item: the head — its kind,
|
|
1541
|
+
* title, row, buttons and band, the tabs placed in it — then the body, which
|
|
1542
|
+
* holds its alerts and its parts (a tab's panels, when its tabs are in the
|
|
1543
|
+
* head). The record's breadcrumbs and pager, when the app gives them, over
|
|
1544
|
+
* it; its gear menu is the head's ⋮.
|
|
1545
|
+
*/
|
|
1546
|
+
function headedLayout(node, part) {
|
|
1547
|
+
const pageBox = el('div', { class: 'fd-sheet-page fd-head-page', 'data-node': node.id });
|
|
1548
|
+
const head = part.recordHead({
|
|
1549
|
+
doc,
|
|
1550
|
+
el,
|
|
1551
|
+
form,
|
|
1552
|
+
page,
|
|
1553
|
+
sheet: node,
|
|
1554
|
+
head: node.head,
|
|
1555
|
+
labels,
|
|
1556
|
+
locale,
|
|
1557
|
+
icons: options.icons,
|
|
1558
|
+
uid,
|
|
1559
|
+
fill,
|
|
1560
|
+
withIcon,
|
|
1561
|
+
press,
|
|
1562
|
+
say: (message, tone) => actionHost.say(message, tone),
|
|
1563
|
+
fieldItem,
|
|
1564
|
+
item,
|
|
1565
|
+
buttonItem,
|
|
1566
|
+
watch: (update) => updaters.push(update),
|
|
1567
|
+
cleanups,
|
|
1568
|
+
showTab: (id) => tabs.get(id)?.() ?? false,
|
|
1569
|
+
unfold(part) {
|
|
1570
|
+
for (let folded = part.closest('.fd-section-folded'); folded; folded = folded.parentElement?.closest('.fd-section-folded') ?? null)
|
|
1571
|
+
folds.get(folded)?.();
|
|
1572
|
+
},
|
|
1573
|
+
root: () => root,
|
|
1574
|
+
});
|
|
1575
|
+
// The parts drawn now: tabs placed in the head go to its band.
|
|
1576
|
+
headNow = head;
|
|
1577
|
+
let parts;
|
|
1578
|
+
try {
|
|
1579
|
+
parts = grid(node.children, 1, top(false));
|
|
1580
|
+
}
|
|
1581
|
+
finally {
|
|
1582
|
+
headNow = null;
|
|
1583
|
+
}
|
|
1584
|
+
const body = el('div', { class: 'fd-head-body' }, ...alertBoxes(node), parts);
|
|
1585
|
+
// The side panel beside the body on a wide page; a dialog leaves it to the full page.
|
|
1586
|
+
const side = node.sidePanel ? el('aside', { class: 'fd-side' }, slotItem(node.sidePanel)) : null;
|
|
1587
|
+
const bar = topBar(node.toolbar ? { ...node.toolbar, menu: undefined } : undefined);
|
|
1588
|
+
pageBox.append(...(bar ? [bar] : []), head.element, side ? el('div', { class: 'fd-head-layout' }, body, side) : body);
|
|
1589
|
+
return pageBox;
|
|
1590
|
+
}
|
|
1426
1591
|
/** The sheet header's statusbar: the same widget a form can place anywhere. */
|
|
1427
1592
|
function statusbar(config) {
|
|
1428
1593
|
const def = page.fields[config.field];
|
|
@@ -1445,42 +1610,76 @@ export function mountViewer(host, given) {
|
|
|
1445
1610
|
}
|
|
1446
1611
|
let wizardForward = null;
|
|
1447
1612
|
const layout = page.layout;
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1613
|
+
/** A list page's way to ask for its records again (handle.reload). */
|
|
1614
|
+
let reloadList = null;
|
|
1615
|
+
/** And its way to open a record as its row's press does (handle.openRecord). */
|
|
1616
|
+
let openInList = null;
|
|
1617
|
+
/**
|
|
1618
|
+
* The page itself — its layout, head and bar — drawn whole: at once when every part it draws from is in, else once
|
|
1619
|
+
* they have come (or could not, when what they would have drawn is drawn plainly). Nothing of it is shown before, so
|
|
1620
|
+
* nothing moves as a part arrives, and no focus is lost: there was nothing to have it.
|
|
1621
|
+
*/
|
|
1622
|
+
function drawPage() {
|
|
1623
|
+
let body;
|
|
1624
|
+
if (layout.type === 'sheet')
|
|
1625
|
+
body = sheetLayout(layout);
|
|
1626
|
+
else if (layout.type === 'wizard')
|
|
1627
|
+
body = wizardLayout(layout);
|
|
1628
|
+
else if (layout.type === 'tabs')
|
|
1629
|
+
body = sectionsLayout({ id: `${layout.id}-page`, children: [layout] });
|
|
1630
|
+
else if (layout.type === 'list') {
|
|
1631
|
+
const list = listView({
|
|
1632
|
+
page,
|
|
1633
|
+
node: layout,
|
|
1634
|
+
form,
|
|
1635
|
+
dataSource: options.dataSource,
|
|
1636
|
+
doc,
|
|
1637
|
+
el,
|
|
1638
|
+
labels,
|
|
1639
|
+
locale,
|
|
1640
|
+
fill,
|
|
1641
|
+
press,
|
|
1642
|
+
withIcon,
|
|
1643
|
+
uid,
|
|
1644
|
+
icons: options.icons,
|
|
1645
|
+
preferences,
|
|
1646
|
+
onOpenRecord: options.onOpenRecord,
|
|
1647
|
+
...(options.listFilter ? { fixedFilter: options.listFilter } : {}),
|
|
1648
|
+
...(options.listViews ? { views: options.listViews } : {}),
|
|
1649
|
+
});
|
|
1650
|
+
cleanups.push(list.destroy);
|
|
1651
|
+
reloadList = list.reload;
|
|
1652
|
+
openInList = list.open;
|
|
1653
|
+
body = list.element;
|
|
1654
|
+
}
|
|
1655
|
+
else
|
|
1656
|
+
body = sectionsLayout(layout);
|
|
1657
|
+
const head = layout.type === 'sheet' ? null : pageHead();
|
|
1658
|
+
// A record of sections or tabs has the bar the app fills — its breadcrumbs and pager — over its title.
|
|
1659
|
+
const bar = layout.type === 'sections' || layout.type === 'tabs' ? topBar(undefined) : null;
|
|
1660
|
+
content.append(...(bar ? [bar] : []), ...(head ? [head] : []), banner, draft, body);
|
|
1661
|
+
pageDrawn = true;
|
|
1662
|
+
}
|
|
1663
|
+
let pageDrawn = false;
|
|
1664
|
+
/** What needs the page drawn — a list's rows to open or ask for again — at once when it is, else once it is. */
|
|
1665
|
+
const afterDrawing = (then) => (pageDrawn ? then() : drawn.then(then));
|
|
1666
|
+
const awaited = partsFor(page, options.widgets, options.slots).filter((part) => !part.loaded);
|
|
1667
|
+
let drawn = Promise.resolve();
|
|
1668
|
+
if (!awaited.length)
|
|
1669
|
+
drawPage();
|
|
1670
|
+
else {
|
|
1671
|
+
root.setAttribute('aria-busy', 'true');
|
|
1672
|
+
drawn = settleParts(awaited).then(() => {
|
|
1673
|
+
if (gone)
|
|
1674
|
+
return;
|
|
1675
|
+
drawPage();
|
|
1676
|
+
root.removeAttribute('aria-busy');
|
|
1677
|
+
render(form.getState());
|
|
1678
|
+
// A record that opened meanwhile: its focused field takes the focus now, as it would have then.
|
|
1679
|
+
if (opened)
|
|
1680
|
+
focusAsOpened(true);
|
|
1474
1681
|
});
|
|
1475
|
-
cleanups.push(list.destroy);
|
|
1476
|
-
body = list.element;
|
|
1477
1682
|
}
|
|
1478
|
-
else
|
|
1479
|
-
body = sectionsLayout(layout);
|
|
1480
|
-
const head = layout.type === 'sheet' ? null : pageHead();
|
|
1481
|
-
// A record of sections or tabs has the bar the app fills — its breadcrumbs and pager — over its title.
|
|
1482
|
-
const bar = layout.type === 'sections' || layout.type === 'tabs' ? topBar(undefined) : null;
|
|
1483
|
-
content.append(...(bar ? [bar] : []), ...(head ? [head] : []), banner, draft, body);
|
|
1484
1683
|
root.append(content, done, announcer);
|
|
1485
1684
|
// The status leaves its place beside Save when the page wants it in a corner or across the top.
|
|
1486
1685
|
if (options.saveStatus === 'toast') {
|
|
@@ -1593,6 +1792,14 @@ export function mountViewer(host, given) {
|
|
|
1593
1792
|
if (!key || !hotkeysHere(event.target))
|
|
1594
1793
|
return;
|
|
1595
1794
|
const button = [...root.querySelectorAll(`button[data-hotkey="${key}"]`)].find((b) => b.closest('.fd-form') === root && !b.closest('[hidden]') && !b.disabled && !b.hasAttribute('aria-busy'));
|
|
1795
|
+
// A field's access key: the first shown with it takes the focus.
|
|
1796
|
+
const field = button ? undefined : fieldKeys.find((one) => one.key === key && one.wrapper.isConnected && !one.wrapper.closest('[hidden]'));
|
|
1797
|
+
if (field) {
|
|
1798
|
+
event.preventDefault();
|
|
1799
|
+
root.removeAttribute('data-hotkeys');
|
|
1800
|
+
field.focus();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1596
1803
|
if (!button)
|
|
1597
1804
|
return;
|
|
1598
1805
|
event.preventDefault();
|
|
@@ -1667,22 +1874,24 @@ export function mountViewer(host, given) {
|
|
|
1667
1874
|
mounted = true;
|
|
1668
1875
|
// As the record opens, its focused field takes the focus — the first shown — unless the person is busy elsewhere:
|
|
1669
1876
|
// only while nothing has the focus, or, the first time, while it is in this form (a dialog's first box).
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
focusers.find((one) => !one.wrapper.closest('[hidden]'))?.focus();
|
|
1683
|
-
});
|
|
1684
|
-
}));
|
|
1877
|
+
function focusAsOpened(first) {
|
|
1878
|
+
if (!focusers.length)
|
|
1879
|
+
return;
|
|
1880
|
+
queueMicrotask(() => {
|
|
1881
|
+
if (gone || !root.isConnected || root.closest('[hidden]') || (typeof root.checkVisibility === 'function' && !root.checkVisibility()))
|
|
1882
|
+
return;
|
|
1883
|
+
const active = doc.activeElement;
|
|
1884
|
+
const free = !active || active === doc.body || active === doc.documentElement;
|
|
1885
|
+
if (!free && !(first && root.contains(active)))
|
|
1886
|
+
return;
|
|
1887
|
+
focusers.find((one) => !one.wrapper.closest('[hidden]'))?.focus();
|
|
1888
|
+
});
|
|
1685
1889
|
}
|
|
1890
|
+
cleanups.push(form.on('open', () => {
|
|
1891
|
+
const first = !opened;
|
|
1892
|
+
opened = true;
|
|
1893
|
+
focusAsOpened(first);
|
|
1894
|
+
}));
|
|
1686
1895
|
if (form.getState().status === 'idle')
|
|
1687
1896
|
void form.load();
|
|
1688
1897
|
return {
|
|
@@ -1723,6 +1932,14 @@ export function mountViewer(host, given) {
|
|
|
1723
1932
|
render(form.getState());
|
|
1724
1933
|
},
|
|
1725
1934
|
isReadonly: () => locked,
|
|
1935
|
+
reload: () => afterDrawing(async () => {
|
|
1936
|
+
if (reloadList)
|
|
1937
|
+
return reloadList();
|
|
1938
|
+
await form.run([{ do: 'reload' }]);
|
|
1939
|
+
}),
|
|
1940
|
+
openRecord: (id, options) => afterDrawing(async () => {
|
|
1941
|
+
await openInList?.(id, options);
|
|
1942
|
+
}),
|
|
1726
1943
|
on: (event, listener) => {
|
|
1727
1944
|
const off = form.on(event, listener);
|
|
1728
1945
|
cleanups.push(off);
|
|
@@ -1730,6 +1947,7 @@ export function mountViewer(host, given) {
|
|
|
1730
1947
|
},
|
|
1731
1948
|
setValues: (values) => form.setValues(values),
|
|
1732
1949
|
run: (steps, context) => form.run(steps, context),
|
|
1950
|
+
drawn,
|
|
1733
1951
|
destroy() {
|
|
1734
1952
|
gone = true;
|
|
1735
1953
|
unsubscribe();
|
|
@@ -1741,6 +1959,8 @@ export function mountViewer(host, given) {
|
|
|
1741
1959
|
},
|
|
1742
1960
|
};
|
|
1743
1961
|
}
|
|
1962
|
+
/** What a field is worked with: where its access key is said. */
|
|
1963
|
+
const CONTROL = 'input:not([type="hidden"]), select, textarea, button, [contenteditable="true"], [tabindex="0"]';
|
|
1744
1964
|
/** The boxes a label can be shown in, as the words of an empty box. */
|
|
1745
1965
|
const TEXT_BOX = 'input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]), textarea';
|
|
1746
1966
|
/** Nothing in it: no value, empty words, nothing picked. */
|