@optionfactory/fml 9.0.0-rc30 → 9.0.0-rc31

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ParsedElement, Attributes, Rendering, Localization, BoundedCache, Fragments, Templates, Nodes } from './ftl.mjs';
1
+ import { ParsedElement, Attributes, Fragments, Rendering, Localization, BoundedCache, Templates, Nodes } from './ftl.mjs';
2
2
  import { Failure, HttpClient } from './httpc.mjs';
3
3
 
4
4
  /**
@@ -798,12 +798,13 @@ class Bindings {
798
798
  *
799
799
  * The claim setters, the validity protocol and the aria wiring all act on the
800
800
  * pieces, so a field with no native control returns a focusable piece of its
801
- * own chrome as the control. The claims, `value` and `focus()` are the
802
- * members that tolerate a not yet rendered element: before the render the
803
- * `value` getter answers what the rendered field would answer from its current
804
- * declared state, and a value written then is held and applied by the render
805
- * (see `_holdValue`). The other members need the pieces and so work only after
806
- * the render.
801
+ * own chrome as the control. The claims, `value`, `focus()` and
802
+ * `setCustomValidity` are the members that tolerate a not yet rendered
803
+ * element: before the render the `value` getter answers what the rendered
804
+ * field would answer from its current declared state, a value written then is
805
+ * held and applied by the render (see `_holdValue`), and a problem reported
806
+ * then is shown by it. The other members need the pieces and so work only
807
+ * after the render.
807
808
  *
808
809
  * The inner controls carry `form=""`, so the host is the only element the form
809
810
  * sees. Enter pressed in an inner `<input>` of any type but `file`, `button`,
@@ -841,6 +842,8 @@ class Field extends ParsedElement {
841
842
  #also = [];
842
843
  /** @type {{ value: any } | null} the value written before the render, which the render applies */
843
844
  #held = null;
845
+ /** @type {{ error: string, context?: string } | null} the problem reported before the render, which the render shows */
846
+ #reported = null;
844
847
  /** Sets the internals' role from the subclass's `static ROLE`. */
845
848
  constructor() {
846
849
  super();
@@ -905,6 +908,11 @@ class Field extends ParsedElement {
905
908
  this.#pendingFocus = null;
906
909
  control.focus(options);
907
910
  }
911
+ if (this.#reported !== null) {
912
+ const { error, context } = this.#reported;
913
+ this.#reported = null;
914
+ this.setCustomValidity(error, context);
915
+ }
908
916
  }
909
917
  /**
910
918
  * @param {EventTarget} el
@@ -984,11 +992,17 @@ class Field extends ParsedElement {
984
992
  * field owning a whole subtree overrides this to route the message to the
985
993
  * inner control the path names.
986
994
  *
987
- * Needs the render: before it there is no error region to write to.
995
+ * Before the render there is no error region to write to: a message is
996
+ * kept (the last one wins) and shown once the field has rendered, and a
997
+ * clear drops the one kept. A field whose render failed shows nothing.
988
998
  * @param {string} [error] the message, empty or missing to clear
989
999
  * @param {string} [context] the path below this field's name, '' on an exact match
990
1000
  */
991
1001
  setCustomValidity(error, context) {
1002
+ if (!this.#fieldError) {
1003
+ this.#reported = error ? { error, context } : null;
1004
+ return;
1005
+ }
992
1006
  Attributes.set(this.#announces ?? this.#control, 'aria-invalid', error ? 'true' : null);
993
1007
  if (!error) {
994
1008
  this.internals.setValidity({});
@@ -1199,11 +1213,16 @@ class Field extends ParsedElement {
1199
1213
  /**
1200
1214
  * Calls `_build(conf)` and wires the pieces it answers. When `_build`
1201
1215
  * returns a promise the pieces are wired once it settles; a field that
1202
- * builds synchronously renders synchronously.
1216
+ * builds synchronously renders synchronously. A default slot holding
1217
+ * nothing but whitespace is emptied first, so the label it becomes is
1218
+ * `:empty` and takes no row.
1203
1219
  * @param {{ slots: Record<string, DocumentFragment> | undefined }} conf
1204
1220
  * @returns {void | Promise<void>}
1205
1221
  */
1206
1222
  render(conf) {
1223
+ if (conf.slots?.default && Fragments.isBlank(conf.slots.default)) {
1224
+ conf.slots.default.replaceChildren();
1225
+ }
1207
1226
  const built = /** @type {any} */ (this._build(conf));
1208
1227
  if (built instanceof Promise) {
1209
1228
  return built.then((pieces) => this.#wire(pieces));
@@ -2543,7 +2562,8 @@ class InputInstant extends Input {
2543
2562
  * size and count limits it declares.
2544
2563
  *
2545
2564
  * The `dropzone` and `item-list` attributes show the dropzone and the list of
2546
- * chosen files. A `dropzone` slot replaces the default dropzone's content, and
2565
+ * chosen files, and `no-input` takes the picker control out of sight, leaving
2566
+ * it to the keyboard and to assistive technology. A `dropzone` slot replaces the default dropzone's content, and
2547
2567
  * an `items` slot holding a template replaces the stock item, rendered with the
2548
2568
  * same `files` overlay so that it reads the File objects it lists. Clicking the
2549
2569
  * dropzone opens the picker, and the host carries a `dragover` attribute while
@@ -2586,6 +2606,7 @@ class InputFile extends Input {
2586
2606
  'multiple:presence',
2587
2607
  'item-list:presence',
2588
2608
  'dropzone:presence',
2609
+ 'no-input:presence',
2589
2610
  'max-files:number',
2590
2611
  'max-file-size:number',
2591
2612
  'max-total-size:number',
@@ -2837,7 +2858,7 @@ class InputFile extends Input {
2837
2858
  * @returns {string[]|string|null}
2838
2859
  */
2839
2860
  get value() {
2840
- const names = Array.from(this._input.files).map((f) => f.name);
2861
+ const names = Array.from(this.files).map((f) => f.name);
2841
2862
  return this.multiple ? names : (names[0] ?? null);
2842
2863
  }
2843
2864
  /**
@@ -2860,7 +2881,7 @@ class InputFile extends Input {
2860
2881
  * The size of the selected files together, in bytes.
2861
2882
  * @returns {number}
2862
2883
  */
2863
- get totalsize() {
2884
+ get totalSize() {
2864
2885
  return Array.from(this.files).reduce((a, f) => a + f.size, 0);
2865
2886
  }
2866
2887
  #maxFiles;
@@ -2935,6 +2956,23 @@ class InputFile extends Input {
2935
2956
  this.#useDropzone = v;
2936
2957
  this.reflectTo('dropzone', v);
2937
2958
  }
2959
+ #noInput;
2960
+ /**
2961
+ * Whether the picker control is taken out of sight, the dropzone and the
2962
+ * item list carrying the field alone; the stylesheet hides it from the
2963
+ * reflected `no-input` attribute. The control stays in the tab order and
2964
+ * keeps the label, the description and the error, and the dropzone shows
2965
+ * the focus ring while it holds the focus.
2966
+ * @returns {boolean}
2967
+ */
2968
+ get noInput() {
2969
+ return this.#noInput;
2970
+ }
2971
+ /** @param {boolean} v */
2972
+ set noInput(v) {
2973
+ this.#noInput = v;
2974
+ this.reflectTo('no-input', v);
2975
+ }
2938
2976
  }
2939
2977
 
2940
2978
  /** the viewport's breathing room when clamping, in pixels */
@@ -4698,9 +4736,7 @@ class RadioGroup extends Field {
4698
4736
  static ROLE = 'radiogroup';
4699
4737
  static template = `
4700
4738
  <fieldset>
4701
- <legend>
4702
- {{{{ slots.default }}}}
4703
- </legend>
4739
+ <legend>{{{{ slots.default }}}}</legend>
4704
4740
  <header data-tpl-if="slots.header">
4705
4741
  {{{{ slots.header }}}}
4706
4742
  </header>
@@ -4748,6 +4784,9 @@ class RadioGroup extends Field {
4748
4784
  radioEls.forEach((el) => {
4749
4785
  el.remove();
4750
4786
  });
4787
+ if (Fragments.isBlank(slots.default)) {
4788
+ slots.default.replaceChildren();
4789
+ }
4751
4790
  const fragment = this.template().withOverlay({ name, slots, inputsAndLabels }).render();
4752
4791
  this.#fieldset = /** @type HTMLElement */ (fragment.firstElementChild);
4753
4792
  this.#firstRadio = fragment.querySelector('input[type=radio]');
@@ -4903,6 +4942,10 @@ class Checkbox extends Field {
4903
4942
  * current `order` in the cycle `asc`, `desc`, `null`. The button does not
4904
4943
  * change its own `order`: whoever handles the event sets it, which `ful-table`
4905
4944
  * does once the sorted page has loaded.
4945
+ *
4946
+ * The stylesheet draws an up arrow for `asc` and a down arrow for `desc`. An
4947
+ * unsorted button shows the up arrow dimmed, `asc` being the order its first
4948
+ * activation asks for.
4906
4949
  */
4907
4950
  class SortButton extends ParsedElement {
4908
4951
  static attributes = ['sorter'];
@@ -5479,18 +5522,19 @@ class Table extends ParsedElement {
5479
5522
  * writing the size is not a request to start loading, which is what
5480
5523
  * `autoload` and `reload()` are for. A write that does not change the size
5481
5524
  * does nothing. A write made before the render is replaced by the declared
5482
- * `page-size` when the table renders.
5525
+ * `page-size` when the table renders, or by ten when that is not a number
5526
+ * above zero.
5483
5527
  *
5484
5528
  * The reload is not awaited: a failure shows the error panel and its
5485
5529
  * rejection is unhandled.
5486
5530
  * @param {number|string|null|undefined} value a number or a string of
5487
5531
  * one, such as a select's value; absent or null is the default of ten,
5488
5532
  * so removing the attribute restores it, and a value that is not a
5489
- * number is ignored
5533
+ * finite number above zero is ignored
5490
5534
  */
5491
5535
  set pageSize(value) {
5492
5536
  const size = Number(value ?? 10);
5493
- if (Number.isNaN(size) || size === this.#latestRequest.pageRequest.size) {
5537
+ if (!Number.isFinite(size) || size <= 0 || size === this.#latestRequest.pageRequest.size) {
5494
5538
  return;
5495
5539
  }
5496
5540
  this.#latestRequest = { ...this.#latestRequest, pageRequest: { page: 0, size } };
@@ -5609,10 +5653,11 @@ class Table extends ParsedElement {
5609
5653
  await Rendering.waitForChildren(this);
5610
5654
 
5611
5655
  const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
5656
+ const declaredSize = this.declared('page-size');
5612
5657
  this.#latestRequest = {
5613
5658
  pageRequest: {
5614
5659
  page: 0,
5615
- size: this.declared('page-size') ?? 10,
5660
+ size: Number.isFinite(declaredSize) && declaredSize > 0 ? declaredSize : 10,
5616
5661
  },
5617
5662
  sortRequest: schema.sort,
5618
5663
  filterRequest: maybeForm?.values ?? {},
@@ -5696,7 +5741,10 @@ class Table extends ParsedElement {
5696
5741
  * `aria-busy` is removed, `load:failure` is dispatched (bubbling, not
5697
5742
  * cancelable) with `detail` set to
5698
5743
  * `{ pageRequest, sortRequest, filterRequest, exception }`, and the
5699
- * returned promise rejects with the same error.
5744
+ * returned promise rejects with the same error. An answer the table cannot
5745
+ * page fails the same way, with an error saying so: one whose `size` is
5746
+ * not a number, as a response of another shape read without a
5747
+ * `response-mapper` is, or a page size of zero.
5700
5748
  *
5701
5749
  * A load started after this one supersedes it: when this one's answer or
5702
5750
  * failure arrives it changes nothing, dispatches nothing and resolves;
@@ -5724,6 +5772,11 @@ class Table extends ParsedElement {
5724
5772
  return;
5725
5773
  }
5726
5774
  lastPage = Math.max(0, Math.ceil(pageResponse.size / pageRequest.size) - 1);
5775
+ if (!Number.isFinite(lastPage)) {
5776
+ throw new Error(
5777
+ `ful-table cannot page the loader's answer: it needs { data, size }, size being the total number of rows, and got a size of ${pageResponse.size} for pages of ${pageRequest.size}. A response of another shape needs a response-mapper`,
5778
+ );
5779
+ }
5727
5780
  if (pageRequest.page <= lastPage) {
5728
5781
  this.#latestRequest = { pageRequest, sortRequest, filterRequest };
5729
5782
  this.#loaded = true;
@@ -6989,8 +7042,10 @@ class Tooltip extends ParsedElement {
6989
7042
  * failure's problems, one per line, or its message when it carries none.
6990
7043
  */
6991
7044
  class SectionRequests {
6992
- /** @type {WeakSet<Element>} */
7045
+ /** @type {WeakSet<Element>} the sections a first delivery succeeded for */
6993
7046
  #entered = new WeakSet();
7047
+ /** @type {WeakSet<Element>} the sections whose first delivery is in flight */
7048
+ #entering = new WeakSet();
6994
7049
  /** @type {WeakMap<Element, Claims>} */
6995
7050
  #claims = new WeakMap();
6996
7051
 
@@ -6999,9 +7054,12 @@ class SectionRequests {
6999
7054
  * is given, then `section:requested:<name>` when a name is given, each
7000
7055
  * bubbling from `host` with the same detail `{ name, section, index, first }`,
7001
7056
  * and awaits every answer attached to any of them. `first` is true until a
7002
- * request for the section is answered by at least one listener, so a request
7003
- * nobody listened to does not spend it. A request with answers removes the
7004
- * error a previous failure painted before it waits.
7057
+ * delivery of the section succeeds: a request nobody listened to does not
7058
+ * spend it, and neither does one whose answers failed, so the next request
7059
+ * is a first one again. While a first delivery is in flight a newer request
7060
+ * for the same section carries `first` false, so the content is not
7061
+ * delivered twice. A request with answers removes the error a previous
7062
+ * failure painted before it waits.
7005
7063
  * @param {Element} host the component the events are dispatched on
7006
7064
  * @param {Element} section the element the answers deliver into
7007
7065
  * @param {string|null} name the section's `data-step`, when it has one
@@ -7012,7 +7070,7 @@ class SectionRequests {
7012
7070
  * problems in the section unless a newer request superseded this one
7013
7071
  */
7014
7072
  async request(host, section, name, index) {
7015
- const first = !this.#entered.has(section);
7073
+ const first = !this.#entered.has(section) && !this.#entering.has(section);
7016
7074
  const detail = { name, section, index, first };
7017
7075
  const types = [
7018
7076
  'section:requested',
@@ -7030,7 +7088,9 @@ class SectionRequests {
7030
7088
  if (promises.length === 0) {
7031
7089
  return undefined;
7032
7090
  }
7033
- this.#entered.add(section);
7091
+ if (first) {
7092
+ this.#entering.add(section);
7093
+ }
7034
7094
  let claims = this.#claims.get(section);
7035
7095
  if (!claims) {
7036
7096
  claims = new Claims();
@@ -7046,13 +7106,20 @@ class SectionRequests {
7046
7106
  }
7047
7107
  });
7048
7108
  try {
7049
- return await Promise.all(promises);
7109
+ const answers = await Promise.all(promises);
7110
+ if (first) {
7111
+ this.#entered.add(section);
7112
+ }
7113
+ return answers;
7050
7114
  } catch (cause) {
7051
7115
  if (owned()) {
7052
7116
  this.#paintError(section, cause);
7053
7117
  }
7054
7118
  throw cause;
7055
7119
  } finally {
7120
+ if (first) {
7121
+ this.#entering.delete(section);
7122
+ }
7056
7123
  cancelAnimationFrame(frame);
7057
7124
  if (owned()) {
7058
7125
  section.toggleAttribute('loading', false);
@@ -7169,15 +7236,27 @@ class DialogSections {
7169
7236
  /**
7170
7237
  * Calls `dismiss` when a press and its release both land on the backdrop,
7171
7238
  * so a selection dragged out of the panel and released outside does not.
7239
+ * The backdrop is where the pointer is outside the dialog's rectangle: a
7240
+ * part of the panel no child covers, the room around the error section
7241
+ * for one, is the panel.
7172
7242
  * @param {() => void} dismiss
7173
7243
  */
7174
7244
  onBackdrop(dismiss) {
7245
+ const onBackdrop = (/** @type MouseEvent */ e) => {
7246
+ if (e.target !== this.#dialog) {
7247
+ return false;
7248
+ }
7249
+ const panel = this.#dialog.getBoundingClientRect();
7250
+ return (
7251
+ e.clientX < panel.left || e.clientX > panel.right || e.clientY < panel.top || e.clientY > panel.bottom
7252
+ );
7253
+ };
7175
7254
  let pressedOutside = false;
7176
7255
  this.#dialog.addEventListener('mousedown', (e) => {
7177
- pressedOutside = e.target === this.#dialog;
7256
+ pressedOutside = onBackdrop(e);
7178
7257
  });
7179
7258
  this.#dialog.addEventListener('click', (e) => {
7180
- if (pressedOutside && e.target === this.#dialog) {
7259
+ if (pressedOutside && onBackdrop(e)) {
7181
7260
  dismiss();
7182
7261
  }
7183
7262
  });
@@ -7454,7 +7533,7 @@ class Dialog extends ParsedElement {
7454
7533
  * opening, hides the error and loading sections, and dispatches
7455
7534
  * `section:requested` on the element (bubbling, with
7456
7535
  * `detail: { section, first, name: null, index: null }`, `section` being
7457
- * the body and `first` true only on the first answered request), so a
7536
+ * the body and `first` true until a delivery succeeds), so a
7458
7537
  * listener can fill the body. A failed request is painted into the body
7459
7538
  * and does not reject. On a dialog already open nothing is shown or
7460
7539
  * requested, and the call waits for the same ending as the calls before it;
@@ -7706,11 +7785,7 @@ class Drawer extends ParsedElement {
7706
7785
  #answer = null;
7707
7786
  #resolvers = [];
7708
7787
  #closing = false;
7709
- #slideOutEnded = (/** @type AnimationEvent */ e) => {
7710
- if (e.target === this.#dialog) {
7711
- this.#closeNow();
7712
- }
7713
- };
7788
+ #slides = new Claims();
7714
7789
  /**
7715
7790
  * @param {{ slots: Record<string, DocumentFragment> }} c
7716
7791
  */
@@ -7837,7 +7912,7 @@ class Drawer extends ParsedElement {
7837
7912
  * opening, hides the error and loading sections, and dispatches
7838
7913
  * `section:requested` on the element (bubbling, with
7839
7914
  * `detail: { section, first, name: null, index: null }`, `section` being
7840
- * the content section and `first` true only on the first answered request),
7915
+ * the content section and `first` true until a delivery succeeds),
7841
7916
  * so a listener can fill the content. A failed request is painted into the
7842
7917
  * content section and does not reject. On a drawer already open nothing is
7843
7918
  * shown or requested, except that one sliding out stops and stays open,
@@ -7888,11 +7963,13 @@ class Drawer extends ParsedElement {
7888
7963
  /**
7889
7964
  * Slides the drawer out and closes it when the animation ends, setting the
7890
7965
  * `closing` attribute on the native dialog meanwhile; the `close` event is
7891
- * dispatched then, after this call has returned. Where the user prefers
7892
- * reduced motion, or no animation runs, it closes at once. A non-empty
7893
- * result answers as the button carrying that `data-result` would; no
7894
- * result, or an empty one, is a dismissal. Does nothing on a drawer that is
7895
- * closed or already sliding out.
7966
+ * dispatched then, after this call has returned. An animation the page
7967
+ * cancels, by hiding the drawer or an ancestor of it, closes it as one
7968
+ * that ended does. Where the user prefers reduced motion, or no animation
7969
+ * runs, it closes at once. A non-empty result answers as the button
7970
+ * carrying that `data-result` would; no result, or an empty one, is a
7971
+ * dismissal. Does nothing on a drawer that is closed or already sliding
7972
+ * out.
7896
7973
  * @param {string} [result]
7897
7974
  */
7898
7975
  close(result) {
@@ -7907,12 +7984,18 @@ class Drawer extends ParsedElement {
7907
7984
  return;
7908
7985
  }
7909
7986
  this.#dialog.setAttribute('closing', '');
7910
- if (this.#dialog.getAnimations().length === 0) {
7987
+ const animations = this.#dialog.getAnimations();
7988
+ if (animations.length === 0) {
7911
7989
  this.#closeNow();
7912
7990
  return;
7913
7991
  }
7914
7992
  this.#closing = true;
7915
- this.#dialog.addEventListener('animationend', this.#slideOutEnded);
7993
+ const slide = this.#slides.take();
7994
+ Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
7995
+ if (!slide.stale) {
7996
+ this.#closeNow();
7997
+ }
7998
+ });
7916
7999
  }
7917
8000
  #closeNow() {
7918
8001
  this.#stopSlidingOut();
@@ -7920,7 +8003,7 @@ class Drawer extends ParsedElement {
7920
8003
  }
7921
8004
  #stopSlidingOut() {
7922
8005
  this.#closing = false;
7923
- this.#dialog.removeEventListener('animationend', this.#slideOutEnded);
8006
+ this.#slides.invalidate();
7924
8007
  this.#dialog.removeAttribute('closing');
7925
8008
  }
7926
8009
  /** @returns {boolean} whether this call is the one that opened the modal */
@@ -8465,7 +8548,7 @@ class Tabs extends ParsedElement {
8465
8548
  }
8466
8549
  /**
8467
8550
  * Fires the section requests again for a panel, whether it is visible or
8468
- * not, with `first` false once an earlier request was answered.
8551
+ * not, with `first` false once an earlier delivery succeeded.
8469
8552
  * @param {number | Element} ref the panel's index, or the panel itself
8470
8553
  * @returns {Promise<any[] | undefined> | undefined} a promise resolving to
8471
8554
  * the answers, or to undefined when nobody listened or the delivery
@@ -8688,7 +8771,7 @@ class Wizard extends ParsedElement {
8688
8771
  }
8689
8772
  /**
8690
8773
  * Fires the section requests again for a section, whether it is current or
8691
- * not, with `first` false once an earlier request was answered.
8774
+ * not, with `first` false once an earlier delivery succeeded.
8692
8775
  * @param {string | Element} ref the section's `data-step`, or the section
8693
8776
  * itself
8694
8777
  * @returns {Promise<any[] | undefined> | undefined} a promise resolving to
@@ -8967,8 +9050,11 @@ class Plugin {
8967
9050
  /**
8968
9051
  * @param {{ language?: string, translations?: Record<string, any>, httpClient?: HttpClient }} [options]
8969
9052
  * `language` is fixed for the page: a full BCP-47 tag or a primary subtag,
8970
- * defaulting to `navigator.language`, then to 'en'. Its primary subtag,
8971
- * lowercased, picks the built-in translations (en, it, es, fr); the messages
9053
+ * defaulting to `navigator.language`, then to 'en', an empty one included.
9054
+ * It is canonicalized (`IT-it` and `it_IT` both read `it-IT`), and one that
9055
+ * is no language tag at all throws here, rather than at the first message
9056
+ * or number formatted in it. Its primary subtag picks the built-in
9057
+ * translations (en, it, es, fr); the messages
8972
9058
  * are the english ones, overridden by that language's, overridden by
8973
9059
  * `translations`, so a language without built-in translations reads in
8974
9060
  * english. `translations` is a flat map of keys to messages: reword built-in
@@ -8976,9 +9062,18 @@ class Plugin {
8976
9062
  * `httpClient` is the client every ful component fetches through, registered
8977
9063
  * as the `http-client` component; without one, `configure` builds a client
8978
9064
  * that sends the csrf token and navigates to '/' on a 401 response.
9065
+ * @throws {RangeError} when `language` is not a language tag
8979
9066
  */
8980
9067
  constructor(options = {}) {
8981
- this.#language = options.language ?? navigator?.language ?? 'en';
9068
+ const language = options.language || navigator?.language || 'en';
9069
+ try {
9070
+ // unicode locale identifiers separate subtags with an underscore too, Intl takes the dash alone
9071
+ this.#language = Intl.getCanonicalLocales(String(language).replaceAll('_', '-'))[0];
9072
+ } catch {
9073
+ throw new RangeError(
9074
+ `Plugin: language '${language}' is not a language tag: pass a BCP-47 tag such as 'it' or 'it-IT', or leave it out for the browser's language`,
9075
+ );
9076
+ }
8982
9077
  this.#translations = options.translations ?? {};
8983
9078
  this.#httpClient = options.httpClient ?? null;
8984
9079
  }
@@ -8987,7 +9082,7 @@ class Plugin {
8987
9082
  * Defines on `registry` the `l10n` module, the `http-client` component,
8988
9083
  * every `ful-*` element, the `loaders:select`, `loaders:form` and
8989
9084
  * `loaders:table` components, and an overlay publishing `l10n` (the merged
8990
- * messages) and `locale` (the `language` as given) to every template.
9085
+ * messages) and `locale` (the `language`, canonicalized) to every template.
8991
9086
  * @param {{ defineModule(name: string, module: any): any, defineComponent(name: string, component: any): any, defineElement(tag: string, klass: CustomElementConstructor): any, defineOverlay(...data: any[]): any }} registry
8992
9087
  * the ftl `Registry`, each of those methods answering the registry itself
8993
9088
  * @returns {void}
@@ -8995,7 +9090,7 @@ class Plugin {
8995
9090
  configure(registry) {
8996
9091
  const httpClient =
8997
9092
  this.#httpClient ?? HttpClient.builder().withCsrfToken().withRedirectOnUnauthorized('/').build();
8998
- const language = this.#language.split('-')[0].toLowerCase();
9093
+ const language = this.#language.split('-')[0];
8999
9094
  const l10n = { ...BUILTIN.en, ...BUILTIN[language], ...this.#translations };
9000
9095
  registry
9001
9096
  .defineModule('l10n', Localization)
@@ -9039,5 +9134,5 @@ class Plugin {
9039
9134
  }
9040
9135
  }
9041
9136
 
9042
- export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, PageLeave, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, TableSchemaParser, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
9137
+ export { Accordion, Anchors, AsyncEvents, Bindings, BooleanFilter, Checkbox, Claims, CompareFilter, Dialog, Drawer, Dropdown, Field, Form, FormLoader, InFilter, Input, InputFile, InputInstant, InputLocalDate, InputLocalTime, Instant, InstantFilter, LocalDate, LocalDateFilter, LocalStorage, Menu, NumberFilter, PageLeave, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Table, TableLoader, Tabs, TextFilter, Timing, Toasts, Tooltip, VersionedLocalStorage, VersionedSessionStorage, Wizard, describable };
9043
9138
  //# sourceMappingURL=ful.mjs.map