@optionfactory/fml 9.0.0-rc16 → 9.0.0-rc18
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/custom-elements.json +17 -3
- package/dist/fml.css +9 -9
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +138 -38
- 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/ful.css +9 -9
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +43 -13
- package/dist/ful.iife.js +138 -38
- 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 +138 -38
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +12 -4
- package/package.json +1 -1
package/dist/ful.mjs
CHANGED
|
@@ -1578,6 +1578,8 @@ const inheritedAutocomplete = (el) => el.closest('form')?.getAttribute('autocomp
|
|
|
1578
1578
|
|
|
1579
1579
|
const INPUT_MODES = { numeric: 'numeric', decimal: 'decimal' };
|
|
1580
1580
|
|
|
1581
|
+
const SEGMENTED = new Set(['date', 'time', 'datetime-local', 'month', 'week']);
|
|
1582
|
+
|
|
1581
1583
|
const signed = (v, digits) => (v.startsWith('-') ? '-' : '') + digits(v);
|
|
1582
1584
|
|
|
1583
1585
|
const NUMERIC_DIGITS = {
|
|
@@ -1639,14 +1641,18 @@ const filterOf = (el) => {
|
|
|
1639
1641
|
* `autocomplete` attribute of the nearest enclosing `<form>`, which the
|
|
1640
1642
|
* platform does not pass on because the control carries `form=""`.
|
|
1641
1643
|
* - `uppercase`, `trim` and `v-type` shape the value as it is read; see `value`.
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
+
* - The control's native `change` is stopped and the field dispatches its own
|
|
1645
|
+
* once the user commits a new value, as a text input does. A date, time,
|
|
1646
|
+
* datetime-local, month or week control fires its native `change` while its
|
|
1647
|
+
* segments are typed, as soon as they make a valid value; for those the field
|
|
1648
|
+
* reports when the control is left or Enter is pressed, at once for a value
|
|
1649
|
+
* picked from the browser's picker, and never a half-typed value.
|
|
1644
1650
|
*
|
|
1645
1651
|
* A subclass template may render more than one control, as the compare filters
|
|
1646
1652
|
* render the two bounds of a range: the first is `_input`, the one the field is
|
|
1647
|
-
* named by
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1653
|
+
* named by, and every one of them gets the inputmode, `autocomplete`, the
|
|
1654
|
+
* `input-` passthrough, the keystroke filter, the placeholder and the `change`
|
|
1655
|
+
* reporting. The passthrough leaves `input-id` to the first alone, so no
|
|
1650
1656
|
* two controls share an id.
|
|
1651
1657
|
*/
|
|
1652
1658
|
class Input extends Field {
|
|
@@ -1713,10 +1719,6 @@ class Input extends Field {
|
|
|
1713
1719
|
for (const control of this.#controls) {
|
|
1714
1720
|
this.#configure(control, mode, autocomplete, strip);
|
|
1715
1721
|
}
|
|
1716
|
-
this._input.addEventListener('change', (evt) => {
|
|
1717
|
-
evt.stopPropagation();
|
|
1718
|
-
this._notifyChange();
|
|
1719
|
-
});
|
|
1720
1722
|
return {
|
|
1721
1723
|
fragment,
|
|
1722
1724
|
control: this._input,
|
|
@@ -1740,6 +1742,7 @@ class Input extends Field {
|
|
|
1740
1742
|
if (control !== this._input) {
|
|
1741
1743
|
Attributes.set(control, 'id', id || null);
|
|
1742
1744
|
}
|
|
1745
|
+
this.#reportChanges(control);
|
|
1743
1746
|
control.addEventListener('input', () => {
|
|
1744
1747
|
if (!strip) {
|
|
1745
1748
|
return;
|
|
@@ -1758,14 +1761,55 @@ class Input extends Field {
|
|
|
1758
1761
|
control.setSelectionRange(caret, caret);
|
|
1759
1762
|
});
|
|
1760
1763
|
}
|
|
1764
|
+
/** @param {HTMLInputElement|HTMLTextAreaElement} control */
|
|
1765
|
+
#reportChanges(control) {
|
|
1766
|
+
if (!SEGMENTED.has(control.type)) {
|
|
1767
|
+
control.addEventListener('change', (evt) => {
|
|
1768
|
+
evt.stopPropagation();
|
|
1769
|
+
this._notifyChange();
|
|
1770
|
+
});
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
/** @type {string|null} */
|
|
1774
|
+
let committed = null;
|
|
1775
|
+
let typed = false;
|
|
1776
|
+
const commit = () => {
|
|
1777
|
+
typed = false;
|
|
1778
|
+
const now = JSON.stringify(this.value);
|
|
1779
|
+
if (now === committed) {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
committed = now;
|
|
1783
|
+
this._notifyChange();
|
|
1784
|
+
};
|
|
1785
|
+
control.addEventListener('focus', () => {
|
|
1786
|
+
committed = JSON.stringify(this.value);
|
|
1787
|
+
});
|
|
1788
|
+
control.addEventListener('keydown', (/** @type any */ evt) => {
|
|
1789
|
+
if (evt.key === 'Enter' && !evt.isComposing) {
|
|
1790
|
+
commit();
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
typed = true;
|
|
1794
|
+
});
|
|
1795
|
+
control.addEventListener('blur', commit);
|
|
1796
|
+
control.addEventListener('change', (evt) => {
|
|
1797
|
+
evt.stopPropagation();
|
|
1798
|
+
if (!typed) {
|
|
1799
|
+
commit();
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1761
1803
|
/**
|
|
1762
1804
|
* The control's text, shaped in this order: upper cased under `uppercase`,
|
|
1763
1805
|
* trimmed under `trim`, `null` when that leaves it empty, every `,` read as
|
|
1764
1806
|
* `.` for `type="decimal"`, and decoded to a number under
|
|
1765
1807
|
* `v-type="number"`, where text that does not decode is answered as it is.
|
|
1766
1808
|
* Writing `null`, `undefined` or `''` empties the control; anything else is
|
|
1767
|
-
* written as its string.
|
|
1768
|
-
*
|
|
1809
|
+
* written as its string. Writing the text the control already holds leaves
|
|
1810
|
+
* it untouched, so a date or time control keeps the segments being typed.
|
|
1811
|
+
* Answers a `string`, a `number` or `null`; typed `any` because subclasses
|
|
1812
|
+
* answer other values.
|
|
1769
1813
|
* @type {any}
|
|
1770
1814
|
*/
|
|
1771
1815
|
get value() {
|
|
@@ -1785,7 +1829,11 @@ class Input extends Field {
|
|
|
1785
1829
|
return normalized;
|
|
1786
1830
|
}
|
|
1787
1831
|
set value(value) {
|
|
1788
|
-
|
|
1832
|
+
const text = value === '' || value === undefined || value === null ? '' : `${value}`;
|
|
1833
|
+
if (this._input.value === text) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
this._input.value = text;
|
|
1789
1837
|
}
|
|
1790
1838
|
/**
|
|
1791
1839
|
* The control's placeholder, `null` when there is none. The control always
|
|
@@ -2054,7 +2102,7 @@ class InputInstant extends Input {
|
|
|
2054
2102
|
}
|
|
2055
2103
|
/** @param {string|null|undefined} v an ISO instant; a falsy value empties the input */
|
|
2056
2104
|
set value(v) {
|
|
2057
|
-
|
|
2105
|
+
super.value = v ? Instant.isoToLocal(v) : '';
|
|
2058
2106
|
}
|
|
2059
2107
|
/**
|
|
2060
2108
|
* The lower bound as a UTC ISO instant.
|
|
@@ -2087,7 +2135,6 @@ class InputInstant extends Input {
|
|
|
2087
2135
|
}
|
|
2088
2136
|
/** @param {string|null} v */
|
|
2089
2137
|
set step(v) {
|
|
2090
|
-
|
|
2091
2138
|
this._input.step = v ?? '';
|
|
2092
2139
|
}
|
|
2093
2140
|
}
|
|
@@ -3964,6 +4011,15 @@ class Select extends Field {
|
|
|
3964
4011
|
#query() {
|
|
3965
4012
|
return this.#editing ? this.#input.value : '';
|
|
3966
4013
|
}
|
|
4014
|
+
/**
|
|
4015
|
+
* The text typed to search the vocabulary: what the dropdown was last
|
|
4016
|
+
* asked to match. `''` while the field is not being searched, its control
|
|
4017
|
+
* then showing the selection's label rather than a query.
|
|
4018
|
+
* @returns {string}
|
|
4019
|
+
*/
|
|
4020
|
+
get query() {
|
|
4021
|
+
return this.#query();
|
|
4022
|
+
}
|
|
3967
4023
|
#display() {
|
|
3968
4024
|
const entry = this.#values.values().next().value;
|
|
3969
4025
|
this.#input.value = this.#multiple ? '' : (entry?.label ?? '');
|
|
@@ -5640,10 +5696,6 @@ class CompareFilter extends Input {
|
|
|
5640
5696
|
},
|
|
5641
5697
|
});
|
|
5642
5698
|
this.operators = this.declared('operators');
|
|
5643
|
-
this._value2.addEventListener('change', (evt) => {
|
|
5644
|
-
evt.stopPropagation();
|
|
5645
|
-
this._notifyChange();
|
|
5646
|
-
});
|
|
5647
5699
|
if (this._operator.value === null) {
|
|
5648
5700
|
this._showDefaultOperator();
|
|
5649
5701
|
}
|
|
@@ -6121,9 +6173,7 @@ class BooleanFilter extends Field {
|
|
|
6121
6173
|
*/
|
|
6122
6174
|
get criterion() {
|
|
6123
6175
|
const token = this._value.value;
|
|
6124
|
-
return asCriterion(labelTextOf(this), this._operator.value, [
|
|
6125
|
-
token === '' ? '' : booleanValueLabel(token),
|
|
6126
|
-
]);
|
|
6176
|
+
return asCriterion(labelTextOf(this), this._operator.value, [token === '' ? '' : booleanValueLabel(token)]);
|
|
6127
6177
|
}
|
|
6128
6178
|
/**
|
|
6129
6179
|
* The field's own disabled claim. Setting it disables the value button and
|
|
@@ -6412,6 +6462,18 @@ class DialogSections {
|
|
|
6412
6462
|
get section() {
|
|
6413
6463
|
return this.#section;
|
|
6414
6464
|
}
|
|
6465
|
+
/**
|
|
6466
|
+
* Dispatches the dismissal on the host and calls `close` unless a listener
|
|
6467
|
+
* prevented it. A dismissal that is not cancelable announces a close the
|
|
6468
|
+
* platform makes whatever the page does, so `close` is not called for it.
|
|
6469
|
+
* @param {CustomEvent} asked
|
|
6470
|
+
* @param {() => void} close
|
|
6471
|
+
*/
|
|
6472
|
+
dismiss(asked, close) {
|
|
6473
|
+
if (this.#host.dispatchEvent(asked) && asked.cancelable) {
|
|
6474
|
+
close();
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6415
6477
|
/**
|
|
6416
6478
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
6417
6479
|
* so a selection dragged out of the panel and released outside does not.
|
|
@@ -6550,6 +6612,18 @@ const wireTargets = () => {
|
|
|
6550
6612
|
* and a press and release both on the backdrop close the dialog, each as a
|
|
6551
6613
|
* dismissal, as Escape does.
|
|
6552
6614
|
*
|
|
6615
|
+
* Before any of those three gestures closes it, the element dispatches a
|
|
6616
|
+
* cancelable, non-bubbling `dialog:dismiss` with `detail: { reason }`, the
|
|
6617
|
+
* reason being `button`, `backdrop` or `escape`; a listener calling
|
|
6618
|
+
* `preventDefault()` keeps it open, to guard unsaved edits. A `data-result`
|
|
6619
|
+
* button, `close-on-submit` and `close()` answer rather than dismiss, and
|
|
6620
|
+
* dispatch none.
|
|
6621
|
+
*
|
|
6622
|
+
* The platform lets a page refuse Escape once per user interaction: a second
|
|
6623
|
+
* Escape with no click or keystroke in between closes the dialog whatever the
|
|
6624
|
+
* page does, `requires-answer` included. That close is still announced by a
|
|
6625
|
+
* `dialog:dismiss`, which is then not cancelable.
|
|
6626
|
+
*
|
|
6553
6627
|
* `requires-answer` withholds the close button and refuses Escape and the
|
|
6554
6628
|
* backdrop, so the dialog is closed only by a button carrying `data-result`
|
|
6555
6629
|
* or from code through `close()`.
|
|
@@ -6622,21 +6696,32 @@ class Dialog extends ParsedElement {
|
|
|
6622
6696
|
this.#dialog.close(result);
|
|
6623
6697
|
}
|
|
6624
6698
|
});
|
|
6699
|
+
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
6700
|
+
this.#sections.dismiss(
|
|
6701
|
+
new CustomEvent('dialog:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
6702
|
+
() => this.#dialog.close(''),
|
|
6703
|
+
);
|
|
6625
6704
|
if (!requiresAnswer) {
|
|
6626
|
-
this.#sections.onBackdrop(() =>
|
|
6705
|
+
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
6627
6706
|
}
|
|
6628
|
-
fragment
|
|
6629
|
-
.querySelector('[data-ref=close]')
|
|
6630
|
-
?.addEventListener('click', () => this.#dialog.close(''));
|
|
6707
|
+
fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
|
|
6631
6708
|
if (closeOnSubmit) {
|
|
6632
6709
|
this.#sections.onSubmitted((response) => {
|
|
6633
6710
|
this.#answer = { dismissed: false, result: null, response };
|
|
6634
6711
|
this.#dialog.close('submitted');
|
|
6635
6712
|
});
|
|
6636
6713
|
}
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6714
|
+
this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
|
|
6715
|
+
if (!e.cancelable) {
|
|
6716
|
+
this.#dialog.returnValue = '';
|
|
6717
|
+
dismiss('escape', true);
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
e.preventDefault();
|
|
6721
|
+
if (!requiresAnswer) {
|
|
6722
|
+
dismiss('escape');
|
|
6723
|
+
}
|
|
6724
|
+
});
|
|
6640
6725
|
this.replaceChildren(fragment);
|
|
6641
6726
|
wireTargets();
|
|
6642
6727
|
}
|
|
@@ -6837,7 +6922,14 @@ class Dialog extends ParsedElement {
|
|
|
6837
6922
|
* default, when absent) or `start`, following the writing direction.
|
|
6838
6923
|
*
|
|
6839
6924
|
* The close button, Escape, and a press and release both on the backdrop all
|
|
6840
|
-
* go through `close()
|
|
6925
|
+
* go through `close()`, once a cancelable, non-bubbling `drawer:dismiss` with
|
|
6926
|
+
* `detail: { reason }` (`button`, `backdrop` or `escape`) has been dispatched
|
|
6927
|
+
* on the element and no listener prevented it: `preventDefault()` keeps the
|
|
6928
|
+
* drawer open, to guard unsaved edits. `close()` and `close-on-submit`
|
|
6929
|
+
* dispatch none. The platform lets a page refuse Escape once per user
|
|
6930
|
+
* interaction: a second Escape with no click or keystroke in between closes
|
|
6931
|
+
* the drawer at once, without the slide, and its `drawer:dismiss` is then not
|
|
6932
|
+
* cancelable. Every close dispatches a non-bubbling `close` event on
|
|
6841
6933
|
* the element with `detail: { dismissed, response }`: `{ dismissed: false,
|
|
6842
6934
|
* response }` when a form closed it under `close-on-submit`, and
|
|
6843
6935
|
* `{ dismissed: true, response: null }` for any other close.
|
|
@@ -6893,13 +6985,23 @@ class Drawer extends ParsedElement {
|
|
|
6893
6985
|
if (placement) {
|
|
6894
6986
|
this.#dialog.setAttribute('placement', placement);
|
|
6895
6987
|
}
|
|
6988
|
+
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
6989
|
+
this.#sections.dismiss(
|
|
6990
|
+
new CustomEvent('drawer:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
6991
|
+
() => this.close(),
|
|
6992
|
+
);
|
|
6896
6993
|
/** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
|
|
6897
|
-
|
|
6994
|
+
dismiss('button'),
|
|
6898
6995
|
);
|
|
6899
|
-
this.#sections.onBackdrop(() =>
|
|
6996
|
+
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
6900
6997
|
this.#dialog.addEventListener('cancel', (e) => {
|
|
6998
|
+
if (!e.cancelable) {
|
|
6999
|
+
this.#stopSlidingOut();
|
|
7000
|
+
dismiss('escape', true);
|
|
7001
|
+
return;
|
|
7002
|
+
}
|
|
6901
7003
|
e.preventDefault();
|
|
6902
|
-
|
|
7004
|
+
dismiss('escape');
|
|
6903
7005
|
});
|
|
6904
7006
|
this.#dialog.addEventListener('close', () => {
|
|
6905
7007
|
this.dispatchEvent(
|
|
@@ -7582,11 +7684,9 @@ class Wizard extends ParsedElement {
|
|
|
7582
7684
|
}
|
|
7583
7685
|
/**
|
|
7584
7686
|
* Moves to the section carrying `data-step="<ref>"`. A move makes that step
|
|
7585
|
-
* and section current, moves the focus to the section, dispatches
|
|
7586
|
-
* on the host (not bubbling) with detail `{ index, step }`,
|
|
7587
|
-
* section requests for it.
|
|
7588
|
-
* bubbles through the host under the same name, so a listener tells the
|
|
7589
|
-
* two apart by `e.target`.
|
|
7687
|
+
* and section current, moves the focus to the section, dispatches
|
|
7688
|
+
* `wizard:change` on the host (not bubbling) with detail `{ index, step }`,
|
|
7689
|
+
* then fires the section requests for it.
|
|
7590
7690
|
* @param {string} ref the section's `data-step`
|
|
7591
7691
|
* @returns {Promise<any[] | undefined> | undefined} a promise resolving to
|
|
7592
7692
|
* the answers once they are delivered, or to undefined when nobody
|
|
@@ -7643,7 +7743,7 @@ class Wizard extends ParsedElement {
|
|
|
7643
7743
|
this.#apply(clamped);
|
|
7644
7744
|
this.#sections[this.#index].focus();
|
|
7645
7745
|
if (this.rendered) {
|
|
7646
|
-
this.dispatchEvent(new CustomEvent('change', { detail: { index: this.#index, step: this.step } }));
|
|
7746
|
+
this.dispatchEvent(new CustomEvent('wizard:change', { detail: { index: this.#index, step: this.step } }));
|
|
7647
7747
|
}
|
|
7648
7748
|
return this.#enter(clamped);
|
|
7649
7749
|
}
|