@optionfactory/ful 0.104.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 +1341 -288
- 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 +1328 -287
- 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,10 +1044,27 @@ 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
|
|
|
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 ?? {}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1037
1068
|
class Bindings {
|
|
1038
1069
|
|
|
1039
1070
|
/**
|
|
@@ -1108,13 +1139,15 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1108
1139
|
return el.value;
|
|
1109
1140
|
}
|
|
1110
1141
|
|
|
1111
|
-
|
|
1142
|
+
/**
|
|
1143
|
+
*
|
|
1144
|
+
* @param {HTMLFormElement} form
|
|
1145
|
+
* @returns
|
|
1146
|
+
*/
|
|
1147
|
+
static extractFrom(form){
|
|
1112
1148
|
let result = {};
|
|
1113
|
-
for(const el of
|
|
1114
|
-
if
|
|
1115
|
-
continue;
|
|
1116
|
-
}
|
|
1117
|
-
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")){
|
|
1118
1151
|
continue;
|
|
1119
1152
|
}
|
|
1120
1153
|
result = Bindings.providePath(result, /** @type {string} */(el.getAttribute('name')), Bindings.extract(el));
|
|
@@ -1139,70 +1172,139 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1139
1172
|
el.value = raw;
|
|
1140
1173
|
}
|
|
1141
1174
|
|
|
1142
|
-
static mutateIn(
|
|
1175
|
+
static mutateIn(form, values){
|
|
1143
1176
|
for (const [flattenedKey, value] of Object.entries(Bindings.flatten(values, ''))) {
|
|
1144
|
-
for(const el of
|
|
1177
|
+
for(const el of form.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`)){
|
|
1145
1178
|
Bindings.mutate(el, value);
|
|
1146
1179
|
}
|
|
1147
1180
|
}
|
|
1148
1181
|
}
|
|
1149
1182
|
|
|
1150
1183
|
|
|
1151
|
-
static errors(
|
|
1184
|
+
static errors(form, es, scrollOnError){
|
|
1152
1185
|
const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
1153
1186
|
const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
1154
|
-
|
|
1155
|
-
|
|
1187
|
+
form.querySelectorAll(`[name]`).forEach(el => el.setCustomValidity?.(""));
|
|
1188
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1156
1189
|
el.replaceChildren();
|
|
1157
1190
|
el.setAttribute('hidden', '');
|
|
1158
1191
|
});
|
|
1159
1192
|
fieldErrors.forEach(e => {
|
|
1160
|
-
const name = e.context.replace("[", ".").replace("].", ".");
|
|
1161
|
-
|
|
1162
|
-
root.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(invalidClass));
|
|
1163
|
-
const fieldErrorsSelector = `ful-field-error[field='${CSS.escape(name)}']`;
|
|
1164
|
-
root.querySelectorAll(fieldErrorsSelector).forEach(el => {
|
|
1165
|
-
const hel = /** @type HTMLElement} */ (el);
|
|
1166
|
-
hel.innerText = e.reason;
|
|
1167
|
-
});
|
|
1193
|
+
const name = e.context.replace("[", ".").replace("].", ".").replace("]", "");
|
|
1194
|
+
form.querySelectorAll(`[name='${CSS.escape(name)}']`).forEach(input => input.setCustomValidity?.(e.reason));
|
|
1168
1195
|
});
|
|
1169
|
-
|
|
1196
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1170
1197
|
const hel = /** @type HTMLElement} */ (el);
|
|
1171
1198
|
hel.innerText = globalErrors.map(e => e.reason).join("\n");
|
|
1172
1199
|
if (globalErrors.length !== 0) {
|
|
1173
1200
|
el.removeAttribute('hidden');
|
|
1174
1201
|
}
|
|
1175
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
|
+
}
|
|
1176
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
|
+
}
|
|
1177
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);
|
|
1178
1251
|
}
|
|
1179
1252
|
}
|
|
1180
1253
|
|
|
1181
|
-
class
|
|
1182
|
-
static
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
class Form extends ftl.ParsedElement {
|
|
1266
|
+
form;
|
|
1186
1267
|
render() {
|
|
1187
|
-
const form = document.createElement('form');
|
|
1268
|
+
const form = this.form = document.createElement('form');
|
|
1269
|
+
form.setAttribute("novalidate", "");
|
|
1188
1270
|
ftl.Attributes.forward('form-', this, form);
|
|
1189
1271
|
form.replaceChildren(...this.childNodes);
|
|
1190
1272
|
form.addEventListener('submit', async (e) => {
|
|
1191
1273
|
e.preventDefault();
|
|
1192
|
-
this.
|
|
1193
|
-
await this.submitter?.(this.values, this);
|
|
1194
|
-
});
|
|
1274
|
+
await this.submit();
|
|
1195
1275
|
});
|
|
1196
1276
|
if (this.hasAttribute("clear-invalid-on-change")) {
|
|
1197
|
-
this.addEventListener('change', evt => {
|
|
1198
|
-
|
|
1199
|
-
target?.querySelectorAll(`.${CSS.escape(Form.INVALID_CLASS)}`).forEach(el => {
|
|
1200
|
-
el.classList.remove(Form.INVALID_CLASS);
|
|
1201
|
-
});
|
|
1277
|
+
this.addEventListener('change', (/** @type any */evt) => {
|
|
1278
|
+
evt.target.setCustomValidity?.("");
|
|
1202
1279
|
});
|
|
1203
1280
|
}
|
|
1204
1281
|
this.replaceChildren(form);
|
|
1205
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
|
+
}
|
|
1206
1308
|
spinner(spin) {
|
|
1207
1309
|
this.querySelectorAll('ful-spinner').forEach(el => {
|
|
1208
1310
|
const hel = /** @type HTMLElement */ (el);
|
|
@@ -1213,238 +1315,452 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1213
1315
|
hel.disabled = spin;
|
|
1214
1316
|
});
|
|
1215
1317
|
}
|
|
1216
|
-
async remoting(fn) {
|
|
1217
|
-
try {
|
|
1218
|
-
await fn();
|
|
1219
|
-
} catch (e) {
|
|
1220
|
-
if (e instanceof Failure) {
|
|
1221
|
-
this.errors = e.problems;
|
|
1222
|
-
}
|
|
1223
|
-
throw e;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
async spinningUntilError(fn) {
|
|
1227
|
-
this.spinner(true);
|
|
1228
|
-
try {
|
|
1229
|
-
await this.remoting(fn);
|
|
1230
|
-
} catch (e) {
|
|
1231
|
-
this.spinner(false);
|
|
1232
|
-
throw e;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
async spinning(fn) {
|
|
1236
|
-
this.spinner(true);
|
|
1237
|
-
try {
|
|
1238
|
-
await this.remoting(fn);
|
|
1239
|
-
} finally {
|
|
1240
|
-
this.spinner(false);
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
1318
|
set values(vs) {
|
|
1244
|
-
Bindings.mutateIn(this, vs);
|
|
1319
|
+
Bindings.mutateIn(this.form, vs);
|
|
1245
1320
|
}
|
|
1246
1321
|
get values() {
|
|
1247
|
-
return Bindings.extractFrom(this
|
|
1322
|
+
return Bindings.extractFrom(this.form);
|
|
1248
1323
|
}
|
|
1249
1324
|
set errors(es) {
|
|
1250
|
-
Bindings.errors(this, es,
|
|
1251
|
-
if (es.length == 0 || !this.hasAttribute('scroll-on-error')) {
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const ys = Array.from(this.querySelectorAll(`ful-errors:not([hidden]), [ful-validated-field]:has(.${Form.INVALID_CLASS}) ful-field-error`))
|
|
1255
|
-
.map(el => el.parentElement ? el.parentElement : el)
|
|
1256
|
-
.map(el => el.getBoundingClientRect().y + window.scrollY);
|
|
1257
|
-
const miny = Math.min(...ys);
|
|
1258
|
-
if (miny !== Infinity) {
|
|
1259
|
-
window.scroll(window.scrollX, miny > Form.SCROLL_OFFSET ? miny - Form.SCROLL_OFFSET : 0);
|
|
1260
|
-
}
|
|
1325
|
+
Bindings.errors(this.form, es, this.hasAttribute('scroll-on-error'));
|
|
1261
1326
|
}
|
|
1262
1327
|
}
|
|
1263
1328
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
<
|
|
1269
|
-
<div
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
</div>
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
class Input extends ftl.ParsedElement({
|
|
1305
|
-
observed: ['value'],
|
|
1306
|
-
slots: true,
|
|
1307
|
-
template: INPUT_TEMPLATE
|
|
1308
|
-
}){
|
|
1309
|
-
input;
|
|
1310
|
-
render({slots}) {
|
|
1311
|
-
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');
|
|
1312
1368
|
this.replaceChildren(fragment);
|
|
1313
1369
|
}
|
|
1314
1370
|
get value() {
|
|
1315
|
-
return this
|
|
1371
|
+
return this.#input.value;
|
|
1316
1372
|
}
|
|
1317
1373
|
set value(value) {
|
|
1318
|
-
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;
|
|
1319
1387
|
}
|
|
1320
1388
|
}
|
|
1321
1389
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
+
}
|
|
1326
1437
|
|
|
1327
|
-
class
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
})();
|
|
1360
|
-
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
|
+
}
|
|
1361
1470
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
+
}
|
|
1367
1485
|
|
|
1368
|
-
//tomselect needs the input to have a parent.
|
|
1369
|
-
//se we move the input to a fragment
|
|
1370
|
-
slots.input = ftl.Fragments.from(input);
|
|
1371
1486
|
|
|
1372
|
-
|
|
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
|
+
}
|
|
1373
1500
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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;
|
|
1377
1518
|
}
|
|
1378
|
-
|
|
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
|
+
}
|
|
1379
1588
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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');
|
|
1386
1640
|
|
|
1387
|
-
|
|
1388
|
-
|
|
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')) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (this.#ddmenu.shown) {
|
|
1649
|
+
this.#ddmenu.hide();
|
|
1389
1650
|
return;
|
|
1390
1651
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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;
|
|
1396
1660
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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;
|
|
1410
1678
|
}
|
|
1411
|
-
|
|
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
|
+
}
|
|
1412
1706
|
});
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
evt.stopPropagation();
|
|
1707
|
+
this.#input.addEventListener('input', e => {
|
|
1708
|
+
dload();
|
|
1416
1709
|
});
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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);
|
|
1427
1720
|
}
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
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);
|
|
1431
1731
|
}
|
|
1432
1732
|
set value(value) {
|
|
1433
1733
|
(async () => {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const silent = true;
|
|
1438
|
-
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();
|
|
1439
1737
|
})();
|
|
1440
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
|
+
}
|
|
1441
1757
|
}
|
|
1442
1758
|
|
|
1443
|
-
class RadioGroup extends ftl.ParsedElement
|
|
1444
|
-
observed
|
|
1445
|
-
slots
|
|
1446
|
-
template
|
|
1447
|
-
<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">
|
|
1448
1764
|
<legend class="form-label">
|
|
1449
1765
|
{{{{ slots.default }}}}
|
|
1450
1766
|
</legend>
|
|
@@ -1459,14 +1775,20 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1459
1775
|
</label>
|
|
1460
1776
|
</div>
|
|
1461
1777
|
</section>
|
|
1462
|
-
<ful-field-error data-tpl-
|
|
1778
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1463
1779
|
<footer data-tpl-if="slots.footer">
|
|
1464
1780
|
{{{{ slots.footer }}}}
|
|
1465
1781
|
</footer>
|
|
1466
1782
|
</fieldset>
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1783
|
+
`;
|
|
1784
|
+
static formAssociated = true;
|
|
1785
|
+
#fieldError;
|
|
1786
|
+
#firstRadio;
|
|
1787
|
+
constructor() {
|
|
1788
|
+
super();
|
|
1789
|
+
this.internals = this.attachInternals();
|
|
1790
|
+
}
|
|
1791
|
+
render({ slots }) {
|
|
1470
1792
|
const name = this.getAttribute('name') ?? ftl.Attributes.uid('ful-radiogroup');
|
|
1471
1793
|
const radioEls = Array.from(slots.default.querySelectorAll('ful-radio'));
|
|
1472
1794
|
const inputsAndLabels = radioEls.map(el => {
|
|
@@ -1475,8 +1797,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1475
1797
|
ftl.Attributes.forward('input-', this, input);
|
|
1476
1798
|
ftl.Attributes.forward('', el, input);
|
|
1477
1799
|
input.setAttribute('name', `${name}-ignore`);
|
|
1478
|
-
input.setAttribute('
|
|
1479
|
-
input.dataset['fulBindInclude'] = 'never';
|
|
1800
|
+
input.setAttribute('form', ``);
|
|
1480
1801
|
input.addEventListener('change', evt => {
|
|
1481
1802
|
evt.stopPropagation();
|
|
1482
1803
|
//change is not cancelable
|
|
@@ -1493,14 +1814,11 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1493
1814
|
});
|
|
1494
1815
|
|
|
1495
1816
|
radioEls.forEach(el => el.remove());
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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]');
|
|
1500
1821
|
}
|
|
1501
|
-
set disabled(value) {
|
|
1502
|
-
this.reflect(() => ftl.Attributes.toggle(this, 'disabled', value));
|
|
1503
|
-
}
|
|
1504
1822
|
get value() {
|
|
1505
1823
|
/** @type {HTMLInputElement|null} */
|
|
1506
1824
|
const checked = this.querySelector('input[type=radio]:checked');
|
|
@@ -1519,45 +1837,780 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1519
1837
|
el.checked = true;
|
|
1520
1838
|
}
|
|
1521
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
|
+
}
|
|
1522
1909
|
}
|
|
1523
1910
|
|
|
1524
|
-
class Spinner extends ftl.ParsedElement
|
|
1525
|
-
slots
|
|
1526
|
-
template
|
|
1911
|
+
class Spinner extends ftl.ParsedElement {
|
|
1912
|
+
static slots = true;
|
|
1913
|
+
static template = `
|
|
1527
1914
|
<div class="ful-spinner-wrapper">
|
|
1528
1915
|
<div class="ful-spinner-text">{{{{ slots.default }}}}</div>
|
|
1529
1916
|
<div class="ful-spinner-icon"></div>
|
|
1530
1917
|
</div>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
render({slots}) {
|
|
1918
|
+
`;
|
|
1919
|
+
render({ slots }) {
|
|
1534
1920
|
this.template().withOverlay({ slots }).renderTo(this);
|
|
1535
1921
|
}
|
|
1536
1922
|
}
|
|
1537
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
|
+
|
|
1538
2580
|
exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
|
|
1539
2581
|
exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
|
|
1540
2582
|
exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
|
|
1541
2583
|
exports.Base64 = Base64;
|
|
1542
2584
|
exports.Bindings = Bindings;
|
|
2585
|
+
exports.Checkbox = Checkbox;
|
|
2586
|
+
exports.Dropdown = Dropdown;
|
|
1543
2587
|
exports.Failure = Failure;
|
|
1544
2588
|
exports.Form = Form;
|
|
2589
|
+
exports.FormLoader = FormLoader;
|
|
1545
2590
|
exports.Hex = Hex;
|
|
1546
2591
|
exports.HttpClient = HttpClient;
|
|
1547
2592
|
exports.HttpClientError = HttpClientError;
|
|
1548
|
-
exports.INPUT_TEMPLATE = INPUT_TEMPLATE;
|
|
1549
2593
|
exports.Input = Input;
|
|
2594
|
+
exports.InstantFilter = InstantFilter;
|
|
2595
|
+
exports.Loaders = Loaders;
|
|
2596
|
+
exports.LocalDateFilter = LocalDateFilter;
|
|
1550
2597
|
exports.LocalStorage = LocalStorage;
|
|
1551
2598
|
exports.MediaType = MediaType;
|
|
2599
|
+
exports.Pagination = Pagination;
|
|
2600
|
+
exports.Plugin = Plugin;
|
|
1552
2601
|
exports.RadioGroup = RadioGroup;
|
|
1553
2602
|
exports.Select = Select;
|
|
2603
|
+
exports.SelectLoader = SelectLoader;
|
|
1554
2604
|
exports.SessionStorage = SessionStorage;
|
|
2605
|
+
exports.SortButton = SortButton;
|
|
1555
2606
|
exports.Spinner = Spinner;
|
|
2607
|
+
exports.Table = Table;
|
|
2608
|
+
exports.TableSchemaParser = TableSchemaParser;
|
|
2609
|
+
exports.TextFilter = TextFilter;
|
|
1556
2610
|
exports.VersionedStorage = VersionedStorage;
|
|
1557
|
-
exports.makeInputFragment = makeInputFragment;
|
|
1558
2611
|
exports.timing = timing;
|
|
1559
2612
|
|
|
1560
2613
|
return exports;
|
|
1561
2614
|
|
|
1562
|
-
})({}, ftl
|
|
2615
|
+
})({}, ftl);
|
|
1563
2616
|
//# sourceMappingURL=ful.iife.js.map
|