@optionfactory/fml 9.0.0-rc22 → 9.0.0-rc24
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 +22 -8
- package/dist/fml.css +3 -3
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +286 -80
- 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 +3 -3
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +127 -42
- package/dist/ful.iife.js +286 -80
- 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 +286 -80
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +11 -3
- package/dist/web-types.json +25 -9
- package/package.json +1 -1
package/dist/ful.iife.js
CHANGED
|
@@ -693,6 +693,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
693
693
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
694
694
|
* composite can use to route the problem to an inner control.
|
|
695
695
|
*
|
|
696
|
+
* A control counts only when it can show a problem, through a
|
|
697
|
+
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
698
|
+
* skipped, so its problems fall to a shorter prefix or to the banner instead
|
|
699
|
+
* of being dropped.
|
|
700
|
+
*
|
|
696
701
|
* Every other problem, and a field problem naming no control, is shown in
|
|
697
702
|
* each `ful-errors` banner of the form: the banner gets `role="alert"`, is
|
|
698
703
|
* revealed and holds the reasons one per line. With no such problem the
|
|
@@ -731,7 +736,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
731
736
|
const parts = name.split('.');
|
|
732
737
|
for (let i = parts.length; i !== 0; --i) {
|
|
733
738
|
const prefix = parts.slice(0, i).join('.');
|
|
734
|
-
const targets = named.get(prefix) ?? [];
|
|
739
|
+
const targets = (named.get(prefix) ?? []).filter((el) => typeof el.setCustomValidity === 'function');
|
|
735
740
|
if (targets.length === 0) {
|
|
736
741
|
continue;
|
|
737
742
|
}
|
|
@@ -1189,6 +1194,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1189
1194
|
* accessibility tree.
|
|
1190
1195
|
* - `also` are further controls that mirror `disabled` and `readOnly`
|
|
1191
1196
|
* beside the claim target.
|
|
1197
|
+
*
|
|
1198
|
+
* The pieces are elements the field rendered itself, native controls or
|
|
1199
|
+
* wrappers of its own. A ful field is not one: it has no `readOnly`, its
|
|
1200
|
+
* host's role takes no aria, and a label's `for` names its host rather than
|
|
1201
|
+
* its control. A composite built from ful fields answers the inner field as
|
|
1202
|
+
* `control`, which forwards `focus()`, lets it render its own label with
|
|
1203
|
+
* `label` and `announces` null, and forwards the claims in its setters,
|
|
1204
|
+
* calling super first.
|
|
1192
1205
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
1193
1206
|
* @returns {any} the pieces, or a promise of them
|
|
1194
1207
|
* @throws {Error} when the subclass does not implement it
|
|
@@ -1463,11 +1476,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1463
1476
|
* A call while a submit is in flight returns at once, doing nothing.
|
|
1464
1477
|
* @param {HTMLElement} [submitter] the button that submitted, if any; it is
|
|
1465
1478
|
* the only button whose name and value are submitted
|
|
1466
|
-
* @returns {Promise<
|
|
1479
|
+
* @returns {Promise<boolean>} settling once the submit has finished: true
|
|
1480
|
+
* after `submit:success`, false when it failed, was cancelled or was dropped
|
|
1481
|
+
* for one in flight
|
|
1467
1482
|
*/
|
|
1468
1483
|
async submit(submitter) {
|
|
1469
1484
|
if (this.#submitting) {
|
|
1470
|
-
return;
|
|
1485
|
+
return false;
|
|
1471
1486
|
}
|
|
1472
1487
|
this.#submitting = true;
|
|
1473
1488
|
this.spinner(true);
|
|
@@ -1484,7 +1499,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1484
1499
|
detail: { submitter, values, request },
|
|
1485
1500
|
});
|
|
1486
1501
|
if (!this.dispatchEvent(se)) {
|
|
1487
|
-
return;
|
|
1502
|
+
return false;
|
|
1488
1503
|
}
|
|
1489
1504
|
this.errors = [];
|
|
1490
1505
|
const sre = new CustomEvent('submit:requested', {
|
|
@@ -1507,6 +1522,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1507
1522
|
detail: { submitter, values, request, response: mapped },
|
|
1508
1523
|
}),
|
|
1509
1524
|
);
|
|
1525
|
+
return true;
|
|
1510
1526
|
} catch (e) {
|
|
1511
1527
|
this.dispatchEvent(
|
|
1512
1528
|
new CustomEvent('submit:failure', {
|
|
@@ -1520,11 +1536,27 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1520
1536
|
} else {
|
|
1521
1537
|
console.warn('failed to submit form', this, 'reason:', e);
|
|
1522
1538
|
}
|
|
1539
|
+
return false;
|
|
1523
1540
|
} finally {
|
|
1524
1541
|
this.#submitting = false;
|
|
1525
1542
|
this.spinner(false);
|
|
1526
1543
|
}
|
|
1527
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Saves the unsaved edits, for a page action that would lose them, such as
|
|
1547
|
+
* one reloading the page: under `track-changes` it submits only while the
|
|
1548
|
+
* form is dirty, and without it always submits.
|
|
1549
|
+
* @param {HTMLElement} [submitter] as `submit` takes it
|
|
1550
|
+
* @returns {Promise<boolean>} whether no unsaved edit is left: false when
|
|
1551
|
+
* the submit did not succeed, or when the user edited the form while it
|
|
1552
|
+
* was in flight
|
|
1553
|
+
*/
|
|
1554
|
+
async save(submitter) {
|
|
1555
|
+
if (this.#tracking && !this.dirty) {
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
return (await this.submit(submitter)) && !this.dirty;
|
|
1559
|
+
}
|
|
1528
1560
|
/**
|
|
1529
1561
|
* Resets the native form, each field returning to its default value. Under
|
|
1530
1562
|
* `track-changes` every control counts as edited: the form is dirty if the
|
|
@@ -1597,12 +1629,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
1597
1629
|
}
|
|
1598
1630
|
}
|
|
1599
1631
|
/**
|
|
1600
|
-
* Counts
|
|
1632
|
+
* Counts controls as edited by the user, for values the page writes on
|
|
1601
1633
|
* the user's behalf: assigning `value` reports nothing, as 0019 wants.
|
|
1602
|
-
* @param {Element}
|
|
1634
|
+
* @param {...Element} els each a control, or an element inside one
|
|
1603
1635
|
*/
|
|
1604
|
-
edited(
|
|
1605
|
-
|
|
1636
|
+
edited(...els) {
|
|
1637
|
+
for (const el of els) {
|
|
1638
|
+
this.#touch(el);
|
|
1639
|
+
}
|
|
1606
1640
|
}
|
|
1607
1641
|
/**
|
|
1608
1642
|
* @param {Map<string, string>} saved
|
|
@@ -4555,7 +4589,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
4555
4589
|
return;
|
|
4556
4590
|
}
|
|
4557
4591
|
/** @type {HTMLInputElement|null} */
|
|
4558
|
-
const el = this.querySelector(`input[type=radio][value
|
|
4592
|
+
const el = this.querySelector(`input[type=radio][value="${CSS.escape(String(value))}"]`);
|
|
4559
4593
|
if (el === null) {
|
|
4560
4594
|
clear();
|
|
4561
4595
|
return;
|
|
@@ -6704,6 +6738,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6704
6738
|
#error;
|
|
6705
6739
|
#requests = new SectionRequests();
|
|
6706
6740
|
#updates = new Claims();
|
|
6741
|
+
/** @type {Promise<boolean>|null} */
|
|
6742
|
+
#confirming = null;
|
|
6707
6743
|
/**
|
|
6708
6744
|
* @param {HTMLElement} host the element the section requests are dispatched on
|
|
6709
6745
|
* @param {DocumentFragment} fragment the rendered template, holding a
|
|
@@ -6727,17 +6763,53 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6727
6763
|
get section() {
|
|
6728
6764
|
return this.#section;
|
|
6729
6765
|
}
|
|
6766
|
+
/**
|
|
6767
|
+
* Whether a `ful-form` of this dialog, not one of a dialog nested in it,
|
|
6768
|
+
* holds unsaved edits (see `Form.dirty`).
|
|
6769
|
+
* @returns {boolean}
|
|
6770
|
+
*/
|
|
6771
|
+
get dirty() {
|
|
6772
|
+
return Array.from(this.#dialog.querySelectorAll('ful-form')).some(
|
|
6773
|
+
(form) => form.closest('dialog') === this.#dialog && /** @type {any} */ (form).dirty,
|
|
6774
|
+
);
|
|
6775
|
+
}
|
|
6730
6776
|
/**
|
|
6731
6777
|
* Dispatches the dismissal on the host and calls `close` unless a listener
|
|
6732
|
-
* prevented it
|
|
6733
|
-
*
|
|
6778
|
+
* prevented it or, with `confirm` given and the dialog `dirty`, `confirm`
|
|
6779
|
+
* answers false. A dismissal that is not cancelable announces a close the
|
|
6780
|
+
* platform makes whatever the page does, so nothing is asked and `close` is
|
|
6781
|
+
* not called for it. While `confirm` is pending a further dismissal waits
|
|
6782
|
+
* for the same answer, and a dialog closed meanwhile is not closed again.
|
|
6734
6783
|
* @param {CustomEvent} asked
|
|
6735
6784
|
* @param {() => void} close
|
|
6785
|
+
* @param {(() => Promise<boolean>)|null} confirm asks whether to drop the unsaved edits
|
|
6786
|
+
* @returns {Promise<boolean>} whether the dismissal closes the dialog
|
|
6736
6787
|
*/
|
|
6737
|
-
dismiss(asked, close) {
|
|
6738
|
-
if (this.#
|
|
6788
|
+
dismiss(asked, close, confirm) {
|
|
6789
|
+
if (this.#confirming) {
|
|
6790
|
+
return this.#confirming;
|
|
6791
|
+
}
|
|
6792
|
+
if (!this.#host.dispatchEvent(asked)) {
|
|
6793
|
+
return Promise.resolve(false);
|
|
6794
|
+
}
|
|
6795
|
+
if (!asked.cancelable) {
|
|
6796
|
+
return Promise.resolve(true);
|
|
6797
|
+
}
|
|
6798
|
+
if (!confirm || !this.dirty) {
|
|
6739
6799
|
close();
|
|
6800
|
+
return Promise.resolve(true);
|
|
6740
6801
|
}
|
|
6802
|
+
this.#confirming = confirm()
|
|
6803
|
+
.then((drop) => {
|
|
6804
|
+
if (drop && this.#dialog.open) {
|
|
6805
|
+
close();
|
|
6806
|
+
}
|
|
6807
|
+
return drop;
|
|
6808
|
+
})
|
|
6809
|
+
.finally(() => {
|
|
6810
|
+
this.#confirming = null;
|
|
6811
|
+
});
|
|
6812
|
+
return this.#confirming;
|
|
6741
6813
|
}
|
|
6742
6814
|
/**
|
|
6743
6815
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
@@ -6791,11 +6863,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6791
6863
|
* `show`, and fills the content with what the callback resolves to. A
|
|
6792
6864
|
* rejection is shown, as the problems' reasons, in the error section, and
|
|
6793
6865
|
* the content is hidden. When a newer `update()` starts before the callback
|
|
6794
|
-
* settles, this call paints nothing
|
|
6795
|
-
*
|
|
6866
|
+
* settles, this call paints nothing and resolves `null`, the section being
|
|
6867
|
+
* the newer call's; it still rejects with the callback's error.
|
|
6796
6868
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
6797
6869
|
* @param {() => void} show opens the dialog when it is closed
|
|
6798
|
-
* @returns {Promise<HTMLElement>} the content section
|
|
6870
|
+
* @returns {Promise<HTMLElement|null>} the content section, or `null` for a
|
|
6871
|
+
* call a newer one overtook
|
|
6799
6872
|
* @throws {any} what the callback threw or rejected with
|
|
6800
6873
|
*/
|
|
6801
6874
|
async update(cb, show) {
|
|
@@ -6808,7 +6881,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6808
6881
|
try {
|
|
6809
6882
|
const delivered = await cb();
|
|
6810
6883
|
if (claim.stale) {
|
|
6811
|
-
return
|
|
6884
|
+
return null;
|
|
6812
6885
|
}
|
|
6813
6886
|
this.#section.replaceChildren(delivered);
|
|
6814
6887
|
this.#loading?.setAttribute('hidden', '');
|
|
@@ -6877,21 +6950,24 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6877
6950
|
* and a press and release both on the backdrop close the dialog, each as a
|
|
6878
6951
|
* dismissal, as Escape does.
|
|
6879
6952
|
*
|
|
6880
|
-
*
|
|
6881
|
-
*
|
|
6882
|
-
*
|
|
6883
|
-
* `
|
|
6884
|
-
*
|
|
6885
|
-
*
|
|
6953
|
+
* Those three gestures go through `dismiss(reason)`, the reason being
|
|
6954
|
+
* `button`, `backdrop` or `escape`: before it closes the dialog, the element
|
|
6955
|
+
* dispatches a cancelable, non-bubbling `dialog:dismiss` with
|
|
6956
|
+
* `detail: { reason }`, and a listener calling `preventDefault()` keeps it
|
|
6957
|
+
* open. Under `guard-dismiss`, a dismissal nobody refused, while a `ful-form`
|
|
6958
|
+
* of the dialog holds unsaved edits (see `Form.dirty`), first asks the user,
|
|
6959
|
+
* through `Dialog.confirm`, whether to close and drop them. A `data-result`
|
|
6960
|
+
* button, `close-on-submit`, `close()` and `update()` are answers or the page
|
|
6961
|
+
* deciding: they dispatch nothing and ask nothing.
|
|
6886
6962
|
*
|
|
6887
6963
|
* The platform lets a page refuse Escape once per user interaction: a second
|
|
6888
6964
|
* Escape with no click or keystroke in between closes the dialog whatever the
|
|
6889
|
-
* page does, `requires-answer` included. That close is
|
|
6890
|
-
* `dialog:dismiss`, which is then not cancelable.
|
|
6965
|
+
* page does, `requires-answer` and `guard-dismiss` included. That close is
|
|
6966
|
+
* still announced by a `dialog:dismiss`, which is then not cancelable.
|
|
6891
6967
|
*
|
|
6892
6968
|
* `requires-answer` withholds the close button and refuses Escape and the
|
|
6893
6969
|
* backdrop, so the dialog is closed only by a button carrying `data-result`
|
|
6894
|
-
* or from code through `close()`.
|
|
6970
|
+
* or from code, through `close()` or `dismiss()`.
|
|
6895
6971
|
*
|
|
6896
6972
|
* `close-on-submit` closes the dialog when a `ful-form` that is a direct child
|
|
6897
6973
|
* of the body dispatches `submit:success`, answering with its
|
|
@@ -6911,7 +6987,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6911
6987
|
* in a form needs.
|
|
6912
6988
|
*/
|
|
6913
6989
|
class Dialog extends index_mjs.ParsedElement {
|
|
6914
|
-
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
|
|
6990
|
+
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence', 'guard-dismiss:presence'];
|
|
6915
6991
|
static slots = true;
|
|
6916
6992
|
static template = `
|
|
6917
6993
|
<dialog data-ref="dialog" class="ful-dialog">
|
|
@@ -6961,15 +7037,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6961
7037
|
this.#dialog.close(result);
|
|
6962
7038
|
}
|
|
6963
7039
|
});
|
|
6964
|
-
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
6965
|
-
this.#sections.dismiss(
|
|
6966
|
-
new CustomEvent('dialog:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
6967
|
-
() => this.#dialog.close(''),
|
|
6968
|
-
);
|
|
6969
7040
|
if (!requiresAnswer) {
|
|
6970
|
-
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
7041
|
+
this.#sections.onBackdrop(() => this.dismiss('backdrop'));
|
|
6971
7042
|
}
|
|
6972
|
-
fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
|
|
7043
|
+
fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => this.dismiss('button'));
|
|
6973
7044
|
if (closeOnSubmit) {
|
|
6974
7045
|
this.#sections.onSubmitted((response) => {
|
|
6975
7046
|
this.#answer = { dismissed: false, result: null, response };
|
|
@@ -6979,12 +7050,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
6979
7050
|
this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
|
|
6980
7051
|
if (!e.cancelable) {
|
|
6981
7052
|
this.#dialog.returnValue = '';
|
|
6982
|
-
dismiss('escape',
|
|
7053
|
+
this.#dismiss('escape', false);
|
|
6983
7054
|
return;
|
|
6984
7055
|
}
|
|
6985
7056
|
e.preventDefault();
|
|
6986
7057
|
if (!requiresAnswer) {
|
|
6987
|
-
dismiss('escape');
|
|
7058
|
+
this.dismiss('escape');
|
|
6988
7059
|
}
|
|
6989
7060
|
});
|
|
6990
7061
|
this.replaceChildren(fragment);
|
|
@@ -7056,11 +7127,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7056
7127
|
*
|
|
7057
7128
|
* No `section:requested` is dispatched, and no `ask()` waiter is added:
|
|
7058
7129
|
* listen for the `close` event to learn how the dialog ended. When a newer
|
|
7059
|
-
* `update()` starts before the callback settles, this call paints nothing
|
|
7060
|
-
*
|
|
7061
|
-
* callback's error.
|
|
7130
|
+
* `update()` starts before the callback settles, this call paints nothing
|
|
7131
|
+
* and resolves `null`, the body being the newer call's; it still rejects
|
|
7132
|
+
* with the callback's error.
|
|
7062
7133
|
* @param {() => Node|string|Promise<Node|string>} cb producing the body content
|
|
7063
|
-
* @returns {Promise<Element>} the body section, in the document
|
|
7134
|
+
* @returns {Promise<Element|null>} the body section, in the document, or
|
|
7135
|
+
* `null` for a call a newer one overtook
|
|
7064
7136
|
* @throws {any} what the callback threw or rejected with
|
|
7065
7137
|
*/
|
|
7066
7138
|
async update(cb) {
|
|
@@ -7076,6 +7148,34 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7076
7148
|
refresh() {
|
|
7077
7149
|
return this.#sections.request();
|
|
7078
7150
|
}
|
|
7151
|
+
/**
|
|
7152
|
+
* Dismisses the dialog as the user would: dispatches the cancelable
|
|
7153
|
+
* `dialog:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
|
|
7154
|
+
* asks before dropping unsaved edits, closing the dialog as a dismissal
|
|
7155
|
+
* unless either refused. `requires-answer` does not refuse it, as it does
|
|
7156
|
+
* not refuse `close()`. Does nothing on a closed dialog.
|
|
7157
|
+
* @param {string} reason why it is dismissed, such as `navigation`
|
|
7158
|
+
* @returns {Promise<boolean>} whether the dialog is closed, false when a
|
|
7159
|
+
* listener or the user kept it open
|
|
7160
|
+
*/
|
|
7161
|
+
dismiss(reason) {
|
|
7162
|
+
if (!this.#dialog.open) {
|
|
7163
|
+
return Promise.resolve(true);
|
|
7164
|
+
}
|
|
7165
|
+
return this.#dismiss(reason, true);
|
|
7166
|
+
}
|
|
7167
|
+
/**
|
|
7168
|
+
* @param {string} reason
|
|
7169
|
+
* @param {boolean} cancelable false for a close the platform makes anyway
|
|
7170
|
+
* @returns {Promise<boolean>}
|
|
7171
|
+
*/
|
|
7172
|
+
#dismiss(reason, cancelable) {
|
|
7173
|
+
return this.#sections.dismiss(
|
|
7174
|
+
new CustomEvent('dialog:dismiss', { cancelable, detail: { reason } }),
|
|
7175
|
+
() => this.#dialog.close(''),
|
|
7176
|
+
this.declared('guard-dismiss') ? discardEdits : null,
|
|
7177
|
+
);
|
|
7178
|
+
}
|
|
7079
7179
|
/**
|
|
7080
7180
|
* Closes an open dialog, doing nothing on a closed one. A non-empty result
|
|
7081
7181
|
* answers as the button carrying that `data-result` would; no result, or
|
|
@@ -7175,6 +7275,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7175
7275
|
}
|
|
7176
7276
|
}
|
|
7177
7277
|
|
|
7278
|
+
/**
|
|
7279
|
+
* Asks, through `Dialog.confirm`, whether to close and drop the unsaved edits,
|
|
7280
|
+
* with the localized `dialog.discard.*` texts.
|
|
7281
|
+
* @returns {Promise<boolean>} true to drop them
|
|
7282
|
+
*/
|
|
7283
|
+
const discardEdits = () => {
|
|
7284
|
+
const l10n = index_mjs.Localization.of();
|
|
7285
|
+
return Dialog.confirm(l10n.t('dialog.discard.header'), l10n.t('dialog.discard.body'), {
|
|
7286
|
+
confirm: l10n.t('dialog.discard.confirm'),
|
|
7287
|
+
cancel: l10n.t('dialog.discard.cancel'),
|
|
7288
|
+
});
|
|
7289
|
+
};
|
|
7290
|
+
|
|
7178
7291
|
/**
|
|
7179
7292
|
* A side panel on the native `<dialog>`, shown as a modal: a header holding
|
|
7180
7293
|
* the title and a localized close button, then loading, error and content
|
|
@@ -7187,17 +7300,23 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7187
7300
|
* default, when absent) or `start`, following the writing direction.
|
|
7188
7301
|
*
|
|
7189
7302
|
* The close button, Escape, and a press and release both on the backdrop all
|
|
7190
|
-
* go through `
|
|
7191
|
-
* `
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7194
|
-
*
|
|
7303
|
+
* go through `dismiss(reason)`, the reason being `button`, `backdrop` or
|
|
7304
|
+
* `escape`: before it closes the drawer, the element dispatches a cancelable,
|
|
7305
|
+
* non-bubbling `drawer:dismiss` with `detail: { reason }`, and a listener
|
|
7306
|
+
* calling `preventDefault()` keeps it open. Under `guard-dismiss`, a dismissal
|
|
7307
|
+
* nobody refused, while a `ful-form` of the drawer holds unsaved edits (see
|
|
7308
|
+
* `Form.dirty`), first asks the user, through `Dialog.confirm`, whether to
|
|
7309
|
+
* close and drop them. A `data-result` button, `close-on-submit`, `close()`
|
|
7310
|
+
* and `update()` are answers or the page deciding: they dispatch nothing and
|
|
7311
|
+
* ask nothing. The platform lets a page refuse Escape once per user
|
|
7195
7312
|
* interaction: a second Escape with no click or keystroke in between closes
|
|
7196
|
-
* the drawer at once, without the slide,
|
|
7197
|
-
*
|
|
7198
|
-
*
|
|
7199
|
-
*
|
|
7200
|
-
*
|
|
7313
|
+
* the drawer at once, without the slide, `guard-dismiss` included, and its
|
|
7314
|
+
* `drawer:dismiss` is then not cancelable.
|
|
7315
|
+
*
|
|
7316
|
+
* Any button inside the drawer carrying `data-result` closes it with that
|
|
7317
|
+
* result, an empty one being a dismissal. Every close dispatches a
|
|
7318
|
+
* non-bubbling `close` event on the element carrying the `DialogOutcome` as
|
|
7319
|
+
* `detail`, before the pending `ask()` calls resolve.
|
|
7201
7320
|
*
|
|
7202
7321
|
* `close-on-submit` closes the drawer when a `ful-form` that is a direct child
|
|
7203
7322
|
* of the content section dispatches `submit:success`, carrying its
|
|
@@ -7209,7 +7328,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7209
7328
|
* element's id calls `open()` on click.
|
|
7210
7329
|
*/
|
|
7211
7330
|
class Drawer extends index_mjs.ParsedElement {
|
|
7212
|
-
static attributes = ['header', 'placement', 'close-on-submit:presence'];
|
|
7331
|
+
static attributes = ['header', 'placement', 'close-on-submit:presence', 'guard-dismiss:presence'];
|
|
7213
7332
|
static slots = true;
|
|
7214
7333
|
static template = `
|
|
7215
7334
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -7226,8 +7345,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7226
7345
|
#dialog;
|
|
7227
7346
|
#title;
|
|
7228
7347
|
#sections;
|
|
7229
|
-
/** @type {{ dismissed: boolean, response: any }|null} */
|
|
7348
|
+
/** @type {{ dismissed: boolean, result: string|null, response: any }|null} */
|
|
7230
7349
|
#answer = null;
|
|
7350
|
+
#resolvers = [];
|
|
7231
7351
|
#closing = false;
|
|
7232
7352
|
#slideOutEnded = (/** @type AnimationEvent */ e) => {
|
|
7233
7353
|
if (e.target === this.#dialog) {
|
|
@@ -7250,32 +7370,33 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7250
7370
|
if (placement) {
|
|
7251
7371
|
this.#dialog.setAttribute('placement', placement);
|
|
7252
7372
|
}
|
|
7253
|
-
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
7254
|
-
this.#sections.dismiss(
|
|
7255
|
-
new CustomEvent('drawer:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
7256
|
-
() => this.close(),
|
|
7257
|
-
);
|
|
7258
7373
|
/** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
|
|
7259
|
-
dismiss('button'),
|
|
7374
|
+
this.dismiss('button'),
|
|
7260
7375
|
);
|
|
7261
|
-
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
7376
|
+
this.#sections.onBackdrop(() => this.dismiss('backdrop'));
|
|
7262
7377
|
this.#dialog.addEventListener('cancel', (e) => {
|
|
7263
7378
|
if (!e.cancelable) {
|
|
7264
7379
|
this.#stopSlidingOut();
|
|
7265
|
-
dismiss('escape',
|
|
7380
|
+
this.#dismiss('escape', false);
|
|
7266
7381
|
return;
|
|
7267
7382
|
}
|
|
7268
7383
|
e.preventDefault();
|
|
7269
|
-
dismiss('escape');
|
|
7384
|
+
this.dismiss('escape');
|
|
7385
|
+
});
|
|
7386
|
+
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
7387
|
+
const result = e.target.closest('button[data-result]')?.dataset.result;
|
|
7388
|
+
if (result !== undefined) {
|
|
7389
|
+
this.close(result);
|
|
7390
|
+
}
|
|
7270
7391
|
});
|
|
7271
7392
|
this.#dialog.addEventListener('close', () => {
|
|
7272
|
-
this
|
|
7273
|
-
|
|
7274
|
-
);
|
|
7393
|
+
const outcome = this.#outcome();
|
|
7394
|
+
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
7395
|
+
this.#settle(outcome);
|
|
7275
7396
|
});
|
|
7276
7397
|
if (this.declared('close-on-submit')) {
|
|
7277
7398
|
this.#sections.onSubmitted((response) => {
|
|
7278
|
-
this.#answer = { dismissed: false, response };
|
|
7399
|
+
this.#answer = { dismissed: false, result: null, response };
|
|
7279
7400
|
this.close();
|
|
7280
7401
|
});
|
|
7281
7402
|
}
|
|
@@ -7301,12 +7422,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7301
7422
|
* which has `role="alert"`, and the content is hidden.
|
|
7302
7423
|
*
|
|
7303
7424
|
* No `section:requested` is dispatched. When a newer `update()` starts
|
|
7304
|
-
* before the callback settles, this call paints nothing
|
|
7305
|
-
* the content section
|
|
7306
|
-
* error.
|
|
7425
|
+
* before the callback settles, this call paints nothing and resolves
|
|
7426
|
+
* `null`, the content section being the newer call's; it still rejects with
|
|
7427
|
+
* the callback's error.
|
|
7307
7428
|
* @param {string} header the title, set as text
|
|
7308
7429
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
7309
|
-
* @returns {Promise<Element>} the content section, in the document
|
|
7430
|
+
* @returns {Promise<Element|null>} the content section, in the document, or
|
|
7431
|
+
* `null` for a call a newer one overtook
|
|
7310
7432
|
* @throws {any} what the callback threw or rejected with
|
|
7311
7433
|
*/
|
|
7312
7434
|
async update(header, cb) {
|
|
@@ -7323,9 +7445,36 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7323
7445
|
refresh() {
|
|
7324
7446
|
return this.#sections.request();
|
|
7325
7447
|
}
|
|
7448
|
+
/** @returns {{ dismissed: boolean, result: string|null, response: any }} */
|
|
7449
|
+
#outcome() {
|
|
7450
|
+
return this.#answer ?? { dismissed: true, result: null, response: null };
|
|
7451
|
+
}
|
|
7452
|
+
/** @param {{ dismissed: boolean, result: string|null, response: any }} outcome */
|
|
7453
|
+
#settle(outcome) {
|
|
7454
|
+
const resolvers = this.#resolvers;
|
|
7455
|
+
this.#resolvers = [];
|
|
7456
|
+
for (const resolve of resolvers) {
|
|
7457
|
+
resolve(outcome);
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7326
7460
|
/**
|
|
7327
|
-
*
|
|
7328
|
-
*
|
|
7461
|
+
* Resolves every pending `ask()` and `open()` with the outcome so far,
|
|
7462
|
+
* a dismissal for a drawer still open, so that no caller waits on a drawer
|
|
7463
|
+
* that left the document. The native dialog is not closed and no `close`
|
|
7464
|
+
* event is dispatched.
|
|
7465
|
+
*/
|
|
7466
|
+
disconnectedCallback() {
|
|
7467
|
+
this.#settle(this.#outcome());
|
|
7468
|
+
}
|
|
7469
|
+
/**
|
|
7470
|
+
* The same as `ask()`.
|
|
7471
|
+
* @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>}
|
|
7472
|
+
*/
|
|
7473
|
+
open() {
|
|
7474
|
+
return this.ask();
|
|
7475
|
+
}
|
|
7476
|
+
/**
|
|
7477
|
+
* Shows the drawer as a modal and resolves with how it ends.
|
|
7329
7478
|
*
|
|
7330
7479
|
* The call that opens the drawer resets the outcome of the previous
|
|
7331
7480
|
* opening, hides the error and loading sections, and dispatches
|
|
@@ -7333,26 +7482,67 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
7333
7482
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
7334
7483
|
* the content section and `first` true only on the first answered request),
|
|
7335
7484
|
* so a listener can fill the content. A failed request is painted into the
|
|
7336
|
-
* content section.
|
|
7485
|
+
* content section and does not reject. On a drawer already open nothing is
|
|
7486
|
+
* shown or requested, except that one sliding out stops and stays open, and
|
|
7487
|
+
* the call waits for the same ending as the calls before it, which is how
|
|
7488
|
+
* a caller learns the outcome of the opening an `update()` made.
|
|
7489
|
+
* @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>} resolving on
|
|
7490
|
+
* close, or on removal from the document with a dismissal
|
|
7337
7491
|
*/
|
|
7338
|
-
|
|
7339
|
-
if (
|
|
7340
|
-
|
|
7492
|
+
ask() {
|
|
7493
|
+
if (this.#show()) {
|
|
7494
|
+
this.#sections.restChrome();
|
|
7495
|
+
this.#sections.request();
|
|
7341
7496
|
}
|
|
7342
|
-
|
|
7343
|
-
|
|
7497
|
+
return new Promise((resolve) => {
|
|
7498
|
+
this.#resolvers.push(resolve);
|
|
7499
|
+
});
|
|
7500
|
+
}
|
|
7501
|
+
/**
|
|
7502
|
+
* Dismisses the drawer as the user would: dispatches the cancelable
|
|
7503
|
+
* `drawer:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
|
|
7504
|
+
* asks before dropping unsaved edits, sliding the drawer out as a
|
|
7505
|
+
* dismissal unless either refused. Does nothing on a drawer that is closed
|
|
7506
|
+
* or already sliding out.
|
|
7507
|
+
* @param {string} reason why it is dismissed, such as `navigation`
|
|
7508
|
+
* @returns {Promise<boolean>} whether the drawer is closed or closing,
|
|
7509
|
+
* false when a listener or the user kept it open
|
|
7510
|
+
*/
|
|
7511
|
+
dismiss(reason) {
|
|
7512
|
+
if (this.#closing || !this.#dialog.open) {
|
|
7513
|
+
return Promise.resolve(true);
|
|
7514
|
+
}
|
|
7515
|
+
return this.#dismiss(reason, true);
|
|
7516
|
+
}
|
|
7517
|
+
/**
|
|
7518
|
+
* @param {string} reason
|
|
7519
|
+
* @param {boolean} cancelable false for a close the platform makes anyway
|
|
7520
|
+
* @returns {Promise<boolean>}
|
|
7521
|
+
*/
|
|
7522
|
+
#dismiss(reason, cancelable) {
|
|
7523
|
+
return this.#sections.dismiss(
|
|
7524
|
+
new CustomEvent('drawer:dismiss', { cancelable, detail: { reason } }),
|
|
7525
|
+
() => this.close(),
|
|
7526
|
+
this.declared('guard-dismiss') ? discardEdits : null,
|
|
7527
|
+
);
|
|
7344
7528
|
}
|
|
7345
7529
|
/**
|
|
7346
7530
|
* Slides the drawer out and closes it when the animation ends, setting the
|
|
7347
7531
|
* `closing` attribute on the native dialog meanwhile; the `close` event is
|
|
7348
7532
|
* dispatched then, after this call has returned. Where the user prefers
|
|
7349
|
-
* reduced motion, or no animation runs, it closes at once.
|
|
7350
|
-
*
|
|
7533
|
+
* reduced motion, or no animation runs, it closes at once. A non-empty
|
|
7534
|
+
* result answers as the button carrying that `data-result` would; no
|
|
7535
|
+
* result, or an empty one, is a dismissal. Does nothing on a drawer that is
|
|
7536
|
+
* closed or already sliding out.
|
|
7537
|
+
* @param {string} [result]
|
|
7351
7538
|
*/
|
|
7352
|
-
close() {
|
|
7539
|
+
close(result) {
|
|
7353
7540
|
if (this.#closing || !this.#dialog.open) {
|
|
7354
7541
|
return;
|
|
7355
7542
|
}
|
|
7543
|
+
if (result) {
|
|
7544
|
+
this.#answer = { dismissed: false, result, response: null };
|
|
7545
|
+
}
|
|
7356
7546
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
7357
7547
|
this.#dialog.close();
|
|
7358
7548
|
return;
|
|
@@ -8064,6 +8254,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8064
8254
|
'dialog.close': 'Close',
|
|
8065
8255
|
'dialog.confirm': 'Confirm',
|
|
8066
8256
|
'dialog.cancel': 'Cancel',
|
|
8257
|
+
'dialog.discard.header': 'Unsaved changes',
|
|
8258
|
+
'dialog.discard.body': 'There are unsaved changes: close anyway?',
|
|
8259
|
+
'dialog.discard.confirm': 'Close without saving',
|
|
8260
|
+
'dialog.discard.cancel': 'Keep editing',
|
|
8067
8261
|
'drawer.close': 'Close',
|
|
8068
8262
|
'spinner.loading': 'Loading…',
|
|
8069
8263
|
'toast.region': 'Notifications',
|
|
@@ -8109,6 +8303,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8109
8303
|
'dialog.close': 'Chiudi',
|
|
8110
8304
|
'dialog.confirm': 'Conferma',
|
|
8111
8305
|
'dialog.cancel': 'Annulla',
|
|
8306
|
+
'dialog.discard.header': 'Modifiche non salvate',
|
|
8307
|
+
'dialog.discard.body': 'Ci sono modifiche non salvate: chiudere comunque?',
|
|
8308
|
+
'dialog.discard.confirm': 'Chiudi senza salvare',
|
|
8309
|
+
'dialog.discard.cancel': 'Continua a modificare',
|
|
8112
8310
|
'drawer.close': 'Chiudi',
|
|
8113
8311
|
'spinner.loading': 'Caricamento…',
|
|
8114
8312
|
'toast.region': 'Notifiche',
|
|
@@ -8157,6 +8355,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8157
8355
|
'dialog.close': 'Cerrar',
|
|
8158
8356
|
'dialog.confirm': 'Confirmar',
|
|
8159
8357
|
'dialog.cancel': 'Cancelar',
|
|
8358
|
+
'dialog.discard.header': 'Cambios sin guardar',
|
|
8359
|
+
'dialog.discard.body': 'Hay cambios sin guardar: ¿cerrar de todos modos?',
|
|
8360
|
+
'dialog.discard.confirm': 'Cerrar sin guardar',
|
|
8361
|
+
'dialog.discard.cancel': 'Seguir editando',
|
|
8160
8362
|
'drawer.close': 'Cerrar',
|
|
8161
8363
|
'spinner.loading': 'Cargando…',
|
|
8162
8364
|
'toast.region': 'Notificaciones',
|
|
@@ -8205,6 +8407,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
|
|
|
8205
8407
|
'dialog.close': 'Fermer',
|
|
8206
8408
|
'dialog.confirm': 'Confirmer',
|
|
8207
8409
|
'dialog.cancel': 'Annuler',
|
|
8410
|
+
'dialog.discard.header': 'Modifications non enregistrées',
|
|
8411
|
+
'dialog.discard.body': 'Des modifications ne sont pas enregistrées : fermer quand même ?',
|
|
8412
|
+
'dialog.discard.confirm': 'Fermer sans enregistrer',
|
|
8413
|
+
'dialog.discard.cancel': 'Continuer à modifier',
|
|
8208
8414
|
'drawer.close': 'Fermer',
|
|
8209
8415
|
'spinner.loading': 'Chargement…',
|
|
8210
8416
|
'toast.region': 'Notifications',
|