@optionfactory/fml 9.0.0-rc22 → 9.0.0-rc23

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/fml.iife.js CHANGED
@@ -7733,11 +7733,13 @@ var fml = (function (exports) {
7733
7733
  * A call while a submit is in flight returns at once, doing nothing.
7734
7734
  * @param {HTMLElement} [submitter] the button that submitted, if any; it is
7735
7735
  * the only button whose name and value are submitted
7736
- * @returns {Promise<void>} settling once the submit has finished, whatever its outcome
7736
+ * @returns {Promise<boolean>} settling once the submit has finished: true
7737
+ * after `submit:success`, false when it failed, was cancelled or was dropped
7738
+ * for one in flight
7737
7739
  */
7738
7740
  async submit(submitter) {
7739
7741
  if (this.#submitting) {
7740
- return;
7742
+ return false;
7741
7743
  }
7742
7744
  this.#submitting = true;
7743
7745
  this.spinner(true);
@@ -7754,7 +7756,7 @@ var fml = (function (exports) {
7754
7756
  detail: { submitter, values, request },
7755
7757
  });
7756
7758
  if (!this.dispatchEvent(se)) {
7757
- return;
7759
+ return false;
7758
7760
  }
7759
7761
  this.errors = [];
7760
7762
  const sre = new CustomEvent('submit:requested', {
@@ -7777,6 +7779,7 @@ var fml = (function (exports) {
7777
7779
  detail: { submitter, values, request, response: mapped },
7778
7780
  }),
7779
7781
  );
7782
+ return true;
7780
7783
  } catch (e) {
7781
7784
  this.dispatchEvent(
7782
7785
  new CustomEvent('submit:failure', {
@@ -7790,11 +7793,27 @@ var fml = (function (exports) {
7790
7793
  } else {
7791
7794
  console.warn('failed to submit form', this, 'reason:', e);
7792
7795
  }
7796
+ return false;
7793
7797
  } finally {
7794
7798
  this.#submitting = false;
7795
7799
  this.spinner(false);
7796
7800
  }
7797
7801
  }
7802
+ /**
7803
+ * Saves the unsaved edits, for a page action that would lose them, such as
7804
+ * one reloading the page: under `track-changes` it submits only while the
7805
+ * form is dirty, and without it always submits.
7806
+ * @param {HTMLElement} [submitter] as `submit` takes it
7807
+ * @returns {Promise<boolean>} whether no unsaved edit is left: false when
7808
+ * the submit did not succeed, or when the user edited the form while it
7809
+ * was in flight
7810
+ */
7811
+ async save(submitter) {
7812
+ if (this.#tracking && !this.dirty) {
7813
+ return true;
7814
+ }
7815
+ return (await this.submit(submitter)) && !this.dirty;
7816
+ }
7798
7817
  /**
7799
7818
  * Resets the native form, each field returning to its default value. Under
7800
7819
  * `track-changes` every control counts as edited: the form is dirty if the
@@ -7867,12 +7886,14 @@ var fml = (function (exports) {
7867
7886
  }
7868
7887
  }
7869
7888
  /**
7870
- * Counts a control as edited by the user, for a value the page writes on
7889
+ * Counts controls as edited by the user, for values the page writes on
7871
7890
  * the user's behalf: assigning `value` reports nothing, as 0019 wants.
7872
- * @param {Element} el the control, or an element inside it
7891
+ * @param {...Element} els each a control, or an element inside one
7873
7892
  */
7874
- edited(el) {
7875
- this.#touch(el);
7893
+ edited(...els) {
7894
+ for (const el of els) {
7895
+ this.#touch(el);
7896
+ }
7876
7897
  }
7877
7898
  /**
7878
7899
  * @param {Map<string, string>} saved
@@ -12974,6 +12995,8 @@ var fml = (function (exports) {
12974
12995
  #error;
12975
12996
  #requests = new SectionRequests();
12976
12997
  #updates = new Claims();
12998
+ /** @type {Promise<boolean>|null} */
12999
+ #confirming = null;
12977
13000
  /**
12978
13001
  * @param {HTMLElement} host the element the section requests are dispatched on
12979
13002
  * @param {DocumentFragment} fragment the rendered template, holding a
@@ -12997,17 +13020,53 @@ var fml = (function (exports) {
12997
13020
  get section() {
12998
13021
  return this.#section;
12999
13022
  }
13023
+ /**
13024
+ * Whether a `ful-form` of this dialog, not one of a dialog nested in it,
13025
+ * holds unsaved edits (see `Form.dirty`).
13026
+ * @returns {boolean}
13027
+ */
13028
+ get dirty() {
13029
+ return Array.from(this.#dialog.querySelectorAll('ful-form')).some(
13030
+ (form) => form.closest('dialog') === this.#dialog && /** @type {any} */ (form).dirty,
13031
+ );
13032
+ }
13000
13033
  /**
13001
13034
  * Dispatches the dismissal on the host and calls `close` unless a listener
13002
- * prevented it. A dismissal that is not cancelable announces a close the
13003
- * platform makes whatever the page does, so `close` is not called for it.
13035
+ * prevented it or, with `confirm` given and the dialog `dirty`, `confirm`
13036
+ * answers false. A dismissal that is not cancelable announces a close the
13037
+ * platform makes whatever the page does, so nothing is asked and `close` is
13038
+ * not called for it. While `confirm` is pending a further dismissal waits
13039
+ * for the same answer, and a dialog closed meanwhile is not closed again.
13004
13040
  * @param {CustomEvent} asked
13005
13041
  * @param {() => void} close
13042
+ * @param {(() => Promise<boolean>)|null} confirm asks whether to drop the unsaved edits
13043
+ * @returns {Promise<boolean>} whether the dismissal closes the dialog
13006
13044
  */
13007
- dismiss(asked, close) {
13008
- if (this.#host.dispatchEvent(asked) && asked.cancelable) {
13045
+ dismiss(asked, close, confirm) {
13046
+ if (this.#confirming) {
13047
+ return this.#confirming;
13048
+ }
13049
+ if (!this.#host.dispatchEvent(asked)) {
13050
+ return Promise.resolve(false);
13051
+ }
13052
+ if (!asked.cancelable) {
13053
+ return Promise.resolve(true);
13054
+ }
13055
+ if (!confirm || !this.dirty) {
13009
13056
  close();
13057
+ return Promise.resolve(true);
13010
13058
  }
13059
+ this.#confirming = confirm()
13060
+ .then((drop) => {
13061
+ if (drop && this.#dialog.open) {
13062
+ close();
13063
+ }
13064
+ return drop;
13065
+ })
13066
+ .finally(() => {
13067
+ this.#confirming = null;
13068
+ });
13069
+ return this.#confirming;
13011
13070
  }
13012
13071
  /**
13013
13072
  * Calls `dismiss` when a press and its release both land on the backdrop,
@@ -13147,21 +13206,24 @@ var fml = (function (exports) {
13147
13206
  * and a press and release both on the backdrop close the dialog, each as a
13148
13207
  * dismissal, as Escape does.
13149
13208
  *
13150
- * Before any of those three gestures closes it, the element dispatches a
13151
- * cancelable, non-bubbling `dialog:dismiss` with `detail: { reason }`, the
13152
- * reason being `button`, `backdrop` or `escape`; a listener calling
13153
- * `preventDefault()` keeps it open, to guard unsaved edits. A `data-result`
13154
- * button, `close-on-submit` and `close()` answer rather than dismiss, and
13155
- * dispatch none.
13209
+ * Those three gestures go through `dismiss(reason)`, the reason being
13210
+ * `button`, `backdrop` or `escape`: before it closes the dialog, the element
13211
+ * dispatches a cancelable, non-bubbling `dialog:dismiss` with
13212
+ * `detail: { reason }`, and a listener calling `preventDefault()` keeps it
13213
+ * open. Under `guard-dismiss`, a dismissal nobody refused, while a `ful-form`
13214
+ * of the dialog holds unsaved edits (see `Form.dirty`), first asks the user,
13215
+ * through `Dialog.confirm`, whether to close and drop them. A `data-result`
13216
+ * button, `close-on-submit`, `close()` and `update()` are answers or the page
13217
+ * deciding: they dispatch nothing and ask nothing.
13156
13218
  *
13157
13219
  * The platform lets a page refuse Escape once per user interaction: a second
13158
13220
  * Escape with no click or keystroke in between closes the dialog whatever the
13159
- * page does, `requires-answer` included. That close is still announced by a
13160
- * `dialog:dismiss`, which is then not cancelable.
13221
+ * page does, `requires-answer` and `guard-dismiss` included. That close is
13222
+ * still announced by a `dialog:dismiss`, which is then not cancelable.
13161
13223
  *
13162
13224
  * `requires-answer` withholds the close button and refuses Escape and the
13163
13225
  * backdrop, so the dialog is closed only by a button carrying `data-result`
13164
- * or from code through `close()`.
13226
+ * or from code, through `close()` or `dismiss()`.
13165
13227
  *
13166
13228
  * `close-on-submit` closes the dialog when a `ful-form` that is a direct child
13167
13229
  * of the body dispatches `submit:success`, answering with its
@@ -13181,7 +13243,7 @@ var fml = (function (exports) {
13181
13243
  * in a form needs.
13182
13244
  */
13183
13245
  class Dialog extends ParsedElement {
13184
- static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
13246
+ static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence', 'guard-dismiss:presence'];
13185
13247
  static slots = true;
13186
13248
  static template = `
13187
13249
  <dialog data-ref="dialog" class="ful-dialog">
@@ -13231,15 +13293,10 @@ var fml = (function (exports) {
13231
13293
  this.#dialog.close(result);
13232
13294
  }
13233
13295
  });
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
13296
  if (!requiresAnswer) {
13240
- this.#sections.onBackdrop(() => dismiss('backdrop'));
13297
+ this.#sections.onBackdrop(() => this.dismiss('backdrop'));
13241
13298
  }
13242
- fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
13299
+ fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => this.dismiss('button'));
13243
13300
  if (closeOnSubmit) {
13244
13301
  this.#sections.onSubmitted((response) => {
13245
13302
  this.#answer = { dismissed: false, result: null, response };
@@ -13249,12 +13306,12 @@ var fml = (function (exports) {
13249
13306
  this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
13250
13307
  if (!e.cancelable) {
13251
13308
  this.#dialog.returnValue = '';
13252
- dismiss('escape', true);
13309
+ this.#dismiss('escape', false);
13253
13310
  return;
13254
13311
  }
13255
13312
  e.preventDefault();
13256
13313
  if (!requiresAnswer) {
13257
- dismiss('escape');
13314
+ this.dismiss('escape');
13258
13315
  }
13259
13316
  });
13260
13317
  this.replaceChildren(fragment);
@@ -13346,6 +13403,34 @@ var fml = (function (exports) {
13346
13403
  refresh() {
13347
13404
  return this.#sections.request();
13348
13405
  }
13406
+ /**
13407
+ * Dismisses the dialog as the user would: dispatches the cancelable
13408
+ * `dialog:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
13409
+ * asks before dropping unsaved edits, closing the dialog as a dismissal
13410
+ * unless either refused. `requires-answer` does not refuse it, as it does
13411
+ * not refuse `close()`. Does nothing on a closed dialog.
13412
+ * @param {string} reason why it is dismissed, such as `navigation`
13413
+ * @returns {Promise<boolean>} whether the dialog is closed, false when a
13414
+ * listener or the user kept it open
13415
+ */
13416
+ dismiss(reason) {
13417
+ if (!this.#dialog.open) {
13418
+ return Promise.resolve(true);
13419
+ }
13420
+ return this.#dismiss(reason, true);
13421
+ }
13422
+ /**
13423
+ * @param {string} reason
13424
+ * @param {boolean} cancelable false for a close the platform makes anyway
13425
+ * @returns {Promise<boolean>}
13426
+ */
13427
+ #dismiss(reason, cancelable) {
13428
+ return this.#sections.dismiss(
13429
+ new CustomEvent('dialog:dismiss', { cancelable, detail: { reason } }),
13430
+ () => this.#dialog.close(''),
13431
+ this.declared('guard-dismiss') ? discardEdits : null,
13432
+ );
13433
+ }
13349
13434
  /**
13350
13435
  * Closes an open dialog, doing nothing on a closed one. A non-empty result
13351
13436
  * answers as the button carrying that `data-result` would; no result, or
@@ -13445,6 +13530,19 @@ var fml = (function (exports) {
13445
13530
  }
13446
13531
  }
13447
13532
 
13533
+ /**
13534
+ * Asks, through `Dialog.confirm`, whether to close and drop the unsaved edits,
13535
+ * with the localized `dialog.discard.*` texts.
13536
+ * @returns {Promise<boolean>} true to drop them
13537
+ */
13538
+ const discardEdits = () => {
13539
+ const l10n = Localization.of();
13540
+ return Dialog.confirm(l10n.t('dialog.discard.header'), l10n.t('dialog.discard.body'), {
13541
+ confirm: l10n.t('dialog.discard.confirm'),
13542
+ cancel: l10n.t('dialog.discard.cancel'),
13543
+ });
13544
+ };
13545
+
13448
13546
  /**
13449
13547
  * A side panel on the native `<dialog>`, shown as a modal: a header holding
13450
13548
  * the title and a localized close button, then loading, error and content
@@ -13457,17 +13555,23 @@ var fml = (function (exports) {
13457
13555
  * default, when absent) or `start`, following the writing direction.
13458
13556
  *
13459
13557
  * The close button, Escape, and a press and release both on the backdrop all
13460
- * go through `close()`, once a cancelable, non-bubbling `drawer:dismiss` with
13461
- * `detail: { reason }` (`button`, `backdrop` or `escape`) has been dispatched
13462
- * on the element and no listener prevented it: `preventDefault()` keeps the
13463
- * drawer open, to guard unsaved edits. `close()` and `close-on-submit`
13464
- * dispatch none. The platform lets a page refuse Escape once per user
13558
+ * go through `dismiss(reason)`, the reason being `button`, `backdrop` or
13559
+ * `escape`: before it closes the drawer, the element dispatches a cancelable,
13560
+ * non-bubbling `drawer:dismiss` with `detail: { reason }`, and a listener
13561
+ * calling `preventDefault()` keeps it open. Under `guard-dismiss`, a dismissal
13562
+ * nobody refused, while a `ful-form` of the drawer holds unsaved edits (see
13563
+ * `Form.dirty`), first asks the user, through `Dialog.confirm`, whether to
13564
+ * close and drop them. A `data-result` button, `close-on-submit`, `close()`
13565
+ * and `update()` are answers or the page deciding: they dispatch nothing and
13566
+ * ask nothing. The platform lets a page refuse Escape once per user
13465
13567
  * interaction: a second Escape with no click or keystroke in between closes
13466
- * the drawer at once, without the slide, and its `drawer:dismiss` is then not
13467
- * cancelable. Every close dispatches a non-bubbling `close` event on
13468
- * the element with `detail: { dismissed, response }`: `{ dismissed: false,
13469
- * response }` when a form closed it under `close-on-submit`, and
13470
- * `{ dismissed: true, response: null }` for any other close.
13568
+ * the drawer at once, without the slide, `guard-dismiss` included, and its
13569
+ * `drawer:dismiss` is then not cancelable.
13570
+ *
13571
+ * Any button inside the drawer carrying `data-result` closes it with that
13572
+ * result, an empty one being a dismissal. Every close dispatches a
13573
+ * non-bubbling `close` event on the element carrying the `DialogOutcome` as
13574
+ * `detail`, before the pending `ask()` calls resolve.
13471
13575
  *
13472
13576
  * `close-on-submit` closes the drawer when a `ful-form` that is a direct child
13473
13577
  * of the content section dispatches `submit:success`, carrying its
@@ -13479,7 +13583,7 @@ var fml = (function (exports) {
13479
13583
  * element's id calls `open()` on click.
13480
13584
  */
13481
13585
  class Drawer extends ParsedElement {
13482
- static attributes = ['header', 'placement', 'close-on-submit:presence'];
13586
+ static attributes = ['header', 'placement', 'close-on-submit:presence', 'guard-dismiss:presence'];
13483
13587
  static slots = true;
13484
13588
  static template = `
13485
13589
  <dialog data-ref="dialog" class="ful-drawer">
@@ -13496,8 +13600,9 @@ var fml = (function (exports) {
13496
13600
  #dialog;
13497
13601
  #title;
13498
13602
  #sections;
13499
- /** @type {{ dismissed: boolean, response: any }|null} */
13603
+ /** @type {{ dismissed: boolean, result: string|null, response: any }|null} */
13500
13604
  #answer = null;
13605
+ #resolvers = [];
13501
13606
  #closing = false;
13502
13607
  #slideOutEnded = (/** @type AnimationEvent */ e) => {
13503
13608
  if (e.target === this.#dialog) {
@@ -13520,32 +13625,33 @@ var fml = (function (exports) {
13520
13625
  if (placement) {
13521
13626
  this.#dialog.setAttribute('placement', placement);
13522
13627
  }
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
13628
  /** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
13529
- dismiss('button'),
13629
+ this.dismiss('button'),
13530
13630
  );
13531
- this.#sections.onBackdrop(() => dismiss('backdrop'));
13631
+ this.#sections.onBackdrop(() => this.dismiss('backdrop'));
13532
13632
  this.#dialog.addEventListener('cancel', (e) => {
13533
13633
  if (!e.cancelable) {
13534
13634
  this.#stopSlidingOut();
13535
- dismiss('escape', true);
13635
+ this.#dismiss('escape', false);
13536
13636
  return;
13537
13637
  }
13538
13638
  e.preventDefault();
13539
- dismiss('escape');
13639
+ this.dismiss('escape');
13640
+ });
13641
+ this.#dialog.addEventListener('click', (/** @type any */ e) => {
13642
+ const result = e.target.closest('button[data-result]')?.dataset.result;
13643
+ if (result !== undefined) {
13644
+ this.close(result);
13645
+ }
13540
13646
  });
13541
13647
  this.#dialog.addEventListener('close', () => {
13542
- this.dispatchEvent(
13543
- new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
13544
- );
13648
+ const outcome = this.#outcome();
13649
+ this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
13650
+ this.#settle(outcome);
13545
13651
  });
13546
13652
  if (this.declared('close-on-submit')) {
13547
13653
  this.#sections.onSubmitted((response) => {
13548
- this.#answer = { dismissed: false, response };
13654
+ this.#answer = { dismissed: false, result: null, response };
13549
13655
  this.close();
13550
13656
  });
13551
13657
  }
@@ -13593,9 +13699,36 @@ var fml = (function (exports) {
13593
13699
  refresh() {
13594
13700
  return this.#sections.request();
13595
13701
  }
13702
+ /** @returns {{ dismissed: boolean, result: string|null, response: any }} */
13703
+ #outcome() {
13704
+ return this.#answer ?? { dismissed: true, result: null, response: null };
13705
+ }
13706
+ /** @param {{ dismissed: boolean, result: string|null, response: any }} outcome */
13707
+ #settle(outcome) {
13708
+ const resolvers = this.#resolvers;
13709
+ this.#resolvers = [];
13710
+ for (const resolve of resolvers) {
13711
+ resolve(outcome);
13712
+ }
13713
+ }
13714
+ /**
13715
+ * Resolves every pending `ask()` and `open()` with the outcome so far,
13716
+ * a dismissal for a drawer still open, so that no caller waits on a drawer
13717
+ * that left the document. The native dialog is not closed and no `close`
13718
+ * event is dispatched.
13719
+ */
13720
+ disconnectedCallback() {
13721
+ this.#settle(this.#outcome());
13722
+ }
13723
+ /**
13724
+ * The same as `ask()`.
13725
+ * @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>}
13726
+ */
13727
+ open() {
13728
+ return this.ask();
13729
+ }
13596
13730
  /**
13597
- * Shows the drawer as a modal. On a drawer already open nothing happens,
13598
- * except that one sliding out stops and stays open.
13731
+ * Shows the drawer as a modal and resolves with how it ends.
13599
13732
  *
13600
13733
  * The call that opens the drawer resets the outcome of the previous
13601
13734
  * opening, hides the error and loading sections, and dispatches
@@ -13603,26 +13736,67 @@ var fml = (function (exports) {
13603
13736
  * `detail: { section, first, name: null, index: null }`, `section` being
13604
13737
  * the content section and `first` true only on the first answered request),
13605
13738
  * so a listener can fill the content. A failed request is painted into the
13606
- * content section.
13739
+ * content section and does not reject. On a drawer already open nothing is
13740
+ * shown or requested, except that one sliding out stops and stays open, and
13741
+ * the call waits for the same ending as the calls before it, which is how
13742
+ * a caller learns the outcome of the opening an `update()` made.
13743
+ * @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>} resolving on
13744
+ * close, or on removal from the document with a dismissal
13607
13745
  */
13608
- open() {
13609
- if (!this.#show()) {
13610
- return;
13746
+ ask() {
13747
+ if (this.#show()) {
13748
+ this.#sections.restChrome();
13749
+ this.#sections.request();
13611
13750
  }
13612
- this.#sections.restChrome();
13613
- this.#sections.request();
13751
+ return new Promise((resolve) => {
13752
+ this.#resolvers.push(resolve);
13753
+ });
13754
+ }
13755
+ /**
13756
+ * Dismisses the drawer as the user would: dispatches the cancelable
13757
+ * `drawer:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
13758
+ * asks before dropping unsaved edits, sliding the drawer out as a
13759
+ * dismissal unless either refused. Does nothing on a drawer that is closed
13760
+ * or already sliding out.
13761
+ * @param {string} reason why it is dismissed, such as `navigation`
13762
+ * @returns {Promise<boolean>} whether the drawer is closed or closing,
13763
+ * false when a listener or the user kept it open
13764
+ */
13765
+ dismiss(reason) {
13766
+ if (this.#closing || !this.#dialog.open) {
13767
+ return Promise.resolve(true);
13768
+ }
13769
+ return this.#dismiss(reason, true);
13770
+ }
13771
+ /**
13772
+ * @param {string} reason
13773
+ * @param {boolean} cancelable false for a close the platform makes anyway
13774
+ * @returns {Promise<boolean>}
13775
+ */
13776
+ #dismiss(reason, cancelable) {
13777
+ return this.#sections.dismiss(
13778
+ new CustomEvent('drawer:dismiss', { cancelable, detail: { reason } }),
13779
+ () => this.close(),
13780
+ this.declared('guard-dismiss') ? discardEdits : null,
13781
+ );
13614
13782
  }
13615
13783
  /**
13616
13784
  * Slides the drawer out and closes it when the animation ends, setting the
13617
13785
  * `closing` attribute on the native dialog meanwhile; the `close` event is
13618
13786
  * dispatched then, after this call has returned. Where the user prefers
13619
- * reduced motion, or no animation runs, it closes at once. Does nothing on
13620
- * a drawer that is closed or already sliding out.
13787
+ * reduced motion, or no animation runs, it closes at once. A non-empty
13788
+ * result answers as the button carrying that `data-result` would; no
13789
+ * result, or an empty one, is a dismissal. Does nothing on a drawer that is
13790
+ * closed or already sliding out.
13791
+ * @param {string} [result]
13621
13792
  */
13622
- close() {
13793
+ close(result) {
13623
13794
  if (this.#closing || !this.#dialog.open) {
13624
13795
  return;
13625
13796
  }
13797
+ if (result) {
13798
+ this.#answer = { dismissed: false, result, response: null };
13799
+ }
13626
13800
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
13627
13801
  this.#dialog.close();
13628
13802
  return;
@@ -14334,6 +14508,10 @@ var fml = (function (exports) {
14334
14508
  'dialog.close': 'Close',
14335
14509
  'dialog.confirm': 'Confirm',
14336
14510
  'dialog.cancel': 'Cancel',
14511
+ 'dialog.discard.header': 'Unsaved changes',
14512
+ 'dialog.discard.body': 'There are unsaved changes: close anyway?',
14513
+ 'dialog.discard.confirm': 'Close without saving',
14514
+ 'dialog.discard.cancel': 'Keep editing',
14337
14515
  'drawer.close': 'Close',
14338
14516
  'spinner.loading': 'Loading…',
14339
14517
  'toast.region': 'Notifications',
@@ -14379,6 +14557,10 @@ var fml = (function (exports) {
14379
14557
  'dialog.close': 'Chiudi',
14380
14558
  'dialog.confirm': 'Conferma',
14381
14559
  'dialog.cancel': 'Annulla',
14560
+ 'dialog.discard.header': 'Modifiche non salvate',
14561
+ 'dialog.discard.body': 'Ci sono modifiche non salvate: chiudere comunque?',
14562
+ 'dialog.discard.confirm': 'Chiudi senza salvare',
14563
+ 'dialog.discard.cancel': 'Continua a modificare',
14382
14564
  'drawer.close': 'Chiudi',
14383
14565
  'spinner.loading': 'Caricamento…',
14384
14566
  'toast.region': 'Notifiche',
@@ -14427,6 +14609,10 @@ var fml = (function (exports) {
14427
14609
  'dialog.close': 'Cerrar',
14428
14610
  'dialog.confirm': 'Confirmar',
14429
14611
  'dialog.cancel': 'Cancelar',
14612
+ 'dialog.discard.header': 'Cambios sin guardar',
14613
+ 'dialog.discard.body': 'Hay cambios sin guardar: ¿cerrar de todos modos?',
14614
+ 'dialog.discard.confirm': 'Cerrar sin guardar',
14615
+ 'dialog.discard.cancel': 'Seguir editando',
14430
14616
  'drawer.close': 'Cerrar',
14431
14617
  'spinner.loading': 'Cargando…',
14432
14618
  'toast.region': 'Notificaciones',
@@ -14475,6 +14661,10 @@ var fml = (function (exports) {
14475
14661
  'dialog.close': 'Fermer',
14476
14662
  'dialog.confirm': 'Confirmer',
14477
14663
  'dialog.cancel': 'Annuler',
14664
+ 'dialog.discard.header': 'Modifications non enregistrées',
14665
+ 'dialog.discard.body': 'Des modifications ne sont pas enregistrées : fermer quand même ?',
14666
+ 'dialog.discard.confirm': 'Fermer sans enregistrer',
14667
+ 'dialog.discard.cancel': 'Continuer à modifier',
14478
14668
  'drawer.close': 'Fermer',
14479
14669
  'spinner.loading': 'Chargement…',
14480
14670
  'toast.region': 'Notifications',