@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/ful.iife.js CHANGED
@@ -7,7 +7,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
7
7
  }
8
8
  static load(k) {
9
9
  const got = localStorage.getItem(k);
10
- return got === null ? undefined : JSON.parse(got);
10
+ if (got === null) {
11
+ return undefined;
12
+ }
13
+ try {
14
+ return JSON.parse(got);
15
+ } catch {
16
+ //not what save wrote: drop it, otherwise every later read fails the same way
17
+ localStorage.removeItem(k);
18
+ return undefined;
19
+ }
11
20
  }
12
21
  static remove(k) {
13
22
  localStorage.removeItem(k);
@@ -25,7 +34,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
25
34
  }
26
35
  static load(k) {
27
36
  const got = sessionStorage.getItem(k);
28
- return got === null ? undefined : JSON.parse(got);
37
+ if (got === null) {
38
+ return undefined;
39
+ }
40
+ try {
41
+ return JSON.parse(got);
42
+ } catch {
43
+ //not what save wrote: drop it, otherwise every later read fails the same way
44
+ sessionStorage.removeItem(k);
45
+ return undefined;
46
+ }
29
47
  }
30
48
  static remove(k) {
31
49
  sessionStorage.removeItem(k);
@@ -47,7 +65,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
47
65
  return undefined;
48
66
  }
49
67
  if (stored.revision !== revision) {
50
- localStorage.removeItem(key);
68
+ LocalStorage.remove(key);
51
69
  return undefined;
52
70
  }
53
71
  return stored.data;
@@ -64,7 +82,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
64
82
  return undefined;
65
83
  }
66
84
  if (stored.revision !== revision) {
67
- localStorage.removeItem(key);
85
+ SessionStorage.remove(key);
68
86
  return undefined;
69
87
  }
70
88
  return stored.data;
@@ -199,7 +217,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
199
217
  */
200
218
  static debounce(timeoutMs, func, options) {
201
219
  const opts = options ?? Timing.DEBOUNCE_DEFAULT;
202
- let tid = null;
220
+ let tid = /** @type {number | null} */ (null);
203
221
  let args = [];
204
222
  let previousTimestamp = 0;
205
223
 
@@ -219,8 +237,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
219
237
  }
220
238
  };
221
239
 
222
- const debounced = function () {
223
- args = [...arguments];
240
+ const debounced = (...called) => {
241
+ args = called;
224
242
  previousTimestamp = performance.now();
225
243
  if (tid === null) {
226
244
  tid = setTimeout(later, timeoutMs);
@@ -229,7 +247,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
229
247
  }
230
248
  }
231
249
  };
232
- const abort = () => clearTimeout(tid);
250
+ const abort = () => {
251
+ clearTimeout(tid ?? undefined);
252
+ tid = null;
253
+ args = [];
254
+ };
233
255
  return [debounced, abort];
234
256
  }
235
257
  static THROTTLE_DEFAULT = 0;
@@ -244,7 +266,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
244
266
  */
245
267
  static throttle(timeoutMs, func, options) {
246
268
  const opts = options ?? Timing.THROTTLE_DEFAULT;
247
- let tid = null;
269
+ let tid = /** @type {number | null} */ (null);
248
270
  let args = [];
249
271
  let previousTimestamp = 0;
250
272
 
@@ -256,13 +278,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
256
278
  args = [];
257
279
  }
258
280
  };
259
- const throttled = function () {
281
+ const throttled = (...called) => {
260
282
  const now = performance.now();
261
283
  if (!previousTimestamp && opts & Timing.THROTTLE_NO_LEADING) {
262
284
  previousTimestamp = now;
263
285
  }
264
286
  const remaining = previousTimestamp === 0 ? 0 : timeoutMs - (now - previousTimestamp);
265
- args = [...arguments];
287
+ args = called;
266
288
  if (remaining <= 0 || remaining > timeoutMs) {
267
289
  if (tid !== null) {
268
290
  clearTimeout(tid);
@@ -277,7 +299,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
277
299
  tid = setTimeout(later, remaining);
278
300
  }
279
301
  };
280
- const abort = () => clearTimeout(tid);
302
+ const abort = () => {
303
+ clearTimeout(tid ?? undefined);
304
+ tid = null;
305
+ args = [];
306
+ };
281
307
  return [throttled, abort];
282
308
  }
283
309
  }
@@ -291,7 +317,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
291
317
  */
292
318
  static flatten(obj, prefix, stops) {
293
319
  return Object.keys(obj).reduce((acc, k) => {
294
- const pre = prefix.length ? prefix + '.' + k : k;
320
+ const pre = prefix.length ? `${prefix}.${k}` : k;
295
321
  if (!stops.has(pre) && typeof obj[k] === 'object' && obj[k] !== null) {
296
322
  Object.assign(acc, Bindings.flatten(obj[k], pre, stops));
297
323
  } else {
@@ -309,7 +335,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
309
335
  static providePath(result, path, value) {
310
336
  const keys = path.split('.').map((k) => (/^[0-9]+$/.test(k) ? +k : k));
311
337
  let current = result ?? {};
312
- let previous = null;
338
+ let previous = /** @type {any} */ (null);
313
339
  for (let i = 0; ; ++i) {
314
340
  const ckey = keys[i];
315
341
  const pkey = keys[i - 1];
@@ -342,12 +368,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
342
368
  if (!el.checked) {
343
369
  return undefined;
344
370
  }
345
- return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
371
+ return el.dataset.fulBindType === 'boolean' ? el.value === 'true' : el.value;
346
372
  }
347
373
  if (el.getAttribute('type') === 'checkbox') {
348
374
  return el.checked;
349
375
  }
350
- if (el.dataset['fulBindType'] === 'boolean') {
376
+ if (el.dataset.fulBindType === 'boolean') {
351
377
  return !el.value ? null : el.value === 'true';
352
378
  }
353
379
  if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
@@ -380,12 +406,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
380
406
 
381
407
  /**
382
408
  *
383
- * @param {Element & {checked?: boolean} & {value?: any}} el
409
+ * @param {Element & {dataset?: any} & {checked?: boolean} & {value?: any}} el
384
410
  * @returns
385
411
  */
386
412
  static mutate(el, raw) {
387
413
  if (el.getAttribute('type') === 'radio') {
388
- el.checked = el.getAttribute('value') === raw;
414
+ //values are matched as strings, as ful-radio-group does: extract decodes
415
+ //boolean radios, and payloads carry numbers where the attribute is text
416
+ el.checked = raw != null && el.getAttribute('value') === String(raw);
389
417
  return;
390
418
  }
391
419
  if (el.getAttribute('type') === 'checkbox') {
@@ -409,7 +437,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
409
437
  static errors(form, es, scrollOnError) {
410
438
  const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
411
439
  const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
412
- form.querySelectorAll(`[name]`).forEach((el) => el.setCustomValidity?.(''));
440
+ form.querySelectorAll(`[name]`).forEach((el) => {
441
+ el.setCustomValidity?.('');
442
+ });
413
443
  form.querySelectorAll('ful-errors').forEach((el) => {
414
444
  el.replaceChildren();
415
445
  el.setAttribute('hidden', '');
@@ -417,12 +447,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
417
447
  fieldErrors.forEach((e) => {
418
448
  const name = e.context.replace(/\[/g, '.').replace(/\]\./g, '.').replace(/\]/g, '');
419
449
  const parts = name.split('.');
420
- for (let i = parts.length; i != 0; --i) {
450
+ for (let i = parts.length; i !== 0; --i) {
421
451
  const prefix = parts.slice(0, i).join('.');
422
452
  const suffix = parts.slice(i, parts.length).join('.');
423
- form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) =>
424
- input.setCustomValidity?.(e.reason, suffix),
425
- );
453
+ form.querySelectorAll(`[name='${CSS.escape(prefix)}']`).forEach((input) => {
454
+ input.setCustomValidity?.(e.reason, suffix);
455
+ });
426
456
  }
427
457
  });
428
458
  form.querySelectorAll('ful-errors').forEach((el) => {
@@ -432,7 +462,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
432
462
  el.removeAttribute('hidden');
433
463
  }
434
464
  });
435
- if (es.length == 0 || !scrollOnError) {
465
+ if (es.length === 0 || !scrollOnError) {
436
466
  return;
437
467
  }
438
468
  Array.from(form.querySelectorAll(`:invalid`))
@@ -504,7 +534,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
504
534
  class Form extends index_mjs.ParsedElement {
505
535
  form;
506
536
  render() {
507
- const form = (this.form = document.createElement('form'));
537
+ const form = document.createElement('form');
538
+ this.form = form;
508
539
  form.setAttribute('novalidate', '');
509
540
  index_mjs.Attributes.forward('form-', this, form);
510
541
  form.replaceChildren(...this.childNodes);
@@ -527,50 +558,53 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
527
558
  */
528
559
  async submit(submitter) {
529
560
  this.spinner(true);
561
+ //one try: building the loader and preparing the request are as much part of a
562
+ //submit as sending it, and a mapper that throws is how a caller reports a
563
+ //problem with the values
564
+ let values;
565
+ let request;
530
566
  try {
531
567
  const loader = index_mjs.registry.component(this.getAttribute('loader') ?? 'loaders:form').create(this);
532
- const values = Bindings.extractFrom(this.form, submitter);
533
- let request = await loader.prepare(values, this);
534
- try {
535
- const se = new CustomEvent('submit', {
568
+ values = Bindings.extractFrom(this.form, submitter);
569
+ request = await loader.prepare(values, this);
570
+ const se = new CustomEvent('submit', {
571
+ bubbles: true,
572
+ cancelable: true,
573
+ detail: { submitter, values, request },
574
+ });
575
+ if (!this.dispatchEvent(se)) {
576
+ return;
577
+ }
578
+ this.errors = [];
579
+ const sre = new CustomEvent('submit:requested', {
580
+ bubbles: true,
581
+ cancelable: false,
582
+ detail: { submitter, values: se.detail.values, request: se.detail.request },
583
+ });
584
+ let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
585
+ request = sre.detail.request;
586
+
587
+ response = await loader.submit(request, this, response);
588
+ const mapped = await loader.transform(response, this);
589
+ this.dispatchEvent(
590
+ new CustomEvent('submit:success', {
536
591
  bubbles: true,
537
- cancelable: true,
538
- detail: { submitter, values, request },
539
- });
540
- if (!this.dispatchEvent(se)) {
541
- return;
542
- }
543
- this.errors = [];
544
- const sre = new CustomEvent('submit:requested', {
592
+ cancelable: false,
593
+ detail: { submitter, values, request, response: mapped },
594
+ }),
595
+ );
596
+ } catch (e) {
597
+ this.dispatchEvent(
598
+ new CustomEvent('submit:failure', {
545
599
  bubbles: true,
546
600
  cancelable: false,
547
- detail: { submitter, values: se.detail.values, request: se.detail.request },
548
- });
549
- let response = await AsyncEvents.fireAsync(this, sre, { mode: 'pipeline' });
550
- request = sre.detail.request;
551
-
552
- response = await loader.submit(request, this, response);
553
- const mapped = await loader.transform(response, this);
554
- this.dispatchEvent(
555
- new CustomEvent('submit:success', {
556
- bubbles: true,
557
- cancelable: false,
558
- detail: { submitter, values, request, response: mapped },
559
- }),
560
- );
561
- } catch (e) {
562
- this.dispatchEvent(
563
- new CustomEvent('submit:failure', {
564
- bubbles: true,
565
- cancelable: false,
566
- detail: { submitter, values, request, exception: e },
567
- }),
568
- );
569
- if (e instanceof index_mjs$1.Failure) {
570
- this.errors = e.problems;
571
- }
572
- console.warn('failed to submit form', this, 'reason:', e);
601
+ detail: { submitter, values, request, exception: e },
602
+ }),
603
+ );
604
+ if (e instanceof index_mjs$1.Failure) {
605
+ this.errors = e.problems;
573
606
  }
607
+ console.warn('failed to submit form', this, 'reason:', e);
574
608
  } finally {
575
609
  this.spinner(false);
576
610
  }
@@ -578,7 +612,20 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
578
612
  reset() {
579
613
  this.form.reset();
580
614
  }
615
+ #spinning = 0;
581
616
  spinner(spin) {
617
+ //submits can overlap: only the outermost one saves and restores the button states
618
+ if (spin) {
619
+ ++this.#spinning;
620
+ if (this.#spinning !== 1) {
621
+ return;
622
+ }
623
+ } else {
624
+ this.#spinning = Math.max(0, this.#spinning - 1);
625
+ if (this.#spinning !== 0) {
626
+ return;
627
+ }
628
+ }
582
629
  this.querySelectorAll('ful-spinner').forEach((el) => {
583
630
  const hel = /** @type HTMLElement */ (el);
584
631
  hel.hidden = !spin;
@@ -609,7 +656,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
609
656
  }
610
657
 
611
658
  class Input extends index_mjs.ParsedElement {
612
- static observed = ['value', 'readonly:presence', 'required:presence'];
659
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder'];
613
660
  static slots = true;
614
661
  static template = `
615
662
  <div class="form-label">
@@ -646,12 +693,6 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
646
693
  this._input = fragment.querySelector('input,textarea');
647
694
 
648
695
  index_mjs.Attributes.forward('input-', this, this._input);
649
- if (!skipObservedSetup) {
650
- this.disabled = disabled;
651
- this.readonly = observed.readonly;
652
- this.required = observed.required;
653
- this.value = observed.value;
654
- }
655
696
  this._input.addEventListener('keydown', (evt) => {
656
697
  if (evt.key !== 'Enter' || this._type() === 'textarea') {
657
698
  return;
@@ -667,19 +708,26 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
667
708
  form.requestSubmit(submitter);
668
709
  });
669
710
  this._input.addEventListener('input', (evt) => {
670
- const re = this.getAttribute('mask');
671
- if (!re) {
711
+ const mask = this.getAttribute('mask');
712
+ if (!mask) {
672
713
  return;
673
714
  }
715
+ const strip = (v) => v.replace(new RegExp(mask, 'g'), '');
674
716
  const before = evt.target.value;
675
- const after = before.replace(new RegExp(re, 'g'), '');
717
+ const after = strip(before);
676
718
  if (before === after) {
677
719
  return;
678
720
  }
679
721
  const start = evt.target.selectionStart;
680
- const offset = before.length - after.length;
681
722
  evt.target.value = after;
682
- evt.target.setSelectionRange(start - offset, start - offset);
723
+ if (start === null) {
724
+ //email, number and the date types have no selection to restore
725
+ return;
726
+ }
727
+ //the caret keeps its place among the characters that survived, so only the
728
+ //ones stripped before it count
729
+ const caret = strip(before.slice(0, start)).length;
730
+ evt.target.setSelectionRange(caret, caret);
683
731
  });
684
732
  this._input.addEventListener('change', (evt) => {
685
733
  evt.stopPropagation();
@@ -699,6 +747,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
699
747
  this._input.ariaDescribedByElements = [this._fieldError];
700
748
  this._input.ariaLabelledByElements = [label];
701
749
  this.replaceChildren(fragment);
750
+ if (!skipObservedSetup) {
751
+ // biome-ignore lint/complexity/noUselessThisAlias: keeps checkJs from seeing these as class fields
752
+ const el = this;
753
+ el.disabled = disabled;
754
+ el.readonly = observed.readonly;
755
+ el.required = observed.required;
756
+ el.placeholder = observed.placeholder;
757
+ el.value = observed.value;
758
+ }
702
759
  }
703
760
  get value() {
704
761
  const uppercase = this.hasAttribute('uppercase');
@@ -725,6 +782,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
725
782
  }
726
783
  set disabled(d) {
727
784
  index_mjs.Attributes.toggle(this._input, 'disabled', d);
785
+ //also on the host: a form associated element only matches :disabled through its
786
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
787
+ //is needed, disabled is deliberately not observed: the platform delivers it
788
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
789
+ index_mjs.Attributes.toggle(this, 'disabled', d);
728
790
  }
729
791
  get required() {
730
792
  return this._input.getAttribute('aria-required') === 'true';
@@ -735,6 +797,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
735
797
  index_mjs.Attributes.toggle(this, 'required', d);
736
798
  });
737
799
  }
800
+ get placeholder() {
801
+ const v = this._input.getAttribute('placeholder');
802
+ return v === ' ' ? null : v;
803
+ }
804
+ set placeholder(d) {
805
+ //without a placeholder :placeholder-shown never matches, and floating labels
806
+ //rely on it, so a blank one stands in for none
807
+ index_mjs.Attributes.set(this._input, 'placeholder', d ?? ' ');
808
+ this.reflect(() => {
809
+ index_mjs.Attributes.set(this, 'placeholder', d);
810
+ });
811
+ }
738
812
  focus(options) {
739
813
  this._input.focus(options);
740
814
  }
@@ -754,23 +828,23 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
754
828
 
755
829
  class LocalDate extends index_mjs.ParsedElement {
756
830
  render() {
757
- const content = this.innerHTML.trim();
831
+ const content = this.textContent.trim();
758
832
  if (content === '') {
759
- this.innerHTML = this.getAttribute('default') ?? '';
833
+ this.replaceChildren(this.getAttribute('default') ?? '');
760
834
  return;
761
835
  }
762
836
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
763
837
  const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
764
838
  const [y, m, d] = content.split('-').map(Number);
765
- this.innerHTML = formatter.format(new Date(y, m - 1, d));
839
+ this.replaceChildren(formatter.format(new Date(y, m - 1, d)));
766
840
  }
767
841
  }
768
842
 
769
843
  class Instant extends index_mjs.ParsedElement {
770
844
  render() {
771
- const content = this.innerHTML.trim();
845
+ const content = this.textContent.trim();
772
846
  if (content === '') {
773
- this.innerHTML = this.getAttribute('default') ?? '';
847
+ this.replaceChildren(this.getAttribute('default') ?? '');
774
848
  return;
775
849
  }
776
850
  const locale = this.getAttribute('locale') ?? Intl.DateTimeFormat().resolvedOptions().locale;
@@ -783,7 +857,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
783
857
  second: 'numeric',
784
858
  hour12: false,
785
859
  });
786
- this.innerHTML = format.format(new Date(Instant.isoToLocal(content)));
860
+ this.replaceChildren(format.format(new Date(Instant.isoToLocal(content))));
787
861
  }
788
862
  static isoToLocal(iso) {
789
863
  //this is so sad
@@ -796,16 +870,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
796
870
  }
797
871
 
798
872
  class InputLocalDate extends Input {
799
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
873
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
800
874
  _type() {
801
875
  return 'date';
802
876
  }
803
877
  render(conf) {
804
878
  const { observed } = conf;
805
879
  super.render(conf);
880
+ //step first: on a time input min and max are snapped to its grid
881
+ this.step = observed.step;
806
882
  this.min = observed.min;
807
883
  this.max = observed.max;
808
- this.step = observed.step;
809
884
  }
810
885
  get min() {
811
886
  const v = this._input.min;
@@ -851,13 +926,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
851
926
  case 'd':
852
927
  r.setDate(r.getDate() + offset * sign);
853
928
  break;
854
- case 'm':
929
+ case 'm': {
855
930
  const originalDay = r.getDate();
856
931
  r.setMonth(r.getMonth() + offset * sign);
857
932
  if (r.getDate() !== originalDay) {
858
933
  r.setDate(0);
859
934
  }
860
935
  break;
936
+ }
861
937
  case 'y':
862
938
  r.setFullYear(r.getFullYear() + offset * sign);
863
939
  break;
@@ -870,10 +946,62 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
870
946
  _type() {
871
947
  return 'time';
872
948
  }
949
+ get min() {
950
+ const v = this._input.min;
951
+ return v === '' ? null : v;
952
+ }
953
+ set min(v) {
954
+ this._input.min = this.#fromNowOrOffset(v);
955
+ }
956
+ get max() {
957
+ const v = this._input.max;
958
+ return v === '' ? null : v;
959
+ }
960
+ set max(v) {
961
+ this._input.max = this.#fromNowOrOffset(v);
962
+ }
963
+ /**
964
+ * Resolves `now` and hour or minute offsets against the current time, wrapping
965
+ * around midnight. `m` is minutes here, unlike the date offsets of the parent where
966
+ * it is months: months mean nothing on a time. Anything else is passed through.
967
+ */
968
+ #fromNowOrOffset(v) {
969
+ if (!v) {
970
+ return '';
971
+ }
972
+ const resolved = new Date();
973
+ if (v !== 'now') {
974
+ const re = /^([+-])(\d+)([hm])$/;
975
+ const match = re.exec(v);
976
+ if (!match) {
977
+ return v;
978
+ }
979
+ const sign = match[1] === '-' ? -1 : 1;
980
+ const offset = +match[2] * sign;
981
+ if (match[3] === 'h') {
982
+ resolved.setHours(resolved.getHours() + offset);
983
+ } else {
984
+ resolved.setMinutes(resolved.getMinutes() + offset);
985
+ }
986
+ }
987
+ return InputLocalTime.#snapped(resolved, Number(this._input.step) || 60);
988
+ }
989
+ /**
990
+ * Truncates a time to the step grid: min anchors that grid, so a bound that is not
991
+ * on it makes every value on it invalid.
992
+ */
993
+ static #snapped(date, stepSeconds) {
994
+ const pad = (n) => String(n).padStart(2, '0');
995
+ const seconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
996
+ const snapped = Math.floor(seconds / stepSeconds) * stepSeconds;
997
+ const hh = pad(Math.floor(snapped / 3600));
998
+ const mm = pad(Math.floor((snapped % 3600) / 60));
999
+ return stepSeconds % 60 === 0 ? `${hh}:${mm}` : `${hh}:${mm}:${pad(snapped % 60)}`;
1000
+ }
873
1001
  }
874
1002
 
875
1003
  class InputInstant extends Input {
876
- static observed = ['value', 'readonly:presence', 'required:presence', 'min', 'max', 'step'];
1004
+ static observed = ['value', 'readonly:presence', 'required:presence', 'placeholder', 'min', 'max', 'step'];
877
1005
  _type() {
878
1006
  return 'datetime-local';
879
1007
  }
@@ -918,28 +1046,28 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
918
1046
  static l10n = {
919
1047
  en: {
920
1048
  dropzonelabel: 'Click or drop your files here',
921
- unaccepptablefiletype: 'Only files of type {0} are supported',
1049
+ unacceptablefiletype: 'Only files of type {0} are supported',
922
1050
  maxfilesizeexceeded: 'Maximum supported file size is {0}',
923
1051
  maxtotalsizeexceeded: 'Maximum supported total file size is {0}',
924
1052
  maxfilesexceeded: 'Maximum files count exceeded',
925
1053
  },
926
1054
  it: {
927
1055
  dropzonelabel: 'Clicca o trascina i file qui',
928
- unaccepptablefiletype: 'Solo i file di tipo {0} sono supportati',
1056
+ unacceptablefiletype: 'Solo i file di tipo {0} sono supportati',
929
1057
  maxfilesizeexceeded: 'La dimensione massima di un file è di {0}',
930
1058
  maxtotalsizeexceeded: 'La dimensione massima complessiva dei file è di {0}',
931
1059
  maxfilesexceeded: 'Numero massimo di file superato',
932
1060
  },
933
1061
  es: {
934
1062
  dropzonelabel: 'Haz clic o arrastra tus archivos aquí',
935
- unaccepptablefiletype: 'Solo se admiten archivos de tipo {0}',
1063
+ unacceptablefiletype: 'Solo se admiten archivos de tipo {0}',
936
1064
  maxfilesizeexceeded: 'El tamaño máximo de archivo admitido es {0}',
937
1065
  maxtotalsizeexceeded: 'El tamaño total máximo admitido es {0}',
938
1066
  maxfilesexceeded: 'Se ha superado el número máximo de archivos',
939
1067
  },
940
1068
  fr: {
941
1069
  dropzonelabel: 'Cliquez ou déposez vos fichiers ici',
942
- unaccepptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
1070
+ unacceptablefiletype: 'Seuls les fichiers de type {0} sont pris en charge',
943
1071
  maxfilesizeexceeded: 'La taille maximale de fichier prise en charge est {0}',
944
1072
  maxtotalsizeexceeded: 'La taille totale maximale prise en charge est {0}',
945
1073
  maxfilesexceeded: 'Nombre maximal de fichiers dépassé',
@@ -949,6 +1077,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
949
1077
  'value',
950
1078
  'readonly:presence',
951
1079
  'required:presence',
1080
+ 'placeholder',
952
1081
  'accept:csv',
953
1082
  'multiple:presence',
954
1083
  'itemlist:presence',
@@ -998,7 +1127,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
998
1127
  };
999
1128
  render(conf) {
1000
1129
  const { observed } = conf;
1001
- super.render(conf);
1130
+ super.render({ ...conf, skipObservedSetup: true });
1002
1131
  this.#items = this.querySelector('ful-item-list');
1003
1132
  this.#dropzone = this.querySelector('[data-ref=dropzone]');
1004
1133
  this.#warnings = this.querySelector('ful-field-warnings');
@@ -1009,6 +1138,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1009
1138
  this.maxfiles = observed.maxfiles;
1010
1139
  this.maxfilesize = observed.maxfilesize;
1011
1140
  this.maxtotalsize = observed.maxtotalsize;
1141
+
1142
+ this.disabled = conf.disabled;
1143
+ this.readonly = observed.readonly;
1144
+ this.required = observed.required;
1145
+ this.placeholder = observed.placeholder;
1146
+ this.value = observed.value;
1012
1147
  this.#warnings.addEventListener('animationend', (e) => {
1013
1148
  e.target.remove();
1014
1149
  });
@@ -1016,11 +1151,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1016
1151
  if (!e.target.closest('button')) {
1017
1152
  return;
1018
1153
  }
1019
- const fileName = e.target.closest('ful-item').dataset.name;
1154
+ const idx = [...this.#items.children].indexOf(e.target.closest('ful-item'));
1155
+ if (idx === -1) {
1156
+ return;
1157
+ }
1020
1158
  const dt = new DataTransfer();
1021
- [...this.files].filter((f) => f.name !== fileName).forEach((f) => dt.items.add(f));
1159
+ [...this.files]
1160
+ .filter((f, i) => i !== idx)
1161
+ .forEach((f) => {
1162
+ dt.items.add(f);
1163
+ });
1022
1164
  this.files = dt.files;
1023
- this.#update();
1024
1165
  });
1025
1166
  this.#dropzone.addEventListener('click', (e) => {
1026
1167
  this.querySelector('input')?.click();
@@ -1031,10 +1172,15 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1031
1172
  });
1032
1173
  this.#dropzone.addEventListener('drop', (e) => {
1033
1174
  e.preventDefault();
1175
+ const dropped = [...e.dataTransfer.items].filter((i) => i.kind === 'file');
1176
+ if (dropped.length === 0) {
1177
+ return;
1178
+ }
1034
1179
  const dt = new DataTransfer();
1035
- [...e.dataTransfer.items].filter((i) => i.kind === 'file').forEach((i) => dt.items.add(i.getAsFile()));
1180
+ dropped.forEach((i) => {
1181
+ dt.items.add(i.getAsFile());
1182
+ });
1036
1183
  this.files = dt.files;
1037
- this.#update();
1038
1184
  });
1039
1185
  this._input.addEventListener('change', (e) => {
1040
1186
  this.#update();
@@ -1049,6 +1195,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1049
1195
  }
1050
1196
  #update() {
1051
1197
  this.setCustomValidity();
1198
+ this.#warnings.replaceChildren();
1052
1199
  this.#ensureAcceptable();
1053
1200
  this.#ensureFileSizes();
1054
1201
  this.#ensureTotalSize();
@@ -1059,7 +1206,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1059
1206
  .renderTo(this.#items);
1060
1207
  }
1061
1208
  warning(key, args) {
1062
- this.template('warning').withOverlay({ key, args }).renderTo(this.#warnings);
1209
+ this.template('warning').withOverlay({ key, args }).appendTo(this.#warnings);
1063
1210
  }
1064
1211
  #ensureAcceptable() {
1065
1212
  if (!this.#accept.length) {
@@ -1072,10 +1219,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1072
1219
  if (unacceptable.length === 0) {
1073
1220
  return;
1074
1221
  }
1075
- this.warning('unaccepptablefiletype', this.#accept.join(', '));
1222
+ this.warning('unacceptablefiletype', this.#accept.join(', '));
1076
1223
  const dt = new DataTransfer();
1077
- [...this.files].filter((f) => !unacceptable.includes(f)).forEach((f) => dt.items.add(f));
1078
- this.files = dt.files;
1224
+ [...this.files]
1225
+ .filter((f) => !unacceptable.includes(f))
1226
+ .forEach((f) => {
1227
+ dt.items.add(f);
1228
+ });
1229
+ this._input.files = dt.files;
1079
1230
  }
1080
1231
  #ensureFilesCount() {
1081
1232
  if (this.#maxfiles === null) {
@@ -1085,8 +1236,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1085
1236
  return;
1086
1237
  }
1087
1238
  this.warning('maxfilesexceeded');
1088
- const dt = new DataTransfer();
1089
- this.files = dt.files;
1239
+ this._input.files = new DataTransfer().files;
1090
1240
  }
1091
1241
 
1092
1242
  #ensureFileSizes() {
@@ -1099,8 +1249,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1099
1249
  }
1100
1250
  this.warning('maxfilesizeexceeded', this.#formatByteSize(this.#maxfilesize));
1101
1251
  const dt = new DataTransfer();
1102
- [...this.files].filter((f) => !oversized.includes(f)).forEach((f) => dt.items.add(f));
1103
- this.files = dt.files;
1252
+ [...this.files]
1253
+ .filter((f) => !oversized.includes(f))
1254
+ .forEach((f) => {
1255
+ dt.items.add(f);
1256
+ });
1257
+ this._input.files = dt.files;
1104
1258
  }
1105
1259
  #ensureTotalSize() {
1106
1260
  if (this.#maxtotalsize === null) {
@@ -1111,7 +1265,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1111
1265
  return;
1112
1266
  }
1113
1267
  this.warning('maxtotalsizeexceeded', this.#formatByteSize(this.#maxtotalsize));
1114
- this.files = new DataTransfer().files;
1268
+ this._input.files = new DataTransfer().files;
1115
1269
  }
1116
1270
  get accept() {
1117
1271
  return this.#accept;
@@ -1137,6 +1291,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1137
1291
  }
1138
1292
  set files(vs) {
1139
1293
  this._input.files = vs;
1294
+ this.#update();
1140
1295
  }
1141
1296
  get file() {
1142
1297
  return this.files[0] ?? null;
@@ -1157,7 +1312,6 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1157
1312
  return;
1158
1313
  }
1159
1314
  this.files = new DataTransfer().files;
1160
- this.#update();
1161
1315
  }
1162
1316
  get totalsize() {
1163
1317
  return Array.from(this.files).reduce((a, f) => a + f.size, 0);
@@ -1324,7 +1478,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1324
1478
  const http = index_mjs.registry.component('http-client');
1325
1479
  const responseMapper = SelectLoader.#responseMapperFrom(el);
1326
1480
 
1327
- if ('chunked' == el.getAttribute('mode')) {
1481
+ if ('chunked' === el.getAttribute('mode')) {
1328
1482
  return new PartialRemoteLoader({
1329
1483
  http,
1330
1484
  url: el.getAttribute('src'),
@@ -1400,8 +1554,14 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1400
1554
  });
1401
1555
  this.replaceChildren(fragment);
1402
1556
  }
1557
+ #selected() {
1558
+ return this.#menu?.querySelector('[selected]') ?? this.#menu?.firstElementChild ?? null;
1559
+ }
1403
1560
  acceptSelection() {
1404
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
1561
+ const selected = this.#selected();
1562
+ if (!selected) {
1563
+ return;
1564
+ }
1405
1565
  this.#change(selected);
1406
1566
  }
1407
1567
  update(values) {
@@ -1437,6 +1597,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1437
1597
  try {
1438
1598
  const data = await loader();
1439
1599
  this.update(data);
1600
+ } catch (e) {
1601
+ this.hide();
1602
+ throw e;
1440
1603
  } finally {
1441
1604
  this.#spinner.setAttribute('hidden', '');
1442
1605
  this.#menu.removeAttribute('hidden');
@@ -1444,9 +1607,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1444
1607
  }
1445
1608
  async moveOrShow(forward, loader) {
1446
1609
  if (this.shown) {
1447
- const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
1448
- const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
1449
- if (candidate) {
1610
+ const selected = this.#selected();
1611
+ const candidate = selected?.[`${forward ? 'next' : 'previous'}ElementSibling`];
1612
+ if (selected && candidate) {
1450
1613
  selected.removeAttribute('selected');
1451
1614
  candidate.setAttribute('selected', '');
1452
1615
  candidate.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
@@ -1499,6 +1662,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1499
1662
  #multiple;
1500
1663
  #fieldError;
1501
1664
  #values = new Map();
1665
+ #token = 0;
1502
1666
  constructor() {
1503
1667
  super();
1504
1668
  this.internals = this.attachInternals();
@@ -1511,7 +1675,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1511
1675
  .create(this, { options: slots.options });
1512
1676
 
1513
1677
  this.#multiple = this.hasAttribute('multiple');
1514
- await this.#loader.prefetch?.();
1678
+ try {
1679
+ await this.#loader.prefetch?.();
1680
+ } catch (/** @type any */ e) {
1681
+ console.warn('failed to prefetch select options', this, 'reason:', e);
1682
+ }
1515
1683
  const fragment = this.template().withOverlay({ slots, name }).render();
1516
1684
  this.#input = fragment.querySelector('input');
1517
1685
  this.#items = fragment.querySelector('ful-item-list');
@@ -1530,11 +1698,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1530
1698
  this.#fieldError = fragment.querySelector('ful-field-error');
1531
1699
  this.#input.ariaDescribedByElements = [this.#fieldError];
1532
1700
  this.#input.ariaLabelledByElements = [label];
1533
-
1534
- const self = this;
1535
1701
  const [dload, abortdload] = Timing.throttle(400, () => {
1536
- self.#input.setAttribute('aria-expanded', 'true');
1537
- self.#ddmenu.show(() => self.#loader.load(self.#input.value));
1702
+ this.#input.setAttribute('aria-expanded', 'true');
1703
+ this.#ddmenu.show(() => this.#loader.load(this.#input.value));
1538
1704
  });
1539
1705
  this.addEventListener('click', (/** @type any */ e) => {
1540
1706
  if (e.target.matches('input')) {
@@ -1601,13 +1767,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1601
1767
  case 'ArrowUp': {
1602
1768
  e.preventDefault();
1603
1769
  this.#input.setAttribute('aria-expanded', 'true');
1604
- this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
1770
+ this.#ddmenu.moveOrShow(false, () => this.#loader.load(this.#input.value));
1605
1771
  break;
1606
1772
  }
1607
1773
  case 'ArrowDown': {
1608
1774
  e.preventDefault();
1609
1775
  this.#input.setAttribute('aria-expanded', 'true');
1610
- this.#ddmenu.moveOrShow(true, () => self.#loader.load(self.#input.value));
1776
+ this.#ddmenu.moveOrShow(true, () => this.#loader.load(this.#input.value));
1611
1777
  break;
1612
1778
  }
1613
1779
  case 'Escape': {
@@ -1616,6 +1782,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1616
1782
  break;
1617
1783
  }
1618
1784
  case 'Enter': {
1785
+ if (!this.#ddmenu.shown) {
1786
+ //nothing to accept: submit the form as ful-input does. the inner
1787
+ //input carries form="" so it never submits one on its own
1788
+ this.#requestSubmit();
1789
+ return;
1790
+ }
1619
1791
  e.preventDefault();
1620
1792
  this.#input.setAttribute('aria-expanded', 'false');
1621
1793
  this.#ddmenu.acceptSelection();
@@ -1664,6 +1836,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1664
1836
  async withLoader(fn) {
1665
1837
  return await fn(this.#loader);
1666
1838
  }
1839
+ #requestSubmit() {
1840
+ const form = this.internals.form;
1841
+ if (!form) {
1842
+ return;
1843
+ }
1844
+ const candidates = /** @type [HTMLButtonElement|HTMLInputElement] */ (
1845
+ Array.from(form.querySelectorAll('button:not(:disabled), input:not(:disabled)'))
1846
+ );
1847
+ form.requestSubmit(candidates.find((el) => el.type === 'submit'));
1848
+ }
1667
1849
  #changed() {
1668
1850
  const selection = [...this.#values.entries()].map((e) => ({
1669
1851
  key: e[0],
@@ -1693,16 +1875,43 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1693
1875
  this.template('items').withOverlay({ entries: this.#values.entries() }).renderTo(this.#items);
1694
1876
  }
1695
1877
  set value(vs) {
1696
- if (vs === null) {
1697
- this.#values = new Map();
1698
- this.#syncBadges();
1878
+ //the csvm mapper yields [] for a missing multiple value, an empty string is
1879
+ //left alone: it is a usable key for an <option value="">
1880
+ const keys = vs == null ? [] : Array.isArray(vs) ? vs : [vs];
1881
+ //the keys are known synchronously and are all `value` reads, so they are applied
1882
+ //now: only the labels need the loader, until then a key stands in for its own
1883
+ this.#values = new Map(keys.map((k) => [k, [k]]));
1884
+ this.#syncBadges();
1885
+ const token = ++this.#token;
1886
+ if (keys.length === 0) {
1699
1887
  return;
1700
1888
  }
1701
- (async () => {
1702
- const entries = await (this.#multiple ? this.#loader.exact(...vs) : this.#loader.exact(vs));
1703
- this.#values = new Map(entries.map((e) => [e[0], e.slice(1)]));
1704
- this.#syncBadges();
1705
- })();
1889
+ this.#resolve(keys, token);
1890
+ }
1891
+ /**
1892
+ * Resolves the labels of the assigned keys. A failed lookup is left to reject so
1893
+ * that it is reported like any other failure: the keys stay applied either way.
1894
+ */
1895
+ async #resolve(keys, token) {
1896
+ const entries = await this.#loader.exact(...keys);
1897
+ if (token !== this.#token) {
1898
+ //a newer assignment has been made in the meantime
1899
+ return;
1900
+ }
1901
+ //label the keys that are still selected: a removal made while the lookup was in
1902
+ //flight must not be undone by it, and a key the loader does not know is dropped
1903
+ const resolved = new Map(entries.map((e) => [e[0], e.slice(1)]));
1904
+ for (const key of keys) {
1905
+ if (!this.#values.has(key)) {
1906
+ continue;
1907
+ }
1908
+ if (resolved.has(key)) {
1909
+ this.#values.set(key, resolved.get(key));
1910
+ } else {
1911
+ this.#values.delete(key);
1912
+ }
1913
+ }
1914
+ this.#syncBadges();
1706
1915
  }
1707
1916
  get value() {
1708
1917
  if (this.#multiple) {
@@ -1721,6 +1930,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1721
1930
  }
1722
1931
  set disabled(d) {
1723
1932
  index_mjs.Attributes.toggle(this.#input, 'disabled', d);
1933
+ //also on the host: a form associated element only matches :disabled through its
1934
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
1935
+ //is needed, disabled is deliberately not observed: the platform delivers it
1936
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
1937
+ index_mjs.Attributes.toggle(this, 'disabled', d);
1724
1938
  }
1725
1939
  get readonly() {
1726
1940
  return this.#input.readOnly;
@@ -1826,7 +2040,9 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1826
2040
  return [input, label];
1827
2041
  });
1828
2042
 
1829
- radioEls.forEach((el) => el.remove());
2043
+ radioEls.forEach((el) => {
2044
+ el.remove();
2045
+ });
1830
2046
  this.template().withOverlay({ name, slots, inputsAndLabels }).renderTo(this);
1831
2047
  this.#fieldset = this.firstElementChild;
1832
2048
  this.disabled = disabled;
@@ -1870,6 +2086,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1870
2086
  }
1871
2087
  set disabled(d) {
1872
2088
  index_mjs.Attributes.toggle(this.#fieldset, 'disabled', d);
2089
+ //also on the host: a form associated element only matches :disabled through its
2090
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
2091
+ //is needed, disabled is deliberately not observed: the platform delivers it
2092
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
2093
+ index_mjs.Attributes.toggle(this, 'disabled', d);
1873
2094
  }
1874
2095
  get required() {
1875
2096
  return this.#fieldset.getAttribute('aria-required') === 'true';
@@ -1919,7 +2140,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1919
2140
  this.internals.role = 'presentation';
1920
2141
  }
1921
2142
  render({ slots, observed, disabled }) {
1922
- const isSwitch = this.getAttribute('type') == 'switch';
2143
+ const isSwitch = this.getAttribute('type') === 'switch';
1923
2144
  const klass = isSwitch ? 'form-check form-switch' : 'form-check';
1924
2145
  const fragment = this.template().withOverlay({ slots, klass, isSwitch }).render();
1925
2146
  this.#container = fragment.firstElementChild;
@@ -1983,6 +2204,11 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
1983
2204
  }
1984
2205
  set disabled(d) {
1985
2206
  index_mjs.Attributes.toggle(this.#input, 'disabled', d);
2207
+ //also on the host: a form associated element only matches :disabled through its
2208
+ //own attribute, and that is what keeps it out of the submitted values. no reflect
2209
+ //is needed, disabled is deliberately not observed: the platform delivers it
2210
+ //through formDisabledCallback, which also covers a disabled ancestor fieldset
2211
+ index_mjs.Attributes.toggle(this, 'disabled', d);
1986
2212
  }
1987
2213
  get required() {
1988
2214
  return this.#input.getAttribute('aria-required') === 'true';
@@ -2023,9 +2249,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2023
2249
  class SortButton extends index_mjs.ParsedElement {
2024
2250
  static observed = ['order'];
2025
2251
  #order;
2026
- render() {
2252
+ render({ observed }) {
2027
2253
  const sorter = this.getAttribute('sorter');
2028
2254
  const orders = ['asc', 'desc', null];
2255
+ this.order = observed.order;
2029
2256
  this.addEventListener('click', () => {
2030
2257
  const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
2031
2258
  this.dispatchEvent(
@@ -2115,10 +2342,12 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2115
2342
  #total = 0;
2116
2343
  #current = 0;
2117
2344
  render({ observed }) {
2118
- this.update(observed.current ?? 0, observed.total ?? 0);
2345
+ this.total = observed.total ?? 0;
2346
+ this.current = observed.current ?? 0;
2119
2347
  this.addEventListener('click', (/** @type any */ evt) => {
2120
2348
  const el = evt.target.closest('a');
2121
- if (!el) {
2349
+ if (!el || el.classList.contains('disabled')) {
2350
+ //a disabled link leads nowhere: the page it would ask for does not exist
2122
2351
  return;
2123
2352
  }
2124
2353
  this.dispatchEvent(
@@ -2134,25 +2363,20 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2134
2363
  }
2135
2364
  update(current, total) {
2136
2365
  const maxRender = Number(this.getAttribute('pages') ?? '5');
2137
- const prev = { index: Math.max(0, current - 1), enabled: current > 0 };
2366
+ const hasPrev = current > 0;
2367
+ const hasNext = current + 1 < total;
2368
+ //a disabled arrow carries no page: there is nothing valid for it to point at
2369
+ const prev = { index: hasPrev ? current - 1 : null, enabled: hasPrev };
2138
2370
  const curr = { index: current, label: current + 1 };
2139
- const next = { index: Math.min(total, current + 1), enabled: current + 1 < total };
2140
- const pages = [
2141
- {
2142
- index: current,
2143
- label: current + 1,
2144
- },
2145
- ];
2146
- for (let mid = current, offset = 1; offset !== maxRender && pages.length != maxRender; ++offset) {
2147
- const p = mid - offset;
2148
- if (p >= 0) {
2149
- pages.unshift({ index: p, label: p + 1 });
2150
- }
2151
- const n = mid + offset;
2152
- if (n < total) {
2153
- pages.push({ index: n, label: n + 1 });
2154
- }
2155
- }
2371
+ const next = { index: hasNext ? current + 1 : null, enabled: hasNext };
2372
+ //the window holds at most maxRender pages, centered on the current one and slid
2373
+ //back towards the end so it stays full on the last pages
2374
+ const rendered = Math.max(1, Math.min(maxRender, total));
2375
+ const first = Math.max(0, Math.min(current - Math.floor((rendered - 1) / 2), total - rendered));
2376
+ const pages = Array.from({ length: rendered }, (_, offset) => ({
2377
+ index: first + offset,
2378
+ label: first + offset + 1,
2379
+ }));
2156
2380
  this.template().withOverlay({ total, prev, curr, next, pages }).renderTo(this);
2157
2381
  }
2158
2382
  get total() {
@@ -2179,9 +2403,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2179
2403
 
2180
2404
  class TableSchemaParser {
2181
2405
  static parse(nodeOrFragment, template) {
2182
- const schema = index_mjs.Nodes.queryChildren(nodeOrFragment, 'schema');
2406
+ //nodeOrFragment is undefined when the slot is missing altogether
2407
+ const schema = nodeOrFragment ? index_mjs.Nodes.queryChildren(nodeOrFragment, 'schema') : null;
2183
2408
  if (!schema) {
2184
- throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
2409
+ throw new Error('missing expected <schema>: ful-table needs a <template slot="schema"> holding one');
2185
2410
  }
2186
2411
  const headersTr = document.createElement('tr');
2187
2412
  const rowsTr = document.createElement('tr');
@@ -2400,9 +2625,10 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2400
2625
  this.#noAutoload = table.querySelector(':scope > tbody[data-ref=initial]');
2401
2626
  this.#feedback = table.querySelector(':scope > tbody[data-ref=feedback]');
2402
2627
  this.#paginator = index_mjs.Nodes.queryChildren(fragment, 'ful-pagination');
2403
- this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
2404
2628
  this.replaceChildren(fragment);
2405
- schema.headersTemplate.renderTo(this.querySelector('thead'));
2629
+ const thead = /** @type HTMLTableSectionElement */ (this.querySelector('thead'));
2630
+ schema.headersTemplate.renderTo(thead);
2631
+ this.#sorters = thead.querySelectorAll('ful-sorter');
2406
2632
  await index_mjs.Rendering.waitForChildren(this);
2407
2633
 
2408
2634
  const maybeForm = /** @type any */ (index_mjs.Nodes.queryChildren(this, 'ful-form'));
@@ -2437,11 +2663,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2437
2663
  this.addEventListener('sort-requested', async (/** @type any */ e) => {
2438
2664
  const sortRequest = e.detail.value.order ? e.detail.value : null;
2439
2665
  await this.load(this.#latestRequest.pageRequest, sortRequest, this.#latestRequest.filterRequest);
2440
- this.#sorters.forEach((s) => (s.order = null));
2666
+ this.#sorters.forEach((s) => {
2667
+ s.order = null;
2668
+ });
2441
2669
  e.target.order = e.detail.value.order;
2442
2670
  });
2443
2671
  if (this.hasAttribute('autoload')) {
2444
- await this.reload();
2672
+ //not awaited: the first load must not hold up the upgrade, and a loader that
2673
+ //fails or never answers must not keep ftl:ready from firing for the page.
2674
+ //load renders its own error state and lets the failure reject, so it is reported
2675
+ this.reload();
2445
2676
  }
2446
2677
  }
2447
2678
 
@@ -2454,7 +2685,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2454
2685
  }
2455
2686
  async load(pageRequest, sortRequest, filterRequest) {
2456
2687
  this.#body.replaceChildren();
2457
- this.#loading.removeAttribute('hidden', '');
2688
+ this.#loading.removeAttribute('hidden');
2458
2689
  this.#feedback.setAttribute('hidden', '');
2459
2690
  this.#noAutoload.setAttribute('hidden', '');
2460
2691
  try {
@@ -2463,7 +2694,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2463
2694
  this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
2464
2695
  } catch (/** @type any */ error) {
2465
2696
  this.#loading.setAttribute('hidden', '');
2466
- this.#feedback.removeAttribute('hidden', '');
2697
+ this.#feedback.removeAttribute('hidden');
2467
2698
  if (!error.problems) {
2468
2699
  this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
2469
2700
  } else {
@@ -2505,7 +2736,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2505
2736
  }
2506
2737
 
2507
2738
  class InstantFilter extends Input {
2508
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
2739
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
2509
2740
  static template = `
2510
2741
  <div class="form-label">
2511
2742
  <label>{{{{ slots.default }}}}</label>
@@ -2539,10 +2770,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2539
2770
  this.#operator = this.querySelector('[data-ref=operator]');
2540
2771
  this.#value1 = this.querySelector('[data-ref=value1]');
2541
2772
  this.#value2 = this.querySelector('[data-ref=value2]');
2773
+ //Input.render only re-dispatches changes coming from the first operand
2774
+ this.#value2.addEventListener('change', (evt) => {
2775
+ evt.stopPropagation();
2776
+ this.#notifyChange();
2777
+ });
2542
2778
 
2543
2779
  this.disabled = conf.disabled;
2544
2780
  this.readonly = conf.observed.readonly;
2545
2781
  this.required = conf.observed.required;
2782
+ this.placeholder = conf.observed.placeholder;
2546
2783
  this.value = conf.observed.value;
2547
2784
 
2548
2785
  this.addEventListener('click', (evt) => {
@@ -2552,9 +2789,13 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2552
2789
  }
2553
2790
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
2554
2791
  const value = /** @type String */ (target.getAttribute('value'));
2792
+ const previous = btn.getAttribute('value');
2555
2793
  index_mjs.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
2556
2794
  btn.setAttribute('value', value);
2557
2795
  btn.innerHTML = target.innerHTML;
2796
+ if (previous !== value) {
2797
+ this.#notifyChange();
2798
+ }
2558
2799
  });
2559
2800
  }
2560
2801
 
@@ -2570,14 +2811,40 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2570
2811
  return;
2571
2812
  }
2572
2813
  const [operator, ...values] = v;
2573
- this.#operator.setAttribute('value', operator);
2814
+ this.#showOperator(operator);
2574
2815
  this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
2575
2816
  this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
2576
2817
  }
2818
+ #showOperator(operator) {
2819
+ this.#operator.setAttribute('value', operator);
2820
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
2821
+ const item = items.find((a) => a.getAttribute('value') === operator);
2822
+ if (item) {
2823
+ this.#operator.innerHTML = item.innerHTML;
2824
+ }
2825
+ index_mjs.Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
2826
+ }
2827
+ #notifyChange() {
2828
+ this.dispatchEvent(
2829
+ new CustomEvent('change', {
2830
+ bubbles: true,
2831
+ cancelable: false,
2832
+ detail: {
2833
+ value: this.value,
2834
+ },
2835
+ }),
2836
+ );
2837
+ }
2838
+ get readonly() {
2839
+ return super.readonly;
2840
+ }
2577
2841
  set readonly(v) {
2578
2842
  this.#value2.readOnly = v;
2579
2843
  super.readonly = v;
2580
2844
  }
2845
+ get disabled() {
2846
+ return super.disabled;
2847
+ }
2581
2848
  set disabled(d) {
2582
2849
  index_mjs.Attributes.toggle(this.#value2, 'disabled', d);
2583
2850
  super.disabled = d;
@@ -2585,7 +2852,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2585
2852
  }
2586
2853
 
2587
2854
  class LocalDateFilter extends Input {
2588
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
2855
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
2589
2856
  static template = `
2590
2857
  <div class="form-label">
2591
2858
  <label>{{{{ slots.default }}}}</label>
@@ -2620,10 +2887,16 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2620
2887
  this.#operator = this.querySelector('[data-ref=operator]');
2621
2888
  this.#value1 = this.querySelector('[data-ref=value1]');
2622
2889
  this.#value2 = this.querySelector('[data-ref=value2]');
2890
+ //Input.render only re-dispatches changes coming from the first operand
2891
+ this.#value2.addEventListener('change', (evt) => {
2892
+ evt.stopPropagation();
2893
+ this.#notifyChange();
2894
+ });
2623
2895
 
2624
2896
  this.disabled = conf.disabled;
2625
2897
  this.readonly = conf.observed.readonly;
2626
2898
  this.required = conf.observed.required;
2899
+ this.placeholder = conf.observed.placeholder;
2627
2900
  this.value = conf.observed.value;
2628
2901
 
2629
2902
  this.addEventListener('click', (evt) => {
@@ -2633,14 +2906,18 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2633
2906
  }
2634
2907
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
2635
2908
  const value = /** @type String */ (target.getAttribute('value'));
2909
+ const previous = btn.getAttribute('value');
2636
2910
  index_mjs.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
2637
2911
  btn.setAttribute('value', value);
2638
2912
  btn.innerHTML = target.innerHTML;
2913
+ if (previous !== value) {
2914
+ this.#notifyChange();
2915
+ }
2639
2916
  });
2640
2917
  }
2641
2918
  get value() {
2642
2919
  const operator = this.#operator.getAttribute('value');
2643
- const values = operator == 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
2920
+ const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
2644
2921
  return values.some((v) => v === '') ? undefined : [operator, ...values];
2645
2922
  }
2646
2923
  set value(v) {
@@ -2650,14 +2927,40 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2650
2927
  return;
2651
2928
  }
2652
2929
  const [operator, ...values] = v;
2653
- this.#operator.setAttribute('value', operator);
2930
+ this.#showOperator(operator);
2654
2931
  this.#value1.value = values[0];
2655
2932
  this.#value2.value = values[1];
2656
2933
  }
2934
+ #showOperator(operator) {
2935
+ this.#operator.setAttribute('value', operator);
2936
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
2937
+ const item = items.find((a) => a.getAttribute('value') === operator);
2938
+ if (item) {
2939
+ this.#operator.innerHTML = item.innerHTML;
2940
+ }
2941
+ index_mjs.Attributes.toggle(this.#value2, 'hidden', operator !== 'BETWEEN');
2942
+ }
2943
+ #notifyChange() {
2944
+ this.dispatchEvent(
2945
+ new CustomEvent('change', {
2946
+ bubbles: true,
2947
+ cancelable: false,
2948
+ detail: {
2949
+ value: this.value,
2950
+ },
2951
+ }),
2952
+ );
2953
+ }
2954
+ get readonly() {
2955
+ return super.readonly;
2956
+ }
2657
2957
  set readonly(v) {
2658
2958
  this.#value2.readOnly = v;
2659
2959
  super.readonly = v;
2660
2960
  }
2961
+ get disabled() {
2962
+ return super.disabled;
2963
+ }
2661
2964
  set disabled(d) {
2662
2965
  index_mjs.Attributes.toggle(this.#value2, 'disabled', d);
2663
2966
  super.disabled = d;
@@ -2665,7 +2968,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2665
2968
  }
2666
2969
 
2667
2970
  class TextFilter extends Input {
2668
- static observed = ['value:json', 'readonly:presence', 'required:presence'];
2971
+ static observed = ['value:json', 'readonly:presence', 'required:presence', 'placeholder'];
2669
2972
  static template = `
2670
2973
  <div class="form-label">
2671
2974
  <label>{{{{ slots.default }}}}</label>
@@ -2689,6 +2992,8 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2689
2992
  `;
2690
2993
  #operator;
2691
2994
  #value;
2995
+ //the sensitivity has no control of its own: it is carried through from whoever set the value
2996
+ #sensitivity = 'IGNORE_CASE';
2692
2997
  render(conf) {
2693
2998
  super.render({ ...conf, skipObservedSetup: true });
2694
2999
 
@@ -2698,6 +3003,7 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2698
3003
  this.disabled = conf.disabled;
2699
3004
  this.readonly = conf.observed.readonly;
2700
3005
  this.required = conf.observed.required;
3006
+ this.placeholder = conf.observed.placeholder;
2701
3007
  this.value = conf.observed.value;
2702
3008
 
2703
3009
  this.addEventListener('click', (evt) => {
@@ -2707,13 +3013,17 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2707
3013
  }
2708
3014
  const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
2709
3015
  const value = /** @type String */ (target.getAttribute('value'));
3016
+ const previous = btn.getAttribute('value');
2710
3017
  btn.setAttribute('value', value);
2711
3018
  btn.innerHTML = target.innerHTML;
3019
+ if (previous !== value) {
3020
+ this.#notifyChange();
3021
+ }
2712
3022
  });
2713
3023
  }
2714
3024
  get value() {
2715
3025
  const operator = this.#operator.getAttribute('value');
2716
- return this.#value.value === '' ? undefined : [operator, 'IGNORE_CASE', this.#value.value];
3026
+ return this.#value.value === '' ? undefined : [operator, this.#sensitivity, this.#value.value];
2717
3027
  }
2718
3028
  set value(v) {
2719
3029
  if (v == null) {
@@ -2721,15 +3031,35 @@ var ful = (function (exports, index_mjs, index_mjs$1) {
2721
3031
  return;
2722
3032
  }
2723
3033
  const [operator, sensitivity, value] = v;
2724
- this.#operator.setAttribute('value', operator);
3034
+ this.#showOperator(operator);
3035
+ this.#sensitivity = sensitivity ?? 'IGNORE_CASE';
2725
3036
  this.#value.value = value;
2726
3037
  }
3038
+ #showOperator(operator) {
3039
+ this.#operator.setAttribute('value', operator);
3040
+ const items = Array.from(this.#operator.nextElementSibling?.querySelectorAll('li > a[value]') ?? []);
3041
+ const item = items.find((a) => a.getAttribute('value') === operator);
3042
+ if (item) {
3043
+ this.#operator.innerHTML = item.innerHTML;
3044
+ }
3045
+ }
3046
+ #notifyChange() {
3047
+ this.dispatchEvent(
3048
+ new CustomEvent('change', {
3049
+ bubbles: true,
3050
+ cancelable: false,
3051
+ detail: {
3052
+ value: this.value,
3053
+ },
3054
+ }),
3055
+ );
3056
+ }
2727
3057
  }
2728
3058
 
2729
3059
  class LocalizationModule {
2730
3060
  static t(k, ...args) {
2731
- //@ts-ignore
2732
- const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.['en']?.[k] ?? k;
3061
+ //@ts-expect-error l10n and language come from the element class and the data stack
3062
+ const format = this.l10n?.[this.language]?.[k] ?? this.l10n?.en?.[k] ?? k;
2733
3063
  if (args.length === 0) {
2734
3064
  return format;
2735
3065
  }