@optionfactory/fml 8.0.0 → 8.0.2

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.
Files changed (48) hide show
  1. package/dist/client-errors.iife.js +11 -8
  2. package/dist/client-errors.iife.js.map +1 -1
  3. package/dist/client-errors.iife.min.js +1 -1
  4. package/dist/client-errors.iife.min.js.map +1 -1
  5. package/dist/custom-elements.json +1210 -0
  6. package/dist/fml.css +10 -1
  7. package/dist/fml.css.map +1 -1
  8. package/dist/fml.d.mts +73 -13
  9. package/dist/fml.iife.js +628 -209
  10. package/dist/fml.iife.js.map +1 -1
  11. package/dist/fml.iife.min.js +1 -1
  12. package/dist/fml.iife.min.js.map +1 -1
  13. package/dist/fml.min.mjs +1 -1
  14. package/dist/fml.min.mjs.map +1 -1
  15. package/dist/fml.mjs +628 -209
  16. package/dist/fml.mjs.map +1 -1
  17. package/dist/ftl.d.mts +2 -1
  18. package/dist/ftl.iife.js +90 -34
  19. package/dist/ftl.iife.js.map +1 -1
  20. package/dist/ftl.iife.min.js +1 -1
  21. package/dist/ftl.iife.min.js.map +1 -1
  22. package/dist/ftl.min.mjs +1 -1
  23. package/dist/ftl.min.mjs.map +1 -1
  24. package/dist/ftl.mjs +90 -34
  25. package/dist/ftl.mjs.map +1 -1
  26. package/dist/ful.css +10 -1
  27. package/dist/ful.css.map +1 -1
  28. package/dist/ful.d.mts +22 -6
  29. package/dist/ful.iife.js +496 -166
  30. package/dist/ful.iife.js.map +1 -1
  31. package/dist/ful.iife.min.js +1 -1
  32. package/dist/ful.iife.min.js.map +1 -1
  33. package/dist/ful.min.mjs +1 -1
  34. package/dist/ful.min.mjs.map +1 -1
  35. package/dist/ful.mjs +496 -166
  36. package/dist/ful.mjs.map +1 -1
  37. package/dist/httpc.d.mts +49 -6
  38. package/dist/httpc.iife.js +40 -9
  39. package/dist/httpc.iife.js.map +1 -1
  40. package/dist/httpc.iife.min.js +1 -1
  41. package/dist/httpc.iife.min.js.map +1 -1
  42. package/dist/httpc.min.mjs +1 -1
  43. package/dist/httpc.min.mjs.map +1 -1
  44. package/dist/httpc.mjs +40 -9
  45. package/dist/httpc.mjs.map +1 -1
  46. package/dist/vscode.html-custom-data.json +357 -0
  47. package/dist/web-types.json +1006 -0
  48. package/package.json +11 -9
package/dist/fml.iife.js CHANGED
@@ -3460,7 +3460,7 @@ var fml = (function (exports) {
3460
3460
  case '<=':
3461
3461
  return lhs <= rhs;
3462
3462
  default:
3463
- throw new Error('unknown cmp op ' + node.op);
3463
+ throw new Error(`unknown cmp op ${node.op}`);
3464
3464
  }
3465
3465
  }
3466
3466
  [nodes.call](node) {
@@ -3471,7 +3471,7 @@ var fml = (function (exports) {
3471
3471
  }
3472
3472
  const fn = module[fnRef.value];
3473
3473
  if (!fn) {
3474
- throw new Error(`Function "#${fnRef.module === null ? '' : fnRef.module + ':'}${fnRef.value}" not found`);
3474
+ throw new Error(`Function "#${fnRef.module === null ? '' : `${fnRef.module}:`}${fnRef.value}" not found`);
3475
3475
  }
3476
3476
  const args = node.args.map((arg) => this.visit(arg));
3477
3477
  return fn.apply(this.#resolve_proxy(), args);
@@ -3507,14 +3507,14 @@ var fml = (function (exports) {
3507
3507
  return cond ? cond : this.visit(node.ifFalse);
3508
3508
  }
3509
3509
  [nodes.access](node) {
3510
- let prev = undefined;
3510
+ let prev ;
3511
3511
  let cur = this.visit(node.lhs);
3512
3512
  for (let i = 0; i !== node.rhs.length; ++i) {
3513
3513
  const rhs = node.rhs[i];
3514
3514
  if (rhs.ns && cur == null) {
3515
3515
  return undefined;
3516
3516
  }
3517
- let value = undefined;
3517
+ let value ;
3518
3518
  switch (rhs.type) {
3519
3519
  case nodes.member: {
3520
3520
  value = cur[rhs.rhs];
@@ -3555,7 +3555,7 @@ var fml = (function (exports) {
3555
3555
  case nodes.templated.ten:
3556
3556
  return { type: nodes.dom.n, value: this.visit(node.value) };
3557
3557
  default:
3558
- throw new Error('unknown node type ' + node.type.toString());
3558
+ throw new Error(`unknown node type ${node.type.toString()}`);
3559
3559
  }
3560
3560
  });
3561
3561
  }
@@ -3731,7 +3731,7 @@ var fml = (function (exports) {
3731
3731
  .forEach((a) => {
3732
3732
  const target = a.substring(prefix.length);
3733
3733
  if (target === 'class') {
3734
- const classes = from.getAttribute(prefix + 'class')?.split(/\s+/).filter((a) => a.length) ?? [];
3734
+ const classes = from.getAttribute(`${prefix}class`)?.split(/\s+/).filter((a) => a.length) ?? [];
3735
3735
  to.classList.add(...classes);
3736
3736
  return;
3737
3737
  }
@@ -3823,8 +3823,7 @@ var fml = (function (exports) {
3823
3823
  * @returns
3824
3824
  */
3825
3825
  static isParsed(el) {
3826
- //@ts-ignore
3827
- for (let c = el; c; c = c.parentNode) {
3826
+ for (let c = /** @type {Node | null} */ (el); c; c = c.parentNode) {
3828
3827
  if (c.nextSibling) {
3829
3828
  return true;
3830
3829
  }
@@ -3887,19 +3886,18 @@ var fml = (function (exports) {
3887
3886
  }
3888
3887
 
3889
3888
  class NodeOperations {
3890
- #forRemoval;
3891
- constructor() {
3892
- this.#forRemoval = [];
3893
- }
3889
+ #forRemoval = new Set();
3894
3890
  removed(node) {
3895
- return this.#forRemoval.includes(node);
3891
+ return this.#forRemoval.has(node);
3896
3892
  }
3897
3893
  remove(node) {
3898
3894
  node.replaceChildren?.();
3899
3895
  Object.keys(node.dataset || {})
3900
3896
  .filter((k) => k.startsWith('tpl'))
3901
- .forEach((k) => delete node.dataset[k]);
3902
- this.#forRemoval.push(node);
3897
+ .forEach((k) => {
3898
+ delete node.dataset[k];
3899
+ });
3900
+ this.#forRemoval.add(node);
3903
3901
  }
3904
3902
  popData(node, key) {
3905
3903
  const v = node.dataset[key];
@@ -3917,9 +3915,10 @@ var fml = (function (exports) {
3917
3915
  this.remove(ref);
3918
3916
  }
3919
3917
  cleanup() {
3920
- while (this.#forRemoval.length) {
3921
- this.#forRemoval.pop().remove();
3918
+ for (const node of this.#forRemoval) {
3919
+ node.remove();
3922
3920
  }
3921
+ this.#forRemoval.clear();
3923
3922
  }
3924
3923
  }
3925
3924
 
@@ -3974,7 +3973,7 @@ var fml = (function (exports) {
3974
3973
  }
3975
3974
  static tplRemove(node, value, ops, modules, dataStack) {
3976
3975
  switch (value.toLowerCase()) {
3977
- case 'tag':
3976
+ case 'tag': {
3978
3977
  const fragment = new DocumentFragment();
3979
3978
  while (node.firstChild) {
3980
3979
  fragment.appendChild(node.firstChild);
@@ -3988,6 +3987,7 @@ var fml = (function (exports) {
3988
3987
  ops.remove(node);
3989
3988
  }
3990
3989
  break;
3990
+ }
3991
3991
  case 'body':
3992
3992
  node.replaceChildren();
3993
3993
  break;
@@ -4053,8 +4053,10 @@ var fml = (function (exports) {
4053
4053
  }
4054
4054
  }
4055
4055
 
4056
- // Module-isolated string cache for dataset-to-attribute conversions
4056
+ // Module-isolated string cache for dataset-to-attribute conversions, bounded with
4057
+ // FIFO eviction like the expression ast cache
4057
4058
  const attributeCache = new Map();
4059
+ const ATTRIBUTE_CACHE_MAX_SIZE = 1000;
4058
4060
 
4059
4061
  /**
4060
4062
  * Converts a tpl camelCase dataset key into a kebab-case attribute name.
@@ -4065,6 +4067,9 @@ var fml = (function (exports) {
4065
4067
  function toAttr(dataSetKey) {
4066
4068
  let cached = attributeCache.get(dataSetKey);
4067
4069
  if (!cached) {
4070
+ if (attributeCache.size >= ATTRIBUTE_CACHE_MAX_SIZE) {
4071
+ attributeCache.delete(attributeCache.keys().next().value);
4072
+ }
4068
4073
  cached = dataSetKey
4069
4074
  .substring(3)
4070
4075
  .split(/(?=[A-Z])/)
@@ -4199,9 +4204,10 @@ var fml = (function (exports) {
4199
4204
  * @param {string} expression
4200
4205
  * @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode]
4201
4206
  * @param {...*} data
4207
+ * @returns the evaluated expression result
4202
4208
  */
4203
4209
  evaluate(expression, mode, ...data) {
4204
- new ExpressionEvaluator(this.#modules, this.#dataStack).withOverlay(...data).evaluate(expression, mode);
4210
+ return new ExpressionEvaluator(this.#modules, this.#dataStack).withOverlay(...data).evaluate(expression, mode);
4205
4211
  }
4206
4212
  /**
4207
4213
  * Returns an expression evaluator with bound modules and dataStack.
@@ -4346,8 +4352,8 @@ var fml = (function (exports) {
4346
4352
  if (node.nodeType === Node.TEXT_NODE) {
4347
4353
  node.nodeValue = node.nodeValue.trim();
4348
4354
  }
4349
- for (var n = 0; n < node.childNodes.length; n++) {
4350
- var child = node.childNodes[n];
4355
+ for (let n = 0; n < node.childNodes.length; n++) {
4356
+ const child = node.childNodes[n];
4351
4357
  if (child.nodeType === Node.TEXT_NODE) {
4352
4358
  child.nodeValue = child.nodeValue.trim();
4353
4359
  if (child.nodeValue.length === 0) {
@@ -4366,8 +4372,7 @@ var fml = (function (exports) {
4366
4372
  #q = new Map();
4367
4373
  constructor() {
4368
4374
  document.addEventListener('DOMContentLoaded', async () => {
4369
- const pending = Array.from(this.entries).map(([child, promise]) => promise);
4370
- await Promise.all(pending);
4375
+ await this.settle();
4371
4376
  document.dispatchEvent(
4372
4377
  new CustomEvent('ftl:ready', {
4373
4378
  bubbles: false,
@@ -4376,16 +4381,39 @@ var fml = (function (exports) {
4376
4381
  );
4377
4382
  });
4378
4383
  }
4384
+ #finished = new Map();
4379
4385
  enqueue(el) {
4380
4386
  if (this.#q.has(el)) {
4381
4387
  //already upgrading, can happen when disconnecting an element
4382
4388
  //while it's already queued for upgrade (e.g.: ful-form)
4383
4389
  return;
4384
4390
  }
4391
+ //settling waits on a signal that only ever resolves, so nothing here attaches a
4392
+ //rejection handler to the upgrade itself: a component that fails is still
4393
+ //reported the way it always was
4394
+ const { promise: finished, resolve: markFinished } = /** @type {PromiseWithResolvers<void>} */ (
4395
+ Promise.withResolvers()
4396
+ );
4385
4397
  const promise = Nodes.waitParsed(el)
4386
4398
  .then(() => el.upgrade())
4387
- .finally(() => this.#q.delete(el));
4399
+ .finally(() => {
4400
+ this.#q.delete(el);
4401
+ this.#finished.delete(el);
4402
+ markFinished();
4403
+ });
4388
4404
  this.#q.set(el, promise);
4405
+ this.#finished.set(el, finished);
4406
+ }
4407
+ /**
4408
+ * Waits for every queued upgrade to settle, including the ones enqueued while
4409
+ * waiting: a component is only queued once its parent connects it, so a single pass
4410
+ * would miss everything nested. A component that fails to upgrade does not hold the
4411
+ * others back, readiness means the queue drained rather than that everything worked.
4412
+ */
4413
+ async settle() {
4414
+ while (this.#finished.size !== 0) {
4415
+ await Promise.all(Array.from(this.#finished.values()));
4416
+ }
4389
4417
  }
4390
4418
  get entries() {
4391
4419
  return this.#q.entries();
@@ -4581,18 +4609,30 @@ var fml = (function (exports) {
4581
4609
  }
4582
4610
  }
4583
4611
 
4584
- class Rendering {
4585
- static async waitFor(el) {
4612
+ /**
4613
+ * Waits for the queued upgrades matching the filter, including the ones enqueued while
4614
+ * waiting: a component is only queued once its parent connects it, so a single pass
4615
+ * would miss everything nested.
4616
+ * @param {(el: Element) => boolean} accept
4617
+ */
4618
+ const settle = async (accept) => {
4619
+ for (;;) {
4586
4620
  const pending = Array.from(registry.upgrades)
4587
- .filter(([child, promise]) => el.contains(child))
4588
- .map(([child, promise]) => promise);
4621
+ .filter(([child]) => accept(child))
4622
+ .map(([, promise]) => promise);
4623
+ if (pending.length === 0) {
4624
+ return;
4625
+ }
4589
4626
  await Promise.all(pending);
4590
4627
  }
4628
+ };
4629
+
4630
+ class Rendering {
4631
+ static async waitFor(el) {
4632
+ await settle((child) => el.contains(child));
4633
+ }
4591
4634
  static async waitForChildren(el) {
4592
- const pending = Array.from(registry.upgrades)
4593
- .filter(([child, promise]) => el !== child && el.contains(child))
4594
- .map(([child, promise]) => promise);
4595
- await Promise.all(pending);
4635
+ await settle((child) => el !== child && el.contains(child));
4596
4636
  }
4597
4637
  }
4598
4638
 
@@ -4634,7 +4674,7 @@ var fml = (function (exports) {
4634
4674
  template(name) {
4635
4675
  const { modules, data } = registry.context();
4636
4676
  let t = this.#bits().TEMPLATES[name ?? 'default'].withData(data).withModules(modules);
4637
- for (let k of ['l10n', 'config']) {
4677
+ for (const k of ['l10n', 'config']) {
4638
4678
  const v = this.constructor[k];
4639
4679
  if (v) {
4640
4680
  t = t.withOverlay({ [k]: v });
@@ -4664,6 +4704,19 @@ var fml = (function (exports) {
4664
4704
  return;
4665
4705
  }
4666
4706
  Reflect.set(this, 'disabled', disabled);
4707
+ if (disabled) {
4708
+ this.#unclaimFormDisabled();
4709
+ }
4710
+ }
4711
+ //a disabled ancestor fieldset already matches the element through :disabled, and
4712
+ //an attribute of its own would keep the element disabled once the fieldset is
4713
+ //re-enabled, as formDisabledCallback(false) is only delivered on an actual state
4714
+ //change. a claim of its own made while already disabled by ancestry is safe: the
4715
+ //platform fires no callback for it, so it survives the fieldset being re-enabled
4716
+ #unclaimFormDisabled() {
4717
+ if (this.closest('fieldset:disabled')) {
4718
+ this.removeAttribute('disabled');
4719
+ }
4667
4720
  }
4668
4721
  async upgrade() {
4669
4722
  if (this.#parsed) {
@@ -4679,6 +4732,9 @@ var fml = (function (exports) {
4679
4732
  );
4680
4733
  const disabled = this.#disabledBeforeParsed ?? false;
4681
4734
  await this.render({ slots, observed, disabled });
4735
+ if (disabled) {
4736
+ this.#unclaimFormDisabled();
4737
+ }
4682
4738
  }
4683
4739
  render(c) {}
4684
4740
  reflect(fn) {
@@ -4775,13 +4831,22 @@ var fml = (function (exports) {
4775
4831
 
4776
4832
  let vi = 0;
4777
4833
  let si = 0;
4778
- while (vi < str.length * 0.75) {
4834
+ //every group yields three bytes, but a padded input stops short of the last
4835
+ //one or two: writing them anyway would rely on typed arrays dropping writes
4836
+ //past their length
4837
+ while (vi < nbytes) {
4779
4838
  const v1 = d.indexOf(str.charAt(si++));
4780
4839
  const v2 = d.indexOf(str.charAt(si++));
4781
4840
  const v3 = d.indexOf(str.charAt(si++));
4782
4841
  const v4 = d.indexOf(str.charAt(si++));
4783
4842
  view[vi++] = (v1 << 2) | (v2 >> 4);
4843
+ if (vi === nbytes) {
4844
+ break;
4845
+ }
4784
4846
  view[vi++] = ((v2 & 15) << 4) | (v3 >> 2);
4847
+ if (vi === nbytes) {
4848
+ break;
4849
+ }
4785
4850
  view[vi++] = ((v3 & 3) << 6) | v4;
4786
4851
  }
4787
4852
 
@@ -4808,7 +4873,7 @@ var fml = (function (exports) {
4808
4873
  return Array.from(bytes)
4809
4874
  .map((b) => b.toString(16))
4810
4875
  .map((b) => (upper ? b.toUpperCase() : b))
4811
- .map((o) => o.padStart(2, 0))
4876
+ .map((o) => o.padStart(2, '0'))
4812
4877
  .join('');
4813
4878
  }
4814
4879
  }
@@ -5138,6 +5203,26 @@ var fml = (function (exports) {
5138
5203
  }
5139
5204
  };
5140
5205
 
5206
+ /**
5207
+ * Yields the entries of a headers or params initializer preserving nullish values:
5208
+ * normalizing through `Headers`/`URLSearchParams` first would stringify them to
5209
+ * "null"/"undefined" instead of removing the key.
5210
+ * @param {any} source
5211
+ * @returns {Iterable<[string, any]>}
5212
+ */
5213
+ const rawEntries = (source) => {
5214
+ if (source == null) {
5215
+ return [];
5216
+ }
5217
+ if (typeof source === 'string') {
5218
+ return new URLSearchParams(source);
5219
+ }
5220
+ if (typeof source[Symbol.iterator] === 'function') {
5221
+ return source;
5222
+ }
5223
+ return Object.entries(source);
5224
+ };
5225
+
5141
5226
  class HttpRequestBuilder {
5142
5227
  #client;
5143
5228
  #method;
@@ -5190,11 +5275,11 @@ var fml = (function (exports) {
5190
5275
  }
5191
5276
  /**
5192
5277
  * Add all passed headers to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed.
5193
- * @param {HeadersInit} hs
5278
+ * @param {HeadersInit|Record<string,string|null|undefined>} hs
5194
5279
  * @returns {HttpRequestBuilder} this builder
5195
5280
  */
5196
5281
  headers(hs) {
5197
- for (const [k, v] of new Headers(hs).entries()) {
5282
+ for (const [k, v] of rawEntries(hs)) {
5198
5283
  if (v == null) {
5199
5284
  this.#headers.delete(k);
5200
5285
  } else {
@@ -5219,11 +5304,11 @@ var fml = (function (exports) {
5219
5304
  }
5220
5305
  /**
5221
5306
  * Add all query parameters to the request, overriding existing ones if that key already exists. Null and undefined values cause the key to be removed
5222
- * @param {URLSearchParams|Record<string,string>|string[][]|string} ps
5307
+ * @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
5223
5308
  * @returns {HttpRequestBuilder} this builder
5224
5309
  */
5225
5310
  params(ps) {
5226
- for (const [k, v] of new URLSearchParams(ps).entries()) {
5311
+ for (const [k, v] of rawEntries(ps)) {
5227
5312
  if (v == null) {
5228
5313
  this.#params.delete(k);
5229
5314
  } else {
@@ -5239,8 +5324,10 @@ var fml = (function (exports) {
5239
5324
  * @returns {HttpRequestBuilder} this builder
5240
5325
  */
5241
5326
  param(k, ...vs) {
5327
+ //overriding, as header, headers and params all do: pass every value in one
5328
+ //call to get a multi valued parameter
5329
+ this.#params.delete(k);
5242
5330
  if (vs.length === 0 || vs[0] == null) {
5243
- this.#params.delete(k);
5244
5331
  return this;
5245
5332
  }
5246
5333
  for (const v of vs) {
@@ -5273,7 +5360,7 @@ var fml = (function (exports) {
5273
5360
  /**
5274
5361
  * Sets the request body as a FormData configured using the callback.
5275
5362
  * `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
5276
- * @param {function(HttpMultipartRequestCustomizer):void} callback
5363
+ * @param {(c: HttpMultipartRequestCustomizer) => void} callback
5277
5364
  */
5278
5365
  multipart(callback) {
5279
5366
  const formData = new FormData();
@@ -5438,7 +5525,7 @@ var fml = (function (exports) {
5438
5525
  * @returns this builder
5439
5526
  */
5440
5527
  blobs(name, values) {
5441
- for (let v of values) {
5528
+ for (const v of values) {
5442
5529
  this.#formData.append(name, v);
5443
5530
  }
5444
5531
  return this;
@@ -5473,7 +5560,16 @@ var fml = (function (exports) {
5473
5560
  }
5474
5561
  static load(k) {
5475
5562
  const got = localStorage.getItem(k);
5476
- return got === null ? undefined : JSON.parse(got);
5563
+ if (got === null) {
5564
+ return undefined;
5565
+ }
5566
+ try {
5567
+ return JSON.parse(got);
5568
+ } catch {
5569
+ //not what save wrote: drop it, otherwise every later read fails the same way
5570
+ localStorage.removeItem(k);
5571
+ return undefined;
5572
+ }
5477
5573
  }
5478
5574
  static remove(k) {
5479
5575
  localStorage.removeItem(k);
@@ -5491,7 +5587,16 @@ var fml = (function (exports) {
5491
5587
  }
5492
5588
  static load(k) {
5493
5589
  const got = sessionStorage.getItem(k);
5494
- return got === null ? undefined : JSON.parse(got);
5590
+ if (got === null) {
5591
+ return undefined;
5592
+ }
5593
+ try {
5594
+ return JSON.parse(got);
5595
+ } catch {
5596
+ //not what save wrote: drop it, otherwise every later read fails the same way
5597
+ sessionStorage.removeItem(k);
5598
+ return undefined;
5599
+ }
5495
5600
  }
5496
5601
  static remove(k) {
5497
5602
  sessionStorage.removeItem(k);
@@ -5513,7 +5618,7 @@ var fml = (function (exports) {
5513
5618
  return undefined;
5514
5619
  }
5515
5620
  if (stored.revision !== revision) {
5516
- localStorage.removeItem(key);
5621
+ LocalStorage.remove(key);
5517
5622
  return undefined;
5518
5623
  }
5519
5624
  return stored.data;
@@ -5530,7 +5635,7 @@ var fml = (function (exports) {
5530
5635
  return undefined;
5531
5636
  }
5532
5637
  if (stored.revision !== revision) {
5533
- localStorage.removeItem(key);
5638
+ SessionStorage.remove(key);
5534
5639
  return undefined;
5535
5640
  }
5536
5641
  return stored.data;
@@ -5665,7 +5770,7 @@ var fml = (function (exports) {
5665
5770
  */
5666
5771
  static debounce(timeoutMs, func, options) {
5667
5772
  const opts = options ?? Timing.DEBOUNCE_DEFAULT;
5668
- let tid = null;
5773
+ let tid = /** @type {number | null} */ (null);
5669
5774
  let args = [];
5670
5775
  let previousTimestamp = 0;
5671
5776
 
@@ -5685,8 +5790,8 @@ var fml = (function (exports) {
5685
5790
  }
5686
5791
  };
5687
5792
 
5688
- const debounced = function () {
5689
- args = [...arguments];
5793
+ const debounced = (...called) => {
5794
+ args = called;
5690
5795
  previousTimestamp = performance.now();
5691
5796
  if (tid === null) {
5692
5797
  tid = setTimeout(later, timeoutMs);
@@ -5695,7 +5800,11 @@ var fml = (function (exports) {
5695
5800
  }
5696
5801
  }
5697
5802
  };
5698
- const abort = () => clearTimeout(tid);
5803
+ const abort = () => {
5804
+ clearTimeout(tid ?? undefined);
5805
+ tid = null;
5806
+ args = [];
5807
+ };
5699
5808
  return [debounced, abort];
5700
5809
  }
5701
5810
  static THROTTLE_DEFAULT = 0;
@@ -5710,7 +5819,7 @@ var fml = (function (exports) {
5710
5819
  */
5711
5820
  static throttle(timeoutMs, func, options) {
5712
5821
  const opts = options ?? Timing.THROTTLE_DEFAULT;
5713
- let tid = null;
5822
+ let tid = /** @type {number | null} */ (null);
5714
5823
  let args = [];
5715
5824
  let previousTimestamp = 0;
5716
5825
 
@@ -5722,13 +5831,13 @@ var fml = (function (exports) {
5722
5831
  args = [];
5723
5832
  }
5724
5833
  };
5725
- const throttled = function () {
5834
+ const throttled = (...called) => {
5726
5835
  const now = performance.now();
5727
5836
  if (!previousTimestamp && opts & Timing.THROTTLE_NO_LEADING) {
5728
5837
  previousTimestamp = now;
5729
5838
  }
5730
5839
  const remaining = previousTimestamp === 0 ? 0 : timeoutMs - (now - previousTimestamp);
5731
- args = [...arguments];
5840
+ args = called;
5732
5841
  if (remaining <= 0 || remaining > timeoutMs) {
5733
5842
  if (tid !== null) {
5734
5843
  clearTimeout(tid);
@@ -5743,7 +5852,11 @@ var fml = (function (exports) {
5743
5852
  tid = setTimeout(later, remaining);
5744
5853
  }
5745
5854
  };
5746
- const abort = () => clearTimeout(tid);
5855
+ const abort = () => {
5856
+ clearTimeout(tid ?? undefined);
5857
+ tid = null;
5858
+ args = [];
5859
+ };
5747
5860
  return [throttled, abort];
5748
5861
  }
5749
5862
  }
@@ -5757,7 +5870,7 @@ var fml = (function (exports) {
5757
5870
  */
5758
5871
  static flatten(obj, prefix, stops) {
5759
5872
  return Object.keys(obj).reduce((acc, k) => {
5760
- const pre = prefix.length ? prefix + '.' + k : k;
5873
+ const pre = prefix.length ? `${prefix}.${k}` : k;
5761
5874
  if (!stops.has(pre) && typeof obj[k] === 'object' && obj[k] !== null) {
5762
5875
  Object.assign(acc, Bindings.flatten(obj[k], pre, stops));
5763
5876
  } else {
@@ -5775,7 +5888,7 @@ var fml = (function (exports) {
5775
5888
  static providePath(result, path, value) {
5776
5889
  const keys = path.split('.').map((k) => (/^[0-9]+$/.test(k) ? +k : k));
5777
5890
  let current = result ?? {};
5778
- let previous = null;
5891
+ let previous = /** @type {any} */ (null);
5779
5892
  for (let i = 0; ; ++i) {
5780
5893
  const ckey = keys[i];
5781
5894
  const pkey = keys[i - 1];
@@ -5808,12 +5921,12 @@ var fml = (function (exports) {
5808
5921
  if (!el.checked) {
5809
5922
  return undefined;
5810
5923
  }
5811
- return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
5924
+ return el.dataset.fulBindType === 'boolean' ? el.value === 'true' : el.value;
5812
5925
  }
5813
5926
  if (el.getAttribute('type') === 'checkbox') {
5814
5927
  return el.checked;
5815
5928
  }
5816
- if (el.dataset['fulBindType'] === 'boolean') {
5929
+ if (el.dataset.fulBindType === 'boolean') {
5817
5930
  return !el.value ? null : el.value === 'true';
5818
5931
  }
5819
5932
  if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
@@ -5846,12 +5959,14 @@ var fml = (function (exports) {
5846
5959
 
5847
5960
  /**
5848
5961
  *
5849
- * @param {Element & {checked?: boolean} & {value?: any}} el
5962
+ * @param {Element & {dataset?: any} & {checked?: boolean} & {value?: any}} el
5850
5963
  * @returns
5851
5964
  */
5852
5965
  static mutate(el, raw) {
5853
5966
  if (el.getAttribute('type') === 'radio') {
5854
- el.checked = el.getAttribute('value') === raw;
5967
+ //values are matched as strings, as ful-radio-group does: extract decodes
5968
+ //boolean radios, and payloads carry numbers where the attribute is text
5969
+ el.checked = raw != null && el.getAttribute('value') === String(raw);
5855
5970
  return;
5856
5971
  }
5857
5972
  if (el.getAttribute('type') === 'checkbox') {
@@ -5875,7 +5990,9 @@ var fml = (function (exports) {
5875
5990
  static errors(form, es, scrollOnError) {
5876
5991
  const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
5877
5992
  const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
5878
- form.querySelectorAll(`[name]`).forEach((el) => el.setCustomValidity?.(''));
5993
+ form.querySelectorAll(`[name]`).forEach((el) => {
5994
+ el.setCustomValidity?.('');
5995
+ });
5879
5996
  form.querySelectorAll('ful-errors').forEach((el) => {
5880
5997
  el.replaceChildren();
5881
5998
  el.setAttribute('hidden', '');
@@ -5883,12 +6000,12 @@ var fml = (function (exports) {
5883
6000
  fieldErrors.forEach((e) => {
5884
6001
  const name = e.context.replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
5885
6002
  const parts = name.split('.');
5886
- for (let i = parts.length; i != 0; --i) {
6003
+ for (let i = parts.length; i !== 0; --i) {
5887
6004
  const prefix = parts.slice(0, i).join('.');
5888
6005
  const suffix = parts.slice(i, parts.length).join('.');
5889
- form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) =>
5890
- input.setCustomValidity?.(e.reason, suffix),
5891
- );
6006
+ form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) => {
6007
+ input.setCustomValidity?.(e.reason, suffix);
6008
+ });
5892
6009
  }
5893
6010
  });
5894
6011
  form.querySelectorAll('ful-errors').forEach((el) => {
@@ -5898,7 +6015,7 @@ var fml = (function (exports) {
5898
6015
  el.removeAttribute('hidden');
5899
6016
  }
5900
6017
  });
5901
- if (es.length == 0 || !scrollOnError) {
6018
+ if (es.length === 0 || !scrollOnError) {
5902
6019
  return;
5903
6020
  }
5904
6021
  Array.from(form.querySelectorAll(`:invalid`))
@@ -5970,7 +6087,8 @@ var fml = (function (exports) {
5970
6087
  class Form extends ParsedElement {
5971
6088
  form;
5972
6089
  render() {
5973
- const form = (this.form = document.createElement('form'));
6090
+ const form = document.createElement('form');
6091
+ this.form = form;
5974
6092
  form.setAttribute('novalidate', '');
5975
6093
  Attributes.forward('form-', this, form);
5976
6094
  form.replaceChildren(...this.childNodes);
@@ -5993,50 +6111,53 @@ var fml = (function (exports) {
5993
6111
  */
5994
6112
  async submit(submitter) {
5995
6113
  this.spinner(true);
6114
+ //one try: building the loader and preparing the request are as much part of a
6115
+ //submit as sending it, and a mapper that throws is how a caller reports a
6116
+ //problem with the values
6117
+ let values;
6118
+ let request;
5996
6119
  try {
5997
6120
  const loader = registry.component(this.getAttribute('loader') ?? 'loaders:form').create(this);
5998
- const values = Bindings.extractFrom(this.form, submitter);
5999
- let request = await loader.prepare(values, this);
6000
- try {
6001
- const se = new CustomEvent('submit', {
6121
+ values = Bindings.extractFrom(this.form, submitter);
6122
+ request = await loader.prepare(values, this);
6123
+ const se = new CustomEvent('submit', {
6124
+ bubbles: true,
6125
+ cancelable: true,
6126
+ detail: { submitter, values, request },
6127
+ });
6128
+ if (!this.dispatchEvent(se)) {
6129
+ return;
6130
+ }
6131
+ this.errors = [];
6132
+ const sre = new CustomEvent('submit:requested', {
6133
+ bubbles: true,
6134
+ cancelable: false,
6135
+ detail: { submitter, values: se.detail.values, request: se.detail.request },
6136
+ });
6137
+ let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
6138
+ request = sre.detail.request;
6139
+
6140
+ response = await loader.submit(request, this, response);
6141
+ const mapped = await loader.transform(response, this);
6142
+ this.dispatchEvent(
6143
+ new CustomEvent('submit:success', {
6002
6144
  bubbles: true,
6003
- cancelable: true,
6004
- detail: { submitter, values, request },
6005
- });
6006
- if (!this.dispatchEvent(se)) {
6007
- return;
6008
- }
6009
- this.errors = [];
6010
- const sre = new CustomEvent('submit:requested', {
6145
+ cancelable: false,
6146
+ detail: { submitter, values, request, response: mapped },
6147
+ }),
6148
+ );
6149
+ } catch (e) {
6150
+ this.dispatchEvent(
6151
+ new CustomEvent('submit:failure', {
6011
6152
  bubbles: true,
6012
6153
  cancelable: false,
6013
- detail: { submitter, values: se.detail.values, request: se.detail.request },
6014
- });
6015
- let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
6016
- request = sre.detail.request;
6017
-
6018
- response = await loader.submit(request, this, response);
6019
- const mapped = await loader.transform(response, this);
6020
- this.dispatchEvent(
6021
- new CustomEvent('submit:success', {
6022
- bubbles: true,
6023
- cancelable: false,
6024
- detail: { submitter, values, request, response: mapped },
6025
- }),
6026
- );
6027
- } catch (e) {
6028
- this.dispatchEvent(
6029
- new CustomEvent('submit:failure', {
6030
- bubbles: true,
6031
- cancelable: false,
6032
- detail: { submitter, values, request, exception: e },
6033
- }),
6034
- );
6035
- if (e instanceof Failure) {
6036
- this.errors = e.problems;
6037
- }
6038
- console.warn('failed to submit form', this, 'reason:', e);
6154
+ detail: { submitter, values, request, exception: e },
6155
+ }),
6156
+ );
6157
+ if (e instanceof Failure) {
6158
+ this.errors = e.problems;
6039
6159
  }
6160
+ console.warn('failed to submit form', this, 'reason:', e);
6040
6161
  } finally {
6041
6162
  this.spinner(false);
6042
6163
  }
@@ -6044,7 +6165,20 @@ var fml = (function (exports) {
6044
6165
  reset() {
6045
6166
  this.form.reset();
6046
6167
  }
6168
+ #spinning = 0;
6047
6169
  spinner(spin) {
6170
+ //submits can overlap: only the outermost one saves and restores the button states
6171
+ if (spin) {
6172
+ ++this.#spinning;
6173
+ if (this.#spinning !== 1) {
6174
+ return;
6175
+ }
6176
+ } else {
6177
+ this.#spinning = Math.max(0, this.#spinning - 1);
6178
+ if (this.#spinning !== 0) {
6179
+ return;
6180
+ }
6181
+ }
6048
6182
  this.querySelectorAll('ful-spinner').forEach((el) => {
6049
6183
  const hel = /** @type HTMLElement */ (el);
6050
6184
  hel.hidden = !spin;
@@ -6075,7 +6209,7 @@ var fml = (function (exports) {
6075
6209
  }
6076
6210
 
6077
6211
  class Input extends ParsedElement {
6078
- static observed = ['value', 'readonly:presence', 'required:presence'];
6212
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder'];
6079
6213
  static slots = true;
6080
6214
  static template = `
6081
6215
  <div class="form-label">
@@ -6112,12 +6246,6 @@ var fml = (function (exports) {
6112
6246
  this._input = fragment.querySelector('input,textarea');
6113
6247
 
6114
6248
  Attributes.forward('input-', this, this._input);
6115
- if (!skipObservedSetup) {
6116
- this.disabled = disabled;
6117
- this.readonly = observed.readonly;
6118
- this.required = observed.required;
6119
- this.value = observed.value;
6120
- }
6121
6249
  this._input.addEventListener('keydown', (evt) => {
6122
6250
  if (evt.key !== 'Enter' || this._type() === 'textarea') {
6123
6251
  return;
@@ -6133,19 +6261,26 @@ var fml = (function (exports) {
6133
6261
  form.requestSubmit(submitter);
6134
6262
  });
6135
6263
  this._input.addEventListener('input', (evt) => {
6136
- const re = this.getAttribute('mask');
6137
- if (!re) {
6264
+ const mask = this.getAttribute('mask');
6265
+ if (!mask) {
6138
6266
  return;
6139
6267
  }
6268
+ const strip = (v) => v.replace(new RegExp(mask, 'g'), '');
6140
6269
  const before = evt.target.value;
6141
- const after = before.replace(new RegExp(re, 'g'), '');
6270
+ const after = strip(before);
6142
6271
  if (before === after) {
6143
6272
  return;
6144
6273
  }
6145
6274
  const start = evt.target.selectionStart;
6146
- const offset = before.length - after.length;
6147
6275
  evt.target.value = after;
6148
- evt.target.setSelectionRange(start - offset, start - offset);
6276
+ if (start === null) {
6277
+ //email, number and the date types have no selection to restore
6278
+ return;
6279
+ }
6280
+ //the caret keeps its place among the characters that survived, so only the
6281
+ //ones stripped before it count
6282
+ const caret = strip(before.slice(0, start)).length;
6283
+ evt.target.setSelectionRange(caret, caret);
6149
6284
  });
6150
6285
  this._input.addEventListener('change', (evt) => {
6151
6286
  evt.stopPropagation();
@@ -6165,6 +6300,15 @@ var fml = (function (exports) {
6165
6300
  this._input.ariaDescribedByElements = [this._fieldError];
6166
6301
  this._input.ariaLabelledByElements = [label];
6167
6302
  this.replaceChildren(fragment);
6303
+ if (!skipObservedSetup) {
6304
+ // biome-ignore lint/complexity/noUselessThisAlias: keeps checkJs from seeing these as class fields
6305
+ const el = this;
6306
+ el.disabled = disabled;
6307
+ el.readonly = observed.readonly;
6308
+ el.required = observed.required;
6309
+ el.placeholder = observed.placeholder;
6310
+ el.value = observed.value;
6311
+ }
6168
6312
  }
6169
6313
  get value() {
6170
6314
  const uppercase = this.hasAttribute('uppercase');
@@ -6191,6 +6335,11 @@ var fml = (function (exports) {
6191
6335
  }
6192
6336
  set disabled(d) {
6193
6337
  Attributes.toggle(this._input, 'disabled', d);
6338
+ //also on the host: a form associated element only matches :disabled through its
6339
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
6340
+ //is needed, disabled is deliberately not observed: the platform delivers it
6341
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
6342
+ Attributes.toggle(this, 'disabled', d);
6194
6343
  }
6195
6344
  get required() {
6196
6345
  return this._input.getAttribute('aria-required') === 'true';
@@ -6201,6 +6350,18 @@ var fml = (function (exports) {
6201
6350
  Attributes.toggle(this, 'required', d);
6202
6351
  });
6203
6352
  }
6353
+ get placeholder() {
6354
+ const v = this._input.getAttribute('placeholder');
6355
+ return v === ' ' ? null : v;
6356
+ }
6357
+ set placeholder(d) {
6358
+ //without a placeholder :placeholder-shown never matches, and floating labels
6359
+ //rely on it, so a blank one stands in for none
6360
+ Attributes.set(this._input, 'placeholder', d ?? ' ');
6361
+ this.reflect(() => {
6362
+ Attributes.set(this, 'placeholder', d);
6363
+ });
6364
+ }
6204
6365
  focus(options) {
6205
6366
  this._input.focus(options);
6206
6367
  }
@@ -6220,23 +6381,23 @@ var fml = (function (exports) {
6220
6381
 
6221
6382
  class LocalDate extends ParsedElement {
6222
6383
  render() {
6223
- const content = this.innerHTML.trim();
6384
+ const content = this.textContent.trim();
6224
6385
  if (content === '') {
6225
- this.innerHTML = this.getAttribute('default') ?? '';
6386
+ this.replaceChildren(this.getAttribute('default') ?? '');
6226
6387
  return;
6227
6388
  }
6228
6389
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
6229
6390
  const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
6230
6391
  const [y, m, d] = content.split('-').map(Number);
6231
- this.innerHTML = formatter.format(new Date(y, m - 1, d));
6392
+ this.replaceChildren(formatter.format(new Date(y, m - 1, d)));
6232
6393
  }
6233
6394
  }
6234
6395
 
6235
6396
  class Instant extends ParsedElement {
6236
6397
  render() {
6237
- const content = this.innerHTML.trim();
6398
+ const content = this.textContent.trim();
6238
6399
  if (content === '') {
6239
- this.innerHTML = this.getAttribute('default') ?? '';
6400
+ this.replaceChildren(this.getAttribute('default') ?? '');
6240
6401
  return;
6241
6402
  }
6242
6403
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
@@ -6249,7 +6410,7 @@ var fml = (function (exports) {
6249
6410
  second: 'numeric',
6250
6411
  hour12: false,
6251
6412
  });
6252
- this.innerHTML = format.format(new Date(Instant.isoToLocal(content)));
6413
+ this.replaceChildren(format.format(new Date(Instant.isoToLocal(content))));
6253
6414
  }
6254
6415
  static isoToLocal(iso) {
6255
6416
  //this is so sad
@@ -6262,16 +6423,17 @@ var fml = (function (exports) {
6262
6423
  }
6263
6424
 
6264
6425
  class InputLocalDate extends Input {
6265
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
6426
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
6266
6427
  _type() {
6267
6428
  return 'date';
6268
6429
  }
6269
6430
  render(conf) {
6270
6431
  const { observed } = conf;
6271
6432
  super.render(conf);
6433
+ //step first: on a time input min and max are snapped to its grid
6434
+ this.step = observed.step;
6272
6435
  this.min = observed.min;
6273
6436
  this.max = observed.max;
6274
- this.step = observed.step;
6275
6437
  }
6276
6438
  get min() {
6277
6439
  const v = this._input.min;
@@ -6317,13 +6479,14 @@ var fml = (function (exports) {
6317
6479
  case 'd':
6318
6480
  r.setDate(r.getDate() + offset * sign);
6319
6481
  break;
6320
- case 'm':
6482
+ case 'm': {
6321
6483
  const originalDay = r.getDate();
6322
6484
  r.setMonth(r.getMonth() + offset * sign);
6323
6485
  if (r.getDate() !== originalDay) {
6324
6486
  r.setDate(0);
6325
6487
  }
6326
6488
  break;
6489
+ }
6327
6490
  case 'y':
6328
6491
  r.setFullYear(r.getFullYear() + offset * sign);
6329
6492
  break;
@@ -6336,10 +6499,62 @@ var fml = (function (exports) {
6336
6499
  _type() {
6337
6500
  return 'time';
6338
6501
  }
6502
+ get min() {
6503
+ const v = this._input.min;
6504
+ return v === '' ? null : v;
6505
+ }
6506
+ set min(v) {
6507
+ this._input.min = this.#fromNowOrOffset(v);
6508
+ }
6509
+ get max() {
6510
+ const v = this._input.max;
6511
+ return v === '' ? null : v;
6512
+ }
6513
+ set max(v) {
6514
+ this._input.max = this.#fromNowOrOffset(v);
6515
+ }
6516
+ /**
6517
+ * Resolves `now` and hour or minute offsets against the current time, wrapping
6518
+ * around midnight. `m` is minutes here, unlike the date offsets of the parent where
6519
+ * it is months: months mean nothing on a time. Anything else is passed through.
6520
+ */
6521
+ #fromNowOrOffset(v) {
6522
+ if (!v) {
6523
+ return '';
6524
+ }
6525
+ const resolved = new Date();
6526
+ if (v !== 'now') {
6527
+ const re = /^([+-])(\d+)([hm])$/;
6528
+ const match = re.exec(v);
6529
+ if (!match) {
6530
+ return v;
6531
+ }
6532
+ const sign = match[1] === '-' ? -1 : 1;
6533
+ const offset = +match[2] * sign;
6534
+ if (match[3] === 'h') {
6535
+ resolved.setHours(resolved.getHours() + offset);
6536
+ } else {
6537
+ resolved.setMinutes(resolved.getMinutes() + offset);
6538
+ }
6539
+ }
6540
+ return InputLocalTime.#snapped(resolved, Number(this._input.step) || 60);
6541
+ }
6542
+ /**
6543
+ * Truncates a time to the step grid: min anchors that grid, so a bound that is not
6544
+ * on it makes every value on it invalid.
6545
+ */
6546
+ static #snapped(date, stepSeconds) {
6547
+ const pad = (n) => String(n).padStart(2, '0');
6548
+ const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
6549
+ const snapped = Math.floor(seconds / stepSeconds) * stepSeconds;
6550
+ const hh = pad(Math.floor(snapped / 3600));
6551
+ const mm = pad(Math.floor((snapped % 3600) / 60));
6552
+ return stepSeconds % 60 === 0 ? `${hh}:${mm}` : `${hh}:${mm}:${pad(snapped % 60)}`;
6553
+ }
6339
6554
  }
6340
6555
 
6341
6556
  class InputInstant extends Input {
6342
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
6557
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
6343
6558
  _type() {
6344
6559
  return 'datetime-local';
6345
6560
  }
@@ -6384,28 +6599,28 @@ var fml = (function (exports) {
6384
6599
  static l10n = {
6385
6600
  en: {
6386
6601
  dropzonelabel: 'Click or drop your files here',
6387
- unaccepptablefiletype: 'Only files of type {0} are supported',
6602
+ unacceptablefiletype: 'Only files of type {0} are supported',
6388
6603
  maxfilesizeexceeded: 'Maximum supported file size is {0}',
6389
6604
  maxtotalsizeexceeded: 'Maximum supported total file size is {0}',
6390
6605
  maxfilesexceeded: 'Maximum files count exceeded',
6391
6606
  },
6392
6607
  it: {
6393
6608
  dropzonelabel: 'Clicca o trascina i file qui',
6394
- unaccepptablefiletype: 'Solo i file di tipo {0} sono supportati',
6609
+ unacceptablefiletype: 'Solo i file di tipo {0} sono supportati',
6395
6610
  maxfilesizeexceeded: 'La dimensione massima di un file è di {0}',
6396
6611
  maxtotalsizeexceeded: 'La dimensione massima complessiva dei file è di {0}',
6397
6612
  maxfilesexceeded: 'Numero massimo di file superato',
6398
6613
  },
6399
6614
  es: {
6400
6615
  dropzonelabel: 'Haz clic o arrastra tus archivos aquí',
6401
- unaccepptablefiletype: 'Solo se admiten archivos de tipo {0}',
6616
+ unacceptablefiletype: 'Solo se admiten archivos de tipo {0}',
6402
6617
  maxfilesizeexceeded: 'El tamaño máximo de archivo admitido es {0}',
6403
6618
  maxtotalsizeexceeded: 'El tamaño total máximo admitido es {0}',
6404
6619
  maxfilesexceeded: 'Se ha superado el número máximo de archivos',
6405
6620
  },
6406
6621
  fr: {
6407
6622
  dropzonelabel: 'Cliquez ou déposez vos fichiers ici',
6408
- unaccepptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
6623
+ unacceptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
6409
6624
  maxfilesizeexceeded: 'La taille maximale de fichier prise en charge est {0}',
6410
6625
  maxtotalsizeexceeded: 'La taille totale maximale prise en charge est {0}',
6411
6626
  maxfilesexceeded: 'Nombre maximal de fichiers dépassé',
@@ -6415,6 +6630,7 @@ var fml = (function (exports) {
6415
6630
  'value',
6416
6631
  'readonly:presence',
6417
6632
  'required:presence',
6633
+ 'placeholder',
6418
6634
  'accept:csv',
6419
6635
  'multiple:presence',
6420
6636
  'itemlist:presence',
@@ -6464,7 +6680,7 @@ var fml = (function (exports) {
6464
6680
  };
6465
6681
  render(conf) {
6466
6682
  const { observed } = conf;
6467
- super.render(conf);
6683
+ super.render({ ...conf, skipObservedSetup: true });
6468
6684
  this.#items = this.querySelector('ful-item-list');
6469
6685
  this.#dropzone = this.querySelector('[data-ref=dropzone]');
6470
6686
  this.#warnings = this.querySelector('ful-field-warnings');
@@ -6475,6 +6691,12 @@ var fml = (function (exports) {
6475
6691
  this.maxfiles = observed.maxfiles;
6476
6692
  this.maxfilesize = observed.maxfilesize;
6477
6693
  this.maxtotalsize = observed.maxtotalsize;
6694
+
6695
+ this.disabled = conf.disabled;
6696
+ this.readonly = observed.readonly;
6697
+ this.required = observed.required;
6698
+ this.placeholder = observed.placeholder;
6699
+ this.value = observed.value;
6478
6700
  this.#warnings.addEventListener('animationend', (e) => {
6479
6701
  e.target.remove();
6480
6702
  });
@@ -6482,11 +6704,17 @@ var fml = (function (exports) {
6482
6704
  if (!e.target.closest('button')) {
6483
6705
  return;
6484
6706
  }
6485
- const fileName = e.target.closest('ful-item').dataset.name;
6707
+ const idx = [...this.#items.children].indexOf(e.target.closest('ful-item'));
6708
+ if (idx === -1) {
6709
+ return;
6710
+ }
6486
6711
  const dt = new DataTransfer();
6487
- [...this.files].filter((f) => f.name !== fileName).forEach((f) => dt.items.add(f));
6712
+ [...this.files]
6713
+ .filter((f, i) => i !== idx)
6714
+ .forEach((f) => {
6715
+ dt.items.add(f);
6716
+ });
6488
6717
  this.files = dt.files;
6489
- this.#update();
6490
6718
  });
6491
6719
  this.#dropzone.addEventListener('click', (e) => {
6492
6720
  this.querySelector('input')?.click();
@@ -6497,10 +6725,15 @@ var fml = (function (exports) {
6497
6725
  });
6498
6726
  this.#dropzone.addEventListener('drop', (e) => {
6499
6727
  e.preventDefault();
6728
+ const dropped = [...e.dataTransfer.items].filter((i) => i.kind === 'file');
6729
+ if (dropped.length === 0) {
6730
+ return;
6731
+ }
6500
6732
  const dt = new DataTransfer();
6501
- [...e.dataTransfer.items].filter((i) => i.kind === 'file').forEach((i) => dt.items.add(i.getAsFile()));
6733
+ dropped.forEach((i) => {
6734
+ dt.items.add(i.getAsFile());
6735
+ });
6502
6736
  this.files = dt.files;
6503
- this.#update();
6504
6737
  });
6505
6738
  this._input.addEventListener('change', (e) => {
6506
6739
  this.#update();
@@ -6515,6 +6748,7 @@ var fml = (function (exports) {
6515
6748
  }
6516
6749
  #update() {
6517
6750
  this.setCustomValidity();
6751
+ this.#warnings.replaceChildren();
6518
6752
  this.#ensureAcceptable();
6519
6753
  this.#ensureFileSizes();
6520
6754
  this.#ensureTotalSize();
@@ -6525,7 +6759,7 @@ var fml = (function (exports) {
6525
6759
  .renderTo(this.#items);
6526
6760
  }
6527
6761
  warning(key, args) {
6528
- this.template('warning').withOverlay({ key, args }).renderTo(this.#warnings);
6762
+ this.template('warning').withOverlay({ key, args }).appendTo(this.#warnings);
6529
6763
  }
6530
6764
  #ensureAcceptable() {
6531
6765
  if (!this.#accept.length) {
@@ -6538,10 +6772,14 @@ var fml = (function (exports) {
6538
6772
  if (unacceptable.length === 0) {
6539
6773
  return;
6540
6774
  }
6541
- this.warning('unaccepptablefiletype', this.#accept.join(', '));
6775
+ this.warning('unacceptablefiletype', this.#accept.join(', '));
6542
6776
  const dt = new DataTransfer();
6543
- [...this.files].filter((f) => !unacceptable.includes(f)).forEach((f) => dt.items.add(f));
6544
- this.files = dt.files;
6777
+ [...this.files]
6778
+ .filter((f) => !unacceptable.includes(f))
6779
+ .forEach((f) => {
6780
+ dt.items.add(f);
6781
+ });
6782
+ this._input.files = dt.files;
6545
6783
  }
6546
6784
  #ensureFilesCount() {
6547
6785
  if (this.#maxfiles === null) {
@@ -6551,8 +6789,7 @@ var fml = (function (exports) {
6551
6789
  return;
6552
6790
  }
6553
6791
  this.warning('maxfilesexceeded');
6554
- const dt = new DataTransfer();
6555
- this.files = dt.files;
6792
+ this._input.files = new DataTransfer().files;
6556
6793
  }
6557
6794
 
6558
6795
  #ensureFileSizes() {
@@ -6565,8 +6802,12 @@ var fml = (function (exports) {
6565
6802
  }
6566
6803
  this.warning('maxfilesizeexceeded', this.#formatByteSize(this.#maxfilesize));
6567
6804
  const dt = new DataTransfer();
6568
- [...this.files].filter((f) => !oversized.includes(f)).forEach((f) => dt.items.add(f));
6569
- this.files = dt.files;
6805
+ [...this.files]
6806
+ .filter((f) => !oversized.includes(f))
6807
+ .forEach((f) => {
6808
+ dt.items.add(f);
6809
+ });
6810
+ this._input.files = dt.files;
6570
6811
  }
6571
6812
  #ensureTotalSize() {
6572
6813
  if (this.#maxtotalsize === null) {
@@ -6577,7 +6818,7 @@ var fml = (function (exports) {
6577
6818
  return;
6578
6819
  }
6579
6820
  this.warning('maxtotalsizeexceeded', this.#formatByteSize(this.#maxtotalsize));
6580
- this.files = new DataTransfer().files;
6821
+ this._input.files = new DataTransfer().files;
6581
6822
  }
6582
6823
  get accept() {
6583
6824
  return this.#accept;
@@ -6603,6 +6844,7 @@ var fml = (function (exports) {
6603
6844
  }
6604
6845
  set files(vs) {
6605
6846
  this._input.files = vs;
6847
+ this.#update();
6606
6848
  }
6607
6849
  get file() {
6608
6850
  return this.files[0] ?? null;
@@ -6623,7 +6865,6 @@ var fml = (function (exports) {
6623
6865
  return;
6624
6866
  }
6625
6867
  this.files = new DataTransfer().files;
6626
- this.#update();
6627
6868
  }
6628
6869
  get totalsize() {
6629
6870
  return Array.from(this.files).reduce((a, f) => a + f.size, 0);
@@ -6790,7 +7031,7 @@ var fml = (function (exports) {
6790
7031
  const http = registry.component('http-client');
6791
7032
  const responseMapper = SelectLoader.#responseMapperFrom(el);
6792
7033
 
6793
- if ('chunked' == el.getAttribute('mode')) {
7034
+ if ('chunked' === el.getAttribute('mode')) {
6794
7035
  return new PartialRemoteLoader({
6795
7036
  http,
6796
7037
  url: el.getAttribute('src'),
@@ -6866,8 +7107,14 @@ var fml = (function (exports) {
6866
7107
  });
6867
7108
  this.replaceChildren(fragment);
6868
7109
  }
7110
+ #selected() {
7111
+ return this.#menu?.querySelector('[selected]') ?? this.#menu?.firstElementChild ?? null;
7112
+ }
6869
7113
  acceptSelection() {
6870
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
7114
+ const selected = this.#selected();
7115
+ if (!selected) {
7116
+ return;
7117
+ }
6871
7118
  this.#change(selected);
6872
7119
  }
6873
7120
  update(values) {
@@ -6903,6 +7150,9 @@ var fml = (function (exports) {
6903
7150
  try {
6904
7151
  const data = await loader();
6905
7152
  this.update(data);
7153
+ } catch (e) {
7154
+ this.hide();
7155
+ throw e;
6906
7156
  } finally {
6907
7157
  this.#spinner.setAttribute('hidden', '');
6908
7158
  this.#menu.removeAttribute('hidden');
@@ -6910,9 +7160,9 @@ var fml = (function (exports) {
6910
7160
  }
6911
7161
  async moveOrShow(forward, loader) {
6912
7162
  if (this.shown) {
6913
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
6914
- const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
6915
- if (candidate) {
7163
+ const selected = this.#selected();
7164
+ const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
7165
+ if (selected && candidate) {
6916
7166
  selected.removeAttribute('selected');
6917
7167
  candidate.setAttribute('selected', '');
6918
7168
  candidate.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
@@ -6965,6 +7215,7 @@ var fml = (function (exports) {
6965
7215
  #multiple;
6966
7216
  #fieldError;
6967
7217
  #values = new Map();
7218
+ #token = 0;
6968
7219
  constructor() {
6969
7220
  super();
6970
7221
  this.internals = this.attachInternals();
@@ -6977,7 +7228,11 @@ var fml = (function (exports) {
6977
7228
  .create(this, { options: slots.options });
6978
7229
 
6979
7230
  this.#multiple = this.hasAttribute('multiple');
6980
- await this.#loader.prefetch?.();
7231
+ try {
7232
+ await this.#loader.prefetch?.();
7233
+ } catch (/** @type any */ e) {
7234
+ console.warn('failed to prefetch select options', this, 'reason:', e);
7235
+ }
6981
7236
  const fragment = this.template().withOverlay({ slots, name }).render();
6982
7237
  this.#input = fragment.querySelector('input');
6983
7238
  this.#items = fragment.querySelector('ful-item-list');
@@ -6996,11 +7251,9 @@ var fml = (function (exports) {
6996
7251
  this.#fieldError = fragment.querySelector('ful-field-error');
6997
7252
  this.#input.ariaDescribedByElements = [this.#fieldError];
6998
7253
  this.#input.ariaLabelledByElements = [label];
6999
-
7000
- const self = this;
7001
7254
  const [dload, abortdload] = Timing.throttle(400, () => {
7002
- self.#input.setAttribute('aria-expanded', 'true');
7003
- self.#ddmenu.show(() => self.#loader.load(self.#input.value));
7255
+ this.#input.setAttribute('aria-expanded', 'true');
7256
+ this.#ddmenu.show(() => this.#loader.load(this.#input.value));
7004
7257
  });
7005
7258
  this.addEventListener('click', (/** @type any */ e) => {
7006
7259
  if (e.target.matches('input')) {
@@ -7067,13 +7320,13 @@ var fml = (function (exports) {
7067
7320
  case 'ArrowUp': {
7068
7321
  e.preventDefault();
7069
7322
  this.#input.setAttribute('aria-expanded', 'true');
7070
- this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
7323
+ this.#ddmenu.moveOrShow(false, () => this.#loader.load(this.#input.value));
7071
7324
  break;
7072
7325
  }
7073
7326
  case 'ArrowDown': {
7074
7327
  e.preventDefault();
7075
7328
  this.#input.setAttribute('aria-expanded', 'true');
7076
- this.#ddmenu.moveOrShow(true, () => self.#loader.load(self.#input.value));
7329
+ this.#ddmenu.moveOrShow(true, () => this.#loader.load(this.#input.value));
7077
7330
  break;
7078
7331
  }
7079
7332
  case 'Escape': {
@@ -7082,6 +7335,12 @@ var fml = (function (exports) {
7082
7335
  break;
7083
7336
  }
7084
7337
  case 'Enter': {
7338
+ if (!this.#ddmenu.shown) {
7339
+ //nothing to accept: submit the form as ful-input does. the inner
7340
+ //input carries form="" so it never submits one on its own
7341
+ this.#requestSubmit();
7342
+ return;
7343
+ }
7085
7344
  e.preventDefault();
7086
7345
  this.#input.setAttribute('aria-expanded', 'false');
7087
7346
  this.#ddmenu.acceptSelection();
@@ -7130,6 +7389,16 @@ var fml = (function (exports) {
7130
7389
  async withLoader(fn) {
7131
7390
  return await fn(this.#loader);
7132
7391
  }
7392
+ #requestSubmit() {
7393
+ const form = this.internals.form;
7394
+ if (!form) {
7395
+ return;
7396
+ }
7397
+ const candidates = /** @type [HTMLButtonElement|HTMLInputElement] */ (
7398
+ Array.from(form.querySelectorAll('button:not(:disabled), input:not(:disabled)'))
7399
+ );
7400
+ form.requestSubmit(candidates.find((el) => el.type === 'submit'));
7401
+ }
7133
7402
  #changed() {
7134
7403
  const selection = [...this.#values.entries()].map((e) => ({
7135
7404
  key: e[0],
@@ -7159,16 +7428,43 @@ var fml = (function (exports) {
7159
7428
  this.template('items').withOverlay({ entries: this.#values.entries() }).renderTo(this.#items);
7160
7429
  }
7161
7430
  set value(vs) {
7162
- if (vs === null) {
7163
- this.#values = new Map();
7164
- this.#syncBadges();
7431
+ //the csvm mapper yields [] for a missing multiple value, an empty string is
7432
+ //left alone: it is a usable key for an <option value="">
7433
+ const keys = vs == null ? [] : Array.isArray(vs) ? vs : [vs];
7434
+ //the keys are known synchronously and are all `value` reads, so they are applied
7435
+ //now: only the labels need the loader, until then a key stands in for its own
7436
+ this.#values = new Map(keys.map((k) => [k, [k]]));
7437
+ this.#syncBadges();
7438
+ const token = ++this.#token;
7439
+ if (keys.length === 0) {
7165
7440
  return;
7166
7441
  }
7167
- (async () => {
7168
- const entries = await (this.#multiple ? this.#loader.exact(...vs) : this.#loader.exact(vs));
7169
- this.#values = new Map(entries.map((e) => [e[0], e.slice(1)]));
7170
- this.#syncBadges();
7171
- })();
7442
+ this.#resolve(keys, token);
7443
+ }
7444
+ /**
7445
+ * Resolves the labels of the assigned keys. A failed lookup is left to reject so
7446
+ * that it is reported like any other failure: the keys stay applied either way.
7447
+ */
7448
+ async #resolve(keys, token) {
7449
+ const entries = await this.#loader.exact(...keys);
7450
+ if (token !== this.#token) {
7451
+ //a newer assignment has been made in the meantime
7452
+ return;
7453
+ }
7454
+ //label the keys that are still selected: a removal made while the lookup was in
7455
+ //flight must not be undone by it, and a key the loader does not know is dropped
7456
+ const resolved = new Map(entries.map((e) => [e[0], e.slice(1)]));
7457
+ for (const key of keys) {
7458
+ if (!this.#values.has(key)) {
7459
+ continue;
7460
+ }
7461
+ if (resolved.has(key)) {
7462
+ this.#values.set(key, resolved.get(key));
7463
+ } else {
7464
+ this.#values.delete(key);
7465
+ }
7466
+ }
7467
+ this.#syncBadges();
7172
7468
  }
7173
7469
  get value() {
7174
7470
  if (this.#multiple) {
@@ -7187,6 +7483,11 @@ var fml = (function (exports) {
7187
7483
  }
7188
7484
  set disabled(d) {
7189
7485
  Attributes.toggle(this.#input, 'disabled', d);
7486
+ //also on the host: a form associated element only matches :disabled through its
7487
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7488
+ //is needed, disabled is deliberately not observed: the platform delivers it
7489
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7490
+ Attributes.toggle(this, 'disabled', d);
7190
7491
  }
7191
7492
  get readonly() {
7192
7493
  return this.#input.readOnly;
@@ -7292,7 +7593,9 @@ var fml = (function (exports) {
7292
7593
  return [input, label];
7293
7594
  });
7294
7595
 
7295
- radioEls.forEach((el) => el.remove());
7596
+ radioEls.forEach((el) => {
7597
+ el.remove();
7598
+ });
7296
7599
  this.template().withOverlay({ name, slots, inputsAndLabels }).renderTo(this);
7297
7600
  this.#fieldset = this.firstElementChild;
7298
7601
  this.disabled = disabled;
@@ -7336,6 +7639,11 @@ var fml = (function (exports) {
7336
7639
  }
7337
7640
  set disabled(d) {
7338
7641
  Attributes.toggle(this.#fieldset, 'disabled', d);
7642
+ //also on the host: a form associated element only matches :disabled through its
7643
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7644
+ //is needed, disabled is deliberately not observed: the platform delivers it
7645
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7646
+ Attributes.toggle(this, 'disabled', d);
7339
7647
  }
7340
7648
  get required() {
7341
7649
  return this.#fieldset.getAttribute('aria-required') === 'true';
@@ -7385,7 +7693,7 @@ var fml = (function (exports) {
7385
7693
  this.internals.role = 'presentation';
7386
7694
  }
7387
7695
  render({ slots, observed, disabled }) {
7388
- const isSwitch = this.getAttribute('type') == 'switch';
7696
+ const isSwitch = this.getAttribute('type') === 'switch';
7389
7697
  const klass = isSwitch ? 'form-check form-switch' : 'form-check';
7390
7698
  const fragment = this.template().withOverlay({ slots, klass, isSwitch }).render();
7391
7699
  this.#container = fragment.firstElementChild;
@@ -7449,6 +7757,11 @@ var fml = (function (exports) {
7449
7757
  }
7450
7758
  set disabled(d) {
7451
7759
  Attributes.toggle(this.#input, 'disabled', d);
7760
+ //also on the host: a form associated element only matches :disabled through its
7761
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7762
+ //is needed, disabled is deliberately not observed: the platform delivers it
7763
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7764
+ Attributes.toggle(this, 'disabled', d);
7452
7765
  }
7453
7766
  get required() {
7454
7767
  return this.#input.getAttribute('aria-required') === 'true';
@@ -7489,9 +7802,10 @@ var fml = (function (exports) {
7489
7802
  class SortButton extends ParsedElement {
7490
7803
  static observed = ['order'];
7491
7804
  #order;
7492
- render() {
7805
+ render({ observed }) {
7493
7806
  const sorter = this.getAttribute('sorter');
7494
7807
  const orders = ['asc', 'desc', null];
7808
+ this.order = observed.order;
7495
7809
  this.addEventListener('click', () => {
7496
7810
  const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
7497
7811
  this.dispatchEvent(
@@ -7581,10 +7895,12 @@ var fml = (function (exports) {
7581
7895
  #total = 0;
7582
7896
  #current = 0;
7583
7897
  render({ observed }) {
7584
- this.update(observed.current ?? 0, observed.total ?? 0);
7898
+ this.total = observed.total ?? 0;
7899
+ this.current = observed.current ?? 0;
7585
7900
  this.addEventListener('click', (/** @type any */ evt) => {
7586
7901
  const el = evt.target.closest('a');
7587
- if (!el) {
7902
+ if (!el || el.classList.contains('disabled')) {
7903
+ //a disabled link leads nowhere: the page it would ask for does not exist
7588
7904
  return;
7589
7905
  }
7590
7906
  this.dispatchEvent(
@@ -7600,25 +7916,20 @@ var fml = (function (exports) {
7600
7916
  }
7601
7917
  update(current, total) {
7602
7918
  const maxRender = Number(this.getAttribute('pages') ?? '5');
7603
- const prev = { index: Math.max(0, current - 1), enabled: current > 0 };
7919
+ const hasPrev = current > 0;
7920
+ const hasNext = current + 1 < total;
7921
+ //a disabled arrow carries no page: there is nothing valid for it to point at
7922
+ const prev = { index: hasPrev ? current - 1 : null, enabled: hasPrev };
7604
7923
  const curr = { index: current, label: current + 1 };
7605
- const next = { index: Math.min(total, current + 1), enabled: current + 1 < total };
7606
- const pages = [
7607
- {
7608
- index: current,
7609
- label: current + 1,
7610
- },
7611
- ];
7612
- for (let mid = current, offset = 1; offset !== maxRender && pages.length != maxRender; ++offset) {
7613
- const p = mid - offset;
7614
- if (p >= 0) {
7615
- pages.unshift({ index: p, label: p + 1 });
7616
- }
7617
- const n = mid + offset;
7618
- if (n < total) {
7619
- pages.push({ index: n, label: n + 1 });
7620
- }
7621
- }
7924
+ const next = { index: hasNext ? current + 1 : null, enabled: hasNext };
7925
+ //the window holds at most maxRender pages, centered on the current one and slid
7926
+ //back towards the end so it stays full on the last pages
7927
+ const rendered = Math.max(1, Math.min(maxRender, total));
7928
+ const first = Math.max(0, Math.min(current - Math.floor((rendered - 1) / 2), total - rendered));
7929
+ const pages = Array.from({ length: rendered }, (_, offset) => ({
7930
+ index: first + offset,
7931
+ label: first + offset + 1,
7932
+ }));
7622
7933
  this.template().withOverlay({ total, prev, curr, next, pages }).renderTo(this);
7623
7934
  }
7624
7935
  get total() {
@@ -7645,9 +7956,10 @@ var fml = (function (exports) {
7645
7956
 
7646
7957
  class TableSchemaParser {
7647
7958
  static parse(nodeOrFragment, template) {
7648
- const schema = Nodes.queryChildren(nodeOrFragment, 'schema');
7959
+ //nodeOrFragment is undefined when the slot is missing altogether
7960
+ const schema = nodeOrFragment ? Nodes.queryChildren(nodeOrFragment, 'schema') : null;
7649
7961
  if (!schema) {
7650
- throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
7962
+ throw new Error('missing expected <schema>: ful-table needs a <template slot="schema"> holding one');
7651
7963
  }
7652
7964
  const headersTr = document.createElement('tr');
7653
7965
  const rowsTr = document.createElement('tr');
@@ -7866,9 +8178,10 @@ var fml = (function (exports) {
7866
8178
  this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
7867
8179
  this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
7868
8180
  this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
7869
- this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
7870
8181
  this.replaceChildren(fragment);
7871
- schema.headersTemplate.renderTo(this.querySelector('thead'));
8182
+ const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
8183
+ schema.headersTemplate.renderTo(thead);
8184
+ this.#sorters = thead.querySelectorAll('ful-sorter');
7872
8185
  await Rendering.waitForChildren(this);
7873
8186
 
7874
8187
  const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
@@ -7903,11 +8216,16 @@ var fml = (function (exports) {
7903
8216
  this.addEventListener('sort-requested', async (/** @type any */ e) => {
7904
8217
  const sortRequest = e.detail.value.order ? e.detail.value : null;
7905
8218
  await this.load(this.#latestRequest.pageRequest, sortRequest, this.#latestRequest.filterRequest);
7906
- this.#sorters.forEach((s) => (s.order = null));
8219
+ this.#sorters.forEach((s) => {
8220
+ s.order = null;
8221
+ });
7907
8222
  e.target.order = e.detail.value.order;
7908
8223
  });
7909
8224
  if (this.hasAttribute('autoload')) {
7910
- await this.reload();
8225
+ //not awaited: the first load must not hold up the upgrade, and a loader that
8226
+ //fails or never answers must not keep ftl:ready from firing for the page.
8227
+ //load renders its own error state and lets the failure reject, so it is reported
8228
+ this.reload();
7911
8229
  }
7912
8230
  }
7913
8231
 
@@ -7920,7 +8238,7 @@ var fml = (function (exports) {
7920
8238
  }
7921
8239
  async load(pageRequest, sortRequest, filterRequest) {
7922
8240
  this.#body.replaceChildren();
7923
- this.#loading.removeAttribute('hidden', '');
8241
+ this.#loading.removeAttribute('hidden');
7924
8242
  this.#feedback.setAttribute('hidden', '');
7925
8243
  this.#noAutoload.setAttribute('hidden', '');
7926
8244
  try {
@@ -7929,7 +8247,7 @@ var fml = (function (exports) {
7929
8247
  this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
7930
8248
  } catch (/** @type any */ error) {
7931
8249
  this.#loading.setAttribute('hidden', '');
7932
- this.#feedback.removeAttribute('hidden', '');
8250
+ this.#feedback.removeAttribute('hidden');
7933
8251
  if (!error.problems) {
7934
8252
  this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
7935
8253
  } else {
@@ -7971,7 +8289,7 @@ var fml = (function (exports) {
7971
8289
  }
7972
8290
 
7973
8291
  class InstantFilter extends Input {
7974
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8292
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
7975
8293
  static template = `
7976
8294
  <div class="form-label">
7977
8295
  <label>{{{{ slots.default }}}}</label>
@@ -8005,10 +8323,16 @@ var fml = (function (exports) {
8005
8323
  this.#operator = this.querySelector('[data-ref=operator]');
8006
8324
  this.#value1 = this.querySelector('[data-ref=value1]');
8007
8325
  this.#value2 = this.querySelector('[data-ref=value2]');
8326
+ //Input.render only re-dispatches changes coming from the first operand
8327
+ this.#value2.addEventListener('change', (evt) => {
8328
+ evt.stopPropagation();
8329
+ this.#notifyChange();
8330
+ });
8008
8331
 
8009
8332
  this.disabled = conf.disabled;
8010
8333
  this.readonly = conf.observed.readonly;
8011
8334
  this.required = conf.observed.required;
8335
+ this.placeholder = conf.observed.placeholder;
8012
8336
  this.value = conf.observed.value;
8013
8337
 
8014
8338
  this.addEventListener('click', (evt) => {
@@ -8018,9 +8342,13 @@ var fml = (function (exports) {
8018
8342
  }
8019
8343
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8020
8344
  const value = /** @type String */ (target.getAttribute('value'));
8345
+ const previous = btn.getAttribute('value');
8021
8346
  Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
8022
8347
  btn.setAttribute('value', value);
8023
8348
  btn.innerHTML = target.innerHTML;
8349
+ if (previous !== value) {
8350
+ this.#notifyChange();
8351
+ }
8024
8352
  });
8025
8353
  }
8026
8354
 
@@ -8036,14 +8364,40 @@ var fml = (function (exports) {
8036
8364
  return;
8037
8365
  }
8038
8366
  const [operator, ...values] = v;
8039
- this.#operator.setAttribute('value', operator);
8367
+ this.#showOperator(operator);
8040
8368
  this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
8041
8369
  this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
8042
8370
  }
8371
+ #showOperator(operator) {
8372
+ this.#operator.setAttribute('value', operator);
8373
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8374
+ const item = items.find((a) => a.getAttribute('value') === operator);
8375
+ if (item) {
8376
+ this.#operator.innerHTML = item.innerHTML;
8377
+ }
8378
+ Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
8379
+ }
8380
+ #notifyChange() {
8381
+ this.dispatchEvent(
8382
+ new CustomEvent('change', {
8383
+ bubbles: true,
8384
+ cancelable: false,
8385
+ detail: {
8386
+ value: this.value,
8387
+ },
8388
+ }),
8389
+ );
8390
+ }
8391
+ get readonly() {
8392
+ return super.readonly;
8393
+ }
8043
8394
  set readonly(v) {
8044
8395
  this.#value2.readOnly = v;
8045
8396
  super.readonly = v;
8046
8397
  }
8398
+ get disabled() {
8399
+ return super.disabled;
8400
+ }
8047
8401
  set disabled(d) {
8048
8402
  Attributes.toggle(this.#value2, 'disabled', d);
8049
8403
  super.disabled = d;
@@ -8051,7 +8405,7 @@ var fml = (function (exports) {
8051
8405
  }
8052
8406
 
8053
8407
  class LocalDateFilter extends Input {
8054
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8408
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
8055
8409
  static template = `
8056
8410
  <div class="form-label">
8057
8411
  <label>{{{{ slots.default }}}}</label>
@@ -8086,10 +8440,16 @@ var fml = (function (exports) {
8086
8440
  this.#operator = this.querySelector('[data-ref=operator]');
8087
8441
  this.#value1 = this.querySelector('[data-ref=value1]');
8088
8442
  this.#value2 = this.querySelector('[data-ref=value2]');
8443
+ //Input.render only re-dispatches changes coming from the first operand
8444
+ this.#value2.addEventListener('change', (evt) => {
8445
+ evt.stopPropagation();
8446
+ this.#notifyChange();
8447
+ });
8089
8448
 
8090
8449
  this.disabled = conf.disabled;
8091
8450
  this.readonly = conf.observed.readonly;
8092
8451
  this.required = conf.observed.required;
8452
+ this.placeholder = conf.observed.placeholder;
8093
8453
  this.value = conf.observed.value;
8094
8454
 
8095
8455
  this.addEventListener('click', (evt) => {
@@ -8099,14 +8459,18 @@ var fml = (function (exports) {
8099
8459
  }
8100
8460
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8101
8461
  const value = /** @type String */ (target.getAttribute('value'));
8462
+ const previous = btn.getAttribute('value');
8102
8463
  Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
8103
8464
  btn.setAttribute('value', value);
8104
8465
  btn.innerHTML = target.innerHTML;
8466
+ if (previous !== value) {
8467
+ this.#notifyChange();
8468
+ }
8105
8469
  });
8106
8470
  }
8107
8471
  get value() {
8108
8472
  const operator = this.#operator.getAttribute('value');
8109
- const values = operator == 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
8473
+ const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
8110
8474
  return values.some((v) => v === '') ? undefined : [operator, ...values];
8111
8475
  }
8112
8476
  set value(v) {
@@ -8116,14 +8480,40 @@ var fml = (function (exports) {
8116
8480
  return;
8117
8481
  }
8118
8482
  const [operator, ...values] = v;
8119
- this.#operator.setAttribute('value', operator);
8483
+ this.#showOperator(operator);
8120
8484
  this.#value1.value = values[0];
8121
8485
  this.#value2.value = values[1];
8122
8486
  }
8487
+ #showOperator(operator) {
8488
+ this.#operator.setAttribute('value', operator);
8489
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8490
+ const item = items.find((a) => a.getAttribute('value') === operator);
8491
+ if (item) {
8492
+ this.#operator.innerHTML = item.innerHTML;
8493
+ }
8494
+ Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
8495
+ }
8496
+ #notifyChange() {
8497
+ this.dispatchEvent(
8498
+ new CustomEvent('change', {
8499
+ bubbles: true,
8500
+ cancelable: false,
8501
+ detail: {
8502
+ value: this.value,
8503
+ },
8504
+ }),
8505
+ );
8506
+ }
8507
+ get readonly() {
8508
+ return super.readonly;
8509
+ }
8123
8510
  set readonly(v) {
8124
8511
  this.#value2.readOnly = v;
8125
8512
  super.readonly = v;
8126
8513
  }
8514
+ get disabled() {
8515
+ return super.disabled;
8516
+ }
8127
8517
  set disabled(d) {
8128
8518
  Attributes.toggle(this.#value2, 'disabled', d);
8129
8519
  super.disabled = d;
@@ -8131,7 +8521,7 @@ var fml = (function (exports) {
8131
8521
  }
8132
8522
 
8133
8523
  class TextFilter extends Input {
8134
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8524
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
8135
8525
  static template = `
8136
8526
  <div class="form-label">
8137
8527
  <label>{{{{ slots.default }}}}</label>
@@ -8155,6 +8545,8 @@ var fml = (function (exports) {
8155
8545
  `;
8156
8546
  #operator;
8157
8547
  #value;
8548
+ //the sensitivity has no control of its own: it is carried through from whoever set the value
8549
+ #sensitivity = 'IGNORE_CASE';
8158
8550
  render(conf) {
8159
8551
  super.render({ ...conf, skipObservedSetup: true });
8160
8552
 
@@ -8164,6 +8556,7 @@ var fml = (function (exports) {
8164
8556
  this.disabled = conf.disabled;
8165
8557
  this.readonly = conf.observed.readonly;
8166
8558
  this.required = conf.observed.required;
8559
+ this.placeholder = conf.observed.placeholder;
8167
8560
  this.value = conf.observed.value;
8168
8561
 
8169
8562
  this.addEventListener('click', (evt) => {
@@ -8173,13 +8566,17 @@ var fml = (function (exports) {
8173
8566
  }
8174
8567
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8175
8568
  const value = /** @type String */ (target.getAttribute('value'));
8569
+ const previous = btn.getAttribute('value');
8176
8570
  btn.setAttribute('value', value);
8177
8571
  btn.innerHTML = target.innerHTML;
8572
+ if (previous !== value) {
8573
+ this.#notifyChange();
8574
+ }
8178
8575
  });
8179
8576
  }
8180
8577
  get value() {
8181
8578
  const operator = this.#operator.getAttribute('value');
8182
- return this.#value.value === '' ? undefined : [operator, 'IGNORE_CASE', this.#value.value];
8579
+ return this.#value.value === '' ? undefined : [operator, this.#sensitivity, this.#value.value];
8183
8580
  }
8184
8581
  set value(v) {
8185
8582
  if (v == null) {
@@ -8187,15 +8584,35 @@ var fml = (function (exports) {
8187
8584
  return;
8188
8585
  }
8189
8586
  const [operator, sensitivity, value] = v;
8190
- this.#operator.setAttribute('value', operator);
8587
+ this.#showOperator(operator);
8588
+ this.#sensitivity = sensitivity ?? 'IGNORE_CASE';
8191
8589
  this.#value.value = value;
8192
8590
  }
8591
+ #showOperator(operator) {
8592
+ this.#operator.setAttribute('value', operator);
8593
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8594
+ const item = items.find((a) => a.getAttribute('value') === operator);
8595
+ if (item) {
8596
+ this.#operator.innerHTML = item.innerHTML;
8597
+ }
8598
+ }
8599
+ #notifyChange() {
8600
+ this.dispatchEvent(
8601
+ new CustomEvent('change', {
8602
+ bubbles: true,
8603
+ cancelable: false,
8604
+ detail: {
8605
+ value: this.value,
8606
+ },
8607
+ }),
8608
+ );
8609
+ }
8193
8610
  }
8194
8611
 
8195
8612
  class LocalizationModule {
8196
8613
  static t(k, ...args) {
8197
- //@ts-ignore
8198
- const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.['en']?.[k] ?? k;
8614
+ //@ts-expect-error l10n and language come from the element class and the data stack
8615
+ const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.en?.[k] ?? k;
8199
8616
  if (args.length === 0) {
8200
8617
  return format;
8201
8618
  }
@@ -8278,9 +8695,11 @@ var fml = (function (exports) {
8278
8695
  });
8279
8696
 
8280
8697
  if (typeof window !== 'undefined') {
8698
+ // biome-ignore-start lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
8281
8699
  window['ftl'] = ftl;
8282
8700
  window['httpc'] = httpc;
8283
8701
  window['ful'] = ful;
8702
+ // biome-ignore-end lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
8284
8703
  }
8285
8704
 
8286
8705
  exports.AsyncEvents = AsyncEvents;