@optionfactory/ful 3.0.2 → 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) {
@@ -1444,14 +1455,15 @@ var ful = (function (exports, ftl) {
1444
1455
  this.internals = this.attachInternals();
1445
1456
  this.internals.role = 'presentation';
1446
1457
  }
1447
- _type(){
1448
- return this.getAttribute("type") ?? 'text'; }
1449
- _fragment(type, slots){
1450
- return this.template().withOverlay({ type, slots }).render();
1458
+ _type() {
1459
+ return this.getAttribute("type") ?? 'text';
1460
+ }
1461
+ _fragment(type, slots) {
1462
+ return this.template().withOverlay({ type, slots }).render();
1451
1463
  }
1452
1464
  render({ slots, observed, disabled }) {
1453
1465
  const type = this._type();
1454
- const fragment = this._fragment(type, slots );
1466
+ const fragment = this._fragment(type, slots);
1455
1467
  this._input = fragment.querySelector("input,textarea");
1456
1468
 
1457
1469
  ftl.Attributes.forward('input-', this, this._input);
@@ -1482,16 +1494,17 @@ var ful = (function (exports, ftl) {
1482
1494
  set value(value) {
1483
1495
  this._input.value = value === '' ? null : value;
1484
1496
  }
1485
- get readonly(){
1497
+ get readonly() {
1486
1498
  return this._input.readOnly;
1487
1499
  }
1488
1500
  set readonly(v) {
1489
1501
  this._input.readOnly = v;
1490
- }
1491
- get disabled(){
1502
+ }
1503
+ //@ts-ignore
1504
+ get disabled() {
1492
1505
  return this._input.hasAttribute('disabled');
1493
1506
  }
1494
- set disabled(d){
1507
+ set disabled(d) {
1495
1508
  ftl.Attributes.toggle(this._input, 'disabled', d);
1496
1509
  }
1497
1510
  focus(options) {
@@ -1506,24 +1519,151 @@ var ful = (function (exports, ftl) {
1506
1519
  this.internals.setValidity({ customError: true }, " ");
1507
1520
  this._fieldError.innerText = error;
1508
1521
  }
1509
- formResetCallback(){
1522
+ formResetCallback() {
1510
1523
  this.value = this.getAttribute("value");
1511
1524
  }
1512
1525
  }
1513
1526
 
1514
- 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 {
1515
1643
  #http;
1516
1644
  #url;
1517
1645
  #method;
1518
1646
  #responseMapper;
1519
1647
  #prefetch;
1648
+ #revision;
1520
1649
  #data;
1521
- 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}) {
1522
1661
  this.#http = http;
1523
1662
  this.#url = url;
1524
1663
  this.#method = method;
1525
1664
  this.#responseMapper = responseMapper;
1526
1665
  this.#prefetch = prefetch;
1666
+ this.#revision = revision;
1527
1667
  this.#data = null;
1528
1668
  }
1529
1669
  async prefetch() {
@@ -1534,37 +1674,47 @@ var ful = (function (exports, ftl) {
1534
1674
  }
1535
1675
  async exact(...keys) {
1536
1676
  await this.#ensureFetched();
1537
- return this.#data.filter(([k, v]) => keys.includes(k));
1677
+ return this.#data.filter(([k, v]) => keys.some(r => r == k));
1538
1678
  }
1539
1679
  async load(needle) {
1540
1680
  await this.#ensureFetched();
1541
- return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
1681
+ return this.#data.filter(([k, v]) => (v ?? '').includes(needle?.toLowerCase()));
1542
1682
  }
1543
1683
  async #ensureFetched() {
1544
1684
  if (this.#data !== null) {
1545
1685
  return
1546
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
+ }
1547
1695
  const data = await this.#http.request(this.#method, this.#url)
1548
1696
  .fetchJson();
1549
1697
  this.#data = this.#responseMapper(data);
1550
- }
1551
- static create({ el, http, responseMapper }) {
1552
- return new CompleteSelectLoader(
1553
- http,
1554
- el.getAttribute("src"),
1555
- el.getAttribute("method") ?? 'POST',
1556
- responseMapper,
1557
- el.hasAttribute("preload")
1558
- );
1698
+ if(this.#revision !== null){
1699
+ VersionedLocalStorage.save(storageKey, this.#revision, this.#data);
1700
+ }
1559
1701
  }
1560
1702
  }
1561
1703
 
1562
- class ChunkedSelectLoader {
1704
+ class PartialRemoteLoader {
1563
1705
  #http;
1564
1706
  #url;
1565
1707
  #method;
1566
1708
  #responseMapper;
1567
- 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}) {
1568
1718
  this.#http = http;
1569
1719
  this.#url = url;
1570
1720
  this.#method = method;
@@ -1582,26 +1732,21 @@ var ful = (function (exports, ftl) {
1582
1732
  .fetchJson();
1583
1733
  return this.#responseMapper(data);
1584
1734
  }
1585
- static create({ el, http, responseMapper }) {
1586
- return new ChunkedSelectLoader(
1587
- http,
1588
- el.getAttribute("src"),
1589
- el.getAttribute("method") ?? 'POST',
1590
- responseMapper
1591
- );
1592
- }
1593
1735
  }
1594
1736
 
1595
- class OptionsSlotSelectLoader {
1737
+ class InMemoryLoader {
1596
1738
  #data
1597
1739
  constructor(data) {
1598
1740
  this.#data = data;
1599
1741
  }
1600
- async exact(...keys) {
1601
- return this.#data.filter(([k, v]) => keys.includes(k));
1742
+ update(data) {
1743
+ this.#data = data;
1602
1744
  }
1603
- async load(needle) {
1604
- 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()));
1605
1750
  }
1606
1751
  }
1607
1752
 
@@ -1613,10 +1758,10 @@ var ful = (function (exports, ftl) {
1613
1758
  const data = els.map(e => {
1614
1759
  return [e.getAttribute("value") ?? e.innerText.trim(), e.innerText.trim()];
1615
1760
  });
1616
- return new OptionsSlotSelectLoader(data);
1761
+ return new InMemoryLoader(data);
1617
1762
  }
1618
1763
  const chunked = "chunked" == conf.el.getAttribute("mode");
1619
- return chunked ? ChunkedSelectLoader.create(conf) : CompleteSelectLoader.create(conf);
1764
+ return chunked ? PartialRemoteLoader.create(conf) : RemoteLoader.create(conf);
1620
1765
  }
1621
1766
  }
1622
1767
 
@@ -1626,8 +1771,9 @@ var ful = (function (exports, ftl) {
1626
1771
  <ful-spinner class="centered" hidden></ful-spinner>
1627
1772
  <menu tabindex="-1" hidden></menu>
1628
1773
  `;
1629
- #spinner
1630
- #menu
1774
+ #spinner;
1775
+ #menu;
1776
+ #options = new Map();
1631
1777
  render({ slots }) {
1632
1778
  const fragment = this.template().render();
1633
1779
  this.#spinner = fragment.querySelector("ful-spinner");
@@ -1650,30 +1796,31 @@ var ful = (function (exports, ftl) {
1650
1796
  if (values === undefined) {
1651
1797
  throw new Error("null data");
1652
1798
  }
1799
+ this.#options = new Map(values.map((v,i) => [String(i), v]));
1653
1800
  if (values.length === 0) {
1654
1801
  const el = document.createElement('div');
1655
1802
  el.classList.add('text-center', 'py-2', 'bi', 'bi-database-slash');
1656
1803
  this.#menu.replaceChildren(el);
1657
1804
  return;
1658
1805
  }
1659
- this.#menu.replaceChildren(...values.map(([k, v], i) => {
1806
+ this.#menu.replaceChildren(...values.map(([k, v, m], i) => {
1660
1807
  const el = document.createElement('li');
1661
1808
  if (i === 0) {
1662
1809
  el.setAttribute("selected", '');
1663
1810
  }
1664
- el.setAttribute("value", k);
1811
+ el.setAttribute("value", i);
1665
1812
  el.innerText = v;
1666
1813
  return el;
1667
1814
  }));
1668
1815
  }
1669
1816
  #change(target) {
1670
- const value = target.getAttribute('value');
1671
- const label = target.innerText;
1817
+ const index = target.getAttribute('value');
1818
+ const data = this.#options.get(index);
1672
1819
  this.hide();
1673
1820
  this.dispatchEvent(new CustomEvent('change', {
1674
1821
  bubbles: true,
1675
1822
  cancelable: false,
1676
- detail: { label, value }
1823
+ detail: { index, data }
1677
1824
  }));
1678
1825
  }
1679
1826
  hide() {
@@ -1695,12 +1842,13 @@ var ful = (function (exports, ftl) {
1695
1842
  }
1696
1843
  }
1697
1844
  async moveOrShow(forward, loader) {
1698
- if (!this.hasAttribute("hidden")) {
1845
+ if (this.shown) {
1699
1846
  const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
1700
1847
  const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
1701
1848
  if (candidate) {
1702
1849
  selected.removeAttribute('selected');
1703
1850
  candidate.setAttribute("selected", "");
1851
+ candidate.scrollIntoView({block: "nearest", behavior: "smooth"});
1704
1852
  }
1705
1853
  return;
1706
1854
  }
@@ -1719,14 +1867,16 @@ var ful = (function (exports, ftl) {
1719
1867
  <div class="input-group flex-nowrap" tabindex="-1">
1720
1868
  <span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
1721
1869
  {{{{ slots.before }}}}
1722
- <div class="ful-select-input">
1723
- <badges></badges>
1724
- <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>
1725
1876
  </div>
1726
1877
  {{{{ slots.after }}}}
1727
1878
  <span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
1728
1879
  </div>
1729
- <ful-dropdown hidden></ful-dropdown>
1730
1880
  <ful-field-error></ful-field-error>
1731
1881
  `;
1732
1882
  static mappers = {
@@ -1754,6 +1904,7 @@ var ful = (function (exports, ftl) {
1754
1904
  async render({ slots, observed, disabled }) {
1755
1905
  const name = this.getAttribute("name");
1756
1906
  this.#loader = Loaders.fromAttributes(this, 'loaders:select', { options: slots.options });
1907
+ this.#multiple = this.hasAttribute("multiple");
1757
1908
  await this.#loader.prefetch?.();
1758
1909
  const fragment = this.template().withOverlay({ slots, name }).render();
1759
1910
  this.#input = fragment.querySelector('input');
@@ -1764,7 +1915,6 @@ var ful = (function (exports, ftl) {
1764
1915
  this.readonly = observed.readonly;
1765
1916
 
1766
1917
  this.#ddmenu = fragment.querySelector('ful-dropdown');
1767
- this.#multiple = this.hasAttribute("multiple");
1768
1918
  const label = fragment.querySelector('label');
1769
1919
  label.addEventListener('click', () => this.focus());
1770
1920
  this.#fieldError = fragment.querySelector('ful-field-error');
@@ -1772,12 +1922,14 @@ var ful = (function (exports, ftl) {
1772
1922
  this.#input.ariaLabelledByElements = [label];
1773
1923
 
1774
1924
  const self = this;
1775
- 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)));
1776
1926
  this.addEventListener('click', (/** @type any */e) => {
1777
- e.stopPropagation();
1778
1927
  if (e.target.matches('input')) {
1779
1928
  return;
1780
1929
  }
1930
+ if(this.disabled || this.readonly){
1931
+ return;
1932
+ }
1781
1933
  if (this.#ddmenu.shown) {
1782
1934
  this.#ddmenu.hide();
1783
1935
  return;
@@ -1787,15 +1939,20 @@ var ful = (function (exports, ftl) {
1787
1939
  });
1788
1940
  this.#badges.addEventListener('click', (e) => {
1789
1941
  e.stopPropagation();
1942
+ if(this.disabled || this.readonly){
1943
+ return;
1944
+ }
1790
1945
  const idx = [...this.#badges.children].indexOf(e.target);
1791
1946
  if (idx === -1) {
1792
1947
  return;
1793
1948
  }
1794
1949
  this.#values.delete(Array.from(this.#values.keys()).pop());
1950
+ this.#changed();
1795
1951
  this.#syncBadges();
1796
1952
  });
1797
1953
 
1798
1954
  this.#input.addEventListener('blur', e => {
1955
+ e.stopPropagation();
1799
1956
  if (e.relatedTarget && this.contains(e.relatedTarget)) {
1800
1957
  return;
1801
1958
  }
@@ -1804,6 +1961,10 @@ var ful = (function (exports, ftl) {
1804
1961
  this.#input.value = '';
1805
1962
  });
1806
1963
  this.#input.addEventListener('keydown', e => {
1964
+ e.stopPropagation();
1965
+ if(this.disabled || this.readonly){
1966
+ return;
1967
+ }
1807
1968
  switch (e.code) {
1808
1969
  case 'ArrowUp': {
1809
1970
  this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
@@ -1826,6 +1987,7 @@ var ful = (function (exports, ftl) {
1826
1987
  //remove last if caret a position 0
1827
1988
  if (this.#values.size && this.#input.selectionStart === 0 && this.#input.selectionEnd === 0) {
1828
1989
  this.#values.delete(Array.from(this.#values.keys()).pop());
1990
+ this.#changed();
1829
1991
  this.#syncBadges();
1830
1992
  }
1831
1993
  break;
@@ -1838,19 +2000,37 @@ var ful = (function (exports, ftl) {
1838
2000
  }
1839
2001
  });
1840
2002
  this.#input.addEventListener('input', e => {
2003
+ e.stopPropagation();
2004
+ if(this.disabled || this.readonly){
2005
+ return;
2006
+ }
1841
2007
  dload();
1842
2008
  });
1843
2009
  this.#ddmenu.addEventListener('change', (e) => {
2010
+ e.stopPropagation();
1844
2011
  if (!this.#multiple) {
1845
2012
  this.#values.clear();
1846
2013
  }
1847
- 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();
1848
2016
  this.#syncBadges();
1849
2017
  this.#input.focus();
1850
2018
  this.#ddmenu.hide();
1851
2019
  });
1852
2020
  this.replaceChildren(fragment);
1853
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
+ }
1854
2034
  #syncBadges() {
1855
2035
  const badges = Array.from(this.#values.entries()).map(([k, v]) => {
1856
2036
  const b = document.createElement('badge');
@@ -1862,14 +2042,14 @@ var ful = (function (exports, ftl) {
1862
2042
  this.#badges.innerHTML = '';
1863
2043
  this.#badges.append(...badges);
1864
2044
  }
1865
- set value(value) {
1866
- if(value === null){
2045
+ set value(vs) {
2046
+ if(vs === null){
1867
2047
  this.#values = new Map();
1868
2048
  this.#syncBadges();
1869
2049
  return;
1870
2050
  }
1871
2051
  (async () => {
1872
- 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));
1873
2053
  this.#values = new Map(entries);
1874
2054
  this.#syncBadges();
1875
2055
  })();
@@ -1880,6 +2060,25 @@ var ful = (function (exports, ftl) {
1880
2060
  }
1881
2061
  return [...this.#values.keys()][0] ?? null;
1882
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
+ }
1883
2082
  focus(options) {
1884
2083
  this.#input.focus(options);
1885
2084
  }
@@ -2545,21 +2744,12 @@ var ful = (function (exports, ftl) {
2545
2744
  }
2546
2745
  const [operator, ...values] = v;
2547
2746
  this.#operator.setAttribute('value', operator);
2548
- this.#value1.value = values[0] ? InstantFilter.isoToLocal(values[0]) : values[0];
2549
- 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];
2550
2749
  this.reflect(() => {
2551
2750
  this.setAttribute('value', JSON.stringify(v));
2552
2751
  });
2553
2752
  }
2554
-
2555
- static isoToLocal(iso) {
2556
- //this is so sad
2557
- const d = new Date(iso);
2558
- const pad = (n, v) => String(v).padStart(n, '0');
2559
- const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
2560
- const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
2561
- return `${date}T${time}`
2562
- }
2563
2753
  focus(options) {
2564
2754
  this.#value1.focus(options);
2565
2755
  }
@@ -2768,6 +2958,11 @@ var ful = (function (exports, ftl) {
2768
2958
  .defineElement('ful-form', Form)
2769
2959
  .defineElement('ful-checkbox', Checkbox)
2770
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)
2771
2966
  .defineElement('ful-radio-group', RadioGroup)
2772
2967
  .defineElement('ful-table', Table)
2773
2968
  .defineElement('ful-pagination', Pagination)
@@ -2798,8 +2993,13 @@ var ful = (function (exports, ftl) {
2798
2993
  exports.HttpClient = HttpClient;
2799
2994
  exports.HttpClientError = HttpClientError;
2800
2995
  exports.Input = Input;
2996
+ exports.InputInstant = InputInstant;
2997
+ exports.InputLocalDate = InputLocalDate;
2998
+ exports.InputLocalTime = InputLocalTime;
2999
+ exports.Instant = Instant;
2801
3000
  exports.InstantFilter = InstantFilter;
2802
3001
  exports.Loaders = Loaders;
3002
+ exports.LocalDate = LocalDate;
2803
3003
  exports.LocalDateFilter = LocalDateFilter;
2804
3004
  exports.LocalStorage = LocalStorage;
2805
3005
  exports.MediaType = MediaType;
@@ -2814,8 +3014,9 @@ var ful = (function (exports, ftl) {
2814
3014
  exports.Table = Table;
2815
3015
  exports.TableSchemaParser = TableSchemaParser;
2816
3016
  exports.TextFilter = TextFilter;
2817
- exports.VersionedStorage = VersionedStorage;
2818
- exports.timing = timing;
3017
+ exports.Timing = Timing;
3018
+ exports.VersionedLocalStorage = VersionedLocalStorage;
3019
+ exports.VersionedSessionStorage = VersionedSessionStorage;
2819
3020
 
2820
3021
  return exports;
2821
3022