@optionfactory/fml 9.0.0-rc16 → 9.0.0-rc19

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
@@ -7848,6 +7848,8 @@ var fml = (function (exports) {
7848
7848
 
7849
7849
  const INPUT_MODES = { numeric: 'numeric', decimal: 'decimal' };
7850
7850
 
7851
+ const SEGMENTED = new Set(['date', 'time', 'datetime-local', 'month', 'week']);
7852
+
7851
7853
  const signed = (v, digits) => (v.startsWith('-') ? '-' : '') + digits(v);
7852
7854
 
7853
7855
  const NUMERIC_DIGITS = {
@@ -7909,14 +7911,18 @@ var fml = (function (exports) {
7909
7911
  * `autocomplete` attribute of the nearest enclosing `<form>`, which the
7910
7912
  * platform does not pass on because the control carries `form=""`.
7911
7913
  * - `uppercase`, `trim` and `v-type` shape the value as it is read; see `value`.
7912
- *
7913
- * The control's native `change` is stopped and republished as the field's own.
7914
+ * - The control's native `change` is stopped and the field dispatches its own
7915
+ * once the user commits a new value, as a text input does. A date, time,
7916
+ * datetime-local, month or week control fires its native `change` while its
7917
+ * segments are typed, as soon as they make a valid value; for those the field
7918
+ * reports when the control is left or Enter is pressed, at once for a value
7919
+ * picked from the browser's picker, and never a half-typed value.
7914
7920
  *
7915
7921
  * A subclass template may render more than one control, as the compare filters
7916
7922
  * render the two bounds of a range: the first is `_input`, the one the field is
7917
- * named by and whose `change` it republishes, and every one of them gets the
7918
- * inputmode, `autocomplete`, the `input-` passthrough, the keystroke filter and
7919
- * the placeholder. The passthrough leaves `input-id` to the first alone, so no
7923
+ * named by, and every one of them gets the inputmode, `autocomplete`, the
7924
+ * `input-` passthrough, the keystroke filter, the placeholder and the `change`
7925
+ * reporting. The passthrough leaves `input-id` to the first alone, so no
7920
7926
  * two controls share an id.
7921
7927
  */
7922
7928
  class Input extends Field {
@@ -7983,10 +7989,6 @@ var fml = (function (exports) {
7983
7989
  for (const control of this.#controls) {
7984
7990
  this.#configure(control, mode, autocomplete, strip);
7985
7991
  }
7986
- this._input.addEventListener('change', (evt) => {
7987
- evt.stopPropagation();
7988
- this._notifyChange();
7989
- });
7990
7992
  return {
7991
7993
  fragment,
7992
7994
  control: this._input,
@@ -8010,6 +8012,7 @@ var fml = (function (exports) {
8010
8012
  if (control !== this._input) {
8011
8013
  Attributes.set(control, 'id', id || null);
8012
8014
  }
8015
+ this.#reportChanges(control);
8013
8016
  control.addEventListener('input', () => {
8014
8017
  if (!strip) {
8015
8018
  return;
@@ -8028,13 +8031,53 @@ var fml = (function (exports) {
8028
8031
  control.setSelectionRange(caret, caret);
8029
8032
  });
8030
8033
  }
8034
+ /** @param {HTMLInputElement|HTMLTextAreaElement} control */
8035
+ #reportChanges(control) {
8036
+ if (!SEGMENTED.has(control.type)) {
8037
+ control.addEventListener('change', (evt) => {
8038
+ evt.stopPropagation();
8039
+ this._notifyChange();
8040
+ });
8041
+ return;
8042
+ }
8043
+ /** @type {string|null} */
8044
+ let committed = null;
8045
+ let typed = false;
8046
+ const commit = () => {
8047
+ typed = false;
8048
+ const now = JSON.stringify(this.value);
8049
+ if (now === committed) {
8050
+ return;
8051
+ }
8052
+ committed = now;
8053
+ this._notifyChange();
8054
+ };
8055
+ control.addEventListener('focus', () => {
8056
+ committed = JSON.stringify(this.value);
8057
+ });
8058
+ control.addEventListener('keydown', (/** @type any */ evt) => {
8059
+ if (evt.key === 'Enter' && !evt.isComposing) {
8060
+ commit();
8061
+ return;
8062
+ }
8063
+ typed = true;
8064
+ });
8065
+ control.addEventListener('blur', commit);
8066
+ control.addEventListener('change', (evt) => {
8067
+ evt.stopPropagation();
8068
+ if (!typed) {
8069
+ commit();
8070
+ }
8071
+ });
8072
+ }
8031
8073
  /**
8032
8074
  * The control's text, shaped in this order: upper cased under `uppercase`,
8033
8075
  * trimmed under `trim`, `null` when that leaves it empty, every `,` read as
8034
8076
  * `.` for `type="decimal"`, and decoded to a number under
8035
8077
  * `v-type="number"`, where text that does not decode is answered as it is.
8036
8078
  * Writing `null`, `undefined` or `''` empties the control; anything else is
8037
- * written as its string. Answers a `string`, a `number` or `null`; typed
8079
+ * written as its string, replacing whatever was being typed, as a native
8080
+ * control's value does. Answers a `string`, a `number` or `null`; typed
8038
8081
  * `any` because subclasses answer other values.
8039
8082
  * @type {any}
8040
8083
  */
@@ -8055,7 +8098,7 @@ var fml = (function (exports) {
8055
8098
  return normalized;
8056
8099
  }
8057
8100
  set value(value) {
8058
- this._input.value = value === '' || value === undefined ? null : value;
8101
+ this._input.value = value === '' || value === undefined || value === null ? '' : `${value}`;
8059
8102
  }
8060
8103
  /**
8061
8104
  * The control's placeholder, `null` when there is none. The control always
@@ -8324,7 +8367,7 @@ var fml = (function (exports) {
8324
8367
  }
8325
8368
  /** @param {string|null|undefined} v an ISO instant; a falsy value empties the input */
8326
8369
  set value(v) {
8327
- this._input.value = v ? Instant.isoToLocal(v) : '';
8370
+ super.value = v ? Instant.isoToLocal(v) : '';
8328
8371
  }
8329
8372
  /**
8330
8373
  * The lower bound as a UTC ISO instant.
@@ -8357,7 +8400,6 @@ var fml = (function (exports) {
8357
8400
  }
8358
8401
  /** @param {string|null} v */
8359
8402
  set step(v) {
8360
-
8361
8403
  this._input.step = v ?? '';
8362
8404
  }
8363
8405
  }
@@ -10234,6 +10276,15 @@ var fml = (function (exports) {
10234
10276
  #query() {
10235
10277
  return this.#editing ? this.#input.value : '';
10236
10278
  }
10279
+ /**
10280
+ * The text typed to search the vocabulary: what the dropdown was last
10281
+ * asked to match. `''` while the field is not being searched, its control
10282
+ * then showing the selection's label rather than a query.
10283
+ * @returns {string}
10284
+ */
10285
+ get query() {
10286
+ return this.#query();
10287
+ }
10237
10288
  #display() {
10238
10289
  const entry = this.#values.values().next().value;
10239
10290
  this.#input.value = this.#multiple ? '' : (entry?.label ?? '');
@@ -11910,10 +11961,6 @@ var fml = (function (exports) {
11910
11961
  },
11911
11962
  });
11912
11963
  this.operators = this.declared('operators');
11913
- this._value2.addEventListener('change', (evt) => {
11914
- evt.stopPropagation();
11915
- this._notifyChange();
11916
- });
11917
11964
  if (this._operator.value === null) {
11918
11965
  this._showDefaultOperator();
11919
11966
  }
@@ -12391,9 +12438,7 @@ var fml = (function (exports) {
12391
12438
  */
12392
12439
  get criterion() {
12393
12440
  const token = this._value.value;
12394
- return asCriterion(labelTextOf(this), this._operator.value, [
12395
- token === '' ? '' : booleanValueLabel(token),
12396
- ]);
12441
+ return asCriterion(labelTextOf(this), this._operator.value, [token === '' ? '' : booleanValueLabel(token)]);
12397
12442
  }
12398
12443
  /**
12399
12444
  * The field's own disabled claim. Setting it disables the value button and
@@ -12682,6 +12727,18 @@ var fml = (function (exports) {
12682
12727
  get section() {
12683
12728
  return this.#section;
12684
12729
  }
12730
+ /**
12731
+ * Dispatches the dismissal on the host and calls `close` unless a listener
12732
+ * prevented it. A dismissal that is not cancelable announces a close the
12733
+ * platform makes whatever the page does, so `close` is not called for it.
12734
+ * @param {CustomEvent} asked
12735
+ * @param {() => void} close
12736
+ */
12737
+ dismiss(asked, close) {
12738
+ if (this.#host.dispatchEvent(asked) && asked.cancelable) {
12739
+ close();
12740
+ }
12741
+ }
12685
12742
  /**
12686
12743
  * Calls `dismiss` when a press and its release both land on the backdrop,
12687
12744
  * so a selection dragged out of the panel and released outside does not.
@@ -12820,6 +12877,18 @@ var fml = (function (exports) {
12820
12877
  * and a press and release both on the backdrop close the dialog, each as a
12821
12878
  * dismissal, as Escape does.
12822
12879
  *
12880
+ * Before any of those three gestures closes it, the element dispatches a
12881
+ * cancelable, non-bubbling `dialog:dismiss` with `detail: { reason }`, the
12882
+ * reason being `button`, `backdrop` or `escape`; a listener calling
12883
+ * `preventDefault()` keeps it open, to guard unsaved edits. A `data-result`
12884
+ * button, `close-on-submit` and `close()` answer rather than dismiss, and
12885
+ * dispatch none.
12886
+ *
12887
+ * The platform lets a page refuse Escape once per user interaction: a second
12888
+ * Escape with no click or keystroke in between closes the dialog whatever the
12889
+ * page does, `requires-answer` included. That close is still announced by a
12890
+ * `dialog:dismiss`, which is then not cancelable.
12891
+ *
12823
12892
  * `requires-answer` withholds the close button and refuses Escape and the
12824
12893
  * backdrop, so the dialog is closed only by a button carrying `data-result`
12825
12894
  * or from code through `close()`.
@@ -12892,21 +12961,32 @@ var fml = (function (exports) {
12892
12961
  this.#dialog.close(result);
12893
12962
  }
12894
12963
  });
12964
+ const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
12965
+ this.#sections.dismiss(
12966
+ new CustomEvent('dialog:dismiss', { cancelable: !platformCloses, detail: { reason } }),
12967
+ () => this.#dialog.close(''),
12968
+ );
12895
12969
  if (!requiresAnswer) {
12896
- this.#sections.onBackdrop(() => this.#dialog.close(''));
12970
+ this.#sections.onBackdrop(() => dismiss('backdrop'));
12897
12971
  }
12898
- fragment
12899
- .querySelector('[data-ref=close]')
12900
- ?.addEventListener('click', () => this.#dialog.close(''));
12972
+ fragment.querySelector('[data-ref=close]')?.addEventListener('click', () => dismiss('button'));
12901
12973
  if (closeOnSubmit) {
12902
12974
  this.#sections.onSubmitted((response) => {
12903
12975
  this.#answer = { dismissed: false, result: null, response };
12904
12976
  this.#dialog.close('submitted');
12905
12977
  });
12906
12978
  }
12907
- if (requiresAnswer) {
12908
- this.#dialog.addEventListener('cancel', (/** @type any */ e) => e.preventDefault());
12909
- }
12979
+ this.#dialog.addEventListener('cancel', (/** @type any */ e) => {
12980
+ if (!e.cancelable) {
12981
+ this.#dialog.returnValue = '';
12982
+ dismiss('escape', true);
12983
+ return;
12984
+ }
12985
+ e.preventDefault();
12986
+ if (!requiresAnswer) {
12987
+ dismiss('escape');
12988
+ }
12989
+ });
12910
12990
  this.replaceChildren(fragment);
12911
12991
  wireTargets();
12912
12992
  }
@@ -13107,7 +13187,14 @@ var fml = (function (exports) {
13107
13187
  * default, when absent) or `start`, following the writing direction.
13108
13188
  *
13109
13189
  * The close button, Escape, and a press and release both on the backdrop all
13110
- * go through `close()`. Every close dispatches a non-bubbling `close` event on
13190
+ * go through `close()`, once a cancelable, non-bubbling `drawer:dismiss` with
13191
+ * `detail: { reason }` (`button`, `backdrop` or `escape`) has been dispatched
13192
+ * on the element and no listener prevented it: `preventDefault()` keeps the
13193
+ * drawer open, to guard unsaved edits. `close()` and `close-on-submit`
13194
+ * dispatch none. The platform lets a page refuse Escape once per user
13195
+ * interaction: a second Escape with no click or keystroke in between closes
13196
+ * the drawer at once, without the slide, and its `drawer:dismiss` is then not
13197
+ * cancelable. Every close dispatches a non-bubbling `close` event on
13111
13198
  * the element with `detail: { dismissed, response }`: `{ dismissed: false,
13112
13199
  * response }` when a form closed it under `close-on-submit`, and
13113
13200
  * `{ dismissed: true, response: null }` for any other close.
@@ -13163,13 +13250,23 @@ var fml = (function (exports) {
13163
13250
  if (placement) {
13164
13251
  this.#dialog.setAttribute('placement', placement);
13165
13252
  }
13253
+ const dismiss = (/** @type {'button'|'backdrop'|'escape'} */ reason, platformCloses = false) =>
13254
+ this.#sections.dismiss(
13255
+ new CustomEvent('drawer:dismiss', { cancelable: !platformCloses, detail: { reason } }),
13256
+ () => this.close(),
13257
+ );
13166
13258
  /** @type {HTMLElement} */ (fragment.querySelector('[data-ref=close]')).addEventListener('click', () =>
13167
- this.close(),
13259
+ dismiss('button'),
13168
13260
  );
13169
- this.#sections.onBackdrop(() => this.close());
13261
+ this.#sections.onBackdrop(() => dismiss('backdrop'));
13170
13262
  this.#dialog.addEventListener('cancel', (e) => {
13263
+ if (!e.cancelable) {
13264
+ this.#stopSlidingOut();
13265
+ dismiss('escape', true);
13266
+ return;
13267
+ }
13171
13268
  e.preventDefault();
13172
- this.close();
13269
+ dismiss('escape');
13173
13270
  });
13174
13271
  this.#dialog.addEventListener('close', () => {
13175
13272
  this.dispatchEvent(
@@ -13852,11 +13949,9 @@ var fml = (function (exports) {
13852
13949
  }
13853
13950
  /**
13854
13951
  * Moves to the section carrying `data-step="<ref>"`. A move makes that step
13855
- * and section current, moves the focus to the section, dispatches `change`
13856
- * on the host (not bubbling) with detail `{ index, step }`, then fires the
13857
- * section requests for it. A native `change` of a control inside a section
13858
- * bubbles through the host under the same name, so a listener tells the
13859
- * two apart by `e.target`.
13952
+ * and section current, moves the focus to the section, dispatches
13953
+ * `wizard:change` on the host (not bubbling) with detail `{ index, step }`,
13954
+ * then fires the section requests for it.
13860
13955
  * @param {string} ref the section's `data-step`
13861
13956
  * @returns {Promise<any[] | undefined> | undefined} a promise resolving to
13862
13957
  * the answers once they are delivered, or to undefined when nobody
@@ -13913,7 +14008,7 @@ var fml = (function (exports) {
13913
14008
  this.#apply(clamped);
13914
14009
  this.#sections[this.#index].focus();
13915
14010
  if (this.rendered) {
13916
- this.dispatchEvent(new CustomEvent('change', { detail: { index: this.#index, step: this.step } }));
14011
+ this.dispatchEvent(new CustomEvent('wizard:change', { detail: { index: this.#index, step: this.step } }));
13917
14012
  }
13918
14013
  return this.#enter(clamped);
13919
14014
  }