@optionfactory/fml 8.0.0 → 8.0.1

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 (45) 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/fml.css +10 -1
  6. package/dist/fml.css.map +1 -1
  7. package/dist/fml.d.mts +73 -13
  8. package/dist/fml.iife.js +598 -208
  9. package/dist/fml.iife.js.map +1 -1
  10. package/dist/fml.iife.min.js +1 -1
  11. package/dist/fml.iife.min.js.map +1 -1
  12. package/dist/fml.min.mjs +1 -1
  13. package/dist/fml.min.mjs.map +1 -1
  14. package/dist/fml.mjs +598 -208
  15. package/dist/fml.mjs.map +1 -1
  16. package/dist/ftl.d.mts +2 -1
  17. package/dist/ftl.iife.js +60 -33
  18. package/dist/ftl.iife.js.map +1 -1
  19. package/dist/ftl.iife.min.js +1 -1
  20. package/dist/ftl.iife.min.js.map +1 -1
  21. package/dist/ftl.min.mjs +1 -1
  22. package/dist/ftl.min.mjs.map +1 -1
  23. package/dist/ftl.mjs +60 -33
  24. package/dist/ftl.mjs.map +1 -1
  25. package/dist/ful.css +10 -1
  26. package/dist/ful.css.map +1 -1
  27. package/dist/ful.d.mts +22 -6
  28. package/dist/ful.iife.js +496 -166
  29. package/dist/ful.iife.js.map +1 -1
  30. package/dist/ful.iife.min.js +1 -1
  31. package/dist/ful.iife.min.js.map +1 -1
  32. package/dist/ful.min.mjs +1 -1
  33. package/dist/ful.min.mjs.map +1 -1
  34. package/dist/ful.mjs +496 -166
  35. package/dist/ful.mjs.map +1 -1
  36. package/dist/httpc.d.mts +49 -6
  37. package/dist/httpc.iife.js +40 -9
  38. package/dist/httpc.iife.js.map +1 -1
  39. package/dist/httpc.iife.min.js +1 -1
  40. package/dist/httpc.iife.min.js.map +1 -1
  41. package/dist/httpc.min.mjs +1 -1
  42. package/dist/httpc.min.mjs.map +1 -1
  43. package/dist/httpc.mjs +40 -9
  44. package/dist/httpc.mjs.map +1 -1
  45. package/package.json +6 -7
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,
@@ -4387,6 +4392,16 @@ var fml = (function (exports) {
4387
4392
  .finally(() => this.#q.delete(el));
4388
4393
  this.#q.set(el, promise);
4389
4394
  }
4395
+ /**
4396
+ * Waits for every queued upgrade, including the ones enqueued while waiting:
4397
+ * a component is only queued once its parent connects it, so a single pass would
4398
+ * miss everything nested.
4399
+ */
4400
+ async settle() {
4401
+ while (this.#q.size !== 0) {
4402
+ await Promise.all(Array.from(this.#q.values()));
4403
+ }
4404
+ }
4390
4405
  get entries() {
4391
4406
  return this.#q.entries();
4392
4407
  }
@@ -4581,18 +4596,30 @@ var fml = (function (exports) {
4581
4596
  }
4582
4597
  }
4583
4598
 
4584
- class Rendering {
4585
- static async waitFor(el) {
4599
+ /**
4600
+ * Waits for the queued upgrades matching the filter, including the ones enqueued while
4601
+ * waiting: a component is only queued once its parent connects it, so a single pass
4602
+ * would miss everything nested.
4603
+ * @param {(el: Element) => boolean} accept
4604
+ */
4605
+ const settle = async (accept) => {
4606
+ for (;;) {
4586
4607
  const pending = Array.from(registry.upgrades)
4587
- .filter(([child, promise]) => el.contains(child))
4588
- .map(([child, promise]) => promise);
4608
+ .filter(([child]) => accept(child))
4609
+ .map(([, promise]) => promise);
4610
+ if (pending.length === 0) {
4611
+ return;
4612
+ }
4589
4613
  await Promise.all(pending);
4590
4614
  }
4615
+ };
4616
+
4617
+ class Rendering {
4618
+ static async waitFor(el) {
4619
+ await settle((child) => el.contains(child));
4620
+ }
4591
4621
  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);
4622
+ await settle((child) => el !== child && el.contains(child));
4596
4623
  }
4597
4624
  }
4598
4625
 
@@ -4634,7 +4661,7 @@ var fml = (function (exports) {
4634
4661
  template(name) {
4635
4662
  const { modules, data } = registry.context();
4636
4663
  let t = this.#bits().TEMPLATES[name ?? 'default'].withData(data).withModules(modules);
4637
- for (let k of ['l10n', 'config']) {
4664
+ for (const k of ['l10n', 'config']) {
4638
4665
  const v = this.constructor[k];
4639
4666
  if (v) {
4640
4667
  t = t.withOverlay({ [k]: v });
@@ -4775,13 +4802,22 @@ var fml = (function (exports) {
4775
4802
 
4776
4803
  let vi = 0;
4777
4804
  let si = 0;
4778
- while (vi < str.length * 0.75) {
4805
+ //every group yields three bytes, but a padded input stops short of the last
4806
+ //one or two: writing them anyway would rely on typed arrays dropping writes
4807
+ //past their length
4808
+ while (vi < nbytes) {
4779
4809
  const v1 = d.indexOf(str.charAt(si++));
4780
4810
  const v2 = d.indexOf(str.charAt(si++));
4781
4811
  const v3 = d.indexOf(str.charAt(si++));
4782
4812
  const v4 = d.indexOf(str.charAt(si++));
4783
4813
  view[vi++] = (v1 << 2) | (v2 >> 4);
4814
+ if (vi === nbytes) {
4815
+ break;
4816
+ }
4784
4817
  view[vi++] = ((v2 & 15) << 4) | (v3 >> 2);
4818
+ if (vi === nbytes) {
4819
+ break;
4820
+ }
4785
4821
  view[vi++] = ((v3 & 3) << 6) | v4;
4786
4822
  }
4787
4823
 
@@ -4808,7 +4844,7 @@ var fml = (function (exports) {
4808
4844
  return Array.from(bytes)
4809
4845
  .map((b) => b.toString(16))
4810
4846
  .map((b) => (upper ? b.toUpperCase() : b))
4811
- .map((o) => o.padStart(2, 0))
4847
+ .map((o) => o.padStart(2, '0'))
4812
4848
  .join('');
4813
4849
  }
4814
4850
  }
@@ -5138,6 +5174,26 @@ var fml = (function (exports) {
5138
5174
  }
5139
5175
  };
5140
5176
 
5177
+ /**
5178
+ * Yields the entries of a headers or params initializer preserving nullish values:
5179
+ * normalizing through `Headers`/`URLSearchParams` first would stringify them to
5180
+ * "null"/"undefined" instead of removing the key.
5181
+ * @param {any} source
5182
+ * @returns {Iterable<[string, any]>}
5183
+ */
5184
+ const rawEntries = (source) => {
5185
+ if (source == null) {
5186
+ return [];
5187
+ }
5188
+ if (typeof source === 'string') {
5189
+ return new URLSearchParams(source);
5190
+ }
5191
+ if (typeof source[Symbol.iterator] === 'function') {
5192
+ return source;
5193
+ }
5194
+ return Object.entries(source);
5195
+ };
5196
+
5141
5197
  class HttpRequestBuilder {
5142
5198
  #client;
5143
5199
  #method;
@@ -5190,11 +5246,11 @@ var fml = (function (exports) {
5190
5246
  }
5191
5247
  /**
5192
5248
  * 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
5249
+ * @param {HeadersInit|Record<string,string|null|undefined>} hs
5194
5250
  * @returns {HttpRequestBuilder} this builder
5195
5251
  */
5196
5252
  headers(hs) {
5197
- for (const [k, v] of new Headers(hs).entries()) {
5253
+ for (const [k, v] of rawEntries(hs)) {
5198
5254
  if (v == null) {
5199
5255
  this.#headers.delete(k);
5200
5256
  } else {
@@ -5219,11 +5275,11 @@ var fml = (function (exports) {
5219
5275
  }
5220
5276
  /**
5221
5277
  * 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
5278
+ * @param {URLSearchParams|Record<string,string|null|undefined>|string[][]|string} ps
5223
5279
  * @returns {HttpRequestBuilder} this builder
5224
5280
  */
5225
5281
  params(ps) {
5226
- for (const [k, v] of new URLSearchParams(ps).entries()) {
5282
+ for (const [k, v] of rawEntries(ps)) {
5227
5283
  if (v == null) {
5228
5284
  this.#params.delete(k);
5229
5285
  } else {
@@ -5239,8 +5295,10 @@ var fml = (function (exports) {
5239
5295
  * @returns {HttpRequestBuilder} this builder
5240
5296
  */
5241
5297
  param(k, ...vs) {
5298
+ //overriding, as header, headers and params all do: pass every value in one
5299
+ //call to get a multi valued parameter
5300
+ this.#params.delete(k);
5242
5301
  if (vs.length === 0 || vs[0] == null) {
5243
- this.#params.delete(k);
5244
5302
  return this;
5245
5303
  }
5246
5304
  for (const v of vs) {
@@ -5273,7 +5331,7 @@ var fml = (function (exports) {
5273
5331
  /**
5274
5332
  * Sets the request body as a FormData configured using the callback.
5275
5333
  * `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
5276
- * @param {function(HttpMultipartRequestCustomizer):void} callback
5334
+ * @param {(c: HttpMultipartRequestCustomizer) => void} callback
5277
5335
  */
5278
5336
  multipart(callback) {
5279
5337
  const formData = new FormData();
@@ -5438,7 +5496,7 @@ var fml = (function (exports) {
5438
5496
  * @returns this builder
5439
5497
  */
5440
5498
  blobs(name, values) {
5441
- for (let v of values) {
5499
+ for (const v of values) {
5442
5500
  this.#formData.append(name, v);
5443
5501
  }
5444
5502
  return this;
@@ -5473,7 +5531,16 @@ var fml = (function (exports) {
5473
5531
  }
5474
5532
  static load(k) {
5475
5533
  const got = localStorage.getItem(k);
5476
- return got === null ? undefined : JSON.parse(got);
5534
+ if (got === null) {
5535
+ return undefined;
5536
+ }
5537
+ try {
5538
+ return JSON.parse(got);
5539
+ } catch {
5540
+ //not what save wrote: drop it, otherwise every later read fails the same way
5541
+ localStorage.removeItem(k);
5542
+ return undefined;
5543
+ }
5477
5544
  }
5478
5545
  static remove(k) {
5479
5546
  localStorage.removeItem(k);
@@ -5491,7 +5558,16 @@ var fml = (function (exports) {
5491
5558
  }
5492
5559
  static load(k) {
5493
5560
  const got = sessionStorage.getItem(k);
5494
- return got === null ? undefined : JSON.parse(got);
5561
+ if (got === null) {
5562
+ return undefined;
5563
+ }
5564
+ try {
5565
+ return JSON.parse(got);
5566
+ } catch {
5567
+ //not what save wrote: drop it, otherwise every later read fails the same way
5568
+ sessionStorage.removeItem(k);
5569
+ return undefined;
5570
+ }
5495
5571
  }
5496
5572
  static remove(k) {
5497
5573
  sessionStorage.removeItem(k);
@@ -5513,7 +5589,7 @@ var fml = (function (exports) {
5513
5589
  return undefined;
5514
5590
  }
5515
5591
  if (stored.revision !== revision) {
5516
- localStorage.removeItem(key);
5592
+ LocalStorage.remove(key);
5517
5593
  return undefined;
5518
5594
  }
5519
5595
  return stored.data;
@@ -5530,7 +5606,7 @@ var fml = (function (exports) {
5530
5606
  return undefined;
5531
5607
  }
5532
5608
  if (stored.revision !== revision) {
5533
- localStorage.removeItem(key);
5609
+ SessionStorage.remove(key);
5534
5610
  return undefined;
5535
5611
  }
5536
5612
  return stored.data;
@@ -5665,7 +5741,7 @@ var fml = (function (exports) {
5665
5741
  */
5666
5742
  static debounce(timeoutMs, func, options) {
5667
5743
  const opts = options ?? Timing.DEBOUNCE_DEFAULT;
5668
- let tid = null;
5744
+ let tid = /** @type {number | null} */ (null);
5669
5745
  let args = [];
5670
5746
  let previousTimestamp = 0;
5671
5747
 
@@ -5685,8 +5761,8 @@ var fml = (function (exports) {
5685
5761
  }
5686
5762
  };
5687
5763
 
5688
- const debounced = function () {
5689
- args = [...arguments];
5764
+ const debounced = (...called) => {
5765
+ args = called;
5690
5766
  previousTimestamp = performance.now();
5691
5767
  if (tid === null) {
5692
5768
  tid = setTimeout(later, timeoutMs);
@@ -5695,7 +5771,11 @@ var fml = (function (exports) {
5695
5771
  }
5696
5772
  }
5697
5773
  };
5698
- const abort = () => clearTimeout(tid);
5774
+ const abort = () => {
5775
+ clearTimeout(tid ?? undefined);
5776
+ tid = null;
5777
+ args = [];
5778
+ };
5699
5779
  return [debounced, abort];
5700
5780
  }
5701
5781
  static THROTTLE_DEFAULT = 0;
@@ -5710,7 +5790,7 @@ var fml = (function (exports) {
5710
5790
  */
5711
5791
  static throttle(timeoutMs, func, options) {
5712
5792
  const opts = options ?? Timing.THROTTLE_DEFAULT;
5713
- let tid = null;
5793
+ let tid = /** @type {number | null} */ (null);
5714
5794
  let args = [];
5715
5795
  let previousTimestamp = 0;
5716
5796
 
@@ -5722,13 +5802,13 @@ var fml = (function (exports) {
5722
5802
  args = [];
5723
5803
  }
5724
5804
  };
5725
- const throttled = function () {
5805
+ const throttled = (...called) => {
5726
5806
  const now = performance.now();
5727
5807
  if (!previousTimestamp && opts & Timing.THROTTLE_NO_LEADING) {
5728
5808
  previousTimestamp = now;
5729
5809
  }
5730
5810
  const remaining = previousTimestamp === 0 ? 0 : timeoutMs - (now - previousTimestamp);
5731
- args = [...arguments];
5811
+ args = called;
5732
5812
  if (remaining <= 0 || remaining > timeoutMs) {
5733
5813
  if (tid !== null) {
5734
5814
  clearTimeout(tid);
@@ -5743,7 +5823,11 @@ var fml = (function (exports) {
5743
5823
  tid = setTimeout(later, remaining);
5744
5824
  }
5745
5825
  };
5746
- const abort = () => clearTimeout(tid);
5826
+ const abort = () => {
5827
+ clearTimeout(tid ?? undefined);
5828
+ tid = null;
5829
+ args = [];
5830
+ };
5747
5831
  return [throttled, abort];
5748
5832
  }
5749
5833
  }
@@ -5757,7 +5841,7 @@ var fml = (function (exports) {
5757
5841
  */
5758
5842
  static flatten(obj, prefix, stops) {
5759
5843
  return Object.keys(obj).reduce((acc, k) => {
5760
- const pre = prefix.length ? prefix + '.' + k : k;
5844
+ const pre = prefix.length ? `${prefix}.${k}` : k;
5761
5845
  if (!stops.has(pre) && typeof obj[k] === 'object' && obj[k] !== null) {
5762
5846
  Object.assign(acc, Bindings.flatten(obj[k], pre, stops));
5763
5847
  } else {
@@ -5775,7 +5859,7 @@ var fml = (function (exports) {
5775
5859
  static providePath(result, path, value) {
5776
5860
  const keys = path.split('.').map((k) => (/^[0-9]+$/.test(k) ? +k : k));
5777
5861
  let current = result ?? {};
5778
- let previous = null;
5862
+ let previous = /** @type {any} */ (null);
5779
5863
  for (let i = 0; ; ++i) {
5780
5864
  const ckey = keys[i];
5781
5865
  const pkey = keys[i - 1];
@@ -5808,12 +5892,12 @@ var fml = (function (exports) {
5808
5892
  if (!el.checked) {
5809
5893
  return undefined;
5810
5894
  }
5811
- return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
5895
+ return el.dataset.fulBindType === 'boolean' ? el.value === 'true' : el.value;
5812
5896
  }
5813
5897
  if (el.getAttribute('type') === 'checkbox') {
5814
5898
  return el.checked;
5815
5899
  }
5816
- if (el.dataset['fulBindType'] === 'boolean') {
5900
+ if (el.dataset.fulBindType === 'boolean') {
5817
5901
  return !el.value ? null : el.value === 'true';
5818
5902
  }
5819
5903
  if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
@@ -5846,12 +5930,14 @@ var fml = (function (exports) {
5846
5930
 
5847
5931
  /**
5848
5932
  *
5849
- * @param {Element & {checked?: boolean} & {value?: any}} el
5933
+ * @param {Element & {dataset?: any} & {checked?: boolean} & {value?: any}} el
5850
5934
  * @returns
5851
5935
  */
5852
5936
  static mutate(el, raw) {
5853
5937
  if (el.getAttribute('type') === 'radio') {
5854
- el.checked = el.getAttribute('value') === raw;
5938
+ //values are matched as strings, as ful-radio-group does: extract decodes
5939
+ //boolean radios, and payloads carry numbers where the attribute is text
5940
+ el.checked = raw != null && el.getAttribute('value') === String(raw);
5855
5941
  return;
5856
5942
  }
5857
5943
  if (el.getAttribute('type') === 'checkbox') {
@@ -5875,7 +5961,9 @@ var fml = (function (exports) {
5875
5961
  static errors(form, es, scrollOnError) {
5876
5962
  const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
5877
5963
  const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
5878
- form.querySelectorAll(`[name]`).forEach((el) => el.setCustomValidity?.(''));
5964
+ form.querySelectorAll(`[name]`).forEach((el) => {
5965
+ el.setCustomValidity?.('');
5966
+ });
5879
5967
  form.querySelectorAll('ful-errors').forEach((el) => {
5880
5968
  el.replaceChildren();
5881
5969
  el.setAttribute('hidden', '');
@@ -5883,12 +5971,12 @@ var fml = (function (exports) {
5883
5971
  fieldErrors.forEach((e) => {
5884
5972
  const name = e.context.replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
5885
5973
  const parts = name.split('.');
5886
- for (let i = parts.length; i != 0; --i) {
5974
+ for (let i = parts.length; i !== 0; --i) {
5887
5975
  const prefix = parts.slice(0, i).join('.');
5888
5976
  const suffix = parts.slice(i, parts.length).join('.');
5889
- form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) =>
5890
- input.setCustomValidity?.(e.reason, suffix),
5891
- );
5977
+ form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) => {
5978
+ input.setCustomValidity?.(e.reason, suffix);
5979
+ });
5892
5980
  }
5893
5981
  });
5894
5982
  form.querySelectorAll('ful-errors').forEach((el) => {
@@ -5898,7 +5986,7 @@ var fml = (function (exports) {
5898
5986
  el.removeAttribute('hidden');
5899
5987
  }
5900
5988
  });
5901
- if (es.length == 0 || !scrollOnError) {
5989
+ if (es.length === 0 || !scrollOnError) {
5902
5990
  return;
5903
5991
  }
5904
5992
  Array.from(form.querySelectorAll(`:invalid`))
@@ -5970,7 +6058,8 @@ var fml = (function (exports) {
5970
6058
  class Form extends ParsedElement {
5971
6059
  form;
5972
6060
  render() {
5973
- const form = (this.form = document.createElement('form'));
6061
+ const form = document.createElement('form');
6062
+ this.form = form;
5974
6063
  form.setAttribute('novalidate', '');
5975
6064
  Attributes.forward('form-', this, form);
5976
6065
  form.replaceChildren(...this.childNodes);
@@ -5993,50 +6082,53 @@ var fml = (function (exports) {
5993
6082
  */
5994
6083
  async submit(submitter) {
5995
6084
  this.spinner(true);
6085
+ //one try: building the loader and preparing the request are as much part of a
6086
+ //submit as sending it, and a mapper that throws is how a caller reports a
6087
+ //problem with the values
6088
+ let values;
6089
+ let request;
5996
6090
  try {
5997
6091
  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', {
6092
+ values = Bindings.extractFrom(this.form, submitter);
6093
+ request = await loader.prepare(values, this);
6094
+ const se = new CustomEvent('submit', {
6095
+ bubbles: true,
6096
+ cancelable: true,
6097
+ detail: { submitter, values, request },
6098
+ });
6099
+ if (!this.dispatchEvent(se)) {
6100
+ return;
6101
+ }
6102
+ this.errors = [];
6103
+ const sre = new CustomEvent('submit:requested', {
6104
+ bubbles: true,
6105
+ cancelable: false,
6106
+ detail: { submitter, values: se.detail.values, request: se.detail.request },
6107
+ });
6108
+ let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
6109
+ request = sre.detail.request;
6110
+
6111
+ response = await loader.submit(request, this, response);
6112
+ const mapped = await loader.transform(response, this);
6113
+ this.dispatchEvent(
6114
+ new CustomEvent('submit:success', {
6002
6115
  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', {
6116
+ cancelable: false,
6117
+ detail: { submitter, values, request, response: mapped },
6118
+ }),
6119
+ );
6120
+ } catch (e) {
6121
+ this.dispatchEvent(
6122
+ new CustomEvent('submit:failure', {
6011
6123
  bubbles: true,
6012
6124
  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);
6125
+ detail: { submitter, values, request, exception: e },
6126
+ }),
6127
+ );
6128
+ if (e instanceof Failure) {
6129
+ this.errors = e.problems;
6039
6130
  }
6131
+ console.warn('failed to submit form', this, 'reason:', e);
6040
6132
  } finally {
6041
6133
  this.spinner(false);
6042
6134
  }
@@ -6044,7 +6136,20 @@ var fml = (function (exports) {
6044
6136
  reset() {
6045
6137
  this.form.reset();
6046
6138
  }
6139
+ #spinning = 0;
6047
6140
  spinner(spin) {
6141
+ //submits can overlap: only the outermost one saves and restores the button states
6142
+ if (spin) {
6143
+ ++this.#spinning;
6144
+ if (this.#spinning !== 1) {
6145
+ return;
6146
+ }
6147
+ } else {
6148
+ this.#spinning = Math.max(0, this.#spinning - 1);
6149
+ if (this.#spinning !== 0) {
6150
+ return;
6151
+ }
6152
+ }
6048
6153
  this.querySelectorAll('ful-spinner').forEach((el) => {
6049
6154
  const hel = /** @type HTMLElement */ (el);
6050
6155
  hel.hidden = !spin;
@@ -6075,7 +6180,7 @@ var fml = (function (exports) {
6075
6180
  }
6076
6181
 
6077
6182
  class Input extends ParsedElement {
6078
- static observed = ['value', 'readonly:presence', 'required:presence'];
6183
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder'];
6079
6184
  static slots = true;
6080
6185
  static template = `
6081
6186
  <div class="form-label">
@@ -6112,12 +6217,6 @@ var fml = (function (exports) {
6112
6217
  this._input = fragment.querySelector('input,textarea');
6113
6218
 
6114
6219
  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
6220
  this._input.addEventListener('keydown', (evt) => {
6122
6221
  if (evt.key !== 'Enter' || this._type() === 'textarea') {
6123
6222
  return;
@@ -6133,19 +6232,26 @@ var fml = (function (exports) {
6133
6232
  form.requestSubmit(submitter);
6134
6233
  });
6135
6234
  this._input.addEventListener('input', (evt) => {
6136
- const re = this.getAttribute('mask');
6137
- if (!re) {
6235
+ const mask = this.getAttribute('mask');
6236
+ if (!mask) {
6138
6237
  return;
6139
6238
  }
6239
+ const strip = (v) => v.replace(new RegExp(mask, 'g'), '');
6140
6240
  const before = evt.target.value;
6141
- const after = before.replace(new RegExp(re, 'g'), '');
6241
+ const after = strip(before);
6142
6242
  if (before === after) {
6143
6243
  return;
6144
6244
  }
6145
6245
  const start = evt.target.selectionStart;
6146
- const offset = before.length - after.length;
6147
6246
  evt.target.value = after;
6148
- evt.target.setSelectionRange(start - offset, start - offset);
6247
+ if (start === null) {
6248
+ //email, number and the date types have no selection to restore
6249
+ return;
6250
+ }
6251
+ //the caret keeps its place among the characters that survived, so only the
6252
+ //ones stripped before it count
6253
+ const caret = strip(before.slice(0, start)).length;
6254
+ evt.target.setSelectionRange(caret, caret);
6149
6255
  });
6150
6256
  this._input.addEventListener('change', (evt) => {
6151
6257
  evt.stopPropagation();
@@ -6165,6 +6271,15 @@ var fml = (function (exports) {
6165
6271
  this._input.ariaDescribedByElements = [this._fieldError];
6166
6272
  this._input.ariaLabelledByElements = [label];
6167
6273
  this.replaceChildren(fragment);
6274
+ if (!skipObservedSetup) {
6275
+ // biome-ignore lint/complexity/noUselessThisAlias: keeps checkJs from seeing these as class fields
6276
+ const el = this;
6277
+ el.disabled = disabled;
6278
+ el.readonly = observed.readonly;
6279
+ el.required = observed.required;
6280
+ el.placeholder = observed.placeholder;
6281
+ el.value = observed.value;
6282
+ }
6168
6283
  }
6169
6284
  get value() {
6170
6285
  const uppercase = this.hasAttribute('uppercase');
@@ -6191,6 +6306,11 @@ var fml = (function (exports) {
6191
6306
  }
6192
6307
  set disabled(d) {
6193
6308
  Attributes.toggle(this._input, 'disabled', d);
6309
+ //also on the host: a form associated element only matches :disabled through its
6310
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
6311
+ //is needed, disabled is deliberately not observed: the platform delivers it
6312
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
6313
+ Attributes.toggle(this, 'disabled', d);
6194
6314
  }
6195
6315
  get required() {
6196
6316
  return this._input.getAttribute('aria-required') === 'true';
@@ -6201,6 +6321,18 @@ var fml = (function (exports) {
6201
6321
  Attributes.toggle(this, 'required', d);
6202
6322
  });
6203
6323
  }
6324
+ get placeholder() {
6325
+ const v = this._input.getAttribute('placeholder');
6326
+ return v === ' ' ? null : v;
6327
+ }
6328
+ set placeholder(d) {
6329
+ //without a placeholder :placeholder-shown never matches, and floating labels
6330
+ //rely on it, so a blank one stands in for none
6331
+ Attributes.set(this._input, 'placeholder', d ?? ' ');
6332
+ this.reflect(() => {
6333
+ Attributes.set(this, 'placeholder', d);
6334
+ });
6335
+ }
6204
6336
  focus(options) {
6205
6337
  this._input.focus(options);
6206
6338
  }
@@ -6220,23 +6352,23 @@ var fml = (function (exports) {
6220
6352
 
6221
6353
  class LocalDate extends ParsedElement {
6222
6354
  render() {
6223
- const content = this.innerHTML.trim();
6355
+ const content = this.textContent.trim();
6224
6356
  if (content === '') {
6225
- this.innerHTML = this.getAttribute('default') ?? '';
6357
+ this.replaceChildren(this.getAttribute('default') ?? '');
6226
6358
  return;
6227
6359
  }
6228
6360
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
6229
6361
  const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
6230
6362
  const [y, m, d] = content.split('-').map(Number);
6231
- this.innerHTML = formatter.format(new Date(y, m - 1, d));
6363
+ this.replaceChildren(formatter.format(new Date(y, m - 1, d)));
6232
6364
  }
6233
6365
  }
6234
6366
 
6235
6367
  class Instant extends ParsedElement {
6236
6368
  render() {
6237
- const content = this.innerHTML.trim();
6369
+ const content = this.textContent.trim();
6238
6370
  if (content === '') {
6239
- this.innerHTML = this.getAttribute('default') ?? '';
6371
+ this.replaceChildren(this.getAttribute('default') ?? '');
6240
6372
  return;
6241
6373
  }
6242
6374
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
@@ -6249,7 +6381,7 @@ var fml = (function (exports) {
6249
6381
  second: 'numeric',
6250
6382
  hour12: false,
6251
6383
  });
6252
- this.innerHTML = format.format(new Date(Instant.isoToLocal(content)));
6384
+ this.replaceChildren(format.format(new Date(Instant.isoToLocal(content))));
6253
6385
  }
6254
6386
  static isoToLocal(iso) {
6255
6387
  //this is so sad
@@ -6262,16 +6394,17 @@ var fml = (function (exports) {
6262
6394
  }
6263
6395
 
6264
6396
  class InputLocalDate extends Input {
6265
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
6397
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
6266
6398
  _type() {
6267
6399
  return 'date';
6268
6400
  }
6269
6401
  render(conf) {
6270
6402
  const { observed } = conf;
6271
6403
  super.render(conf);
6404
+ //step first: on a time input min and max are snapped to its grid
6405
+ this.step = observed.step;
6272
6406
  this.min = observed.min;
6273
6407
  this.max = observed.max;
6274
- this.step = observed.step;
6275
6408
  }
6276
6409
  get min() {
6277
6410
  const v = this._input.min;
@@ -6317,13 +6450,14 @@ var fml = (function (exports) {
6317
6450
  case 'd':
6318
6451
  r.setDate(r.getDate() + offset * sign);
6319
6452
  break;
6320
- case 'm':
6453
+ case 'm': {
6321
6454
  const originalDay = r.getDate();
6322
6455
  r.setMonth(r.getMonth() + offset * sign);
6323
6456
  if (r.getDate() !== originalDay) {
6324
6457
  r.setDate(0);
6325
6458
  }
6326
6459
  break;
6460
+ }
6327
6461
  case 'y':
6328
6462
  r.setFullYear(r.getFullYear() + offset * sign);
6329
6463
  break;
@@ -6336,10 +6470,62 @@ var fml = (function (exports) {
6336
6470
  _type() {
6337
6471
  return 'time';
6338
6472
  }
6473
+ get min() {
6474
+ const v = this._input.min;
6475
+ return v === '' ? null : v;
6476
+ }
6477
+ set min(v) {
6478
+ this._input.min = this.#fromNowOrOffset(v);
6479
+ }
6480
+ get max() {
6481
+ const v = this._input.max;
6482
+ return v === '' ? null : v;
6483
+ }
6484
+ set max(v) {
6485
+ this._input.max = this.#fromNowOrOffset(v);
6486
+ }
6487
+ /**
6488
+ * Resolves `now` and hour or minute offsets against the current time, wrapping
6489
+ * around midnight. `m` is minutes here, unlike the date offsets of the parent where
6490
+ * it is months: months mean nothing on a time. Anything else is passed through.
6491
+ */
6492
+ #fromNowOrOffset(v) {
6493
+ if (!v) {
6494
+ return '';
6495
+ }
6496
+ const resolved = new Date();
6497
+ if (v !== 'now') {
6498
+ const re = /^([+-])(\d+)([hm])$/;
6499
+ const match = re.exec(v);
6500
+ if (!match) {
6501
+ return v;
6502
+ }
6503
+ const sign = match[1] === '-' ? -1 : 1;
6504
+ const offset = +match[2] * sign;
6505
+ if (match[3] === 'h') {
6506
+ resolved.setHours(resolved.getHours() + offset);
6507
+ } else {
6508
+ resolved.setMinutes(resolved.getMinutes() + offset);
6509
+ }
6510
+ }
6511
+ return InputLocalTime.#snapped(resolved, Number(this._input.step) || 60);
6512
+ }
6513
+ /**
6514
+ * Truncates a time to the step grid: min anchors that grid, so a bound that is not
6515
+ * on it makes every value on it invalid.
6516
+ */
6517
+ static #snapped(date, stepSeconds) {
6518
+ const pad = (n) => String(n).padStart(2, '0');
6519
+ const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
6520
+ const snapped = Math.floor(seconds / stepSeconds) * stepSeconds;
6521
+ const hh = pad(Math.floor(snapped / 3600));
6522
+ const mm = pad(Math.floor((snapped % 3600) / 60));
6523
+ return stepSeconds % 60 === 0 ? `${hh}:${mm}` : `${hh}:${mm}:${pad(snapped % 60)}`;
6524
+ }
6339
6525
  }
6340
6526
 
6341
6527
  class InputInstant extends Input {
6342
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
6528
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
6343
6529
  _type() {
6344
6530
  return 'datetime-local';
6345
6531
  }
@@ -6384,28 +6570,28 @@ var fml = (function (exports) {
6384
6570
  static l10n = {
6385
6571
  en: {
6386
6572
  dropzonelabel: 'Click or drop your files here',
6387
- unaccepptablefiletype: 'Only files of type {0} are supported',
6573
+ unacceptablefiletype: 'Only files of type {0} are supported',
6388
6574
  maxfilesizeexceeded: 'Maximum supported file size is {0}',
6389
6575
  maxtotalsizeexceeded: 'Maximum supported total file size is {0}',
6390
6576
  maxfilesexceeded: 'Maximum files count exceeded',
6391
6577
  },
6392
6578
  it: {
6393
6579
  dropzonelabel: 'Clicca o trascina i file qui',
6394
- unaccepptablefiletype: 'Solo i file di tipo {0} sono supportati',
6580
+ unacceptablefiletype: 'Solo i file di tipo {0} sono supportati',
6395
6581
  maxfilesizeexceeded: 'La dimensione massima di un file è di {0}',
6396
6582
  maxtotalsizeexceeded: 'La dimensione massima complessiva dei file è di {0}',
6397
6583
  maxfilesexceeded: 'Numero massimo di file superato',
6398
6584
  },
6399
6585
  es: {
6400
6586
  dropzonelabel: 'Haz clic o arrastra tus archivos aquí',
6401
- unaccepptablefiletype: 'Solo se admiten archivos de tipo {0}',
6587
+ unacceptablefiletype: 'Solo se admiten archivos de tipo {0}',
6402
6588
  maxfilesizeexceeded: 'El tamaño máximo de archivo admitido es {0}',
6403
6589
  maxtotalsizeexceeded: 'El tamaño total máximo admitido es {0}',
6404
6590
  maxfilesexceeded: 'Se ha superado el número máximo de archivos',
6405
6591
  },
6406
6592
  fr: {
6407
6593
  dropzonelabel: 'Cliquez ou déposez vos fichiers ici',
6408
- unaccepptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
6594
+ unacceptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
6409
6595
  maxfilesizeexceeded: 'La taille maximale de fichier prise en charge est {0}',
6410
6596
  maxtotalsizeexceeded: 'La taille totale maximale prise en charge est {0}',
6411
6597
  maxfilesexceeded: 'Nombre maximal de fichiers dépassé',
@@ -6415,6 +6601,7 @@ var fml = (function (exports) {
6415
6601
  'value',
6416
6602
  'readonly:presence',
6417
6603
  'required:presence',
6604
+ 'placeholder',
6418
6605
  'accept:csv',
6419
6606
  'multiple:presence',
6420
6607
  'itemlist:presence',
@@ -6464,7 +6651,7 @@ var fml = (function (exports) {
6464
6651
  };
6465
6652
  render(conf) {
6466
6653
  const { observed } = conf;
6467
- super.render(conf);
6654
+ super.render({ ...conf, skipObservedSetup: true });
6468
6655
  this.#items = this.querySelector('ful-item-list');
6469
6656
  this.#dropzone = this.querySelector('[data-ref=dropzone]');
6470
6657
  this.#warnings = this.querySelector('ful-field-warnings');
@@ -6475,6 +6662,12 @@ var fml = (function (exports) {
6475
6662
  this.maxfiles = observed.maxfiles;
6476
6663
  this.maxfilesize = observed.maxfilesize;
6477
6664
  this.maxtotalsize = observed.maxtotalsize;
6665
+
6666
+ this.disabled = conf.disabled;
6667
+ this.readonly = observed.readonly;
6668
+ this.required = observed.required;
6669
+ this.placeholder = observed.placeholder;
6670
+ this.value = observed.value;
6478
6671
  this.#warnings.addEventListener('animationend', (e) => {
6479
6672
  e.target.remove();
6480
6673
  });
@@ -6482,11 +6675,17 @@ var fml = (function (exports) {
6482
6675
  if (!e.target.closest('button')) {
6483
6676
  return;
6484
6677
  }
6485
- const fileName = e.target.closest('ful-item').dataset.name;
6678
+ const idx = [...this.#items.children].indexOf(e.target.closest('ful-item'));
6679
+ if (idx === -1) {
6680
+ return;
6681
+ }
6486
6682
  const dt = new DataTransfer();
6487
- [...this.files].filter((f) => f.name !== fileName).forEach((f) => dt.items.add(f));
6683
+ [...this.files]
6684
+ .filter((f, i) => i !== idx)
6685
+ .forEach((f) => {
6686
+ dt.items.add(f);
6687
+ });
6488
6688
  this.files = dt.files;
6489
- this.#update();
6490
6689
  });
6491
6690
  this.#dropzone.addEventListener('click', (e) => {
6492
6691
  this.querySelector('input')?.click();
@@ -6497,10 +6696,15 @@ var fml = (function (exports) {
6497
6696
  });
6498
6697
  this.#dropzone.addEventListener('drop', (e) => {
6499
6698
  e.preventDefault();
6699
+ const dropped = [...e.dataTransfer.items].filter((i) => i.kind === 'file');
6700
+ if (dropped.length === 0) {
6701
+ return;
6702
+ }
6500
6703
  const dt = new DataTransfer();
6501
- [...e.dataTransfer.items].filter((i) => i.kind === 'file').forEach((i) => dt.items.add(i.getAsFile()));
6704
+ dropped.forEach((i) => {
6705
+ dt.items.add(i.getAsFile());
6706
+ });
6502
6707
  this.files = dt.files;
6503
- this.#update();
6504
6708
  });
6505
6709
  this._input.addEventListener('change', (e) => {
6506
6710
  this.#update();
@@ -6515,6 +6719,7 @@ var fml = (function (exports) {
6515
6719
  }
6516
6720
  #update() {
6517
6721
  this.setCustomValidity();
6722
+ this.#warnings.replaceChildren();
6518
6723
  this.#ensureAcceptable();
6519
6724
  this.#ensureFileSizes();
6520
6725
  this.#ensureTotalSize();
@@ -6525,7 +6730,7 @@ var fml = (function (exports) {
6525
6730
  .renderTo(this.#items);
6526
6731
  }
6527
6732
  warning(key, args) {
6528
- this.template('warning').withOverlay({ key, args }).renderTo(this.#warnings);
6733
+ this.template('warning').withOverlay({ key, args }).appendTo(this.#warnings);
6529
6734
  }
6530
6735
  #ensureAcceptable() {
6531
6736
  if (!this.#accept.length) {
@@ -6538,10 +6743,14 @@ var fml = (function (exports) {
6538
6743
  if (unacceptable.length === 0) {
6539
6744
  return;
6540
6745
  }
6541
- this.warning('unaccepptablefiletype', this.#accept.join(', '));
6746
+ this.warning('unacceptablefiletype', this.#accept.join(', '));
6542
6747
  const dt = new DataTransfer();
6543
- [...this.files].filter((f) => !unacceptable.includes(f)).forEach((f) => dt.items.add(f));
6544
- this.files = dt.files;
6748
+ [...this.files]
6749
+ .filter((f) => !unacceptable.includes(f))
6750
+ .forEach((f) => {
6751
+ dt.items.add(f);
6752
+ });
6753
+ this._input.files = dt.files;
6545
6754
  }
6546
6755
  #ensureFilesCount() {
6547
6756
  if (this.#maxfiles === null) {
@@ -6551,8 +6760,7 @@ var fml = (function (exports) {
6551
6760
  return;
6552
6761
  }
6553
6762
  this.warning('maxfilesexceeded');
6554
- const dt = new DataTransfer();
6555
- this.files = dt.files;
6763
+ this._input.files = new DataTransfer().files;
6556
6764
  }
6557
6765
 
6558
6766
  #ensureFileSizes() {
@@ -6565,8 +6773,12 @@ var fml = (function (exports) {
6565
6773
  }
6566
6774
  this.warning('maxfilesizeexceeded', this.#formatByteSize(this.#maxfilesize));
6567
6775
  const dt = new DataTransfer();
6568
- [...this.files].filter((f) => !oversized.includes(f)).forEach((f) => dt.items.add(f));
6569
- this.files = dt.files;
6776
+ [...this.files]
6777
+ .filter((f) => !oversized.includes(f))
6778
+ .forEach((f) => {
6779
+ dt.items.add(f);
6780
+ });
6781
+ this._input.files = dt.files;
6570
6782
  }
6571
6783
  #ensureTotalSize() {
6572
6784
  if (this.#maxtotalsize === null) {
@@ -6577,7 +6789,7 @@ var fml = (function (exports) {
6577
6789
  return;
6578
6790
  }
6579
6791
  this.warning('maxtotalsizeexceeded', this.#formatByteSize(this.#maxtotalsize));
6580
- this.files = new DataTransfer().files;
6792
+ this._input.files = new DataTransfer().files;
6581
6793
  }
6582
6794
  get accept() {
6583
6795
  return this.#accept;
@@ -6603,6 +6815,7 @@ var fml = (function (exports) {
6603
6815
  }
6604
6816
  set files(vs) {
6605
6817
  this._input.files = vs;
6818
+ this.#update();
6606
6819
  }
6607
6820
  get file() {
6608
6821
  return this.files[0] ?? null;
@@ -6623,7 +6836,6 @@ var fml = (function (exports) {
6623
6836
  return;
6624
6837
  }
6625
6838
  this.files = new DataTransfer().files;
6626
- this.#update();
6627
6839
  }
6628
6840
  get totalsize() {
6629
6841
  return Array.from(this.files).reduce((a, f) => a + f.size, 0);
@@ -6790,7 +7002,7 @@ var fml = (function (exports) {
6790
7002
  const http = registry.component('http-client');
6791
7003
  const responseMapper = SelectLoader.#responseMapperFrom(el);
6792
7004
 
6793
- if ('chunked' == el.getAttribute('mode')) {
7005
+ if ('chunked' === el.getAttribute('mode')) {
6794
7006
  return new PartialRemoteLoader({
6795
7007
  http,
6796
7008
  url: el.getAttribute('src'),
@@ -6866,8 +7078,14 @@ var fml = (function (exports) {
6866
7078
  });
6867
7079
  this.replaceChildren(fragment);
6868
7080
  }
7081
+ #selected() {
7082
+ return this.#menu?.querySelector('[selected]') ?? this.#menu?.firstElementChild ?? null;
7083
+ }
6869
7084
  acceptSelection() {
6870
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
7085
+ const selected = this.#selected();
7086
+ if (!selected) {
7087
+ return;
7088
+ }
6871
7089
  this.#change(selected);
6872
7090
  }
6873
7091
  update(values) {
@@ -6903,6 +7121,9 @@ var fml = (function (exports) {
6903
7121
  try {
6904
7122
  const data = await loader();
6905
7123
  this.update(data);
7124
+ } catch (e) {
7125
+ this.hide();
7126
+ throw e;
6906
7127
  } finally {
6907
7128
  this.#spinner.setAttribute('hidden', '');
6908
7129
  this.#menu.removeAttribute('hidden');
@@ -6910,9 +7131,9 @@ var fml = (function (exports) {
6910
7131
  }
6911
7132
  async moveOrShow(forward, loader) {
6912
7133
  if (this.shown) {
6913
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
6914
- const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
6915
- if (candidate) {
7134
+ const selected = this.#selected();
7135
+ const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
7136
+ if (selected && candidate) {
6916
7137
  selected.removeAttribute('selected');
6917
7138
  candidate.setAttribute('selected', '');
6918
7139
  candidate.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
@@ -6965,6 +7186,7 @@ var fml = (function (exports) {
6965
7186
  #multiple;
6966
7187
  #fieldError;
6967
7188
  #values = new Map();
7189
+ #token = 0;
6968
7190
  constructor() {
6969
7191
  super();
6970
7192
  this.internals = this.attachInternals();
@@ -6977,7 +7199,11 @@ var fml = (function (exports) {
6977
7199
  .create(this, { options: slots.options });
6978
7200
 
6979
7201
  this.#multiple = this.hasAttribute('multiple');
6980
- await this.#loader.prefetch?.();
7202
+ try {
7203
+ await this.#loader.prefetch?.();
7204
+ } catch (/** @type any */ e) {
7205
+ console.warn('failed to prefetch select options', this, 'reason:', e);
7206
+ }
6981
7207
  const fragment = this.template().withOverlay({ slots, name }).render();
6982
7208
  this.#input = fragment.querySelector('input');
6983
7209
  this.#items = fragment.querySelector('ful-item-list');
@@ -6996,11 +7222,9 @@ var fml = (function (exports) {
6996
7222
  this.#fieldError = fragment.querySelector('ful-field-error');
6997
7223
  this.#input.ariaDescribedByElements = [this.#fieldError];
6998
7224
  this.#input.ariaLabelledByElements = [label];
6999
-
7000
- const self = this;
7001
7225
  const [dload, abortdload] = Timing.throttle(400, () => {
7002
- self.#input.setAttribute('aria-expanded', 'true');
7003
- self.#ddmenu.show(() => self.#loader.load(self.#input.value));
7226
+ this.#input.setAttribute('aria-expanded', 'true');
7227
+ this.#ddmenu.show(() => this.#loader.load(this.#input.value));
7004
7228
  });
7005
7229
  this.addEventListener('click', (/** @type any */ e) => {
7006
7230
  if (e.target.matches('input')) {
@@ -7067,13 +7291,13 @@ var fml = (function (exports) {
7067
7291
  case 'ArrowUp': {
7068
7292
  e.preventDefault();
7069
7293
  this.#input.setAttribute('aria-expanded', 'true');
7070
- this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
7294
+ this.#ddmenu.moveOrShow(false, () => this.#loader.load(this.#input.value));
7071
7295
  break;
7072
7296
  }
7073
7297
  case 'ArrowDown': {
7074
7298
  e.preventDefault();
7075
7299
  this.#input.setAttribute('aria-expanded', 'true');
7076
- this.#ddmenu.moveOrShow(true, () => self.#loader.load(self.#input.value));
7300
+ this.#ddmenu.moveOrShow(true, () => this.#loader.load(this.#input.value));
7077
7301
  break;
7078
7302
  }
7079
7303
  case 'Escape': {
@@ -7082,6 +7306,12 @@ var fml = (function (exports) {
7082
7306
  break;
7083
7307
  }
7084
7308
  case 'Enter': {
7309
+ if (!this.#ddmenu.shown) {
7310
+ //nothing to accept: submit the form as ful-input does. the inner
7311
+ //input carries form="" so it never submits one on its own
7312
+ this.#requestSubmit();
7313
+ return;
7314
+ }
7085
7315
  e.preventDefault();
7086
7316
  this.#input.setAttribute('aria-expanded', 'false');
7087
7317
  this.#ddmenu.acceptSelection();
@@ -7130,6 +7360,16 @@ var fml = (function (exports) {
7130
7360
  async withLoader(fn) {
7131
7361
  return await fn(this.#loader);
7132
7362
  }
7363
+ #requestSubmit() {
7364
+ const form = this.internals.form;
7365
+ if (!form) {
7366
+ return;
7367
+ }
7368
+ const candidates = /** @type [HTMLButtonElement|HTMLInputElement] */ (
7369
+ Array.from(form.querySelectorAll('button:not(:disabled), input:not(:disabled)'))
7370
+ );
7371
+ form.requestSubmit(candidates.find((el) => el.type === 'submit'));
7372
+ }
7133
7373
  #changed() {
7134
7374
  const selection = [...this.#values.entries()].map((e) => ({
7135
7375
  key: e[0],
@@ -7159,16 +7399,43 @@ var fml = (function (exports) {
7159
7399
  this.template('items').withOverlay({ entries: this.#values.entries() }).renderTo(this.#items);
7160
7400
  }
7161
7401
  set value(vs) {
7162
- if (vs === null) {
7163
- this.#values = new Map();
7164
- this.#syncBadges();
7402
+ //the csvm mapper yields [] for a missing multiple value, an empty string is
7403
+ //left alone: it is a usable key for an <option value="">
7404
+ const keys = vs == null ? [] : Array.isArray(vs) ? vs : [vs];
7405
+ //the keys are known synchronously and are all `value` reads, so they are applied
7406
+ //now: only the labels need the loader, until then a key stands in for its own
7407
+ this.#values = new Map(keys.map((k) => [k, [k]]));
7408
+ this.#syncBadges();
7409
+ const token = ++this.#token;
7410
+ if (keys.length === 0) {
7165
7411
  return;
7166
7412
  }
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
- })();
7413
+ this.#resolve(keys, token);
7414
+ }
7415
+ /**
7416
+ * Resolves the labels of the assigned keys. A failed lookup is left to reject so
7417
+ * that it is reported like any other failure: the keys stay applied either way.
7418
+ */
7419
+ async #resolve(keys, token) {
7420
+ const entries = await this.#loader.exact(...keys);
7421
+ if (token !== this.#token) {
7422
+ //a newer assignment has been made in the meantime
7423
+ return;
7424
+ }
7425
+ //label the keys that are still selected: a removal made while the lookup was in
7426
+ //flight must not be undone by it, and a key the loader does not know is dropped
7427
+ const resolved = new Map(entries.map((e) => [e[0], e.slice(1)]));
7428
+ for (const key of keys) {
7429
+ if (!this.#values.has(key)) {
7430
+ continue;
7431
+ }
7432
+ if (resolved.has(key)) {
7433
+ this.#values.set(key, resolved.get(key));
7434
+ } else {
7435
+ this.#values.delete(key);
7436
+ }
7437
+ }
7438
+ this.#syncBadges();
7172
7439
  }
7173
7440
  get value() {
7174
7441
  if (this.#multiple) {
@@ -7187,6 +7454,11 @@ var fml = (function (exports) {
7187
7454
  }
7188
7455
  set disabled(d) {
7189
7456
  Attributes.toggle(this.#input, 'disabled', d);
7457
+ //also on the host: a form associated element only matches :disabled through its
7458
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7459
+ //is needed, disabled is deliberately not observed: the platform delivers it
7460
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7461
+ Attributes.toggle(this, 'disabled', d);
7190
7462
  }
7191
7463
  get readonly() {
7192
7464
  return this.#input.readOnly;
@@ -7292,7 +7564,9 @@ var fml = (function (exports) {
7292
7564
  return [input, label];
7293
7565
  });
7294
7566
 
7295
- radioEls.forEach((el) => el.remove());
7567
+ radioEls.forEach((el) => {
7568
+ el.remove();
7569
+ });
7296
7570
  this.template().withOverlay({ name, slots, inputsAndLabels }).renderTo(this);
7297
7571
  this.#fieldset = this.firstElementChild;
7298
7572
  this.disabled = disabled;
@@ -7336,6 +7610,11 @@ var fml = (function (exports) {
7336
7610
  }
7337
7611
  set disabled(d) {
7338
7612
  Attributes.toggle(this.#fieldset, 'disabled', d);
7613
+ //also on the host: a form associated element only matches :disabled through its
7614
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7615
+ //is needed, disabled is deliberately not observed: the platform delivers it
7616
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7617
+ Attributes.toggle(this, 'disabled', d);
7339
7618
  }
7340
7619
  get required() {
7341
7620
  return this.#fieldset.getAttribute('aria-required') === 'true';
@@ -7385,7 +7664,7 @@ var fml = (function (exports) {
7385
7664
  this.internals.role = 'presentation';
7386
7665
  }
7387
7666
  render({ slots, observed, disabled }) {
7388
- const isSwitch = this.getAttribute('type') == 'switch';
7667
+ const isSwitch = this.getAttribute('type') === 'switch';
7389
7668
  const klass = isSwitch ? 'form-check form-switch' : 'form-check';
7390
7669
  const fragment = this.template().withOverlay({ slots, klass, isSwitch }).render();
7391
7670
  this.#container = fragment.firstElementChild;
@@ -7449,6 +7728,11 @@ var fml = (function (exports) {
7449
7728
  }
7450
7729
  set disabled(d) {
7451
7730
  Attributes.toggle(this.#input, 'disabled', d);
7731
+ //also on the host: a form associated element only matches :disabled through its
7732
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
7733
+ //is needed, disabled is deliberately not observed: the platform delivers it
7734
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
7735
+ Attributes.toggle(this, 'disabled', d);
7452
7736
  }
7453
7737
  get required() {
7454
7738
  return this.#input.getAttribute('aria-required') === 'true';
@@ -7489,9 +7773,10 @@ var fml = (function (exports) {
7489
7773
  class SortButton extends ParsedElement {
7490
7774
  static observed = ['order'];
7491
7775
  #order;
7492
- render() {
7776
+ render({ observed }) {
7493
7777
  const sorter = this.getAttribute('sorter');
7494
7778
  const orders = ['asc', 'desc', null];
7779
+ this.order = observed.order;
7495
7780
  this.addEventListener('click', () => {
7496
7781
  const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
7497
7782
  this.dispatchEvent(
@@ -7581,10 +7866,12 @@ var fml = (function (exports) {
7581
7866
  #total = 0;
7582
7867
  #current = 0;
7583
7868
  render({ observed }) {
7584
- this.update(observed.current ?? 0, observed.total ?? 0);
7869
+ this.total = observed.total ?? 0;
7870
+ this.current = observed.current ?? 0;
7585
7871
  this.addEventListener('click', (/** @type any */ evt) => {
7586
7872
  const el = evt.target.closest('a');
7587
- if (!el) {
7873
+ if (!el || el.classList.contains('disabled')) {
7874
+ //a disabled link leads nowhere: the page it would ask for does not exist
7588
7875
  return;
7589
7876
  }
7590
7877
  this.dispatchEvent(
@@ -7600,25 +7887,20 @@ var fml = (function (exports) {
7600
7887
  }
7601
7888
  update(current, total) {
7602
7889
  const maxRender = Number(this.getAttribute('pages') ?? '5');
7603
- const prev = { index: Math.max(0, current - 1), enabled: current > 0 };
7890
+ const hasPrev = current > 0;
7891
+ const hasNext = current + 1 < total;
7892
+ //a disabled arrow carries no page: there is nothing valid for it to point at
7893
+ const prev = { index: hasPrev ? current - 1 : null, enabled: hasPrev };
7604
7894
  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
- }
7895
+ const next = { index: hasNext ? current + 1 : null, enabled: hasNext };
7896
+ //the window holds at most maxRender pages, centered on the current one and slid
7897
+ //back towards the end so it stays full on the last pages
7898
+ const rendered = Math.max(1, Math.min(maxRender, total));
7899
+ const first = Math.max(0, Math.min(current - Math.floor((rendered - 1) / 2), total - rendered));
7900
+ const pages = Array.from({ length: rendered }, (_, offset) => ({
7901
+ index: first + offset,
7902
+ label: first + offset + 1,
7903
+ }));
7622
7904
  this.template().withOverlay({ total, prev, curr, next, pages }).renderTo(this);
7623
7905
  }
7624
7906
  get total() {
@@ -7645,9 +7927,10 @@ var fml = (function (exports) {
7645
7927
 
7646
7928
  class TableSchemaParser {
7647
7929
  static parse(nodeOrFragment, template) {
7648
- const schema = Nodes.queryChildren(nodeOrFragment, 'schema');
7930
+ //nodeOrFragment is undefined when the slot is missing altogether
7931
+ const schema = nodeOrFragment ? Nodes.queryChildren(nodeOrFragment, 'schema') : null;
7649
7932
  if (!schema) {
7650
- throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
7933
+ throw new Error('missing expected <schema>: ful-table needs a <template slot="schema"> holding one');
7651
7934
  }
7652
7935
  const headersTr = document.createElement('tr');
7653
7936
  const rowsTr = document.createElement('tr');
@@ -7866,9 +8149,10 @@ var fml = (function (exports) {
7866
8149
  this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
7867
8150
  this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
7868
8151
  this.#paginator = Nodes.queryChildren(fragment, 'ful-pagination');
7869
- this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
7870
8152
  this.replaceChildren(fragment);
7871
- schema.headersTemplate.renderTo(this.querySelector('thead'));
8153
+ const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
8154
+ schema.headersTemplate.renderTo(thead);
8155
+ this.#sorters = thead.querySelectorAll('ful-sorter');
7872
8156
  await Rendering.waitForChildren(this);
7873
8157
 
7874
8158
  const maybeForm = /** @type any */ (Nodes.queryChildren(this, 'ful-form'));
@@ -7903,11 +8187,16 @@ var fml = (function (exports) {
7903
8187
  this.addEventListener('sort-requested', async (/** @type any */ e) => {
7904
8188
  const sortRequest = e.detail.value.order ? e.detail.value : null;
7905
8189
  await this.load(this.#latestRequest.pageRequest, sortRequest, this.#latestRequest.filterRequest);
7906
- this.#sorters.forEach((s) => (s.order = null));
8190
+ this.#sorters.forEach((s) => {
8191
+ s.order = null;
8192
+ });
7907
8193
  e.target.order = e.detail.value.order;
7908
8194
  });
7909
8195
  if (this.hasAttribute('autoload')) {
7910
- await this.reload();
8196
+ //not awaited: the first load must not hold up the upgrade, and a loader that
8197
+ //fails or never answers must not keep ftl:ready from firing for the page.
8198
+ //load renders its own error state and lets the failure reject, so it is reported
8199
+ this.reload();
7911
8200
  }
7912
8201
  }
7913
8202
 
@@ -7920,7 +8209,7 @@ var fml = (function (exports) {
7920
8209
  }
7921
8210
  async load(pageRequest, sortRequest, filterRequest) {
7922
8211
  this.#body.replaceChildren();
7923
- this.#loading.removeAttribute('hidden', '');
8212
+ this.#loading.removeAttribute('hidden');
7924
8213
  this.#feedback.setAttribute('hidden', '');
7925
8214
  this.#noAutoload.setAttribute('hidden', '');
7926
8215
  try {
@@ -7929,7 +8218,7 @@ var fml = (function (exports) {
7929
8218
  this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
7930
8219
  } catch (/** @type any */ error) {
7931
8220
  this.#loading.setAttribute('hidden', '');
7932
- this.#feedback.removeAttribute('hidden', '');
8221
+ this.#feedback.removeAttribute('hidden');
7933
8222
  if (!error.problems) {
7934
8223
  this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
7935
8224
  } else {
@@ -7971,7 +8260,7 @@ var fml = (function (exports) {
7971
8260
  }
7972
8261
 
7973
8262
  class InstantFilter extends Input {
7974
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8263
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
7975
8264
  static template = `
7976
8265
  <div class="form-label">
7977
8266
  <label>{{{{ slots.default }}}}</label>
@@ -8005,10 +8294,16 @@ var fml = (function (exports) {
8005
8294
  this.#operator = this.querySelector('[data-ref=operator]');
8006
8295
  this.#value1 = this.querySelector('[data-ref=value1]');
8007
8296
  this.#value2 = this.querySelector('[data-ref=value2]');
8297
+ //Input.render only re-dispatches changes coming from the first operand
8298
+ this.#value2.addEventListener('change', (evt) => {
8299
+ evt.stopPropagation();
8300
+ this.#notifyChange();
8301
+ });
8008
8302
 
8009
8303
  this.disabled = conf.disabled;
8010
8304
  this.readonly = conf.observed.readonly;
8011
8305
  this.required = conf.observed.required;
8306
+ this.placeholder = conf.observed.placeholder;
8012
8307
  this.value = conf.observed.value;
8013
8308
 
8014
8309
  this.addEventListener('click', (evt) => {
@@ -8018,9 +8313,13 @@ var fml = (function (exports) {
8018
8313
  }
8019
8314
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8020
8315
  const value = /** @type String */ (target.getAttribute('value'));
8316
+ const previous = btn.getAttribute('value');
8021
8317
  Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
8022
8318
  btn.setAttribute('value', value);
8023
8319
  btn.innerHTML = target.innerHTML;
8320
+ if (previous !== value) {
8321
+ this.#notifyChange();
8322
+ }
8024
8323
  });
8025
8324
  }
8026
8325
 
@@ -8036,14 +8335,40 @@ var fml = (function (exports) {
8036
8335
  return;
8037
8336
  }
8038
8337
  const [operator, ...values] = v;
8039
- this.#operator.setAttribute('value', operator);
8338
+ this.#showOperator(operator);
8040
8339
  this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
8041
8340
  this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
8042
8341
  }
8342
+ #showOperator(operator) {
8343
+ this.#operator.setAttribute('value', operator);
8344
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8345
+ const item = items.find((a) => a.getAttribute('value') === operator);
8346
+ if (item) {
8347
+ this.#operator.innerHTML = item.innerHTML;
8348
+ }
8349
+ Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
8350
+ }
8351
+ #notifyChange() {
8352
+ this.dispatchEvent(
8353
+ new CustomEvent('change', {
8354
+ bubbles: true,
8355
+ cancelable: false,
8356
+ detail: {
8357
+ value: this.value,
8358
+ },
8359
+ }),
8360
+ );
8361
+ }
8362
+ get readonly() {
8363
+ return super.readonly;
8364
+ }
8043
8365
  set readonly(v) {
8044
8366
  this.#value2.readOnly = v;
8045
8367
  super.readonly = v;
8046
8368
  }
8369
+ get disabled() {
8370
+ return super.disabled;
8371
+ }
8047
8372
  set disabled(d) {
8048
8373
  Attributes.toggle(this.#value2, 'disabled', d);
8049
8374
  super.disabled = d;
@@ -8051,7 +8376,7 @@ var fml = (function (exports) {
8051
8376
  }
8052
8377
 
8053
8378
  class LocalDateFilter extends Input {
8054
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8379
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
8055
8380
  static template = `
8056
8381
  <div class="form-label">
8057
8382
  <label>{{{{ slots.default }}}}</label>
@@ -8086,10 +8411,16 @@ var fml = (function (exports) {
8086
8411
  this.#operator = this.querySelector('[data-ref=operator]');
8087
8412
  this.#value1 = this.querySelector('[data-ref=value1]');
8088
8413
  this.#value2 = this.querySelector('[data-ref=value2]');
8414
+ //Input.render only re-dispatches changes coming from the first operand
8415
+ this.#value2.addEventListener('change', (evt) => {
8416
+ evt.stopPropagation();
8417
+ this.#notifyChange();
8418
+ });
8089
8419
 
8090
8420
  this.disabled = conf.disabled;
8091
8421
  this.readonly = conf.observed.readonly;
8092
8422
  this.required = conf.observed.required;
8423
+ this.placeholder = conf.observed.placeholder;
8093
8424
  this.value = conf.observed.value;
8094
8425
 
8095
8426
  this.addEventListener('click', (evt) => {
@@ -8099,14 +8430,18 @@ var fml = (function (exports) {
8099
8430
  }
8100
8431
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8101
8432
  const value = /** @type String */ (target.getAttribute('value'));
8433
+ const previous = btn.getAttribute('value');
8102
8434
  Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
8103
8435
  btn.setAttribute('value', value);
8104
8436
  btn.innerHTML = target.innerHTML;
8437
+ if (previous !== value) {
8438
+ this.#notifyChange();
8439
+ }
8105
8440
  });
8106
8441
  }
8107
8442
  get value() {
8108
8443
  const operator = this.#operator.getAttribute('value');
8109
- const values = operator == 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
8444
+ const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
8110
8445
  return values.some((v) => v === '') ? undefined : [operator, ...values];
8111
8446
  }
8112
8447
  set value(v) {
@@ -8116,14 +8451,40 @@ var fml = (function (exports) {
8116
8451
  return;
8117
8452
  }
8118
8453
  const [operator, ...values] = v;
8119
- this.#operator.setAttribute('value', operator);
8454
+ this.#showOperator(operator);
8120
8455
  this.#value1.value = values[0];
8121
8456
  this.#value2.value = values[1];
8122
8457
  }
8458
+ #showOperator(operator) {
8459
+ this.#operator.setAttribute('value', operator);
8460
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8461
+ const item = items.find((a) => a.getAttribute('value') === operator);
8462
+ if (item) {
8463
+ this.#operator.innerHTML = item.innerHTML;
8464
+ }
8465
+ Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
8466
+ }
8467
+ #notifyChange() {
8468
+ this.dispatchEvent(
8469
+ new CustomEvent('change', {
8470
+ bubbles: true,
8471
+ cancelable: false,
8472
+ detail: {
8473
+ value: this.value,
8474
+ },
8475
+ }),
8476
+ );
8477
+ }
8478
+ get readonly() {
8479
+ return super.readonly;
8480
+ }
8123
8481
  set readonly(v) {
8124
8482
  this.#value2.readOnly = v;
8125
8483
  super.readonly = v;
8126
8484
  }
8485
+ get disabled() {
8486
+ return super.disabled;
8487
+ }
8127
8488
  set disabled(d) {
8128
8489
  Attributes.toggle(this.#value2, 'disabled', d);
8129
8490
  super.disabled = d;
@@ -8131,7 +8492,7 @@ var fml = (function (exports) {
8131
8492
  }
8132
8493
 
8133
8494
  class TextFilter extends Input {
8134
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
8495
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
8135
8496
  static template = `
8136
8497
  <div class="form-label">
8137
8498
  <label>{{{{ slots.default }}}}</label>
@@ -8155,6 +8516,8 @@ var fml = (function (exports) {
8155
8516
  `;
8156
8517
  #operator;
8157
8518
  #value;
8519
+ //the sensitivity has no control of its own: it is carried through from whoever set the value
8520
+ #sensitivity = 'IGNORE_CASE';
8158
8521
  render(conf) {
8159
8522
  super.render({ ...conf, skipObservedSetup: true });
8160
8523
 
@@ -8164,6 +8527,7 @@ var fml = (function (exports) {
8164
8527
  this.disabled = conf.disabled;
8165
8528
  this.readonly = conf.observed.readonly;
8166
8529
  this.required = conf.observed.required;
8530
+ this.placeholder = conf.observed.placeholder;
8167
8531
  this.value = conf.observed.value;
8168
8532
 
8169
8533
  this.addEventListener('click', (evt) => {
@@ -8173,13 +8537,17 @@ var fml = (function (exports) {
8173
8537
  }
8174
8538
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
8175
8539
  const value = /** @type String */ (target.getAttribute('value'));
8540
+ const previous = btn.getAttribute('value');
8176
8541
  btn.setAttribute('value', value);
8177
8542
  btn.innerHTML = target.innerHTML;
8543
+ if (previous !== value) {
8544
+ this.#notifyChange();
8545
+ }
8178
8546
  });
8179
8547
  }
8180
8548
  get value() {
8181
8549
  const operator = this.#operator.getAttribute('value');
8182
- return this.#value.value === '' ? undefined : [operator, 'IGNORE_CASE', this.#value.value];
8550
+ return this.#value.value === '' ? undefined : [operator, this.#sensitivity, this.#value.value];
8183
8551
  }
8184
8552
  set value(v) {
8185
8553
  if (v == null) {
@@ -8187,15 +8555,35 @@ var fml = (function (exports) {
8187
8555
  return;
8188
8556
  }
8189
8557
  const [operator, sensitivity, value] = v;
8190
- this.#operator.setAttribute('value', operator);
8558
+ this.#showOperator(operator);
8559
+ this.#sensitivity = sensitivity ?? 'IGNORE_CASE';
8191
8560
  this.#value.value = value;
8192
8561
  }
8562
+ #showOperator(operator) {
8563
+ this.#operator.setAttribute('value', operator);
8564
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
8565
+ const item = items.find((a) => a.getAttribute('value') === operator);
8566
+ if (item) {
8567
+ this.#operator.innerHTML = item.innerHTML;
8568
+ }
8569
+ }
8570
+ #notifyChange() {
8571
+ this.dispatchEvent(
8572
+ new CustomEvent('change', {
8573
+ bubbles: true,
8574
+ cancelable: false,
8575
+ detail: {
8576
+ value: this.value,
8577
+ },
8578
+ }),
8579
+ );
8580
+ }
8193
8581
  }
8194
8582
 
8195
8583
  class LocalizationModule {
8196
8584
  static t(k, ...args) {
8197
- //@ts-ignore
8198
- const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.['en']?.[k] ?? k;
8585
+ //@ts-expect-error l10n and language come from the element class and the data stack
8586
+ const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.en?.[k] ?? k;
8199
8587
  if (args.length === 0) {
8200
8588
  return format;
8201
8589
  }
@@ -8278,9 +8666,11 @@ var fml = (function (exports) {
8278
8666
  });
8279
8667
 
8280
8668
  if (typeof window !== 'undefined') {
8669
+ // biome-ignore-start lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
8281
8670
  window['ftl'] = ftl;
8282
8671
  window['httpc'] = httpc;
8283
8672
  window['ful'] = ful;
8673
+ // biome-ignore-end lint/complexity/useLiteralKeys: checkJs rejects unknown properties on Window
8284
8674
  }
8285
8675
 
8286
8676
  exports.AsyncEvents = AsyncEvents;