@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.css +1 -1
- package/dist/ful.css.map +1 -1
- package/dist/ful.iife.js +346 -145
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +339 -144
- package/dist/ful.mjs.map +1 -1
- package/package.json +2 -2
package/dist/ful.mjs
CHANGED
|
@@ -721,69 +721,82 @@ class HttpMultipartRequestCustomizer {
|
|
|
721
721
|
}
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
-
class Storage {
|
|
725
|
-
|
|
726
|
-
|
|
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 =
|
|
734
|
-
return 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
|
-
|
|
732
|
+
static remove(k) {
|
|
733
|
+
localStorage.removeItem(k);
|
|
738
734
|
}
|
|
739
|
-
pop(k) {
|
|
740
|
-
const decoded =
|
|
741
|
-
|
|
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
|
-
|
|
754
|
-
|
|
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
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
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
|
-
|
|
1027
|
-
sleep(ms) {
|
|
1038
|
+
class Timing {
|
|
1039
|
+
static sleep(ms) {
|
|
1028
1040
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1029
|
-
}
|
|
1030
|
-
DEBOUNCE_DEFAULT
|
|
1031
|
-
DEBOUNCE_IMMEDIATE
|
|
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 ??
|
|
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 !==
|
|
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 ===
|
|
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
|
|
1078
|
-
THROTTLE_NO_LEADING
|
|
1079
|
-
THROTTLE_NO_TRAILING
|
|
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
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1092
|
+
* @param {*} timeoutMs
|
|
1093
|
+
* @param {*} func
|
|
1094
|
+
* @param {*} [options]
|
|
1095
|
+
* @returns {[function, function]}
|
|
1085
1096
|
*/
|
|
1086
|
-
throttle(timeoutMs, func, options) {
|
|
1087
|
-
const opts = options ??
|
|
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 &
|
|
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 &
|
|
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 &
|
|
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
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1551
|
-
|
|
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
|
|
1703
|
+
class PartialRemoteLoader {
|
|
1562
1704
|
#http;
|
|
1563
1705
|
#url;
|
|
1564
1706
|
#method;
|
|
1565
1707
|
#responseMapper;
|
|
1566
|
-
|
|
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
|
|
1736
|
+
class InMemoryLoader {
|
|
1595
1737
|
#data
|
|
1596
1738
|
constructor(data) {
|
|
1597
1739
|
this.#data = data;
|
|
1598
1740
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1741
|
+
update(data) {
|
|
1742
|
+
this.#data = data;
|
|
1601
1743
|
}
|
|
1602
|
-
|
|
1603
|
-
return this.#data.filter(([k, v]) =>
|
|
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
|
|
1760
|
+
return new InMemoryLoader(data);
|
|
1616
1761
|
}
|
|
1617
1762
|
const chunked = "chunked" == conf.el.getAttribute("mode");
|
|
1618
|
-
return chunked ?
|
|
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",
|
|
1810
|
+
el.setAttribute("value", i);
|
|
1664
1811
|
el.innerText = v;
|
|
1665
1812
|
return el;
|
|
1666
1813
|
}));
|
|
1667
1814
|
}
|
|
1668
1815
|
#change(target) {
|
|
1669
|
-
const
|
|
1670
|
-
const
|
|
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: {
|
|
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 (
|
|
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
|
-
<
|
|
1723
|
-
|
|
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] =
|
|
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.
|
|
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(
|
|
1865
|
-
if(
|
|
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(...
|
|
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] ?
|
|
2548
|
-
this.#value2.value = 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,
|
|
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
|