@optionfactory/fml 9.0.0-rc27 → 9.0.0-rc29
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/dist/client-errors.iife.js +21 -16
- 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 +16 -16
- package/dist/fml.css +13 -13
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +871 -883
- 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.d.mts +3 -3
- package/dist/ftl.iife.js +275 -679
- 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 +275 -679
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +13 -13
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +195 -71
- package/dist/ful.iife.js +564 -189
- 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 +564 -189
- package/dist/ful.mjs.map +1 -1
- package/dist/httpc.d.mts +9 -7
- package/dist/httpc.iife.js +32 -15
- package/dist/httpc.iife.js.map +1 -1
- package/dist/httpc.iife.min.js +1 -1
- package/dist/httpc.iife.min.js.map +1 -1
- package/dist/httpc.min.mjs +1 -1
- package/dist/httpc.min.mjs.map +1 -1
- package/dist/httpc.mjs +32 -15
- package/dist/httpc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +13 -13
- package/dist/web-types.json +17 -17
- package/package.json +2 -2
package/dist/ful.iife.js
CHANGED
|
@@ -691,7 +691,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
691
691
|
* alone and a composite field named for a prefix catches the problems of its
|
|
692
692
|
* subtree. Each control of that name gets `setCustomValidity(reason, rest)`,
|
|
693
693
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
694
|
-
* composite can use to route the problem to an inner control.
|
|
694
|
+
* composite can use to route the problem to an inner control. Several
|
|
695
|
+
* problems reaching one control with the same `rest` are joined one per
|
|
696
|
+
* line into a single call.
|
|
695
697
|
*
|
|
696
698
|
* A control counts only when it can show a problem, through a
|
|
697
699
|
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
@@ -706,7 +708,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
706
708
|
* Each `ful-field-error` of the form gets `aria-live="off"` when
|
|
707
709
|
* `scrollOnError` is set, since focusing the field announces its error, and
|
|
708
710
|
* `aria-live="polite"` otherwise. With `scrollOnError` and at least one
|
|
709
|
-
* problem, the topmost `:invalid` control is focused
|
|
711
|
+
* problem, the topmost `:invalid` control is focused; a `<fieldset>`
|
|
712
|
+
* matching `:invalid` for the controls it holds is not a candidate.
|
|
710
713
|
*
|
|
711
714
|
* A banner or `ful-field-error` belongs to the nearest enclosing form, so
|
|
712
715
|
* those of a nested form are left alone.
|
|
@@ -731,6 +734,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
731
734
|
});
|
|
732
735
|
}
|
|
733
736
|
const unmatched = [];
|
|
737
|
+
/** @type {Map<any, Map<string, string[]>>} */
|
|
738
|
+
const pins = new Map();
|
|
734
739
|
fieldErrors.forEach((e) => {
|
|
735
740
|
const name = /** @type {string} */ (e.context).replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
|
|
736
741
|
const parts = name.split('.');
|
|
@@ -742,12 +747,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
742
747
|
}
|
|
743
748
|
const context = parts.slice(i).join('.');
|
|
744
749
|
targets.forEach((input) => {
|
|
745
|
-
|
|
750
|
+
const contexts = pins.get(input) ?? new Map();
|
|
751
|
+
contexts.set(context, [...(contexts.get(context) ?? []), e.reason]);
|
|
752
|
+
pins.set(input, contexts);
|
|
746
753
|
});
|
|
747
754
|
return;
|
|
748
755
|
}
|
|
749
756
|
unmatched.push(e);
|
|
750
757
|
});
|
|
758
|
+
for (const [input, contexts] of pins) {
|
|
759
|
+
for (const [context, reasons] of contexts) {
|
|
760
|
+
input.setCustomValidity(reasons.join('\n'), context);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
751
763
|
const bannered = [...globalErrors, ...unmatched];
|
|
752
764
|
form.querySelectorAll('ful-errors').forEach((el) => {
|
|
753
765
|
if (!ofForm(el)) {
|
|
@@ -767,7 +779,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
767
779
|
return;
|
|
768
780
|
}
|
|
769
781
|
/** @type {HTMLElement[]} */ (Array.from(form.elements))
|
|
770
|
-
.filter((el) => el.matches(':invalid'))
|
|
782
|
+
.filter((el) => el.localName !== 'fieldset' && el.matches(':invalid'))
|
|
771
783
|
.sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y)[0]
|
|
772
784
|
?.focus();
|
|
773
785
|
}
|
|
@@ -786,9 +798,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
786
798
|
*
|
|
787
799
|
* The claim setters, the validity protocol and the aria wiring all act on the
|
|
788
800
|
* pieces, so a field with no native control returns a focusable piece of its
|
|
789
|
-
* own chrome as the control. The
|
|
790
|
-
* members that tolerate a not yet rendered element
|
|
791
|
-
*
|
|
801
|
+
* own chrome as the control. The claims, `value` and `focus()` are the
|
|
802
|
+
* members that tolerate a not yet rendered element: before the render the
|
|
803
|
+
* `value` getter answers what the rendered field would answer from its current
|
|
804
|
+
* declared state, and a value written then is held and applied by the render
|
|
805
|
+
* (see `_holdValue`). The other members need the pieces and so work only after
|
|
806
|
+
* the render.
|
|
792
807
|
*
|
|
793
808
|
* The inner controls carry `form=""`, so the host is the only element the form
|
|
794
809
|
* sees. Enter pressed in an inner `<input>` of any type but `file`, `button`,
|
|
@@ -824,6 +839,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
824
839
|
#claims;
|
|
825
840
|
#announces;
|
|
826
841
|
#also = [];
|
|
842
|
+
/** @type {{ value: any } | null} the value written before the render, which the render applies */
|
|
843
|
+
#held = null;
|
|
827
844
|
/** Sets the internals' role from the subclass's `static ROLE`. */
|
|
828
845
|
constructor() {
|
|
829
846
|
super();
|
|
@@ -1057,11 +1074,47 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1057
1074
|
_interactive() {
|
|
1058
1075
|
return !this.matches(':disabled') && !this.readonly;
|
|
1059
1076
|
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Upgrades as `ParsedElement.upgrade` does, then writes the value held by
|
|
1079
|
+
* `_holdValue`, if any, through the `value` setter: the value written
|
|
1080
|
+
* before the render wins over the declared one.
|
|
1081
|
+
* @returns {Promise<void>} rejecting with what the render threw
|
|
1082
|
+
*/
|
|
1083
|
+
async upgrade() {
|
|
1084
|
+
await super.upgrade();
|
|
1085
|
+
if (this.#held === null) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const { value } = this.#held;
|
|
1089
|
+
this.#held = null;
|
|
1090
|
+
this.value = value;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Keeps a value written before the field has its control, for a subclass's
|
|
1094
|
+
* `value` setter to call in place of writing it: the upgrade writes it
|
|
1095
|
+
* through the setter once the field has rendered, over the declared one.
|
|
1096
|
+
* The `value` attribute is left alone, so a reset still restores it.
|
|
1097
|
+
* @param {any} value
|
|
1098
|
+
*/
|
|
1099
|
+
_holdValue(value) {
|
|
1100
|
+
this.#held = { value };
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* The value a subclass's `value` getter shapes before the field has its
|
|
1104
|
+
* control: the one `_holdValue` kept, else the declared `value` attribute
|
|
1105
|
+
* read through its mapper.
|
|
1106
|
+
* @returns {any}
|
|
1107
|
+
*/
|
|
1108
|
+
_heldValue() {
|
|
1109
|
+
return this.#held === null ? this.declared('value') : this.#held.value;
|
|
1110
|
+
}
|
|
1060
1111
|
/**
|
|
1061
1112
|
* The field's value, `null` when empty. Every concrete field overrides the
|
|
1062
1113
|
* pair with its own semantics; the form extraction, `values =` assignment,
|
|
1063
1114
|
* the `change` detail and `formResetCallback` all go through it. The base
|
|
1064
|
-
* pair reads `undefined` and ignores writes.
|
|
1115
|
+
* pair reads `undefined` and ignores writes. A concrete field answers and
|
|
1116
|
+
* takes its value before the render too, through `_heldValue` and
|
|
1117
|
+
* `_holdValue`.
|
|
1065
1118
|
* @type {any}
|
|
1066
1119
|
*/
|
|
1067
1120
|
get value() {
|
|
@@ -1430,6 +1483,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1430
1483
|
this.addEventListener('input', (evt) => this.#touch(evt.target));
|
|
1431
1484
|
this.addEventListener('change', (evt) => this.#touch(evt.target));
|
|
1432
1485
|
this.addEventListener('focusin', (evt) => this.#focus(evt.target));
|
|
1486
|
+
this.form.addEventListener('reset', (evt) => {
|
|
1487
|
+
if (evt.target !== this.form) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
for (const name of new Set([...this.#saved.keys(), ...this.#snapshot().keys()])) {
|
|
1491
|
+
this.#edits.set(name, ++this.#sequence);
|
|
1492
|
+
}
|
|
1493
|
+
// the controls are restored only after the event, so the state is read at the next frame
|
|
1494
|
+
if (this.#frame === 0) {
|
|
1495
|
+
this.#frame = requestAnimationFrame(() => this.#refresh());
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1433
1498
|
index_mjs.Rendering.waitForChildren(this)
|
|
1434
1499
|
.catch(() => undefined)
|
|
1435
1500
|
.then(() => this.commit());
|
|
@@ -1447,7 +1512,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1447
1512
|
}
|
|
1448
1513
|
#submitting = false;
|
|
1449
1514
|
/**
|
|
1450
|
-
* Runs the submit pipeline, with the spinner up
|
|
1515
|
+
* Runs the submit pipeline, with the spinner up until its outcome:
|
|
1451
1516
|
*
|
|
1452
1517
|
* 1. builds the loader: the component the `loader` attribute names
|
|
1453
1518
|
* (`loaders:form` by default), called as `create(form)`
|
|
@@ -1464,8 +1529,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1464
1529
|
* 6. has the loader `submit` the request and `transform` the response
|
|
1465
1530
|
* 7. under `track-changes`, commits the values extracted at step 2 (see
|
|
1466
1531
|
* `commit`): an edit made while the submit was in flight stays unsaved
|
|
1467
|
-
* 8. fires `submit:success` with `{ submitter,
|
|
1468
|
-
* `response` being the transformed one
|
|
1532
|
+
* 8. releases the spinner and fires `submit:success` with `{ submitter,
|
|
1533
|
+
* values, request, response }`, `response` being the transformed one
|
|
1469
1534
|
*
|
|
1470
1535
|
* Anything thrown from step 1 on, a missing loader or a throwing mapper
|
|
1471
1536
|
* included, fires `submit:failure` with `{ submitter, values, request,
|
|
@@ -1474,6 +1539,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1474
1539
|
* `console.warn`. The `values` of both outcome events are the extracted
|
|
1475
1540
|
* ones, not a replacement a `submit` listener put on its detail.
|
|
1476
1541
|
*
|
|
1542
|
+
* The form settles before either outcome event (0016): its listeners see
|
|
1543
|
+
* the spinner released and the buttons no longer held off, the problems of
|
|
1544
|
+
* a `Failure` already shown, and the submit no longer in flight, so one
|
|
1545
|
+
* of them may submit again.
|
|
1546
|
+
*
|
|
1477
1547
|
* A call while a submit is in flight returns at once, doing nothing.
|
|
1478
1548
|
* @param {HTMLElement} [submitter] the button that submitted, if any; it is
|
|
1479
1549
|
* the only button whose name and value are submitted
|
|
@@ -1489,6 +1559,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1489
1559
|
this.spinner(true);
|
|
1490
1560
|
let values;
|
|
1491
1561
|
let request;
|
|
1562
|
+
let outcome;
|
|
1492
1563
|
try {
|
|
1493
1564
|
const loader = this.component(this.declared('loader') ?? 'loaders:form').create(this);
|
|
1494
1565
|
values = Bindings.extractFrom(this.form, submitter);
|
|
@@ -1516,32 +1587,28 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1516
1587
|
if (submitted) {
|
|
1517
1588
|
this.#commitFrom(submitted.saved, submitted.upTo);
|
|
1518
1589
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
}),
|
|
1525
|
-
);
|
|
1526
|
-
return true;
|
|
1590
|
+
outcome = new CustomEvent('submit:success', {
|
|
1591
|
+
bubbles: true,
|
|
1592
|
+
cancelable: false,
|
|
1593
|
+
detail: { submitter, values, request, response: mapped },
|
|
1594
|
+
});
|
|
1527
1595
|
} catch (e) {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}),
|
|
1534
|
-
);
|
|
1596
|
+
outcome = new CustomEvent('submit:failure', {
|
|
1597
|
+
bubbles: true,
|
|
1598
|
+
cancelable: false,
|
|
1599
|
+
detail: { submitter, values, request, exception: e },
|
|
1600
|
+
});
|
|
1535
1601
|
if (e instanceof index_mjs$1.Failure) {
|
|
1536
1602
|
this.errors = e.problems;
|
|
1537
1603
|
} else {
|
|
1538
1604
|
console.warn('failed to submit form', this, 'reason:', e);
|
|
1539
1605
|
}
|
|
1540
|
-
return false;
|
|
1541
1606
|
} finally {
|
|
1542
1607
|
this.#submitting = false;
|
|
1543
1608
|
this.spinner(false);
|
|
1544
1609
|
}
|
|
1610
|
+
this.dispatchEvent(outcome);
|
|
1611
|
+
return outcome.type === 'submit:success';
|
|
1545
1612
|
}
|
|
1546
1613
|
/**
|
|
1547
1614
|
* Saves the unsaved edits, for a page action that would lose them, such as
|
|
@@ -1560,18 +1627,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1560
1627
|
}
|
|
1561
1628
|
/**
|
|
1562
1629
|
* Resets the native form, each field returning to its default value. Under
|
|
1563
|
-
* `track-changes`
|
|
1564
|
-
*
|
|
1630
|
+
* `track-changes` a reset, this one or a native reset button's, counts
|
|
1631
|
+
* every control as edited: the form is dirty if the defaults differ from
|
|
1632
|
+
* what was last committed.
|
|
1565
1633
|
*/
|
|
1566
1634
|
reset() {
|
|
1567
1635
|
this.form.reset();
|
|
1568
|
-
if (
|
|
1569
|
-
|
|
1570
|
-
}
|
|
1571
|
-
for (const name of new Set([...this.#saved.keys(), ...this.#snapshot().keys()])) {
|
|
1572
|
-
this.#edits.set(name, ++this.#sequence);
|
|
1636
|
+
if (this.#tracking) {
|
|
1637
|
+
this.#refresh();
|
|
1573
1638
|
}
|
|
1574
|
-
this.#refresh();
|
|
1575
1639
|
}
|
|
1576
1640
|
/** name → the serialized values the last commit recorded */
|
|
1577
1641
|
#saved = new Map();
|
|
@@ -1996,6 +2060,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1996
2060
|
_input;
|
|
1997
2061
|
/** @type {(HTMLInputElement|HTMLTextAreaElement)[]} */
|
|
1998
2062
|
#controls = [];
|
|
2063
|
+
/**
|
|
2064
|
+
* The serialized value a date or time control last reported, or held when
|
|
2065
|
+
* focused or written.
|
|
2066
|
+
* @type {string|null}
|
|
2067
|
+
*/
|
|
2068
|
+
#committed = null;
|
|
1999
2069
|
/**
|
|
2000
2070
|
* The type the field renders, which a subclass overrides to wrap another
|
|
2001
2071
|
* native type. The base answers the declared `type`, else `number` under
|
|
@@ -2075,20 +2145,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2075
2145
|
});
|
|
2076
2146
|
return;
|
|
2077
2147
|
}
|
|
2078
|
-
/** @type {string|null} */
|
|
2079
|
-
let committed = null;
|
|
2080
2148
|
let typed = false;
|
|
2081
2149
|
const commit = () => {
|
|
2082
2150
|
typed = false;
|
|
2083
2151
|
const now = JSON.stringify(this.value);
|
|
2084
|
-
if (now === committed) {
|
|
2152
|
+
if (now === this.#committed) {
|
|
2085
2153
|
return;
|
|
2086
2154
|
}
|
|
2087
|
-
committed = now;
|
|
2155
|
+
this.#committed = now;
|
|
2088
2156
|
this._notifyChange();
|
|
2089
2157
|
};
|
|
2090
2158
|
control.addEventListener('focus', () => {
|
|
2091
|
-
committed = JSON.stringify(this.value);
|
|
2159
|
+
this.#committed = JSON.stringify(this.value);
|
|
2092
2160
|
});
|
|
2093
2161
|
control.addEventListener('keydown', (/** @type any */ evt) => {
|
|
2094
2162
|
if (evt.key === 'Enter' && !evt.isComposing) {
|
|
@@ -2112,14 +2180,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2112
2180
|
* `v-type="number"`, where text that does not decode is answered as it is.
|
|
2113
2181
|
* Writing `null`, `undefined` or `''` empties the control; anything else is
|
|
2114
2182
|
* written as its string, replacing whatever was being typed, as a native
|
|
2115
|
-
* control's value does
|
|
2116
|
-
* `
|
|
2183
|
+
* control's value does, and is not an edit: a date or time control that
|
|
2184
|
+
* has the focus reports no `change` for it when it is left. Answers a
|
|
2185
|
+
* `string`, a `number` or `null`; typed `any` because subclasses answer
|
|
2186
|
+
* other values. Before the render the same shaping reads the value last
|
|
2187
|
+
* written, or the `value` attribute, as its text, and a write is held for
|
|
2188
|
+
* the render to apply.
|
|
2117
2189
|
* @type {any}
|
|
2118
2190
|
*/
|
|
2119
2191
|
get value() {
|
|
2120
2192
|
const uppercase = this.declared('uppercase');
|
|
2121
2193
|
const trim = this.declared('trim');
|
|
2122
|
-
const v = this._input.value
|
|
2194
|
+
const v = this._input ? this._input.value : `${this._heldValue() ?? ''}`;
|
|
2123
2195
|
const uppercased = uppercase ? v.toUpperCase() : v;
|
|
2124
2196
|
const trimmed = trim ? uppercased.trim() : uppercased;
|
|
2125
2197
|
if (trimmed === '') {
|
|
@@ -2133,7 +2205,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2133
2205
|
return normalized;
|
|
2134
2206
|
}
|
|
2135
2207
|
set value(value) {
|
|
2208
|
+
if (!this._input) {
|
|
2209
|
+
this._holdValue(value);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2136
2212
|
this._input.value = value === '' || value === undefined || value === null ? '' : `${value}`;
|
|
2213
|
+
this.#committed = JSON.stringify(this.value);
|
|
2137
2214
|
}
|
|
2138
2215
|
/**
|
|
2139
2216
|
* The control's placeholder, `null` when there is none. The control always
|
|
@@ -2156,6 +2233,20 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2156
2233
|
|
|
2157
2234
|
const blankAsNull = (v) => (v === '' ? null : v);
|
|
2158
2235
|
|
|
2236
|
+
/**
|
|
2237
|
+
* The local midnight of the yyyy-mm-dd date a text names, taking a year below
|
|
2238
|
+
* 100 as it is and refusing a day its month does not have.
|
|
2239
|
+
* @param {string} text
|
|
2240
|
+
* @returns {Date} an invalid date when the text names no date
|
|
2241
|
+
*/
|
|
2242
|
+
const localMidnight = (text) => {
|
|
2243
|
+
const [y, m, d] = text.split('-').map(Number);
|
|
2244
|
+
const date = new Date(0);
|
|
2245
|
+
date.setFullYear(y, m - 1, d);
|
|
2246
|
+
date.setHours(0, 0, 0, 0);
|
|
2247
|
+
return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d ? date : new Date(Number.NaN);
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2159
2250
|
/**
|
|
2160
2251
|
* Formats the yyyy-mm-dd date in its content as a numeric date, in the locale
|
|
2161
2252
|
* its `locale` attribute names, else the page's, else the platform default.
|
|
@@ -2165,10 +2256,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2165
2256
|
class LocalDate extends index_mjs.ParsedElement {
|
|
2166
2257
|
static attributes = ['locale', 'default'];
|
|
2167
2258
|
render() {
|
|
2168
|
-
const
|
|
2169
|
-
|
|
2170
|
-
const parsed = content === '' ? null : new Date(y, m - 1, d);
|
|
2171
|
-
if (parsed === null || Number.isNaN(parsed.getTime())) {
|
|
2259
|
+
const parsed = localMidnight(this.textContent.trim());
|
|
2260
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
2172
2261
|
this.replaceChildren(this.declared('default') ?? '');
|
|
2173
2262
|
return;
|
|
2174
2263
|
}
|
|
@@ -2207,7 +2296,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2207
2296
|
);
|
|
2208
2297
|
}
|
|
2209
2298
|
static #parse(v) {
|
|
2210
|
-
return /^\d{4}-\d{2}-\d{2}$/.test(v) ?
|
|
2299
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(v) ? localMidnight(v) : new Date(v);
|
|
2211
2300
|
}
|
|
2212
2301
|
/**
|
|
2213
2302
|
* Converts an ISO instant to the `yyyy-mm-ddThh:mm:ss.sss` wall clock time
|
|
@@ -2394,14 +2483,24 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2394
2483
|
return 'datetime-local';
|
|
2395
2484
|
}
|
|
2396
2485
|
/**
|
|
2397
|
-
* The entered time as a UTC ISO instant.
|
|
2486
|
+
* The entered time as a UTC ISO instant. Before the render it answers the
|
|
2487
|
+
* value last written, or the `value` attribute, as the rendered field
|
|
2488
|
+
* would, and a write is held for the render to apply.
|
|
2398
2489
|
* @returns {string|null} null when empty or when the input holds text that does not parse
|
|
2399
2490
|
*/
|
|
2400
2491
|
get value() {
|
|
2492
|
+
if (!this._input) {
|
|
2493
|
+
const held = this._heldValue();
|
|
2494
|
+
return Instant.localToIso(held ? Instant.isoToLocal(held) : '');
|
|
2495
|
+
}
|
|
2401
2496
|
return Instant.localToIso(this._input.value);
|
|
2402
2497
|
}
|
|
2403
2498
|
/** @param {string|null|undefined} v an ISO instant; a falsy value empties the input */
|
|
2404
2499
|
set value(v) {
|
|
2500
|
+
if (!this._input) {
|
|
2501
|
+
this._holdValue(v);
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2405
2504
|
super.value = v ? Instant.isoToLocal(v) : '';
|
|
2406
2505
|
}
|
|
2407
2506
|
/**
|
|
@@ -2581,9 +2680,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2581
2680
|
this.files = InputFile.list(files);
|
|
2582
2681
|
this._notifyChange();
|
|
2583
2682
|
});
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2683
|
+
// capture runs before the base listener notifying change, so the event carries the constrained files
|
|
2684
|
+
this._input.addEventListener(
|
|
2685
|
+
'change',
|
|
2686
|
+
() => {
|
|
2687
|
+
this.#update();
|
|
2688
|
+
},
|
|
2689
|
+
{ capture: true },
|
|
2690
|
+
);
|
|
2587
2691
|
return { ...pieces, freeze: this.#group };
|
|
2588
2692
|
}
|
|
2589
2693
|
#update() {
|
|
@@ -2690,7 +2794,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2690
2794
|
* @returns {boolean}
|
|
2691
2795
|
*/
|
|
2692
2796
|
get multiple() {
|
|
2693
|
-
return this._input.multiple;
|
|
2797
|
+
return this._input ? this._input.multiple : this.declared('multiple');
|
|
2694
2798
|
}
|
|
2695
2799
|
/** @param {boolean} v */
|
|
2696
2800
|
set multiple(v) {
|
|
@@ -2698,11 +2802,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2698
2802
|
this.reflectTo('multiple', v);
|
|
2699
2803
|
}
|
|
2700
2804
|
/**
|
|
2701
|
-
* The selected files, after the constraints.
|
|
2805
|
+
* The selected files, after the constraints; none before the render.
|
|
2702
2806
|
* @returns {FileList}
|
|
2703
2807
|
*/
|
|
2704
2808
|
get files() {
|
|
2705
|
-
return this._input.files;
|
|
2809
|
+
return this._input ? this._input.files : InputFile.list();
|
|
2706
2810
|
}
|
|
2707
2811
|
/**
|
|
2708
2812
|
* Replaces the selection and runs the constraints over it, refreshing the
|
|
@@ -2729,7 +2833,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2729
2833
|
}
|
|
2730
2834
|
/**
|
|
2731
2835
|
* The names of the selected files: an array when `multiple`, otherwise the
|
|
2732
|
-
* one name or null.
|
|
2836
|
+
* one name or null. Before the render no file is selected yet.
|
|
2733
2837
|
* @returns {string[]|string|null}
|
|
2734
2838
|
*/
|
|
2735
2839
|
get value() {
|
|
@@ -2738,11 +2842,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
2738
2842
|
}
|
|
2739
2843
|
/**
|
|
2740
2844
|
* Empties the selection for a falsy value and ignores any other, since a
|
|
2741
|
-
* file name cannot select a file.
|
|
2845
|
+
* file name cannot select a file. Before the render there is nothing to
|
|
2846
|
+
* empty, so it does nothing.
|
|
2742
2847
|
* @param {any} v
|
|
2743
2848
|
*/
|
|
2744
2849
|
set value(v) {
|
|
2745
|
-
if (v) {
|
|
2850
|
+
if (v || !this._input) {
|
|
2746
2851
|
return;
|
|
2747
2852
|
}
|
|
2748
2853
|
this.files = InputFile.list();
|
|
@@ -3026,7 +3131,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3026
3131
|
* which light dismiss then closes with no script of your own. The popover
|
|
3027
3132
|
* is given the generated id when it has none. A `button` or `input`
|
|
3028
3133
|
* invoker gets a `popovertarget` naming it; any other element toggles it
|
|
3029
|
-
* on click and on the Enter and Space keys
|
|
3134
|
+
* on click and on the Enter and Space keys; a click on it while the
|
|
3135
|
+
* popover is open closes the popover rather than opening it again
|
|
3030
3136
|
* @param {boolean} [options.expanded] sets the invoker's `aria-expanded` to
|
|
3031
3137
|
* `false` at once and keeps it in step with the popover's `toggle` events
|
|
3032
3138
|
* @param {boolean} [options.stretch] in the script placement, sizes the
|
|
@@ -3050,7 +3156,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3050
3156
|
if (invoker.localName === 'button' || invoker.localName === 'input') {
|
|
3051
3157
|
invoker.setAttribute('popovertarget', popover.id);
|
|
3052
3158
|
} else {
|
|
3053
|
-
|
|
3159
|
+
let pressedOpen = false;
|
|
3160
|
+
invoker.addEventListener(
|
|
3161
|
+
'pointerdown',
|
|
3162
|
+
() => {
|
|
3163
|
+
pressedOpen = popover.matches(':popover-open');
|
|
3164
|
+
},
|
|
3165
|
+
true,
|
|
3166
|
+
);
|
|
3167
|
+
invoker.addEventListener('click', () => {
|
|
3168
|
+
const dismissed = pressedOpen && !popover.matches(':popover-open');
|
|
3169
|
+
pressedOpen = false;
|
|
3170
|
+
if (!dismissed) {
|
|
3171
|
+
popover.togglePopover();
|
|
3172
|
+
}
|
|
3173
|
+
});
|
|
3054
3174
|
invoker.addEventListener('keydown', (/** @type any */ evt) => {
|
|
3055
3175
|
if (evt.key !== 'Enter' && evt.key !== ' ') {
|
|
3056
3176
|
return;
|
|
@@ -3152,7 +3272,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3152
3272
|
* @typedef {{ request(method: string, url: string): any }} SelectHttpClient
|
|
3153
3273
|
*/
|
|
3154
3274
|
|
|
3155
|
-
const byKeys = (entries, keys) => entries.filter(({ key }) => keys.some((r) => r
|
|
3275
|
+
const byKeys = (entries, keys) => entries.filter(({ key }) => keys.some((r) => String(r) === String(key)));
|
|
3156
3276
|
const byLabel = (entries, needle) =>
|
|
3157
3277
|
entries.filter(({ label }) => (label ?? '').toLowerCase().includes(needle?.toLowerCase() ?? ''));
|
|
3158
3278
|
|
|
@@ -3197,7 +3317,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3197
3317
|
await this.#ensureFetched();
|
|
3198
3318
|
}
|
|
3199
3319
|
/**
|
|
3200
|
-
* The entries whose key
|
|
3320
|
+
* The entries whose key reads as the same text as one of the keys, in
|
|
3321
|
+
* vocabulary order.
|
|
3201
3322
|
* @param {...any} keys
|
|
3202
3323
|
* @returns {Promise<SelectEntry[]>}
|
|
3203
3324
|
* @throws when the fetch fails, or when an invalidation supersedes it
|
|
@@ -3339,7 +3460,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3339
3460
|
*/
|
|
3340
3461
|
async invalidate() {}
|
|
3341
3462
|
/**
|
|
3342
|
-
* The entries whose key
|
|
3463
|
+
* The entries whose key reads as the same text as one of the keys, in
|
|
3464
|
+
* vocabulary order.
|
|
3343
3465
|
* @param {...any} keys
|
|
3344
3466
|
* @returns {SelectEntry[]}
|
|
3345
3467
|
*/
|
|
@@ -3558,11 +3680,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3558
3680
|
this.replaceChildren(fragment);
|
|
3559
3681
|
}
|
|
3560
3682
|
#selected() {
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
);
|
|
3683
|
+
if (!this.#menu || this.#menu.hidden) {
|
|
3684
|
+
return null;
|
|
3685
|
+
}
|
|
3686
|
+
return this.#menu.querySelector('li[selected]') ?? this.#menu.querySelector('li:not([aria-disabled="true"])');
|
|
3566
3687
|
}
|
|
3567
3688
|
#highlight(li) {
|
|
3568
3689
|
if (!li) {
|
|
@@ -3581,11 +3702,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3581
3702
|
}
|
|
3582
3703
|
/**
|
|
3583
3704
|
* Picks the highlighted row, or the first enabled one when none is: hides
|
|
3584
|
-
* the dropdown and dispatches `change`. Does nothing when
|
|
3705
|
+
* the dropdown and dispatches `change`. Does nothing when the highlighted
|
|
3706
|
+
* row is disabled, when no row is enabled, or while a search is loading
|
|
3707
|
+
* and the rows are hidden.
|
|
3585
3708
|
*/
|
|
3586
3709
|
acceptSelection() {
|
|
3587
3710
|
const selected = this.#selected();
|
|
3588
|
-
if (!selected) {
|
|
3711
|
+
if (!selected || selected.matches('[aria-disabled="true"]')) {
|
|
3589
3712
|
return;
|
|
3590
3713
|
}
|
|
3591
3714
|
this.#change(selected);
|
|
@@ -3594,7 +3717,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3594
3717
|
* Renders the entries as rows and highlights the first one whose key is
|
|
3595
3718
|
* picked, else the row the template marked `selected`, else the first
|
|
3596
3719
|
* enabled row. A row is marked `picked` and
|
|
3597
|
-
* `aria-selected="true"` when its key
|
|
3720
|
+
* `aria-selected="true"` when its key reads as the same text as one of the keys, and
|
|
3598
3721
|
* `aria-disabled="true"` when its `metadata.disabled` is truthy, titled with
|
|
3599
3722
|
* `metadata.reason`. No entries shows the empty state instead of the menu.
|
|
3600
3723
|
* @param {SelectEntry[]} values
|
|
@@ -3610,7 +3733,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3610
3733
|
this.#optionstemplate.withOverlay(data).renderTo(this.#menu);
|
|
3611
3734
|
for (const [index, li] of [...this.#menu.children].entries()) {
|
|
3612
3735
|
const entry = values[index];
|
|
3613
|
-
const picked = keys.some((r) => r
|
|
3736
|
+
const picked = keys.some((r) => String(r) === String(entry?.key));
|
|
3614
3737
|
li.toggleAttribute('picked', picked);
|
|
3615
3738
|
li.setAttribute('aria-selected', picked ? 'true' : 'false');
|
|
3616
3739
|
const disabled = !!entry?.metadata?.disabled;
|
|
@@ -3627,8 +3750,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3627
3750
|
}
|
|
3628
3751
|
this.#empty.toggleAttribute('hidden', values.length !== 0);
|
|
3629
3752
|
this.#menu.toggleAttribute('hidden', values.length === 0);
|
|
3630
|
-
const current = values.findIndex(({ key }) => keys.some((r) => r
|
|
3631
|
-
this.#highlight(
|
|
3753
|
+
const current = values.findIndex(({ key }) => keys.some((r) => String(r) === String(key)));
|
|
3754
|
+
this.#highlight(
|
|
3755
|
+
current >= 0
|
|
3756
|
+
? this.#menu.children[current]
|
|
3757
|
+
: (this.#menu.querySelector('li[selected]:not([aria-disabled="true"])') ??
|
|
3758
|
+
this.#menu.querySelector('li:not([aria-disabled="true"])')),
|
|
3759
|
+
);
|
|
3632
3760
|
}
|
|
3633
3761
|
#change(target) {
|
|
3634
3762
|
const index = target.getAttribute('value');
|
|
@@ -3702,7 +3830,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3702
3830
|
}
|
|
3703
3831
|
/**
|
|
3704
3832
|
* Moves the highlight to the next or previous enabled row when the dropdown
|
|
3705
|
-
* is shown, staying put at the edge
|
|
3833
|
+
* is shown, staying put at the edge and while a search is loading, and
|
|
3834
|
+
* shows it otherwise.
|
|
3706
3835
|
* @param {boolean} forward
|
|
3707
3836
|
* @param {() => SelectEntry[]|Promise<SelectEntry[]>} loader used only to show
|
|
3708
3837
|
* @param {any[]} [keys] the keys currently selected, used only to show
|
|
@@ -3730,10 +3859,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3730
3859
|
return null;
|
|
3731
3860
|
}
|
|
3732
3861
|
/**
|
|
3733
|
-
* Highlights the first enabled row, or the last one.
|
|
3862
|
+
* Highlights the first enabled row, or the last one. Does nothing while a
|
|
3863
|
+
* search is loading and the rows are hidden.
|
|
3734
3864
|
* @param {boolean} first
|
|
3735
3865
|
*/
|
|
3736
3866
|
jump(first) {
|
|
3867
|
+
if (this.#menu.hidden) {
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3737
3870
|
const edge = first ? this.#menu.firstElementChild : this.#menu.lastElementChild;
|
|
3738
3871
|
const target = this.#walk(edge, first);
|
|
3739
3872
|
if (target) {
|
|
@@ -3743,6 +3876,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3743
3876
|
/**
|
|
3744
3877
|
* Moves the highlight by as many enabled rows as the menu shows at once,
|
|
3745
3878
|
* one when that cannot be measured, stopping at the last enabled row.
|
|
3879
|
+
* Does nothing while a search is loading.
|
|
3746
3880
|
* @param {boolean} forward
|
|
3747
3881
|
*/
|
|
3748
3882
|
page(forward) {
|
|
@@ -3867,6 +4001,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3867
4001
|
#control;
|
|
3868
4002
|
#ddmenu;
|
|
3869
4003
|
#input;
|
|
4004
|
+
#label;
|
|
3870
4005
|
#items;
|
|
3871
4006
|
#itemstemplate;
|
|
3872
4007
|
#multiple;
|
|
@@ -3891,6 +4026,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3891
4026
|
});
|
|
3892
4027
|
const fragment = this.template().withOverlay({ slots, name }).render();
|
|
3893
4028
|
this.#input = fragment.querySelector('ful-control > input');
|
|
4029
|
+
this.#label = fragment.querySelector('label');
|
|
3894
4030
|
this.#items = fragment.querySelector('ful-item-list');
|
|
3895
4031
|
this.#itemstemplate =
|
|
3896
4032
|
slots.items && !index_mjs.Fragments.isBlank(slots.items) ? index_mjs.Templates.fromFragment(slots.items) : null;
|
|
@@ -3922,16 +4058,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3922
4058
|
fragment,
|
|
3923
4059
|
control: this.#input,
|
|
3924
4060
|
error: fragment.querySelector('ful-field-error'),
|
|
3925
|
-
label:
|
|
4061
|
+
label: this.#label,
|
|
3926
4062
|
};
|
|
3927
4063
|
}
|
|
3928
4064
|
/**
|
|
3929
4065
|
* Pointer interaction: the element toggles the dropdown, the item list's
|
|
3930
|
-
* remove buttons and the control's badges drop their entry.
|
|
4066
|
+
* remove buttons and the control's badges drop their entry. A click on the
|
|
4067
|
+
* label is left to the click the browser forwards to the input, which
|
|
4068
|
+
* toggles once.
|
|
3931
4069
|
*/
|
|
3932
4070
|
#wireChrome() {
|
|
3933
4071
|
this.addEventListener('click', (/** @type any */ e) => {
|
|
3934
|
-
if (!this._interactive()) {
|
|
4072
|
+
if (!this._interactive() || this.#label.contains(e.target)) {
|
|
3935
4073
|
return;
|
|
3936
4074
|
}
|
|
3937
4075
|
const elsewhere = e.target.closest('button, [role="button"], a[href], input, select, textarea');
|
|
@@ -3939,6 +4077,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
3939
4077
|
return;
|
|
3940
4078
|
}
|
|
3941
4079
|
if (this.#ddmenu.shown) {
|
|
4080
|
+
this.#abortdload();
|
|
3942
4081
|
this.#close();
|
|
3943
4082
|
return;
|
|
3944
4083
|
}
|
|
@@ -4021,6 +4160,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4021
4160
|
#wireSelection() {
|
|
4022
4161
|
this.#ddmenu.addEventListener('change', (e) => {
|
|
4023
4162
|
e.stopPropagation();
|
|
4163
|
+
this.#abortdload();
|
|
4024
4164
|
if (!this._interactive()) {
|
|
4025
4165
|
this.#close();
|
|
4026
4166
|
return;
|
|
@@ -4055,21 +4195,25 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4055
4195
|
* another control needs when that control changes. A key the loader no
|
|
4056
4196
|
* longer knows is dropped from the selection, so a value invalidated by the
|
|
4057
4197
|
* change does not survive it, and one it still knows keeps its place with a
|
|
4058
|
-
* fresh label. The badges and items follow,
|
|
4198
|
+
* fresh label. The badges and items follow, an open dropdown runs its
|
|
4199
|
+
* search again, and no `change` is dispatched.
|
|
4059
4200
|
*
|
|
4060
4201
|
* Where the vocabulary lives at a different address, write `src` or call
|
|
4061
4202
|
* `reconfigure`, which build the loader for the new url and reload.
|
|
4062
4203
|
* @returns {Promise<void>}
|
|
4063
|
-
* @throws what the loader's invalidate, prefetch
|
|
4204
|
+
* @throws what the loader's invalidate, prefetch, key lookup or search
|
|
4205
|
+
* throws
|
|
4064
4206
|
*/
|
|
4065
4207
|
async reload() {
|
|
4066
4208
|
await this.#loader.invalidate?.();
|
|
4067
4209
|
await this.#loader.prefetch?.();
|
|
4068
4210
|
const keys = [...this.#values.keys()];
|
|
4069
|
-
if (keys.length
|
|
4070
|
-
|
|
4211
|
+
if (keys.length !== 0) {
|
|
4212
|
+
await this.#resolve(keys, this.#assignments.take());
|
|
4213
|
+
}
|
|
4214
|
+
if (this.#ddmenu.shown) {
|
|
4215
|
+
await this.#open();
|
|
4071
4216
|
}
|
|
4072
|
-
await this.#resolve(keys, this.#assignments.take());
|
|
4073
4217
|
}
|
|
4074
4218
|
/**
|
|
4075
4219
|
* The url the loader asks for the vocabulary, reflected as the `src`
|
|
@@ -4260,6 +4404,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4260
4404
|
return;
|
|
4261
4405
|
}
|
|
4262
4406
|
e.preventDefault();
|
|
4407
|
+
this.#abortdload();
|
|
4263
4408
|
this.#editing = false;
|
|
4264
4409
|
this.#display();
|
|
4265
4410
|
this.#ddmenu.acceptSelection();
|
|
@@ -4284,6 +4429,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4284
4429
|
if (forward && !this.#ddmenu.shown) {
|
|
4285
4430
|
this.#open();
|
|
4286
4431
|
} else if (!forward && this.#ddmenu.shown) {
|
|
4432
|
+
this.#abortdload();
|
|
4287
4433
|
this.#close();
|
|
4288
4434
|
}
|
|
4289
4435
|
return;
|
|
@@ -4380,27 +4526,30 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4380
4526
|
}
|
|
4381
4527
|
/**
|
|
4382
4528
|
* Selects the keys, coerced by `k-type`: a key, an array of keys, or null or
|
|
4383
|
-
* undefined for none. The empty string is a key like any other
|
|
4384
|
-
* are applied at once, each labeled by itself until the loader's `exact`
|
|
4529
|
+
* undefined for none. The empty string is a key like any other, and a
|
|
4530
|
+
* single select keeps only the first key. The keys are applied at once, each labeled by itself until the loader's `exact`
|
|
4385
4531
|
* answers; a key it does not answer is then dropped, a key removed in the
|
|
4386
4532
|
* meantime stays removed, and a later assignment wins over the late answer.
|
|
4387
4533
|
* A failed lookup keeps the keys and surfaces as an unhandled rejection.
|
|
4388
4534
|
* No `change` is dispatched. A key containing a
|
|
4389
4535
|
* comma, which the value attribute cannot carry, is kept and warned about
|
|
4390
|
-
* once per element.
|
|
4536
|
+
* once per element. Before the render the write is held for the render to
|
|
4537
|
+
* apply, over the `value` attribute, which it leaves alone.
|
|
4391
4538
|
* @param {any} vs
|
|
4392
4539
|
*/
|
|
4393
4540
|
set value(vs) {
|
|
4394
|
-
|
|
4541
|
+
if (!this.#control) {
|
|
4542
|
+
this._holdValue(vs);
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
const given = (vs == null ? [] : Array.isArray(vs) ? vs : [vs]).map((k) => this.#coerceKey(k));
|
|
4546
|
+
const keys = this.#multiple ? given : given.slice(0, 1);
|
|
4395
4547
|
if (!this.#warnedComma && keys.some((k) => typeof k === 'string' && k.includes(','))) {
|
|
4396
4548
|
this.#warnedComma = true;
|
|
4397
4549
|
console.warn('a ful-select key cannot contain a comma: it is unexpressible in the value attribute', this);
|
|
4398
4550
|
}
|
|
4399
4551
|
this.#values = new Map(keys.map((k) => [k, { key: k, label: k, metadata: undefined }]));
|
|
4400
4552
|
const claim = this.#assignments.take();
|
|
4401
|
-
if (!this.#control) {
|
|
4402
|
-
return;
|
|
4403
|
-
}
|
|
4404
4553
|
this.#syncBadges();
|
|
4405
4554
|
if (keys.length === 0) {
|
|
4406
4555
|
return;
|
|
@@ -4427,10 +4576,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4427
4576
|
}
|
|
4428
4577
|
/**
|
|
4429
4578
|
* The selected keys, in selection order, for a multiple select; the one
|
|
4430
|
-
* key, or null, for a single one.
|
|
4579
|
+
* key, or null, for a single one. Before the render it answers the keys
|
|
4580
|
+
* last written, or the `value` attribute, coerced and cut to one key as the
|
|
4581
|
+
* rendered select would.
|
|
4431
4582
|
* @returns {any}
|
|
4432
4583
|
*/
|
|
4433
4584
|
get value() {
|
|
4585
|
+
if (!this.#control) {
|
|
4586
|
+
const held = this._heldValue();
|
|
4587
|
+
const keys = [held ?? []].flat().map((k) => this.#coerceKey(k));
|
|
4588
|
+
return this.multiple ? keys : (keys[0] ?? null);
|
|
4589
|
+
}
|
|
4434
4590
|
if (this.#multiple) {
|
|
4435
4591
|
return [...this.#values.keys()];
|
|
4436
4592
|
}
|
|
@@ -4449,20 +4605,60 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4449
4605
|
}
|
|
4450
4606
|
return selection[0] ?? null;
|
|
4451
4607
|
}
|
|
4608
|
+
/**
|
|
4609
|
+
* The field's disabled claim; turning it on while the dropdown is open
|
|
4610
|
+
* closes it and drops a pending search.
|
|
4611
|
+
* @type {boolean}
|
|
4612
|
+
*/
|
|
4613
|
+
get disabled() {
|
|
4614
|
+
return super.disabled;
|
|
4615
|
+
}
|
|
4616
|
+
set disabled(v) {
|
|
4617
|
+
super.disabled = v;
|
|
4618
|
+
if (v && this.#ddmenu?.shown) {
|
|
4619
|
+
this.#abortdload();
|
|
4620
|
+
this.#close();
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
/**
|
|
4624
|
+
* The field's readonly claim; turning it on while the dropdown is open
|
|
4625
|
+
* closes it and drops a pending search.
|
|
4626
|
+
* @type {boolean}
|
|
4627
|
+
*/
|
|
4628
|
+
get readonly() {
|
|
4629
|
+
return super.readonly;
|
|
4630
|
+
}
|
|
4631
|
+
set readonly(v) {
|
|
4632
|
+
super.readonly = v;
|
|
4633
|
+
if (v && this.#ddmenu?.shown) {
|
|
4634
|
+
this.#abortdload();
|
|
4635
|
+
this.#close();
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4452
4638
|
#useItemList;
|
|
4453
4639
|
/**
|
|
4454
4640
|
* Whether the select holds several keys: `value` and `entry` answer arrays,
|
|
4455
4641
|
* the input stays empty and the selection shows as removable badges in the
|
|
4456
|
-
* control.
|
|
4642
|
+
* control. Before the render it answers the `multiple` attribute.
|
|
4457
4643
|
* @returns {boolean}
|
|
4458
4644
|
*/
|
|
4459
4645
|
get multiple() {
|
|
4460
|
-
return this.#multiple;
|
|
4646
|
+
return this.#multiple ?? this.declared('multiple');
|
|
4461
4647
|
}
|
|
4462
|
-
/**
|
|
4648
|
+
/**
|
|
4649
|
+
* Turning it off keeps only the first selected key; either way the badges,
|
|
4650
|
+
* the items and the input follow at once.
|
|
4651
|
+
* @param {boolean} v
|
|
4652
|
+
*/
|
|
4463
4653
|
set multiple(v) {
|
|
4464
4654
|
this.#multiple = v;
|
|
4465
4655
|
this.reflectTo('multiple', v);
|
|
4656
|
+
if (!v) {
|
|
4657
|
+
this.#values = new Map([...this.#values].slice(0, 1));
|
|
4658
|
+
}
|
|
4659
|
+
if (this.#control) {
|
|
4660
|
+
this.#syncBadges();
|
|
4661
|
+
}
|
|
4466
4662
|
}
|
|
4467
4663
|
/**
|
|
4468
4664
|
* Whether the selection shows as the `items` list below the control instead
|
|
@@ -4484,10 +4680,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4484
4680
|
* a radio input carrying its attributes, labelled by its content; every
|
|
4485
4681
|
* `input-*` attribute on the group is forwarded onto each radio as well. The
|
|
4486
4682
|
* default slot is the `<legend>`, and `header` and `footer` slots frame the
|
|
4487
|
-
* list. The radios share a name of their own
|
|
4488
|
-
* when there is
|
|
4489
|
-
*
|
|
4490
|
-
* group's own.
|
|
4683
|
+
* list. The radios share a name of their own, minted afresh for each group
|
|
4684
|
+
* (from `name` when there is one) so that same-named groups stay apart, and
|
|
4685
|
+
* carry `form=""`, so the group is the only element the form sees. A radio's
|
|
4686
|
+
* native `change` is stopped and republished as the group's own.
|
|
4491
4687
|
*
|
|
4492
4688
|
* The host carries the `radiogroup` role, `aria-readonly`, `aria-required`,
|
|
4493
4689
|
* `aria-invalid` and the description. The disabled and readonly claims act on
|
|
@@ -4532,7 +4728,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4532
4728
|
* `announces` pieces
|
|
4533
4729
|
*/
|
|
4534
4730
|
_build({ slots }) {
|
|
4535
|
-
const name = this.declared('name') ??
|
|
4731
|
+
const name = index_mjs.Attributes.uid(this.declared('name') ?? 'ful-radiogroup');
|
|
4536
4732
|
const radioEls = Array.from(slots.default.querySelectorAll('ful-radio'));
|
|
4537
4733
|
const inputsAndLabels = radioEls.map((el) => {
|
|
4538
4734
|
const input = document.createElement('input');
|
|
@@ -4570,15 +4766,31 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4570
4766
|
* The value of the checked radio, `null` when none is. Under
|
|
4571
4767
|
* `type="boolean"` it answers `true` for a radio whose value is `true` and
|
|
4572
4768
|
* `false` for any other. Writing a value checks the radio carrying it, and
|
|
4573
|
-
* writing `null` or a value no radio carries clears the selection.
|
|
4769
|
+
* writing `null` or a value no radio carries clears the selection. Before
|
|
4770
|
+
* the render it answers the value last written, or the `value` attribute,
|
|
4771
|
+
* when a `ful-radio` carries it, and a write is held for the render to
|
|
4772
|
+
* apply.
|
|
4574
4773
|
* @type {string | boolean | null}
|
|
4575
4774
|
*/
|
|
4576
4775
|
get value() {
|
|
4776
|
+
if (!this.#fieldset) {
|
|
4777
|
+
const held = this._heldValue();
|
|
4778
|
+
const radio = held === null ? null : this.querySelector(`ful-radio[value="${CSS.escape(String(held))}"]`);
|
|
4779
|
+
const key = radio?.getAttribute('value') ?? null;
|
|
4780
|
+
if (key === null || this.declared('type') !== 'boolean') {
|
|
4781
|
+
return key;
|
|
4782
|
+
}
|
|
4783
|
+
return key === 'true';
|
|
4784
|
+
}
|
|
4577
4785
|
/** @type {HTMLInputElement|null} */
|
|
4578
4786
|
const checked = this.querySelector('input[type=radio]:checked');
|
|
4579
4787
|
return checked ? (this.#booleanType ? checked.value === 'true' : checked.value) : null;
|
|
4580
4788
|
}
|
|
4581
4789
|
set value(value) {
|
|
4790
|
+
if (!this.#fieldset) {
|
|
4791
|
+
this._holdValue(value);
|
|
4792
|
+
return;
|
|
4793
|
+
}
|
|
4582
4794
|
const radios = this.querySelectorAll(`input[type=radio]`);
|
|
4583
4795
|
const clear = () => {
|
|
4584
4796
|
radios.forEach((el) => {
|
|
@@ -4648,13 +4860,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4648
4860
|
}
|
|
4649
4861
|
/**
|
|
4650
4862
|
* Whether the box is checked. The `value` attribute is read with the `bool`
|
|
4651
|
-
* mapper, where only the text `true` means checked.
|
|
4863
|
+
* mapper, where only the text `true` means checked. Before the render it
|
|
4864
|
+
* answers the value last written, or the `value` attribute, and a write is
|
|
4865
|
+
* held for the render to apply.
|
|
4652
4866
|
* @type {boolean}
|
|
4653
4867
|
*/
|
|
4654
4868
|
get value() {
|
|
4655
|
-
return this.#input.checked;
|
|
4869
|
+
return this.#input ? this.#input.checked : Boolean(this._heldValue());
|
|
4656
4870
|
}
|
|
4657
4871
|
set value(value) {
|
|
4872
|
+
if (!this.#input) {
|
|
4873
|
+
this._holdValue(value);
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4658
4876
|
this.#input.checked = value;
|
|
4659
4877
|
}
|
|
4660
4878
|
}
|
|
@@ -5004,8 +5222,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5004
5222
|
*/
|
|
5005
5223
|
async load(pageRequest, sortRequest, filterRequest) {
|
|
5006
5224
|
const rows = this.#sorted(sortRequest);
|
|
5007
|
-
const
|
|
5008
|
-
const
|
|
5225
|
+
const size = Number(pageRequest.size);
|
|
5226
|
+
const begin = pageRequest.page * size;
|
|
5227
|
+
const end = begin + size;
|
|
5009
5228
|
const page = rows.slice(begin, end);
|
|
5010
5229
|
const totalElements = rows.length;
|
|
5011
5230
|
return {
|
|
@@ -5138,12 +5357,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5138
5357
|
* for it. Without it the table shows an initial panel until `load`,
|
|
5139
5358
|
* `reload` or `resetWithFilter` is called.
|
|
5140
5359
|
*
|
|
5141
|
-
* Inside the table a `
|
|
5360
|
+
* Inside the table, and not inside a `ful-table` nested in it, a
|
|
5361
|
+
* `page:requested` loads that page with the current size,
|
|
5142
5362
|
* sort and filters; a `sort:requested` loads the current page with the new
|
|
5143
5363
|
* sort (none for a `null` order) and, once that load is the one showing, sets
|
|
5144
5364
|
* the order of the sorter that asked and clears the others; a `submit:success`
|
|
5145
5365
|
* of the filter form loads the first page with the submitted request as the
|
|
5146
|
-
* filters. The
|
|
5366
|
+
* filters. The first load goes through the filter form too: `reload`, and so
|
|
5367
|
+
* `autoload`, submits it (see `reload`), so a `submit` listener rewriting
|
|
5368
|
+
* `detail.request` shapes every load alike. The filters a page, a sort or a size change carries are those of
|
|
5369
|
+
* the last load asked for, whether it is still running, loaded or failed, so
|
|
5370
|
+
* they are the ones the filter form shows. The loads these start, and the autoload, are not awaited by
|
|
5147
5371
|
* anything: a failure shows the error panel and its rejection is unhandled.
|
|
5148
5372
|
*/
|
|
5149
5373
|
class Table extends index_mjs.ParsedElement {
|
|
@@ -5230,6 +5454,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5230
5454
|
#latestRequest = { pageRequest: { page: 0, size: 10 }, sortRequest: null, filterRequest: {} };
|
|
5231
5455
|
/** whether a load has been asked for, by autoload or by a caller */
|
|
5232
5456
|
#loadRequested = false;
|
|
5457
|
+
/** the filters of the last load asked for, which page, sort and size changes carry */
|
|
5458
|
+
#requestedFilters = {};
|
|
5459
|
+
/** @type {any} the `ful-form` wrapping the `filters` slot, if any */
|
|
5460
|
+
#filters = null;
|
|
5461
|
+
/** whether a load has shown its rows, after which `reload` repeats it */
|
|
5462
|
+
#loaded = false;
|
|
5463
|
+
/** @type {Promise<void>|null} the load the last `submit:success` of the filter form started */
|
|
5464
|
+
#filterLoad = null;
|
|
5233
5465
|
#loads = new Claims();
|
|
5234
5466
|
/** @type {{ src: string|null, method: string|null }} */
|
|
5235
5467
|
#builtFrom = { src: null, method: null };
|
|
@@ -5251,19 +5483,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5251
5483
|
*
|
|
5252
5484
|
* The reload is not awaited: a failure shows the error panel and its
|
|
5253
5485
|
* rejection is unhandled.
|
|
5254
|
-
* @param {number|null|undefined} value
|
|
5255
|
-
*
|
|
5486
|
+
* @param {number|string|null|undefined} value a number or a string of
|
|
5487
|
+
* one, such as a select's value; absent or null is the default of ten,
|
|
5488
|
+
* so removing the attribute restores it, and a value that is not a
|
|
5489
|
+
* number is ignored
|
|
5256
5490
|
*/
|
|
5257
5491
|
set pageSize(value) {
|
|
5258
|
-
const size = value ?? 10;
|
|
5259
|
-
if (size === this.#latestRequest.pageRequest.size) {
|
|
5492
|
+
const size = Number(value ?? 10);
|
|
5493
|
+
if (Number.isNaN(size) || size === this.#latestRequest.pageRequest.size) {
|
|
5260
5494
|
return;
|
|
5261
5495
|
}
|
|
5262
5496
|
this.#latestRequest = { ...this.#latestRequest, pageRequest: { page: 0, size } };
|
|
5263
5497
|
if (!this.#loadRequested) {
|
|
5264
5498
|
return;
|
|
5265
5499
|
}
|
|
5266
|
-
this.
|
|
5500
|
+
this.load(this.#latestRequest.pageRequest, this.#latestRequest.sortRequest, this.#requestedFilters);
|
|
5267
5501
|
}
|
|
5268
5502
|
/**
|
|
5269
5503
|
* The url the loader asks for rows, reflected as the `src` attribute.
|
|
@@ -5383,15 +5617,24 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5383
5617
|
sortRequest: schema.sort,
|
|
5384
5618
|
filterRequest: maybeForm?.values ?? {},
|
|
5385
5619
|
};
|
|
5620
|
+
this.#requestedFilters = this.#latestRequest.filterRequest;
|
|
5621
|
+
this.#filters = maybeForm;
|
|
5386
5622
|
maybeForm?.addEventListener('submit:success', async (evt) => {
|
|
5387
|
-
|
|
5623
|
+
this.#filterLoad = this.#loadPage(0, evt.detail.request);
|
|
5624
|
+
await this.#filterLoad;
|
|
5388
5625
|
});
|
|
5389
5626
|
this.addEventListener('page:requested', async (/** @type any */ e) => {
|
|
5390
|
-
|
|
5627
|
+
if (e.target.closest('ful-table') !== this) {
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5630
|
+
await this.#loadPage(e.detail.value, this.#requestedFilters);
|
|
5391
5631
|
});
|
|
5392
5632
|
this.addEventListener('sort:requested', async (/** @type any */ e) => {
|
|
5633
|
+
if (e.target.closest('ful-table') !== this) {
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5393
5636
|
const sortRequest = e.detail.value.order ? e.detail.value : null;
|
|
5394
|
-
await this.load(this.#latestRequest.pageRequest, sortRequest, this.#
|
|
5637
|
+
await this.load(this.#latestRequest.pageRequest, sortRequest, this.#requestedFilters);
|
|
5395
5638
|
if (this.#latestRequest.sortRequest !== sortRequest) {
|
|
5396
5639
|
return;
|
|
5397
5640
|
}
|
|
@@ -5407,12 +5650,23 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5407
5650
|
|
|
5408
5651
|
/**
|
|
5409
5652
|
* Loads the last request that loaded, or, before any has, the first page
|
|
5410
|
-
* with the declared size and sort
|
|
5411
|
-
*
|
|
5412
|
-
*
|
|
5653
|
+
* with the declared size and sort. A size written through `pageSize` since
|
|
5654
|
+
* then replaces the page with the first one at that size.
|
|
5655
|
+
*
|
|
5656
|
+
* Before any load has loaded, a table with a `filters` slot submits its
|
|
5657
|
+
* filter form instead, so the first request goes through the same
|
|
5658
|
+
* `submit` listeners as the later ones, and the load its `submit:success`
|
|
5659
|
+
* starts is the one awaited. A filter submit that is cancelled, fails or
|
|
5660
|
+
* is dropped for one in flight loads nothing, and the promise resolves.
|
|
5413
5661
|
* @returns {Promise<void>} as `load`
|
|
5414
5662
|
*/
|
|
5415
5663
|
async reload() {
|
|
5664
|
+
if (this.#filters && !this.#loaded) {
|
|
5665
|
+
if (await this.#filters.submit()) {
|
|
5666
|
+
await this.#filterLoad;
|
|
5667
|
+
}
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5416
5670
|
return await this.load(
|
|
5417
5671
|
this.#latestRequest.pageRequest,
|
|
5418
5672
|
this.#latestRequest.sortRequest,
|
|
@@ -5420,7 +5674,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5420
5674
|
);
|
|
5421
5675
|
}
|
|
5422
5676
|
/**
|
|
5423
|
-
* Asks the loader for a page and shows it.
|
|
5677
|
+
* Asks the loader for a page and shows it. Its filters are, from now on,
|
|
5678
|
+
* the ones a page, a sort or a size change carries, even while it runs
|
|
5679
|
+
* or once it failed.
|
|
5424
5680
|
*
|
|
5425
5681
|
* While the load runs the table carries `aria-busy="true"`, and the rows
|
|
5426
5682
|
* already shown stay; the spinner panel shows only when there are none.
|
|
@@ -5428,7 +5684,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5428
5684
|
* panel shows for an answer with no rows (the `empty` slot rendered with
|
|
5429
5685
|
* `schema`, `pageRequest`, `filterRequest` and `pageResponse` in scope),
|
|
5430
5686
|
* the request becomes the one
|
|
5431
|
-
* `reload` repeats
|
|
5687
|
+
* `reload` repeats; once the elements of the rows and of the empty panel
|
|
5688
|
+
* have rendered, `aria-busy` is removed and `load:success` is
|
|
5432
5689
|
* dispatched (bubbling, not cancelable) with `detail` set to
|
|
5433
5690
|
* `{ pageRequest, sortRequest, filterRequest, response }`. A page beyond
|
|
5434
5691
|
* the last one the answer reports is not shown: the last page is loaded
|
|
@@ -5442,7 +5699,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5442
5699
|
* returned promise rejects with the same error.
|
|
5443
5700
|
*
|
|
5444
5701
|
* A load started after this one supersedes it: when this one's answer or
|
|
5445
|
-
* failure arrives it changes nothing, dispatches nothing and resolves
|
|
5702
|
+
* failure arrives it changes nothing, dispatches nothing and resolves;
|
|
5703
|
+
* superseded while its rows render, it leaves them to the newer load and
|
|
5704
|
+
* dispatches nothing.
|
|
5446
5705
|
* @param {TablePageRequest} pageRequest
|
|
5447
5706
|
* @param {TableSortRequest|null} sortRequest
|
|
5448
5707
|
* @param {Record<string, any>} filterRequest
|
|
@@ -5451,6 +5710,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5451
5710
|
*/
|
|
5452
5711
|
async load(pageRequest, sortRequest, filterRequest) {
|
|
5453
5712
|
this.#loadRequested = true;
|
|
5713
|
+
this.#requestedFilters = filterRequest;
|
|
5454
5714
|
const claim = this.#loads.take();
|
|
5455
5715
|
this.#loading.toggleAttribute('hidden', this.#body.childElementCount > 0);
|
|
5456
5716
|
this.#feedback.setAttribute('hidden', '');
|
|
@@ -5466,7 +5726,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
5466
5726
|
lastPage = Math.max(0, Math.ceil(pageResponse.size / pageRequest.size) - 1);
|
|
5467
5727
|
if (pageRequest.page <= lastPage) {
|
|
5468
5728
|
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
5729
|
+
this.#loaded = true;
|
|
5469
5730
|
this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
|
|
5731
|
+
// a row element whose render fails leaves the rows shown: it does not fail the load
|
|
5732
|
+
await Promise.all([
|
|
5733
|
+
index_mjs.Rendering.waitForChildren(this.#body),
|
|
5734
|
+
index_mjs.Rendering.waitForChildren(this.#empty),
|
|
5735
|
+
]).catch(() => undefined);
|
|
5736
|
+
if (claim.stale) {
|
|
5737
|
+
return;
|
|
5738
|
+
}
|
|
5470
5739
|
this.removeAttribute('aria-busy');
|
|
5471
5740
|
this.dispatchEvent(
|
|
5472
5741
|
new CustomEvent('load:success', {
|
|
@@ -6066,7 +6335,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6066
6335
|
* The operators the menu offers, narrowed to the vocabulary: an empty list,
|
|
6067
6336
|
* or one where no name is known, offers all of it. A single operator pins
|
|
6068
6337
|
* the button, and an assigned tuple then carries that operator whatever it
|
|
6069
|
-
* names.
|
|
6338
|
+
* names. When the operator shown leaves the list, the first listed one is
|
|
6339
|
+
* shown. An assignment made before the upgrade is held narrowed and
|
|
6070
6340
|
* answered back until the render, when the `operators` attribute is applied
|
|
6071
6341
|
* over it, an absent attribute meaning the whole vocabulary. Assignments are
|
|
6072
6342
|
* ignored while the `operator` attribute fixes the operator.
|
|
@@ -6085,27 +6355,61 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6085
6355
|
return;
|
|
6086
6356
|
}
|
|
6087
6357
|
this._operator.allowed = declared;
|
|
6088
|
-
this.
|
|
6358
|
+
if (this._operator.value !== null) {
|
|
6359
|
+
this._showOperator(this._operator.preferring(this._operator.value));
|
|
6360
|
+
}
|
|
6089
6361
|
}
|
|
6090
6362
|
/**
|
|
6091
6363
|
* The filter's tuple, `[operator, operand]` or `[operator, from, to]` for
|
|
6092
6364
|
* BETWEEN, each operand converted by `_serialize` (an ISO instant for the
|
|
6093
6365
|
* instant filter). Null while any operand the operator uses is empty.
|
|
6366
|
+
* Before the render it answers the tuple last written, or the `value`
|
|
6367
|
+
* attribute, as the rendered filter would show it.
|
|
6094
6368
|
* @returns {any[]|null}
|
|
6095
6369
|
*/
|
|
6096
6370
|
get value() {
|
|
6371
|
+
if (!this._operator) {
|
|
6372
|
+
return this._shownTuple(this._heldValue());
|
|
6373
|
+
}
|
|
6097
6374
|
return this._tuple();
|
|
6098
6375
|
}
|
|
6099
6376
|
/**
|
|
6100
|
-
* Shows a tuple: the operator it names,
|
|
6101
|
-
*
|
|
6377
|
+
* Shows a tuple: the operator it names, or the first allowed one where the
|
|
6378
|
+
* whitelist, a pin or a fixed operator does not allow it, and its operands, an operand missing from a shorter tuple
|
|
6102
6379
|
* leaving its input empty. Null or undefined empties both operands and
|
|
6103
|
-
* keeps the operator.
|
|
6380
|
+
* keeps the operator. Before the render the tuple is held for the render
|
|
6381
|
+
* to apply, over the `value` attribute, which it leaves alone.
|
|
6104
6382
|
* @param {any[]|null|undefined} v
|
|
6105
6383
|
*/
|
|
6106
6384
|
set value(v) {
|
|
6385
|
+
if (!this._operator) {
|
|
6386
|
+
this._holdValue(v);
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6107
6389
|
this._applyTuple(v);
|
|
6108
6390
|
}
|
|
6391
|
+
/**
|
|
6392
|
+
* The tuple `_tuple` answers once `_applyTuple(v)` has shown `v`, read
|
|
6393
|
+
* from the declared `operator` and `operators` in place of the menu.
|
|
6394
|
+
* @param {any[]|null|undefined} v
|
|
6395
|
+
* @returns {any[]|null}
|
|
6396
|
+
*/
|
|
6397
|
+
_shownTuple(v) {
|
|
6398
|
+
if (v == null) {
|
|
6399
|
+
return null;
|
|
6400
|
+
}
|
|
6401
|
+
const [declared, ...values] = v;
|
|
6402
|
+
const fixed = this.declared('operator');
|
|
6403
|
+
const known = this._vocabulary().includes(fixed);
|
|
6404
|
+
const allowed =
|
|
6405
|
+
fixed === null
|
|
6406
|
+
? ChoiceButton.narrow(this.declared('operators'), this._vocabulary())
|
|
6407
|
+
: [known ? fixed : this._defaultOperator()];
|
|
6408
|
+
const operator = allowed.includes(declared) ? declared : allowed[0];
|
|
6409
|
+
const shown = [values[0], values[1]].map((o) => `${o ? this._deserialize(o) : (o ?? '')}`);
|
|
6410
|
+
const operands = operator === 'BETWEEN' ? shown : shown.slice(0, 1);
|
|
6411
|
+
return operands.some((o) => o === '') ? null : [operator, ...operands.map((o) => this._serialize(o))];
|
|
6412
|
+
}
|
|
6109
6413
|
_tuple() {
|
|
6110
6414
|
const operator = this._operator.value;
|
|
6111
6415
|
const values = this.#operands();
|
|
@@ -6118,8 +6422,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6118
6422
|
return;
|
|
6119
6423
|
}
|
|
6120
6424
|
const [declared, ...values] = v;
|
|
6121
|
-
|
|
6122
|
-
this._showOperator(operator);
|
|
6425
|
+
this._showOperator(this._operator.preferring(declared));
|
|
6123
6426
|
this._value1.value = values[0] ? this._deserialize(values[0]) : (values[0] ?? '');
|
|
6124
6427
|
this._value2.value = values[1] ? this._deserialize(values[1]) : (values[1] ?? '');
|
|
6125
6428
|
}
|
|
@@ -6136,10 +6439,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6136
6439
|
* them rather than as serialized: a date filter describes what is in its
|
|
6137
6440
|
* control, not the ISO string it sends. Null while every one of those
|
|
6138
6441
|
* operands is empty, so a BETWEEN with one bound answers that bound while
|
|
6139
|
-
* its value is still null.
|
|
6442
|
+
* its value is still null, and before the filter has rendered.
|
|
6140
6443
|
* @returns {FilterCriterion|null}
|
|
6141
6444
|
*/
|
|
6142
6445
|
get criterion() {
|
|
6446
|
+
if (!this._operator) {
|
|
6447
|
+
return null;
|
|
6448
|
+
}
|
|
6143
6449
|
return asCriterion(labelTextOf(this), this._operator.value, this.#operands());
|
|
6144
6450
|
}
|
|
6145
6451
|
#operands() {
|
|
@@ -6299,10 +6605,26 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6299
6605
|
}
|
|
6300
6606
|
/**
|
|
6301
6607
|
* The tuple with the sensitivity after the operator, `[operator,
|
|
6302
|
-
* sensitivity, ...operands]`. Null while any operand the operator uses is
|
|
6608
|
+
* sensitivity, ...operands]`. Null while any operand the operator uses is
|
|
6609
|
+
* empty. Before the render it answers the tuple last written, or the
|
|
6610
|
+
* `value` attribute, as the rendered filter would show it.
|
|
6303
6611
|
* @returns {any[]|null}
|
|
6304
6612
|
*/
|
|
6305
6613
|
get value() {
|
|
6614
|
+
if (!this._sensitivityButton) {
|
|
6615
|
+
const held = this._heldValue();
|
|
6616
|
+
const tuple = held == null ? null : this._shownTuple([held[0], ...held.slice(2)]);
|
|
6617
|
+
if (tuple === null) {
|
|
6618
|
+
return null;
|
|
6619
|
+
}
|
|
6620
|
+
const fixed = this.declared('sensitivity');
|
|
6621
|
+
const allowed =
|
|
6622
|
+
fixed === null
|
|
6623
|
+
? ChoiceButton.narrow(this.declared('sensitivities'), SENSITIVITIES)
|
|
6624
|
+
: [SENSITIVITIES.includes(fixed) ? fixed : 'IGNORE_CASE'];
|
|
6625
|
+
const kept = allowed.includes(held[1]) ? held[1] : 'IGNORE_CASE';
|
|
6626
|
+
return [tuple[0], allowed.includes(kept) ? kept : allowed[0], ...tuple.slice(1)];
|
|
6627
|
+
}
|
|
6306
6628
|
const tuple = this._tuple();
|
|
6307
6629
|
return tuple == null ? null : [tuple[0], this._sensitivity, ...tuple.slice(1)];
|
|
6308
6630
|
}
|
|
@@ -6310,10 +6632,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6310
6632
|
* Shows a tuple as the compare filter does, reading the sensitivity at
|
|
6311
6633
|
* index 1. A sensitivity the whitelist does not allow leaves the current
|
|
6312
6634
|
* one in place. Null or undefined empties the operands and keeps the
|
|
6313
|
-
* operator and the sensitivity.
|
|
6635
|
+
* operator and the sensitivity. Before the render the tuple is held, as
|
|
6636
|
+
* on the compare filters.
|
|
6314
6637
|
* @param {any[]|null|undefined} v
|
|
6315
6638
|
*/
|
|
6316
6639
|
set value(v) {
|
|
6640
|
+
if (!this._sensitivityButton) {
|
|
6641
|
+
this._holdValue(v);
|
|
6642
|
+
return;
|
|
6643
|
+
}
|
|
6317
6644
|
if (v == null) {
|
|
6318
6645
|
this._applyTuple(v);
|
|
6319
6646
|
return;
|
|
@@ -6418,7 +6745,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6418
6745
|
/**
|
|
6419
6746
|
* The operators the menu offers, narrowed to EQ and NEQ: an empty list, or
|
|
6420
6747
|
* one where no name is known, offers both, and a single one pins the
|
|
6421
|
-
* button.
|
|
6748
|
+
* button. When the operator shown leaves the list, the first listed one is
|
|
6749
|
+
* shown. Held before the upgrade and replaced by the `operators` attribute
|
|
6422
6750
|
* at the render, as on the compare filters.
|
|
6423
6751
|
* @returns {string[]|undefined} undefined before the upgrade when nothing was assigned
|
|
6424
6752
|
*/
|
|
@@ -6432,30 +6760,49 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6432
6760
|
return;
|
|
6433
6761
|
}
|
|
6434
6762
|
this._operator.allowed = declared;
|
|
6763
|
+
if (this._operator.value !== null) {
|
|
6764
|
+
this._operator.value = this._operator.preferring(this._operator.value);
|
|
6765
|
+
}
|
|
6435
6766
|
}
|
|
6436
6767
|
/** @returns {string[]} */
|
|
6437
6768
|
_vocabulary() {
|
|
6438
6769
|
return BooleanFilter.OPERATORS;
|
|
6439
6770
|
}
|
|
6440
6771
|
/**
|
|
6441
|
-
* `[operator, token]`, the token being 'true' or 'false'; null while the
|
|
6772
|
+
* `[operator, token]`, the token being 'true' or 'false'; null while the
|
|
6773
|
+
* value menu is on any. Before the render it answers the tuple last
|
|
6774
|
+
* written, or the `value` attribute, as the rendered filter would show it.
|
|
6442
6775
|
* @returns {string[]|null}
|
|
6443
6776
|
*/
|
|
6444
6777
|
get value() {
|
|
6778
|
+
if (!this._value) {
|
|
6779
|
+
const held = this._heldValue();
|
|
6780
|
+
const token = `${held?.[1] ?? ''}`;
|
|
6781
|
+
if (token === '') {
|
|
6782
|
+
return null;
|
|
6783
|
+
}
|
|
6784
|
+
const allowed = ChoiceButton.narrow(this.declared('operators'), this._vocabulary());
|
|
6785
|
+
return [allowed.includes(held[0]) ? held[0] : allowed[0], token];
|
|
6786
|
+
}
|
|
6445
6787
|
return this._value.value === '' ? null : [this._operator.value, this._value.value];
|
|
6446
6788
|
}
|
|
6447
6789
|
/**
|
|
6448
|
-
* Shows a tuple: its operator,
|
|
6449
|
-
* token, a missing token meaning any. Null or undefined sets the value menu
|
|
6450
|
-
* back to any and keeps the operator.
|
|
6790
|
+
* Shows a tuple: its operator, or the first allowed one where the whitelist
|
|
6791
|
+
* does not allow it, and its token, a missing token meaning any. Null or undefined sets the value menu
|
|
6792
|
+
* back to any and keeps the operator. Before the render the tuple is held,
|
|
6793
|
+
* as on the compare filters.
|
|
6451
6794
|
* @param {any[]|null|undefined} v
|
|
6452
6795
|
*/
|
|
6453
6796
|
set value(v) {
|
|
6797
|
+
if (!this._value) {
|
|
6798
|
+
this._holdValue(v);
|
|
6799
|
+
return;
|
|
6800
|
+
}
|
|
6454
6801
|
if (v == null) {
|
|
6455
6802
|
this._value.value = '';
|
|
6456
6803
|
return;
|
|
6457
6804
|
}
|
|
6458
|
-
this._operator.value = this._operator.
|
|
6805
|
+
this._operator.value = this._operator.preferring(v[0]);
|
|
6459
6806
|
this._value.value = v[1] ?? '';
|
|
6460
6807
|
}
|
|
6461
6808
|
/**
|
|
@@ -6470,10 +6817,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6470
6817
|
}
|
|
6471
6818
|
/**
|
|
6472
6819
|
* The operator and the localized word the value menu shows, not the token.
|
|
6473
|
-
* Null while the value menu is on any.
|
|
6820
|
+
* Null while the value menu is on any, and before the filter has rendered.
|
|
6474
6821
|
* @returns {FilterCriterion|null}
|
|
6475
6822
|
*/
|
|
6476
6823
|
get criterion() {
|
|
6824
|
+
if (!this._value) {
|
|
6825
|
+
return null;
|
|
6826
|
+
}
|
|
6477
6827
|
const token = this._value.value;
|
|
6478
6828
|
return asCriterion(labelTextOf(this), this._operator.value, [token === '' ? '' : booleanValueLabel(token)]);
|
|
6479
6829
|
}
|
|
@@ -6515,7 +6865,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6515
6865
|
* The chosen keys as an array, a single choice included, led by the
|
|
6516
6866
|
* declared `operator` when there is one. Null while nothing is chosen, so
|
|
6517
6867
|
* that a table drops the filter instead of sending an empty set that
|
|
6518
|
-
* matches nothing.
|
|
6868
|
+
* matches nothing. Before the render it answers the keys the select answers
|
|
6869
|
+
* then.
|
|
6519
6870
|
* @returns {any[]|null}
|
|
6520
6871
|
*/
|
|
6521
6872
|
get value() {
|
|
@@ -6530,7 +6881,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6530
6881
|
* Selects the keys of an array or a single bare key; a leading element equal
|
|
6531
6882
|
* to the declared `operator` is dropped first, so the filter takes back what
|
|
6532
6883
|
* it answered. A single select takes the first key. Null or undefined clears
|
|
6533
|
-
* the selection.
|
|
6884
|
+
* the selection. Before the render the keys are held, as the select holds
|
|
6885
|
+
* them.
|
|
6534
6886
|
* @param {any} v
|
|
6535
6887
|
*/
|
|
6536
6888
|
set value(v) {
|
|
@@ -6543,7 +6895,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6543
6895
|
}
|
|
6544
6896
|
/**
|
|
6545
6897
|
* The declared operator, or null, and the labels of the chosen entries. Null
|
|
6546
|
-
* while nothing is chosen.
|
|
6898
|
+
* while nothing is chosen, as before the filter has rendered.
|
|
6547
6899
|
* @returns {FilterCriterion|null}
|
|
6548
6900
|
*/
|
|
6549
6901
|
get criterion() {
|
|
@@ -6839,7 +7191,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6839
7191
|
*/
|
|
6840
7192
|
onSubmitted(answer) {
|
|
6841
7193
|
this.#section.addEventListener('submit:success', (/** @type any */ e) => {
|
|
6842
|
-
if (e.target !==
|
|
7194
|
+
if (e.target.localName !== 'ful-form' || e.target.parentElement !== this.#section) {
|
|
6843
7195
|
return;
|
|
6844
7196
|
}
|
|
6845
7197
|
answer(e.detail.response);
|
|
@@ -7002,10 +7354,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7002
7354
|
<section data-ref="loading" hidden><ful-spinner class="centered" role="status"><span class="ful-sr-only">{{ #l10n:t('spinner.loading') }}</span></ful-spinner></section>
|
|
7003
7355
|
<section data-ref="error" role="alert" hidden></section>
|
|
7004
7356
|
<div data-ref="body" class="ful-dialog-body">{{{{ slots.default }}}}</div>
|
|
7005
|
-
<footer class="ful-dialog-footer">
|
|
7006
|
-
<button type="button" data-ref="acknowledge" data-result="acknowledged" data-tpl-if="!slots.buttons && !closeOnSubmit" data-tpl-aria-label="#l10n:t('dialog.acknowledge')">{{ #l10n:t('dialog.acknowledge') }}</button>
|
|
7007
|
-
{{{{ slots.buttons }}}}
|
|
7008
|
-
</footer>
|
|
7357
|
+
<footer class="ful-dialog-footer"><button type="button" data-ref="acknowledge" data-result="acknowledged" data-tpl-if="!slots.buttons && !closeOnSubmit" data-tpl-aria-label="#l10n:t('dialog.acknowledge')">{{ #l10n:t('dialog.acknowledge') }}</button>{{{{ slots.buttons }}}}</footer>
|
|
7009
7358
|
</dialog>
|
|
7010
7359
|
`;
|
|
7011
7360
|
#dialog;
|
|
@@ -7035,9 +7384,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7035
7384
|
this.#settle(outcome);
|
|
7036
7385
|
});
|
|
7037
7386
|
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
7038
|
-
const
|
|
7039
|
-
if (
|
|
7040
|
-
this.#dialog.close(result);
|
|
7387
|
+
const button = e.target.closest('button[data-result]');
|
|
7388
|
+
if (button && button.closest('dialog') === this.#dialog) {
|
|
7389
|
+
this.#dialog.close(button.dataset.result);
|
|
7041
7390
|
}
|
|
7042
7391
|
});
|
|
7043
7392
|
if (!requiresAnswer) {
|
|
@@ -7108,7 +7457,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7108
7457
|
* the body and `first` true only on the first answered request), so a
|
|
7109
7458
|
* listener can fill the body. A failed request is painted into the body
|
|
7110
7459
|
* and does not reject. On a dialog already open nothing is shown or
|
|
7111
|
-
* requested, and the call waits for the same ending as the calls before it
|
|
7460
|
+
* requested, and the call waits for the same ending as the calls before it;
|
|
7461
|
+
* one the page moved while open, which is no longer modal, opens anew.
|
|
7112
7462
|
* @returns {Promise<DialogOutcome>} resolving on close, or on removal from
|
|
7113
7463
|
* the document with a dismissal
|
|
7114
7464
|
*/
|
|
@@ -7190,6 +7540,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7190
7540
|
}
|
|
7191
7541
|
/** @returns {boolean} whether this call is the one that opened the modal */
|
|
7192
7542
|
#show() {
|
|
7543
|
+
//a move in the page takes an open modal out of the top layer; close() would queue a close event answering the next opening
|
|
7544
|
+
if (this.#dialog.open && !this.#dialog.matches(':modal')) {
|
|
7545
|
+
this.#dialog.removeAttribute('open');
|
|
7546
|
+
}
|
|
7193
7547
|
if (this.#dialog.open) {
|
|
7194
7548
|
return false;
|
|
7195
7549
|
}
|
|
@@ -7387,9 +7741,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7387
7741
|
this.dismiss('escape');
|
|
7388
7742
|
});
|
|
7389
7743
|
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
7390
|
-
const
|
|
7391
|
-
if (
|
|
7392
|
-
this.close(result);
|
|
7744
|
+
const button = e.target.closest('button[data-result]');
|
|
7745
|
+
if (button && button.closest('dialog') === this.#dialog) {
|
|
7746
|
+
this.close(button.dataset.result);
|
|
7393
7747
|
}
|
|
7394
7748
|
});
|
|
7395
7749
|
this.#dialog.addEventListener('close', () => {
|
|
@@ -7486,9 +7840,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7486
7840
|
* the content section and `first` true only on the first answered request),
|
|
7487
7841
|
* so a listener can fill the content. A failed request is painted into the
|
|
7488
7842
|
* content section and does not reject. On a drawer already open nothing is
|
|
7489
|
-
* shown or requested, except that one sliding out stops and stays open,
|
|
7490
|
-
* the
|
|
7491
|
-
*
|
|
7843
|
+
* shown or requested, except that one sliding out stops and stays open,
|
|
7844
|
+
* forgetting the result it was closing with, and the call waits for the
|
|
7845
|
+
* same ending as the calls before it, which is how a caller learns the
|
|
7846
|
+
* outcome of the opening an `update()` made. One the page moved while open,
|
|
7847
|
+
* which is no longer modal, opens anew.
|
|
7492
7848
|
* @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>} resolving on
|
|
7493
7849
|
* close, or on removal from the document with a dismissal
|
|
7494
7850
|
*/
|
|
@@ -7569,7 +7925,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7569
7925
|
}
|
|
7570
7926
|
/** @returns {boolean} whether this call is the one that opened the modal */
|
|
7571
7927
|
#show() {
|
|
7928
|
+
if (this.#closing) {
|
|
7929
|
+
this.#answer = null;
|
|
7930
|
+
}
|
|
7572
7931
|
this.#stopSlidingOut();
|
|
7932
|
+
//a move in the page takes an open modal out of the top layer; close() would queue a close event answering the next opening
|
|
7933
|
+
if (this.#dialog.open && !this.#dialog.matches(':modal')) {
|
|
7934
|
+
this.#dialog.removeAttribute('open');
|
|
7935
|
+
}
|
|
7573
7936
|
if (this.#dialog.open) {
|
|
7574
7937
|
return false;
|
|
7575
7938
|
}
|
|
@@ -7608,6 +7971,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7608
7971
|
#invoker = null;
|
|
7609
7972
|
/** @type {HTMLElement|null} */
|
|
7610
7973
|
#pressed = null;
|
|
7974
|
+
#notePress = (/** @type PointerEvent */ evt) => {
|
|
7975
|
+
const target = /** @type Node */ (evt.target);
|
|
7976
|
+
this.#pressed = this.matches(':popover-open') && this.#invoker?.contains(target) ? this.#invoker : null;
|
|
7977
|
+
};
|
|
7978
|
+
/** Upgrades the menu, and has a shared one that came back follow the presses on its invoker again. */
|
|
7979
|
+
connectedCallback() {
|
|
7980
|
+
super.connectedCallback();
|
|
7981
|
+
if (this.rendered && !this.declared('for')) {
|
|
7982
|
+
document.addEventListener('pointerdown', this.#notePress, true);
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
/** Stops a shared menu following the presses while it is out of the document. */
|
|
7986
|
+
disconnectedCallback() {
|
|
7987
|
+
document.removeEventListener('pointerdown', this.#notePress, true);
|
|
7988
|
+
}
|
|
7611
7989
|
/**
|
|
7612
7990
|
* Gives the menu the `menu` role and an empty `popover` attribute unless
|
|
7613
7991
|
* it already has one, and marks the items. With `for`, read here once, it
|
|
@@ -7693,14 +8071,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7693
8071
|
return this.#mark().filter((item) => !item.hasAttribute('disabled') && !item.hasAttribute('hidden'));
|
|
7694
8072
|
}
|
|
7695
8073
|
#wireShared() {
|
|
7696
|
-
document.addEventListener(
|
|
7697
|
-
'pointerdown',
|
|
7698
|
-
(evt) => {
|
|
7699
|
-
const target = /** @type Node */ (evt.target);
|
|
7700
|
-
this.#pressed = this.matches(':popover-open') && this.#invoker?.contains(target) ? this.#invoker : null;
|
|
7701
|
-
},
|
|
7702
|
-
true,
|
|
7703
|
-
);
|
|
8074
|
+
document.addEventListener('pointerdown', this.#notePress, true);
|
|
7704
8075
|
this.addEventListener('toggle', (/** @type any */ evt) => {
|
|
7705
8076
|
if (evt.newState === 'closed') {
|
|
7706
8077
|
this.#invoker?.setAttribute('aria-expanded', 'false');
|
|
@@ -8028,17 +8399,21 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8028
8399
|
#showScrollButtons() {
|
|
8029
8400
|
const list = this.#tablist;
|
|
8030
8401
|
const overflow = list.scrollWidth - list.clientWidth;
|
|
8031
|
-
|
|
8032
|
-
this.#
|
|
8402
|
+
const scrolled = Math.abs(list.scrollLeft);
|
|
8403
|
+
this.#scrollStart.hidden = scrolled <= 1;
|
|
8404
|
+
this.#scrollEnd.hidden = scrolled >= overflow - 1;
|
|
8033
8405
|
this.#scrollStart.style.blockSize = this.#scrollEnd.style.blockSize = `${list.clientHeight}px`;
|
|
8034
8406
|
}
|
|
8035
8407
|
/**
|
|
8036
|
-
* The part of the tablist that no scroll button covers
|
|
8037
|
-
*
|
|
8408
|
+
* The part of the tablist that no scroll button covers, in viewport
|
|
8409
|
+
* coordinates, the start button being on the right under rtl.
|
|
8410
|
+
* @returns {{ left: number, right: number }}
|
|
8038
8411
|
*/
|
|
8039
8412
|
#visibleRange() {
|
|
8040
8413
|
const view = this.#tablist.getBoundingClientRect();
|
|
8041
|
-
|
|
8414
|
+
const rtl = getComputedStyle(this.#tablist).direction === 'rtl';
|
|
8415
|
+
const [onLeft, onRight] = rtl ? [this.#scrollEnd, this.#scrollStart] : [this.#scrollStart, this.#scrollEnd];
|
|
8416
|
+
return { left: view.left + onLeft.offsetWidth, right: view.right - onRight.offsetWidth };
|
|
8042
8417
|
}
|
|
8043
8418
|
/**
|
|
8044
8419
|
* Scrolls the tablist so that the first tab cut by the side the direction
|
|
@@ -8047,11 +8422,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8047
8422
|
* @returns {void}
|
|
8048
8423
|
*/
|
|
8049
8424
|
#scrollByOneTab(direction) {
|
|
8050
|
-
const {
|
|
8051
|
-
const
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8425
|
+
const { left, right } = this.#visibleRange();
|
|
8426
|
+
const rightwards = direction > 0 !== (getComputedStyle(this.#tablist).direction === 'rtl');
|
|
8427
|
+
const cut = rightwards
|
|
8428
|
+
? (/** @type HTMLElement */ tab) => tab.getBoundingClientRect().right > right + 1
|
|
8429
|
+
: (/** @type HTMLElement */ tab) => tab.getBoundingClientRect().left < left - 1;
|
|
8430
|
+
const target = direction > 0 ? this.#tabs.find(cut) : this.#tabs.findLast(cut);
|
|
8055
8431
|
this.#bringIntoView(target, matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth');
|
|
8056
8432
|
}
|
|
8057
8433
|
/**
|
|
@@ -8065,11 +8441,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8065
8441
|
if (!tab) {
|
|
8066
8442
|
return;
|
|
8067
8443
|
}
|
|
8068
|
-
const {
|
|
8444
|
+
const { left, right } = this.#visibleRange();
|
|
8069
8445
|
const box = tab.getBoundingClientRect();
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8446
|
+
if (box.right > right) {
|
|
8447
|
+
this.#tablist.scrollBy({ left: Math.ceil(box.right - right), behavior });
|
|
8448
|
+
} else if (box.left < left) {
|
|
8449
|
+
this.#tablist.scrollBy({ left: Math.floor(box.left - left), behavior });
|
|
8073
8450
|
}
|
|
8074
8451
|
}
|
|
8075
8452
|
/**
|
|
@@ -8296,14 +8673,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8296
8673
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
8297
8674
|
* the answers once they are delivered, or to undefined when nobody
|
|
8298
8675
|
* listened; undefined when the section is already current, and undefined
|
|
8299
|
-
* with a warning when no section carries `ref`
|
|
8676
|
+
* with a warning when no section carries `ref` or the one carrying it has
|
|
8677
|
+
* no step paired with it
|
|
8300
8678
|
* @throws what the entered section's delivery rejected with, as the rejection
|
|
8301
8679
|
* of the returned promise, after its error is painted in the section
|
|
8302
8680
|
*/
|
|
8303
8681
|
move(ref) {
|
|
8304
8682
|
const index = this.#sections.findIndex((s) => s.getAttribute('data-step') === ref);
|
|
8305
|
-
if (index === -1) {
|
|
8306
|
-
console.warn(`ful-wizard: no section carries data-step="${ref}"`);
|
|
8683
|
+
if (index === -1 || index >= this.#steps.length) {
|
|
8684
|
+
console.warn(`ful-wizard: no section paired with a step carries data-step="${ref}"`);
|
|
8307
8685
|
return undefined;
|
|
8308
8686
|
}
|
|
8309
8687
|
return this.#move(index);
|
|
@@ -8316,17 +8694,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8316
8694
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
8317
8695
|
* the answers, or to undefined when nobody listened or the delivery failed
|
|
8318
8696
|
* (its error painted in the section); undefined, with a warning, when `ref`
|
|
8319
|
-
* names no section
|
|
8697
|
+
* names no section or one with no step paired with it
|
|
8320
8698
|
*/
|
|
8321
8699
|
refresh(ref) {
|
|
8322
|
-
const
|
|
8700
|
+
const index =
|
|
8323
8701
|
ref instanceof Element
|
|
8324
|
-
? ref
|
|
8325
|
-
:
|
|
8326
|
-
|
|
8327
|
-
: undefined;
|
|
8328
|
-
const index = this.#sections.indexOf(section);
|
|
8329
|
-
if (index === -1) {
|
|
8702
|
+
? this.#sections.indexOf(ref)
|
|
8703
|
+
: this.#sections.findIndex((s) => s.getAttribute('data-step') === ref);
|
|
8704
|
+
if (index === -1 || index >= this.#steps.length) {
|
|
8330
8705
|
console.warn(`ful-wizard: no section answers to "${ref}"`);
|
|
8331
8706
|
return undefined;
|
|
8332
8707
|
}
|