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