@optionfactory/ful 0.103.0 → 1.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-client-errors.iife.js +11 -11
- package/dist/ful-client-errors.iife.js.map +1 -1
- package/dist/ful-client-errors.iife.min.js.map +1 -1
- package/dist/ful.css +1 -1
- package/dist/ful.css.map +1 -1
- package/dist/ful.iife.js +1337 -294
- 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 +1324 -292
- package/dist/ful.mjs.map +1 -1
- package/package.json +6 -7
package/dist/ful.iife.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var ful = (function (exports, ftl
|
|
1
|
+
var ful = (function (exports, ftl) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
class Base64 {
|
|
@@ -66,10 +66,10 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
66
66
|
}
|
|
67
67
|
static encode(bytes, upper) {
|
|
68
68
|
return Array.from(bytes)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
.map(b => b.toString(16))
|
|
70
|
+
.map(b => upper ? b.toUpperCase() : b)
|
|
71
|
+
.map(o => o.padStart(2, 0))
|
|
72
|
+
.join('');
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -121,7 +121,6 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
125
124
|
/**
|
|
126
125
|
* @typedef {Int8Array| Uint8Array| Uint8ClampedArray| Int16Array| Uint16Array| Int32Array| Uint32Array| Float32Array| Float64Array| BigInt64Array| BigUint64Array} TypedArray
|
|
127
126
|
*/
|
|
@@ -390,7 +389,6 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
390
389
|
}
|
|
391
390
|
};
|
|
392
391
|
|
|
393
|
-
|
|
394
392
|
class HttpRequestBuilder {
|
|
395
393
|
#client;
|
|
396
394
|
#method;
|
|
@@ -451,7 +449,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
451
449
|
this.#headers.delete(k);
|
|
452
450
|
} else {
|
|
453
451
|
this.#headers.set(k, v);
|
|
454
|
-
}
|
|
452
|
+
}
|
|
455
453
|
}
|
|
456
454
|
return this;
|
|
457
455
|
}
|
|
@@ -485,16 +483,17 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
485
483
|
return this;
|
|
486
484
|
}
|
|
487
485
|
/**
|
|
488
|
-
* Adds a query parameter to the request, overriding it if it already exists.
|
|
486
|
+
* Adds a query parameter to the request, overriding it if it already exists. Empty vs, or a single null or undefined value cause the key to be removed.
|
|
489
487
|
* @param {string} k
|
|
490
|
-
* @param {string}
|
|
488
|
+
* @param {...string} vs
|
|
491
489
|
* @returns {HttpRequestBuilder} this builder
|
|
492
490
|
*/
|
|
493
|
-
param(k,
|
|
494
|
-
if (
|
|
491
|
+
param(k, ...vs) {
|
|
492
|
+
if (vs.length === 0 || vs[0] === null || vs[0] === undefined) {
|
|
495
493
|
this.#params.delete(k);
|
|
496
|
-
}
|
|
497
|
-
|
|
494
|
+
}
|
|
495
|
+
for (const v of vs) {
|
|
496
|
+
this.#params.append(k, v);
|
|
498
497
|
}
|
|
499
498
|
return this;
|
|
500
499
|
}
|
|
@@ -539,7 +538,6 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
539
538
|
*/
|
|
540
539
|
options(kvs) {
|
|
541
540
|
for (const [k, v] of Object.entries(kvs)) {
|
|
542
|
-
// @ts-ignore
|
|
543
541
|
this.#options[k] = v;
|
|
544
542
|
}
|
|
545
543
|
return this;
|
|
@@ -647,7 +645,6 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
647
645
|
}
|
|
648
646
|
}
|
|
649
647
|
|
|
650
|
-
|
|
651
648
|
class HttpMultipartRequestCustomizer {
|
|
652
649
|
#formData;
|
|
653
650
|
/**
|
|
@@ -744,14 +741,14 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
744
741
|
}
|
|
745
742
|
|
|
746
743
|
class VersionedStorage {
|
|
747
|
-
constructor(storage, key, dataSupplier){
|
|
744
|
+
constructor(storage, key, dataSupplier) {
|
|
748
745
|
this.storage = storage;
|
|
749
746
|
this.key = key;
|
|
750
747
|
this.dataSupplier = dataSupplier;
|
|
751
748
|
this.cache = null;
|
|
752
|
-
|
|
749
|
+
|
|
753
750
|
}
|
|
754
|
-
async load(revision){
|
|
751
|
+
async load(revision) {
|
|
755
752
|
const saved = this.storage.load(this.key);
|
|
756
753
|
if (!!saved && saved.revision === revision) {
|
|
757
754
|
this.cache = saved.value;
|
|
@@ -764,13 +761,13 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
764
761
|
});
|
|
765
762
|
this.cache = freshData;
|
|
766
763
|
}
|
|
767
|
-
data(){
|
|
764
|
+
data() {
|
|
768
765
|
return this.cache;
|
|
769
766
|
}
|
|
770
767
|
}
|
|
771
768
|
|
|
772
769
|
class AuthorizationCodeFlow {
|
|
773
|
-
static forKeycloak(clientId, realmBaseUrl, redirectUri){
|
|
770
|
+
static forKeycloak(clientId, realmBaseUrl, redirectUri) {
|
|
774
771
|
const scope = "openid profile";
|
|
775
772
|
return new AuthorizationCodeFlow(clientId, scope, {
|
|
776
773
|
auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
|
|
@@ -778,15 +775,15 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
778
775
|
logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
|
|
779
776
|
registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
|
|
780
777
|
redirect: redirectUri
|
|
781
|
-
});
|
|
778
|
+
});
|
|
782
779
|
}
|
|
783
|
-
constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
|
|
780
|
+
constructor(clientId, scope, { auth, token, registration, logout, redirect }) {
|
|
784
781
|
this.storage = new SessionStorage(clientId);
|
|
785
782
|
this.clientId = clientId;
|
|
786
783
|
this.scope = scope;
|
|
787
|
-
this.uri = {auth, token, registration, logout, redirect};
|
|
784
|
+
this.uri = { auth, token, registration, logout, redirect };
|
|
788
785
|
}
|
|
789
|
-
async action(uri, additionalParams){
|
|
786
|
+
async action(uri, additionalParams) {
|
|
790
787
|
const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
|
|
791
788
|
const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
|
|
792
789
|
const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
|
|
@@ -807,10 +804,10 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
807
804
|
});
|
|
808
805
|
window.location.href = url.toString();
|
|
809
806
|
}
|
|
810
|
-
async registration(additionalParams){
|
|
807
|
+
async registration(additionalParams) {
|
|
811
808
|
await this.action(this.uri.registration, additionalParams);
|
|
812
809
|
}
|
|
813
|
-
async applicationInitiatedAction(kcAction, additionalParams){
|
|
810
|
+
async applicationInitiatedAction(kcAction, additionalParams) {
|
|
814
811
|
await this.action(this.uri.auth, {
|
|
815
812
|
...additionalParams,
|
|
816
813
|
kc_action: kcAction,
|
|
@@ -867,8 +864,8 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
867
864
|
payload: JSON.parse(utf8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
|
|
868
865
|
signature: signature
|
|
869
866
|
};
|
|
870
|
-
}
|
|
871
|
-
constructor(clientId, t, {token, logout, redirect}) {
|
|
867
|
+
}
|
|
868
|
+
constructor(clientId, t, { token, logout, redirect }) {
|
|
872
869
|
this.clientId = clientId;
|
|
873
870
|
this.token = t;
|
|
874
871
|
this.idToken = AuthorizationCodeFlowSession.parseToken(t.id_token);
|
|
@@ -928,9 +925,9 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
928
925
|
bearerToken() {
|
|
929
926
|
return `Bearer ${this.token.access_token}`;
|
|
930
927
|
}
|
|
931
|
-
|
|
932
|
-
interceptor(gracePeriodBefore, gracePeriodAfter){
|
|
933
|
-
return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
|
|
928
|
+
|
|
929
|
+
interceptor(gracePeriodBefore, gracePeriodAfter) {
|
|
930
|
+
return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
|
|
934
931
|
}
|
|
935
932
|
}
|
|
936
933
|
|
|
@@ -958,11 +955,21 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
958
955
|
},
|
|
959
956
|
DEBOUNCE_DEFAULT: 0,
|
|
960
957
|
DEBOUNCE_IMMEDIATE: 1,
|
|
958
|
+
/**
|
|
959
|
+
* Executes only after a period of inactivity (pause in events).
|
|
960
|
+
* Delays execution until events stop for a set duration.
|
|
961
|
+
* Consolidates multiple rapid events into a single execution.
|
|
962
|
+
* Respond to the "end" of a series of events.
|
|
963
|
+
* @param {*} timeoutMs
|
|
964
|
+
* @param {*} func
|
|
965
|
+
* @param {*} [options]
|
|
966
|
+
* @returns {[function, function]}
|
|
967
|
+
*/
|
|
961
968
|
debounce(timeoutMs, func, options) {
|
|
969
|
+
const opts = options ?? timing.DEBOUNCE_DEFAULT;
|
|
962
970
|
let tid = null;
|
|
963
971
|
let args = [];
|
|
964
972
|
let previousTimestamp = 0;
|
|
965
|
-
let opts = options || timing.DEBOUNCE_DEFAULT;
|
|
966
973
|
|
|
967
974
|
const later = () => {
|
|
968
975
|
const elapsed = new Date().getTime() - previousTimestamp;
|
|
@@ -980,7 +987,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
980
987
|
}
|
|
981
988
|
};
|
|
982
989
|
|
|
983
|
-
|
|
990
|
+
const debounced = function () {
|
|
984
991
|
args = [...arguments];
|
|
985
992
|
previousTimestamp = new Date().getTime();
|
|
986
993
|
if (tid === null) {
|
|
@@ -990,15 +997,23 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
990
997
|
}
|
|
991
998
|
}
|
|
992
999
|
};
|
|
1000
|
+
const abort = () => clearTimeout(tid);
|
|
1001
|
+
return [debounced, abort];
|
|
993
1002
|
},
|
|
994
1003
|
THROTTLE_DEFAULT: 0,
|
|
995
1004
|
THROTTLE_NO_LEADING: 1,
|
|
996
1005
|
THROTTLE_NO_TRAILING: 2,
|
|
1006
|
+
/**
|
|
1007
|
+
* Executes at most once per specified time interval, regardless of ongoing events.
|
|
1008
|
+
* Executes regularly as long as events are firing, but at a controlled rate.
|
|
1009
|
+
* Allows execution periodically during a burst of events.
|
|
1010
|
+
* Ensure a function doesn't fire too frequently during continuous events.
|
|
1011
|
+
*/
|
|
997
1012
|
throttle(timeoutMs, func, options) {
|
|
1013
|
+
const opts = options ?? timing.THROTTLE_DEFAULT;
|
|
998
1014
|
let tid = null;
|
|
999
1015
|
let args = [];
|
|
1000
1016
|
let previousTimestamp = 0;
|
|
1001
|
-
let opts = options || timing.THROTTLE_DEFAULT;
|
|
1002
1017
|
|
|
1003
1018
|
const later = () => {
|
|
1004
1019
|
previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
|
|
@@ -1008,8 +1023,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1008
1023
|
args = [];
|
|
1009
1024
|
}
|
|
1010
1025
|
};
|
|
1011
|
-
|
|
1012
|
-
return function () {
|
|
1026
|
+
const throttled = function () {
|
|
1013
1027
|
const now = new Date().getTime();
|
|
1014
1028
|
if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
|
|
1015
1029
|
previousTimestamp = now;
|
|
@@ -1030,15 +1044,23 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1030
1044
|
tid = setTimeout(later, remaining);
|
|
1031
1045
|
}
|
|
1032
1046
|
};
|
|
1033
|
-
|
|
1047
|
+
const abort = () => clearTimeout(tid);
|
|
1048
|
+
return [throttled, abort];
|
|
1034
1049
|
}
|
|
1035
1050
|
};
|
|
1036
1051
|
|
|
1037
|
-
class
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1052
|
+
class Loaders {
|
|
1053
|
+
static fromAttributes(el, defaultLoader, options) {
|
|
1054
|
+
const http = ftl.registry.component("http-client");
|
|
1055
|
+
const requestMapper = el.hasAttribute("request-mapper") ? ftl.registry.component(el.getAttribute("request-mapper")) : v => v;
|
|
1056
|
+
const responseMapper = el.hasAttribute("response-mapper") ? ftl.registry.component(el.getAttribute("response-mapper")) : v => v;
|
|
1057
|
+
const loaderClass = ftl.registry.component(el.getAttribute("loader") ?? defaultLoader);
|
|
1058
|
+
return loaderClass.create({
|
|
1059
|
+
el,
|
|
1060
|
+
http,
|
|
1061
|
+
requestMapper,
|
|
1062
|
+
responseMapper,
|
|
1063
|
+
options: options ?? {}
|
|
1042
1064
|
});
|
|
1043
1065
|
}
|
|
1044
1066
|
}
|
|
@@ -1117,13 +1139,15 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1117
1139
|
return el.value;
|
|
1118
1140
|
}
|
|
1119
1141
|
|
|
1120
|
-
|
|
1142
|
+
/**
|
|
1143
|
+
*
|
|
1144
|
+
* @param {HTMLFormElement} form
|
|
1145
|
+
* @returns
|
|
1146
|
+
*/
|
|
1147
|
+
static extractFrom(form){
|
|
1121
1148
|
let result = {};
|
|
1122
|
-
for(const el of
|
|
1123
|
-
if
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
if(ignoredChildrenSelector && el.dataset['fulBindInclude'] !== 'always' && el.closest(ignoredChildrenSelector) !== null){
|
|
1149
|
+
for(const el of form.elements){
|
|
1150
|
+
if(!el.hasAttribute("name") || el.matches(":disabled")){
|
|
1127
1151
|
continue;
|
|
1128
1152
|
}
|
|
1129
1153
|
result = Bindings.providePath(result, /** @type {string} */(el.getAttribute('name')), Bindings.extract(el));
|
|
@@ -1148,70 +1172,139 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1148
1172
|
el.value = raw;
|
|
1149
1173
|
}
|
|
1150
1174
|
|
|
1151
|
-
static mutateIn(
|
|
1175
|
+
static mutateIn(form, values){
|
|
1152
1176
|
for (const [flattenedKey, value] of Object.entries(Bindings.flatten(values, ''))) {
|
|
1153
|
-
for(const el of
|
|
1177
|
+
for(const el of form.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`)){
|
|
1154
1178
|
Bindings.mutate(el, value);
|
|
1155
1179
|
}
|
|
1156
1180
|
}
|
|
1157
1181
|
}
|
|
1158
1182
|
|
|
1159
1183
|
|
|
1160
|
-
static errors(
|
|
1184
|
+
static errors(form, es, scrollOnError){
|
|
1161
1185
|
const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
1162
1186
|
const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
1163
|
-
|
|
1164
|
-
|
|
1187
|
+
form.querySelectorAll(`[name]`).forEach(el => el.setCustomValidity?.(""));
|
|
1188
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1165
1189
|
el.replaceChildren();
|
|
1166
1190
|
el.setAttribute('hidden', '');
|
|
1167
1191
|
});
|
|
1168
1192
|
fieldErrors.forEach(e => {
|
|
1169
|
-
const name = e.context.replace("[", ".").replace("].", ".");
|
|
1170
|
-
|
|
1171
|
-
root.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(invalidClass));
|
|
1172
|
-
const fieldErrorsSelector = `ful-field-error[field='${CSS.escape(name)}']`;
|
|
1173
|
-
root.querySelectorAll(fieldErrorsSelector).forEach(el => {
|
|
1174
|
-
const hel = /** @type HTMLElement} */ (el);
|
|
1175
|
-
hel.innerText = e.reason;
|
|
1176
|
-
});
|
|
1193
|
+
const name = e.context.replace("[", ".").replace("].", ".").replace("]", "");
|
|
1194
|
+
form.querySelectorAll(`[name='${CSS.escape(name)}']`).forEach(input => input.setCustomValidity?.(e.reason));
|
|
1177
1195
|
});
|
|
1178
|
-
|
|
1196
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1179
1197
|
const hel = /** @type HTMLElement} */ (el);
|
|
1180
1198
|
hel.innerText = globalErrors.map(e => e.reason).join("\n");
|
|
1181
1199
|
if (globalErrors.length !== 0) {
|
|
1182
1200
|
el.removeAttribute('hidden');
|
|
1183
1201
|
}
|
|
1184
1202
|
});
|
|
1203
|
+
if (es.length == 0 || !scrollOnError) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
Array.from(form.querySelectorAll(`:invalid`)).sort((a,b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y)[0]?.focus();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
class RemoteJsonFormLoader {
|
|
1211
|
+
#http;
|
|
1212
|
+
#url;
|
|
1213
|
+
#method;
|
|
1214
|
+
#requestMapper;
|
|
1215
|
+
#responseMapper;
|
|
1216
|
+
constructor(http, url, method, requestMapper, responseMapper) {
|
|
1217
|
+
this.#http = http;
|
|
1218
|
+
this.#url = url;
|
|
1219
|
+
this.#method = method;
|
|
1220
|
+
this.#requestMapper = requestMapper;
|
|
1221
|
+
this.#responseMapper = responseMapper;
|
|
1222
|
+
}
|
|
1223
|
+
prepare(values, form) {
|
|
1224
|
+
return this.#requestMapper(values, form);
|
|
1225
|
+
}
|
|
1226
|
+
async submit(values, form) {
|
|
1227
|
+
return await this.#http.request(this.#method, this.#url)
|
|
1228
|
+
.json(values)
|
|
1229
|
+
.fetch()
|
|
1230
|
+
}
|
|
1231
|
+
transform(response, form) {
|
|
1232
|
+
return this.#responseMapper(response, form);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1185
1235
|
|
|
1236
|
+
class LocalFormLoader {
|
|
1237
|
+
#requestMapper;
|
|
1238
|
+
#responseMapper;
|
|
1239
|
+
constructor(requestMapper, responseMapper) {
|
|
1240
|
+
this.#requestMapper = requestMapper;
|
|
1241
|
+
this.#responseMapper = responseMapper;
|
|
1242
|
+
}
|
|
1243
|
+
async prepare(values, form) {
|
|
1244
|
+
return await this.#requestMapper(values, form);
|
|
1245
|
+
}
|
|
1246
|
+
async submit(values, form) {
|
|
1247
|
+
return values;
|
|
1248
|
+
}
|
|
1249
|
+
async transform(response, form) {
|
|
1250
|
+
return await this.#responseMapper(response, form);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1186
1253
|
|
|
1254
|
+
class FormLoader {
|
|
1255
|
+
static create({ el, http, requestMapper, responseMapper }) {
|
|
1256
|
+
const url = el.getAttribute("action");
|
|
1257
|
+
if (!url) {
|
|
1258
|
+
return new LocalFormLoader(requestMapper, responseMapper);
|
|
1259
|
+
}
|
|
1260
|
+
const method = el.getAttribute("method") ?? 'POST';
|
|
1261
|
+
return new RemoteJsonFormLoader(http, url, method, requestMapper, responseMapper);
|
|
1187
1262
|
}
|
|
1188
1263
|
}
|
|
1189
1264
|
|
|
1190
|
-
class Form extends ftl.ParsedElement
|
|
1191
|
-
|
|
1192
|
-
static SCROLL_OFFSET = 50;
|
|
1193
|
-
static INVALID_CLASS = 'is-invalid';
|
|
1194
|
-
submitter;
|
|
1265
|
+
class Form extends ftl.ParsedElement {
|
|
1266
|
+
form;
|
|
1195
1267
|
render() {
|
|
1196
|
-
const form = document.createElement('form');
|
|
1268
|
+
const form = this.form = document.createElement('form');
|
|
1269
|
+
form.setAttribute("novalidate", "");
|
|
1197
1270
|
ftl.Attributes.forward('form-', this, form);
|
|
1198
1271
|
form.replaceChildren(...this.childNodes);
|
|
1199
1272
|
form.addEventListener('submit', async (e) => {
|
|
1200
1273
|
e.preventDefault();
|
|
1201
|
-
this.
|
|
1202
|
-
await this.submitter?.(this.values, this);
|
|
1203
|
-
});
|
|
1274
|
+
await this.submit();
|
|
1204
1275
|
});
|
|
1205
1276
|
if (this.hasAttribute("clear-invalid-on-change")) {
|
|
1206
|
-
this.addEventListener('change', evt => {
|
|
1207
|
-
|
|
1208
|
-
target?.querySelectorAll(`.${CSS.escape(Form.INVALID_CLASS)}`).forEach(el => {
|
|
1209
|
-
el.classList.remove(Form.INVALID_CLASS);
|
|
1210
|
-
});
|
|
1277
|
+
this.addEventListener('change', (/** @type any */evt) => {
|
|
1278
|
+
evt.target.setCustomValidity?.("");
|
|
1211
1279
|
});
|
|
1212
1280
|
}
|
|
1213
1281
|
this.replaceChildren(form);
|
|
1214
1282
|
}
|
|
1283
|
+
async submit() {
|
|
1284
|
+
this.spinner(true);
|
|
1285
|
+
try {
|
|
1286
|
+
const loader = Loaders.fromAttributes(this, 'loaders:form');
|
|
1287
|
+
const values = this.values;
|
|
1288
|
+
const request = await loader.prepare(values, this);
|
|
1289
|
+
const se = new CustomEvent('submit', { bubbles: true, cancelable: true, detail: { values, request } });
|
|
1290
|
+
if (!this.dispatchEvent(se)) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const response = await loader.submit(se.detail.request, this);
|
|
1295
|
+
const mapped = await loader.transform(response, this);
|
|
1296
|
+
this.dispatchEvent(new CustomEvent('submit:success', { bubbles: true, cancelable: false, detail: { values, request, response: mapped } }));
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
this.dispatchEvent(new CustomEvent('submit:failure', { bubbles: true, cancelable: false, detail: { values, request, exception: e } }));
|
|
1299
|
+
if (e instanceof Failure) {
|
|
1300
|
+
this.errors = e.problems;
|
|
1301
|
+
}
|
|
1302
|
+
throw e;
|
|
1303
|
+
}
|
|
1304
|
+
} finally {
|
|
1305
|
+
this.spinner(false);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1215
1308
|
spinner(spin) {
|
|
1216
1309
|
this.querySelectorAll('ful-spinner').forEach(el => {
|
|
1217
1310
|
const hel = /** @type HTMLElement */ (el);
|
|
@@ -1222,238 +1315,452 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1222
1315
|
hel.disabled = spin;
|
|
1223
1316
|
});
|
|
1224
1317
|
}
|
|
1225
|
-
async remoting(fn) {
|
|
1226
|
-
try {
|
|
1227
|
-
await fn();
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
if (e instanceof Failure) {
|
|
1230
|
-
this.errors = e.problems;
|
|
1231
|
-
}
|
|
1232
|
-
throw e;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
async spinningUntilError(fn) {
|
|
1236
|
-
this.spinner(true);
|
|
1237
|
-
try {
|
|
1238
|
-
await this.remoting(fn);
|
|
1239
|
-
} catch (e) {
|
|
1240
|
-
this.spinner(false);
|
|
1241
|
-
throw e;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
async spinning(fn) {
|
|
1245
|
-
this.spinner(true);
|
|
1246
|
-
try {
|
|
1247
|
-
await this.remoting(fn);
|
|
1248
|
-
} finally {
|
|
1249
|
-
this.spinner(false);
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
1318
|
set values(vs) {
|
|
1253
|
-
Bindings.mutateIn(this, vs);
|
|
1319
|
+
Bindings.mutateIn(this.form, vs);
|
|
1254
1320
|
}
|
|
1255
1321
|
get values() {
|
|
1256
|
-
return Bindings.extractFrom(this
|
|
1322
|
+
return Bindings.extractFrom(this.form);
|
|
1257
1323
|
}
|
|
1258
1324
|
set errors(es) {
|
|
1259
|
-
Bindings.errors(this, es,
|
|
1260
|
-
if (es.length == 0 || !this.hasAttribute('scroll-on-error')) {
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
const ys = Array.from(this.querySelectorAll(`ful-errors:not([hidden]), [ful-validated-field]:has(.${Form.INVALID_CLASS}) ful-field-error`))
|
|
1264
|
-
.map(el => el.parentElement ? el.parentElement : el)
|
|
1265
|
-
.map(el => el.getBoundingClientRect().y + window.scrollY);
|
|
1266
|
-
const miny = Math.min(...ys);
|
|
1267
|
-
if (miny !== Infinity) {
|
|
1268
|
-
window.scroll(window.scrollX, miny > Form.SCROLL_OFFSET ? miny - Form.SCROLL_OFFSET : 0);
|
|
1269
|
-
}
|
|
1325
|
+
Bindings.errors(this.form, es, this.hasAttribute('scroll-on-error'));
|
|
1270
1326
|
}
|
|
1271
1327
|
}
|
|
1272
1328
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
<
|
|
1278
|
-
<div
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
</div>
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
class Input extends ftl.ParsedElement({
|
|
1314
|
-
observed: ['value'],
|
|
1315
|
-
slots: true,
|
|
1316
|
-
template: INPUT_TEMPLATE
|
|
1317
|
-
}){
|
|
1318
|
-
input;
|
|
1319
|
-
render({slots}) {
|
|
1320
|
-
const fragment = makeInputFragment(this, this.template(), slots);
|
|
1329
|
+
class Input extends ftl.ParsedElement {
|
|
1330
|
+
static observed = ['value'];
|
|
1331
|
+
static slots = true;
|
|
1332
|
+
static template = `
|
|
1333
|
+
<label data-tpl-for="id" class="form-label">{{{{ slots.default }}}}</label>
|
|
1334
|
+
<div class="input-group">
|
|
1335
|
+
<span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
|
|
1336
|
+
{{{{ slots.before }}}}
|
|
1337
|
+
<input class="form-control" data-tpl-id="id" type="text" placeholder=" " data-tpl-aria-describedby="fieldErrorId" form="">
|
|
1338
|
+
{{{{ slots.after }}}}
|
|
1339
|
+
<span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
|
|
1340
|
+
</div>
|
|
1341
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1342
|
+
`;
|
|
1343
|
+
static formAssociated = true;
|
|
1344
|
+
#input;
|
|
1345
|
+
#fieldError;
|
|
1346
|
+
constructor() {
|
|
1347
|
+
super();
|
|
1348
|
+
this.internals = this.attachInternals();
|
|
1349
|
+
}
|
|
1350
|
+
render({ slots }) {
|
|
1351
|
+
const id = ftl.Attributes.uid('ful-input');
|
|
1352
|
+
const fieldErrorId = `${id}-error`;
|
|
1353
|
+
|
|
1354
|
+
const fragment = this.template().withOverlay({ id, fieldErrorId, slots }).render();
|
|
1355
|
+
this.#input = fragment.querySelector("input");
|
|
1356
|
+
ftl.Attributes.forward('input-', this, this.#input);
|
|
1357
|
+
this.#input.addEventListener('change', (evt) => {
|
|
1358
|
+
evt.stopPropagation();
|
|
1359
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1360
|
+
bubbles: true,
|
|
1361
|
+
cancelable: false,
|
|
1362
|
+
detail: {
|
|
1363
|
+
value: this.value
|
|
1364
|
+
}
|
|
1365
|
+
}));
|
|
1366
|
+
});
|
|
1367
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1321
1368
|
this.replaceChildren(fragment);
|
|
1322
1369
|
}
|
|
1323
1370
|
get value() {
|
|
1324
|
-
return this
|
|
1371
|
+
return this.#input.value;
|
|
1325
1372
|
}
|
|
1326
1373
|
set value(value) {
|
|
1327
|
-
this
|
|
1374
|
+
this.#input.value = value;
|
|
1375
|
+
}
|
|
1376
|
+
focus(options) {
|
|
1377
|
+
this.#input.focus(options);
|
|
1378
|
+
}
|
|
1379
|
+
setCustomValidity(error) {
|
|
1380
|
+
if (!error) {
|
|
1381
|
+
this.internals.setValidity({});
|
|
1382
|
+
this.#fieldError.innerText = "";
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1386
|
+
this.#fieldError.innerText = error;
|
|
1328
1387
|
}
|
|
1329
1388
|
}
|
|
1330
1389
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1390
|
+
class CompleteSelectLoader {
|
|
1391
|
+
#http;
|
|
1392
|
+
#url;
|
|
1393
|
+
#method;
|
|
1394
|
+
#responseMapper;
|
|
1395
|
+
#prefetch;
|
|
1396
|
+
#data;
|
|
1397
|
+
constructor(http, url, method, responseMapper, prefetch) {
|
|
1398
|
+
this.#http = http;
|
|
1399
|
+
this.#url = url;
|
|
1400
|
+
this.#method = method;
|
|
1401
|
+
this.#responseMapper = responseMapper;
|
|
1402
|
+
this.#prefetch = prefetch;
|
|
1403
|
+
this.#data = null;
|
|
1404
|
+
}
|
|
1405
|
+
async prefetch() {
|
|
1406
|
+
if (!this.#prefetch) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
await this.#ensureFetched();
|
|
1410
|
+
}
|
|
1411
|
+
async exact(...keys) {
|
|
1412
|
+
await this.#ensureFetched();
|
|
1413
|
+
return this.#data.filter(([k, v]) => keys.includes(k));
|
|
1414
|
+
}
|
|
1415
|
+
async load(needle) {
|
|
1416
|
+
await this.#ensureFetched();
|
|
1417
|
+
return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
|
|
1418
|
+
}
|
|
1419
|
+
async #ensureFetched() {
|
|
1420
|
+
if (this.#data !== null) {
|
|
1421
|
+
return
|
|
1422
|
+
}
|
|
1423
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1424
|
+
.fetchJson();
|
|
1425
|
+
this.#data = this.#responseMapper(data);
|
|
1426
|
+
}
|
|
1427
|
+
static create({ el, http, responseMapper }) {
|
|
1428
|
+
return new CompleteSelectLoader(
|
|
1429
|
+
http,
|
|
1430
|
+
el.getAttribute("src"),
|
|
1431
|
+
el.getAttribute("method") ?? 'POST',
|
|
1432
|
+
responseMapper,
|
|
1433
|
+
el.hasAttribute("preload")
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1335
1437
|
|
|
1336
|
-
class
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
})();
|
|
1369
|
-
input.setAttribute('ful-validation-target', '');
|
|
1438
|
+
class ChunkedSelectLoader {
|
|
1439
|
+
#http;
|
|
1440
|
+
#url;
|
|
1441
|
+
#method;
|
|
1442
|
+
#responseMapper;
|
|
1443
|
+
constructor(http, url, method, responseMapper) {
|
|
1444
|
+
this.#http = http;
|
|
1445
|
+
this.#url = url;
|
|
1446
|
+
this.#method = method;
|
|
1447
|
+
this.#responseMapper = responseMapper;
|
|
1448
|
+
}
|
|
1449
|
+
async exact(...keys) {
|
|
1450
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1451
|
+
.param("k", ...keys)
|
|
1452
|
+
.fetchJson();
|
|
1453
|
+
return this.#responseMapper(data);
|
|
1454
|
+
}
|
|
1455
|
+
async load(needle) {
|
|
1456
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1457
|
+
.param("s", needle)
|
|
1458
|
+
.fetchJson();
|
|
1459
|
+
return this.#responseMapper(data);
|
|
1460
|
+
}
|
|
1461
|
+
static create({ el, http, responseMapper }) {
|
|
1462
|
+
return new ChunkedSelectLoader(
|
|
1463
|
+
http,
|
|
1464
|
+
el.getAttribute("src"),
|
|
1465
|
+
el.getAttribute("method") ?? 'POST',
|
|
1466
|
+
responseMapper
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1370
1470
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1471
|
+
class OptionsSlotSelectLoader {
|
|
1472
|
+
#data
|
|
1473
|
+
constructor(data) {
|
|
1474
|
+
this.#data = data;
|
|
1475
|
+
}
|
|
1476
|
+
async exact(...keys) {
|
|
1477
|
+
await timing.sleep(500);
|
|
1478
|
+
return this.#data.filter(([k, v]) => keys.includes(k));
|
|
1479
|
+
}
|
|
1480
|
+
async load(needle) {
|
|
1481
|
+
await timing.sleep(500);
|
|
1482
|
+
return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1376
1485
|
|
|
1377
|
-
//tomselect needs the input to have a parent.
|
|
1378
|
-
//se we move the input to a fragment
|
|
1379
|
-
slots.input = ftl.Fragments.from(input);
|
|
1380
1486
|
|
|
1381
|
-
|
|
1487
|
+
class SelectLoader {
|
|
1488
|
+
static create(conf) {
|
|
1489
|
+
if (!conf.el.hasAttribute("src")) {
|
|
1490
|
+
const els = Array.from(conf.options.options?.querySelectorAll('option') ?? []);
|
|
1491
|
+
const data = els.map(e => {
|
|
1492
|
+
return [e.getAttribute("value") ?? e.innerText.trim(), e.innerText.trim()];
|
|
1493
|
+
});
|
|
1494
|
+
return new OptionsSlotSelectLoader(data);
|
|
1495
|
+
}
|
|
1496
|
+
const chunked = "chunked" == conf.el.getAttribute("mode");
|
|
1497
|
+
return chunked ? ChunkedSelectLoader.create(conf) : CompleteSelectLoader.create(conf);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1382
1500
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1501
|
+
class Dropdown extends ftl.ParsedElement {
|
|
1502
|
+
static slots = true
|
|
1503
|
+
static template = `
|
|
1504
|
+
<ful-spinner class="centered" hidden></ful-spinner>
|
|
1505
|
+
<menu tabindex="-1" hidden></menu>
|
|
1506
|
+
`;
|
|
1507
|
+
#spinner
|
|
1508
|
+
#menu
|
|
1509
|
+
render({ slots }) {
|
|
1510
|
+
const fragment = this.template().render();
|
|
1511
|
+
this.#spinner = fragment.querySelector("ful-spinner");
|
|
1512
|
+
this.#menu = fragment.querySelector("menu");
|
|
1513
|
+
this.#menu.addEventListener('click', evt => {
|
|
1514
|
+
evt.stopPropagation();
|
|
1515
|
+
if (!evt.target.matches('li')) {
|
|
1516
|
+
this.hide();
|
|
1517
|
+
return;
|
|
1386
1518
|
}
|
|
1387
|
-
|
|
1519
|
+
this.#change(evt.target);
|
|
1520
|
+
});
|
|
1521
|
+
this.replaceChildren(fragment);
|
|
1522
|
+
}
|
|
1523
|
+
acceptSelection() {
|
|
1524
|
+
const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
|
|
1525
|
+
this.#change(selected);
|
|
1526
|
+
}
|
|
1527
|
+
update(values) {
|
|
1528
|
+
if (values === undefined) {
|
|
1529
|
+
throw new Error("null data");
|
|
1530
|
+
}
|
|
1531
|
+
if (values.length === 0) {
|
|
1532
|
+
const el = document.createElement('div');
|
|
1533
|
+
el.classList.add('text-center', 'py-2', 'bi', 'bi-database-slash');
|
|
1534
|
+
this.#menu.replaceChildren(el);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
this.#menu.replaceChildren(...values.map(([k, v], i) => {
|
|
1538
|
+
const el = document.createElement('li');
|
|
1539
|
+
if (i === 0) {
|
|
1540
|
+
el.setAttribute("selected", '');
|
|
1541
|
+
}
|
|
1542
|
+
el.setAttribute("value", k);
|
|
1543
|
+
el.innerText = v;
|
|
1544
|
+
return el;
|
|
1545
|
+
}));
|
|
1546
|
+
}
|
|
1547
|
+
#change(target) {
|
|
1548
|
+
const value = target.getAttribute('value');
|
|
1549
|
+
const label = target.innerText;
|
|
1550
|
+
this.hide();
|
|
1551
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1552
|
+
bubbles: true,
|
|
1553
|
+
cancelable: false,
|
|
1554
|
+
detail: { label, value }
|
|
1555
|
+
}));
|
|
1556
|
+
}
|
|
1557
|
+
hide() {
|
|
1558
|
+
this.setAttribute('hidden', '');
|
|
1559
|
+
}
|
|
1560
|
+
get shown() {
|
|
1561
|
+
return !this.hasAttribute('hidden');
|
|
1562
|
+
}
|
|
1563
|
+
async show(loader) {
|
|
1564
|
+
this.removeAttribute('hidden');
|
|
1565
|
+
this.#menu.setAttribute('hidden', '');
|
|
1566
|
+
this.#spinner.removeAttribute('hidden');
|
|
1567
|
+
try {
|
|
1568
|
+
const data = await loader();
|
|
1569
|
+
this.update(data);
|
|
1570
|
+
} finally {
|
|
1571
|
+
this.#spinner.setAttribute('hidden', '');
|
|
1572
|
+
this.#menu.removeAttribute('hidden');
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
async moveOrShow(forward, loader) {
|
|
1576
|
+
if (!this.hasAttribute("hidden")) {
|
|
1577
|
+
const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
|
|
1578
|
+
const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
1579
|
+
if (candidate) {
|
|
1580
|
+
selected.removeAttribute('selected');
|
|
1581
|
+
candidate.setAttribute("selected", "");
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
await this.show(loader);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1388
1588
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1589
|
+
class Select extends ftl.ParsedElement {
|
|
1590
|
+
static observed = ['value:csvm']
|
|
1591
|
+
static slots = true
|
|
1592
|
+
static template = `
|
|
1593
|
+
<label data-tpl-for="id" class="form-label">{{{{ slots.default }}}}</label>
|
|
1594
|
+
<div class="input-group flex-nowrap" tabindex="-1">
|
|
1595
|
+
<span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
|
|
1596
|
+
{{{{ slots.before }}}}
|
|
1597
|
+
<div class="ful-select-input">
|
|
1598
|
+
<badges></badges>
|
|
1599
|
+
<input data-tpl-id="id" data-tpl-ariadesribed-by="fieldErrorId" type="text" form="">
|
|
1600
|
+
</div>
|
|
1601
|
+
{{{{ slots.after }}}}
|
|
1602
|
+
<span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
|
|
1603
|
+
</div>
|
|
1604
|
+
<ful-dropdown hidden></ful-dropdown>
|
|
1605
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1606
|
+
`;
|
|
1607
|
+
static mappers = {
|
|
1608
|
+
"csvm": (v, name, el) => {
|
|
1609
|
+
if (el.hasAttribute("multiple")) {
|
|
1610
|
+
return v === null ? [] : v.split(",").map(e => e.trim()).filter(e => e)
|
|
1611
|
+
}
|
|
1612
|
+
return v === null || v === '' ? null : v
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
static formAssociated = true
|
|
1616
|
+
internals
|
|
1617
|
+
#loader
|
|
1618
|
+
#badges
|
|
1619
|
+
#ddmenu
|
|
1620
|
+
#input
|
|
1621
|
+
#multiple
|
|
1622
|
+
#fieldError
|
|
1623
|
+
#values = new Map()
|
|
1624
|
+
constructor() {
|
|
1625
|
+
super();
|
|
1626
|
+
this.internals = this.attachInternals();
|
|
1627
|
+
}
|
|
1628
|
+
async render({ slots, observed }) {
|
|
1629
|
+
const name = this.getAttribute("name");
|
|
1630
|
+
const id = ftl.Attributes.uid('ful-select');
|
|
1631
|
+
const fieldErrorId = id + "-error";
|
|
1632
|
+
this.#loader = Loaders.fromAttributes(this, 'loaders:select', { options: slots.options });
|
|
1633
|
+
await this.#loader.prefetch?.();
|
|
1634
|
+
const fragment = this.template().withOverlay({ slots, name, id, fieldErrorId }).render();
|
|
1635
|
+
this.#input = fragment.querySelector('input');
|
|
1636
|
+
this.#badges = fragment.querySelector('badges');
|
|
1637
|
+
this.#ddmenu = fragment.querySelector('ful-dropdown');
|
|
1638
|
+
this.#multiple = this.hasAttribute("multiple");
|
|
1639
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1395
1640
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1641
|
+
const self = this;
|
|
1642
|
+
const [dload, abortdload] = timing.debounce(400, () => self.#ddmenu.show(() => self.#loader.load(self.#input.value)));
|
|
1643
|
+
this.addEventListener('click', (/** @type any */e) => {
|
|
1644
|
+
e.stopPropagation();
|
|
1645
|
+
if (e.target.matches('input')) {
|
|
1398
1646
|
return;
|
|
1399
1647
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (type !== 'id') {
|
|
1404
|
-
this.loaded = true;
|
|
1648
|
+
if (this.#ddmenu.shown) {
|
|
1649
|
+
this.#ddmenu.hide();
|
|
1650
|
+
return;
|
|
1405
1651
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1652
|
+
this.#input.focus();
|
|
1653
|
+
dload();
|
|
1654
|
+
});
|
|
1655
|
+
this.#badges.addEventListener('click', (e) => {
|
|
1656
|
+
e.stopPropagation();
|
|
1657
|
+
const idx = [...this.#badges.children].indexOf(e.target);
|
|
1658
|
+
if (idx === -1) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
this.#values.delete(Array.from(this.#values.keys()).pop());
|
|
1662
|
+
this.#syncBadges();
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
this.#input.addEventListener('blur', e => {
|
|
1666
|
+
if (e.relatedTarget && this.contains(e.relatedTarget)) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
abortdload();
|
|
1670
|
+
this.#ddmenu.hide();
|
|
1671
|
+
this.#input.value = '';
|
|
1672
|
+
});
|
|
1673
|
+
this.#input.addEventListener('keydown', e => {
|
|
1674
|
+
switch (e.code) {
|
|
1675
|
+
case 'ArrowUp': {
|
|
1676
|
+
this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
|
|
1677
|
+
break;
|
|
1419
1678
|
}
|
|
1420
|
-
|
|
1679
|
+
case 'ArrowDown': {
|
|
1680
|
+
this.#ddmenu.moveOrShow(true, () => self.#loader.load(self.#input.value));
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
case 'Escape': {
|
|
1684
|
+
this.#ddmenu.hide();
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
case 'Enter': {
|
|
1688
|
+
this.#ddmenu.acceptSelection();
|
|
1689
|
+
this.#input.value = '';
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
case 'Backspace': {
|
|
1693
|
+
//remove last if caret a position 0
|
|
1694
|
+
if (this.#values.size && this.#input.selectionStart === 0 && this.#input.selectionEnd === 0) {
|
|
1695
|
+
this.#values.delete(Array.from(this.#values.keys()).pop());
|
|
1696
|
+
this.#syncBadges();
|
|
1697
|
+
}
|
|
1698
|
+
break;
|
|
1699
|
+
}
|
|
1700
|
+
case 'Tab': {
|
|
1701
|
+
this.#ddmenu.hide();
|
|
1702
|
+
abortdload();
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1421
1706
|
});
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
evt.stopPropagation();
|
|
1707
|
+
this.#input.addEventListener('input', e => {
|
|
1708
|
+
dload();
|
|
1425
1709
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1710
|
+
this.#ddmenu.addEventListener('change', (e) => {
|
|
1711
|
+
if (!this.#multiple) {
|
|
1712
|
+
this.#values.clear();
|
|
1713
|
+
}
|
|
1714
|
+
this.#values.set(e.detail.value, e.detail.label);
|
|
1715
|
+
this.#syncBadges();
|
|
1716
|
+
this.#input.focus();
|
|
1717
|
+
this.#ddmenu.hide();
|
|
1718
|
+
});
|
|
1719
|
+
this.replaceChildren(fragment);
|
|
1436
1720
|
}
|
|
1437
|
-
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1721
|
+
#syncBadges() {
|
|
1722
|
+
const badges = Array.from(this.#values.entries()).map(([k, v]) => {
|
|
1723
|
+
const b = document.createElement('badge');
|
|
1724
|
+
b.setAttribute("role", "button");
|
|
1725
|
+
b.setAttribute("value", k);
|
|
1726
|
+
b.innerText = v;
|
|
1727
|
+
return b;
|
|
1728
|
+
});
|
|
1729
|
+
this.#badges.innerHTML = '';
|
|
1730
|
+
this.#badges.append(...badges);
|
|
1440
1731
|
}
|
|
1441
1732
|
set value(value) {
|
|
1442
1733
|
(async () => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
const silent = true;
|
|
1447
|
-
this.ts.setValue(value, silent);
|
|
1734
|
+
const entries = await (this.#multiple ? this.#loader.exact(...value) : this.#loader.exact(value));
|
|
1735
|
+
this.#values = new Map(entries);
|
|
1736
|
+
this.#syncBadges();
|
|
1448
1737
|
})();
|
|
1449
1738
|
}
|
|
1739
|
+
get value() {
|
|
1740
|
+
if (this.#multiple) {
|
|
1741
|
+
return [...this.#values.keys()];
|
|
1742
|
+
}
|
|
1743
|
+
return [...this.#values.keys()][0] ?? null;
|
|
1744
|
+
}
|
|
1745
|
+
focus(options) {
|
|
1746
|
+
this.#input.focus(options);
|
|
1747
|
+
}
|
|
1748
|
+
setCustomValidity(error) {
|
|
1749
|
+
if (!error) {
|
|
1750
|
+
this.internals.setValidity({});
|
|
1751
|
+
this.#fieldError.innerText = "";
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1755
|
+
this.#fieldError.innerText = error;
|
|
1756
|
+
}
|
|
1450
1757
|
}
|
|
1451
1758
|
|
|
1452
|
-
class RadioGroup extends ftl.ParsedElement
|
|
1453
|
-
observed
|
|
1454
|
-
slots
|
|
1455
|
-
template
|
|
1456
|
-
<fieldset
|
|
1759
|
+
class RadioGroup extends ftl.ParsedElement {
|
|
1760
|
+
static observed = ['value'];
|
|
1761
|
+
static slots = true;
|
|
1762
|
+
static template = `
|
|
1763
|
+
<fieldset data-tpl-aria-describedby="fieldErrorId">
|
|
1457
1764
|
<legend class="form-label">
|
|
1458
1765
|
{{{{ slots.default }}}}
|
|
1459
1766
|
</legend>
|
|
@@ -1468,14 +1775,20 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1468
1775
|
</label>
|
|
1469
1776
|
</div>
|
|
1470
1777
|
</section>
|
|
1471
|
-
<ful-field-error data-tpl-
|
|
1778
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1472
1779
|
<footer data-tpl-if="slots.footer">
|
|
1473
1780
|
{{{{ slots.footer }}}}
|
|
1474
1781
|
</footer>
|
|
1475
1782
|
</fieldset>
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1783
|
+
`;
|
|
1784
|
+
static formAssociated = true;
|
|
1785
|
+
#fieldError;
|
|
1786
|
+
#firstRadio;
|
|
1787
|
+
constructor() {
|
|
1788
|
+
super();
|
|
1789
|
+
this.internals = this.attachInternals();
|
|
1790
|
+
}
|
|
1791
|
+
render({ slots }) {
|
|
1479
1792
|
const name = this.getAttribute('name') ?? ftl.Attributes.uid('ful-radiogroup');
|
|
1480
1793
|
const radioEls = Array.from(slots.default.querySelectorAll('ful-radio'));
|
|
1481
1794
|
const inputsAndLabels = radioEls.map(el => {
|
|
@@ -1484,8 +1797,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1484
1797
|
ftl.Attributes.forward('input-', this, input);
|
|
1485
1798
|
ftl.Attributes.forward('', el, input);
|
|
1486
1799
|
input.setAttribute('name', `${name}-ignore`);
|
|
1487
|
-
input.setAttribute('
|
|
1488
|
-
input.dataset['fulBindInclude'] = 'never';
|
|
1800
|
+
input.setAttribute('form', ``);
|
|
1489
1801
|
input.addEventListener('change', evt => {
|
|
1490
1802
|
evt.stopPropagation();
|
|
1491
1803
|
//change is not cancelable
|
|
@@ -1502,14 +1814,11 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1502
1814
|
});
|
|
1503
1815
|
|
|
1504
1816
|
radioEls.forEach(el => el.remove());
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1817
|
+
const fieldErrorId = ftl.Attributes.uid("ful-error");
|
|
1818
|
+
this.template().withOverlay({ name, fieldErrorId, slots, inputsAndLabels }).renderTo(this);
|
|
1819
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
1820
|
+
this.#firstRadio = this.querySelector('input[type=radio]');
|
|
1509
1821
|
}
|
|
1510
|
-
set disabled(value) {
|
|
1511
|
-
this.reflect(() => ftl.Attributes.toggle(this, 'disabled', value));
|
|
1512
|
-
}
|
|
1513
1822
|
get value() {
|
|
1514
1823
|
/** @type {HTMLInputElement|null} */
|
|
1515
1824
|
const checked = this.querySelector('input[type=radio]:checked');
|
|
@@ -1528,46 +1837,780 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1528
1837
|
el.checked = true;
|
|
1529
1838
|
}
|
|
1530
1839
|
}
|
|
1840
|
+
focus(options) {
|
|
1841
|
+
this.#firstRadio.focus(options);
|
|
1842
|
+
}
|
|
1843
|
+
setCustomValidity(error) {
|
|
1844
|
+
if (!error) {
|
|
1845
|
+
this.internals.setValidity({});
|
|
1846
|
+
this.#fieldError.innerText = "";
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1850
|
+
this.#fieldError.innerText = error;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
class Checkbox extends ftl.ParsedElement {
|
|
1855
|
+
static observed = ['value:bool'];
|
|
1856
|
+
static slots = true;
|
|
1857
|
+
static template = `
|
|
1858
|
+
<div data-tpl-class="klass">
|
|
1859
|
+
<input dat-tpl-id="id" class="form-check-input" type="checkbox" role="switch" form="" placeholder=" " data-tpl-aria-describedby="fieldErrorId">
|
|
1860
|
+
<label data-tpl-for="id" class="form-check-label">{{{{ slots.default }}}}</label>
|
|
1861
|
+
</div>
|
|
1862
|
+
<ful-field-error data-tpl-if="fieldErrorId"></ful-field-error>
|
|
1863
|
+
`;
|
|
1864
|
+
#input;
|
|
1865
|
+
#fieldError;
|
|
1866
|
+
static formAssociated = true;
|
|
1867
|
+
constructor() {
|
|
1868
|
+
super();
|
|
1869
|
+
this.internals = this.attachInternals();
|
|
1870
|
+
}
|
|
1871
|
+
render({ slots }) {
|
|
1872
|
+
const id = ftl.Attributes.uid("ful-checkbox");
|
|
1873
|
+
const fieldErrorId = id + "-error";
|
|
1874
|
+
const klass = this.getAttribute('type') == 'switch' ? "form-check form-switch" : "form-check";
|
|
1875
|
+
const fragment = this.template().withOverlay({ slots, klass, id, fieldErrorId }).render();
|
|
1876
|
+
this.#input = fragment.querySelector("input");
|
|
1877
|
+
ftl.Attributes.forward('input-', this, this.#input);
|
|
1878
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1879
|
+
this.#input.addEventListener('change', (evt) => {
|
|
1880
|
+
evt.stopPropagation();
|
|
1881
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1882
|
+
bubbles: true,
|
|
1883
|
+
cancelable: false,
|
|
1884
|
+
detail: {
|
|
1885
|
+
value: this.value
|
|
1886
|
+
}
|
|
1887
|
+
}));
|
|
1888
|
+
});
|
|
1889
|
+
this.replaceChildren(fragment);
|
|
1890
|
+
}
|
|
1891
|
+
get value() {
|
|
1892
|
+
return this.#input.checked;
|
|
1893
|
+
}
|
|
1894
|
+
set value(value) {
|
|
1895
|
+
this.#input.checked = value;
|
|
1896
|
+
}
|
|
1897
|
+
focus(options) {
|
|
1898
|
+
this.#input.focus(options);
|
|
1899
|
+
}
|
|
1900
|
+
setCustomValidity(error) {
|
|
1901
|
+
if (!error) {
|
|
1902
|
+
this.internals.setValidity({});
|
|
1903
|
+
this.#fieldError.innerText = "";
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1907
|
+
this.#fieldError.innerText = error;
|
|
1908
|
+
}
|
|
1531
1909
|
}
|
|
1532
1910
|
|
|
1533
|
-
class Spinner extends ftl.ParsedElement
|
|
1534
|
-
slots
|
|
1535
|
-
template
|
|
1911
|
+
class Spinner extends ftl.ParsedElement {
|
|
1912
|
+
static slots = true;
|
|
1913
|
+
static template = `
|
|
1536
1914
|
<div class="ful-spinner-wrapper">
|
|
1537
1915
|
<div class="ful-spinner-text">{{{{ slots.default }}}}</div>
|
|
1538
1916
|
<div class="ful-spinner-icon"></div>
|
|
1539
1917
|
</div>
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
render({slots}) {
|
|
1918
|
+
`;
|
|
1919
|
+
render({ slots }) {
|
|
1543
1920
|
this.template().withOverlay({ slots }).renderTo(this);
|
|
1544
1921
|
}
|
|
1545
1922
|
}
|
|
1546
1923
|
|
|
1924
|
+
class SortButton extends ftl.ParsedElement {
|
|
1925
|
+
static observed = ["order"];
|
|
1926
|
+
#order;
|
|
1927
|
+
render() {
|
|
1928
|
+
const sorter = this.getAttribute("sorter");
|
|
1929
|
+
const orders = ["asc", "desc", null];
|
|
1930
|
+
this.addEventListener('click', () => {
|
|
1931
|
+
const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
|
|
1932
|
+
this.dispatchEvent(new CustomEvent('sort-requested', {
|
|
1933
|
+
bubbles: true,
|
|
1934
|
+
cancelable: true,
|
|
1935
|
+
detail: {
|
|
1936
|
+
value: { sorter, order: nextOrder }
|
|
1937
|
+
}
|
|
1938
|
+
}));
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
get order() {
|
|
1943
|
+
return this.#order || null;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
set order(value) {
|
|
1947
|
+
this.#order = value || null;
|
|
1948
|
+
this.reflect(() => {
|
|
1949
|
+
if (this.#order) {
|
|
1950
|
+
this.setAttribute("order", value);
|
|
1951
|
+
} else {
|
|
1952
|
+
this.removeAttribute("order");
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
class Pagination extends ftl.ParsedElement {
|
|
1959
|
+
static observed = ["total:number", "current:number"];
|
|
1960
|
+
static template = `
|
|
1961
|
+
<nav aria-label="Page navigation" class="user-select-none">
|
|
1962
|
+
<ul class="pagination">
|
|
1963
|
+
<li class="page-item ms-auto me-2" data-tpl-if="paginationLabel"> Showing page {{ curr.label }} of {{ total }}</li>
|
|
1964
|
+
<li class="page-item ms-auto me-2" data-tpl-if="!paginationLabel"></li>
|
|
1965
|
+
<li class="page-item">
|
|
1966
|
+
<a data-tpl-class="prev.enabled?'page-link':'page-link disabled'" aria-label="Previous" role="button" data-tpl-data-page="prev.index">
|
|
1967
|
+
<span aria-hidden="true">«</span>
|
|
1968
|
+
</a>
|
|
1969
|
+
</li>
|
|
1970
|
+
<li class="page-item" data-tpl-each="pages" data-tpl-var="page">
|
|
1971
|
+
<a data-tpl-class="curr.index != page.index ? 'page-link': 'page-link disabled'" role="button" data-tpl-data-page="page.index" >
|
|
1972
|
+
{{ page.label }}
|
|
1973
|
+
</a>
|
|
1974
|
+
</li>
|
|
1975
|
+
<li class="page-item">
|
|
1976
|
+
<a data-tpl-class="next.enabled?'page-link':'page-link disabled'" aria-label="Next" role="button" data-tpl-data-page="next.index">
|
|
1977
|
+
<span aria-hidden="true">»</span>
|
|
1978
|
+
</a>
|
|
1979
|
+
</li>
|
|
1980
|
+
</ul>
|
|
1981
|
+
</nav>
|
|
1982
|
+
`;
|
|
1983
|
+
#paginationLabel;
|
|
1984
|
+
#total = 0;
|
|
1985
|
+
#current = 0;
|
|
1986
|
+
render({ observed }) {
|
|
1987
|
+
this.#paginationLabel = this.hasAttribute('pagination-label');
|
|
1988
|
+
this.update(observed.current ?? 0, observed.total ?? 0);
|
|
1989
|
+
this.addEventListener('click', (/** @type any */evt) => {
|
|
1990
|
+
const el = evt.target.closest('a');
|
|
1991
|
+
if (!el) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
this.dispatchEvent(new CustomEvent('page-requested', {
|
|
1995
|
+
bubbles: true,
|
|
1996
|
+
cancelable: true,
|
|
1997
|
+
detail: {
|
|
1998
|
+
value: Number(el.dataset.page)
|
|
1999
|
+
}
|
|
2000
|
+
}));
|
|
2001
|
+
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
update(current, total) {
|
|
2005
|
+
const maxRender = Number(this.getAttribute('pages') ?? "5");
|
|
2006
|
+
const prev = { index: Math.max(0, current - 1), enabled: current > 0 };
|
|
2007
|
+
const curr = { index: current, label: current + 1 };
|
|
2008
|
+
const next = { index: Math.min(total, current + 1), enabled: current + 1 < total };
|
|
2009
|
+
const pages = [{
|
|
2010
|
+
index: current,
|
|
2011
|
+
label: current + 1
|
|
2012
|
+
}];
|
|
2013
|
+
for (let mid = current, offset = 1; offset !== maxRender && pages.length != maxRender; ++offset) {
|
|
2014
|
+
const p = mid - offset;
|
|
2015
|
+
if (p >= 0) {
|
|
2016
|
+
pages.unshift({ index: p, label: p + 1 });
|
|
2017
|
+
}
|
|
2018
|
+
const n = mid + offset;
|
|
2019
|
+
if (n < total) {
|
|
2020
|
+
pages.push({ index: n, label: n + 1 });
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const paginationLabel = this.#paginationLabel;
|
|
2024
|
+
this.template().withOverlay({ total, prev, curr, next, pages, paginationLabel }).renderTo(this);
|
|
2025
|
+
}
|
|
2026
|
+
get total() {
|
|
2027
|
+
return this.#total;
|
|
2028
|
+
}
|
|
2029
|
+
set total(value) {
|
|
2030
|
+
this.#total = value;
|
|
2031
|
+
this.reflect(() => {
|
|
2032
|
+
this.setAttribute('total', String(value));
|
|
2033
|
+
this.update(this.#current ?? 0, this.#total);
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
get current() {
|
|
2037
|
+
return this.#current;
|
|
2038
|
+
}
|
|
2039
|
+
set current(value) {
|
|
2040
|
+
this.#current = value;
|
|
2041
|
+
this.reflect(() => {
|
|
2042
|
+
this.setAttribute('current', String(value));
|
|
2043
|
+
this.update(this.#current, this.#total ?? 0);
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
class TableSchemaParser {
|
|
2049
|
+
static parse(nodeOrFragment, template) {
|
|
2050
|
+
const schema = ftl.Nodes.queryChildren(nodeOrFragment, "schema");
|
|
2051
|
+
if (!schema) {
|
|
2052
|
+
throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
|
|
2053
|
+
}
|
|
2054
|
+
return ftl.Nodes.queryChildrenAll(schema, "column")
|
|
2055
|
+
.map(el => {
|
|
2056
|
+
return {
|
|
2057
|
+
sorter: el.getAttribute("sorter"),
|
|
2058
|
+
order: el.getAttribute("order"),
|
|
2059
|
+
title: TableSchemaParser.#parseTitle(el, template),
|
|
2060
|
+
content: TableSchemaParser.#parseContent(el, template)
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
static #parseTitle(el, template) {
|
|
2066
|
+
const maybeTitleTag = ftl.Nodes.queryChildren(el, 'title');
|
|
2067
|
+
if (maybeTitleTag) {
|
|
2068
|
+
maybeTitleTag.remove();
|
|
2069
|
+
}
|
|
2070
|
+
const fragment = maybeTitleTag ? template.withFragment(ftl.Fragments.fromChildNodes(maybeTitleTag)).render() : document.createTextNode(el.getAttribute("title") ?? '');
|
|
2071
|
+
return {
|
|
2072
|
+
classes: el.getAttribute("th-class"),
|
|
2073
|
+
fragment
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
static #parseContent(el, template) {
|
|
2078
|
+
return {
|
|
2079
|
+
classes: el.getAttribute("td-class"),
|
|
2080
|
+
template: template.withFragment(ftl.Fragments.fromChildNodes(el))
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
class RemoteTableLoader{
|
|
2086
|
+
#http;
|
|
2087
|
+
#url;
|
|
2088
|
+
#method;
|
|
2089
|
+
constructor(http, url, method){
|
|
2090
|
+
this.#http = http;
|
|
2091
|
+
this.#url = url;
|
|
2092
|
+
this.#method = method;
|
|
2093
|
+
}
|
|
2094
|
+
async load(pageRequest, sortRequest, filterRequest){
|
|
2095
|
+
const filters = Object.entries(filterRequest).filter(([k, v]) => v);
|
|
2096
|
+
return await this.#http.request(this.#method, this.#url)
|
|
2097
|
+
.param("page", pageRequest.page)
|
|
2098
|
+
.param("size", pageRequest.size)
|
|
2099
|
+
.param("sort", sortRequest.order ? `${sortRequest.sorter},${sortRequest.order}` : null)
|
|
2100
|
+
.param("filters", filters.length > 0 ? JSON.stringify(Object.fromEntries(filters)) : null)
|
|
2101
|
+
.fetchJson();
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
class TableLoader{
|
|
2107
|
+
static create({el, http}){
|
|
2108
|
+
const url = el.getAttribute("src");
|
|
2109
|
+
const method = el.getAttribute("method") ?? 'GET';
|
|
2110
|
+
return new RemoteTableLoader(http, url, method);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
class Table extends ftl.ParsedElement {
|
|
2115
|
+
static slots = true;
|
|
2116
|
+
static template = `
|
|
2117
|
+
<ful-form data-tpl-if="slots.filters">
|
|
2118
|
+
{{{{ slots.filters }}}}
|
|
2119
|
+
</ful-form>
|
|
2120
|
+
<table class="table">
|
|
2121
|
+
<caption data-tpl-if="slots.caption">{{{{ slots.caption }}}}</caption>
|
|
2122
|
+
<thead>
|
|
2123
|
+
<tr>
|
|
2124
|
+
<th data-tpl-each="schema" scope="col" data-tpl-class="title.classes">
|
|
2125
|
+
{{{{ title.fragment }}}}
|
|
2126
|
+
<ful-sorter data-tpl-if="sorter || order" data-tpl-sorter="sorter" data-tpl-order="order"></ful-sorter>
|
|
2127
|
+
</th>
|
|
2128
|
+
</tr>
|
|
2129
|
+
</thead>
|
|
2130
|
+
<tbody></tbody>
|
|
2131
|
+
<tbody data-ref="no-autoload">
|
|
2132
|
+
<tr>
|
|
2133
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2134
|
+
<i class="bi bi-search" style="font-size: 40px; color: #BDC3CA"></i>
|
|
2135
|
+
<p class="mt-3 mb-0" style="color: #BDC3CA">
|
|
2136
|
+
Avvia la ricerca per visualizzare i risultati...
|
|
2137
|
+
</p>
|
|
2138
|
+
</td>
|
|
2139
|
+
</tr>
|
|
2140
|
+
</tbody>
|
|
2141
|
+
<tbody data-ref="loading" hidden>
|
|
2142
|
+
<tr>
|
|
2143
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2144
|
+
<ful-spinner class="big"></ful-spinner>
|
|
2145
|
+
</td>
|
|
2146
|
+
</tr>
|
|
2147
|
+
</tbody>
|
|
2148
|
+
<tbody data-ref="feedback" hidden>
|
|
2149
|
+
<tr>
|
|
2150
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2151
|
+
<div class="alert alert-danger">
|
|
2152
|
+
<p>Errore nel caricamento della tabella:</p>
|
|
2153
|
+
<p class="mb-0" data-ref="feedback-error"></p>
|
|
2154
|
+
</div>
|
|
2155
|
+
</td>
|
|
2156
|
+
</tr>
|
|
2157
|
+
</tbody>
|
|
2158
|
+
<tfoot data-tpl-if="slots.footer">
|
|
2159
|
+
{{{{ slots.footer }}}}
|
|
2160
|
+
</tfoot>
|
|
2161
|
+
</table>
|
|
2162
|
+
<ful-pagination current="0" total="1"></ful-pagination>
|
|
2163
|
+
`;
|
|
2164
|
+
static templates = {
|
|
2165
|
+
row: `
|
|
2166
|
+
<tr data-tpl-if="pageResponse.data.length == 0">
|
|
2167
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2168
|
+
Nessun elemento trovato.
|
|
2169
|
+
</td>
|
|
2170
|
+
</tr>
|
|
2171
|
+
<tr data-tpl-each="pageResponse.data" data-tpl-var="row">
|
|
2172
|
+
<td data-tpl-each="schema" data-tpl-class="content.classes">
|
|
2173
|
+
{{{{ content.template.withOverlay(row).render() }}}}
|
|
2174
|
+
</td>
|
|
2175
|
+
</tr>
|
|
2176
|
+
`
|
|
2177
|
+
};
|
|
2178
|
+
#schema;
|
|
2179
|
+
#body;
|
|
2180
|
+
#loading;
|
|
2181
|
+
#noAutoload;
|
|
2182
|
+
#feedback;
|
|
2183
|
+
#paginator;
|
|
2184
|
+
#sorters;
|
|
2185
|
+
#latestRequest;
|
|
2186
|
+
async render({ slots, observed }) {
|
|
2187
|
+
const template = this.template();
|
|
2188
|
+
const schema = TableSchemaParser.parse(slots.default, template);
|
|
2189
|
+
const fragment = template.withOverlay({ slots, schema }).render();
|
|
2190
|
+
const table = /** @type HTMLTableElement */ (ftl.Nodes.queryChildren(fragment, 'table'));
|
|
2191
|
+
ftl.Attributes.forward('table-', this, table);
|
|
2192
|
+
this.#schema = schema;
|
|
2193
|
+
this.#body = table.querySelector(':scope > tbody');
|
|
2194
|
+
this.#loading = table.querySelector(":scope > tbody[data-ref=loading]");
|
|
2195
|
+
this.#noAutoload = table.querySelector(":scope > tbody[data-ref=no-autoload]");
|
|
2196
|
+
this.#feedback = table.querySelector(":scope > tbody[data-ref=feedback]");
|
|
2197
|
+
this.#paginator = ftl.Nodes.queryChildren(fragment, 'ful-pagination');
|
|
2198
|
+
this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
|
|
2199
|
+
this.replaceChildren(fragment);
|
|
2200
|
+
await ftl.Nodes.waitForUpgrades();
|
|
2201
|
+
const orderFromSchema = schema.find(v => v.order);
|
|
2202
|
+
|
|
2203
|
+
const maybeForm = /** @type any */(ftl.Nodes.queryChildren(this, 'ful-form'));
|
|
2204
|
+
this.#latestRequest = {
|
|
2205
|
+
pageRequest: {
|
|
2206
|
+
page: 0,
|
|
2207
|
+
size: this.getAttribute("page-size") ? Number(this.getAttribute("page-size")) : 10
|
|
2208
|
+
},
|
|
2209
|
+
sortRequest: { order: orderFromSchema?.order, sorter: orderFromSchema?.sorter },
|
|
2210
|
+
filterRequest: maybeForm?.values ?? {}
|
|
2211
|
+
};
|
|
2212
|
+
maybeForm?.addEventListener('submit:success', async (evt) => {
|
|
2213
|
+
await this.load({
|
|
2214
|
+
page: 0,
|
|
2215
|
+
size: this.#latestRequest.pageRequest.size
|
|
2216
|
+
}, this.#latestRequest.sortRequest, evt.detail.request);
|
|
2217
|
+
});
|
|
2218
|
+
if (maybeForm) {
|
|
2219
|
+
maybeForm.submitter = async (filterRequest, form) => {
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
this.addEventListener('page-requested', async (/** @type any */e) => {
|
|
2223
|
+
await this.load({
|
|
2224
|
+
page: e.detail.value,
|
|
2225
|
+
size: this.#latestRequest.pageRequest.size
|
|
2226
|
+
}, this.#latestRequest.sortRequest, this.#latestRequest.filterRequest);
|
|
2227
|
+
});
|
|
2228
|
+
this.addEventListener('sort-requested', async (/** @type any */e) => {
|
|
2229
|
+
await this.load(this.#latestRequest.pageRequest, e.detail.value, this.#latestRequest.filterRequest);
|
|
2230
|
+
this.#sorters.forEach(s => s.order = null);
|
|
2231
|
+
e.target.order = e.detail.value.order;
|
|
2232
|
+
});
|
|
2233
|
+
if (this.hasAttribute('autoload')) {
|
|
2234
|
+
await this.reload();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
async reload() {
|
|
2239
|
+
return await this.load(this.#latestRequest.pageRequest, this.#latestRequest.sortRequest, this.#latestRequest.filterRequest);
|
|
2240
|
+
}
|
|
2241
|
+
async load(pageRequest, sortRequest, filterRequest) {
|
|
2242
|
+
this.#body.innerHTML = "";
|
|
2243
|
+
this.#loading.removeAttribute("hidden", "");
|
|
2244
|
+
this.#feedback.setAttribute("hidden", "");
|
|
2245
|
+
this.#noAutoload.setAttribute("hidden", "");
|
|
2246
|
+
try {
|
|
2247
|
+
const loader = Loaders.fromAttributes(this, 'loaders:table');
|
|
2248
|
+
const pageResponse =await loader.load(pageRequest, sortRequest, filterRequest);
|
|
2249
|
+
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
2250
|
+
this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
|
|
2251
|
+
} catch (/** @type any */error) {
|
|
2252
|
+
this.#loading.setAttribute("hidden", "");
|
|
2253
|
+
this.#feedback.removeAttribute("hidden", "");
|
|
2254
|
+
if (!error.problems) {
|
|
2255
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
|
|
2256
|
+
} else {
|
|
2257
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error.problems.map(p => `${p.reason}`);
|
|
2258
|
+
}
|
|
2259
|
+
throw error;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
async resetWithFilter(filterRequest) {
|
|
2264
|
+
return await this.load({
|
|
2265
|
+
page: 0,
|
|
2266
|
+
size: this.#latestRequest.pageRequest.size
|
|
2267
|
+
}, this.#latestRequest.sortRequest, filterRequest);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
#update(pageRequest, sortRequest, filterRequest, pageResponse) {
|
|
2271
|
+
this.#loading.setAttribute("hidden", "");
|
|
2272
|
+
this.#body.replaceChildren(this.template('row').withOverlay({
|
|
2273
|
+
schema: this.#schema,
|
|
2274
|
+
pageRequest,
|
|
2275
|
+
filterRequest,
|
|
2276
|
+
pageResponse
|
|
2277
|
+
}).render());
|
|
2278
|
+
this.#paginator.current = pageRequest.page;
|
|
2279
|
+
this.#paginator.total = Math.ceil(pageResponse.size / pageRequest.size);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
class InstantFilter extends ftl.ParsedElement {
|
|
2284
|
+
static observed = ["value:json"];
|
|
2285
|
+
static slots = true;
|
|
2286
|
+
static template = `
|
|
2287
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2288
|
+
<div class="input-group">
|
|
2289
|
+
<button data-ref="operator" class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" value="LTE" form="">≼</button>
|
|
2290
|
+
<ul class="dropdown-menu">
|
|
2291
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2292
|
+
<li><a class="dropdown-item" role="button" value="NEQ">≠</a></li>
|
|
2293
|
+
<li><a class="dropdown-item" role="button" value="LT">≺</a></li>
|
|
2294
|
+
<li><a class="dropdown-item" role="button" value="GT">≻</a></li>
|
|
2295
|
+
<li><a class="dropdown-item" role="button" value="LTE">≼</a></li>
|
|
2296
|
+
<li><a class="dropdown-item" role="button" value="GTE">≽</a></li>
|
|
2297
|
+
<li><a class="dropdown-item" role="button" value="BETWEEN">↔</a></li>
|
|
2298
|
+
</ul>
|
|
2299
|
+
<input data-tpl-id="id" data-ref="value1" type="datetime-local" class="form-control" form="">
|
|
2300
|
+
<input data-ref="value2" type="datetime-local" class="form-control" form="" hidden>
|
|
2301
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2302
|
+
</div>
|
|
2303
|
+
<ful-field-error></ful-field-error>
|
|
2304
|
+
`;
|
|
2305
|
+
static formAssociated = true;
|
|
2306
|
+
#operator;
|
|
2307
|
+
#value1;
|
|
2308
|
+
#value2;
|
|
2309
|
+
#fieldError;
|
|
2310
|
+
constructor() {
|
|
2311
|
+
super();
|
|
2312
|
+
this.internals = this.attachInternals();
|
|
2313
|
+
}
|
|
2314
|
+
render({ slots }) {
|
|
2315
|
+
const id = ftl.Attributes.uid('instant-filter');
|
|
2316
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2317
|
+
const name = this.getAttribute("name");
|
|
2318
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2319
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2320
|
+
this.#value1 = fragment.querySelector('[data-ref=value1]');
|
|
2321
|
+
this.#value2 = fragment.querySelector('[data-ref=value2]');
|
|
2322
|
+
this.replaceChildren(fragment);
|
|
2323
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2324
|
+
this.addEventListener('click', (evt) => {
|
|
2325
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
2326
|
+
if (!target.matches('ul > li > a')) {
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2330
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2331
|
+
ftl.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
2332
|
+
btn.setAttribute('value', value);
|
|
2333
|
+
btn.innerHTML = target.innerHTML;
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
get value() {
|
|
2338
|
+
const operator = this.#operator.getAttribute('value');
|
|
2339
|
+
const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
2340
|
+
return values.some(v => v === '') ? undefined : [operator, ...values.map(v => new Date(v).toISOString())];
|
|
2341
|
+
}
|
|
2342
|
+
set value(v) {
|
|
2343
|
+
if (v === null || v === undefined) {
|
|
2344
|
+
this.#value1.value = '';
|
|
2345
|
+
this.#value2.value = '';
|
|
2346
|
+
this.reflect(() => {
|
|
2347
|
+
this.removeAttribute('value');
|
|
2348
|
+
});
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const [operator, ...values] = v;
|
|
2352
|
+
this.#operator.setAttribute('value', operator);
|
|
2353
|
+
this.#value1.value = values[0] ? InstantFilter.isoToLocal(values[0]) : values[0];
|
|
2354
|
+
this.#value2.value = values[1] ? InstantFilter.isoToLocal(values[1]) : values[1];
|
|
2355
|
+
this.reflect(() => {
|
|
2356
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
static isoToLocal(iso) {
|
|
2361
|
+
//this is so sad
|
|
2362
|
+
const d = new Date(iso);
|
|
2363
|
+
const pad = (n, v) => String(v).padStart(n, '0');
|
|
2364
|
+
const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
|
|
2365
|
+
const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
|
|
2366
|
+
return `${date}T${time}`
|
|
2367
|
+
}
|
|
2368
|
+
focus(options) {
|
|
2369
|
+
this.#value1.focus(options);
|
|
2370
|
+
}
|
|
2371
|
+
setCustomValidity(error) {
|
|
2372
|
+
if (!error) {
|
|
2373
|
+
this.internals.setValidity({});
|
|
2374
|
+
this.#fieldError.innerText = "";
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2378
|
+
this.#fieldError.innerText = error;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
class LocalDateFilter extends ftl.ParsedElement {
|
|
2383
|
+
static observed = ["value:json"];
|
|
2384
|
+
static slots = true;
|
|
2385
|
+
static template = `
|
|
2386
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2387
|
+
<div class="input-group">
|
|
2388
|
+
<button data-ref="operator" class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" value="EQ" form="">=</button>
|
|
2389
|
+
<ul class="dropdown-menu">
|
|
2390
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2391
|
+
<li><a class="dropdown-item" role="button" value="NEQ">≠</a></li>
|
|
2392
|
+
<li><a class="dropdown-item" role="button" value="LT">≺</a></li>
|
|
2393
|
+
<li><a class="dropdown-item" role="button" value="GT">≻</a></li>
|
|
2394
|
+
<li><a class="dropdown-item" role="button" value="LTE">≼</a></li>
|
|
2395
|
+
<li><a class="dropdown-item" role="button" value="GTE">≽</a></li>
|
|
2396
|
+
<li><a class="dropdown-item" role="button" value="BETWEEN">↔</a></li>
|
|
2397
|
+
</ul>
|
|
2398
|
+
<input data-tpl-id="id" data-ref="value1" type="date" class="form-control" form="">
|
|
2399
|
+
<input data-ref="value2" type="date" class="form-control" form="" hidden>
|
|
2400
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2401
|
+
|
|
2402
|
+
</div>
|
|
2403
|
+
<ful-field-error></ful-field-error>
|
|
2404
|
+
`;
|
|
2405
|
+
static formAssociated = true;
|
|
2406
|
+
#operator;
|
|
2407
|
+
#value1;
|
|
2408
|
+
#value2;
|
|
2409
|
+
#fieldError;
|
|
2410
|
+
constructor() {
|
|
2411
|
+
super();
|
|
2412
|
+
this.internals = this.attachInternals();
|
|
2413
|
+
}
|
|
2414
|
+
render({ slots }) {
|
|
2415
|
+
const id = ftl.Attributes.uid('instant-filter');
|
|
2416
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2417
|
+
const name = this.getAttribute("name");
|
|
2418
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2419
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2420
|
+
this.#value1 = fragment.querySelector('[data-ref=value1]');
|
|
2421
|
+
this.#value2 = fragment.querySelector('[data-ref=value2]');
|
|
2422
|
+
this.replaceChildren(fragment);
|
|
2423
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2424
|
+
this.addEventListener('click', (evt) => {
|
|
2425
|
+
const target = /** @type HTMLElement */(evt.target);
|
|
2426
|
+
if (!target.matches('ul > li > a')) {
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2430
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2431
|
+
ftl.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
2432
|
+
btn.setAttribute('value', value);
|
|
2433
|
+
btn.innerHTML = target.innerHTML;
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
get value() {
|
|
2437
|
+
const operator = this.#operator.getAttribute('value');
|
|
2438
|
+
const values = operator == 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
2439
|
+
return values.some(v => v === '') ? undefined : [operator, "ISO_8601", ...values];
|
|
2440
|
+
}
|
|
2441
|
+
set value(v) {
|
|
2442
|
+
if (v === null || v === undefined) {
|
|
2443
|
+
this.#value1.value = '';
|
|
2444
|
+
this.#value2.value = '';
|
|
2445
|
+
this.reflect(() => {
|
|
2446
|
+
this.removeAttribute('value');
|
|
2447
|
+
});
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const [operator, ...values] = v;
|
|
2451
|
+
this.#operator.setAttibute('value', operator);
|
|
2452
|
+
this.#value1.value = values[0];
|
|
2453
|
+
this.#value2.value = values[1];
|
|
2454
|
+
this.reflect(() => {
|
|
2455
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
focus(options) {
|
|
2459
|
+
this.#value1.focus(options);
|
|
2460
|
+
}
|
|
2461
|
+
setCustomValidity(error) {
|
|
2462
|
+
if (!error) {
|
|
2463
|
+
this.internals.setValidity({});
|
|
2464
|
+
this.#fieldError.innerText = "";
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2468
|
+
this.#fieldError.innerText = error;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
class TextFilter extends ftl.ParsedElement {
|
|
2473
|
+
static observed = ["value:json"];
|
|
2474
|
+
static slots = true;
|
|
2475
|
+
static template = `
|
|
2476
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2477
|
+
<div class="input-group">
|
|
2478
|
+
<button data-ref="operator" class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" value="CONTAINS" form="">…a…</button>
|
|
2479
|
+
<ul class="dropdown-menu">
|
|
2480
|
+
<li><a class="dropdown-item" role="button" value="CONTAINS">…a…</a></li>
|
|
2481
|
+
<li><a class="dropdown-item" role="button" value="STARTS_WITH">a…</a></li>
|
|
2482
|
+
<li><a class="dropdown-item" role="button" value="ENDS_WITH">…a</a></li>
|
|
2483
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2484
|
+
</ul>
|
|
2485
|
+
<input data-tpl-id="id" data-ref="value" type="text" class="form-control" form="">
|
|
2486
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2487
|
+
</div>
|
|
2488
|
+
<ful-field-error></ful-field-error>
|
|
2489
|
+
`;
|
|
2490
|
+
static formAssociated = true;
|
|
2491
|
+
#operator;
|
|
2492
|
+
#value;
|
|
2493
|
+
#fieldError;
|
|
2494
|
+
constructor() {
|
|
2495
|
+
super();
|
|
2496
|
+
this.internals = this.attachInternals();
|
|
2497
|
+
}
|
|
2498
|
+
render({ slots }) {
|
|
2499
|
+
const id = ftl.Attributes.uid('string-filter');
|
|
2500
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2501
|
+
const name = this.getAttribute("name");
|
|
2502
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2503
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2504
|
+
this.#value = fragment.querySelector('[data-ref=value]');
|
|
2505
|
+
this.replaceChildren(fragment);
|
|
2506
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2507
|
+
this.addEventListener('click', (evt) => {
|
|
2508
|
+
const target = /** @type HTMLElement */(evt.target);
|
|
2509
|
+
if (!target.matches('ul > li > a')) {
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2513
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2514
|
+
btn.setAttribute('value', value);
|
|
2515
|
+
btn.innerHTML = target.innerHTML;
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
get value() {
|
|
2520
|
+
const operator = this.#operator.getAttribute('value');
|
|
2521
|
+
return this.#value.value === '' ? undefined : [operator, 'IGNORE_CASE', this.#value.value];
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
set value(v) {
|
|
2525
|
+
if (v === null || v === undefined) {
|
|
2526
|
+
this.#value.value = '';
|
|
2527
|
+
this.reflect(() => {
|
|
2528
|
+
this.removeAttribute('value');
|
|
2529
|
+
});
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
const [operator, sensitivity, value] = v;
|
|
2533
|
+
this.#operator.setAttribute('value', operator);
|
|
2534
|
+
this.#value.value = value;
|
|
2535
|
+
this.reflect(() => {
|
|
2536
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
focus(options) {
|
|
2540
|
+
this.#value.focus(options);
|
|
2541
|
+
}
|
|
2542
|
+
setCustomValidity(error) {
|
|
2543
|
+
if (!error) {
|
|
2544
|
+
this.internals.setValidity({});
|
|
2545
|
+
this.#fieldError.innerText = "";
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2549
|
+
this.#fieldError.innerText = error;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
class Plugin {
|
|
2554
|
+
configure(registry) {
|
|
2555
|
+
const httpClient = HttpClient.builder()
|
|
2556
|
+
.withCsrfToken()
|
|
2557
|
+
.withRedirectOnUnauthorized("/")
|
|
2558
|
+
.build();
|
|
2559
|
+
registry
|
|
2560
|
+
.defineComponent('http-client', httpClient)
|
|
2561
|
+
.defineElement('ful-spinner', Spinner)
|
|
2562
|
+
.defineElement('ful-form', Form)
|
|
2563
|
+
.defineElement('ful-checkbox', Checkbox)
|
|
2564
|
+
.defineElement('ful-input', Input)
|
|
2565
|
+
.defineElement('ful-radio-group', RadioGroup)
|
|
2566
|
+
.defineElement('ful-table', Table)
|
|
2567
|
+
.defineElement('ful-pagination', Pagination)
|
|
2568
|
+
.defineElement('ful-sorter', SortButton)
|
|
2569
|
+
.defineElement('ful-filter-instant', InstantFilter)
|
|
2570
|
+
.defineElement('ful-filter-local-date', LocalDateFilter)
|
|
2571
|
+
.defineElement('ful-filter-text', TextFilter)
|
|
2572
|
+
.defineElement('ful-select', Select)
|
|
2573
|
+
.defineElement('ful-dropdown', Dropdown)
|
|
2574
|
+
.defineComponent("loaders:select", SelectLoader)
|
|
2575
|
+
.defineComponent("loaders:form", FormLoader)
|
|
2576
|
+
.defineComponent("loaders:table", TableLoader);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
1547
2580
|
exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
|
|
1548
2581
|
exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
|
|
1549
2582
|
exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
|
|
1550
2583
|
exports.Base64 = Base64;
|
|
1551
2584
|
exports.Bindings = Bindings;
|
|
1552
|
-
exports.
|
|
2585
|
+
exports.Checkbox = Checkbox;
|
|
2586
|
+
exports.Dropdown = Dropdown;
|
|
1553
2587
|
exports.Failure = Failure;
|
|
1554
2588
|
exports.Form = Form;
|
|
2589
|
+
exports.FormLoader = FormLoader;
|
|
1555
2590
|
exports.Hex = Hex;
|
|
1556
2591
|
exports.HttpClient = HttpClient;
|
|
1557
2592
|
exports.HttpClientError = HttpClientError;
|
|
1558
|
-
exports.INPUT_TEMPLATE = INPUT_TEMPLATE;
|
|
1559
2593
|
exports.Input = Input;
|
|
2594
|
+
exports.InstantFilter = InstantFilter;
|
|
2595
|
+
exports.Loaders = Loaders;
|
|
2596
|
+
exports.LocalDateFilter = LocalDateFilter;
|
|
1560
2597
|
exports.LocalStorage = LocalStorage;
|
|
1561
2598
|
exports.MediaType = MediaType;
|
|
2599
|
+
exports.Pagination = Pagination;
|
|
2600
|
+
exports.Plugin = Plugin;
|
|
1562
2601
|
exports.RadioGroup = RadioGroup;
|
|
1563
2602
|
exports.Select = Select;
|
|
2603
|
+
exports.SelectLoader = SelectLoader;
|
|
1564
2604
|
exports.SessionStorage = SessionStorage;
|
|
2605
|
+
exports.SortButton = SortButton;
|
|
1565
2606
|
exports.Spinner = Spinner;
|
|
2607
|
+
exports.Table = Table;
|
|
2608
|
+
exports.TableSchemaParser = TableSchemaParser;
|
|
2609
|
+
exports.TextFilter = TextFilter;
|
|
1566
2610
|
exports.VersionedStorage = VersionedStorage;
|
|
1567
|
-
exports.makeInputFragment = makeInputFragment;
|
|
1568
2611
|
exports.timing = timing;
|
|
1569
2612
|
|
|
1570
2613
|
return exports;
|
|
1571
2614
|
|
|
1572
|
-
})({}, ftl
|
|
2615
|
+
})({}, ftl);
|
|
1573
2616
|
//# sourceMappingURL=ful.iife.js.map
|