@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.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) {
@@ -1436,24 +1447,30 @@ class Input extends ParsedElement {
1436
1447
  <ful-field-error></ful-field-error>
1437
1448
  `;
1438
1449
  static formAssociated = true;
1439
- #input;
1440
- #fieldError;
1450
+ _input;
1451
+ _fieldError;
1441
1452
  constructor() {
1442
1453
  super();
1443
1454
  this.internals = this.attachInternals();
1444
1455
  this.internals.role = 'presentation';
1445
1456
  }
1457
+ _type() {
1458
+ return this.getAttribute("type") ?? 'text';
1459
+ }
1460
+ _fragment(type, slots) {
1461
+ return this.template().withOverlay({ type, slots }).render();
1462
+ }
1446
1463
  render({ slots, observed, disabled }) {
1447
- const type = this.getAttribute("type") ?? 'text';
1448
- const fragment = this.template().withOverlay({ type, slots }).render();
1449
- this.#input = fragment.querySelector("input,textarea");
1464
+ const type = this._type();
1465
+ const fragment = this._fragment(type, slots);
1466
+ this._input = fragment.querySelector("input,textarea");
1450
1467
 
1451
- Attributes.forward('input-', this, this.#input);
1468
+ Attributes.forward('input-', this, this._input);
1452
1469
  this.disabled = disabled;
1453
1470
  this.readonly = observed.readonly;
1454
1471
  this.value = observed.value;
1455
1472
 
1456
- this.#input.addEventListener('change', (evt) => {
1473
+ this._input.addEventListener('change', (evt) => {
1457
1474
  evt.stopPropagation();
1458
1475
  this.dispatchEvent(new CustomEvent('change', {
1459
1476
  bubbles: true,
@@ -1465,59 +1482,187 @@ class Input extends ParsedElement {
1465
1482
  });
1466
1483
  const label = fragment.querySelector('label');
1467
1484
  label.addEventListener('click', () => this.focus());
1468
- this.#fieldError = fragment.querySelector('ful-field-error');
1469
- this.#input.ariaDescribedByElements = [this.#fieldError];
1470
- this.#input.ariaLabelledByElements = [label];
1485
+ this._fieldError = fragment.querySelector('ful-field-error');
1486
+ this._input.ariaDescribedByElements = [this._fieldError];
1487
+ this._input.ariaLabelledByElements = [label];
1471
1488
  this.replaceChildren(fragment);
1472
1489
  }
1473
1490
  get value() {
1474
- return this.#input.value === '' ? null : this.#input.value;
1491
+ return this._input.value === '' ? null : this._input.value;
1475
1492
  }
1476
1493
  set value(value) {
1477
- this.#input.value = value === '' ? null : value;
1494
+ this._input.value = value === '' ? null : value;
1478
1495
  }
1479
- get readonly(){
1480
- return this.#input.readOnly;
1496
+ get readonly() {
1497
+ return this._input.readOnly;
1481
1498
  }
1482
1499
  set readonly(v) {
1483
- this.#input.readOnly = v;
1484
- }
1485
- get disabled(){
1486
- return this.#input.hasAttribute('disabled');
1500
+ this._input.readOnly = v;
1487
1501
  }
1488
- set disabled(d){
1489
- Attributes.toggle(this.#input, 'disabled', d);
1502
+ //@ts-ignore
1503
+ get disabled() {
1504
+ return this._input.hasAttribute('disabled');
1505
+ }
1506
+ set disabled(d) {
1507
+ Attributes.toggle(this._input, 'disabled', d);
1490
1508
  }
1491
1509
  focus(options) {
1492
- this.#input.focus(options);
1510
+ this._input.focus(options);
1493
1511
  }
1494
1512
  setCustomValidity(error) {
1495
1513
  if (!error) {
1496
1514
  this.internals.setValidity({});
1497
- this.#fieldError.innerText = "";
1515
+ this._fieldError.innerText = "";
1498
1516
  return;
1499
1517
  }
1500
1518
  this.internals.setValidity({ customError: true }, " ");
1501
- this.#fieldError.innerText = error;
1519
+ this._fieldError.innerText = error;
1502
1520
  }
1503
- formResetCallback(){
1521
+ formResetCallback() {
1504
1522
  this.value = this.getAttribute("value");
1505
1523
  }
1506
1524
  }
1507
1525
 
1508
- 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 {
1509
1642
  #http;
1510
1643
  #url;
1511
1644
  #method;
1512
1645
  #responseMapper;
1513
1646
  #prefetch;
1647
+ #revision;
1514
1648
  #data;
1515
- 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}) {
1516
1660
  this.#http = http;
1517
1661
  this.#url = url;
1518
1662
  this.#method = method;
1519
1663
  this.#responseMapper = responseMapper;
1520
1664
  this.#prefetch = prefetch;
1665
+ this.#revision = revision;
1521
1666
  this.#data = null;
1522
1667
  }
1523
1668
  async prefetch() {
@@ -1528,37 +1673,47 @@ class CompleteSelectLoader {
1528
1673
  }
1529
1674
  async exact(...keys) {
1530
1675
  await this.#ensureFetched();
1531
- return this.#data.filter(([k, v]) => keys.includes(k));
1676
+ return this.#data.filter(([k, v]) => keys.some(r => r == k));
1532
1677
  }
1533
1678
  async load(needle) {
1534
1679
  await this.#ensureFetched();
1535
- return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
1680
+ return this.#data.filter(([k, v]) => (v ?? '').includes(needle?.toLowerCase()));
1536
1681
  }
1537
1682
  async #ensureFetched() {
1538
1683
  if (this.#data !== null) {
1539
1684
  return
1540
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
+ }
1541
1694
  const data = await this.#http.request(this.#method, this.#url)
1542
1695
  .fetchJson();
1543
1696
  this.#data = this.#responseMapper(data);
1544
- }
1545
- static create({ el, http, responseMapper }) {
1546
- return new CompleteSelectLoader(
1547
- http,
1548
- el.getAttribute("src"),
1549
- el.getAttribute("method") ?? 'POST',
1550
- responseMapper,
1551
- el.hasAttribute("preload")
1552
- );
1697
+ if(this.#revision !== null){
1698
+ VersionedLocalStorage.save(storageKey, this.#revision, this.#data);
1699
+ }
1553
1700
  }
1554
1701
  }
1555
1702
 
1556
- class ChunkedSelectLoader {
1703
+ class PartialRemoteLoader {
1557
1704
  #http;
1558
1705
  #url;
1559
1706
  #method;
1560
1707
  #responseMapper;
1561
- 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}) {
1562
1717
  this.#http = http;
1563
1718
  this.#url = url;
1564
1719
  this.#method = method;
@@ -1576,26 +1731,21 @@ class ChunkedSelectLoader {
1576
1731
  .fetchJson();
1577
1732
  return this.#responseMapper(data);
1578
1733
  }
1579
- static create({ el, http, responseMapper }) {
1580
- return new ChunkedSelectLoader(
1581
- http,
1582
- el.getAttribute("src"),
1583
- el.getAttribute("method") ?? 'POST',
1584
- responseMapper
1585
- );
1586
- }
1587
1734
  }
1588
1735
 
1589
- class OptionsSlotSelectLoader {
1736
+ class InMemoryLoader {
1590
1737
  #data
1591
1738
  constructor(data) {
1592
1739
  this.#data = data;
1593
1740
  }
1594
- async exact(...keys) {
1595
- return this.#data.filter(([k, v]) => keys.includes(k));
1741
+ update(data) {
1742
+ this.#data = data;
1596
1743
  }
1597
- async load(needle) {
1598
- 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()));
1599
1749
  }
1600
1750
  }
1601
1751
 
@@ -1607,10 +1757,10 @@ class SelectLoader {
1607
1757
  const data = els.map(e => {
1608
1758
  return [e.getAttribute("value") ?? e.innerText.trim(), e.innerText.trim()];
1609
1759
  });
1610
- return new OptionsSlotSelectLoader(data);
1760
+ return new InMemoryLoader(data);
1611
1761
  }
1612
1762
  const chunked = "chunked" == conf.el.getAttribute("mode");
1613
- return chunked ? ChunkedSelectLoader.create(conf) : CompleteSelectLoader.create(conf);
1763
+ return chunked ? PartialRemoteLoader.create(conf) : RemoteLoader.create(conf);
1614
1764
  }
1615
1765
  }
1616
1766
 
@@ -1620,8 +1770,9 @@ class Dropdown extends ParsedElement {
1620
1770
  <ful-spinner class="centered" hidden></ful-spinner>
1621
1771
  <menu tabindex="-1" hidden></menu>
1622
1772
  `;
1623
- #spinner
1624
- #menu
1773
+ #spinner;
1774
+ #menu;
1775
+ #options = new Map();
1625
1776
  render({ slots }) {
1626
1777
  const fragment = this.template().render();
1627
1778
  this.#spinner = fragment.querySelector("ful-spinner");
@@ -1644,30 +1795,31 @@ class Dropdown extends ParsedElement {
1644
1795
  if (values === undefined) {
1645
1796
  throw new Error("null data");
1646
1797
  }
1798
+ this.#options = new Map(values.map((v,i) => [String(i), v]));
1647
1799
  if (values.length === 0) {
1648
1800
  const el = document.createElement('div');
1649
1801
  el.classList.add('text-center', 'py-2', 'bi', 'bi-database-slash');
1650
1802
  this.#menu.replaceChildren(el);
1651
1803
  return;
1652
1804
  }
1653
- this.#menu.replaceChildren(...values.map(([k, v], i) => {
1805
+ this.#menu.replaceChildren(...values.map(([k, v, m], i) => {
1654
1806
  const el = document.createElement('li');
1655
1807
  if (i === 0) {
1656
1808
  el.setAttribute("selected", '');
1657
1809
  }
1658
- el.setAttribute("value", k);
1810
+ el.setAttribute("value", i);
1659
1811
  el.innerText = v;
1660
1812
  return el;
1661
1813
  }));
1662
1814
  }
1663
1815
  #change(target) {
1664
- const value = target.getAttribute('value');
1665
- const label = target.innerText;
1816
+ const index = target.getAttribute('value');
1817
+ const data = this.#options.get(index);
1666
1818
  this.hide();
1667
1819
  this.dispatchEvent(new CustomEvent('change', {
1668
1820
  bubbles: true,
1669
1821
  cancelable: false,
1670
- detail: { label, value }
1822
+ detail: { index, data }
1671
1823
  }));
1672
1824
  }
1673
1825
  hide() {
@@ -1689,12 +1841,13 @@ class Dropdown extends ParsedElement {
1689
1841
  }
1690
1842
  }
1691
1843
  async moveOrShow(forward, loader) {
1692
- if (!this.hasAttribute("hidden")) {
1844
+ if (this.shown) {
1693
1845
  const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
1694
1846
  const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
1695
1847
  if (candidate) {
1696
1848
  selected.removeAttribute('selected');
1697
1849
  candidate.setAttribute("selected", "");
1850
+ candidate.scrollIntoView({block: "nearest", behavior: "smooth"});
1698
1851
  }
1699
1852
  return;
1700
1853
  }
@@ -1713,14 +1866,16 @@ class Select extends ParsedElement {
1713
1866
  <div class="input-group flex-nowrap" tabindex="-1">
1714
1867
  <span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
1715
1868
  {{{{ slots.before }}}}
1716
- <div class="ful-select-input">
1717
- <badges></badges>
1718
- <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>
1719
1875
  </div>
1720
1876
  {{{{ slots.after }}}}
1721
1877
  <span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
1722
1878
  </div>
1723
- <ful-dropdown hidden></ful-dropdown>
1724
1879
  <ful-field-error></ful-field-error>
1725
1880
  `;
1726
1881
  static mappers = {
@@ -1748,6 +1903,7 @@ class Select extends ParsedElement {
1748
1903
  async render({ slots, observed, disabled }) {
1749
1904
  const name = this.getAttribute("name");
1750
1905
  this.#loader = Loaders.fromAttributes(this, 'loaders:select', { options: slots.options });
1906
+ this.#multiple = this.hasAttribute("multiple");
1751
1907
  await this.#loader.prefetch?.();
1752
1908
  const fragment = this.template().withOverlay({ slots, name }).render();
1753
1909
  this.#input = fragment.querySelector('input');
@@ -1758,7 +1914,6 @@ class Select extends ParsedElement {
1758
1914
  this.readonly = observed.readonly;
1759
1915
 
1760
1916
  this.#ddmenu = fragment.querySelector('ful-dropdown');
1761
- this.#multiple = this.hasAttribute("multiple");
1762
1917
  const label = fragment.querySelector('label');
1763
1918
  label.addEventListener('click', () => this.focus());
1764
1919
  this.#fieldError = fragment.querySelector('ful-field-error');
@@ -1766,12 +1921,14 @@ class Select extends ParsedElement {
1766
1921
  this.#input.ariaLabelledByElements = [label];
1767
1922
 
1768
1923
  const self = this;
1769
- 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)));
1770
1925
  this.addEventListener('click', (/** @type any */e) => {
1771
- e.stopPropagation();
1772
1926
  if (e.target.matches('input')) {
1773
1927
  return;
1774
1928
  }
1929
+ if(this.disabled || this.readonly){
1930
+ return;
1931
+ }
1775
1932
  if (this.#ddmenu.shown) {
1776
1933
  this.#ddmenu.hide();
1777
1934
  return;
@@ -1781,15 +1938,20 @@ class Select extends ParsedElement {
1781
1938
  });
1782
1939
  this.#badges.addEventListener('click', (e) => {
1783
1940
  e.stopPropagation();
1941
+ if(this.disabled || this.readonly){
1942
+ return;
1943
+ }
1784
1944
  const idx = [...this.#badges.children].indexOf(e.target);
1785
1945
  if (idx === -1) {
1786
1946
  return;
1787
1947
  }
1788
1948
  this.#values.delete(Array.from(this.#values.keys()).pop());
1949
+ this.#changed();
1789
1950
  this.#syncBadges();
1790
1951
  });
1791
1952
 
1792
1953
  this.#input.addEventListener('blur', e => {
1954
+ e.stopPropagation();
1793
1955
  if (e.relatedTarget && this.contains(e.relatedTarget)) {
1794
1956
  return;
1795
1957
  }
@@ -1798,6 +1960,10 @@ class Select extends ParsedElement {
1798
1960
  this.#input.value = '';
1799
1961
  });
1800
1962
  this.#input.addEventListener('keydown', e => {
1963
+ e.stopPropagation();
1964
+ if(this.disabled || this.readonly){
1965
+ return;
1966
+ }
1801
1967
  switch (e.code) {
1802
1968
  case 'ArrowUp': {
1803
1969
  this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
@@ -1820,6 +1986,7 @@ class Select extends ParsedElement {
1820
1986
  //remove last if caret a position 0
1821
1987
  if (this.#values.size && this.#input.selectionStart === 0 && this.#input.selectionEnd === 0) {
1822
1988
  this.#values.delete(Array.from(this.#values.keys()).pop());
1989
+ this.#changed();
1823
1990
  this.#syncBadges();
1824
1991
  }
1825
1992
  break;
@@ -1832,19 +1999,37 @@ class Select extends ParsedElement {
1832
1999
  }
1833
2000
  });
1834
2001
  this.#input.addEventListener('input', e => {
2002
+ e.stopPropagation();
2003
+ if(this.disabled || this.readonly){
2004
+ return;
2005
+ }
1835
2006
  dload();
1836
2007
  });
1837
2008
  this.#ddmenu.addEventListener('change', (e) => {
2009
+ e.stopPropagation();
1838
2010
  if (!this.#multiple) {
1839
2011
  this.#values.clear();
1840
2012
  }
1841
- 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();
1842
2015
  this.#syncBadges();
1843
2016
  this.#input.focus();
1844
2017
  this.#ddmenu.hide();
1845
2018
  });
1846
2019
  this.replaceChildren(fragment);
1847
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
+ }
1848
2033
  #syncBadges() {
1849
2034
  const badges = Array.from(this.#values.entries()).map(([k, v]) => {
1850
2035
  const b = document.createElement('badge');
@@ -1856,14 +2041,14 @@ class Select extends ParsedElement {
1856
2041
  this.#badges.innerHTML = '';
1857
2042
  this.#badges.append(...badges);
1858
2043
  }
1859
- set value(value) {
1860
- if(value === null){
2044
+ set value(vs) {
2045
+ if(vs === null){
1861
2046
  this.#values = new Map();
1862
2047
  this.#syncBadges();
1863
2048
  return;
1864
2049
  }
1865
2050
  (async () => {
1866
- 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));
1867
2052
  this.#values = new Map(entries);
1868
2053
  this.#syncBadges();
1869
2054
  })();
@@ -1874,6 +2059,25 @@ class Select extends ParsedElement {
1874
2059
  }
1875
2060
  return [...this.#values.keys()][0] ?? null;
1876
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
+ }
1877
2081
  focus(options) {
1878
2082
  this.#input.focus(options);
1879
2083
  }
@@ -2539,21 +2743,12 @@ class InstantFilter extends ParsedElement {
2539
2743
  }
2540
2744
  const [operator, ...values] = v;
2541
2745
  this.#operator.setAttribute('value', operator);
2542
- this.#value1.value = values[0] ? InstantFilter.isoToLocal(values[0]) : values[0];
2543
- 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];
2544
2748
  this.reflect(() => {
2545
2749
  this.setAttribute('value', JSON.stringify(v));
2546
2750
  });
2547
2751
  }
2548
-
2549
- static isoToLocal(iso) {
2550
- //this is so sad
2551
- const d = new Date(iso);
2552
- const pad = (n, v) => String(v).padStart(n, '0');
2553
- const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
2554
- const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
2555
- return `${date}T${time}`
2556
- }
2557
2752
  focus(options) {
2558
2753
  this.#value1.focus(options);
2559
2754
  }
@@ -2762,6 +2957,11 @@ class Plugin {
2762
2957
  .defineElement('ful-form', Form)
2763
2958
  .defineElement('ful-checkbox', Checkbox)
2764
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)
2765
2965
  .defineElement('ful-radio-group', RadioGroup)
2766
2966
  .defineElement('ful-table', Table)
2767
2967
  .defineElement('ful-pagination', Pagination)
@@ -2777,5 +2977,5 @@ class Plugin {
2777
2977
  }
2778
2978
  }
2779
2979
 
2780
- 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 };
2781
2981
  //# sourceMappingURL=ful.mjs.map