@optionfactory/ful 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ful.iife.js CHANGED
@@ -722,69 +722,82 @@ var ful = (function (exports, ftl) {
722
722
  }
723
723
  }
724
724
 
725
- class Storage {
726
- constructor(prefix, storage) {
727
- this.prefix = prefix;
728
- this.storage = storage;
729
- }
730
- save(k, v) {
731
- this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
725
+ class LocalStorage extends Storage {
726
+ static save(k, v) {
727
+ localStorage.setItem(k, JSON.stringify(v));
732
728
  }
733
- load(k) {
734
- const got = this.storage.getItem(`${this.prefix}-${k}`);
735
- return got === undefined ? undefined : JSON.parse(got);
729
+ static load(k) {
730
+ const got = localStorage.getItem(k);
731
+ return got === null ? undefined : JSON.parse(got);
736
732
  }
737
- remove(k) {
738
- this.storage.removeItem(`${this.prefix}-${k}`);
733
+ static remove(k) {
734
+ localStorage.removeItem(k);
739
735
  }
740
- pop(k) {
741
- const decoded = this.load(k);
742
- this.remove(k);
736
+ static pop(k) {
737
+ const decoded = LocalStorage.load(k);
738
+ LocalStorage.remove(k);
743
739
  return decoded;
744
740
  }
745
- }
746
741
 
747
- class LocalStorage extends Storage {
748
- constructor(prefix) {
749
- super(prefix, localStorage);
750
- }
751
742
  }
752
743
 
744
+
745
+
753
746
  class SessionStorage extends Storage {
754
- constructor(prefix) {
755
- super(prefix, sessionStorage);
747
+ static save(k, v) {
748
+ sessionStorage.setItem(k, JSON.stringify(v));
749
+ }
750
+ static load(k) {
751
+ const got = sessionStorage.getItem(k);
752
+ return got === null ? undefined : JSON.parse(got);
753
+ }
754
+ static remove(k) {
755
+ sessionStorage.removeItem(k);
756
+ }
757
+ static pop(k) {
758
+ const decoded = SessionStorage.load(k);
759
+ SessionStorage.remove(k);
760
+ return decoded;
756
761
  }
757
762
  }
758
763
 
759
- class VersionedStorage {
760
- constructor(storage, key, dataSupplier) {
761
- this.storage = storage;
762
- this.key = key;
763
- this.dataSupplier = dataSupplier;
764
- this.cache = null;
765
-
766
- }
767
- async load(revision) {
768
- const saved = this.storage.load(this.key);
769
- if (!!saved && saved.revision === revision) {
770
- this.cache = saved.value;
771
- return;
764
+ class VersionedLocalStorage {
765
+ static save(key, revision, data){
766
+ LocalStorage.save(key, {revision, data});
767
+ }
768
+ static load(key, revision){
769
+ const stored = LocalStorage.load(key);
770
+ if(stored === undefined){
771
+ return undefined;
772
772
  }
773
- const freshData = await this.dataSupplier(revision, this.key);
774
- this.storage.save(this.key, {
775
- revision: revision,
776
- value: freshData
777
- });
778
- this.cache = freshData;
773
+ if(stored.revision !== revision){
774
+ localStorage.removeItem(key);
775
+ return undefined;
776
+ }
777
+ return stored.data;
779
778
  }
780
- data() {
781
- return this.cache;
779
+ }
780
+
781
+ class VersionedSessionStorage {
782
+ static save(key, revision, data){
783
+ SessionStorage.save(key, {revision, data});
784
+ }
785
+ static load(key, revision){
786
+ const stored = SessionStorage.load(key);
787
+ if(stored === undefined){
788
+ return undefined;
789
+ }
790
+ if(stored.revision !== revision){
791
+ localStorage.removeItem(key);
792
+ return undefined;
793
+ }
794
+ return stored.data;
782
795
  }
783
796
  }
784
797
 
785
798
  class AuthorizationCodeFlow {
786
- static forKeycloak(clientId, realmBaseUrl, redirectUri) {
787
- const scope = "openid profile";
799
+ static forKeycloak(clientId, realmBaseUrl, redirectUri, maybeScope) {
800
+ const scope = maybeScope ?? "openid profile";
788
801
  return new AuthorizationCodeFlow(clientId, scope, {
789
802
  auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
790
803
  token: new URL("protocol/openid-connect/token", realmBaseUrl),
@@ -794,7 +807,6 @@ var ful = (function (exports, ftl) {
794
807
  });
795
808
  }
796
809
  constructor(clientId, scope, { auth, token, registration, logout, redirect }) {
797
- this.storage = new SessionStorage(clientId);
798
810
  this.clientId = clientId;
799
811
  this.scope = scope;
800
812
  this.uri = { auth, token, registration, logout, redirect };
@@ -803,7 +815,7 @@ var ful = (function (exports, ftl) {
803
815
  const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
804
816
  const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
805
817
  const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
806
- this.storage.save(AuthorizationCodeFlow.PKCE_AND_STATE_KEY, {
818
+ SessionStorage.save(`${AuthorizationCodeFlow.PKCE_AND_STATE_KEY}-${this.clientId}`, {
807
819
  state: state,
808
820
  verifier: pkceVerifier
809
821
  });
@@ -831,7 +843,7 @@ var ful = (function (exports, ftl) {
831
843
  }
832
844
  async #tokenExchange(code, state) {
833
845
  window.history.replaceState('', "", this.uri.redirect);
834
- const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
846
+ const stateAndVerifier = SessionStorage.pop(`${AuthorizationCodeFlow.PKCE_AND_STATE_KEY}-${this.clientId}`);
835
847
  if (stateAndVerifier.state !== state) {
836
848
  throw new Error("State mismatch");
837
849
  }
@@ -859,7 +871,7 @@ var ful = (function (exports, ftl) {
859
871
  async ensureLoggedIn() {
860
872
  const url = new URL(window.location.href);
861
873
  const code = url.searchParams.get("code");
862
- if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
874
+ if (code && SessionStorage.load(`${AuthorizationCodeFlow.PKCE_AND_STATE_KEY}-${this.clientId}`)) {
863
875
  //if callback from keycloak and we have our state still stored
864
876
  const state = url.searchParams.get("state");
865
877
  return await this.#tokenExchange(code, state);
@@ -1024,24 +1036,22 @@ var ful = (function (exports, ftl) {
1024
1036
  }
1025
1037
  }
1026
1038
 
1027
- const timing = {
1028
- sleep(ms) {
1039
+ class Timing {
1040
+ static sleep(ms) {
1029
1041
  return new Promise(resolve => setTimeout(resolve, ms));
1030
- },
1031
- DEBOUNCE_DEFAULT: 0,
1032
- DEBOUNCE_IMMEDIATE: 1,
1042
+ }
1043
+ static DEBOUNCE_DEFAULT = 0;
1044
+ static DEBOUNCE_IMMEDIATE = 1;
1033
1045
  /**
1034
1046
  * Executes only after a period of inactivity (pause in events).
1035
- * Delays execution until events stop for a set duration.
1036
- * Consolidates multiple rapid events into a single execution.
1037
1047
  * Respond to the "end" of a series of events.
1038
1048
  * @param {*} timeoutMs
1039
1049
  * @param {*} func
1040
1050
  * @param {*} [options]
1041
1051
  * @returns {[function, function]}
1042
1052
  */
1043
- debounce(timeoutMs, func, options) {
1044
- const opts = options ?? timing.DEBOUNCE_DEFAULT;
1053
+ static debounce(timeoutMs, func, options) {
1054
+ const opts = options ?? Timing.DEBOUNCE_DEFAULT;
1045
1055
  let tid = null;
1046
1056
  let args = [];
1047
1057
  let previousTimestamp = 0;
@@ -1053,7 +1063,7 @@ var ful = (function (exports, ftl) {
1053
1063
  return;
1054
1064
  }
1055
1065
  tid = null;
1056
- if (opts !== timing.DEBOUNCE_IMMEDIATE) {
1066
+ if (opts !== Timing.DEBOUNCE_IMMEDIATE) {
1057
1067
  func(...args);
1058
1068
  }
1059
1069
  // This check is needed because `func` can recursively invoke `debounced`.
@@ -1067,31 +1077,32 @@ var ful = (function (exports, ftl) {
1067
1077
  previousTimestamp = new Date().getTime();
1068
1078
  if (tid === null) {
1069
1079
  tid = setTimeout(later, timeoutMs);
1070
- if (opts === timing.DEBOUNCE_IMMEDIATE) {
1080
+ if (opts === Timing.DEBOUNCE_IMMEDIATE) {
1071
1081
  func(...args);
1072
1082
  }
1073
1083
  }
1074
1084
  };
1075
1085
  const abort = () => clearTimeout(tid);
1076
1086
  return [debounced, abort];
1077
- },
1078
- THROTTLE_DEFAULT: 0,
1079
- THROTTLE_NO_LEADING: 1,
1080
- THROTTLE_NO_TRAILING: 2,
1087
+ }
1088
+ static THROTTLE_DEFAULT = 0;
1089
+ static THROTTLE_NO_LEADING = 1;
1090
+ static THROTTLE_NO_TRAILING = 2;
1081
1091
  /**
1082
1092
  * Executes at most once per specified time interval, regardless of ongoing events.
1083
- * Executes regularly as long as events are firing, but at a controlled rate.
1084
- * Allows execution periodically during a burst of events.
1085
- * Ensure a function doesn't fire too frequently during continuous events.
1093
+ * @param {*} timeoutMs
1094
+ * @param {*} func
1095
+ * @param {*} [options]
1096
+ * @returns {[function, function]}
1086
1097
  */
1087
- throttle(timeoutMs, func, options) {
1088
- const opts = options ?? timing.THROTTLE_DEFAULT;
1098
+ static throttle(timeoutMs, func, options) {
1099
+ const opts = options ?? Timing.THROTTLE_DEFAULT;
1089
1100
  let tid = null;
1090
1101
  let args = [];
1091
1102
  let previousTimestamp = 0;
1092
1103
 
1093
1104
  const later = () => {
1094
- previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
1105
+ previousTimestamp = (opts & Timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
1095
1106
  tid = null;
1096
1107
  func(...args);
1097
1108
  if (tid === null) {
@@ -1100,7 +1111,7 @@ var ful = (function (exports, ftl) {
1100
1111
  };
1101
1112
  const throttled = function () {
1102
1113
  const now = new Date().getTime();
1103
- if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
1114
+ if (!previousTimestamp && (opts & Timing.THROTTLE_NO_LEADING)) {
1104
1115
  previousTimestamp = now;
1105
1116
  }
1106
1117
  const remaining = timeoutMs - (now - previousTimestamp);
@@ -1115,14 +1126,14 @@ var ful = (function (exports, ftl) {
1115
1126
  if (tid === null) {
1116
1127
  args = [];
1117
1128
  }
1118
- } else if (tid === null && !(opts & timing.THROTTLE_NO_TRAILING)) {
1129
+ } else if (tid === null && !(opts & Timing.THROTTLE_NO_TRAILING)) {
1119
1130
  tid = setTimeout(later, remaining);
1120
1131
  }
1121
1132
  };
1122
1133
  const abort = () => clearTimeout(tid);
1123
1134
  return [throttled, abort];
1124
1135
  }
1125
- };
1136
+ }
1126
1137
 
1127
1138
  class Loaders {
1128
1139
  static fromAttributes(el, defaultLoader, options) {
@@ -1437,24 +1448,30 @@ var ful = (function (exports, ftl) {
1437
1448
  <ful-field-error></ful-field-error>
1438
1449
  `;
1439
1450
  static formAssociated = true;
1440
- #input;
1441
- #fieldError;
1451
+ _input;
1452
+ _fieldError;
1442
1453
  constructor() {
1443
1454
  super();
1444
1455
  this.internals = this.attachInternals();
1445
1456
  this.internals.role = 'presentation';
1446
1457
  }
1458
+ _type() {
1459
+ return this.getAttribute("type") ?? 'text';
1460
+ }
1461
+ _fragment(type, slots) {
1462
+ return this.template().withOverlay({ type, slots }).render();
1463
+ }
1447
1464
  render({ slots, observed, disabled }) {
1448
- const type = this.getAttribute("type") ?? 'text';
1449
- const fragment = this.template().withOverlay({ type, slots }).render();
1450
- this.#input = fragment.querySelector("input,textarea");
1465
+ const type = this._type();
1466
+ const fragment = this._fragment(type, slots);
1467
+ this._input = fragment.querySelector("input,textarea");
1451
1468
 
1452
- ftl.Attributes.forward('input-', this, this.#input);
1469
+ ftl.Attributes.forward('input-', this, this._input);
1453
1470
  this.disabled = disabled;
1454
1471
  this.readonly = observed.readonly;
1455
1472
  this.value = observed.value;
1456
1473
 
1457
- this.#input.addEventListener('change', (evt) => {
1474
+ this._input.addEventListener('change', (evt) => {
1458
1475
  evt.stopPropagation();
1459
1476
  this.dispatchEvent(new CustomEvent('change', {
1460
1477
  bubbles: true,
@@ -1466,59 +1483,187 @@ var ful = (function (exports, ftl) {
1466
1483
  });
1467
1484
  const label = fragment.querySelector('label');
1468
1485
  label.addEventListener('click', () => this.focus());
1469
- this.#fieldError = fragment.querySelector('ful-field-error');
1470
- this.#input.ariaDescribedByElements = [this.#fieldError];
1471
- this.#input.ariaLabelledByElements = [label];
1486
+ this._fieldError = fragment.querySelector('ful-field-error');
1487
+ this._input.ariaDescribedByElements = [this._fieldError];
1488
+ this._input.ariaLabelledByElements = [label];
1472
1489
  this.replaceChildren(fragment);
1473
1490
  }
1474
1491
  get value() {
1475
- return this.#input.value === '' ? null : this.#input.value;
1492
+ return this._input.value === '' ? null : this._input.value;
1476
1493
  }
1477
1494
  set value(value) {
1478
- this.#input.value = value === '' ? null : value;
1495
+ this._input.value = value === '' ? null : value;
1479
1496
  }
1480
- get readonly(){
1481
- return this.#input.readOnly;
1497
+ get readonly() {
1498
+ return this._input.readOnly;
1482
1499
  }
1483
1500
  set readonly(v) {
1484
- this.#input.readOnly = v;
1485
- }
1486
- get disabled(){
1487
- return this.#input.hasAttribute('disabled');
1501
+ this._input.readOnly = v;
1488
1502
  }
1489
- set disabled(d){
1490
- ftl.Attributes.toggle(this.#input, 'disabled', d);
1503
+ //@ts-ignore
1504
+ get disabled() {
1505
+ return this._input.hasAttribute('disabled');
1506
+ }
1507
+ set disabled(d) {
1508
+ ftl.Attributes.toggle(this._input, 'disabled', d);
1491
1509
  }
1492
1510
  focus(options) {
1493
- this.#input.focus(options);
1511
+ this._input.focus(options);
1494
1512
  }
1495
1513
  setCustomValidity(error) {
1496
1514
  if (!error) {
1497
1515
  this.internals.setValidity({});
1498
- this.#fieldError.innerText = "";
1516
+ this._fieldError.innerText = "";
1499
1517
  return;
1500
1518
  }
1501
1519
  this.internals.setValidity({ customError: true }, " ");
1502
- this.#fieldError.innerText = error;
1520
+ this._fieldError.innerText = error;
1503
1521
  }
1504
- formResetCallback(){
1522
+ formResetCallback() {
1505
1523
  this.value = this.getAttribute("value");
1506
1524
  }
1507
1525
  }
1508
1526
 
1509
- class CompleteSelectLoader {
1527
+ class LocalDate extends ftl.ParsedElement {
1528
+ render() {
1529
+ const content = this.innerHTML.trim();
1530
+ if (content === '') {
1531
+ this.innerHTML = this.getAttribute('default') ?? '';
1532
+ return;
1533
+ }
1534
+ const locale = this.getAttribute("locale") ?? Intl.DateTimeFormat().resolvedOptions().locale;
1535
+ const formatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' });
1536
+ const [y, m, d] = content.split('-').map(Number);
1537
+ this.innerHTML = formatter.format(new Date(y, m - 1, d));
1538
+ }
1539
+ }
1540
+
1541
+ class Instant extends ftl.ParsedElement {
1542
+ render() {
1543
+ const content = this.innerHTML.trim();
1544
+ if (content === '') {
1545
+ this.innerHTML = this.getAttribute('default') ?? '';
1546
+ return;
1547
+ }
1548
+ const locale = this.getAttribute("locale") ?? Intl.DateTimeFormat().resolvedOptions().locale;
1549
+ const format = new Intl.DateTimeFormat(locale, {
1550
+ year: 'numeric',
1551
+ month: 'numeric',
1552
+ day: 'numeric',
1553
+ hour: 'numeric',
1554
+ minute: 'numeric',
1555
+ second: 'numeric',
1556
+ hour12: false
1557
+ });
1558
+ this.innerHTML = format.format(new Date(Instant.isoToLocal(content)));
1559
+ }
1560
+ static isoToLocal(iso) {
1561
+ //this is so sad
1562
+ const d = new Date(iso);
1563
+ const pad = (n, v) => String(v).padStart(n, '0');
1564
+ const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
1565
+ const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
1566
+ return `${date}T${time}`
1567
+ }
1568
+ }
1569
+
1570
+
1571
+ class InputLocalDate extends Input {
1572
+ static observed = ['value', 'readonly:presence', 'min', 'max', 'step'];
1573
+ _type() {
1574
+ return 'date';
1575
+ }
1576
+ get min() {
1577
+ const v = this._input.min;
1578
+ return v === '' ? null : v;
1579
+ }
1580
+ set min(v) {
1581
+ this._input.min = v;
1582
+ }
1583
+ get max() {
1584
+ const v = this._input.max;
1585
+ return v === '' ? null : v;
1586
+ }
1587
+ set max(v) {
1588
+ this._input.max = v;
1589
+ }
1590
+ get step() {
1591
+ const v = this._input.step;
1592
+ return v === '' ? null : v;
1593
+ }
1594
+ set step(v) {
1595
+ this._input.step = (v ?? '');
1596
+ }
1597
+
1598
+ }
1599
+
1600
+ class InputLocalTime extends InputLocalDate {
1601
+ _type() {
1602
+ return 'time';
1603
+ }
1604
+ }
1605
+
1606
+
1607
+ class InputInstant extends Input {
1608
+ static observed = ['value', 'readonly:presence', 'min', 'max', 'step'];
1609
+ _type() {
1610
+ return 'datetime-local';
1611
+ }
1612
+ get value() {
1613
+ const v = this._input.value;
1614
+ return v === '' ? null : new Date(v).toISOString();
1615
+ }
1616
+ set value(v) {
1617
+ this._input.value = v ? Instant.isoToLocal(v) : '';
1618
+ }
1619
+ get min() {
1620
+ const v = this._input.min;
1621
+ return v === '' ? null : new Date(v).toISOString();
1622
+ }
1623
+ set min(v) {
1624
+ this._input.min = v ? Instant.isoToLocal(v) : '';
1625
+ }
1626
+ get max() {
1627
+ const v = this._input.max;
1628
+ return v === '' ? null : new Date(v).toISOString();
1629
+ }
1630
+ set max(v) {
1631
+ this._input.max = v ? Instant.isoToLocal(v) : '';
1632
+ }
1633
+ get step() {
1634
+ const v = this._input.step;
1635
+ return v === '' ? null : v;
1636
+ }
1637
+ set step(v) {
1638
+ this._input.step = (v ?? '');
1639
+ }
1640
+ }
1641
+
1642
+ class RemoteLoader {
1510
1643
  #http;
1511
1644
  #url;
1512
1645
  #method;
1513
1646
  #responseMapper;
1514
1647
  #prefetch;
1648
+ #revision;
1515
1649
  #data;
1516
- constructor(http, url, method, responseMapper, prefetch) {
1650
+ static create({ el, http, responseMapper }) {
1651
+ return new RemoteLoader({
1652
+ http,
1653
+ url: el.getAttribute("src"),
1654
+ method: el.getAttribute("method") ?? 'POST',
1655
+ responseMapper,
1656
+ prefetch: el.hasAttribute("preload"),
1657
+ revision: el.getAttribute("revision")
1658
+ });
1659
+ }
1660
+ constructor({http, url, method, responseMapper, prefetch, revision}) {
1517
1661
  this.#http = http;
1518
1662
  this.#url = url;
1519
1663
  this.#method = method;
1520
1664
  this.#responseMapper = responseMapper;
1521
1665
  this.#prefetch = prefetch;
1666
+ this.#revision = revision;
1522
1667
  this.#data = null;
1523
1668
  }
1524
1669
  async prefetch() {
@@ -1529,37 +1674,47 @@ var ful = (function (exports, ftl) {
1529
1674
  }
1530
1675
  async exact(...keys) {
1531
1676
  await this.#ensureFetched();
1532
- return this.#data.filter(([k, v]) => keys.includes(k));
1677
+ return this.#data.filter(([k, v]) => keys.some(r => r == k));
1533
1678
  }
1534
1679
  async load(needle) {
1535
1680
  await this.#ensureFetched();
1536
- return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
1681
+ return this.#data.filter(([k, v]) => (v ?? '').includes(needle?.toLowerCase()));
1537
1682
  }
1538
1683
  async #ensureFetched() {
1539
1684
  if (this.#data !== null) {
1540
1685
  return
1541
1686
  }
1687
+ const storageKey = `${this.#method}@${this.#url}`;
1688
+ if(this.#revision !== null){
1689
+ const data = VersionedLocalStorage.load(storageKey, this.#revision);
1690
+ if(data !== undefined){
1691
+ this.#data = data;
1692
+ return;
1693
+ }
1694
+ }
1542
1695
  const data = await this.#http.request(this.#method, this.#url)
1543
1696
  .fetchJson();
1544
1697
  this.#data = this.#responseMapper(data);
1545
- }
1546
- static create({ el, http, responseMapper }) {
1547
- return new CompleteSelectLoader(
1548
- http,
1549
- el.getAttribute("src"),
1550
- el.getAttribute("method") ?? 'POST',
1551
- responseMapper,
1552
- el.hasAttribute("preload")
1553
- );
1698
+ if(this.#revision !== null){
1699
+ VersionedLocalStorage.save(storageKey, this.#revision, this.#data);
1700
+ }
1554
1701
  }
1555
1702
  }
1556
1703
 
1557
- class ChunkedSelectLoader {
1704
+ class PartialRemoteLoader {
1558
1705
  #http;
1559
1706
  #url;
1560
1707
  #method;
1561
1708
  #responseMapper;
1562
- constructor(http, url, method, responseMapper) {
1709
+ static create({ el, http, responseMapper }) {
1710
+ return new PartialRemoteLoader({
1711
+ http,
1712
+ url: el.getAttribute("src"),
1713
+ method: el.getAttribute("method") ?? 'POST',
1714
+ responseMapper
1715
+ });
1716
+ }
1717
+ constructor({http, url, method, responseMapper}) {
1563
1718
  this.#http = http;
1564
1719
  this.#url = url;
1565
1720
  this.#method = method;
@@ -1577,26 +1732,21 @@ var ful = (function (exports, ftl) {
1577
1732
  .fetchJson();
1578
1733
  return this.#responseMapper(data);
1579
1734
  }
1580
- static create({ el, http, responseMapper }) {
1581
- return new ChunkedSelectLoader(
1582
- http,
1583
- el.getAttribute("src"),
1584
- el.getAttribute("method") ?? 'POST',
1585
- responseMapper
1586
- );
1587
- }
1588
1735
  }
1589
1736
 
1590
- class OptionsSlotSelectLoader {
1737
+ class InMemoryLoader {
1591
1738
  #data
1592
1739
  constructor(data) {
1593
1740
  this.#data = data;
1594
1741
  }
1595
- async exact(...keys) {
1596
- return this.#data.filter(([k, v]) => keys.includes(k));
1742
+ update(data) {
1743
+ this.#data = data;
1597
1744
  }
1598
- async load(needle) {
1599
- return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
1745
+ exact(...keys) {
1746
+ return this.#data.filter(([k, v]) => keys.some(r => r == k));
1747
+ }
1748
+ load(needle) {
1749
+ return this.#data.filter(([k, v]) => (v ?? '').includes(needle?.toLowerCase()));
1600
1750
  }
1601
1751
  }
1602
1752
 
@@ -1608,10 +1758,10 @@ var ful = (function (exports, ftl) {
1608
1758
  const data = els.map(e => {
1609
1759
  return [e.getAttribute("value") ?? e.innerText.trim(), e.innerText.trim()];
1610
1760
  });
1611
- return new OptionsSlotSelectLoader(data);
1761
+ return new InMemoryLoader(data);
1612
1762
  }
1613
1763
  const chunked = "chunked" == conf.el.getAttribute("mode");
1614
- return chunked ? ChunkedSelectLoader.create(conf) : CompleteSelectLoader.create(conf);
1764
+ return chunked ? PartialRemoteLoader.create(conf) : RemoteLoader.create(conf);
1615
1765
  }
1616
1766
  }
1617
1767
 
@@ -1621,8 +1771,9 @@ var ful = (function (exports, ftl) {
1621
1771
  <ful-spinner class="centered" hidden></ful-spinner>
1622
1772
  <menu tabindex="-1" hidden></menu>
1623
1773
  `;
1624
- #spinner
1625
- #menu
1774
+ #spinner;
1775
+ #menu;
1776
+ #options = new Map();
1626
1777
  render({ slots }) {
1627
1778
  const fragment = this.template().render();
1628
1779
  this.#spinner = fragment.querySelector("ful-spinner");
@@ -1645,30 +1796,31 @@ var ful = (function (exports, ftl) {
1645
1796
  if (values === undefined) {
1646
1797
  throw new Error("null data");
1647
1798
  }
1799
+ this.#options = new Map(values.map((v,i) => [String(i), v]));
1648
1800
  if (values.length === 0) {
1649
1801
  const el = document.createElement('div');
1650
1802
  el.classList.add('text-center', 'py-2', 'bi', 'bi-database-slash');
1651
1803
  this.#menu.replaceChildren(el);
1652
1804
  return;
1653
1805
  }
1654
- this.#menu.replaceChildren(...values.map(([k, v], i) => {
1806
+ this.#menu.replaceChildren(...values.map(([k, v, m], i) => {
1655
1807
  const el = document.createElement('li');
1656
1808
  if (i === 0) {
1657
1809
  el.setAttribute("selected", '');
1658
1810
  }
1659
- el.setAttribute("value", k);
1811
+ el.setAttribute("value", i);
1660
1812
  el.innerText = v;
1661
1813
  return el;
1662
1814
  }));
1663
1815
  }
1664
1816
  #change(target) {
1665
- const value = target.getAttribute('value');
1666
- const label = target.innerText;
1817
+ const index = target.getAttribute('value');
1818
+ const data = this.#options.get(index);
1667
1819
  this.hide();
1668
1820
  this.dispatchEvent(new CustomEvent('change', {
1669
1821
  bubbles: true,
1670
1822
  cancelable: false,
1671
- detail: { label, value }
1823
+ detail: { index, data }
1672
1824
  }));
1673
1825
  }
1674
1826
  hide() {
@@ -1690,12 +1842,13 @@ var ful = (function (exports, ftl) {
1690
1842
  }
1691
1843
  }
1692
1844
  async moveOrShow(forward, loader) {
1693
- if (!this.hasAttribute("hidden")) {
1845
+ if (this.shown) {
1694
1846
  const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
1695
1847
  const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
1696
1848
  if (candidate) {
1697
1849
  selected.removeAttribute('selected');
1698
1850
  candidate.setAttribute("selected", "");
1851
+ candidate.scrollIntoView({block: "nearest", behavior: "smooth"});
1699
1852
  }
1700
1853
  return;
1701
1854
  }
@@ -1714,14 +1867,16 @@ var ful = (function (exports, ftl) {
1714
1867
  <div class="input-group flex-nowrap" tabindex="-1">
1715
1868
  <span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
1716
1869
  {{{{ slots.before }}}}
1717
- <div class="ful-select-input">
1718
- <badges></badges>
1719
- <input type="text" form="">
1870
+ <div class="ful-select-input-container">
1871
+ <div class="ful-select-input">
1872
+ <badges></badges>
1873
+ <input type="text" form="">
1874
+ </div>
1875
+ <ful-dropdown hidden popover="manual"></ful-dropdown>
1720
1876
  </div>
1721
1877
  {{{{ slots.after }}}}
1722
1878
  <span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
1723
1879
  </div>
1724
- <ful-dropdown hidden></ful-dropdown>
1725
1880
  <ful-field-error></ful-field-error>
1726
1881
  `;
1727
1882
  static mappers = {
@@ -1749,6 +1904,7 @@ var ful = (function (exports, ftl) {
1749
1904
  async render({ slots, observed, disabled }) {
1750
1905
  const name = this.getAttribute("name");
1751
1906
  this.#loader = Loaders.fromAttributes(this, 'loaders:select', { options: slots.options });
1907
+ this.#multiple = this.hasAttribute("multiple");
1752
1908
  await this.#loader.prefetch?.();
1753
1909
  const fragment = this.template().withOverlay({ slots, name }).render();
1754
1910
  this.#input = fragment.querySelector('input');
@@ -1759,7 +1915,6 @@ var ful = (function (exports, ftl) {
1759
1915
  this.readonly = observed.readonly;
1760
1916
 
1761
1917
  this.#ddmenu = fragment.querySelector('ful-dropdown');
1762
- this.#multiple = this.hasAttribute("multiple");
1763
1918
  const label = fragment.querySelector('label');
1764
1919
  label.addEventListener('click', () => this.focus());
1765
1920
  this.#fieldError = fragment.querySelector('ful-field-error');
@@ -1767,12 +1922,14 @@ var ful = (function (exports, ftl) {
1767
1922
  this.#input.ariaLabelledByElements = [label];
1768
1923
 
1769
1924
  const self = this;
1770
- const [dload, abortdload] = timing.debounce(400, () => self.#ddmenu.show(() => self.#loader.load(self.#input.value)));
1925
+ const [dload, abortdload] = Timing.throttle(400, () => self.#ddmenu.show(() => self.#loader.load(self.#input.value)));
1771
1926
  this.addEventListener('click', (/** @type any */e) => {
1772
- e.stopPropagation();
1773
1927
  if (e.target.matches('input')) {
1774
1928
  return;
1775
1929
  }
1930
+ if(this.disabled || this.readonly){
1931
+ return;
1932
+ }
1776
1933
  if (this.#ddmenu.shown) {
1777
1934
  this.#ddmenu.hide();
1778
1935
  return;
@@ -1782,15 +1939,20 @@ var ful = (function (exports, ftl) {
1782
1939
  });
1783
1940
  this.#badges.addEventListener('click', (e) => {
1784
1941
  e.stopPropagation();
1942
+ if(this.disabled || this.readonly){
1943
+ return;
1944
+ }
1785
1945
  const idx = [...this.#badges.children].indexOf(e.target);
1786
1946
  if (idx === -1) {
1787
1947
  return;
1788
1948
  }
1789
1949
  this.#values.delete(Array.from(this.#values.keys()).pop());
1950
+ this.#changed();
1790
1951
  this.#syncBadges();
1791
1952
  });
1792
1953
 
1793
1954
  this.#input.addEventListener('blur', e => {
1955
+ e.stopPropagation();
1794
1956
  if (e.relatedTarget && this.contains(e.relatedTarget)) {
1795
1957
  return;
1796
1958
  }
@@ -1799,6 +1961,10 @@ var ful = (function (exports, ftl) {
1799
1961
  this.#input.value = '';
1800
1962
  });
1801
1963
  this.#input.addEventListener('keydown', e => {
1964
+ e.stopPropagation();
1965
+ if(this.disabled || this.readonly){
1966
+ return;
1967
+ }
1802
1968
  switch (e.code) {
1803
1969
  case 'ArrowUp': {
1804
1970
  this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
@@ -1821,6 +1987,7 @@ var ful = (function (exports, ftl) {
1821
1987
  //remove last if caret a position 0
1822
1988
  if (this.#values.size && this.#input.selectionStart === 0 && this.#input.selectionEnd === 0) {
1823
1989
  this.#values.delete(Array.from(this.#values.keys()).pop());
1990
+ this.#changed();
1824
1991
  this.#syncBadges();
1825
1992
  }
1826
1993
  break;
@@ -1833,19 +2000,37 @@ var ful = (function (exports, ftl) {
1833
2000
  }
1834
2001
  });
1835
2002
  this.#input.addEventListener('input', e => {
2003
+ e.stopPropagation();
2004
+ if(this.disabled || this.readonly){
2005
+ return;
2006
+ }
1836
2007
  dload();
1837
2008
  });
1838
2009
  this.#ddmenu.addEventListener('change', (e) => {
2010
+ e.stopPropagation();
1839
2011
  if (!this.#multiple) {
1840
2012
  this.#values.clear();
1841
2013
  }
1842
- this.#values.set(e.detail.value, e.detail.label);
2014
+ this.#values.set(e.detail.data[0], e.detail.data.slice(1));
2015
+ this.#changed();
1843
2016
  this.#syncBadges();
1844
2017
  this.#input.focus();
1845
2018
  this.#ddmenu.hide();
1846
2019
  });
1847
2020
  this.replaceChildren(fragment);
1848
2021
  }
2022
+ withLoader(fn) {
2023
+ fn(this.#loader);
2024
+ }
2025
+ #changed() {
2026
+ const selection = [...this.#values.entries()].map(e => ({key: e[0], label: e[1][0], metadata: e[1].slice(1)}));
2027
+ const value = this.#multiple ? selection : (selection[0] ?? null);
2028
+ this.dispatchEvent(new CustomEvent('change', {
2029
+ bubbles: true,
2030
+ cancelable: false,
2031
+ detail: { value }
2032
+ }));
2033
+ }
1849
2034
  #syncBadges() {
1850
2035
  const badges = Array.from(this.#values.entries()).map(([k, v]) => {
1851
2036
  const b = document.createElement('badge');
@@ -1857,14 +2042,14 @@ var ful = (function (exports, ftl) {
1857
2042
  this.#badges.innerHTML = '';
1858
2043
  this.#badges.append(...badges);
1859
2044
  }
1860
- set value(value) {
1861
- if(value === null){
2045
+ set value(vs) {
2046
+ if(vs === null){
1862
2047
  this.#values = new Map();
1863
2048
  this.#syncBadges();
1864
2049
  return;
1865
2050
  }
1866
2051
  (async () => {
1867
- const entries = await (this.#multiple ? this.#loader.exact(...value) : this.#loader.exact(value));
2052
+ const entries = await (this.#multiple ? this.#loader.exact(...vs) : this.#loader.exact(vs));
1868
2053
  this.#values = new Map(entries);
1869
2054
  this.#syncBadges();
1870
2055
  })();
@@ -1875,6 +2060,25 @@ var ful = (function (exports, ftl) {
1875
2060
  }
1876
2061
  return [...this.#values.keys()][0] ?? null;
1877
2062
  }
2063
+ get entry() {
2064
+ if (this.#multiple) {
2065
+ return [...this.#values.entries()];
2066
+ }
2067
+ return [...this.#values.entries()][0] ?? null;
2068
+ }
2069
+ //@ts-ignore
2070
+ get disabled(){
2071
+ return this.#input.hasAttribute('disabled');
2072
+ }
2073
+ set disabled(d){
2074
+ ftl.Attributes.toggle(this.#input, 'disabled', d);
2075
+ }
2076
+ get readonly(){
2077
+ return this.#input.readOnly;
2078
+ }
2079
+ set readonly(v) {
2080
+ this.#input.readOnly = v;
2081
+ }
1878
2082
  focus(options) {
1879
2083
  this.#input.focus(options);
1880
2084
  }
@@ -2540,21 +2744,12 @@ var ful = (function (exports, ftl) {
2540
2744
  }
2541
2745
  const [operator, ...values] = v;
2542
2746
  this.#operator.setAttribute('value', operator);
2543
- this.#value1.value = values[0] ? InstantFilter.isoToLocal(values[0]) : values[0];
2544
- this.#value2.value = values[1] ? InstantFilter.isoToLocal(values[1]) : values[1];
2747
+ this.#value1.value = values[0] ? Instant.isoToLocal(values[0]) : values[0];
2748
+ this.#value2.value = values[1] ? Instant.isoToLocal(values[1]) : values[1];
2545
2749
  this.reflect(() => {
2546
2750
  this.setAttribute('value', JSON.stringify(v));
2547
2751
  });
2548
2752
  }
2549
-
2550
- static isoToLocal(iso) {
2551
- //this is so sad
2552
- const d = new Date(iso);
2553
- const pad = (n, v) => String(v).padStart(n, '0');
2554
- const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
2555
- const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
2556
- return `${date}T${time}`
2557
- }
2558
2753
  focus(options) {
2559
2754
  this.#value1.focus(options);
2560
2755
  }
@@ -2763,6 +2958,11 @@ var ful = (function (exports, ftl) {
2763
2958
  .defineElement('ful-form', Form)
2764
2959
  .defineElement('ful-checkbox', Checkbox)
2765
2960
  .defineElement('ful-input', Input)
2961
+ .defineElement('ful-local-date', LocalDate)
2962
+ .defineElement('ful-instant', Instant)
2963
+ .defineElement('ful-input-local-date', InputLocalDate)
2964
+ .defineElement('ful-input-local-time', InputLocalTime)
2965
+ .defineElement('ful-input-instant', InputInstant)
2766
2966
  .defineElement('ful-radio-group', RadioGroup)
2767
2967
  .defineElement('ful-table', Table)
2768
2968
  .defineElement('ful-pagination', Pagination)
@@ -2793,8 +2993,13 @@ var ful = (function (exports, ftl) {
2793
2993
  exports.HttpClient = HttpClient;
2794
2994
  exports.HttpClientError = HttpClientError;
2795
2995
  exports.Input = Input;
2996
+ exports.InputInstant = InputInstant;
2997
+ exports.InputLocalDate = InputLocalDate;
2998
+ exports.InputLocalTime = InputLocalTime;
2999
+ exports.Instant = Instant;
2796
3000
  exports.InstantFilter = InstantFilter;
2797
3001
  exports.Loaders = Loaders;
3002
+ exports.LocalDate = LocalDate;
2798
3003
  exports.LocalDateFilter = LocalDateFilter;
2799
3004
  exports.LocalStorage = LocalStorage;
2800
3005
  exports.MediaType = MediaType;
@@ -2809,8 +3014,9 @@ var ful = (function (exports, ftl) {
2809
3014
  exports.Table = Table;
2810
3015
  exports.TableSchemaParser = TableSchemaParser;
2811
3016
  exports.TextFilter = TextFilter;
2812
- exports.VersionedStorage = VersionedStorage;
2813
- exports.timing = timing;
3017
+ exports.Timing = Timing;
3018
+ exports.VersionedLocalStorage = VersionedLocalStorage;
3019
+ exports.VersionedSessionStorage = VersionedSessionStorage;
2814
3020
 
2815
3021
  return exports;
2816
3022