@optionfactory/fml 9.0.0-rc21 → 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/ful.iife.js CHANGED
@@ -1463,11 +1463,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1463
1463
  * A call while a submit is in flight returns at once, doing nothing.
1464
1464
  * @param {HTMLElement} [submitter] the button that submitted, if any; it is
1465
1465
  * the only button whose name and value are submitted
1466
- * @returns {Promise<void>} settling once the submit has finished, whatever its outcome
1466
+ * @returns {Promise<boolean>} settling once the submit has finished: true
1467
+ * after `submit:success`, false when it failed, was cancelled or was dropped
1468
+ * for one in flight
1467
1469
  */
1468
1470
  async submit(submitter) {
1469
1471
  if (this.#submitting) {
1470
- return;
1472
+ return false;
1471
1473
  }
1472
1474
  this.#submitting = true;
1473
1475
  this.spinner(true);
@@ -1484,7 +1486,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1484
1486
  detail: { submitter, values, request },
1485
1487
  });
1486
1488
  if (!this.dispatchEvent(se)) {
1487
- return;
1489
+ return false;
1488
1490
  }
1489
1491
  this.errors = [];
1490
1492
  const sre = new CustomEvent('submit:requested', {
@@ -1507,6 +1509,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1507
1509
  detail: { submitter, values, request, response: mapped },
1508
1510
  }),
1509
1511
  );
1512
+ return true;
1510
1513
  } catch (e) {
1511
1514
  this.dispatchEvent(
1512
1515
  new CustomEvent('submit:failure', {
@@ -1520,11 +1523,27 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1520
1523
  } else {
1521
1524
  console.warn('failed to submit form', this, 'reason:', e);
1522
1525
  }
1526
+ return false;
1523
1527
  } finally {
1524
1528
  this.#submitting = false;
1525
1529
  this.spinner(false);
1526
1530
  }
1527
1531
  }
1532
+ /**
1533
+ * Saves the unsaved edits, for a page action that would lose them, such as
1534
+ * one reloading the page: under `track-changes` it submits only while the
1535
+ * form is dirty, and without it always submits.
1536
+ * @param {HTMLElement} [submitter] as `submit` takes it
1537
+ * @returns {Promise<boolean>} whether no unsaved edit is left: false when
1538
+ * the submit did not succeed, or when the user edited the form while it
1539
+ * was in flight
1540
+ */
1541
+ async save(submitter) {
1542
+ if (this.#tracking && !this.dirty) {
1543
+ return true;
1544
+ }
1545
+ return (await this.submit(submitter)) && !this.dirty;
1546
+ }
1528
1547
  /**
1529
1548
  * Resets the native form, each field returning to its default value. Under
1530
1549
  * `track-changes` every control counts as edited: the form is dirty if the
@@ -1597,12 +1616,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1597
1616
  }
1598
1617
  }
1599
1618
  /**
1600
- * Counts a control as edited by the user, for a value the page writes on
1619
+ * Counts controls as edited by the user, for values the page writes on
1601
1620
  * the user's behalf: assigning `value` reports nothing, as 0019 wants.
1602
- * @param {Element} el the control, or an element inside it
1621
+ * @param {...Element} els each a control, or an element inside one
1603
1622
  */
1604
- edited(el) {
1605
- this.#touch(el);
1623
+ edited(...els) {
1624
+ for (const el of els) {
1625
+ this.#touch(el);
1626
+ }
1606
1627
  }
1607
1628
  /**
1608
1629
  * @param {Map<string, string>} saved
@@ -6704,6 +6725,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6704
6725
  #error;
6705
6726
  #requests = new SectionRequests();
6706
6727
  #updates = new Claims();
6728
+ /** @type {Promise<boolean>|null} */
6729
+ #confirming = null;
6707
6730
  /**
6708
6731
  * @param {HTMLElement} host the element the section requests are dispatched on
6709
6732
  * @param {DocumentFragment} fragment the rendered template, holding a
@@ -6727,17 +6750,53 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6727
6750
  get section() {
6728
6751
  return this.#section;
6729
6752
  }
6753
+ /**
6754
+ * Whether a `ful-form` of this dialog, not one of a dialog nested in it,
6755
+ * holds unsaved edits (see `Form.dirty`).
6756
+ * @returns {boolean}
6757
+ */
6758
+ get dirty() {
6759
+ return Array.from(this.#dialog.querySelectorAll('ful-form')).some(
6760
+ (form) => form.closest('dialog') === this.#dialog && /** @type {any} */ (form).dirty,
6761
+ );
6762
+ }
6730
6763
  /**
6731
6764
  * Dispatches the dismissal on the host and calls `close` unless a listener
6732
- * prevented it. A dismissal that is not cancelable announces a close the
6733
- * platform makes whatever the page does, so `close` is not called for it.
6765
+ * prevented it or, with `confirm` given and the dialog `dirty`, `confirm`
6766
+ * answers false. A dismissal that is not cancelable announces a close the
6767
+ * platform makes whatever the page does, so nothing is asked and `close` is
6768
+ * not called for it. While `confirm` is pending a further dismissal waits
6769
+ * for the same answer, and a dialog closed meanwhile is not closed again.
6734
6770
  * @param {CustomEvent} asked
6735
6771
  * @param {() => void} close
6772
+ * @param {(() => Promise<boolean>)|null} confirm asks whether to drop the unsaved edits
6773
+ * @returns {Promise<boolean>} whether the dismissal closes the dialog
6736
6774
  */
6737
- dismiss(asked, close) {
6738
- if (this.#host.dispatchEvent(asked) && asked.cancelable) {
6775
+ dismiss(asked, close, confirm) {
6776
+ if (this.#confirming) {
6777
+ return this.#confirming;
6778
+ }
6779
+ if (!this.#host.dispatchEvent(asked)) {
6780
+ return Promise.resolve(false);
6781
+ }
6782
+ if (!asked.cancelable) {
6783
+ return Promise.resolve(true);
6784
+ }
6785
+ if (!confirm || !this.dirty) {
6739
6786
  close();
6787
+ return Promise.resolve(true);
6740
6788
  }
6789
+ this.#confirming = confirm()
6790
+ .then((drop) => {
6791
+ if (drop && this.#dialog.open) {
6792
+ close();
6793
+ }
6794
+ return drop;
6795
+ })
6796
+ .finally(() => {
6797
+ this.#confirming = null;
6798
+ });
6799
+ return this.#confirming;
6741
6800
  }
6742
6801
  /**
6743
6802
  * Calls `dismiss` when a press and its release both land on the backdrop,
@@ -6877,21 +6936,24 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6877
6936
  * and a press and release both on the backdrop close the dialog, each as a
6878
6937
  * dismissal, as Escape does.
6879
6938
  *
6880
- * Before any of those three gestures closes it, the element dispatches a
6881
- * cancelable, non-bubbling `dialog:dismiss` with `detail: { reason }`, the
6882
- * reason being `button`, `backdrop` or `escape`; a listener calling
6883
- * `preventDefault()` keeps it open, to guard unsaved edits. A `data-result`
6884
- * button, `close-on-submit` and `close()` answer rather than dismiss, and
6885
- * dispatch none.
6939
+ * Those three gestures go through `dismiss(reason)`, the reason being
6940
+ * `button`, `backdrop` or `escape`: before it closes the dialog, the element
6941
+ * dispatches a cancelable, non-bubbling `dialog:dismiss` with
6942
+ * `detail: { reason }`, and a listener calling `preventDefault()` keeps it
6943
+ * open. Under `guard-dismiss`, a dismissal nobody refused, while a `ful-form`
6944
+ * of the dialog holds unsaved edits (see `Form.dirty`), first asks the user,
6945
+ * through `Dialog.confirm`, whether to close and drop them. A `data-result`
6946
+ * button, `close-on-submit`, `close()` and `update()` are answers or the page
6947
+ * deciding: they dispatch nothing and ask nothing.
6886
6948
  *
6887
6949
  * The platform lets a page refuse Escape once per user interaction: a second
6888
6950
  * Escape with no click or keystroke in between closes the dialog whatever the
6889
- * page does, `requires-answer` included. That close is still announced by a
6890
- * `dialog:dismiss`, which is then not cancelable.
6951
+ * page does, `requires-answer` and `guard-dismiss` included. That close is
6952
+ * still announced by a `dialog:dismiss`, which is then not cancelable.
6891
6953
  *
6892
6954
  * `requires-answer` withholds the close button and refuses Escape and the
6893
6955
  * backdrop, so the dialog is closed only by a button carrying `data-result`
6894
- * or from code through `close()`.
6956
+ * or from code, through `close()` or `dismiss()`.
6895
6957
  *
6896
6958
  * `close-on-submit` closes the dialog when a `ful-form` that is a direct child
6897
6959
  * of the body dispatches `submit:success`, answering with its
@@ -6911,7 +6973,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6911
6973
  * in a form needs.
6912
6974
  */
6913
6975
  class Dialog extends index_mjs.ParsedElement {
6914
- static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence'];
6976
+ static attributes = ['header', 'requires-answer:presence', 'close-on-submit:presence', 'guard-dismiss:presence'];
6915
6977
  static slots = true;
6916
6978
  static template = `
6917
6979
  <dialog data-ref="dialog" class="ful-dialog">
@@ -6961,15 +7023,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6961
7023
  this.#dialog.close(result);
6962
7024
  }
6963
7025
  });
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
7026
  if (!requiresAnswer) {
6970
- this.#sections.onBackdrop(() => dismiss('backdrop'));
7027
+ this.#sections.onBackdrop(() => this.dismiss('backdrop'));
6971
7028
  }
6972
- fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
7029
+ fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => this.dismiss('button'));
6973
7030
  if (closeOnSubmit) {
6974
7031
  this.#sections.onSubmitted((response) => {
6975
7032
  this.#answer = { dismissed: false, result: null, response };
@@ -6979,12 +7036,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
6979
7036
  this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
6980
7037
  if (!e.cancelable) {
6981
7038
  this.#dialog.returnValue = '';
6982
- dismiss('escape', true);
7039
+ this.#dismiss('escape', false);
6983
7040
  return;
6984
7041
  }
6985
7042
  e.preventDefault();
6986
7043
  if (!requiresAnswer) {
6987
- dismiss('escape');
7044
+ this.dismiss('escape');
6988
7045
  }
6989
7046
  });
6990
7047
  this.replaceChildren(fragment);
@@ -7076,6 +7133,34 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7076
7133
  refresh() {
7077
7134
  return this.#sections.request();
7078
7135
  }
7136
+ /**
7137
+ * Dismisses the dialog as the user would: dispatches the cancelable
7138
+ * `dialog:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
7139
+ * asks before dropping unsaved edits, closing the dialog as a dismissal
7140
+ * unless either refused. `requires-answer` does not refuse it, as it does
7141
+ * not refuse `close()`. Does nothing on a closed dialog.
7142
+ * @param {string} reason why it is dismissed, such as `navigation`
7143
+ * @returns {Promise<boolean>} whether the dialog is closed, false when a
7144
+ * listener or the user kept it open
7145
+ */
7146
+ dismiss(reason) {
7147
+ if (!this.#dialog.open) {
7148
+ return Promise.resolve(true);
7149
+ }
7150
+ return this.#dismiss(reason, true);
7151
+ }
7152
+ /**
7153
+ * @param {string} reason
7154
+ * @param {boolean} cancelable false for a close the platform makes anyway
7155
+ * @returns {Promise<boolean>}
7156
+ */
7157
+ #dismiss(reason, cancelable) {
7158
+ return this.#sections.dismiss(
7159
+ new CustomEvent('dialog:dismiss', { cancelable, detail: { reason } }),
7160
+ () => this.#dialog.close(''),
7161
+ this.declared('guard-dismiss') ? discardEdits : null,
7162
+ );
7163
+ }
7079
7164
  /**
7080
7165
  * Closes an open dialog, doing nothing on a closed one. A non-empty result
7081
7166
  * answers as the button carrying that `data-result` would; no result, or
@@ -7175,6 +7260,19 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7175
7260
  }
7176
7261
  }
7177
7262
 
7263
+ /**
7264
+ * Asks, through `Dialog.confirm`, whether to close and drop the unsaved edits,
7265
+ * with the localized `dialog.discard.*` texts.
7266
+ * @returns {Promise<boolean>} true to drop them
7267
+ */
7268
+ const discardEdits = () => {
7269
+ const l10n = index_mjs.Localization.of();
7270
+ return Dialog.confirm(l10n.t('dialog.discard.header'), l10n.t('dialog.discard.body'), {
7271
+ confirm: l10n.t('dialog.discard.confirm'),
7272
+ cancel: l10n.t('dialog.discard.cancel'),
7273
+ });
7274
+ };
7275
+
7178
7276
  /**
7179
7277
  * A side panel on the native `<dialog>`, shown as a modal: a header holding
7180
7278
  * the title and a localized close button, then loading, error and content
@@ -7187,17 +7285,23 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7187
7285
  * default, when absent) or `start`, following the writing direction.
7188
7286
  *
7189
7287
  * The close button, Escape, and a press and release both on the backdrop all
7190
- * go through `close()`, once a cancelable, non-bubbling `drawer:dismiss` with
7191
- * `detail: { reason }` (`button`, `backdrop` or `escape`) has been dispatched
7192
- * on the element and no listener prevented it: `preventDefault()` keeps the
7193
- * drawer open, to guard unsaved edits. `close()` and `close-on-submit`
7194
- * dispatch none. The platform lets a page refuse Escape once per user
7288
+ * go through `dismiss(reason)`, the reason being `button`, `backdrop` or
7289
+ * `escape`: before it closes the drawer, the element dispatches a cancelable,
7290
+ * non-bubbling `drawer:dismiss` with `detail: { reason }`, and a listener
7291
+ * calling `preventDefault()` keeps it open. Under `guard-dismiss`, a dismissal
7292
+ * nobody refused, while a `ful-form` of the drawer holds unsaved edits (see
7293
+ * `Form.dirty`), first asks the user, through `Dialog.confirm`, whether to
7294
+ * close and drop them. A `data-result` button, `close-on-submit`, `close()`
7295
+ * and `update()` are answers or the page deciding: they dispatch nothing and
7296
+ * ask nothing. The platform lets a page refuse Escape once per user
7195
7297
  * interaction: a second Escape with no click or keystroke in between closes
7196
- * the drawer at once, without the slide, and its `drawer:dismiss` is then not
7197
- * cancelable. Every close dispatches a non-bubbling `close` event on
7198
- * the element with `detail: { dismissed, response }`: `{ dismissed: false,
7199
- * response }` when a form closed it under `close-on-submit`, and
7200
- * `{ dismissed: true, response: null }` for any other close.
7298
+ * the drawer at once, without the slide, `guard-dismiss` included, and its
7299
+ * `drawer:dismiss` is then not cancelable.
7300
+ *
7301
+ * Any button inside the drawer carrying `data-result` closes it with that
7302
+ * result, an empty one being a dismissal. Every close dispatches a
7303
+ * non-bubbling `close` event on the element carrying the `DialogOutcome` as
7304
+ * `detail`, before the pending `ask()` calls resolve.
7201
7305
  *
7202
7306
  * `close-on-submit` closes the drawer when a `ful-form` that is a direct child
7203
7307
  * of the content section dispatches `submit:success`, carrying its
@@ -7209,7 +7313,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7209
7313
  * element's id calls `open()` on click.
7210
7314
  */
7211
7315
  class Drawer extends index_mjs.ParsedElement {
7212
- static attributes = ['header', 'placement', 'close-on-submit:presence'];
7316
+ static attributes = ['header', 'placement', 'close-on-submit:presence', 'guard-dismiss:presence'];
7213
7317
  static slots = true;
7214
7318
  static template = `
7215
7319
  <dialog data-ref="dialog" class="ful-drawer">
@@ -7226,8 +7330,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7226
7330
  #dialog;
7227
7331
  #title;
7228
7332
  #sections;
7229
- /** @type {{ dismissed: boolean, response: any }|null} */
7333
+ /** @type {{ dismissed: boolean, result: string|null, response: any }|null} */
7230
7334
  #answer = null;
7335
+ #resolvers = [];
7231
7336
  #closing = false;
7232
7337
  #slideOutEnded = (/** @type AnimationEvent */ e) => {
7233
7338
  if (e.target === this.#dialog) {
@@ -7250,32 +7355,33 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7250
7355
  if (placement) {
7251
7356
  this.#dialog.setAttribute('placement', placement);
7252
7357
  }
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
7358
  /** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
7259
- dismiss('button'),
7359
+ this.dismiss('button'),
7260
7360
  );
7261
- this.#sections.onBackdrop(() => dismiss('backdrop'));
7361
+ this.#sections.onBackdrop(() => this.dismiss('backdrop'));
7262
7362
  this.#dialog.addEventListener('cancel', (e) => {
7263
7363
  if (!e.cancelable) {
7264
7364
  this.#stopSlidingOut();
7265
- dismiss('escape', true);
7365
+ this.#dismiss('escape', false);
7266
7366
  return;
7267
7367
  }
7268
7368
  e.preventDefault();
7269
- dismiss('escape');
7369
+ this.dismiss('escape');
7370
+ });
7371
+ this.#dialog.addEventListener('click', (/** @type any */ e) => {
7372
+ const result = e.target.closest('button[data-result]')?.dataset.result;
7373
+ if (result !== undefined) {
7374
+ this.close(result);
7375
+ }
7270
7376
  });
7271
7377
  this.#dialog.addEventListener('close', () => {
7272
- this.dispatchEvent(
7273
- new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),
7274
- );
7378
+ const outcome = this.#outcome();
7379
+ this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
7380
+ this.#settle(outcome);
7275
7381
  });
7276
7382
  if (this.declared('close-on-submit')) {
7277
7383
  this.#sections.onSubmitted((response) => {
7278
- this.#answer = { dismissed: false, response };
7384
+ this.#answer = { dismissed: false, result: null, response };
7279
7385
  this.close();
7280
7386
  });
7281
7387
  }
@@ -7323,9 +7429,36 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7323
7429
  refresh() {
7324
7430
  return this.#sections.request();
7325
7431
  }
7432
+ /** @returns {{ dismissed: boolean, result: string|null, response: any }} */
7433
+ #outcome() {
7434
+ return this.#answer ?? { dismissed: true, result: null, response: null };
7435
+ }
7436
+ /** @param {{ dismissed: boolean, result: string|null, response: any }} outcome */
7437
+ #settle(outcome) {
7438
+ const resolvers = this.#resolvers;
7439
+ this.#resolvers = [];
7440
+ for (const resolve of resolvers) {
7441
+ resolve(outcome);
7442
+ }
7443
+ }
7444
+ /**
7445
+ * Resolves every pending `ask()` and `open()` with the outcome so far,
7446
+ * a dismissal for a drawer still open, so that no caller waits on a drawer
7447
+ * that left the document. The native dialog is not closed and no `close`
7448
+ * event is dispatched.
7449
+ */
7450
+ disconnectedCallback() {
7451
+ this.#settle(this.#outcome());
7452
+ }
7453
+ /**
7454
+ * The same as `ask()`.
7455
+ * @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>}
7456
+ */
7457
+ open() {
7458
+ return this.ask();
7459
+ }
7326
7460
  /**
7327
- * Shows the drawer as a modal. On a drawer already open nothing happens,
7328
- * except that one sliding out stops and stays open.
7461
+ * Shows the drawer as a modal and resolves with how it ends.
7329
7462
  *
7330
7463
  * The call that opens the drawer resets the outcome of the previous
7331
7464
  * opening, hides the error and loading sections, and dispatches
@@ -7333,26 +7466,67 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7333
7466
  * `detail: { section, first, name: null, index: null }`, `section` being
7334
7467
  * the content section and `first` true only on the first answered request),
7335
7468
  * so a listener can fill the content. A failed request is painted into the
7336
- * content section.
7469
+ * content section and does not reject. On a drawer already open nothing is
7470
+ * shown or requested, except that one sliding out stops and stays open, and
7471
+ * the call waits for the same ending as the calls before it, which is how
7472
+ * a caller learns the outcome of the opening an `update()` made.
7473
+ * @returns {Promise<{ dismissed: boolean, result: string|null, response: any }>} resolving on
7474
+ * close, or on removal from the document with a dismissal
7337
7475
  */
7338
- open() {
7339
- if (!this.#show()) {
7340
- return;
7476
+ ask() {
7477
+ if (this.#show()) {
7478
+ this.#sections.restChrome();
7479
+ this.#sections.request();
7341
7480
  }
7342
- this.#sections.restChrome();
7343
- this.#sections.request();
7481
+ return new Promise((resolve) => {
7482
+ this.#resolvers.push(resolve);
7483
+ });
7484
+ }
7485
+ /**
7486
+ * Dismisses the drawer as the user would: dispatches the cancelable
7487
+ * `drawer:dismiss` with `detail: { reason }` and, under `guard-dismiss`,
7488
+ * asks before dropping unsaved edits, sliding the drawer out as a
7489
+ * dismissal unless either refused. Does nothing on a drawer that is closed
7490
+ * or already sliding out.
7491
+ * @param {string} reason why it is dismissed, such as `navigation`
7492
+ * @returns {Promise<boolean>} whether the drawer is closed or closing,
7493
+ * false when a listener or the user kept it open
7494
+ */
7495
+ dismiss(reason) {
7496
+ if (this.#closing || !this.#dialog.open) {
7497
+ return Promise.resolve(true);
7498
+ }
7499
+ return this.#dismiss(reason, true);
7500
+ }
7501
+ /**
7502
+ * @param {string} reason
7503
+ * @param {boolean} cancelable false for a close the platform makes anyway
7504
+ * @returns {Promise<boolean>}
7505
+ */
7506
+ #dismiss(reason, cancelable) {
7507
+ return this.#sections.dismiss(
7508
+ new CustomEvent('drawer:dismiss', { cancelable, detail: { reason } }),
7509
+ () => this.close(),
7510
+ this.declared('guard-dismiss') ? discardEdits : null,
7511
+ );
7344
7512
  }
7345
7513
  /**
7346
7514
  * Slides the drawer out and closes it when the animation ends, setting the
7347
7515
  * `closing` attribute on the native dialog meanwhile; the `close` event is
7348
7516
  * dispatched then, after this call has returned. Where the user prefers
7349
- * reduced motion, or no animation runs, it closes at once. Does nothing on
7350
- * a drawer that is closed or already sliding out.
7517
+ * reduced motion, or no animation runs, it closes at once. A non-empty
7518
+ * result answers as the button carrying that `data-result` would; no
7519
+ * result, or an empty one, is a dismissal. Does nothing on a drawer that is
7520
+ * closed or already sliding out.
7521
+ * @param {string} [result]
7351
7522
  */
7352
- close() {
7523
+ close(result) {
7353
7524
  if (this.#closing || !this.#dialog.open) {
7354
7525
  return;
7355
7526
  }
7527
+ if (result) {
7528
+ this.#answer = { dismissed: false, result, response: null };
7529
+ }
7356
7530
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
7357
7531
  this.#dialog.close();
7358
7532
  return;
@@ -8064,6 +8238,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
8064
8238
  'dialog.close': 'Close',
8065
8239
  'dialog.confirm': 'Confirm',
8066
8240
  'dialog.cancel': 'Cancel',
8241
+ 'dialog.discard.header': 'Unsaved changes',
8242
+ 'dialog.discard.body': 'There are unsaved changes: close anyway?',
8243
+ 'dialog.discard.confirm': 'Close without saving',
8244
+ 'dialog.discard.cancel': 'Keep editing',
8067
8245
  'drawer.close': 'Close',
8068
8246
  'spinner.loading': 'Loading…',
8069
8247
  'toast.region': 'Notifications',
@@ -8109,6 +8287,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
8109
8287
  'dialog.close': 'Chiudi',
8110
8288
  'dialog.confirm': 'Conferma',
8111
8289
  'dialog.cancel': 'Annulla',
8290
+ 'dialog.discard.header': 'Modifiche non salvate',
8291
+ 'dialog.discard.body': 'Ci sono modifiche non salvate: chiudere comunque?',
8292
+ 'dialog.discard.confirm': 'Chiudi senza salvare',
8293
+ 'dialog.discard.cancel': 'Continua a modificare',
8112
8294
  'drawer.close': 'Chiudi',
8113
8295
  'spinner.loading': 'Caricamento…',
8114
8296
  'toast.region': 'Notifiche',
@@ -8157,6 +8339,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
8157
8339
  'dialog.close': 'Cerrar',
8158
8340
  'dialog.confirm': 'Confirmar',
8159
8341
  'dialog.cancel': 'Cancelar',
8342
+ 'dialog.discard.header': 'Cambios sin guardar',
8343
+ 'dialog.discard.body': 'Hay cambios sin guardar: ¿cerrar de todos modos?',
8344
+ 'dialog.discard.confirm': 'Cerrar sin guardar',
8345
+ 'dialog.discard.cancel': 'Seguir editando',
8160
8346
  'drawer.close': 'Cerrar',
8161
8347
  'spinner.loading': 'Cargando…',
8162
8348
  'toast.region': 'Notificaciones',
@@ -8205,6 +8391,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
8205
8391
  'dialog.close': 'Fermer',
8206
8392
  'dialog.confirm': 'Confirmer',
8207
8393
  'dialog.cancel': 'Annuler',
8394
+ 'dialog.discard.header': 'Modifications non enregistrées',
8395
+ 'dialog.discard.body': 'Des modifications ne sont pas enregistrées : fermer quand même ?',
8396
+ 'dialog.discard.confirm': 'Fermer sans enregistrer',
8397
+ 'dialog.discard.cancel': 'Continuer à modifier',
8208
8398
  'drawer.close': 'Fermer',
8209
8399
  'spinner.loading': 'Chargement…',
8210
8400
  'toast.region': 'Notifications',