@optionfactory/fml 9.0.0-rc12 → 9.0.0-rc14
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 +2 -2
- 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 +45 -10
- package/dist/fml.css +7 -7
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +503 -62
- 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 +7 -7
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +79 -7
- package/dist/ful.iife.js +501 -60
- 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 +501 -60
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +27 -7
- package/dist/web-types.json +51 -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
|
+
requestAnimationFrame(() => {
|
|
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
|
|
@@ -8531,7 +8611,18 @@ var fml = (function (exports) {
|
|
|
8531
8611
|
if (invoke) {
|
|
8532
8612
|
//popovertarget needs a target that can be named
|
|
8533
8613
|
popover.id = popover.id || uid;
|
|
8534
|
-
invoker.
|
|
8614
|
+
if (invoker.localName === 'button' || invoker.localName === 'input') {
|
|
8615
|
+
invoker.setAttribute('popovertarget', popover.id);
|
|
8616
|
+
} else {
|
|
8617
|
+
invoker.addEventListener('click', () => popover.togglePopover());
|
|
8618
|
+
invoker.addEventListener('keydown', (/** @type any */ evt) => {
|
|
8619
|
+
if (evt.key !== 'Enter' && evt.key !== ' ') {
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8622
|
+
evt.preventDefault();
|
|
8623
|
+
popover.togglePopover();
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
8535
8626
|
}
|
|
8536
8627
|
const anchor = `--${uid}`;
|
|
8537
8628
|
invoker.style.anchorName = anchor;
|
|
@@ -8550,10 +8641,8 @@ var fml = (function (exports) {
|
|
|
8550
8641
|
}
|
|
8551
8642
|
const anchored = { invoker, stretch };
|
|
8552
8643
|
popover.addEventListener('beforetoggle', (/** @type any */ evt) => {
|
|
8553
|
-
//placed before the showing, refined once laid out: the platform's
|
|
8554
|
-
//centered or corner spot never paints
|
|
8555
8644
|
if (evt.newState === 'open') {
|
|
8556
|
-
|
|
8645
|
+
showOncePlaced(popover, anchored);
|
|
8557
8646
|
}
|
|
8558
8647
|
});
|
|
8559
8648
|
popover.addEventListener('toggle', (/** @type any */ evt) => {
|
|
@@ -8571,6 +8660,57 @@ var fml = (function (exports) {
|
|
|
8571
8660
|
window.addEventListener('resize', schedule);
|
|
8572
8661
|
}
|
|
8573
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 before it is placed, so the first position anybody
|
|
8676
|
+
* sees is the right one.
|
|
8677
|
+
*
|
|
8678
|
+
* @param {HTMLElement} popover the `[popover]` element to show, or an
|
|
8679
|
+
* already-shown element to place
|
|
8680
|
+
* @param {Element|DOMRect} anchor the element, or the rectangle, to place
|
|
8681
|
+
* beside
|
|
8682
|
+
* @param {object} [options]
|
|
8683
|
+
* @param {boolean} [options.flip] place above the anchor where no room is
|
|
8684
|
+
* left below
|
|
8685
|
+
* @param {number} [options.gap] the distance from the anchor, in pixels
|
|
8686
|
+
* @returns {HTMLElement} the popover
|
|
8687
|
+
*/
|
|
8688
|
+
static show(popover, anchor, { flip = false, gap = 0 } = {}) {
|
|
8689
|
+
const placeShown = () => {
|
|
8690
|
+
const box = anchor instanceof Element ? anchor.getBoundingClientRect() : anchor;
|
|
8691
|
+
const viewport = document.documentElement;
|
|
8692
|
+
const here = popover.getBoundingClientRect();
|
|
8693
|
+
const below = box.bottom + gap;
|
|
8694
|
+
const above = box.top - gap - here.height;
|
|
8695
|
+
const top = flip && below + here.height > viewport.clientHeight - PAD && above >= PAD ? above : below;
|
|
8696
|
+
popover.style.right = 'auto';
|
|
8697
|
+
popover.style.bottom = 'auto';
|
|
8698
|
+
popover.style.margin = '0';
|
|
8699
|
+
popover.style.left = `${clamp(box.left, PAD, Math.max(PAD, viewport.clientWidth - here.width - PAD))}px`;
|
|
8700
|
+
popover.style.top = `${clamp(top, PAD, Math.max(PAD, viewport.clientHeight - here.height - PAD))}px`;
|
|
8701
|
+
};
|
|
8702
|
+
if (popover.matches(':popover-open') || typeof popover.showPopover !== 'function') {
|
|
8703
|
+
placeShown();
|
|
8704
|
+
return popover;
|
|
8705
|
+
}
|
|
8706
|
+
popover.style.visibility = 'hidden';
|
|
8707
|
+
popover.showPopover();
|
|
8708
|
+
requestAnimationFrame(() => {
|
|
8709
|
+
placeShown();
|
|
8710
|
+
popover.style.removeProperty('visibility');
|
|
8711
|
+
});
|
|
8712
|
+
return popover;
|
|
8713
|
+
}
|
|
8574
8714
|
}
|
|
8575
8715
|
|
|
8576
8716
|
/**
|
|
@@ -8867,12 +9007,19 @@ var fml = (function (exports) {
|
|
|
8867
9007
|
this.hide();
|
|
8868
9008
|
return;
|
|
8869
9009
|
}
|
|
9010
|
+
if (li.hasAttribute('aria-disabled')) {
|
|
9011
|
+
return;
|
|
9012
|
+
}
|
|
8870
9013
|
this.#change(li);
|
|
8871
9014
|
});
|
|
8872
9015
|
this.replaceChildren(fragment);
|
|
8873
9016
|
}
|
|
8874
9017
|
#selected() {
|
|
8875
|
-
return
|
|
9018
|
+
return (
|
|
9019
|
+
this.#menu?.querySelector('[selected]:not([aria-disabled])') ??
|
|
9020
|
+
this.#menu?.querySelector('li:not([aria-disabled])') ??
|
|
9021
|
+
null
|
|
9022
|
+
);
|
|
8876
9023
|
}
|
|
8877
9024
|
#highlight(li) {
|
|
8878
9025
|
if (!li) {
|
|
@@ -8904,11 +9051,20 @@ var fml = (function (exports) {
|
|
|
8904
9051
|
const data = values.map((entry, index) => ({ index, ...entry }));
|
|
8905
9052
|
this.#optionstemplate.withOverlay(data).renderTo(this.#menu);
|
|
8906
9053
|
for (const [index, li] of [...this.#menu.children].entries()) {
|
|
8907
|
-
const
|
|
9054
|
+
const entry = values[index];
|
|
9055
|
+
const picked = keys.some((r) => r == entry?.key);
|
|
8908
9056
|
li.toggleAttribute('picked', picked);
|
|
8909
9057
|
//what is picked is what aria-selected means for a listbox: a tint alone
|
|
8910
9058
|
//says it to whoever can see it and to no one else
|
|
8911
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
|
+
li.toggleAttribute('aria-disabled', disabled);
|
|
9063
|
+
if (disabled && entry.metadata.reason) {
|
|
9064
|
+
li.setAttribute('title', String(entry.metadata.reason));
|
|
9065
|
+
} else {
|
|
9066
|
+
li.removeAttribute('title');
|
|
9067
|
+
}
|
|
8912
9068
|
}
|
|
8913
9069
|
this.#empty.toggleAttribute('hidden', values.length !== 0);
|
|
8914
9070
|
this.#menu.toggleAttribute('hidden', values.length === 0);
|
|
@@ -8982,15 +9138,25 @@ var fml = (function (exports) {
|
|
|
8982
9138
|
if (this.shown) {
|
|
8983
9139
|
const selected = this.#selected();
|
|
8984
9140
|
const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
8985
|
-
|
|
8986
|
-
|
|
9141
|
+
const target = candidate ? this.#walk(candidate, forward) : null;
|
|
9142
|
+
if (selected && target) {
|
|
9143
|
+
this.#highlight(target);
|
|
8987
9144
|
}
|
|
8988
9145
|
return;
|
|
8989
9146
|
}
|
|
8990
9147
|
await this.show(loader, keys);
|
|
8991
9148
|
}
|
|
9149
|
+
#walk(from, forward) {
|
|
9150
|
+
for (let li = from; li; li = li[`${forward ? 'next' : 'previous'}ElementSibling`]) {
|
|
9151
|
+
if (!li.hasAttribute('aria-disabled')) {
|
|
9152
|
+
return li;
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
return null;
|
|
9156
|
+
}
|
|
8992
9157
|
jump(first) {
|
|
8993
|
-
const
|
|
9158
|
+
const edge = first ? this.#menu.firstElementChild : this.#menu.lastElementChild;
|
|
9159
|
+
const target = this.#walk(edge, first);
|
|
8994
9160
|
if (target) {
|
|
8995
9161
|
this.#highlight(target);
|
|
8996
9162
|
}
|
|
@@ -9000,9 +9166,14 @@ var fml = (function (exports) {
|
|
|
9000
9166
|
if (!selected) {
|
|
9001
9167
|
return;
|
|
9002
9168
|
}
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9169
|
+
let target = selected;
|
|
9170
|
+
for (let i = 0; i !== this.#page(); ++i) {
|
|
9171
|
+
const next = this.#walk(target[`${forward ? 'next' : 'previous'}ElementSibling`], forward);
|
|
9172
|
+
if (!next) {
|
|
9173
|
+
break;
|
|
9174
|
+
}
|
|
9175
|
+
target = next;
|
|
9176
|
+
}
|
|
9006
9177
|
this.#highlight(target);
|
|
9007
9178
|
}
|
|
9008
9179
|
#page() {
|
|
@@ -9038,6 +9209,12 @@ var fml = (function (exports) {
|
|
|
9038
9209
|
//`set value` normalizes a list of one to a single key, and the getter answers a
|
|
9039
9210
|
//scalar for a single select, so nothing downstream has to know which it was
|
|
9040
9211
|
static observed = ['multiple:presence', 'item-list:presence', 'value:csv'];
|
|
9212
|
+
unmarshal(attr, str) {
|
|
9213
|
+
if (attr === 'value' && str === '' && !this.hasAttribute('multiple')) {
|
|
9214
|
+
return [''];
|
|
9215
|
+
}
|
|
9216
|
+
return super.unmarshal(attr, str);
|
|
9217
|
+
}
|
|
9041
9218
|
static slots = true;
|
|
9042
9219
|
//a manual popover: the combobox keeps the focus on its input and owns
|
|
9043
9220
|
//the whole lifecycle (typing, arrows, blur, Escape, Tab), so no light
|
|
@@ -9143,7 +9320,7 @@ var fml = (function (exports) {
|
|
|
9143
9320
|
//the select's: a tooltip marker slotted into `info`, a button a page
|
|
9144
9321
|
//put in an affix. Without this, reading the note beside a select
|
|
9145
9322
|
//also stole the focus and dropped the dropdown over the note
|
|
9146
|
-
const elsewhere = e.target.closest('button, a[href], input, select, textarea');
|
|
9323
|
+
const elsewhere = e.target.closest('button, [role="button"], a[href], input, select, textarea');
|
|
9147
9324
|
if (elsewhere && elsewhere !== this.#input) {
|
|
9148
9325
|
return;
|
|
9149
9326
|
}
|
|
@@ -10131,7 +10308,7 @@ var fml = (function (exports) {
|
|
|
10131
10308
|
<tbody data-ref="empty" hidden>
|
|
10132
10309
|
<tr>
|
|
10133
10310
|
<td data-tpl-colspan="schema.length">
|
|
10134
|
-
<ful-empty data-tpl-if="slots.empty"
|
|
10311
|
+
<ful-empty data-tpl-if="slots.empty" data-ref="empty-authors"></ful-empty>
|
|
10135
10312
|
<ful-empty data-tpl-if="!slots.empty">
|
|
10136
10313
|
<ful-icon data-tpl-if="config.emptyIcon" data-tpl-name="config.emptyIcon" aria-hidden="true"></ful-icon>
|
|
10137
10314
|
{{ #l10n:t('table.no-data') }}
|
|
@@ -10174,6 +10351,8 @@ var fml = (function (exports) {
|
|
|
10174
10351
|
#loading;
|
|
10175
10352
|
#noAutoload;
|
|
10176
10353
|
#empty;
|
|
10354
|
+
#emptyAuthors;
|
|
10355
|
+
#emptyTemplate;
|
|
10177
10356
|
#feedback;
|
|
10178
10357
|
#paginator;
|
|
10179
10358
|
#sorters;
|
|
@@ -10225,6 +10404,9 @@ var fml = (function (exports) {
|
|
|
10225
10404
|
this.#loading = table.querySelector(':scope > tbody[data-ref=loading]');
|
|
10226
10405
|
this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
|
|
10227
10406
|
this.#empty = table.querySelector(':scope > tbody[data-ref=empty]');
|
|
10407
|
+
this.#emptyAuthors = this.#empty.querySelector('ful-empty[data-ref=empty-authors]');
|
|
10408
|
+
this.#emptyTemplate =
|
|
10409
|
+
slots.empty && !Fragments.isBlank(slots.empty) ? Templates.fromFragment(slots.empty) : null;
|
|
10228
10410
|
this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
|
|
10229
10411
|
this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
10230
10412
|
this.replaceChildren(fragment);
|
|
@@ -10396,6 +10578,15 @@ var fml = (function (exports) {
|
|
|
10396
10578
|
.render(),
|
|
10397
10579
|
);
|
|
10398
10580
|
this.#empty.toggleAttribute('hidden', pageResponse.data.length !== 0);
|
|
10581
|
+
if (this.#emptyTemplate && pageResponse.data.length === 0) {
|
|
10582
|
+
//rendered per load, the rows' own scope: the empty state can say
|
|
10583
|
+
//whether nothing was ever there or the filters matched nothing
|
|
10584
|
+
this.#emptyAuthors.replaceChildren(
|
|
10585
|
+
this.#emptyTemplate
|
|
10586
|
+
.withOverlay({ schema: this.#schema, pageRequest, filterRequest, pageResponse })
|
|
10587
|
+
.render(),
|
|
10588
|
+
);
|
|
10589
|
+
}
|
|
10399
10590
|
//one move, one repaint: the page and the count are the same state
|
|
10400
10591
|
this.#paginator.update({ current: pageRequest.page, total: pages });
|
|
10401
10592
|
}
|
|
@@ -10486,6 +10677,13 @@ var fml = (function (exports) {
|
|
|
10486
10677
|
get pinned() {
|
|
10487
10678
|
return this.#allowed.length < 2;
|
|
10488
10679
|
}
|
|
10680
|
+
/**
|
|
10681
|
+
* The host's fixed claim: the button and its chrome leave while the value
|
|
10682
|
+
* keeps answering, which is what a fixed operator asks of its glyph.
|
|
10683
|
+
*/
|
|
10684
|
+
set fixed(fixed) {
|
|
10685
|
+
this.#button.toggleAttribute('hidden', !!fixed);
|
|
10686
|
+
}
|
|
10489
10687
|
get value() {
|
|
10490
10688
|
return this.#button.getAttribute('value');
|
|
10491
10689
|
}
|
|
@@ -10669,6 +10867,7 @@ var fml = (function (exports) {
|
|
|
10669
10867
|
*/
|
|
10670
10868
|
class CompareFilter extends Input {
|
|
10671
10869
|
static observed = ['value:json', 'operators:csv'];
|
|
10870
|
+
static attributes = ['operator'];
|
|
10672
10871
|
static OPERATORS = COMPARE_OPERATORS;
|
|
10673
10872
|
static DEFAULT_OPERATOR = 'EQ';
|
|
10674
10873
|
static template = `
|
|
@@ -10719,11 +10918,38 @@ var fml = (function (exports) {
|
|
|
10719
10918
|
if (this._operator.value === null) {
|
|
10720
10919
|
this._showDefaultOperator();
|
|
10721
10920
|
}
|
|
10921
|
+
this.#fixOperator();
|
|
10722
10922
|
//the second operand mirrors the claims like the first one does, and the
|
|
10723
10923
|
//freeze reaches the operator and sensitivity buttons, whose popovers an
|
|
10724
10924
|
//input's readOnly cannot touch
|
|
10725
10925
|
return { ...pieces, freeze: this._container, also: [this._value2] };
|
|
10726
10926
|
}
|
|
10927
|
+
/**
|
|
10928
|
+
* The singular `operator` fixes the operator for the element's life: not a
|
|
10929
|
+
* choice, so no menu and no button, every tuple carrying it. The plural is
|
|
10930
|
+
* the menu's whitelist; declaring both is a confused page and says so,
|
|
10931
|
+
* the singular winning, and an unknown name warns and pins the default.
|
|
10932
|
+
*/
|
|
10933
|
+
#fixOperator() {
|
|
10934
|
+
const fixed = this.declared('operator');
|
|
10935
|
+
if (fixed === null) {
|
|
10936
|
+
return;
|
|
10937
|
+
}
|
|
10938
|
+
if (this.hasAttribute('operators')) {
|
|
10939
|
+
console.warn(`${this.localName}: operator fixes what operators offers a menu for, the plural is ignored`, this);
|
|
10940
|
+
}
|
|
10941
|
+
const vocabulary = this._vocabulary();
|
|
10942
|
+
/** @type {string|null} */
|
|
10943
|
+
let preferred = null;
|
|
10944
|
+
if (vocabulary.includes(fixed)) {
|
|
10945
|
+
preferred = fixed;
|
|
10946
|
+
} else {
|
|
10947
|
+
console.warn(`${this.localName}: '${fixed}' is not one of its operators, the default is pinned instead`, this);
|
|
10948
|
+
preferred = this._defaultOperator();
|
|
10949
|
+
}
|
|
10950
|
+
this._operator.allowed = [preferred];
|
|
10951
|
+
this._operator.fixed = true;
|
|
10952
|
+
}
|
|
10727
10953
|
_showDefaultOperator() {
|
|
10728
10954
|
const preferred = this._defaultOperator();
|
|
10729
10955
|
const allowed = this._operator.allowed;
|
|
@@ -10760,6 +10986,9 @@ var fml = (function (exports) {
|
|
|
10760
10986
|
return this._operator ? this._operator.allowed : this._declaredOperators;
|
|
10761
10987
|
}
|
|
10762
10988
|
set operators(declared) {
|
|
10989
|
+
if (this.declared('operator') !== null) {
|
|
10990
|
+
return;
|
|
10991
|
+
}
|
|
10763
10992
|
if (!this._operator) {
|
|
10764
10993
|
this._declaredOperators = ChoiceButton.narrow(declared, this._vocabulary());
|
|
10765
10994
|
return;
|
|
@@ -10863,6 +11092,7 @@ var fml = (function (exports) {
|
|
|
10863
11092
|
/** The compare filter over text, carrying a case sensitivity beside the operator. */
|
|
10864
11093
|
class TextFilter extends CompareFilter {
|
|
10865
11094
|
static observed = ['sensitivities:csv'];
|
|
11095
|
+
static attributes = ['sensitivity'];
|
|
10866
11096
|
static template = `
|
|
10867
11097
|
<label>{{{{ slots.default }}}}</label>
|
|
10868
11098
|
{{{{ slots.info }}}}
|
|
@@ -10906,8 +11136,28 @@ var fml = (function (exports) {
|
|
|
10906
11136
|
);
|
|
10907
11137
|
this._sensitivityButton.allowed = null;
|
|
10908
11138
|
this._sensitivityButton.value = SENSITIVITIES[0];
|
|
11139
|
+
this.#fixSensitivity();
|
|
10909
11140
|
return pieces;
|
|
10910
11141
|
}
|
|
11142
|
+
#fixSensitivity() {
|
|
11143
|
+
const fixed = this.declared('sensitivity');
|
|
11144
|
+
if (fixed === null) {
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
11147
|
+
if (this.hasAttribute('sensitivities')) {
|
|
11148
|
+
console.warn('ful-filter-text: sensitivity fixes what sensitivities offers a menu for, the plural is ignored', this);
|
|
11149
|
+
}
|
|
11150
|
+
/** @type {string|null} */
|
|
11151
|
+
let preferred = null;
|
|
11152
|
+
if (SENSITIVITIES.includes(fixed)) {
|
|
11153
|
+
preferred = fixed;
|
|
11154
|
+
} else {
|
|
11155
|
+
console.warn(`ful-filter-text: '${fixed}' is not one of its sensitivities, the default is pinned instead`, this);
|
|
11156
|
+
preferred = 'IGNORE_CASE';
|
|
11157
|
+
}
|
|
11158
|
+
this._sensitivityButton.allowed = [preferred];
|
|
11159
|
+
this._sensitivityButton.fixed = true;
|
|
11160
|
+
}
|
|
10911
11161
|
_choices() {
|
|
10912
11162
|
return [...super._choices(), this._sensitivityButton].filter((c) => c);
|
|
10913
11163
|
}
|
|
@@ -10919,6 +11169,9 @@ var fml = (function (exports) {
|
|
|
10919
11169
|
return this._sensitivityButton ? this._sensitivityButton.allowed : this._declaredSensitivities;
|
|
10920
11170
|
}
|
|
10921
11171
|
set sensitivities(declared) {
|
|
11172
|
+
if (this.declared('sensitivity') !== null) {
|
|
11173
|
+
return;
|
|
11174
|
+
}
|
|
10922
11175
|
if (!this._sensitivityButton) {
|
|
10923
11176
|
this._declaredSensitivities = ChoiceButton.narrow(declared, SENSITIVITIES);
|
|
10924
11177
|
return;
|
|
@@ -11048,6 +11301,15 @@ var fml = (function (exports) {
|
|
|
11048
11301
|
this._operator.value = this._operator.pinned ? this._operator.allowed[0] : v[0];
|
|
11049
11302
|
this._value.value = v[1] ?? '';
|
|
11050
11303
|
}
|
|
11304
|
+
formResetCallback() {
|
|
11305
|
+
super.formResetCallback();
|
|
11306
|
+
if (!this.hasAttribute('value')) {
|
|
11307
|
+
const allowed = this._operator.allowed;
|
|
11308
|
+
this._operator.value = allowed.includes(BooleanFilter.DEFAULT_OPERATOR)
|
|
11309
|
+
? BooleanFilter.DEFAULT_OPERATOR
|
|
11310
|
+
: allowed[0];
|
|
11311
|
+
}
|
|
11312
|
+
}
|
|
11051
11313
|
/** @returns {FilterCriterion|null} */
|
|
11052
11314
|
get criterion() {
|
|
11053
11315
|
const token = this._value.value;
|
|
@@ -11074,7 +11336,6 @@ var fml = (function (exports) {
|
|
|
11074
11336
|
*/
|
|
11075
11337
|
class InFilter extends Select {
|
|
11076
11338
|
static attributes = ['operator'];
|
|
11077
|
-
/** the selection as the array the wire takes, whether one key was chosen or many */
|
|
11078
11339
|
#keys() {
|
|
11079
11340
|
const chosen = super.value;
|
|
11080
11341
|
if (this.multiple) {
|
|
@@ -11095,8 +11356,6 @@ var fml = (function (exports) {
|
|
|
11095
11356
|
set value(v) {
|
|
11096
11357
|
const operator = this.declared('operator');
|
|
11097
11358
|
let keys = v === null || v === undefined ? [] : Array.isArray(v) ? v : [v];
|
|
11098
|
-
//what the getter answers has to be assignable back, and it carries the
|
|
11099
|
-
//operator in front of the keys
|
|
11100
11359
|
if (operator && keys[0] === operator) {
|
|
11101
11360
|
keys = keys.slice(1);
|
|
11102
11361
|
}
|
|
@@ -11243,7 +11502,7 @@ var fml = (function (exports) {
|
|
|
11243
11502
|
icon: 'info-circle-fill',
|
|
11244
11503
|
};
|
|
11245
11504
|
static template = `
|
|
11246
|
-
<
|
|
11505
|
+
<span role="button" tabindex="0" class="ful-tip" data-ref="trigger" data-tpl-aria-label="#l10n:t('info.tooltip')"><ful-icon data-tpl-name="icon ?? config.icon" aria-hidden="true"></ful-icon></span>
|
|
11247
11506
|
<ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
|
|
11248
11507
|
`;
|
|
11249
11508
|
render({ slots }) {
|
|
@@ -11347,15 +11606,32 @@ var fml = (function (exports) {
|
|
|
11347
11606
|
this.#body = fragment.querySelector('[data-ref=body]');
|
|
11348
11607
|
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11349
11608
|
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11609
|
+
//a dialog is a named surface: a reader announces the heading on opening,
|
|
11610
|
+
//which it cannot do unless the heading names the dialog. The name travels
|
|
11611
|
+
//as a reference rather than as aria-label text, so it stays the heading's
|
|
11612
|
+
//own however it is reworded
|
|
11613
|
+
const heading = fragment.querySelector('h2');
|
|
11614
|
+
if (heading) {
|
|
11615
|
+
heading.id ||= Attributes.uid('ful-dialog-title');
|
|
11616
|
+
this.#dialog.setAttribute('aria-labelledby', heading.id);
|
|
11617
|
+
}
|
|
11350
11618
|
this.#dialog.addEventListener('close', () => {
|
|
11351
11619
|
const outcome = this.#outcome();
|
|
11352
11620
|
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
11353
11621
|
this.#settle(outcome);
|
|
11354
11622
|
});
|
|
11623
|
+
let pressedOutside = false;
|
|
11624
|
+
this.#dialog.addEventListener('mousedown', (/** @type any */ e) => {
|
|
11625
|
+
pressedOutside = e.target === this.#dialog;
|
|
11626
|
+
});
|
|
11355
11627
|
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
11356
11628
|
const result = e.target.closest('button[data-result]')?.dataset.result;
|
|
11357
11629
|
if (result !== undefined) {
|
|
11358
11630
|
this.#dialog.close(result);
|
|
11631
|
+
return;
|
|
11632
|
+
}
|
|
11633
|
+
if (!requiresAnswer && pressedOutside && e.target === this.#dialog) {
|
|
11634
|
+
this.#dialog.close('');
|
|
11359
11635
|
}
|
|
11360
11636
|
});
|
|
11361
11637
|
//dismissal, not an answer: the waiters are settled with a dismissal, as
|
|
@@ -11506,6 +11782,69 @@ var fml = (function (exports) {
|
|
|
11506
11782
|
this.#loading?.setAttribute('hidden', '');
|
|
11507
11783
|
this.#body?.removeAttribute('hidden');
|
|
11508
11784
|
}
|
|
11785
|
+
/**
|
|
11786
|
+
* Asks a question in a dialog of the page's own, resolving with its outcome
|
|
11787
|
+
* and removing the element afterwards: the element stays the author's when
|
|
11788
|
+
* the question lives in the page, while a question asked from a button had
|
|
11789
|
+
* to build, render, show, answer and clean up by hand.
|
|
11790
|
+
*
|
|
11791
|
+
* The body is text or a node: a string is never read as markup, an
|
|
11792
|
+
* `innerHTML` sink being a thing the library does not carry. Each button is
|
|
11793
|
+
* a `[result, label, className?]` tuple, rendered as its answer.
|
|
11794
|
+
* @param {string} header
|
|
11795
|
+
* @param {string|Node} body
|
|
11796
|
+
* @param {[string, string, string?][]} [buttons]
|
|
11797
|
+
* @returns {Promise<DialogOutcome>}
|
|
11798
|
+
*/
|
|
11799
|
+
static async ask(header, body, buttons = []) {
|
|
11800
|
+
const dialog = document.createElement('ful-dialog');
|
|
11801
|
+
dialog.setAttribute('header', header);
|
|
11802
|
+
if (typeof body === 'string') {
|
|
11803
|
+
dialog.textContent = body;
|
|
11804
|
+
} else if (body) {
|
|
11805
|
+
dialog.append(body);
|
|
11806
|
+
}
|
|
11807
|
+
//buttons are slotted only when there are any: the acknowledge button
|
|
11808
|
+
//is the answer of a question asked without choices of its own
|
|
11809
|
+
if (buttons.length > 0) {
|
|
11810
|
+
const choices = document.createElement('template');
|
|
11811
|
+
choices.setAttribute('slot', 'buttons');
|
|
11812
|
+
for (const [result, label, className] of buttons) {
|
|
11813
|
+
const button = document.createElement('button');
|
|
11814
|
+
button.type = 'button';
|
|
11815
|
+
button.dataset.result = result;
|
|
11816
|
+
if (className) {
|
|
11817
|
+
button.className = className;
|
|
11818
|
+
}
|
|
11819
|
+
button.textContent = label;
|
|
11820
|
+
choices.content.append(button);
|
|
11821
|
+
}
|
|
11822
|
+
dialog.append(choices);
|
|
11823
|
+
}
|
|
11824
|
+
document.body.append(dialog);
|
|
11825
|
+
try {
|
|
11826
|
+
await Rendering.waitFor(dialog);
|
|
11827
|
+
return await /** @type {Dialog} */ (dialog).ask();
|
|
11828
|
+
} finally {
|
|
11829
|
+
dialog.remove();
|
|
11830
|
+
}
|
|
11831
|
+
}
|
|
11832
|
+
/**
|
|
11833
|
+
* Asks a yes/no question, resolving with the confirm button's answer:
|
|
11834
|
+
* the cancel button, the close button and Escape all answer false, a
|
|
11835
|
+
* dismissal being the negative.
|
|
11836
|
+
* @param {string} header
|
|
11837
|
+
* @param {string|Node} body
|
|
11838
|
+
* @param {{ confirm?: string, cancel?: string }} [labels] overriding the localized defaults
|
|
11839
|
+
* @returns {Promise<boolean>}
|
|
11840
|
+
*/
|
|
11841
|
+
static async confirm(header, body, labels = {}) {
|
|
11842
|
+
const outcome = await Dialog.ask(header, body, [
|
|
11843
|
+
['cancel', labels.cancel ?? Localization.of().t('dialog.cancel')],
|
|
11844
|
+
['confirm', labels.confirm ?? Localization.of().t('dialog.confirm'), 'ful-button'],
|
|
11845
|
+
]);
|
|
11846
|
+
return !outcome.dismissed && outcome.result === 'confirm';
|
|
11847
|
+
}
|
|
11509
11848
|
}
|
|
11510
11849
|
|
|
11511
11850
|
/**
|
|
@@ -11518,7 +11857,7 @@ var fml = (function (exports) {
|
|
|
11518
11857
|
* it.
|
|
11519
11858
|
*/
|
|
11520
11859
|
class Drawer extends ParsedElement {
|
|
11521
|
-
static attributes = ['
|
|
11860
|
+
static attributes = ['header', 'placement', 'close-on-submit:presence'];
|
|
11522
11861
|
static slots = true;
|
|
11523
11862
|
static template = `
|
|
11524
11863
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -11543,12 +11882,23 @@ var fml = (function (exports) {
|
|
|
11543
11882
|
//a save answering with no body at all is still a save
|
|
11544
11883
|
/** @type {{ dismissed: boolean, response: any }|null} */
|
|
11545
11884
|
#answer = null;
|
|
11885
|
+
#closing = false;
|
|
11886
|
+
#slideOutEnded = (/** @type AnimationEvent */ e) => {
|
|
11887
|
+
if (e.target === this.#dialog) {
|
|
11888
|
+
this.#closeNow();
|
|
11889
|
+
}
|
|
11890
|
+
};
|
|
11546
11891
|
render({ slots }) {
|
|
11547
11892
|
const fragment = this.template()
|
|
11548
|
-
.withOverlay({ slots, title: this.declared('
|
|
11893
|
+
.withOverlay({ slots, title: this.declared('header') ?? '' })
|
|
11549
11894
|
.render();
|
|
11550
11895
|
this.#dialog = fragment.querySelector('[data-ref=dialog]');
|
|
11551
11896
|
this.#title = fragment.querySelector('[data-ref=title]');
|
|
11897
|
+
//a named surface, like a dialog: the heading announces the drawer on
|
|
11898
|
+
//opening only where it names it, and the reference survives every
|
|
11899
|
+
//textContent update() writes into the heading
|
|
11900
|
+
this.#title.id ||= Attributes.uid('ful-drawer-title');
|
|
11901
|
+
this.#dialog.setAttribute('aria-labelledby', this.#title.id);
|
|
11552
11902
|
this.#loading = fragment.querySelector('[data-ref=loading]');
|
|
11553
11903
|
this.#error = fragment.querySelector('[data-ref=error]');
|
|
11554
11904
|
this.#content = fragment.querySelector('[data-ref=content]');
|
|
@@ -11557,6 +11907,19 @@ var fml = (function (exports) {
|
|
|
11557
11907
|
this.#dialog.setAttribute('placement', placement);
|
|
11558
11908
|
}
|
|
11559
11909
|
fragment.querySelector('[data-ref=close]').addEventListener('click', () => this.close());
|
|
11910
|
+
let pressedOutside = false;
|
|
11911
|
+
this.#dialog.addEventListener('mousedown', (e) => {
|
|
11912
|
+
pressedOutside = e.target === this.#dialog;
|
|
11913
|
+
});
|
|
11914
|
+
this.#dialog.addEventListener('click', (e) => {
|
|
11915
|
+
if (pressedOutside && e.target === this.#dialog) {
|
|
11916
|
+
this.close();
|
|
11917
|
+
}
|
|
11918
|
+
});
|
|
11919
|
+
this.#dialog.addEventListener('cancel', (e) => {
|
|
11920
|
+
e.preventDefault();
|
|
11921
|
+
this.close();
|
|
11922
|
+
});
|
|
11560
11923
|
this.#dialog.addEventListener('close', () => {
|
|
11561
11924
|
this.dispatchEvent(
|
|
11562
11925
|
new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
|
|
@@ -11580,23 +11943,23 @@ var fml = (function (exports) {
|
|
|
11580
11943
|
this.replaceChildren(fragment);
|
|
11581
11944
|
wireTargets();
|
|
11582
11945
|
}
|
|
11583
|
-
get
|
|
11946
|
+
get header() {
|
|
11584
11947
|
return this.#title.textContent;
|
|
11585
11948
|
}
|
|
11586
|
-
set
|
|
11949
|
+
set header(v) {
|
|
11587
11950
|
this.#title.textContent = v ?? '';
|
|
11588
11951
|
}
|
|
11589
11952
|
/**
|
|
11590
|
-
* Opens the drawer under the given
|
|
11953
|
+
* Opens the drawer under the given header and waits for the callback: a
|
|
11591
11954
|
* resolved value paints the content section (which is returned), a
|
|
11592
11955
|
* rejection paints the problems and travels to the caller, and an update
|
|
11593
11956
|
* superseded by a newer one paints nothing.
|
|
11594
11957
|
*/
|
|
11595
|
-
async update(
|
|
11958
|
+
async update(header, cb) {
|
|
11596
11959
|
//the claim detaches any update still in flight: its outcome belongs to
|
|
11597
11960
|
//an abandoned opening and must neither be painted nor own the drawer
|
|
11598
11961
|
const claim = this.#updates.take();
|
|
11599
|
-
this.
|
|
11962
|
+
this.header = header;
|
|
11600
11963
|
this.#content.replaceChildren();
|
|
11601
11964
|
this.#restChrome();
|
|
11602
11965
|
this.#loading.removeAttribute('hidden');
|
|
@@ -11642,10 +12005,33 @@ var fml = (function (exports) {
|
|
|
11642
12005
|
this.#requests.request(this, this.#content, null, null)?.catch(() => undefined);
|
|
11643
12006
|
}
|
|
11644
12007
|
close() {
|
|
12008
|
+
if (this.#closing || !this.#dialog.open) {
|
|
12009
|
+
return;
|
|
12010
|
+
}
|
|
12011
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
12012
|
+
this.#dialog.close();
|
|
12013
|
+
return;
|
|
12014
|
+
}
|
|
12015
|
+
this.#dialog.setAttribute('closing', '');
|
|
12016
|
+
if (this.#dialog.getAnimations().length === 0) {
|
|
12017
|
+
this.#closeNow();
|
|
12018
|
+
return;
|
|
12019
|
+
}
|
|
12020
|
+
this.#closing = true;
|
|
12021
|
+
this.#dialog.addEventListener('animationend', this.#slideOutEnded);
|
|
12022
|
+
}
|
|
12023
|
+
#closeNow() {
|
|
12024
|
+
this.#stopSlidingOut();
|
|
11645
12025
|
this.#dialog.close();
|
|
11646
12026
|
}
|
|
12027
|
+
#stopSlidingOut() {
|
|
12028
|
+
this.#closing = false;
|
|
12029
|
+
this.#dialog.removeEventListener('animationend', this.#slideOutEnded);
|
|
12030
|
+
this.#dialog.removeAttribute('closing');
|
|
12031
|
+
}
|
|
11647
12032
|
/** Shows the modal, answering whether this call is the one that opened it. */
|
|
11648
12033
|
#show() {
|
|
12034
|
+
this.#stopSlidingOut();
|
|
11649
12035
|
if (this.#dialog.open) {
|
|
11650
12036
|
return false;
|
|
11651
12037
|
}
|
|
@@ -11692,6 +12078,9 @@ var fml = (function (exports) {
|
|
|
11692
12078
|
if (!listenerWired) {
|
|
11693
12079
|
listenerWired = true;
|
|
11694
12080
|
document.addEventListener('show-toast', (/** @type any */ e) => {
|
|
12081
|
+
if (!e.detail) {
|
|
12082
|
+
return;
|
|
12083
|
+
}
|
|
11695
12084
|
for (const region of REGIONS) {
|
|
11696
12085
|
region.show(e.detail.message, e.detail);
|
|
11697
12086
|
}
|
|
@@ -11703,8 +12092,13 @@ var fml = (function (exports) {
|
|
|
11703
12092
|
* Appends a toast carrying the message (a Failure shows its problems'
|
|
11704
12093
|
* reasons, one per line), severity picking the theme and the announcement,
|
|
11705
12094
|
* the toast retiring through its own timer or its dismiss button.
|
|
12095
|
+
*
|
|
12096
|
+
* The timer holds while the toast is hovered or holds the focus, so an
|
|
12097
|
+
* actionable toast waits for its reader: `action` is `{ label, onClick }`,
|
|
12098
|
+
* a button beside the message whose click answers and retires. The same
|
|
12099
|
+
* options travel in the `show-toast` event's detail.
|
|
11706
12100
|
* @param {any} message
|
|
11707
|
-
* @param {any} [options] severity and
|
|
12101
|
+
* @param {any} [options] severity, timeout and action
|
|
11708
12102
|
* @returns {HTMLElement}
|
|
11709
12103
|
*/
|
|
11710
12104
|
show(message, options = {}) {
|
|
@@ -11721,14 +12115,13 @@ var fml = (function (exports) {
|
|
|
11721
12115
|
icon.setAttribute('name', 'x-lg');
|
|
11722
12116
|
icon.setAttribute('aria-hidden', 'true');
|
|
11723
12117
|
dismiss.append(icon);
|
|
11724
|
-
item.append(body
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
}
|
|
11729
|
-
});
|
|
12118
|
+
item.append(body);
|
|
12119
|
+
let remaining = options.timeout ?? this.#timeout;
|
|
12120
|
+
let startedAt = performance.now();
|
|
12121
|
+
let timer = 0;
|
|
11730
12122
|
const retire = () => {
|
|
11731
|
-
|
|
12123
|
+
clearTimeout(timer);
|
|
12124
|
+
//the toast may hold the focus, on its own buttons: handing it
|
|
11732
12125
|
//back to the region keeps the reader somewhere rather than on <body>
|
|
11733
12126
|
if (item.contains(document.activeElement)) {
|
|
11734
12127
|
/** @type HTMLElement */ (this).focus();
|
|
@@ -11742,9 +12135,49 @@ var fml = (function (exports) {
|
|
|
11742
12135
|
item.remove();
|
|
11743
12136
|
}
|
|
11744
12137
|
};
|
|
12138
|
+
if (options.action?.label) {
|
|
12139
|
+
const action = document.createElement('button');
|
|
12140
|
+
action.type = 'button';
|
|
12141
|
+
action.className = 'ful-toast-action';
|
|
12142
|
+
action.textContent = String(options.action.label);
|
|
12143
|
+
action.addEventListener('click', () => {
|
|
12144
|
+
options.action.onClick?.();
|
|
12145
|
+
retire();
|
|
12146
|
+
});
|
|
12147
|
+
item.append(action);
|
|
12148
|
+
}
|
|
12149
|
+
item.append(dismiss);
|
|
12150
|
+
item.addEventListener('animationend', () => {
|
|
12151
|
+
if (item.classList.contains('ful-toast-out')) {
|
|
12152
|
+
item.remove();
|
|
12153
|
+
}
|
|
12154
|
+
});
|
|
12155
|
+
//the pointer and the focus hold independently: a count rather than a
|
|
12156
|
+
//flag, so the pointer leaving while the focus stays keeps the hold
|
|
12157
|
+
let holds = 0;
|
|
12158
|
+
const hold = () => {
|
|
12159
|
+
if (holds === 0) {
|
|
12160
|
+
clearTimeout(timer);
|
|
12161
|
+
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
|
12162
|
+
}
|
|
12163
|
+
++holds;
|
|
12164
|
+
};
|
|
12165
|
+
const release = () => {
|
|
12166
|
+
if (holds === 0) {
|
|
12167
|
+
return;
|
|
12168
|
+
}
|
|
12169
|
+
if (--holds === 0) {
|
|
12170
|
+
startedAt = performance.now();
|
|
12171
|
+
timer = setTimeout(retire, remaining);
|
|
12172
|
+
}
|
|
12173
|
+
};
|
|
12174
|
+
item.addEventListener('mouseenter', hold);
|
|
12175
|
+
item.addEventListener('mouseleave', release);
|
|
12176
|
+
item.addEventListener('focusin', hold);
|
|
12177
|
+
item.addEventListener('focusout', release);
|
|
11745
12178
|
dismiss.addEventListener('click', retire);
|
|
11746
12179
|
this.append(item);
|
|
11747
|
-
setTimeout(retire,
|
|
12180
|
+
timer = setTimeout(retire, remaining);
|
|
11748
12181
|
return item;
|
|
11749
12182
|
}
|
|
11750
12183
|
}
|
|
@@ -11855,7 +12288,7 @@ var fml = (function (exports) {
|
|
|
11855
12288
|
this.#active = index;
|
|
11856
12289
|
this.reflectTo('active', index);
|
|
11857
12290
|
if (this.rendered && index !== previous) {
|
|
11858
|
-
this.dispatchEvent(new CustomEvent('change', { detail: { active: index, previous } }));
|
|
12291
|
+
this.dispatchEvent(new CustomEvent('tabs:change', { detail: { active: index, previous } }));
|
|
11859
12292
|
}
|
|
11860
12293
|
if (this.#panels.length > 0 && (index !== previous || !this.rendered)) {
|
|
11861
12294
|
//the activation is the reader's own gesture: the chrome reports a
|
|
@@ -12076,6 +12509,8 @@ var fml = (function (exports) {
|
|
|
12076
12509
|
'info.tooltip': 'More information',
|
|
12077
12510
|
'dialog.acknowledge': 'Got it',
|
|
12078
12511
|
'dialog.close': 'Close',
|
|
12512
|
+
'dialog.confirm': 'Confirm',
|
|
12513
|
+
'dialog.cancel': 'Cancel',
|
|
12079
12514
|
'drawer.close': 'Close',
|
|
12080
12515
|
'spinner.loading': 'Loading…',
|
|
12081
12516
|
'toast.region': 'Notifications',
|
|
@@ -12118,6 +12553,8 @@ var fml = (function (exports) {
|
|
|
12118
12553
|
'info.tooltip': 'Maggiori informazioni',
|
|
12119
12554
|
'dialog.acknowledge': 'Ho capito',
|
|
12120
12555
|
'dialog.close': 'Chiudi',
|
|
12556
|
+
'dialog.confirm': 'Conferma',
|
|
12557
|
+
'dialog.cancel': 'Annulla',
|
|
12121
12558
|
'drawer.close': 'Chiudi',
|
|
12122
12559
|
'spinner.loading': 'Caricamento…',
|
|
12123
12560
|
'toast.region': 'Notifiche',
|
|
@@ -12160,6 +12597,8 @@ var fml = (function (exports) {
|
|
|
12160
12597
|
'info.tooltip': 'Más información',
|
|
12161
12598
|
'dialog.acknowledge': 'Entendido',
|
|
12162
12599
|
'dialog.close': 'Cerrar',
|
|
12600
|
+
'dialog.confirm': 'Confirmar',
|
|
12601
|
+
'dialog.cancel': 'Cancelar',
|
|
12163
12602
|
'drawer.close': 'Cerrar',
|
|
12164
12603
|
'spinner.loading': 'Cargando…',
|
|
12165
12604
|
'toast.region': 'Notificaciones',
|
|
@@ -12205,6 +12644,8 @@ var fml = (function (exports) {
|
|
|
12205
12644
|
'info.tooltip': 'Plus d’informations',
|
|
12206
12645
|
'dialog.acknowledge': 'J’ai compris',
|
|
12207
12646
|
'dialog.close': 'Fermer',
|
|
12647
|
+
'dialog.confirm': 'Confirmer',
|
|
12648
|
+
'dialog.cancel': 'Annuler',
|
|
12208
12649
|
'drawer.close': 'Fermer',
|
|
12209
12650
|
'spinner.loading': 'Chargement…',
|
|
12210
12651
|
'toast.region': 'Notifications',
|
|
@@ -12245,7 +12686,7 @@ var fml = (function (exports) {
|
|
|
12245
12686
|
const httpClient =
|
|
12246
12687
|
this.#httpClient ?? HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
|
|
12247
12688
|
//the fallback chain is baked here: en, the active language, the consumer's own strings
|
|
12248
|
-
const language = this.#language.split('-')[0];
|
|
12689
|
+
const language = this.#language.split('-')[0].toLowerCase();
|
|
12249
12690
|
const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
|
|
12250
12691
|
registry
|
|
12251
12692
|
.defineModule('l10n', Localization)
|