@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/fml.iife.js
CHANGED
|
@@ -6963,6 +6963,11 @@ var fml = (function (exports) {
|
|
|
6963
6963
|
* `rest` being the remainder of the path (`''` on an exact match) that a
|
|
6964
6964
|
* composite can use to route the problem to an inner control.
|
|
6965
6965
|
*
|
|
6966
|
+
* A control counts only when it can show a problem, through a
|
|
6967
|
+
* `setCustomValidity` of its own: a form-associated element that has none is
|
|
6968
|
+
* skipped, so its problems fall to a shorter prefix or to the banner instead
|
|
6969
|
+
* of being dropped.
|
|
6970
|
+
*
|
|
6966
6971
|
* Every other problem, and a field problem naming no control, is shown in
|
|
6967
6972
|
* each `ful-errors` banner of the form: the banner gets `role="alert"`, is
|
|
6968
6973
|
* revealed and holds the reasons one per line. With no such problem the
|
|
@@ -7001,7 +7006,7 @@ var fml = (function (exports) {
|
|
|
7001
7006
|
const parts = name.split('.');
|
|
7002
7007
|
for (let i = parts.length; i !== 0; --i) {
|
|
7003
7008
|
const prefix = parts.slice(0, i).join('.');
|
|
7004
|
-
const targets = named.get(prefix) ?? [];
|
|
7009
|
+
const targets = (named.get(prefix) ?? []).filter((el) => typeof el.setCustomValidity === 'function');
|
|
7005
7010
|
if (targets.length === 0) {
|
|
7006
7011
|
continue;
|
|
7007
7012
|
}
|
|
@@ -7459,6 +7464,14 @@ var fml = (function (exports) {
|
|
|
7459
7464
|
* accessibility tree.
|
|
7460
7465
|
* - `also` are further controls that mirror `disabled` and `readOnly`
|
|
7461
7466
|
* beside the claim target.
|
|
7467
|
+
*
|
|
7468
|
+
* The pieces are elements the field rendered itself, native controls or
|
|
7469
|
+
* wrappers of its own. A ful field is not one: it has no `readOnly`, its
|
|
7470
|
+
* host's role takes no aria, and a label's `for` names its host rather than
|
|
7471
|
+
* its control. A composite built from ful fields answers the inner field as
|
|
7472
|
+
* `control`, which forwards `focus()`, lets it render its own label with
|
|
7473
|
+
* `label` and `announces` null, and forwards the claims in its setters,
|
|
7474
|
+
* calling super first.
|
|
7462
7475
|
* @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
|
|
7463
7476
|
* @returns {any} the pieces, or a promise of them
|
|
7464
7477
|
* @throws {Error} when the subclass does not implement it
|
|
@@ -7733,11 +7746,13 @@ var fml = (function (exports) {
|
|
|
7733
7746
|
* A call while a submit is in flight returns at once, doing nothing.
|
|
7734
7747
|
* @param {HTMLElement} [submitter] the button that submitted, if any; it is
|
|
7735
7748
|
* the only button whose name and value are submitted
|
|
7736
|
-
* @returns {Promise<
|
|
7749
|
+
* @returns {Promise<boolean>} settling once the submit has finished: true
|
|
7750
|
+
* after `submit:success`, false when it failed, was cancelled or was dropped
|
|
7751
|
+
* for one in flight
|
|
7737
7752
|
*/
|
|
7738
7753
|
async submit(submitter) {
|
|
7739
7754
|
if (this.#submitting) {
|
|
7740
|
-
return;
|
|
7755
|
+
return false;
|
|
7741
7756
|
}
|
|
7742
7757
|
this.#submitting = true;
|
|
7743
7758
|
this.spinner(true);
|
|
@@ -7754,7 +7769,7 @@ var fml = (function (exports) {
|
|
|
7754
7769
|
detail: { submitter, values, request },
|
|
7755
7770
|
});
|
|
7756
7771
|
if (!this.dispatchEvent(se)) {
|
|
7757
|
-
return;
|
|
7772
|
+
return false;
|
|
7758
7773
|
}
|
|
7759
7774
|
this.errors = [];
|
|
7760
7775
|
const sre = new CustomEvent('submit:requested', {
|
|
@@ -7777,6 +7792,7 @@ var fml = (function (exports) {
|
|
|
7777
7792
|
detail: { submitter, values, request, response: mapped },
|
|
7778
7793
|
}),
|
|
7779
7794
|
);
|
|
7795
|
+
return true;
|
|
7780
7796
|
} catch (e) {
|
|
7781
7797
|
this.dispatchEvent(
|
|
7782
7798
|
new CustomEvent('submit:failure', {
|
|
@@ -7790,11 +7806,27 @@ var fml = (function (exports) {
|
|
|
7790
7806
|
} else {
|
|
7791
7807
|
console.warn('failed to submit form', this, 'reason:', e);
|
|
7792
7808
|
}
|
|
7809
|
+
return false;
|
|
7793
7810
|
} finally {
|
|
7794
7811
|
this.#submitting = false;
|
|
7795
7812
|
this.spinner(false);
|
|
7796
7813
|
}
|
|
7797
7814
|
}
|
|
7815
|
+
/**
|
|
7816
|
+
* Saves the unsaved edits, for a page action that would lose them, such as
|
|
7817
|
+
* one reloading the page: under `track-changes` it submits only while the
|
|
7818
|
+
* form is dirty, and without it always submits.
|
|
7819
|
+
* @param {HTMLElement} [submitter] as `submit` takes it
|
|
7820
|
+
* @returns {Promise<boolean>} whether no unsaved edit is left: false when
|
|
7821
|
+
* the submit did not succeed, or when the user edited the form while it
|
|
7822
|
+
* was in flight
|
|
7823
|
+
*/
|
|
7824
|
+
async save(submitter) {
|
|
7825
|
+
if (this.#tracking && !this.dirty) {
|
|
7826
|
+
return true;
|
|
7827
|
+
}
|
|
7828
|
+
return (await this.submit(submitter)) && !this.dirty;
|
|
7829
|
+
}
|
|
7798
7830
|
/**
|
|
7799
7831
|
* Resets the native form, each field returning to its default value. Under
|
|
7800
7832
|
* `track-changes` every control counts as edited: the form is dirty if the
|
|
@@ -7867,12 +7899,14 @@ var fml = (function (exports) {
|
|
|
7867
7899
|
}
|
|
7868
7900
|
}
|
|
7869
7901
|
/**
|
|
7870
|
-
* Counts
|
|
7902
|
+
* Counts controls as edited by the user, for values the page writes on
|
|
7871
7903
|
* the user's behalf: assigning `value` reports nothing, as 0019 wants.
|
|
7872
|
-
* @param {Element}
|
|
7904
|
+
* @param {...Element} els each a control, or an element inside one
|
|
7873
7905
|
*/
|
|
7874
|
-
edited(
|
|
7875
|
-
|
|
7906
|
+
edited(...els) {
|
|
7907
|
+
for (const el of els) {
|
|
7908
|
+
this.#touch(el);
|
|
7909
|
+
}
|
|
7876
7910
|
}
|
|
7877
7911
|
/**
|
|
7878
7912
|
* @param {Map<string, string>} saved
|
|
@@ -10825,7 +10859,7 @@ var fml = (function (exports) {
|
|
|
10825
10859
|
return;
|
|
10826
10860
|
}
|
|
10827
10861
|
/** @type {HTMLInputElement|null} */
|
|
10828
|
-
const el = this.querySelector(`input[type=radio][value
|
|
10862
|
+
const el = this.querySelector(`input[type=radio][value="${CSS.escape(String(value))}"]`);
|
|
10829
10863
|
if (el === null) {
|
|
10830
10864
|
clear();
|
|
10831
10865
|
return;
|
|
@@ -12974,6 +13008,8 @@ var fml = (function (exports) {
|
|
|
12974
13008
|
#error;
|
|
12975
13009
|
#requests = new SectionRequests();
|
|
12976
13010
|
#updates = new Claims();
|
|
13011
|
+
/** @type {Promise<boolean>|null} */
|
|
13012
|
+
#confirming = null;
|
|
12977
13013
|
/**
|
|
12978
13014
|
* @param {HTMLElement} host the element the section requests are dispatched on
|
|
12979
13015
|
* @param {DocumentFragment} fragment the rendered template, holding a
|
|
@@ -12997,17 +13033,53 @@ var fml = (function (exports) {
|
|
|
12997
13033
|
get section() {
|
|
12998
13034
|
return this.#section;
|
|
12999
13035
|
}
|
|
13036
|
+
/**
|
|
13037
|
+
* Whether a `ful-form` of this dialog, not one of a dialog nested in it,
|
|
13038
|
+
* holds unsaved edits (see `Form.dirty`).
|
|
13039
|
+
* @returns {boolean}
|
|
13040
|
+
*/
|
|
13041
|
+
get dirty() {
|
|
13042
|
+
return Array.from(this.#dialog.querySelectorAll('ful-form')).some(
|
|
13043
|
+
(form) => form.closest('dialog') === this.#dialog && /** @type {any} */ (form).dirty,
|
|
13044
|
+
);
|
|
13045
|
+
}
|
|
13000
13046
|
/**
|
|
13001
13047
|
* Dispatches the dismissal on the host and calls `close` unless a listener
|
|
13002
|
-
* prevented it
|
|
13003
|
-
*
|
|
13048
|
+
* prevented it or, with `confirm` given and the dialog `dirty`, `confirm`
|
|
13049
|
+
* answers false. A dismissal that is not cancelable announces a close the
|
|
13050
|
+
* platform makes whatever the page does, so nothing is asked and `close` is
|
|
13051
|
+
* not called for it. While `confirm` is pending a further dismissal waits
|
|
13052
|
+
* for the same answer, and a dialog closed meanwhile is not closed again.
|
|
13004
13053
|
* @param {CustomEvent} asked
|
|
13005
13054
|
* @param {() => void} close
|
|
13055
|
+
* @param {(() => Promise<boolean>)|null} confirm asks whether to drop the unsaved edits
|
|
13056
|
+
* @returns {Promise<boolean>} whether the dismissal closes the dialog
|
|
13006
13057
|
*/
|
|
13007
|
-
dismiss(asked, close) {
|
|
13008
|
-
if (this.#
|
|
13058
|
+
dismiss(asked, close, confirm) {
|
|
13059
|
+
if (this.#confirming) {
|
|
13060
|
+
return this.#confirming;
|
|
13061
|
+
}
|
|
13062
|
+
if (!this.#host.dispatchEvent(asked)) {
|
|
13063
|
+
return Promise.resolve(false);
|
|
13064
|
+
}
|
|
13065
|
+
if (!asked.cancelable) {
|
|
13066
|
+
return Promise.resolve(true);
|
|
13067
|
+
}
|
|
13068
|
+
if (!confirm || !this.dirty) {
|
|
13009
13069
|
close();
|
|
13070
|
+
return Promise.resolve(true);
|
|
13010
13071
|
}
|
|
13072
|
+
this.#confirming = confirm()
|
|
13073
|
+
.then((drop) => {
|
|
13074
|
+
if (drop && this.#dialog.open) {
|
|
13075
|
+
close();
|
|
13076
|
+
}
|
|
13077
|
+
return drop;
|
|
13078
|
+
})
|
|
13079
|
+
.finally(() => {
|
|
13080
|
+
this.#confirming = null;
|
|
13081
|
+
});
|
|
13082
|
+
return this.#confirming;
|
|
13011
13083
|
}
|
|
13012
13084
|
/**
|
|
13013
13085
|
* Calls `dismiss` when a press and its release both land on the backdrop,
|
|
@@ -13061,11 +13133,12 @@ var fml = (function (exports) {
|
|
|
13061
13133
|
* `show`, and fills the content with what the callback resolves to. A
|
|
13062
13134
|
* rejection is shown, as the problems' reasons, in the error section, and
|
|
13063
13135
|
* the content is hidden. When a newer `update()` starts before the callback
|
|
13064
|
-
* settles, this call paints nothing
|
|
13065
|
-
*
|
|
13136
|
+
* settles, this call paints nothing and resolves `null`, the section being
|
|
13137
|
+
* the newer call's; it still rejects with the callback's error.
|
|
13066
13138
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
13067
13139
|
* @param {() => void} show opens the dialog when it is closed
|
|
13068
|
-
* @returns {Promise<HTMLElement>} the content section
|
|
13140
|
+
* @returns {Promise<HTMLElement|null>} the content section, or `null` for a
|
|
13141
|
+
* call a newer one overtook
|
|
13069
13142
|
* @throws {any} what the callback threw or rejected with
|
|
13070
13143
|
*/
|
|
13071
13144
|
async update(cb, show) {
|
|
@@ -13078,7 +13151,7 @@ var fml = (function (exports) {
|
|
|
13078
13151
|
try {
|
|
13079
13152
|
const delivered = await cb();
|
|
13080
13153
|
if (claim.stale) {
|
|
13081
|
-
return
|
|
13154
|
+
return null;
|
|
13082
13155
|
}
|
|
13083
13156
|
this.#section.replaceChildren(delivered);
|
|
13084
13157
|
this.#loading?.setAttribute('hidden', '');
|
|
@@ -13147,21 +13220,24 @@ var fml = (function (exports) {
|
|
|
13147
13220
|
* and a press and release both on the backdrop close the dialog, each as a
|
|
13148
13221
|
* dismissal, as Escape does.
|
|
13149
13222
|
*
|
|
13150
|
-
*
|
|
13151
|
-
*
|
|
13152
|
-
*
|
|
13153
|
-
* `
|
|
13154
|
-
*
|
|
13155
|
-
*
|
|
13223
|
+
* Those three gestures go through `dismiss(reason)`, the reason being
|
|
13224
|
+
* `button`, `backdrop` or `escape`: before it closes the dialog, the element
|
|
13225
|
+
* dispatches a cancelable, non-bubbling `dialog:dismiss` with
|
|
13226
|
+
* `detail: { reason }`, and a listener calling `preventDefault()` keeps it
|
|
13227
|
+
* open. Under `guard-dismiss`, a dismissal nobody refused, while a `ful-form`
|
|
13228
|
+
* of the dialog holds unsaved edits (see `Form.dirty`), first asks the user,
|
|
13229
|
+
* through `Dialog.confirm`, whether to close and drop them. A `data-result`
|
|
13230
|
+
* button, `close-on-submit`, `close()` and `update()` are answers or the page
|
|
13231
|
+
* deciding: they dispatch nothing and ask nothing.
|
|
13156
13232
|
*
|
|
13157
13233
|
* The platform lets a page refuse Escape once per user interaction: a second
|
|
13158
13234
|
* Escape with no click or keystroke in between closes the dialog whatever the
|
|
13159
|
-
* page does, `requires-answer` included. That close is
|
|
13160
|
-
* `dialog:dismiss`, which is then not cancelable.
|
|
13235
|
+
* page does, `requires-answer` and `guard-dismiss` included. That close is
|
|
13236
|
+
* still announced by a `dialog:dismiss`, which is then not cancelable.
|
|
13161
13237
|
*
|
|
13162
13238
|
* `requires-answer` withholds the close button and refuses Escape and the
|
|
13163
13239
|
* backdrop, so the dialog is closed only by a button carrying `data-result`
|
|
13164
|
-
* or from code through `close()`.
|
|
13240
|
+
* or from code, through `close()` or `dismiss()`.
|
|
13165
13241
|
*
|
|
13166
13242
|
* `close-on-submit` closes the dialog when a `ful-form` that is a direct child
|
|
13167
13243
|
* of the body dispatches `submit:success`, answering with its
|
|
@@ -13181,7 +13257,7 @@ var fml = (function (exports) {
|
|
|
13181
13257
|
* in a form needs.
|
|
13182
13258
|
*/
|
|
13183
13259
|
class Dialog extends ParsedElement {
|
|
13184
|
-
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
|
|
13260
|
+
static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence', 'guard-dismiss:presence'];
|
|
13185
13261
|
static slots = true;
|
|
13186
13262
|
static template = `
|
|
13187
13263
|
<dialog data-ref="dialog" class="ful-dialog">
|
|
@@ -13231,15 +13307,10 @@ var fml = (function (exports) {
|
|
|
13231
13307
|
this.#dialog.close(result);
|
|
13232
13308
|
}
|
|
13233
13309
|
});
|
|
13234
|
-
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
13235
|
-
this.#sections.dismiss(
|
|
13236
|
-
new CustomEvent('dialog:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
13237
|
-
() => this.#dialog.close(''),
|
|
13238
|
-
);
|
|
13239
13310
|
if (!requiresAnswer) {
|
|
13240
|
-
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
13311
|
+
this.#sections.onBackdrop(() => this.dismiss('backdrop'));
|
|
13241
13312
|
}
|
|
13242
|
-
fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
|
|
13313
|
+
fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => this.dismiss('button'));
|
|
13243
13314
|
if (closeOnSubmit) {
|
|
13244
13315
|
this.#sections.onSubmitted((response) => {
|
|
13245
13316
|
this.#answer = { dismissed: false, result: null, response };
|
|
@@ -13249,12 +13320,12 @@ var fml = (function (exports) {
|
|
|
13249
13320
|
this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
|
|
13250
13321
|
if (!e.cancelable) {
|
|
13251
13322
|
this.#dialog.returnValue = '';
|
|
13252
|
-
dismiss('escape',
|
|
13323
|
+
this.#dismiss('escape', false);
|
|
13253
13324
|
return;
|
|
13254
13325
|
}
|
|
13255
13326
|
e.preventDefault();
|
|
13256
13327
|
if (!requiresAnswer) {
|
|
13257
|
-
dismiss('escape');
|
|
13328
|
+
this.dismiss('escape');
|
|
13258
13329
|
}
|
|
13259
13330
|
});
|
|
13260
13331
|
this.replaceChildren(fragment);
|
|
@@ -13326,11 +13397,12 @@ var fml = (function (exports) {
|
|
|
13326
13397
|
*
|
|
13327
13398
|
* No `section:requested` is dispatched, and no `ask()` waiter is added:
|
|
13328
13399
|
* listen for the `close` event to learn how the dialog ended. When a newer
|
|
13329
|
-
* `update()` starts before the callback settles, this call paints nothing
|
|
13330
|
-
*
|
|
13331
|
-
* callback's error.
|
|
13400
|
+
* `update()` starts before the callback settles, this call paints nothing
|
|
13401
|
+
* and resolves `null`, the body being the newer call's; it still rejects
|
|
13402
|
+
* with the callback's error.
|
|
13332
13403
|
* @param {() => Node|string|Promise<Node|string>} cb producing the body content
|
|
13333
|
-
* @returns {Promise<Element>} the body section, in the document
|
|
13404
|
+
* @returns {Promise<Element|null>} the body section, in the document, or
|
|
13405
|
+
* `null` for a call a newer one overtook
|
|
13334
13406
|
* @throws {any} what the callback threw or rejected with
|
|
13335
13407
|
*/
|
|
13336
13408
|
async update(cb) {
|
|
@@ -13346,6 +13418,34 @@ var fml = (function (exports) {
|
|
|
13346
13418
|
refresh() {
|
|
13347
13419
|
return this.#sections.request();
|
|
13348
13420
|
}
|
|
13421
|
+
/**
|
|
13422
|
+
* Dismisses the dialog as the user would: dispatches the cancelable
|
|
13423
|
+
* `dialog:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
|
|
13424
|
+
* asks before dropping unsaved edits, closing the dialog as a dismissal
|
|
13425
|
+
* unless either refused. `requires-answer` does not refuse it, as it does
|
|
13426
|
+
* not refuse `close()`. Does nothing on a closed dialog.
|
|
13427
|
+
* @param {string} reason why it is dismissed, such as `navigation`
|
|
13428
|
+
* @returns {Promise<boolean>} whether the dialog is closed, false when a
|
|
13429
|
+
* listener or the user kept it open
|
|
13430
|
+
*/
|
|
13431
|
+
dismiss(reason) {
|
|
13432
|
+
if (!this.#dialog.open) {
|
|
13433
|
+
return Promise.resolve(true);
|
|
13434
|
+
}
|
|
13435
|
+
return this.#dismiss(reason, true);
|
|
13436
|
+
}
|
|
13437
|
+
/**
|
|
13438
|
+
* @param {string} reason
|
|
13439
|
+
* @param {boolean} cancelable false for a close the platform makes anyway
|
|
13440
|
+
* @returns {Promise<boolean>}
|
|
13441
|
+
*/
|
|
13442
|
+
#dismiss(reason, cancelable) {
|
|
13443
|
+
return this.#sections.dismiss(
|
|
13444
|
+
new CustomEvent('dialog:dismiss', { cancelable, detail: { reason } }),
|
|
13445
|
+
() => this.#dialog.close(''),
|
|
13446
|
+
this.declared('guard-dismiss') ? discardEdits : null,
|
|
13447
|
+
);
|
|
13448
|
+
}
|
|
13349
13449
|
/**
|
|
13350
13450
|
* Closes an open dialog, doing nothing on a closed one. A non-empty result
|
|
13351
13451
|
* answers as the button carrying that `data-result` would; no result, or
|
|
@@ -13445,6 +13545,19 @@ var fml = (function (exports) {
|
|
|
13445
13545
|
}
|
|
13446
13546
|
}
|
|
13447
13547
|
|
|
13548
|
+
/**
|
|
13549
|
+
* Asks, through `Dialog.confirm`, whether to close and drop the unsaved edits,
|
|
13550
|
+
* with the localized `dialog.discard.*` texts.
|
|
13551
|
+
* @returns {Promise<boolean>} true to drop them
|
|
13552
|
+
*/
|
|
13553
|
+
const discardEdits = () => {
|
|
13554
|
+
const l10n = Localization.of();
|
|
13555
|
+
return Dialog.confirm(l10n.t('dialog.discard.header'), l10n.t('dialog.discard.body'), {
|
|
13556
|
+
confirm: l10n.t('dialog.discard.confirm'),
|
|
13557
|
+
cancel: l10n.t('dialog.discard.cancel'),
|
|
13558
|
+
});
|
|
13559
|
+
};
|
|
13560
|
+
|
|
13448
13561
|
/**
|
|
13449
13562
|
* A side panel on the native `<dialog>`, shown as a modal: a header holding
|
|
13450
13563
|
* the title and a localized close button, then loading, error and content
|
|
@@ -13457,17 +13570,23 @@ var fml = (function (exports) {
|
|
|
13457
13570
|
* default, when absent) or `start`, following the writing direction.
|
|
13458
13571
|
*
|
|
13459
13572
|
* The close button, Escape, and a press and release both on the backdrop all
|
|
13460
|
-
* go through `
|
|
13461
|
-
* `
|
|
13462
|
-
*
|
|
13463
|
-
*
|
|
13464
|
-
*
|
|
13573
|
+
* go through `dismiss(reason)`, the reason being `button`, `backdrop` or
|
|
13574
|
+
* `escape`: before it closes the drawer, the element dispatches a cancelable,
|
|
13575
|
+
* non-bubbling `drawer:dismiss` with `detail: { reason }`, and a listener
|
|
13576
|
+
* calling `preventDefault()` keeps it open. Under `guard-dismiss`, a dismissal
|
|
13577
|
+
* nobody refused, while a `ful-form` of the drawer holds unsaved edits (see
|
|
13578
|
+
* `Form.dirty`), first asks the user, through `Dialog.confirm`, whether to
|
|
13579
|
+
* close and drop them. A `data-result` button, `close-on-submit`, `close()`
|
|
13580
|
+
* and `update()` are answers or the page deciding: they dispatch nothing and
|
|
13581
|
+
* ask nothing. The platform lets a page refuse Escape once per user
|
|
13465
13582
|
* interaction: a second Escape with no click or keystroke in between closes
|
|
13466
|
-
* the drawer at once, without the slide,
|
|
13467
|
-
*
|
|
13468
|
-
*
|
|
13469
|
-
*
|
|
13470
|
-
*
|
|
13583
|
+
* the drawer at once, without the slide, `guard-dismiss` included, and its
|
|
13584
|
+
* `drawer:dismiss` is then not cancelable.
|
|
13585
|
+
*
|
|
13586
|
+
* Any button inside the drawer carrying `data-result` closes it with that
|
|
13587
|
+
* result, an empty one being a dismissal. Every close dispatches a
|
|
13588
|
+
* non-bubbling `close` event on the element carrying the `DialogOutcome` as
|
|
13589
|
+
* `detail`, before the pending `ask()` calls resolve.
|
|
13471
13590
|
*
|
|
13472
13591
|
* `close-on-submit` closes the drawer when a `ful-form` that is a direct child
|
|
13473
13592
|
* of the content section dispatches `submit:success`, carrying its
|
|
@@ -13479,7 +13598,7 @@ var fml = (function (exports) {
|
|
|
13479
13598
|
* element's id calls `open()` on click.
|
|
13480
13599
|
*/
|
|
13481
13600
|
class Drawer extends ParsedElement {
|
|
13482
|
-
static attributes = ['header', 'placement', 'close-on-submit:presence'];
|
|
13601
|
+
static attributes = ['header', 'placement', 'close-on-submit:presence', 'guard-dismiss:presence'];
|
|
13483
13602
|
static slots = true;
|
|
13484
13603
|
static template = `
|
|
13485
13604
|
<dialog data-ref="dialog" class="ful-drawer">
|
|
@@ -13496,8 +13615,9 @@ var fml = (function (exports) {
|
|
|
13496
13615
|
#dialog;
|
|
13497
13616
|
#title;
|
|
13498
13617
|
#sections;
|
|
13499
|
-
/** @type {{ dismissed: boolean, response: any }|null} */
|
|
13618
|
+
/** @type {{ dismissed: boolean, result: string|null, response: any }|null} */
|
|
13500
13619
|
#answer = null;
|
|
13620
|
+
#resolvers = [];
|
|
13501
13621
|
#closing = false;
|
|
13502
13622
|
#slideOutEnded = (/** @type AnimationEvent */ e) => {
|
|
13503
13623
|
if (e.target === this.#dialog) {
|
|
@@ -13520,32 +13640,33 @@ var fml = (function (exports) {
|
|
|
13520
13640
|
if (placement) {
|
|
13521
13641
|
this.#dialog.setAttribute('placement', placement);
|
|
13522
13642
|
}
|
|
13523
|
-
const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
|
|
13524
|
-
this.#sections.dismiss(
|
|
13525
|
-
new CustomEvent('drawer:dismiss', { cancelable: !platformCloses, detail: { reason } }),
|
|
13526
|
-
() => this.close(),
|
|
13527
|
-
);
|
|
13528
13643
|
/** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
|
|
13529
|
-
dismiss('button'),
|
|
13644
|
+
this.dismiss('button'),
|
|
13530
13645
|
);
|
|
13531
|
-
this.#sections.onBackdrop(() => dismiss('backdrop'));
|
|
13646
|
+
this.#sections.onBackdrop(() => this.dismiss('backdrop'));
|
|
13532
13647
|
this.#dialog.addEventListener('cancel', (e) => {
|
|
13533
13648
|
if (!e.cancelable) {
|
|
13534
13649
|
this.#stopSlidingOut();
|
|
13535
|
-
dismiss('escape',
|
|
13650
|
+
this.#dismiss('escape', false);
|
|
13536
13651
|
return;
|
|
13537
13652
|
}
|
|
13538
13653
|
e.preventDefault();
|
|
13539
|
-
dismiss('escape');
|
|
13654
|
+
this.dismiss('escape');
|
|
13655
|
+
});
|
|
13656
|
+
this.#dialog.addEventListener('click', (/** @type any */ e) => {
|
|
13657
|
+
const result = e.target.closest('button[data-result]')?.dataset.result;
|
|
13658
|
+
if (result !== undefined) {
|
|
13659
|
+
this.close(result);
|
|
13660
|
+
}
|
|
13540
13661
|
});
|
|
13541
13662
|
this.#dialog.addEventListener('close', () => {
|
|
13542
|
-
this
|
|
13543
|
-
|
|
13544
|
-
);
|
|
13663
|
+
const outcome = this.#outcome();
|
|
13664
|
+
this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
|
|
13665
|
+
this.#settle(outcome);
|
|
13545
13666
|
});
|
|
13546
13667
|
if (this.declared('close-on-submit')) {
|
|
13547
13668
|
this.#sections.onSubmitted((response) => {
|
|
13548
|
-
this.#answer = { dismissed: false, response };
|
|
13669
|
+
this.#answer = { dismissed: false, result: null, response };
|
|
13549
13670
|
this.close();
|
|
13550
13671
|
});
|
|
13551
13672
|
}
|
|
@@ -13571,12 +13692,13 @@ var fml = (function (exports) {
|
|
|
13571
13692
|
* which has `role="alert"`, and the content is hidden.
|
|
13572
13693
|
*
|
|
13573
13694
|
* No `section:requested` is dispatched. When a newer `update()` starts
|
|
13574
|
-
* before the callback settles, this call paints nothing
|
|
13575
|
-
* the content section
|
|
13576
|
-
* error.
|
|
13695
|
+
* before the callback settles, this call paints nothing and resolves
|
|
13696
|
+
* `null`, the content section being the newer call's; it still rejects with
|
|
13697
|
+
* the callback's error.
|
|
13577
13698
|
* @param {string} header the title, set as text
|
|
13578
13699
|
* @param {() => Node|string|Promise<Node|string>} cb producing the content
|
|
13579
|
-
* @returns {Promise<Element>} the content section, in the document
|
|
13700
|
+
* @returns {Promise<Element|null>} the content section, in the document, or
|
|
13701
|
+
* `null` for a call a newer one overtook
|
|
13580
13702
|
* @throws {any} what the callback threw or rejected with
|
|
13581
13703
|
*/
|
|
13582
13704
|
async update(header, cb) {
|
|
@@ -13593,9 +13715,36 @@ var fml = (function (exports) {
|
|
|
13593
13715
|
refresh() {
|
|
13594
13716
|
return this.#sections.request();
|
|
13595
13717
|
}
|
|
13718
|
+
/** @returns {{ dismissed: boolean, result: string|null, response: any }} */
|
|
13719
|
+
#outcome() {
|
|
13720
|
+
return this.#answer ?? { dismissed: true, result: null, response: null };
|
|
13721
|
+
}
|
|
13722
|
+
/** @param {{ dismissed: boolean, result: string|null, response: any }} outcome */
|
|
13723
|
+
#settle(outcome) {
|
|
13724
|
+
const resolvers = this.#resolvers;
|
|
13725
|
+
this.#resolvers = [];
|
|
13726
|
+
for (const resolve of resolvers) {
|
|
13727
|
+
resolve(outcome);
|
|
13728
|
+
}
|
|
13729
|
+
}
|
|
13596
13730
|
/**
|
|
13597
|
-
*
|
|
13598
|
-
*
|
|
13731
|
+
* Resolves every pending `ask()` and `open()` with the outcome so far,
|
|
13732
|
+
* a dismissal for a drawer still open, so that no caller waits on a drawer
|
|
13733
|
+
* that left the document. The native dialog is not closed and no `close`
|
|
13734
|
+
* event is dispatched.
|
|
13735
|
+
*/
|
|
13736
|
+
disconnectedCallback() {
|
|
13737
|
+
this.#settle(this.#outcome());
|
|
13738
|
+
}
|
|
13739
|
+
/**
|
|
13740
|
+
* The same as `ask()`.
|
|
13741
|
+
* @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>}
|
|
13742
|
+
*/
|
|
13743
|
+
open() {
|
|
13744
|
+
return this.ask();
|
|
13745
|
+
}
|
|
13746
|
+
/**
|
|
13747
|
+
* Shows the drawer as a modal and resolves with how it ends.
|
|
13599
13748
|
*
|
|
13600
13749
|
* The call that opens the drawer resets the outcome of the previous
|
|
13601
13750
|
* opening, hides the error and loading sections, and dispatches
|
|
@@ -13603,26 +13752,67 @@ var fml = (function (exports) {
|
|
|
13603
13752
|
* `detail: { section, first, name: null, index: null }`, `section` being
|
|
13604
13753
|
* the content section and `first` true only on the first answered request),
|
|
13605
13754
|
* so a listener can fill the content. A failed request is painted into the
|
|
13606
|
-
* content section.
|
|
13755
|
+
* content section and does not reject. On a drawer already open nothing is
|
|
13756
|
+
* shown or requested, except that one sliding out stops and stays open, and
|
|
13757
|
+
* the call waits for the same ending as the calls before it, which is how
|
|
13758
|
+
* a caller learns the outcome of the opening an `update()` made.
|
|
13759
|
+
* @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>} resolving on
|
|
13760
|
+
* close, or on removal from the document with a dismissal
|
|
13607
13761
|
*/
|
|
13608
|
-
|
|
13609
|
-
if (
|
|
13610
|
-
|
|
13762
|
+
ask() {
|
|
13763
|
+
if (this.#show()) {
|
|
13764
|
+
this.#sections.restChrome();
|
|
13765
|
+
this.#sections.request();
|
|
13611
13766
|
}
|
|
13612
|
-
|
|
13613
|
-
|
|
13767
|
+
return new Promise((resolve) => {
|
|
13768
|
+
this.#resolvers.push(resolve);
|
|
13769
|
+
});
|
|
13770
|
+
}
|
|
13771
|
+
/**
|
|
13772
|
+
* Dismisses the drawer as the user would: dispatches the cancelable
|
|
13773
|
+
* `drawer:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
|
|
13774
|
+
* asks before dropping unsaved edits, sliding the drawer out as a
|
|
13775
|
+
* dismissal unless either refused. Does nothing on a drawer that is closed
|
|
13776
|
+
* or already sliding out.
|
|
13777
|
+
* @param {string} reason why it is dismissed, such as `navigation`
|
|
13778
|
+
* @returns {Promise<boolean>} whether the drawer is closed or closing,
|
|
13779
|
+
* false when a listener or the user kept it open
|
|
13780
|
+
*/
|
|
13781
|
+
dismiss(reason) {
|
|
13782
|
+
if (this.#closing || !this.#dialog.open) {
|
|
13783
|
+
return Promise.resolve(true);
|
|
13784
|
+
}
|
|
13785
|
+
return this.#dismiss(reason, true);
|
|
13786
|
+
}
|
|
13787
|
+
/**
|
|
13788
|
+
* @param {string} reason
|
|
13789
|
+
* @param {boolean} cancelable false for a close the platform makes anyway
|
|
13790
|
+
* @returns {Promise<boolean>}
|
|
13791
|
+
*/
|
|
13792
|
+
#dismiss(reason, cancelable) {
|
|
13793
|
+
return this.#sections.dismiss(
|
|
13794
|
+
new CustomEvent('drawer:dismiss', { cancelable, detail: { reason } }),
|
|
13795
|
+
() => this.close(),
|
|
13796
|
+
this.declared('guard-dismiss') ? discardEdits : null,
|
|
13797
|
+
);
|
|
13614
13798
|
}
|
|
13615
13799
|
/**
|
|
13616
13800
|
* Slides the drawer out and closes it when the animation ends, setting the
|
|
13617
13801
|
* `closing` attribute on the native dialog meanwhile; the `close` event is
|
|
13618
13802
|
* dispatched then, after this call has returned. Where the user prefers
|
|
13619
|
-
* reduced motion, or no animation runs, it closes at once.
|
|
13620
|
-
*
|
|
13803
|
+
* reduced motion, or no animation runs, it closes at once. A non-empty
|
|
13804
|
+
* result answers as the button carrying that `data-result` would; no
|
|
13805
|
+
* result, or an empty one, is a dismissal. Does nothing on a drawer that is
|
|
13806
|
+
* closed or already sliding out.
|
|
13807
|
+
* @param {string} [result]
|
|
13621
13808
|
*/
|
|
13622
|
-
close() {
|
|
13809
|
+
close(result) {
|
|
13623
13810
|
if (this.#closing || !this.#dialog.open) {
|
|
13624
13811
|
return;
|
|
13625
13812
|
}
|
|
13813
|
+
if (result) {
|
|
13814
|
+
this.#answer = { dismissed: false, result, response: null };
|
|
13815
|
+
}
|
|
13626
13816
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
13627
13817
|
this.#dialog.close();
|
|
13628
13818
|
return;
|
|
@@ -14334,6 +14524,10 @@ var fml = (function (exports) {
|
|
|
14334
14524
|
'dialog.close': 'Close',
|
|
14335
14525
|
'dialog.confirm': 'Confirm',
|
|
14336
14526
|
'dialog.cancel': 'Cancel',
|
|
14527
|
+
'dialog.discard.header': 'Unsaved changes',
|
|
14528
|
+
'dialog.discard.body': 'There are unsaved changes: close anyway?',
|
|
14529
|
+
'dialog.discard.confirm': 'Close without saving',
|
|
14530
|
+
'dialog.discard.cancel': 'Keep editing',
|
|
14337
14531
|
'drawer.close': 'Close',
|
|
14338
14532
|
'spinner.loading': 'Loading…',
|
|
14339
14533
|
'toast.region': 'Notifications',
|
|
@@ -14379,6 +14573,10 @@ var fml = (function (exports) {
|
|
|
14379
14573
|
'dialog.close': 'Chiudi',
|
|
14380
14574
|
'dialog.confirm': 'Conferma',
|
|
14381
14575
|
'dialog.cancel': 'Annulla',
|
|
14576
|
+
'dialog.discard.header': 'Modifiche non salvate',
|
|
14577
|
+
'dialog.discard.body': 'Ci sono modifiche non salvate: chiudere comunque?',
|
|
14578
|
+
'dialog.discard.confirm': 'Chiudi senza salvare',
|
|
14579
|
+
'dialog.discard.cancel': 'Continua a modificare',
|
|
14382
14580
|
'drawer.close': 'Chiudi',
|
|
14383
14581
|
'spinner.loading': 'Caricamento…',
|
|
14384
14582
|
'toast.region': 'Notifiche',
|
|
@@ -14427,6 +14625,10 @@ var fml = (function (exports) {
|
|
|
14427
14625
|
'dialog.close': 'Cerrar',
|
|
14428
14626
|
'dialog.confirm': 'Confirmar',
|
|
14429
14627
|
'dialog.cancel': 'Cancelar',
|
|
14628
|
+
'dialog.discard.header': 'Cambios sin guardar',
|
|
14629
|
+
'dialog.discard.body': 'Hay cambios sin guardar: ¿cerrar de todos modos?',
|
|
14630
|
+
'dialog.discard.confirm': 'Cerrar sin guardar',
|
|
14631
|
+
'dialog.discard.cancel': 'Seguir editando',
|
|
14430
14632
|
'drawer.close': 'Cerrar',
|
|
14431
14633
|
'spinner.loading': 'Cargando…',
|
|
14432
14634
|
'toast.region': 'Notificaciones',
|
|
@@ -14475,6 +14677,10 @@ var fml = (function (exports) {
|
|
|
14475
14677
|
'dialog.close': 'Fermer',
|
|
14476
14678
|
'dialog.confirm': 'Confirmer',
|
|
14477
14679
|
'dialog.cancel': 'Annuler',
|
|
14680
|
+
'dialog.discard.header': 'Modifications non enregistrées',
|
|
14681
|
+
'dialog.discard.body': 'Des modifications ne sont pas enregistrées : fermer quand même ?',
|
|
14682
|
+
'dialog.discard.confirm': 'Fermer sans enregistrer',
|
|
14683
|
+
'dialog.discard.cancel': 'Continuer à modifier',
|
|
14478
14684
|
'drawer.close': 'Fermer',
|
|
14479
14685
|
'spinner.loading': 'Chargement…',
|
|
14480
14686
|
'toast.region': 'Notifications',
|