@optionfactory/fml 9.0.0-rc13 → 9.0.0-rc15
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/dist/client-errors.iife.js +6 -11
- package/dist/client-errors.iife.js.map +1 -1
- package/dist/client-errors.iife.min.js +1 -1
- package/dist/client-errors.iife.min.js.map +1 -1
- package/dist/custom-elements.json +71 -10
- package/dist/fml.css +6 -5
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +658 -117
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ftl.iife.js +2 -2
- package/dist/ftl.iife.js.map +1 -1
- package/dist/ftl.iife.min.js +1 -1
- package/dist/ftl.iife.min.js.map +1 -1
- package/dist/ftl.min.mjs +1 -1
- package/dist/ftl.min.mjs.map +1 -1
- package/dist/ftl.mjs +2 -2
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +6 -5
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +110 -8
- package/dist/ful.iife.js +655 -115
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +655 -116
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +37 -7
- package/dist/web-types.json +67 -11
- package/package.json +1 -1
package/dist/fml.iife.js
CHANGED
|
@@ -5147,7 +5147,7 @@ var fml = (function (exports) {
|
|
|
5147
5147
|
*/
|
|
5148
5148
|
static t(key, ...args) {
|
|
5149
5149
|
const messages = this.l10n ?? {};
|
|
5150
|
-
let message = messages[key];
|
|
5150
|
+
let message = Object.hasOwn(messages, key) ? messages[key] : undefined;
|
|
5151
5151
|
if (message === undefined) {
|
|
5152
5152
|
warnOnce(`missing message "${key}"`);
|
|
5153
5153
|
return key;
|
|
@@ -5170,7 +5170,7 @@ var fml = (function (exports) {
|
|
|
5170
5170
|
if (args.length === 1 && isPlainObject(args[0])) {
|
|
5171
5171
|
const named = args[0];
|
|
5172
5172
|
return message.replace(PLACEHOLDER, (literal, name) => {
|
|
5173
|
-
if (!(name
|
|
5173
|
+
if (!Object.hasOwn(named, name)) {
|
|
5174
5174
|
warnOnce(`message "${key}" wants {${name}}`);
|
|
5175
5175
|
return literal;
|
|
5176
5176
|
}
|
|
@@ -6789,30 +6789,62 @@ var fml = (function (exports) {
|
|
|
6789
6789
|
}
|
|
6790
6790
|
|
|
6791
6791
|
static mutateIn(form, values) {
|
|
6792
|
-
const
|
|
6793
|
-
|
|
6794
|
-
.
|
|
6795
|
-
for (const [flattenedKey, value] of Object.entries(Bindings.flatten(values, '', new Set(names)))) {
|
|
6796
|
-
for (const el of form.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`)) {
|
|
6792
|
+
const named = Bindings.#named(form);
|
|
6793
|
+
for (const [flattenedKey, value] of Object.entries(Bindings.flatten(values, '', new Set(named.keys())))) {
|
|
6794
|
+
for (const el of named.get(flattenedKey) ?? []) {
|
|
6797
6795
|
Bindings.mutate(el, value);
|
|
6798
6796
|
}
|
|
6799
6797
|
}
|
|
6800
6798
|
}
|
|
6801
6799
|
|
|
6800
|
+
/**
|
|
6801
|
+
* The form's own named controls, by name: `form.elements`, the platform's
|
|
6802
|
+
* association list, so a field in the subtree that belongs to another form
|
|
6803
|
+
* (a nested form built by script, a `form` attribute pointing elsewhere) is
|
|
6804
|
+
* not the caller's to touch, where a subtree query would have reached it.
|
|
6805
|
+
*/
|
|
6806
|
+
static #named(form) {
|
|
6807
|
+
const named = new Map();
|
|
6808
|
+
for (const el of form.elements) {
|
|
6809
|
+
const name = el.getAttribute('name');
|
|
6810
|
+
if (!name) {
|
|
6811
|
+
continue;
|
|
6812
|
+
}
|
|
6813
|
+
const bucket = named.get(name);
|
|
6814
|
+
if (bucket) {
|
|
6815
|
+
bucket.push(el);
|
|
6816
|
+
} else {
|
|
6817
|
+
named.set(name, [el]);
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
return named;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6802
6823
|
static errors(form, es, scrollOnError) {
|
|
6803
6824
|
//focus management announces the error of the field it lands on through
|
|
6804
6825
|
//aria-describedby: a live region on top of that would read everything twice,
|
|
6805
|
-
//so the polite announcement exists only when nothing takes the focus
|
|
6806
|
-
form
|
|
6807
|
-
|
|
6808
|
-
|
|
6826
|
+
//so the polite announcement exists only when nothing takes the focus.
|
|
6827
|
+
//The regions and the banner belong to the form whose subtree holds them
|
|
6828
|
+
//nearest: a nested form's are not this form's to touch
|
|
6829
|
+
const ofForm = (el) => el.closest('form') === form;
|
|
6830
|
+
Array.from(form.querySelectorAll('ful-field-error'))
|
|
6831
|
+
.filter(ofForm)
|
|
6832
|
+
.forEach((el) => {
|
|
6833
|
+
el.setAttribute('aria-live', scrollOnError ? 'off' : 'polite');
|
|
6834
|
+
});
|
|
6809
6835
|
const pinned = (e) => (e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT') && e.context;
|
|
6810
6836
|
const fieldErrors = es.filter(pinned);
|
|
6811
6837
|
const globalErrors = es.filter((e) => !pinned(e));
|
|
6812
|
-
form
|
|
6813
|
-
|
|
6814
|
-
|
|
6838
|
+
const named = Bindings.#named(form);
|
|
6839
|
+
for (const targets of named.values()) {
|
|
6840
|
+
targets.forEach((el) => {
|
|
6841
|
+
el.setCustomValidity?.('');
|
|
6842
|
+
});
|
|
6843
|
+
}
|
|
6815
6844
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
6845
|
+
if (!ofForm(el)) {
|
|
6846
|
+
return;
|
|
6847
|
+
}
|
|
6816
6848
|
el.setAttribute('role', 'alert');
|
|
6817
6849
|
el.replaceChildren();
|
|
6818
6850
|
el.setAttribute('hidden', '');
|
|
@@ -6823,7 +6855,7 @@ var fml = (function (exports) {
|
|
|
6823
6855
|
const parts = name.split('.');
|
|
6824
6856
|
for (let i = parts.length; i !== 0; --i) {
|
|
6825
6857
|
const prefix = parts.slice(0, i).join('.');
|
|
6826
|
-
const targets =
|
|
6858
|
+
const targets = named.get(prefix) ?? [];
|
|
6827
6859
|
if (targets.length === 0) {
|
|
6828
6860
|
continue;
|
|
6829
6861
|
}
|
|
@@ -6843,6 +6875,9 @@ var fml = (function (exports) {
|
|
|
6843
6875
|
});
|
|
6844
6876
|
const bannered = [...globalErrors, ...unmatched];
|
|
6845
6877
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
6878
|
+
if (!ofForm(el)) {
|
|
6879
|
+
return;
|
|
6880
|
+
}
|
|
6846
6881
|
const hel = /** @type HTMLElement} */ (el);
|
|
6847
6882
|
if (bannered.length === 0) {
|
|
6848
6883
|
hel.innerText = '';
|
|
@@ -6857,7 +6892,8 @@ var fml = (function (exports) {
|
|
|
6857
6892
|
if (es.length === 0 || !scrollOnError) {
|
|
6858
6893
|
return;
|
|
6859
6894
|
}
|
|
6860
|
-
Array.from(form.
|
|
6895
|
+
Array.from(form.elements)
|
|
6896
|
+
.filter((el) => el.matches(':invalid'))
|
|
6861
6897
|
.sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y)[0]
|
|
6862
6898
|
?.focus();
|
|
6863
6899
|
}
|
|
@@ -6898,6 +6934,7 @@ var fml = (function (exports) {
|
|
|
6898
6934
|
/** the role the element internals carry, 'presentation' unless the control is its own */
|
|
6899
6935
|
static ROLE = 'presentation';
|
|
6900
6936
|
#control;
|
|
6937
|
+
#pendingFocus = null;
|
|
6901
6938
|
#described;
|
|
6902
6939
|
#descriptions = [];
|
|
6903
6940
|
#errorId = null;
|
|
@@ -6979,12 +7016,17 @@ var fml = (function (exports) {
|
|
|
6979
7016
|
//only where the platform cannot: a control still associated with the
|
|
6980
7017
|
//form, an author's own input in a slot among them, submits on its own
|
|
6981
7018
|
//and would otherwise submit twice
|
|
6982
|
-
if (target.form === this.
|
|
7019
|
+
if (target.form === this.form || !Field.#submitsOnEnter(target)) {
|
|
6983
7020
|
return;
|
|
6984
7021
|
}
|
|
6985
7022
|
this._requestSubmit();
|
|
6986
7023
|
});
|
|
6987
7024
|
this.replaceChildren(fragment);
|
|
7025
|
+
if (this.#pendingFocus !== null) {
|
|
7026
|
+
const options = this.#pendingFocus;
|
|
7027
|
+
this.#pendingFocus = null;
|
|
7028
|
+
control.focus(options);
|
|
7029
|
+
}
|
|
6988
7030
|
}
|
|
6989
7031
|
/**
|
|
6990
7032
|
* The platform's own rule for which control Enter submits from, measured on
|
|
@@ -7052,7 +7094,11 @@ var fml = (function (exports) {
|
|
|
7052
7094
|
}
|
|
7053
7095
|
}
|
|
7054
7096
|
focus(options) {
|
|
7055
|
-
this.#control
|
|
7097
|
+
if (this.#control) {
|
|
7098
|
+
this.#control.focus(options);
|
|
7099
|
+
return;
|
|
7100
|
+
}
|
|
7101
|
+
this.#pendingFocus = options ?? {};
|
|
7056
7102
|
}
|
|
7057
7103
|
/**
|
|
7058
7104
|
* Clears or reports one validation problem: the text lands on the field's
|
|
@@ -7079,9 +7125,13 @@ var fml = (function (exports) {
|
|
|
7079
7125
|
this.internals.setValidity({ customError: true }, ' ');
|
|
7080
7126
|
this.#fieldError.innerText = error;
|
|
7081
7127
|
}
|
|
7128
|
+
/** The form the platform associated the field with, null outside one: a native control's own `form`. */
|
|
7129
|
+
get form() {
|
|
7130
|
+
return this.internals.form;
|
|
7131
|
+
}
|
|
7082
7132
|
/** Submits the associated form through its first submitter, as Enter on a native control would. */
|
|
7083
7133
|
_requestSubmit() {
|
|
7084
|
-
const form = this.
|
|
7134
|
+
const form = this.form;
|
|
7085
7135
|
if (!form) {
|
|
7086
7136
|
return;
|
|
7087
7137
|
}
|
|
@@ -7439,6 +7489,11 @@ var fml = (function (exports) {
|
|
|
7439
7489
|
);
|
|
7440
7490
|
if (this.declared('clear-invalid-on-change')) {
|
|
7441
7491
|
this.addEventListener('change', (/** @type any */ evt) => {
|
|
7492
|
+
//a form-associated custom element carries no `form` property of
|
|
7493
|
+
//its own: the owner reads through the internals it attached
|
|
7494
|
+
if ((evt.target.form ?? evt.target.internals?.form) !== this.form) {
|
|
7495
|
+
return;
|
|
7496
|
+
}
|
|
7442
7497
|
evt.target.setCustomValidity?.('');
|
|
7443
7498
|
});
|
|
7444
7499
|
}
|
|
@@ -7504,8 +7559,9 @@ var fml = (function (exports) {
|
|
|
7504
7559
|
);
|
|
7505
7560
|
if (e instanceof Failure) {
|
|
7506
7561
|
this.errors = e.problems;
|
|
7562
|
+
} else {
|
|
7563
|
+
console.warn('failed to submit form', this, 'reason:', e);
|
|
7507
7564
|
}
|
|
7508
|
-
console.warn('failed to submit form', this, 'reason:', e);
|
|
7509
7565
|
} finally {
|
|
7510
7566
|
this.#submitting = false;
|
|
7511
7567
|
this.spinner(false);
|
|
@@ -7555,6 +7611,9 @@ var fml = (function (exports) {
|
|
|
7555
7611
|
//same way, and a form that only dimmed its button said it to no one
|
|
7556
7612
|
Attributes.set(this, 'aria-busy', spin ? 'true' : null);
|
|
7557
7613
|
this.querySelectorAll('ful-spinner').forEach((el) => {
|
|
7614
|
+
if (el.closest('form') !== this.form) {
|
|
7615
|
+
return;
|
|
7616
|
+
}
|
|
7558
7617
|
const hel = /** @type HTMLElement */ (el);
|
|
7559
7618
|
if (spin) {
|
|
7560
7619
|
this.#announce(hel);
|
|
@@ -7565,7 +7624,7 @@ var fml = (function (exports) {
|
|
|
7565
7624
|
});
|
|
7566
7625
|
this.querySelectorAll('input,button').forEach((el) => {
|
|
7567
7626
|
const hel = /** @type HTMLButtonElement|HTMLInputElement */ (el);
|
|
7568
|
-
if (hel.type !== 'submit' && hel.type !== 'reset') {
|
|
7627
|
+
if (hel.form !== this.form || (hel.type !== 'submit' && hel.type !== 'reset')) {
|
|
7569
7628
|
return;
|
|
7570
7629
|
}
|
|
7571
7630
|
if (spin) {
|
|
@@ -8406,11 +8465,18 @@ var fml = (function (exports) {
|
|
|
8406
8465
|
popover.style.bottom = 'auto';
|
|
8407
8466
|
popover.style.margin = '0';
|
|
8408
8467
|
if (stretch) {
|
|
8409
|
-
|
|
8410
|
-
popover.style.width
|
|
8468
|
+
popover.style.removeProperty('width');
|
|
8469
|
+
popover.style.removeProperty('min-width');
|
|
8470
|
+
popover.style.removeProperty('max-width');
|
|
8471
|
+
const cap = Math.min(parseFloat(computed.maxWidth) || vw, vw - 2 * PAD);
|
|
8472
|
+
popover.style.minWidth = `${Math.min(box.width, vw - 2 * PAD)}px`;
|
|
8473
|
+
popover.style.maxWidth = `${cap}px`;
|
|
8474
|
+
const width = popover.getBoundingClientRect().width;
|
|
8411
8475
|
popover.style.left = `${clamp(box.left, PAD, vw - width - PAD)}px`;
|
|
8412
8476
|
const height = popover.getBoundingClientRect().height;
|
|
8413
|
-
|
|
8477
|
+
const below = box.bottom + gap.top;
|
|
8478
|
+
const top = below + height <= vh - PAD ? below : box.top - gap.bottom - height;
|
|
8479
|
+
popover.style.top = `${clamp(top, PAD, vh - height - PAD)}px`;
|
|
8414
8480
|
return;
|
|
8415
8481
|
}
|
|
8416
8482
|
//a popover wraps against the spot it lands on: the width is measured
|
|
@@ -8455,7 +8521,9 @@ var fml = (function (exports) {
|
|
|
8455
8521
|
'bottom',
|
|
8456
8522
|
'margin',
|
|
8457
8523
|
'max-width',
|
|
8524
|
+
'min-width',
|
|
8458
8525
|
'width',
|
|
8526
|
+
'visibility',
|
|
8459
8527
|
'--ful-note-callout-inline',
|
|
8460
8528
|
'--ful-note-callout-block',
|
|
8461
8529
|
]) {
|
|
@@ -8463,6 +8531,16 @@ var fml = (function (exports) {
|
|
|
8463
8531
|
}
|
|
8464
8532
|
};
|
|
8465
8533
|
|
|
8534
|
+
const showOncePlaced = (popover, anchored) => {
|
|
8535
|
+
popover.style.visibility = 'hidden';
|
|
8536
|
+
queueMicrotask(() => {
|
|
8537
|
+
if (popover.matches(':popover-open')) {
|
|
8538
|
+
place(popover, anchored);
|
|
8539
|
+
}
|
|
8540
|
+
popover.style.removeProperty('visibility');
|
|
8541
|
+
});
|
|
8542
|
+
};
|
|
8543
|
+
|
|
8466
8544
|
const reflow = () => {
|
|
8467
8545
|
frame = 0;
|
|
8468
8546
|
for (const [popover, anchored] of open) {
|
|
@@ -8513,8 +8591,10 @@ var fml = (function (exports) {
|
|
|
8513
8591
|
* the popover, giving toggle and light dismiss with no script of your own
|
|
8514
8592
|
* @param {boolean} [options.expanded] keeps the invoker's `aria-expanded` in
|
|
8515
8593
|
* step with the popover
|
|
8516
|
-
* @param {boolean} [options.stretch] widens the popover to
|
|
8517
|
-
* is what a combobox
|
|
8594
|
+
* @param {boolean} [options.stretch] widens the popover to at least its
|
|
8595
|
+
* invoker's width, capped by its own `max-width`, which is what a combobox
|
|
8596
|
+
* dropdown wants; it flips above the invoker where the viewport leaves no
|
|
8597
|
+
* room below
|
|
8518
8598
|
* @param {boolean} [options.handPlace] places here on every platform rather
|
|
8519
8599
|
* than only as a fallback, which a popover asks for when it needs to know
|
|
8520
8600
|
* where its invoker ended up: the tooltip's note points a callout at it, and
|
|
@@ -8561,10 +8641,8 @@ var fml = (function (exports) {
|
|
|
8561
8641
|
}
|
|
8562
8642
|
const anchored = { invoker, stretch };
|
|
8563
8643
|
popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
8564
|
-
//placed before the showing, refined once laid out: the platform's
|
|
8565
|
-
//centered or corner spot never paints
|
|
8566
8644
|
if (evt.newState === 'open') {
|
|
8567
|
-
|
|
8645
|
+
showOncePlaced(popover, anchored);
|
|
8568
8646
|
}
|
|
8569
8647
|
});
|
|
8570
8648
|
popover.addEventListener('toggle', (/** @type any */ evt) => {
|
|
@@ -8582,6 +8660,52 @@ var fml = (function (exports) {
|
|
|
8582
8660
|
window.addEventListener('resize', schedule);
|
|
8583
8661
|
}
|
|
8584
8662
|
}
|
|
8663
|
+
/**
|
|
8664
|
+
* Shows and places a popover beside an anchor, once: the popover shared by
|
|
8665
|
+
* many invokers, each open handing over its own anchor, and an anchor that
|
|
8666
|
+
* is a rectangle rather than an element, one read out of an iframe being
|
|
8667
|
+
* the case a paired invoker could not cover. No pairing, no reflow
|
|
8668
|
+
* following, no close wiring: closing is the caller's, or the platform's
|
|
8669
|
+
* for a light-dismissing popover.
|
|
8670
|
+
*
|
|
8671
|
+
* The placement is the menus' own geometry, below the anchor and
|
|
8672
|
+
* start-aligned, clamped into the viewport; `flip` moves it above the
|
|
8673
|
+
* anchor where the viewport leaves no room below, as the css
|
|
8674
|
+
* `position-try-fallbacks` the paired popovers declare. A popover not
|
|
8675
|
+
* already open is shown and placed before the call returns, so the first
|
|
8676
|
+
* position anybody sees is the right one and the caller can move the focus
|
|
8677
|
+
* into it on the next line.
|
|
8678
|
+
*
|
|
8679
|
+
* @param {HTMLElement} popover the `[popover]` element to show, or an
|
|
8680
|
+
* already-shown element to place
|
|
8681
|
+
* @param {Element|DOMRect} anchor the element, or the rectangle, to place
|
|
8682
|
+
* beside
|
|
8683
|
+
* @param {object} [options]
|
|
8684
|
+
* @param {boolean} [options.flip] place above the anchor where no room is
|
|
8685
|
+
* left below
|
|
8686
|
+
* @param {number} [options.gap] the distance from the anchor, in pixels
|
|
8687
|
+
* @returns {HTMLElement} the popover
|
|
8688
|
+
*/
|
|
8689
|
+
static show(popover, anchor, { flip = false, gap = 0 } = {}) {
|
|
8690
|
+
const placeShown = () => {
|
|
8691
|
+
const box = anchor instanceof Element ? anchor.getBoundingClientRect() : anchor;
|
|
8692
|
+
const viewport = document.documentElement;
|
|
8693
|
+
const here = popover.getBoundingClientRect();
|
|
8694
|
+
const below = box.bottom + gap;
|
|
8695
|
+
const above = box.top - gap - here.height;
|
|
8696
|
+
const top = flip && below + here.height > viewport.clientHeight - PAD && above >= PAD ? above : below;
|
|
8697
|
+
popover.style.right = 'auto';
|
|
8698
|
+
popover.style.bottom = 'auto';
|
|
8699
|
+
popover.style.margin = '0';
|
|
8700
|
+
popover.style.left = `${clamp(box.left, PAD, Math.max(PAD, viewport.clientWidth - here.width - PAD))}px`;
|
|
8701
|
+
popover.style.top = `${clamp(top, PAD, Math.max(PAD, viewport.clientHeight - here.height - PAD))}px`;
|
|
8702
|
+
};
|
|
8703
|
+
if (typeof popover.showPopover === 'function' && !popover.matches(':popover-open')) {
|
|
8704
|
+
popover.showPopover();
|
|
8705
|
+
}
|
|
8706
|
+
placeShown();
|
|
8707
|
+
return popover;
|
|
8708
|
+
}
|
|
8585
8709
|
}
|
|
8586
8710
|
|
|
8587
8711
|
/**
|
|
@@ -8871,6 +8995,11 @@ var fml = (function (exports) {
|
|
|
8871
8995
|
//from the host when it gave one, since it has to set aria-controls before
|
|
8872
8996
|
//this element upgrades
|
|
8873
8997
|
this.#menu.id = this.declared('listbox') || Attributes.uid('ful-listbox');
|
|
8998
|
+
this.#menu.addEventListener('mousedown', (evt) => {
|
|
8999
|
+
if (evt.target.closest('li')) {
|
|
9000
|
+
evt.preventDefault();
|
|
9001
|
+
}
|
|
9002
|
+
});
|
|
8874
9003
|
this.#menu.addEventListener('click', (evt) => {
|
|
8875
9004
|
evt.stopPropagation();
|
|
8876
9005
|
const li = evt.target.closest('li');
|
|
@@ -8878,12 +9007,19 @@ var fml = (function (exports) {
|
|
|
8878
9007
|
this.hide();
|
|
8879
9008
|
return;
|
|
8880
9009
|
}
|
|
9010
|
+
if (li.matches('[aria-disabled="true"]')) {
|
|
9011
|
+
return;
|
|
9012
|
+
}
|
|
8881
9013
|
this.#change(li);
|
|
8882
9014
|
});
|
|
8883
9015
|
this.replaceChildren(fragment);
|
|
8884
9016
|
}
|
|
8885
9017
|
#selected() {
|
|
8886
|
-
return
|
|
9018
|
+
return (
|
|
9019
|
+
this.#menu?.querySelector('[selected]:not([aria-disabled="true"])') ??
|
|
9020
|
+
this.#menu?.querySelector('li:not([aria-disabled="true"])') ??
|
|
9021
|
+
null
|
|
9022
|
+
);
|
|
8887
9023
|
}
|
|
8888
9024
|
#highlight(li) {
|
|
8889
9025
|
if (!li) {
|
|
@@ -8915,11 +9051,24 @@ var fml = (function (exports) {
|
|
|
8915
9051
|
const data = values.map((entry, index) => ({ index, ...entry }));
|
|
8916
9052
|
this.#optionstemplate.withOverlay(data).renderTo(this.#menu);
|
|
8917
9053
|
for (const [index, li] of [...this.#menu.children].entries()) {
|
|
8918
|
-
const
|
|
9054
|
+
const entry = values[index];
|
|
9055
|
+
const picked = keys.some((r) => r == entry?.key);
|
|
8919
9056
|
li.toggleAttribute('picked', picked);
|
|
8920
9057
|
//what is picked is what aria-selected means for a listbox: a tint alone
|
|
8921
9058
|
//says it to whoever can see it and to no one else
|
|
8922
9059
|
li.setAttribute('aria-selected', picked ? 'true' : 'false');
|
|
9060
|
+
//metadata.disabled refuses the entry, metadata.reason saying why
|
|
9061
|
+
const disabled = !!entry?.metadata?.disabled;
|
|
9062
|
+
if (disabled) {
|
|
9063
|
+
li.setAttribute('aria-disabled', 'true');
|
|
9064
|
+
} else {
|
|
9065
|
+
li.removeAttribute('aria-disabled');
|
|
9066
|
+
}
|
|
9067
|
+
if (disabled && entry.metadata.reason) {
|
|
9068
|
+
li.setAttribute('title', String(entry.metadata.reason));
|
|
9069
|
+
} else {
|
|
9070
|
+
li.removeAttribute('title');
|
|
9071
|
+
}
|
|
8923
9072
|
}
|
|
8924
9073
|
this.#empty.toggleAttribute('hidden', values.length !== 0);
|
|
8925
9074
|
this.#menu.toggleAttribute('hidden', values.length === 0);
|
|
@@ -8993,15 +9142,25 @@ var fml = (function (exports) {
|
|
|
8993
9142
|
if (this.shown) {
|
|
8994
9143
|
const selected = this.#selected();
|
|
8995
9144
|
const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
8996
|
-
|
|
8997
|
-
|
|
9145
|
+
const target = candidate ? this.#walk(candidate, forward) : null;
|
|
9146
|
+
if (selected && target) {
|
|
9147
|
+
this.#highlight(target);
|
|
8998
9148
|
}
|
|
8999
9149
|
return;
|
|
9000
9150
|
}
|
|
9001
9151
|
await this.show(loader, keys);
|
|
9002
9152
|
}
|
|
9153
|
+
#walk(from, forward) {
|
|
9154
|
+
for (let li = from; li; li = li[`${forward ? 'next' : 'previous'}ElementSibling`]) {
|
|
9155
|
+
if (!li.matches('[aria-disabled="true"]')) {
|
|
9156
|
+
return li;
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
return null;
|
|
9160
|
+
}
|
|
9003
9161
|
jump(first) {
|
|
9004
|
-
const
|
|
9162
|
+
const edge = first ? this.#menu.firstElementChild : this.#menu.lastElementChild;
|
|
9163
|
+
const target = this.#walk(edge, first);
|
|
9005
9164
|
if (target) {
|
|
9006
9165
|
this.#highlight(target);
|
|
9007
9166
|
}
|
|
@@ -9011,9 +9170,14 @@ var fml = (function (exports) {
|
|
|
9011
9170
|
if (!selected) {
|
|
9012
9171
|
return;
|
|
9013
9172
|
}
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9173
|
+
let target = selected;
|
|
9174
|
+
for (let i = 0; i !== this.#page(); ++i) {
|
|
9175
|
+
const next = this.#walk(target[`${forward ? 'next' : 'previous'}ElementSibling`], forward);
|
|
9176
|
+
if (!next) {
|
|
9177
|
+
break;
|
|
9178
|
+
}
|
|
9179
|
+
target = next;
|
|
9180
|
+
}
|
|
9017
9181
|
this.#highlight(target);
|
|
9018
9182
|
}
|
|
9019
9183
|
#page() {
|
|
@@ -9049,6 +9213,12 @@ var fml = (function (exports) {
|
|
|
9049
9213
|
//`set value` normalizes a list of one to a single key, and the getter answers a
|
|
9050
9214
|
//scalar for a single select, so nothing downstream has to know which it was
|
|
9051
9215
|
static observed = ['multiple:presence', 'item-list:presence', 'value:csv'];
|
|
9216
|
+
unmarshal(attr, str) {
|
|
9217
|
+
if (attr === 'value' && str === '' && !this.hasAttribute('multiple')) {
|
|
9218
|
+
return [''];
|
|
9219
|
+
}
|
|
9220
|
+
return super.unmarshal(attr, str);
|
|
9221
|
+
}
|
|
9052
9222
|
static slots = true;
|
|
9053
9223
|
//a manual popover: the combobox keeps the focus on its input and owns
|
|
9054
9224
|
//the whole lifecycle (typing, arrows, blur, Escape, Tab), so no light
|
|
@@ -10142,7 +10312,7 @@ var fml = (function (exports) {
|
|
|
10142
10312
|
<tbody data-ref="empty" hidden>
|
|
10143
10313
|
<tr>
|
|
10144
10314
|
<td data-tpl-colspan="schema.length">
|
|
10145
|
-
<ful-empty data-tpl-if="slots.empty"
|
|
10315
|
+
<ful-empty data-tpl-if="slots.empty" data-ref="empty-authors"></ful-empty>
|
|
10146
10316
|
<ful-empty data-tpl-if="!slots.empty">
|
|
10147
10317
|
<ful-icon data-tpl-if="config.emptyIcon" data-tpl-name="config.emptyIcon" aria-hidden="true"></ful-icon>
|
|
10148
10318
|
{{ #l10n:t('table.no-data') }}
|
|
@@ -10185,6 +10355,8 @@ var fml = (function (exports) {
|
|
|
10185
10355
|
#loading;
|
|
10186
10356
|
#noAutoload;
|
|
10187
10357
|
#empty;
|
|
10358
|
+
#emptyAuthors;
|
|
10359
|
+
#emptyTemplate;
|
|
10188
10360
|
#feedback;
|
|
10189
10361
|
#paginator;
|
|
10190
10362
|
#sorters;
|
|
@@ -10236,6 +10408,9 @@ var fml = (function (exports) {
|
|
|
10236
10408
|
this.#loading = table.querySelector(':scope > tbody[data-ref=loading]');
|
|
10237
10409
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
10238
10410
|
this.#empty = table.querySelector(':scope > tbody[data-ref=empty]');
|
|
10411
|
+
this.#emptyAuthors = this.#empty.querySelector('ful-empty[data-ref=empty-authors]');
|
|
10412
|
+
this.#emptyTemplate =
|
|
10413
|
+
slots.empty && !Fragments.isBlank(slots.empty) ? Templates.fromFragment(slots.empty) : null;
|
|
10239
10414
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
10240
10415
|
this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
10241
10416
|
this.replaceChildren(fragment);
|
|
@@ -10407,20 +10582,124 @@ var fml = (function (exports) {
|
|
|
10407
10582
|
.render(),
|
|
10408
10583
|
);
|
|
10409
10584
|
this.#empty.toggleAttribute('hidden', pageResponse.data.length !== 0);
|
|
10585
|
+
if (this.#emptyTemplate && pageResponse.data.length === 0) {
|
|
10586
|
+
//rendered per load, the rows' own scope: the empty state can say
|
|
10587
|
+
//whether nothing was ever there or the filters matched nothing
|
|
10588
|
+
this.#emptyAuthors.replaceChildren(
|
|
10589
|
+
this.#emptyTemplate
|
|
10590
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
10591
|
+
.render(),
|
|
10592
|
+
);
|
|
10593
|
+
}
|
|
10410
10594
|
//one move, one repaint: the page and the count are the same state
|
|
10411
10595
|
this.#paginator.update({ current: pageRequest.page, total: pages });
|
|
10412
10596
|
}
|
|
10413
10597
|
}
|
|
10414
10598
|
|
|
10599
|
+
/** how long a type-ahead buffer stands before the next character starts a new search */
|
|
10600
|
+
const TYPEAHEAD_WINDOW = 500;
|
|
10601
|
+
|
|
10602
|
+
/**
|
|
10603
|
+
* The keyboard protocol of a menu popover: the focus moving into the menu as it
|
|
10604
|
+
* opens and back to the invoker as it closes, roving focus over the items with
|
|
10605
|
+
* the arrows, Home and End, type-ahead over their text, and Enter and Space
|
|
10606
|
+
* activating the focused one.
|
|
10607
|
+
*
|
|
10608
|
+
* The menu owns what an item is: `items` answers them in the order they are
|
|
10609
|
+
* walked, read on every gesture so a menu that fills itself later needs no
|
|
10610
|
+
* rewiring, and `current` names the one an opening focuses, the first otherwise.
|
|
10611
|
+
*
|
|
10612
|
+
* @param {HTMLElement} invoker the element the menu belongs to, and where the focus goes back
|
|
10613
|
+
* @param {HTMLElement} menu the `[popover]` holding the items
|
|
10614
|
+
* @param {{items: () => HTMLElement[], current?: () => HTMLElement|null|undefined}} conf
|
|
10615
|
+
*/
|
|
10616
|
+
const wireMenuKeys = (invoker, menu, { items, current = () => null }) => {
|
|
10617
|
+
let typed = '';
|
|
10618
|
+
let typedAt = 0;
|
|
10619
|
+
menu.addEventListener('toggle', (/** @type any */ evt) => {
|
|
10620
|
+
if (evt.newState !== 'open') {
|
|
10621
|
+
//give the invoker back the focus the menu had borrowed, without
|
|
10622
|
+
//stealing it from wherever else the close came from
|
|
10623
|
+
if (menu.contains(document.activeElement)) {
|
|
10624
|
+
invoker.focus();
|
|
10625
|
+
}
|
|
10626
|
+
return;
|
|
10627
|
+
}
|
|
10628
|
+
const all = items();
|
|
10629
|
+
(current() ?? all[0])?.focus();
|
|
10630
|
+
});
|
|
10631
|
+
menu.addEventListener('keydown', (evt) => {
|
|
10632
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
10633
|
+
const all = items();
|
|
10634
|
+
const item = all.find((candidate) => candidate === target || candidate.contains(target));
|
|
10635
|
+
if (!item) {
|
|
10636
|
+
return;
|
|
10637
|
+
}
|
|
10638
|
+
const at = all.indexOf(item);
|
|
10639
|
+
switch (evt.code) {
|
|
10640
|
+
case 'ArrowDown': {
|
|
10641
|
+
evt.preventDefault();
|
|
10642
|
+
all[(at + 1) % all.length]?.focus();
|
|
10643
|
+
return;
|
|
10644
|
+
}
|
|
10645
|
+
case 'ArrowUp': {
|
|
10646
|
+
evt.preventDefault();
|
|
10647
|
+
all[(at - 1 + all.length) % all.length]?.focus();
|
|
10648
|
+
return;
|
|
10649
|
+
}
|
|
10650
|
+
case 'Home': {
|
|
10651
|
+
evt.preventDefault();
|
|
10652
|
+
all[0]?.focus();
|
|
10653
|
+
return;
|
|
10654
|
+
}
|
|
10655
|
+
case 'End': {
|
|
10656
|
+
evt.preventDefault();
|
|
10657
|
+
all[all.length - 1]?.focus();
|
|
10658
|
+
return;
|
|
10659
|
+
}
|
|
10660
|
+
case 'Enter':
|
|
10661
|
+
case 'Space': {
|
|
10662
|
+
evt.preventDefault();
|
|
10663
|
+
item.click();
|
|
10664
|
+
invoker.focus();
|
|
10665
|
+
return;
|
|
10666
|
+
}
|
|
10667
|
+
case 'Escape': {
|
|
10668
|
+
//the platform's close request hides the menu, the focus is placed
|
|
10669
|
+
//on the invoker before the focused item is detached from it
|
|
10670
|
+
invoker.focus();
|
|
10671
|
+
return;
|
|
10672
|
+
}
|
|
10673
|
+
}
|
|
10674
|
+
if (evt.key.length !== 1 || evt.ctrlKey || evt.altKey || evt.metaKey) {
|
|
10675
|
+
return;
|
|
10676
|
+
}
|
|
10677
|
+
const now = Date.now();
|
|
10678
|
+
typed = now - typedAt > TYPEAHEAD_WINDOW ? evt.key : typed + evt.key;
|
|
10679
|
+
typedAt = now;
|
|
10680
|
+
const wanted = typed.toLowerCase();
|
|
10681
|
+
const repeated = [...wanted].every((character) => character === wanted[0]);
|
|
10682
|
+
const search = repeated ? wanted[0] : wanted;
|
|
10683
|
+
const from = repeated ? at + 1 : at;
|
|
10684
|
+
const found = [...all.slice(from), ...all.slice(0, from)].find((candidate) =>
|
|
10685
|
+
(candidate.textContent ?? '').trim().toLowerCase().startsWith(search),
|
|
10686
|
+
);
|
|
10687
|
+
if (found) {
|
|
10688
|
+
evt.preventDefault();
|
|
10689
|
+
found.focus();
|
|
10690
|
+
}
|
|
10691
|
+
});
|
|
10692
|
+
};
|
|
10693
|
+
|
|
10415
10694
|
/**
|
|
10416
10695
|
* An invoker button paired with the `ul[popover][role=menu]` that follows it:
|
|
10417
10696
|
* the chrome behind every filter's operator, sensitivity and boolean value.
|
|
10418
10697
|
*
|
|
10419
10698
|
* It fills the menu from a vocabulary, wires it the first time more than one
|
|
10420
10699
|
* choice survives the whitelist, pins the button to a static glyph when a
|
|
10421
|
-
* single one does,
|
|
10422
|
-
*
|
|
10423
|
-
*
|
|
10700
|
+
* single one does, takes the shared menu keyboard protocol, and keeps the
|
|
10701
|
+
* button's value, glyph and aria-label in step. A pick that changes the value
|
|
10702
|
+
* calls back; the host decides what that means.
|
|
10424
10703
|
*
|
|
10425
10704
|
* The button's `value` attribute is the store, as it is for a native control:
|
|
10426
10705
|
* the menu protocol finds the current item by it, and nothing mirrors it.
|
|
@@ -10497,6 +10776,13 @@ var fml = (function (exports) {
|
|
|
10497
10776
|
get pinned() {
|
|
10498
10777
|
return this.#allowed.length < 2;
|
|
10499
10778
|
}
|
|
10779
|
+
/**
|
|
10780
|
+
* The host's fixed claim: the button and its chrome leave while the value
|
|
10781
|
+
* keeps answering, which is what a fixed operator asks of its glyph.
|
|
10782
|
+
*/
|
|
10783
|
+
set fixed(fixed) {
|
|
10784
|
+
this.#button.toggleAttribute('hidden', !!fixed);
|
|
10785
|
+
}
|
|
10500
10786
|
get value() {
|
|
10501
10787
|
return this.#button.getAttribute('value');
|
|
10502
10788
|
}
|
|
@@ -10571,64 +10857,10 @@ var fml = (function (exports) {
|
|
|
10571
10857
|
return Array.from(this.#menu.querySelectorAll('li > a'), (a) => /** @type HTMLAnchorElement */ (a));
|
|
10572
10858
|
}
|
|
10573
10859
|
#wire() {
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
if (evt.newState !== 'open') {
|
|
10579
|
-
//give the invoker back the focus the menu had borrowed, without
|
|
10580
|
-
//stealing it from wherever else the close came from
|
|
10581
|
-
if (menu.contains(document.activeElement)) {
|
|
10582
|
-
button.focus();
|
|
10583
|
-
}
|
|
10584
|
-
return;
|
|
10585
|
-
}
|
|
10586
|
-
const items = this.#items();
|
|
10587
|
-
(items.find((a) => a.getAttribute('value') === this.value) ?? items[0])?.focus();
|
|
10588
|
-
});
|
|
10589
|
-
menu.addEventListener('keydown', (evt) => {
|
|
10590
|
-
const target = /** @type HTMLElement */ (evt.target);
|
|
10591
|
-
const item = /** @type HTMLAnchorElement | null */ (target.closest('li > a'));
|
|
10592
|
-
if (!item) {
|
|
10593
|
-
return;
|
|
10594
|
-
}
|
|
10595
|
-
const items = this.#items();
|
|
10596
|
-
const at = items.indexOf(item);
|
|
10597
|
-
switch (evt.code) {
|
|
10598
|
-
case 'ArrowDown': {
|
|
10599
|
-
evt.preventDefault();
|
|
10600
|
-
items[(at + 1) % items.length]?.focus();
|
|
10601
|
-
break;
|
|
10602
|
-
}
|
|
10603
|
-
case 'ArrowUp': {
|
|
10604
|
-
evt.preventDefault();
|
|
10605
|
-
items[(at - 1 + items.length) % items.length]?.focus();
|
|
10606
|
-
break;
|
|
10607
|
-
}
|
|
10608
|
-
case 'Home': {
|
|
10609
|
-
evt.preventDefault();
|
|
10610
|
-
items[0]?.focus();
|
|
10611
|
-
break;
|
|
10612
|
-
}
|
|
10613
|
-
case 'End': {
|
|
10614
|
-
evt.preventDefault();
|
|
10615
|
-
items[items.length - 1]?.focus();
|
|
10616
|
-
break;
|
|
10617
|
-
}
|
|
10618
|
-
case 'Enter':
|
|
10619
|
-
case 'Space': {
|
|
10620
|
-
evt.preventDefault();
|
|
10621
|
-
item.click();
|
|
10622
|
-
button.focus();
|
|
10623
|
-
break;
|
|
10624
|
-
}
|
|
10625
|
-
case 'Escape': {
|
|
10626
|
-
//the platform's close request hides the menu, the focus is placed
|
|
10627
|
-
//on the invoker before the focused item is detached from it
|
|
10628
|
-
button.focus();
|
|
10629
|
-
break;
|
|
10630
|
-
}
|
|
10631
|
-
}
|
|
10860
|
+
Anchors.wire(this.#button, this.#menu, { prefix: 'ful-filter-menu', invoke: true, expanded: true });
|
|
10861
|
+
wireMenuKeys(this.#button, this.#menu, {
|
|
10862
|
+
items: () => this.#items(),
|
|
10863
|
+
current: () => this.#items().find((a) => a.getAttribute('value') === this.value),
|
|
10632
10864
|
});
|
|
10633
10865
|
}
|
|
10634
10866
|
}
|
|
@@ -10680,6 +10912,7 @@ var fml = (function (exports) {
|
|
|
10680
10912
|
*/
|
|
10681
10913
|
class CompareFilter extends Input {
|
|
10682
10914
|
static observed = ['value:json', 'operators:csv'];
|
|
10915
|
+
static attributes = ['operator'];
|
|
10683
10916
|
static OPERATORS = COMPARE_OPERATORS;
|
|
10684
10917
|
static DEFAULT_OPERATOR = 'EQ';
|
|
10685
10918
|
static template = `
|
|
@@ -10730,11 +10963,38 @@ var fml = (function (exports) {
|
|
|
10730
10963
|
if (this._operator.value === null) {
|
|
10731
10964
|
this._showDefaultOperator();
|
|
10732
10965
|
}
|
|
10966
|
+
this.#fixOperator();
|
|
10733
10967
|
//the second operand mirrors the claims like the first one does, and the
|
|
10734
10968
|
//freeze reaches the operator and sensitivity buttons, whose popovers an
|
|
10735
10969
|
//input's readOnly cannot touch
|
|
10736
10970
|
return { ...pieces, freeze: this._container, also: [this._value2] };
|
|
10737
10971
|
}
|
|
10972
|
+
/**
|
|
10973
|
+
* The singular `operator` fixes the operator for the element's life: not a
|
|
10974
|
+
* choice, so no menu and no button, every tuple carrying it. The plural is
|
|
10975
|
+
* the menu's whitelist; declaring both is a confused page and says so,
|
|
10976
|
+
* the singular winning, and an unknown name warns and pins the default.
|
|
10977
|
+
*/
|
|
10978
|
+
#fixOperator() {
|
|
10979
|
+
const fixed = this.declared('operator');
|
|
10980
|
+
if (fixed === null) {
|
|
10981
|
+
return;
|
|
10982
|
+
}
|
|
10983
|
+
if (this.hasAttribute('operators')) {
|
|
10984
|
+
console.warn(`${this.localName}: operator fixes what operators offers a menu for, the plural is ignored`, this);
|
|
10985
|
+
}
|
|
10986
|
+
const vocabulary = this._vocabulary();
|
|
10987
|
+
/** @type {string|null} */
|
|
10988
|
+
let preferred = null;
|
|
10989
|
+
if (vocabulary.includes(fixed)) {
|
|
10990
|
+
preferred = fixed;
|
|
10991
|
+
} else {
|
|
10992
|
+
console.warn(`${this.localName}: '${fixed}' is not one of its operators, the default is pinned instead`, this);
|
|
10993
|
+
preferred = this._defaultOperator();
|
|
10994
|
+
}
|
|
10995
|
+
this._operator.allowed = [preferred];
|
|
10996
|
+
this._operator.fixed = true;
|
|
10997
|
+
}
|
|
10738
10998
|
_showDefaultOperator() {
|
|
10739
10999
|
const preferred = this._defaultOperator();
|
|
10740
11000
|
const allowed = this._operator.allowed;
|
|
@@ -10771,6 +11031,9 @@ var fml = (function (exports) {
|
|
|
10771
11031
|
return this._operator ? this._operator.allowed : this._declaredOperators;
|
|
10772
11032
|
}
|
|
10773
11033
|
set operators(declared) {
|
|
11034
|
+
if (this.declared('operator') !== null) {
|
|
11035
|
+
return;
|
|
11036
|
+
}
|
|
10774
11037
|
if (!this._operator) {
|
|
10775
11038
|
this._declaredOperators = ChoiceButton.narrow(declared, this._vocabulary());
|
|
10776
11039
|
return;
|
|
@@ -10874,6 +11137,7 @@ var fml = (function (exports) {
|
|
|
10874
11137
|
/** The compare filter over text, carrying a case sensitivity beside the operator. */
|
|
10875
11138
|
class TextFilter extends CompareFilter {
|
|
10876
11139
|
static observed = ['sensitivities:csv'];
|
|
11140
|
+
static attributes = ['sensitivity'];
|
|
10877
11141
|
static template = `
|
|
10878
11142
|
<label>{{{{ slots.default }}}}</label>
|
|
10879
11143
|
{{{{ slots.info }}}}
|
|
@@ -10917,8 +11181,28 @@ var fml = (function (exports) {
|
|
|
10917
11181
|
);
|
|
10918
11182
|
this._sensitivityButton.allowed = null;
|
|
10919
11183
|
this._sensitivityButton.value = SENSITIVITIES[0];
|
|
11184
|
+
this.#fixSensitivity();
|
|
10920
11185
|
return pieces;
|
|
10921
11186
|
}
|
|
11187
|
+
#fixSensitivity() {
|
|
11188
|
+
const fixed = this.declared('sensitivity');
|
|
11189
|
+
if (fixed === null) {
|
|
11190
|
+
return;
|
|
11191
|
+
}
|
|
11192
|
+
if (this.hasAttribute('sensitivities')) {
|
|
11193
|
+
console.warn('ful-filter-text: sensitivity fixes what sensitivities offers a menu for, the plural is ignored', this);
|
|
11194
|
+
}
|
|
11195
|
+
/** @type {string|null} */
|
|
11196
|
+
let preferred = null;
|
|
11197
|
+
if (SENSITIVITIES.includes(fixed)) {
|
|
11198
|
+
preferred = fixed;
|
|
11199
|
+
} else {
|
|
11200
|
+
console.warn(`ful-filter-text: '${fixed}' is not one of its sensitivities, the default is pinned instead`, this);
|
|
11201
|
+
preferred = 'IGNORE_CASE';
|
|
11202
|
+
}
|
|
11203
|
+
this._sensitivityButton.allowed = [preferred];
|
|
11204
|
+
this._sensitivityButton.fixed = true;
|
|
11205
|
+
}
|
|
10922
11206
|
_choices() {
|
|
10923
11207
|
return [...super._choices(), this._sensitivityButton].filter((c) => c);
|
|
10924
11208
|
}
|
|
@@ -10930,6 +11214,9 @@ var fml = (function (exports) {
|
|
|
10930
11214
|
return this._sensitivityButton ? this._sensitivityButton.allowed : this._declaredSensitivities;
|
|
10931
11215
|
}
|
|
10932
11216
|
set sensitivities(declared) {
|
|
11217
|
+
if (this.declared('sensitivity') !== null) {
|
|
11218
|
+
return;
|
|
11219
|
+
}
|
|
10933
11220
|
if (!this._sensitivityButton) {
|
|
10934
11221
|
this._declaredSensitivities = ChoiceButton.narrow(declared, SENSITIVITIES);
|
|
10935
11222
|
return;
|
|
@@ -11059,6 +11346,15 @@ var fml = (function (exports) {
|
|
|
11059
11346
|
this._operator.value = this._operator.pinned ? this._operator.allowed[0] : v[0];
|
|
11060
11347
|
this._value.value = v[1] ?? '';
|
|
11061
11348
|
}
|
|
11349
|
+
formResetCallback() {
|
|
11350
|
+
super.formResetCallback();
|
|
11351
|
+
if (!this.hasAttribute('value')) {
|
|
11352
|
+
const allowed = this._operator.allowed;
|
|
11353
|
+
this._operator.value = allowed.includes(BooleanFilter.DEFAULT_OPERATOR)
|
|
11354
|
+
? BooleanFilter.DEFAULT_OPERATOR
|
|
11355
|
+
: allowed[0];
|
|
11356
|
+
}
|
|
11357
|
+
}
|
|
11062
11358
|
/** @returns {FilterCriterion|null} */
|
|
11063
11359
|
get criterion() {
|
|
11064
11360
|
const token = this._value.value;
|
|
@@ -11355,6 +11651,15 @@ var fml = (function (exports) {
|
|
|
11355
11651
|
this.#body = fragment.querySelector('[data-ref=body]');
|
|
11356
11652
|
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11357
11653
|
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11654
|
+
//a dialog is a named surface: a reader announces the heading on opening,
|
|
11655
|
+
//which it cannot do unless the heading names the dialog. The name travels
|
|
11656
|
+
//as a reference rather than as aria-label text, so it stays the heading's
|
|
11657
|
+
//own however it is reworded
|
|
11658
|
+
const heading = fragment.querySelector('h2');
|
|
11659
|
+
if (heading) {
|
|
11660
|
+
heading.id ||= Attributes.uid('ful-dialog-title');
|
|
11661
|
+
this.#dialog.setAttribute('aria-labelledby', heading.id);
|
|
11662
|
+
}
|
|
11358
11663
|
this.#dialog.addEventListener('close', () => {
|
|
11359
11664
|
const outcome = this.#outcome();
|
|
11360
11665
|
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
@@ -11522,6 +11827,87 @@ var fml = (function (exports) {
|
|
|
11522
11827
|
this.#loading?.setAttribute('hidden', '');
|
|
11523
11828
|
this.#body?.removeAttribute('hidden');
|
|
11524
11829
|
}
|
|
11830
|
+
/**
|
|
11831
|
+
* Asks a question in a dialog of the page's own, resolving with its outcome
|
|
11832
|
+
* and removing the element afterwards: the element stays the author's when
|
|
11833
|
+
* the question lives in the page, while a question asked from a button had
|
|
11834
|
+
* to build, render, show, answer and clean up by hand.
|
|
11835
|
+
*
|
|
11836
|
+
* The body is text or a node: a string is never read as markup, an
|
|
11837
|
+
* `innerHTML` sink being a thing the library does not carry. Each button is
|
|
11838
|
+
* a `[result, label, className?]` tuple rendered as its answer, or a node
|
|
11839
|
+
* the caller has built itself, which is how a button carries anything a
|
|
11840
|
+
* tuple cannot say and how a footer gets a spacer.
|
|
11841
|
+
* @param {string} header
|
|
11842
|
+
* @param {string|Node} body
|
|
11843
|
+
* @param {([string, string, string?]|Node)[]} [buttons]
|
|
11844
|
+
* @param {{ className?: string }} [options] `className` dresses the dialog,
|
|
11845
|
+
* the way a declared `ful-dialog` is given one to size it
|
|
11846
|
+
* @returns {Promise<DialogOutcome>}
|
|
11847
|
+
*/
|
|
11848
|
+
static async ask(header, body, buttons = [], { className } = {}) {
|
|
11849
|
+
const dialog = document.createElement('ful-dialog');
|
|
11850
|
+
dialog.setAttribute('header', header);
|
|
11851
|
+
if (className) {
|
|
11852
|
+
dialog.className = className;
|
|
11853
|
+
}
|
|
11854
|
+
if (typeof body === 'string') {
|
|
11855
|
+
dialog.textContent = body;
|
|
11856
|
+
} else if (body) {
|
|
11857
|
+
dialog.append(body);
|
|
11858
|
+
}
|
|
11859
|
+
//buttons are slotted only when there are any: the acknowledge button
|
|
11860
|
+
//is the answer of a question asked without choices of its own
|
|
11861
|
+
if (buttons.length > 0) {
|
|
11862
|
+
const choices = document.createElement('template');
|
|
11863
|
+
choices.setAttribute('slot', 'buttons');
|
|
11864
|
+
for (const choice of buttons) {
|
|
11865
|
+
if (choice instanceof Node) {
|
|
11866
|
+
choices.content.append(choice);
|
|
11867
|
+
continue;
|
|
11868
|
+
}
|
|
11869
|
+
const [result, label, buttonClass] = choice;
|
|
11870
|
+
const button = document.createElement('button');
|
|
11871
|
+
button.type = 'button';
|
|
11872
|
+
button.dataset.result = result;
|
|
11873
|
+
if (buttonClass) {
|
|
11874
|
+
button.className = buttonClass;
|
|
11875
|
+
}
|
|
11876
|
+
button.textContent = label;
|
|
11877
|
+
choices.content.append(button);
|
|
11878
|
+
}
|
|
11879
|
+
dialog.append(choices);
|
|
11880
|
+
}
|
|
11881
|
+
document.body.append(dialog);
|
|
11882
|
+
try {
|
|
11883
|
+
await Rendering.waitFor(dialog);
|
|
11884
|
+
return await /** @type {Dialog} */ (dialog).ask();
|
|
11885
|
+
} finally {
|
|
11886
|
+
dialog.remove();
|
|
11887
|
+
}
|
|
11888
|
+
}
|
|
11889
|
+
/**
|
|
11890
|
+
* Asks a yes/no question, resolving with the confirm button's answer:
|
|
11891
|
+
* the cancel button, the close button and Escape all answer false, a
|
|
11892
|
+
* dismissal being the negative.
|
|
11893
|
+
* @param {string} header
|
|
11894
|
+
* @param {string|Node} body
|
|
11895
|
+
* @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
|
|
11896
|
+
* @param {{ className?: string }} [options] as `ask` takes them
|
|
11897
|
+
* @returns {Promise<boolean>}
|
|
11898
|
+
*/
|
|
11899
|
+
static async confirm(header, body, labels = {}, options = {}) {
|
|
11900
|
+
const outcome = await Dialog.ask(
|
|
11901
|
+
header,
|
|
11902
|
+
body,
|
|
11903
|
+
[
|
|
11904
|
+
['cancel', labels.cancel ?? Localization.of().t('dialog.cancel')],
|
|
11905
|
+
['confirm', labels.confirm ?? Localization.of().t('dialog.confirm'), 'ful-button'],
|
|
11906
|
+
],
|
|
11907
|
+
options,
|
|
11908
|
+
);
|
|
11909
|
+
return !outcome.dismissed && outcome.result === 'confirm';
|
|
11910
|
+
}
|
|
11525
11911
|
}
|
|
11526
11912
|
|
|
11527
11913
|
/**
|
|
@@ -11534,7 +11920,7 @@ var fml = (function (exports) {
|
|
|
11534
11920
|
* it.
|
|
11535
11921
|
*/
|
|
11536
11922
|
class Drawer extends ParsedElement {
|
|
11537
|
-
static attributes = ['
|
|
11923
|
+
static attributes = ['header', 'placement', 'close-on-submit:presence'];
|
|
11538
11924
|
static slots = true;
|
|
11539
11925
|
static template = `
|
|
11540
11926
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -11559,12 +11945,23 @@ var fml = (function (exports) {
|
|
|
11559
11945
|
//a save answering with no body at all is still a save
|
|
11560
11946
|
/** @type {{ dismissed: boolean, response: any }|null} */
|
|
11561
11947
|
#answer = null;
|
|
11948
|
+
#closing = false;
|
|
11949
|
+
#slideOutEnded = (/** @type AnimationEvent */ e) => {
|
|
11950
|
+
if (e.target === this.#dialog) {
|
|
11951
|
+
this.#closeNow();
|
|
11952
|
+
}
|
|
11953
|
+
};
|
|
11562
11954
|
render({ slots }) {
|
|
11563
11955
|
const fragment = this.template()
|
|
11564
|
-
.withOverlay({ slots, title: this.declared('
|
|
11956
|
+
.withOverlay({ slots, title: this.declared('header') ?? '' })
|
|
11565
11957
|
.render();
|
|
11566
11958
|
this.#dialog = fragment.querySelector('[data-ref=dialog]');
|
|
11567
11959
|
this.#title = fragment.querySelector('[data-ref=title]');
|
|
11960
|
+
//a named surface, like a dialog: the heading announces the drawer on
|
|
11961
|
+
//opening only where it names it, and the reference survives every
|
|
11962
|
+
//textContent update() writes into the heading
|
|
11963
|
+
this.#title.id ||= Attributes.uid('ful-drawer-title');
|
|
11964
|
+
this.#dialog.setAttribute('aria-labelledby', this.#title.id);
|
|
11568
11965
|
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11569
11966
|
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11570
11967
|
this.#content = fragment.querySelector('[data-ref=content]');
|
|
@@ -11582,6 +11979,10 @@ var fml = (function (exports) {
|
|
|
11582
11979
|
this.close();
|
|
11583
11980
|
}
|
|
11584
11981
|
});
|
|
11982
|
+
this.#dialog.addEventListener('cancel', (e) => {
|
|
11983
|
+
e.preventDefault();
|
|
11984
|
+
this.close();
|
|
11985
|
+
});
|
|
11585
11986
|
this.#dialog.addEventListener('close', () => {
|
|
11586
11987
|
this.dispatchEvent(
|
|
11587
11988
|
new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
|
|
@@ -11605,23 +12006,23 @@ var fml = (function (exports) {
|
|
|
11605
12006
|
this.replaceChildren(fragment);
|
|
11606
12007
|
wireTargets();
|
|
11607
12008
|
}
|
|
11608
|
-
get
|
|
12009
|
+
get header() {
|
|
11609
12010
|
return this.#title.textContent;
|
|
11610
12011
|
}
|
|
11611
|
-
set
|
|
12012
|
+
set header(v) {
|
|
11612
12013
|
this.#title.textContent = v ?? '';
|
|
11613
12014
|
}
|
|
11614
12015
|
/**
|
|
11615
|
-
* Opens the drawer under the given
|
|
12016
|
+
* Opens the drawer under the given header and waits for the callback: a
|
|
11616
12017
|
* resolved value paints the content section (which is returned), a
|
|
11617
12018
|
* rejection paints the problems and travels to the caller, and an update
|
|
11618
12019
|
* superseded by a newer one paints nothing.
|
|
11619
12020
|
*/
|
|
11620
|
-
async update(
|
|
12021
|
+
async update(header, cb) {
|
|
11621
12022
|
//the claim detaches any update still in flight: its outcome belongs to
|
|
11622
12023
|
//an abandoned opening and must neither be painted nor own the drawer
|
|
11623
12024
|
const claim = this.#updates.take();
|
|
11624
|
-
this.
|
|
12025
|
+
this.header = header;
|
|
11625
12026
|
this.#content.replaceChildren();
|
|
11626
12027
|
this.#restChrome();
|
|
11627
12028
|
this.#loading.removeAttribute('hidden');
|
|
@@ -11667,10 +12068,33 @@ var fml = (function (exports) {
|
|
|
11667
12068
|
this.#requests.request(this, this.#content, null, null)?.catch(() => undefined);
|
|
11668
12069
|
}
|
|
11669
12070
|
close() {
|
|
12071
|
+
if (this.#closing || !this.#dialog.open) {
|
|
12072
|
+
return;
|
|
12073
|
+
}
|
|
12074
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
12075
|
+
this.#dialog.close();
|
|
12076
|
+
return;
|
|
12077
|
+
}
|
|
12078
|
+
this.#dialog.setAttribute('closing', '');
|
|
12079
|
+
if (this.#dialog.getAnimations().length === 0) {
|
|
12080
|
+
this.#closeNow();
|
|
12081
|
+
return;
|
|
12082
|
+
}
|
|
12083
|
+
this.#closing = true;
|
|
12084
|
+
this.#dialog.addEventListener('animationend', this.#slideOutEnded);
|
|
12085
|
+
}
|
|
12086
|
+
#closeNow() {
|
|
12087
|
+
this.#stopSlidingOut();
|
|
11670
12088
|
this.#dialog.close();
|
|
11671
12089
|
}
|
|
12090
|
+
#stopSlidingOut() {
|
|
12091
|
+
this.#closing = false;
|
|
12092
|
+
this.#dialog.removeEventListener('animationend', this.#slideOutEnded);
|
|
12093
|
+
this.#dialog.removeAttribute('closing');
|
|
12094
|
+
}
|
|
11672
12095
|
/** Shows the modal, answering whether this call is the one that opened it. */
|
|
11673
12096
|
#show() {
|
|
12097
|
+
this.#stopSlidingOut();
|
|
11674
12098
|
if (this.#dialog.open) {
|
|
11675
12099
|
return false;
|
|
11676
12100
|
}
|
|
@@ -11688,6 +12112,65 @@ var fml = (function (exports) {
|
|
|
11688
12112
|
}
|
|
11689
12113
|
}
|
|
11690
12114
|
|
|
12115
|
+
/**
|
|
12116
|
+
* A menu of commands on the native popover platform: the invoker named by
|
|
12117
|
+
* `for` toggles it and light dismiss and Escape come from the browser, while
|
|
12118
|
+
* the element adds the menu semantics and the keyboard protocol.
|
|
12119
|
+
*
|
|
12120
|
+
* The items are the buttons and links written inside it, kept as they are and
|
|
12121
|
+
* given their menu role and their place in the roving focus. Activation is the
|
|
12122
|
+
* item's own click, which the element only follows by closing.
|
|
12123
|
+
*/
|
|
12124
|
+
class Menu extends ParsedElement {
|
|
12125
|
+
static attributes = ['for'];
|
|
12126
|
+
render() {
|
|
12127
|
+
const named = this.declared('for');
|
|
12128
|
+
const invoker = named ? document.getElementById(named) : null;
|
|
12129
|
+
if (!invoker) {
|
|
12130
|
+
throw new Error(
|
|
12131
|
+
`ful-menu names no invoker: for="${named ?? ''}" must name an element already in the page when the menu upgrades`,
|
|
12132
|
+
);
|
|
12133
|
+
}
|
|
12134
|
+
this.setAttribute('role', 'menu');
|
|
12135
|
+
if (!this.hasAttribute('popover')) {
|
|
12136
|
+
this.setAttribute('popover', '');
|
|
12137
|
+
}
|
|
12138
|
+
invoker.setAttribute('aria-haspopup', 'menu');
|
|
12139
|
+
Anchors.wire(invoker, this, { prefix: 'ful-menu', invoke: true, expanded: true });
|
|
12140
|
+
wireMenuKeys(invoker, this, { items: () => this.items });
|
|
12141
|
+
this.#mark();
|
|
12142
|
+
this.addEventListener('click', (evt) => {
|
|
12143
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
12144
|
+
if (this.items.some((item) => item === target || item.contains(target))) {
|
|
12145
|
+
this.hidePopover();
|
|
12146
|
+
}
|
|
12147
|
+
});
|
|
12148
|
+
}
|
|
12149
|
+
/**
|
|
12150
|
+
* The commands, in the order the arrows walk them: the menu's own buttons
|
|
12151
|
+
* and links, minus the disabled and hidden ones, which keep their role and
|
|
12152
|
+
* leave the walk.
|
|
12153
|
+
*/
|
|
12154
|
+
get items() {
|
|
12155
|
+
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
12156
|
+
}
|
|
12157
|
+
/**
|
|
12158
|
+
* Gives every button and link of the menu its item role and takes it out of
|
|
12159
|
+
* the tab order, and answers them. Read on every gesture as well as at the
|
|
12160
|
+
* render, so a menu a page fills or empties later needs no rewiring.
|
|
12161
|
+
*/
|
|
12162
|
+
#mark() {
|
|
12163
|
+
const all = /** @type HTMLElement[] */ ([...this.querySelectorAll(':scope > button, :scope > a[href]')]);
|
|
12164
|
+
for (const item of all) {
|
|
12165
|
+
if (!item.hasAttribute('role')) {
|
|
12166
|
+
item.setAttribute('role', 'menuitem');
|
|
12167
|
+
}
|
|
12168
|
+
item.tabIndex = -1;
|
|
12169
|
+
}
|
|
12170
|
+
return all;
|
|
12171
|
+
}
|
|
12172
|
+
}
|
|
12173
|
+
|
|
11691
12174
|
const SEVERITIES = ['info', 'success', 'warning', 'error'];
|
|
11692
12175
|
|
|
11693
12176
|
//the regions alive in the document: the show-toast listener is wired once and
|
|
@@ -11717,6 +12200,9 @@ var fml = (function (exports) {
|
|
|
11717
12200
|
if (!listenerWired) {
|
|
11718
12201
|
listenerWired = true;
|
|
11719
12202
|
document.addEventListener('show-toast', (/** @type any */ e) => {
|
|
12203
|
+
if (!e.detail) {
|
|
12204
|
+
return;
|
|
12205
|
+
}
|
|
11720
12206
|
for (const region of REGIONS) {
|
|
11721
12207
|
region.show(e.detail.message, e.detail);
|
|
11722
12208
|
}
|
|
@@ -11728,8 +12214,13 @@ var fml = (function (exports) {
|
|
|
11728
12214
|
* Appends a toast carrying the message (a Failure shows its problems'
|
|
11729
12215
|
* reasons, one per line), severity picking the theme and the announcement,
|
|
11730
12216
|
* the toast retiring through its own timer or its dismiss button.
|
|
12217
|
+
*
|
|
12218
|
+
* The timer holds while the toast is hovered or holds the focus, so an
|
|
12219
|
+
* actionable toast waits for its reader: `action` is `{ label, onClick }`,
|
|
12220
|
+
* a button beside the message whose click answers and retires. The same
|
|
12221
|
+
* options travel in the `show-toast` event's detail.
|
|
11731
12222
|
* @param {any} message
|
|
11732
|
-
* @param {any} [options] severity and
|
|
12223
|
+
* @param {any} [options] severity, timeout and action
|
|
11733
12224
|
* @returns {HTMLElement}
|
|
11734
12225
|
*/
|
|
11735
12226
|
show(message, options = {}) {
|
|
@@ -11746,14 +12237,13 @@ var fml = (function (exports) {
|
|
|
11746
12237
|
icon.setAttribute('name', 'x-lg');
|
|
11747
12238
|
icon.setAttribute('aria-hidden', 'true');
|
|
11748
12239
|
dismiss.append(icon);
|
|
11749
|
-
item.append(body
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
}
|
|
11754
|
-
});
|
|
12240
|
+
item.append(body);
|
|
12241
|
+
let remaining = options.timeout ?? this.#timeout;
|
|
12242
|
+
let startedAt = performance.now();
|
|
12243
|
+
let timer = 0;
|
|
11755
12244
|
const retire = () => {
|
|
11756
|
-
|
|
12245
|
+
clearTimeout(timer);
|
|
12246
|
+
//the toast may hold the focus, on its own buttons: handing it
|
|
11757
12247
|
//back to the region keeps the reader somewhere rather than on <body>
|
|
11758
12248
|
if (item.contains(document.activeElement)) {
|
|
11759
12249
|
/** @type HTMLElement */ (this).focus();
|
|
@@ -11767,9 +12257,49 @@ var fml = (function (exports) {
|
|
|
11767
12257
|
item.remove();
|
|
11768
12258
|
}
|
|
11769
12259
|
};
|
|
12260
|
+
if (options.action?.label) {
|
|
12261
|
+
const action = document.createElement('button');
|
|
12262
|
+
action.type = 'button';
|
|
12263
|
+
action.className = 'ful-toast-action';
|
|
12264
|
+
action.textContent = String(options.action.label);
|
|
12265
|
+
action.addEventListener('click', () => {
|
|
12266
|
+
options.action.onClick?.();
|
|
12267
|
+
retire();
|
|
12268
|
+
});
|
|
12269
|
+
item.append(action);
|
|
12270
|
+
}
|
|
12271
|
+
item.append(dismiss);
|
|
12272
|
+
item.addEventListener('animationend', () => {
|
|
12273
|
+
if (item.classList.contains('ful-toast-out')) {
|
|
12274
|
+
item.remove();
|
|
12275
|
+
}
|
|
12276
|
+
});
|
|
12277
|
+
//the pointer and the focus hold independently: a count rather than a
|
|
12278
|
+
//flag, so the pointer leaving while the focus stays keeps the hold
|
|
12279
|
+
let holds = 0;
|
|
12280
|
+
const hold = () => {
|
|
12281
|
+
if (holds === 0) {
|
|
12282
|
+
clearTimeout(timer);
|
|
12283
|
+
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
|
12284
|
+
}
|
|
12285
|
+
++holds;
|
|
12286
|
+
};
|
|
12287
|
+
const release = () => {
|
|
12288
|
+
if (holds === 0) {
|
|
12289
|
+
return;
|
|
12290
|
+
}
|
|
12291
|
+
if (--holds === 0) {
|
|
12292
|
+
startedAt = performance.now();
|
|
12293
|
+
timer = setTimeout(retire, remaining);
|
|
12294
|
+
}
|
|
12295
|
+
};
|
|
12296
|
+
item.addEventListener('mouseenter', hold);
|
|
12297
|
+
item.addEventListener('mouseleave', release);
|
|
12298
|
+
item.addEventListener('focusin', hold);
|
|
12299
|
+
item.addEventListener('focusout', release);
|
|
11770
12300
|
dismiss.addEventListener('click', retire);
|
|
11771
12301
|
this.append(item);
|
|
11772
|
-
setTimeout(retire,
|
|
12302
|
+
timer = setTimeout(retire, remaining);
|
|
11773
12303
|
return item;
|
|
11774
12304
|
}
|
|
11775
12305
|
}
|
|
@@ -11880,7 +12410,7 @@ var fml = (function (exports) {
|
|
|
11880
12410
|
this.#active = index;
|
|
11881
12411
|
this.reflectTo('active', index);
|
|
11882
12412
|
if (this.rendered && index !== previous) {
|
|
11883
|
-
this.dispatchEvent(new CustomEvent('change', { detail: { active: index, previous } }));
|
|
12413
|
+
this.dispatchEvent(new CustomEvent('tabs:change', { detail: { active: index, previous } }));
|
|
11884
12414
|
}
|
|
11885
12415
|
if (this.#panels.length > 0 && (index !== previous || !this.rendered)) {
|
|
11886
12416
|
//the activation is the reader's own gesture: the chrome reports a
|
|
@@ -12101,6 +12631,8 @@ var fml = (function (exports) {
|
|
|
12101
12631
|
'info.tooltip': 'More information',
|
|
12102
12632
|
'dialog.acknowledge': 'Got it',
|
|
12103
12633
|
'dialog.close': 'Close',
|
|
12634
|
+
'dialog.confirm': 'Confirm',
|
|
12635
|
+
'dialog.cancel': 'Cancel',
|
|
12104
12636
|
'drawer.close': 'Close',
|
|
12105
12637
|
'spinner.loading': 'Loading…',
|
|
12106
12638
|
'toast.region': 'Notifications',
|
|
@@ -12143,6 +12675,8 @@ var fml = (function (exports) {
|
|
|
12143
12675
|
'info.tooltip': 'Maggiori informazioni',
|
|
12144
12676
|
'dialog.acknowledge': 'Ho capito',
|
|
12145
12677
|
'dialog.close': 'Chiudi',
|
|
12678
|
+
'dialog.confirm': 'Conferma',
|
|
12679
|
+
'dialog.cancel': 'Annulla',
|
|
12146
12680
|
'drawer.close': 'Chiudi',
|
|
12147
12681
|
'spinner.loading': 'Caricamento…',
|
|
12148
12682
|
'toast.region': 'Notifiche',
|
|
@@ -12185,6 +12719,8 @@ var fml = (function (exports) {
|
|
|
12185
12719
|
'info.tooltip': 'Más información',
|
|
12186
12720
|
'dialog.acknowledge': 'Entendido',
|
|
12187
12721
|
'dialog.close': 'Cerrar',
|
|
12722
|
+
'dialog.confirm': 'Confirmar',
|
|
12723
|
+
'dialog.cancel': 'Cancelar',
|
|
12188
12724
|
'drawer.close': 'Cerrar',
|
|
12189
12725
|
'spinner.loading': 'Cargando…',
|
|
12190
12726
|
'toast.region': 'Notificaciones',
|
|
@@ -12230,6 +12766,8 @@ var fml = (function (exports) {
|
|
|
12230
12766
|
'info.tooltip': 'Plus d’informations',
|
|
12231
12767
|
'dialog.acknowledge': 'J’ai compris',
|
|
12232
12768
|
'dialog.close': 'Fermer',
|
|
12769
|
+
'dialog.confirm': 'Confirmer',
|
|
12770
|
+
'dialog.cancel': 'Annuler',
|
|
12233
12771
|
'drawer.close': 'Fermer',
|
|
12234
12772
|
'spinner.loading': 'Chargement…',
|
|
12235
12773
|
'toast.region': 'Notifications',
|
|
@@ -12270,7 +12808,7 @@ var fml = (function (exports) {
|
|
|
12270
12808
|
const httpClient =
|
|
12271
12809
|
this.#httpClient ?? HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
|
|
12272
12810
|
//the fallback chain is baked here: en, the active language, the consumer's own strings
|
|
12273
|
-
const language = this.#language.split('-')[0];
|
|
12811
|
+
const language = this.#language.split('-')[0].toLowerCase();
|
|
12274
12812
|
const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
|
|
12275
12813
|
registry
|
|
12276
12814
|
.defineModule('l10n', Localization)
|
|
@@ -12278,6 +12816,7 @@ var fml = (function (exports) {
|
|
|
12278
12816
|
.defineElement('ful-tooltip', Tooltip)
|
|
12279
12817
|
.defineElement('ful-dialog', Dialog)
|
|
12280
12818
|
.defineElement('ful-drawer', Drawer)
|
|
12819
|
+
.defineElement('ful-menu', Menu)
|
|
12281
12820
|
.defineElement('ful-toasts', Toasts)
|
|
12282
12821
|
.defineElement('ful-tabs', Tabs)
|
|
12283
12822
|
.defineElement('ful-accordion', Accordion)
|
|
@@ -12344,6 +12883,7 @@ var fml = (function (exports) {
|
|
|
12344
12883
|
LocalDate: LocalDate,
|
|
12345
12884
|
LocalDateFilter: LocalDateFilter,
|
|
12346
12885
|
LocalStorage: LocalStorage,
|
|
12886
|
+
Menu: Menu,
|
|
12347
12887
|
NumberFilter: NumberFilter,
|
|
12348
12888
|
Pagination: Pagination,
|
|
12349
12889
|
Plugin: Plugin,
|
|
@@ -12416,6 +12956,7 @@ var fml = (function (exports) {
|
|
|
12416
12956
|
exports.LocalStorage = LocalStorage;
|
|
12417
12957
|
exports.Localization = Localization;
|
|
12418
12958
|
exports.MediaType = MediaType;
|
|
12959
|
+
exports.Menu = Menu;
|
|
12419
12960
|
exports.Nodes = Nodes;
|
|
12420
12961
|
exports.NumberFilter = NumberFilter;
|
|
12421
12962
|
exports.Pagination = Pagination;
|