@optionfactory/ful 0.104.0 → 1.0.1
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 +1343 -289
- 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 +1330 -288
- 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,140 @@ 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
|
-
|
|
1193
|
-
|
|
1194
|
-
});
|
|
1274
|
+
e.stopPropagation();
|
|
1275
|
+
await this.submit();
|
|
1195
1276
|
});
|
|
1196
1277
|
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
|
-
});
|
|
1278
|
+
this.addEventListener('change', (/** @type any */evt) => {
|
|
1279
|
+
evt.target.setCustomValidity?.("");
|
|
1202
1280
|
});
|
|
1203
1281
|
}
|
|
1204
1282
|
this.replaceChildren(form);
|
|
1205
1283
|
}
|
|
1284
|
+
async submit() {
|
|
1285
|
+
this.spinner(true);
|
|
1286
|
+
try {
|
|
1287
|
+
const loader = Loaders.fromAttributes(this, 'loaders:form');
|
|
1288
|
+
const values = this.values;
|
|
1289
|
+
const request = await loader.prepare(values, this);
|
|
1290
|
+
const se = new CustomEvent('submit', { bubbles: true, cancelable: true, detail: { values, request } });
|
|
1291
|
+
if (!this.dispatchEvent(se)) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
const response = await loader.submit(se.detail.request, this);
|
|
1296
|
+
const mapped = await loader.transform(response, this);
|
|
1297
|
+
this.dispatchEvent(new CustomEvent('submit:success', { bubbles: true, cancelable: false, detail: { values, request, response: mapped } }));
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
this.dispatchEvent(new CustomEvent('submit:failure', { bubbles: true, cancelable: false, detail: { values, request, exception: e } }));
|
|
1300
|
+
if (e instanceof Failure) {
|
|
1301
|
+
this.errors = e.problems;
|
|
1302
|
+
}
|
|
1303
|
+
throw e;
|
|
1304
|
+
}
|
|
1305
|
+
} finally {
|
|
1306
|
+
this.spinner(false);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1206
1309
|
spinner(spin) {
|
|
1207
1310
|
this.querySelectorAll('ful-spinner').forEach(el => {
|
|
1208
1311
|
const hel = /** @type HTMLElement */ (el);
|
|
@@ -1213,238 +1316,452 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1213
1316
|
hel.disabled = spin;
|
|
1214
1317
|
});
|
|
1215
1318
|
}
|
|
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
1319
|
set values(vs) {
|
|
1244
|
-
Bindings.mutateIn(this, vs);
|
|
1320
|
+
Bindings.mutateIn(this.form, vs);
|
|
1245
1321
|
}
|
|
1246
1322
|
get values() {
|
|
1247
|
-
return Bindings.extractFrom(this
|
|
1323
|
+
return Bindings.extractFrom(this.form);
|
|
1248
1324
|
}
|
|
1249
1325
|
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
|
-
}
|
|
1326
|
+
Bindings.errors(this.form, es, this.hasAttribute('scroll-on-error'));
|
|
1261
1327
|
}
|
|
1262
1328
|
}
|
|
1263
1329
|
|
|
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);
|
|
1330
|
+
class Input extends ftl.ParsedElement {
|
|
1331
|
+
static observed = ['value'];
|
|
1332
|
+
static slots = true;
|
|
1333
|
+
static template = `
|
|
1334
|
+
<label data-tpl-for="id" class="form-label">{{{{ slots.default }}}}</label>
|
|
1335
|
+
<div class="input-group">
|
|
1336
|
+
<span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
|
|
1337
|
+
{{{{ slots.before }}}}
|
|
1338
|
+
<input class="form-control" data-tpl-id="id" type="text" placeholder=" " data-tpl-aria-describedby="fieldErrorId" form="">
|
|
1339
|
+
{{{{ slots.after }}}}
|
|
1340
|
+
<span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
|
|
1341
|
+
</div>
|
|
1342
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1343
|
+
`;
|
|
1344
|
+
static formAssociated = true;
|
|
1345
|
+
#input;
|
|
1346
|
+
#fieldError;
|
|
1347
|
+
constructor() {
|
|
1348
|
+
super();
|
|
1349
|
+
this.internals = this.attachInternals();
|
|
1350
|
+
}
|
|
1351
|
+
render({ slots }) {
|
|
1352
|
+
const id = ftl.Attributes.uid('ful-input');
|
|
1353
|
+
const fieldErrorId = `${id}-error`;
|
|
1354
|
+
|
|
1355
|
+
const fragment = this.template().withOverlay({ id, fieldErrorId, slots }).render();
|
|
1356
|
+
this.#input = fragment.querySelector("input");
|
|
1357
|
+
ftl.Attributes.forward('input-', this, this.#input);
|
|
1358
|
+
this.#input.addEventListener('change', (evt) => {
|
|
1359
|
+
evt.stopPropagation();
|
|
1360
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1361
|
+
bubbles: true,
|
|
1362
|
+
cancelable: false,
|
|
1363
|
+
detail: {
|
|
1364
|
+
value: this.value
|
|
1365
|
+
}
|
|
1366
|
+
}));
|
|
1367
|
+
});
|
|
1368
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1312
1369
|
this.replaceChildren(fragment);
|
|
1313
1370
|
}
|
|
1314
1371
|
get value() {
|
|
1315
|
-
return this
|
|
1372
|
+
return this.#input.value;
|
|
1316
1373
|
}
|
|
1317
1374
|
set value(value) {
|
|
1318
|
-
this
|
|
1375
|
+
this.#input.value = value;
|
|
1376
|
+
}
|
|
1377
|
+
focus(options) {
|
|
1378
|
+
this.#input.focus(options);
|
|
1379
|
+
}
|
|
1380
|
+
setCustomValidity(error) {
|
|
1381
|
+
if (!error) {
|
|
1382
|
+
this.internals.setValidity({});
|
|
1383
|
+
this.#fieldError.innerText = "";
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1387
|
+
this.#fieldError.innerText = error;
|
|
1319
1388
|
}
|
|
1320
1389
|
}
|
|
1321
1390
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1391
|
+
class CompleteSelectLoader {
|
|
1392
|
+
#http;
|
|
1393
|
+
#url;
|
|
1394
|
+
#method;
|
|
1395
|
+
#responseMapper;
|
|
1396
|
+
#prefetch;
|
|
1397
|
+
#data;
|
|
1398
|
+
constructor(http, url, method, responseMapper, prefetch) {
|
|
1399
|
+
this.#http = http;
|
|
1400
|
+
this.#url = url;
|
|
1401
|
+
this.#method = method;
|
|
1402
|
+
this.#responseMapper = responseMapper;
|
|
1403
|
+
this.#prefetch = prefetch;
|
|
1404
|
+
this.#data = null;
|
|
1405
|
+
}
|
|
1406
|
+
async prefetch() {
|
|
1407
|
+
if (!this.#prefetch) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
await this.#ensureFetched();
|
|
1411
|
+
}
|
|
1412
|
+
async exact(...keys) {
|
|
1413
|
+
await this.#ensureFetched();
|
|
1414
|
+
return this.#data.filter(([k, v]) => keys.includes(k));
|
|
1415
|
+
}
|
|
1416
|
+
async load(needle) {
|
|
1417
|
+
await this.#ensureFetched();
|
|
1418
|
+
return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
|
|
1419
|
+
}
|
|
1420
|
+
async #ensureFetched() {
|
|
1421
|
+
if (this.#data !== null) {
|
|
1422
|
+
return
|
|
1423
|
+
}
|
|
1424
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1425
|
+
.fetchJson();
|
|
1426
|
+
this.#data = this.#responseMapper(data);
|
|
1427
|
+
}
|
|
1428
|
+
static create({ el, http, responseMapper }) {
|
|
1429
|
+
return new CompleteSelectLoader(
|
|
1430
|
+
http,
|
|
1431
|
+
el.getAttribute("src"),
|
|
1432
|
+
el.getAttribute("method") ?? 'POST',
|
|
1433
|
+
responseMapper,
|
|
1434
|
+
el.hasAttribute("preload")
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1326
1438
|
|
|
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', '');
|
|
1439
|
+
class ChunkedSelectLoader {
|
|
1440
|
+
#http;
|
|
1441
|
+
#url;
|
|
1442
|
+
#method;
|
|
1443
|
+
#responseMapper;
|
|
1444
|
+
constructor(http, url, method, responseMapper) {
|
|
1445
|
+
this.#http = http;
|
|
1446
|
+
this.#url = url;
|
|
1447
|
+
this.#method = method;
|
|
1448
|
+
this.#responseMapper = responseMapper;
|
|
1449
|
+
}
|
|
1450
|
+
async exact(...keys) {
|
|
1451
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1452
|
+
.param("k", ...keys)
|
|
1453
|
+
.fetchJson();
|
|
1454
|
+
return this.#responseMapper(data);
|
|
1455
|
+
}
|
|
1456
|
+
async load(needle) {
|
|
1457
|
+
const data = await this.#http.request(this.#method, this.#url)
|
|
1458
|
+
.param("s", needle)
|
|
1459
|
+
.fetchJson();
|
|
1460
|
+
return this.#responseMapper(data);
|
|
1461
|
+
}
|
|
1462
|
+
static create({ el, http, responseMapper }) {
|
|
1463
|
+
return new ChunkedSelectLoader(
|
|
1464
|
+
http,
|
|
1465
|
+
el.getAttribute("src"),
|
|
1466
|
+
el.getAttribute("method") ?? 'POST',
|
|
1467
|
+
responseMapper
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1361
1471
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1472
|
+
class OptionsSlotSelectLoader {
|
|
1473
|
+
#data
|
|
1474
|
+
constructor(data) {
|
|
1475
|
+
this.#data = data;
|
|
1476
|
+
}
|
|
1477
|
+
async exact(...keys) {
|
|
1478
|
+
await timing.sleep(500);
|
|
1479
|
+
return this.#data.filter(([k, v]) => keys.includes(k));
|
|
1480
|
+
}
|
|
1481
|
+
async load(needle) {
|
|
1482
|
+
await timing.sleep(500);
|
|
1483
|
+
return this.#data.filter(([k, v]) => v.includes(needle?.toLowerCase()));
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1367
1486
|
|
|
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
1487
|
|
|
1372
|
-
|
|
1488
|
+
class SelectLoader {
|
|
1489
|
+
static create(conf) {
|
|
1490
|
+
if (!conf.el.hasAttribute("src")) {
|
|
1491
|
+
const els = Array.from(conf.options.options?.querySelectorAll('option') ?? []);
|
|
1492
|
+
const data = els.map(e => {
|
|
1493
|
+
return [e.getAttribute("value") ?? e.innerText.trim(), e.innerText.trim()];
|
|
1494
|
+
});
|
|
1495
|
+
return new OptionsSlotSelectLoader(data);
|
|
1496
|
+
}
|
|
1497
|
+
const chunked = "chunked" == conf.el.getAttribute("mode");
|
|
1498
|
+
return chunked ? ChunkedSelectLoader.create(conf) : CompleteSelectLoader.create(conf);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1373
1501
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1502
|
+
class Dropdown extends ftl.ParsedElement {
|
|
1503
|
+
static slots = true
|
|
1504
|
+
static template = `
|
|
1505
|
+
<ful-spinner class="centered" hidden></ful-spinner>
|
|
1506
|
+
<menu tabindex="-1" hidden></menu>
|
|
1507
|
+
`;
|
|
1508
|
+
#spinner
|
|
1509
|
+
#menu
|
|
1510
|
+
render({ slots }) {
|
|
1511
|
+
const fragment = this.template().render();
|
|
1512
|
+
this.#spinner = fragment.querySelector("ful-spinner");
|
|
1513
|
+
this.#menu = fragment.querySelector("menu");
|
|
1514
|
+
this.#menu.addEventListener('click', evt => {
|
|
1515
|
+
evt.stopPropagation();
|
|
1516
|
+
if (!evt.target.matches('li')) {
|
|
1517
|
+
this.hide();
|
|
1518
|
+
return;
|
|
1377
1519
|
}
|
|
1378
|
-
|
|
1520
|
+
this.#change(evt.target);
|
|
1521
|
+
});
|
|
1522
|
+
this.replaceChildren(fragment);
|
|
1523
|
+
}
|
|
1524
|
+
acceptSelection() {
|
|
1525
|
+
const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
|
|
1526
|
+
this.#change(selected);
|
|
1527
|
+
}
|
|
1528
|
+
update(values) {
|
|
1529
|
+
if (values === undefined) {
|
|
1530
|
+
throw new Error("null data");
|
|
1531
|
+
}
|
|
1532
|
+
if (values.length === 0) {
|
|
1533
|
+
const el = document.createElement('div');
|
|
1534
|
+
el.classList.add('text-center', 'py-2', 'bi', 'bi-database-slash');
|
|
1535
|
+
this.#menu.replaceChildren(el);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
this.#menu.replaceChildren(...values.map(([k, v], i) => {
|
|
1539
|
+
const el = document.createElement('li');
|
|
1540
|
+
if (i === 0) {
|
|
1541
|
+
el.setAttribute("selected", '');
|
|
1542
|
+
}
|
|
1543
|
+
el.setAttribute("value", k);
|
|
1544
|
+
el.innerText = v;
|
|
1545
|
+
return el;
|
|
1546
|
+
}));
|
|
1547
|
+
}
|
|
1548
|
+
#change(target) {
|
|
1549
|
+
const value = target.getAttribute('value');
|
|
1550
|
+
const label = target.innerText;
|
|
1551
|
+
this.hide();
|
|
1552
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1553
|
+
bubbles: true,
|
|
1554
|
+
cancelable: false,
|
|
1555
|
+
detail: { label, value }
|
|
1556
|
+
}));
|
|
1557
|
+
}
|
|
1558
|
+
hide() {
|
|
1559
|
+
this.setAttribute('hidden', '');
|
|
1560
|
+
}
|
|
1561
|
+
get shown() {
|
|
1562
|
+
return !this.hasAttribute('hidden');
|
|
1563
|
+
}
|
|
1564
|
+
async show(loader) {
|
|
1565
|
+
this.removeAttribute('hidden');
|
|
1566
|
+
this.#menu.setAttribute('hidden', '');
|
|
1567
|
+
this.#spinner.removeAttribute('hidden');
|
|
1568
|
+
try {
|
|
1569
|
+
const data = await loader();
|
|
1570
|
+
this.update(data);
|
|
1571
|
+
} finally {
|
|
1572
|
+
this.#spinner.setAttribute('hidden', '');
|
|
1573
|
+
this.#menu.removeAttribute('hidden');
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async moveOrShow(forward, loader) {
|
|
1577
|
+
if (!this.hasAttribute("hidden")) {
|
|
1578
|
+
const selected = this.#menu.querySelector('[selected]') ?? this.#menu.firstElementChild;
|
|
1579
|
+
const candidate = selected[`${forward ? 'next' : 'previous'}ElementSibling`];
|
|
1580
|
+
if (candidate) {
|
|
1581
|
+
selected.removeAttribute('selected');
|
|
1582
|
+
candidate.setAttribute("selected", "");
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
await this.show(loader);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1379
1589
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1590
|
+
class Select extends ftl.ParsedElement {
|
|
1591
|
+
static observed = ['value:csvm']
|
|
1592
|
+
static slots = true
|
|
1593
|
+
static template = `
|
|
1594
|
+
<label data-tpl-for="id" class="form-label">{{{{ slots.default }}}}</label>
|
|
1595
|
+
<div class="input-group flex-nowrap" tabindex="-1">
|
|
1596
|
+
<span data-tpl-if="slots.ibefore" class="input-group-text">{{{{ slots.ibefore }}}}</span>
|
|
1597
|
+
{{{{ slots.before }}}}
|
|
1598
|
+
<div class="ful-select-input">
|
|
1599
|
+
<badges></badges>
|
|
1600
|
+
<input data-tpl-id="id" data-tpl-ariadesribed-by="fieldErrorId" type="text" form="">
|
|
1601
|
+
</div>
|
|
1602
|
+
{{{{ slots.after }}}}
|
|
1603
|
+
<span data-tpl-if="slots.iafter" class="input-group-text">{{{{ slots.iafter }}}}</span>
|
|
1604
|
+
</div>
|
|
1605
|
+
<ful-dropdown hidden></ful-dropdown>
|
|
1606
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1607
|
+
`;
|
|
1608
|
+
static mappers = {
|
|
1609
|
+
"csvm": (v, name, el) => {
|
|
1610
|
+
if (el.hasAttribute("multiple")) {
|
|
1611
|
+
return v === null ? [] : v.split(",").map(e => e.trim()).filter(e => e)
|
|
1612
|
+
}
|
|
1613
|
+
return v === null || v === '' ? null : v
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
static formAssociated = true
|
|
1617
|
+
internals
|
|
1618
|
+
#loader
|
|
1619
|
+
#badges
|
|
1620
|
+
#ddmenu
|
|
1621
|
+
#input
|
|
1622
|
+
#multiple
|
|
1623
|
+
#fieldError
|
|
1624
|
+
#values = new Map()
|
|
1625
|
+
constructor() {
|
|
1626
|
+
super();
|
|
1627
|
+
this.internals = this.attachInternals();
|
|
1628
|
+
}
|
|
1629
|
+
async render({ slots, observed }) {
|
|
1630
|
+
const name = this.getAttribute("name");
|
|
1631
|
+
const id = ftl.Attributes.uid('ful-select');
|
|
1632
|
+
const fieldErrorId = id + "-error";
|
|
1633
|
+
this.#loader = Loaders.fromAttributes(this, 'loaders:select', { options: slots.options });
|
|
1634
|
+
await this.#loader.prefetch?.();
|
|
1635
|
+
const fragment = this.template().withOverlay({ slots, name, id, fieldErrorId }).render();
|
|
1636
|
+
this.#input = fragment.querySelector('input');
|
|
1637
|
+
this.#badges = fragment.querySelector('badges');
|
|
1638
|
+
this.#ddmenu = fragment.querySelector('ful-dropdown');
|
|
1639
|
+
this.#multiple = this.hasAttribute("multiple");
|
|
1640
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1386
1641
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1642
|
+
const self = this;
|
|
1643
|
+
const [dload, abortdload] = timing.debounce(400, () => self.#ddmenu.show(() => self.#loader.load(self.#input.value)));
|
|
1644
|
+
this.addEventListener('click', (/** @type any */e) => {
|
|
1645
|
+
e.stopPropagation();
|
|
1646
|
+
if (e.target.matches('input')) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (this.#ddmenu.shown) {
|
|
1650
|
+
this.#ddmenu.hide();
|
|
1389
1651
|
return;
|
|
1390
1652
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1653
|
+
this.#input.focus();
|
|
1654
|
+
dload();
|
|
1655
|
+
});
|
|
1656
|
+
this.#badges.addEventListener('click', (e) => {
|
|
1657
|
+
e.stopPropagation();
|
|
1658
|
+
const idx = [...this.#badges.children].indexOf(e.target);
|
|
1659
|
+
if (idx === -1) {
|
|
1660
|
+
return;
|
|
1396
1661
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1662
|
+
this.#values.delete(Array.from(this.#values.keys()).pop());
|
|
1663
|
+
this.#syncBadges();
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
this.#input.addEventListener('blur', e => {
|
|
1667
|
+
if (e.relatedTarget && this.contains(e.relatedTarget)) {
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
abortdload();
|
|
1671
|
+
this.#ddmenu.hide();
|
|
1672
|
+
this.#input.value = '';
|
|
1673
|
+
});
|
|
1674
|
+
this.#input.addEventListener('keydown', e => {
|
|
1675
|
+
switch (e.code) {
|
|
1676
|
+
case 'ArrowUp': {
|
|
1677
|
+
this.#ddmenu.moveOrShow(false, () => self.#loader.load(self.#input.value));
|
|
1678
|
+
break;
|
|
1410
1679
|
}
|
|
1411
|
-
|
|
1680
|
+
case 'ArrowDown': {
|
|
1681
|
+
this.#ddmenu.moveOrShow(true, () => self.#loader.load(self.#input.value));
|
|
1682
|
+
break;
|
|
1683
|
+
}
|
|
1684
|
+
case 'Escape': {
|
|
1685
|
+
this.#ddmenu.hide();
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
case 'Enter': {
|
|
1689
|
+
this.#ddmenu.acceptSelection();
|
|
1690
|
+
this.#input.value = '';
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
case 'Backspace': {
|
|
1694
|
+
//remove last if caret a position 0
|
|
1695
|
+
if (this.#values.size && this.#input.selectionStart === 0 && this.#input.selectionEnd === 0) {
|
|
1696
|
+
this.#values.delete(Array.from(this.#values.keys()).pop());
|
|
1697
|
+
this.#syncBadges();
|
|
1698
|
+
}
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
case 'Tab': {
|
|
1702
|
+
this.#ddmenu.hide();
|
|
1703
|
+
abortdload();
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1412
1707
|
});
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
evt.stopPropagation();
|
|
1708
|
+
this.#input.addEventListener('input', e => {
|
|
1709
|
+
dload();
|
|
1416
1710
|
});
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1711
|
+
this.#ddmenu.addEventListener('change', (e) => {
|
|
1712
|
+
if (!this.#multiple) {
|
|
1713
|
+
this.#values.clear();
|
|
1714
|
+
}
|
|
1715
|
+
this.#values.set(e.detail.value, e.detail.label);
|
|
1716
|
+
this.#syncBadges();
|
|
1717
|
+
this.#input.focus();
|
|
1718
|
+
this.#ddmenu.hide();
|
|
1719
|
+
});
|
|
1720
|
+
this.replaceChildren(fragment);
|
|
1427
1721
|
}
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1722
|
+
#syncBadges() {
|
|
1723
|
+
const badges = Array.from(this.#values.entries()).map(([k, v]) => {
|
|
1724
|
+
const b = document.createElement('badge');
|
|
1725
|
+
b.setAttribute("role", "button");
|
|
1726
|
+
b.setAttribute("value", k);
|
|
1727
|
+
b.innerText = v;
|
|
1728
|
+
return b;
|
|
1729
|
+
});
|
|
1730
|
+
this.#badges.innerHTML = '';
|
|
1731
|
+
this.#badges.append(...badges);
|
|
1431
1732
|
}
|
|
1432
1733
|
set value(value) {
|
|
1433
1734
|
(async () => {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const silent = true;
|
|
1438
|
-
this.ts.setValue(value, silent);
|
|
1735
|
+
const entries = await (this.#multiple ? this.#loader.exact(...value) : this.#loader.exact(value));
|
|
1736
|
+
this.#values = new Map(entries);
|
|
1737
|
+
this.#syncBadges();
|
|
1439
1738
|
})();
|
|
1440
1739
|
}
|
|
1740
|
+
get value() {
|
|
1741
|
+
if (this.#multiple) {
|
|
1742
|
+
return [...this.#values.keys()];
|
|
1743
|
+
}
|
|
1744
|
+
return [...this.#values.keys()][0] ?? null;
|
|
1745
|
+
}
|
|
1746
|
+
focus(options) {
|
|
1747
|
+
this.#input.focus(options);
|
|
1748
|
+
}
|
|
1749
|
+
setCustomValidity(error) {
|
|
1750
|
+
if (!error) {
|
|
1751
|
+
this.internals.setValidity({});
|
|
1752
|
+
this.#fieldError.innerText = "";
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1756
|
+
this.#fieldError.innerText = error;
|
|
1757
|
+
}
|
|
1441
1758
|
}
|
|
1442
1759
|
|
|
1443
|
-
class RadioGroup extends ftl.ParsedElement
|
|
1444
|
-
observed
|
|
1445
|
-
slots
|
|
1446
|
-
template
|
|
1447
|
-
<fieldset
|
|
1760
|
+
class RadioGroup extends ftl.ParsedElement {
|
|
1761
|
+
static observed = ['value'];
|
|
1762
|
+
static slots = true;
|
|
1763
|
+
static template = `
|
|
1764
|
+
<fieldset data-tpl-aria-describedby="fieldErrorId">
|
|
1448
1765
|
<legend class="form-label">
|
|
1449
1766
|
{{{{ slots.default }}}}
|
|
1450
1767
|
</legend>
|
|
@@ -1455,18 +1772,24 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1455
1772
|
<div class="label-wrapper" data-tpl-each="inputsAndLabels" data-tpl-var="ial">
|
|
1456
1773
|
<label>
|
|
1457
1774
|
{{{{ ial[0] }}}}
|
|
1458
|
-
{{{{ ial[1] }}}}
|
|
1775
|
+
<div>{{{{ ial[1] }}}}</div>
|
|
1459
1776
|
</label>
|
|
1460
1777
|
</div>
|
|
1461
1778
|
</section>
|
|
1462
|
-
<ful-field-error data-tpl-
|
|
1779
|
+
<ful-field-error data-tpl-id="fieldErrorId"></ful-field-error>
|
|
1463
1780
|
<footer data-tpl-if="slots.footer">
|
|
1464
1781
|
{{{{ slots.footer }}}}
|
|
1465
1782
|
</footer>
|
|
1466
1783
|
</fieldset>
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1784
|
+
`;
|
|
1785
|
+
static formAssociated = true;
|
|
1786
|
+
#fieldError;
|
|
1787
|
+
#firstRadio;
|
|
1788
|
+
constructor() {
|
|
1789
|
+
super();
|
|
1790
|
+
this.internals = this.attachInternals();
|
|
1791
|
+
}
|
|
1792
|
+
render({ slots }) {
|
|
1470
1793
|
const name = this.getAttribute('name') ?? ftl.Attributes.uid('ful-radiogroup');
|
|
1471
1794
|
const radioEls = Array.from(slots.default.querySelectorAll('ful-radio'));
|
|
1472
1795
|
const inputsAndLabels = radioEls.map(el => {
|
|
@@ -1475,8 +1798,7 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1475
1798
|
ftl.Attributes.forward('input-', this, input);
|
|
1476
1799
|
ftl.Attributes.forward('', el, input);
|
|
1477
1800
|
input.setAttribute('name', `${name}-ignore`);
|
|
1478
|
-
input.setAttribute('
|
|
1479
|
-
input.dataset['fulBindInclude'] = 'never';
|
|
1801
|
+
input.setAttribute('form', ``);
|
|
1480
1802
|
input.addEventListener('change', evt => {
|
|
1481
1803
|
evt.stopPropagation();
|
|
1482
1804
|
//change is not cancelable
|
|
@@ -1493,14 +1815,11 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1493
1815
|
});
|
|
1494
1816
|
|
|
1495
1817
|
radioEls.forEach(el => el.remove());
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1818
|
+
const fieldErrorId = ftl.Attributes.uid("ful-error");
|
|
1819
|
+
this.template().withOverlay({ name, fieldErrorId, slots, inputsAndLabels }).renderTo(this);
|
|
1820
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
1821
|
+
this.#firstRadio = this.querySelector('input[type=radio]');
|
|
1500
1822
|
}
|
|
1501
|
-
set disabled(value) {
|
|
1502
|
-
this.reflect(() => ftl.Attributes.toggle(this, 'disabled', value));
|
|
1503
|
-
}
|
|
1504
1823
|
get value() {
|
|
1505
1824
|
/** @type {HTMLInputElement|null} */
|
|
1506
1825
|
const checked = this.querySelector('input[type=radio]:checked');
|
|
@@ -1519,45 +1838,780 @@ var ful = (function (exports, ftl, TomSelect) {
|
|
|
1519
1838
|
el.checked = true;
|
|
1520
1839
|
}
|
|
1521
1840
|
}
|
|
1841
|
+
focus(options) {
|
|
1842
|
+
this.#firstRadio.focus(options);
|
|
1843
|
+
}
|
|
1844
|
+
setCustomValidity(error) {
|
|
1845
|
+
if (!error) {
|
|
1846
|
+
this.internals.setValidity({});
|
|
1847
|
+
this.#fieldError.innerText = "";
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1851
|
+
this.#fieldError.innerText = error;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
class Checkbox extends ftl.ParsedElement {
|
|
1856
|
+
static observed = ['value:bool'];
|
|
1857
|
+
static slots = true;
|
|
1858
|
+
static template = `
|
|
1859
|
+
<div data-tpl-class="klass">
|
|
1860
|
+
<input dat-tpl-id="id" class="form-check-input" type="checkbox" role="switch" form="" placeholder=" " data-tpl-aria-describedby="fieldErrorId">
|
|
1861
|
+
<label data-tpl-for="id" class="form-check-label">{{{{ slots.default }}}}</label>
|
|
1862
|
+
</div>
|
|
1863
|
+
<ful-field-error data-tpl-if="fieldErrorId"></ful-field-error>
|
|
1864
|
+
`;
|
|
1865
|
+
#input;
|
|
1866
|
+
#fieldError;
|
|
1867
|
+
static formAssociated = true;
|
|
1868
|
+
constructor() {
|
|
1869
|
+
super();
|
|
1870
|
+
this.internals = this.attachInternals();
|
|
1871
|
+
}
|
|
1872
|
+
render({ slots }) {
|
|
1873
|
+
const id = ftl.Attributes.uid("ful-checkbox");
|
|
1874
|
+
const fieldErrorId = id + "-error";
|
|
1875
|
+
const klass = this.getAttribute('type') == 'switch' ? "form-check form-switch" : "form-check";
|
|
1876
|
+
const fragment = this.template().withOverlay({ slots, klass, id, fieldErrorId }).render();
|
|
1877
|
+
this.#input = fragment.querySelector("input");
|
|
1878
|
+
ftl.Attributes.forward('input-', this, this.#input);
|
|
1879
|
+
this.#fieldError = fragment.querySelector('ful-field-error');
|
|
1880
|
+
this.#input.addEventListener('change', (evt) => {
|
|
1881
|
+
evt.stopPropagation();
|
|
1882
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1883
|
+
bubbles: true,
|
|
1884
|
+
cancelable: false,
|
|
1885
|
+
detail: {
|
|
1886
|
+
value: this.value
|
|
1887
|
+
}
|
|
1888
|
+
}));
|
|
1889
|
+
});
|
|
1890
|
+
this.replaceChildren(fragment);
|
|
1891
|
+
}
|
|
1892
|
+
get value() {
|
|
1893
|
+
return this.#input.checked;
|
|
1894
|
+
}
|
|
1895
|
+
set value(value) {
|
|
1896
|
+
this.#input.checked = value;
|
|
1897
|
+
}
|
|
1898
|
+
focus(options) {
|
|
1899
|
+
this.#input.focus(options);
|
|
1900
|
+
}
|
|
1901
|
+
setCustomValidity(error) {
|
|
1902
|
+
if (!error) {
|
|
1903
|
+
this.internals.setValidity({});
|
|
1904
|
+
this.#fieldError.innerText = "";
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
1908
|
+
this.#fieldError.innerText = error;
|
|
1909
|
+
}
|
|
1522
1910
|
}
|
|
1523
1911
|
|
|
1524
|
-
class Spinner extends ftl.ParsedElement
|
|
1525
|
-
slots
|
|
1526
|
-
template
|
|
1912
|
+
class Spinner extends ftl.ParsedElement {
|
|
1913
|
+
static slots = true;
|
|
1914
|
+
static template = `
|
|
1527
1915
|
<div class="ful-spinner-wrapper">
|
|
1528
1916
|
<div class="ful-spinner-text">{{{{ slots.default }}}}</div>
|
|
1529
1917
|
<div class="ful-spinner-icon"></div>
|
|
1530
1918
|
</div>
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
render({slots}) {
|
|
1919
|
+
`;
|
|
1920
|
+
render({ slots }) {
|
|
1534
1921
|
this.template().withOverlay({ slots }).renderTo(this);
|
|
1535
1922
|
}
|
|
1536
1923
|
}
|
|
1537
1924
|
|
|
1925
|
+
class SortButton extends ftl.ParsedElement {
|
|
1926
|
+
static observed = ["order"];
|
|
1927
|
+
#order;
|
|
1928
|
+
render() {
|
|
1929
|
+
const sorter = this.getAttribute("sorter");
|
|
1930
|
+
const orders = ["asc", "desc", null];
|
|
1931
|
+
this.addEventListener('click', () => {
|
|
1932
|
+
const nextOrder = orders[(orders.indexOf(this.order) + 1) % 3];
|
|
1933
|
+
this.dispatchEvent(new CustomEvent('sort-requested', {
|
|
1934
|
+
bubbles: true,
|
|
1935
|
+
cancelable: true,
|
|
1936
|
+
detail: {
|
|
1937
|
+
value: { sorter, order: nextOrder }
|
|
1938
|
+
}
|
|
1939
|
+
}));
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
get order() {
|
|
1944
|
+
return this.#order || null;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
set order(value) {
|
|
1948
|
+
this.#order = value || null;
|
|
1949
|
+
this.reflect(() => {
|
|
1950
|
+
if (this.#order) {
|
|
1951
|
+
this.setAttribute("order", value);
|
|
1952
|
+
} else {
|
|
1953
|
+
this.removeAttribute("order");
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
class Pagination extends ftl.ParsedElement {
|
|
1960
|
+
static observed = ["total:number", "current:number"];
|
|
1961
|
+
static template = `
|
|
1962
|
+
<nav aria-label="Page navigation" class="user-select-none">
|
|
1963
|
+
<ul class="pagination">
|
|
1964
|
+
<li class="page-item ms-auto me-2" data-tpl-if="paginationLabel"> Showing page {{ curr.label }} of {{ total }}</li>
|
|
1965
|
+
<li class="page-item ms-auto me-2" data-tpl-if="!paginationLabel"></li>
|
|
1966
|
+
<li class="page-item">
|
|
1967
|
+
<a data-tpl-class="prev.enabled?'page-link':'page-link disabled'" aria-label="Previous" role="button" data-tpl-data-page="prev.index">
|
|
1968
|
+
<span aria-hidden="true">«</span>
|
|
1969
|
+
</a>
|
|
1970
|
+
</li>
|
|
1971
|
+
<li class="page-item" data-tpl-each="pages" data-tpl-var="page">
|
|
1972
|
+
<a data-tpl-class="curr.index != page.index ? 'page-link': 'page-link disabled'" role="button" data-tpl-data-page="page.index" >
|
|
1973
|
+
{{ page.label }}
|
|
1974
|
+
</a>
|
|
1975
|
+
</li>
|
|
1976
|
+
<li class="page-item">
|
|
1977
|
+
<a data-tpl-class="next.enabled?'page-link':'page-link disabled'" aria-label="Next" role="button" data-tpl-data-page="next.index">
|
|
1978
|
+
<span aria-hidden="true">»</span>
|
|
1979
|
+
</a>
|
|
1980
|
+
</li>
|
|
1981
|
+
</ul>
|
|
1982
|
+
</nav>
|
|
1983
|
+
`;
|
|
1984
|
+
#paginationLabel;
|
|
1985
|
+
#total = 0;
|
|
1986
|
+
#current = 0;
|
|
1987
|
+
render({ observed }) {
|
|
1988
|
+
this.#paginationLabel = this.hasAttribute('pagination-label');
|
|
1989
|
+
this.update(observed.current ?? 0, observed.total ?? 0);
|
|
1990
|
+
this.addEventListener('click', (/** @type any */evt) => {
|
|
1991
|
+
const el = evt.target.closest('a');
|
|
1992
|
+
if (!el) {
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
this.dispatchEvent(new CustomEvent('page-requested', {
|
|
1996
|
+
bubbles: true,
|
|
1997
|
+
cancelable: true,
|
|
1998
|
+
detail: {
|
|
1999
|
+
value: Number(el.dataset.page)
|
|
2000
|
+
}
|
|
2001
|
+
}));
|
|
2002
|
+
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
update(current, total) {
|
|
2006
|
+
const maxRender = Number(this.getAttribute('pages') ?? "5");
|
|
2007
|
+
const prev = { index: Math.max(0, current - 1), enabled: current > 0 };
|
|
2008
|
+
const curr = { index: current, label: current + 1 };
|
|
2009
|
+
const next = { index: Math.min(total, current + 1), enabled: current + 1 < total };
|
|
2010
|
+
const pages = [{
|
|
2011
|
+
index: current,
|
|
2012
|
+
label: current + 1
|
|
2013
|
+
}];
|
|
2014
|
+
for (let mid = current, offset = 1; offset !== maxRender && pages.length != maxRender; ++offset) {
|
|
2015
|
+
const p = mid - offset;
|
|
2016
|
+
if (p >= 0) {
|
|
2017
|
+
pages.unshift({ index: p, label: p + 1 });
|
|
2018
|
+
}
|
|
2019
|
+
const n = mid + offset;
|
|
2020
|
+
if (n < total) {
|
|
2021
|
+
pages.push({ index: n, label: n + 1 });
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
const paginationLabel = this.#paginationLabel;
|
|
2025
|
+
this.template().withOverlay({ total, prev, curr, next, pages, paginationLabel }).renderTo(this);
|
|
2026
|
+
}
|
|
2027
|
+
get total() {
|
|
2028
|
+
return this.#total;
|
|
2029
|
+
}
|
|
2030
|
+
set total(value) {
|
|
2031
|
+
this.#total = value;
|
|
2032
|
+
this.reflect(() => {
|
|
2033
|
+
this.setAttribute('total', String(value));
|
|
2034
|
+
this.update(this.#current ?? 0, this.#total);
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
get current() {
|
|
2038
|
+
return this.#current;
|
|
2039
|
+
}
|
|
2040
|
+
set current(value) {
|
|
2041
|
+
this.#current = value;
|
|
2042
|
+
this.reflect(() => {
|
|
2043
|
+
this.setAttribute('current', String(value));
|
|
2044
|
+
this.update(this.#current, this.#total ?? 0);
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
class TableSchemaParser {
|
|
2050
|
+
static parse(nodeOrFragment, template) {
|
|
2051
|
+
const schema = ftl.Nodes.queryChildren(nodeOrFragment, "schema");
|
|
2052
|
+
if (!schema) {
|
|
2053
|
+
throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
|
|
2054
|
+
}
|
|
2055
|
+
return ftl.Nodes.queryChildrenAll(schema, "column")
|
|
2056
|
+
.map(el => {
|
|
2057
|
+
return {
|
|
2058
|
+
sorter: el.getAttribute("sorter"),
|
|
2059
|
+
order: el.getAttribute("order"),
|
|
2060
|
+
title: TableSchemaParser.#parseTitle(el, template),
|
|
2061
|
+
content: TableSchemaParser.#parseContent(el, template)
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
static #parseTitle(el, template) {
|
|
2067
|
+
const maybeTitleTag = ftl.Nodes.queryChildren(el, 'title');
|
|
2068
|
+
if (maybeTitleTag) {
|
|
2069
|
+
maybeTitleTag.remove();
|
|
2070
|
+
}
|
|
2071
|
+
const fragment = maybeTitleTag ? template.withFragment(ftl.Fragments.fromChildNodes(maybeTitleTag)).render() : document.createTextNode(el.getAttribute("title") ?? '');
|
|
2072
|
+
return {
|
|
2073
|
+
classes: el.getAttribute("th-class"),
|
|
2074
|
+
fragment
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
static #parseContent(el, template) {
|
|
2079
|
+
return {
|
|
2080
|
+
classes: el.getAttribute("td-class"),
|
|
2081
|
+
template: template.withFragment(ftl.Fragments.fromChildNodes(el))
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
class RemoteTableLoader{
|
|
2087
|
+
#http;
|
|
2088
|
+
#url;
|
|
2089
|
+
#method;
|
|
2090
|
+
constructor(http, url, method){
|
|
2091
|
+
this.#http = http;
|
|
2092
|
+
this.#url = url;
|
|
2093
|
+
this.#method = method;
|
|
2094
|
+
}
|
|
2095
|
+
async load(pageRequest, sortRequest, filterRequest){
|
|
2096
|
+
const filters = Object.entries(filterRequest).filter(([k, v]) => v);
|
|
2097
|
+
return await this.#http.request(this.#method, this.#url)
|
|
2098
|
+
.param("page", pageRequest.page)
|
|
2099
|
+
.param("size", pageRequest.size)
|
|
2100
|
+
.param("sort", sortRequest.order ? `${sortRequest.sorter},${sortRequest.order}` : null)
|
|
2101
|
+
.param("filters", filters.length > 0 ? JSON.stringify(Object.fromEntries(filters)) : null)
|
|
2102
|
+
.fetchJson();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
class TableLoader{
|
|
2108
|
+
static create({el, http}){
|
|
2109
|
+
const url = el.getAttribute("src");
|
|
2110
|
+
const method = el.getAttribute("method") ?? 'GET';
|
|
2111
|
+
return new RemoteTableLoader(http, url, method);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
class Table extends ftl.ParsedElement {
|
|
2116
|
+
static slots = true;
|
|
2117
|
+
static template = `
|
|
2118
|
+
<ful-form data-tpl-if="slots.filters">
|
|
2119
|
+
{{{{ slots.filters }}}}
|
|
2120
|
+
</ful-form>
|
|
2121
|
+
<table class="table">
|
|
2122
|
+
<caption data-tpl-if="slots.caption">{{{{ slots.caption }}}}</caption>
|
|
2123
|
+
<thead>
|
|
2124
|
+
<tr>
|
|
2125
|
+
<th data-tpl-each="schema" scope="col" data-tpl-class="title.classes">
|
|
2126
|
+
{{{{ title.fragment }}}}
|
|
2127
|
+
<ful-sorter data-tpl-if="sorter || order" data-tpl-sorter="sorter" data-tpl-order="order"></ful-sorter>
|
|
2128
|
+
</th>
|
|
2129
|
+
</tr>
|
|
2130
|
+
</thead>
|
|
2131
|
+
<tbody></tbody>
|
|
2132
|
+
<tbody data-ref="no-autoload">
|
|
2133
|
+
<tr>
|
|
2134
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2135
|
+
<i class="bi bi-search" style="font-size: 40px; color: #BDC3CA"></i>
|
|
2136
|
+
<p class="mt-3 mb-0" style="color: #BDC3CA">
|
|
2137
|
+
Avvia la ricerca per visualizzare i risultati...
|
|
2138
|
+
</p>
|
|
2139
|
+
</td>
|
|
2140
|
+
</tr>
|
|
2141
|
+
</tbody>
|
|
2142
|
+
<tbody data-ref="loading" hidden>
|
|
2143
|
+
<tr>
|
|
2144
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2145
|
+
<ful-spinner class="big"></ful-spinner>
|
|
2146
|
+
</td>
|
|
2147
|
+
</tr>
|
|
2148
|
+
</tbody>
|
|
2149
|
+
<tbody data-ref="feedback" hidden>
|
|
2150
|
+
<tr>
|
|
2151
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2152
|
+
<div class="alert alert-danger">
|
|
2153
|
+
<p>Errore nel caricamento della tabella:</p>
|
|
2154
|
+
<p class="mb-0" data-ref="feedback-error"></p>
|
|
2155
|
+
</div>
|
|
2156
|
+
</td>
|
|
2157
|
+
</tr>
|
|
2158
|
+
</tbody>
|
|
2159
|
+
<tfoot data-tpl-if="slots.footer">
|
|
2160
|
+
{{{{ slots.footer }}}}
|
|
2161
|
+
</tfoot>
|
|
2162
|
+
</table>
|
|
2163
|
+
<ful-pagination current="0" total="1"></ful-pagination>
|
|
2164
|
+
`;
|
|
2165
|
+
static templates = {
|
|
2166
|
+
row: `
|
|
2167
|
+
<tr data-tpl-if="pageResponse.data.length == 0">
|
|
2168
|
+
<td data-tpl-colspan="schema.length" class="text-center align-middle p-4">
|
|
2169
|
+
Nessun elemento trovato.
|
|
2170
|
+
</td>
|
|
2171
|
+
</tr>
|
|
2172
|
+
<tr data-tpl-each="pageResponse.data" data-tpl-var="row">
|
|
2173
|
+
<td data-tpl-each="schema" data-tpl-class="content.classes">
|
|
2174
|
+
{{{{ content.template.withOverlay(row).render() }}}}
|
|
2175
|
+
</td>
|
|
2176
|
+
</tr>
|
|
2177
|
+
`
|
|
2178
|
+
};
|
|
2179
|
+
#schema;
|
|
2180
|
+
#body;
|
|
2181
|
+
#loading;
|
|
2182
|
+
#noAutoload;
|
|
2183
|
+
#feedback;
|
|
2184
|
+
#paginator;
|
|
2185
|
+
#sorters;
|
|
2186
|
+
#latestRequest;
|
|
2187
|
+
async render({ slots, observed }) {
|
|
2188
|
+
const template = this.template();
|
|
2189
|
+
const schema = TableSchemaParser.parse(slots.default, template);
|
|
2190
|
+
const fragment = template.withOverlay({ slots, schema }).render();
|
|
2191
|
+
const table = /** @type HTMLTableElement */ (ftl.Nodes.queryChildren(fragment, 'table'));
|
|
2192
|
+
ftl.Attributes.forward('table-', this, table);
|
|
2193
|
+
this.#schema = schema;
|
|
2194
|
+
this.#body = table.querySelector(':scope > tbody');
|
|
2195
|
+
this.#loading = table.querySelector(":scope > tbody[data-ref=loading]");
|
|
2196
|
+
this.#noAutoload = table.querySelector(":scope > tbody[data-ref=no-autoload]");
|
|
2197
|
+
this.#feedback = table.querySelector(":scope > tbody[data-ref=feedback]");
|
|
2198
|
+
this.#paginator = ftl.Nodes.queryChildren(fragment, 'ful-pagination');
|
|
2199
|
+
this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
|
|
2200
|
+
this.replaceChildren(fragment);
|
|
2201
|
+
await ftl.Nodes.waitForUpgrades();
|
|
2202
|
+
const orderFromSchema = schema.find(v => v.order);
|
|
2203
|
+
|
|
2204
|
+
const maybeForm = /** @type any */(ftl.Nodes.queryChildren(this, 'ful-form'));
|
|
2205
|
+
this.#latestRequest = {
|
|
2206
|
+
pageRequest: {
|
|
2207
|
+
page: 0,
|
|
2208
|
+
size: this.getAttribute("page-size") ? Number(this.getAttribute("page-size")) : 10
|
|
2209
|
+
},
|
|
2210
|
+
sortRequest: { order: orderFromSchema?.order, sorter: orderFromSchema?.sorter },
|
|
2211
|
+
filterRequest: maybeForm?.values ?? {}
|
|
2212
|
+
};
|
|
2213
|
+
maybeForm?.addEventListener('submit:success', async (evt) => {
|
|
2214
|
+
await this.load({
|
|
2215
|
+
page: 0,
|
|
2216
|
+
size: this.#latestRequest.pageRequest.size
|
|
2217
|
+
}, this.#latestRequest.sortRequest, evt.detail.request);
|
|
2218
|
+
});
|
|
2219
|
+
if (maybeForm) {
|
|
2220
|
+
maybeForm.submitter = async (filterRequest, form) => {
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
this.addEventListener('page-requested', async (/** @type any */e) => {
|
|
2224
|
+
await this.load({
|
|
2225
|
+
page: e.detail.value,
|
|
2226
|
+
size: this.#latestRequest.pageRequest.size
|
|
2227
|
+
}, this.#latestRequest.sortRequest, this.#latestRequest.filterRequest);
|
|
2228
|
+
});
|
|
2229
|
+
this.addEventListener('sort-requested', async (/** @type any */e) => {
|
|
2230
|
+
await this.load(this.#latestRequest.pageRequest, e.detail.value, this.#latestRequest.filterRequest);
|
|
2231
|
+
this.#sorters.forEach(s => s.order = null);
|
|
2232
|
+
e.target.order = e.detail.value.order;
|
|
2233
|
+
});
|
|
2234
|
+
if (this.hasAttribute('autoload')) {
|
|
2235
|
+
await this.reload();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
async reload() {
|
|
2240
|
+
return await this.load(this.#latestRequest.pageRequest, this.#latestRequest.sortRequest, this.#latestRequest.filterRequest);
|
|
2241
|
+
}
|
|
2242
|
+
async load(pageRequest, sortRequest, filterRequest) {
|
|
2243
|
+
this.#body.innerHTML = "";
|
|
2244
|
+
this.#loading.removeAttribute("hidden", "");
|
|
2245
|
+
this.#feedback.setAttribute("hidden", "");
|
|
2246
|
+
this.#noAutoload.setAttribute("hidden", "");
|
|
2247
|
+
try {
|
|
2248
|
+
const loader = Loaders.fromAttributes(this, 'loaders:table');
|
|
2249
|
+
const pageResponse =await loader.load(pageRequest, sortRequest, filterRequest);
|
|
2250
|
+
this.#latestRequest = { pageRequest, sortRequest, filterRequest };
|
|
2251
|
+
this.#update(pageRequest, sortRequest, filterRequest, pageResponse);
|
|
2252
|
+
} catch (/** @type any */error) {
|
|
2253
|
+
this.#loading.setAttribute("hidden", "");
|
|
2254
|
+
this.#feedback.removeAttribute("hidden", "");
|
|
2255
|
+
if (!error.problems) {
|
|
2256
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error;
|
|
2257
|
+
} else {
|
|
2258
|
+
this.#feedback.querySelector('[data-ref=feedback-error]').textContent = error.problems.map(p => `${p.reason}`);
|
|
2259
|
+
}
|
|
2260
|
+
throw error;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
async resetWithFilter(filterRequest) {
|
|
2265
|
+
return await this.load({
|
|
2266
|
+
page: 0,
|
|
2267
|
+
size: this.#latestRequest.pageRequest.size
|
|
2268
|
+
}, this.#latestRequest.sortRequest, filterRequest);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
#update(pageRequest, sortRequest, filterRequest, pageResponse) {
|
|
2272
|
+
this.#loading.setAttribute("hidden", "");
|
|
2273
|
+
this.#body.replaceChildren(this.template('row').withOverlay({
|
|
2274
|
+
schema: this.#schema,
|
|
2275
|
+
pageRequest,
|
|
2276
|
+
filterRequest,
|
|
2277
|
+
pageResponse
|
|
2278
|
+
}).render());
|
|
2279
|
+
this.#paginator.current = pageRequest.page;
|
|
2280
|
+
this.#paginator.total = Math.ceil(pageResponse.size / pageRequest.size);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
class InstantFilter extends ftl.ParsedElement {
|
|
2285
|
+
static observed = ["value:json"];
|
|
2286
|
+
static slots = true;
|
|
2287
|
+
static template = `
|
|
2288
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2289
|
+
<div class="input-group">
|
|
2290
|
+
<button data-ref="operator" class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" value="LTE" form="">≼</button>
|
|
2291
|
+
<ul class="dropdown-menu">
|
|
2292
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2293
|
+
<li><a class="dropdown-item" role="button" value="NEQ">≠</a></li>
|
|
2294
|
+
<li><a class="dropdown-item" role="button" value="LT">≺</a></li>
|
|
2295
|
+
<li><a class="dropdown-item" role="button" value="GT">≻</a></li>
|
|
2296
|
+
<li><a class="dropdown-item" role="button" value="LTE">≼</a></li>
|
|
2297
|
+
<li><a class="dropdown-item" role="button" value="GTE">≽</a></li>
|
|
2298
|
+
<li><a class="dropdown-item" role="button" value="BETWEEN">↔</a></li>
|
|
2299
|
+
</ul>
|
|
2300
|
+
<input data-tpl-id="id" data-ref="value1" type="datetime-local" class="form-control" form="">
|
|
2301
|
+
<input data-ref="value2" type="datetime-local" class="form-control" form="" hidden>
|
|
2302
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2303
|
+
</div>
|
|
2304
|
+
<ful-field-error></ful-field-error>
|
|
2305
|
+
`;
|
|
2306
|
+
static formAssociated = true;
|
|
2307
|
+
#operator;
|
|
2308
|
+
#value1;
|
|
2309
|
+
#value2;
|
|
2310
|
+
#fieldError;
|
|
2311
|
+
constructor() {
|
|
2312
|
+
super();
|
|
2313
|
+
this.internals = this.attachInternals();
|
|
2314
|
+
}
|
|
2315
|
+
render({ slots }) {
|
|
2316
|
+
const id = ftl.Attributes.uid('instant-filter');
|
|
2317
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2318
|
+
const name = this.getAttribute("name");
|
|
2319
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2320
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2321
|
+
this.#value1 = fragment.querySelector('[data-ref=value1]');
|
|
2322
|
+
this.#value2 = fragment.querySelector('[data-ref=value2]');
|
|
2323
|
+
this.replaceChildren(fragment);
|
|
2324
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2325
|
+
this.addEventListener('click', (evt) => {
|
|
2326
|
+
const target = /** @type HTMLElement */ (evt.target);
|
|
2327
|
+
if (!target.matches('ul > li > a')) {
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2331
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2332
|
+
ftl.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
2333
|
+
btn.setAttribute('value', value);
|
|
2334
|
+
btn.innerHTML = target.innerHTML;
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
get value() {
|
|
2339
|
+
const operator = this.#operator.getAttribute('value');
|
|
2340
|
+
const values = operator === 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
2341
|
+
return values.some(v => v === '') ? undefined : [operator, ...values.map(v => new Date(v).toISOString())];
|
|
2342
|
+
}
|
|
2343
|
+
set value(v) {
|
|
2344
|
+
if (v === null || v === undefined) {
|
|
2345
|
+
this.#value1.value = '';
|
|
2346
|
+
this.#value2.value = '';
|
|
2347
|
+
this.reflect(() => {
|
|
2348
|
+
this.removeAttribute('value');
|
|
2349
|
+
});
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const [operator, ...values] = v;
|
|
2353
|
+
this.#operator.setAttribute('value', operator);
|
|
2354
|
+
this.#value1.value = values[0] ? InstantFilter.isoToLocal(values[0]) : values[0];
|
|
2355
|
+
this.#value2.value = values[1] ? InstantFilter.isoToLocal(values[1]) : values[1];
|
|
2356
|
+
this.reflect(() => {
|
|
2357
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
static isoToLocal(iso) {
|
|
2362
|
+
//this is so sad
|
|
2363
|
+
const d = new Date(iso);
|
|
2364
|
+
const pad = (n, v) => String(v).padStart(n, '0');
|
|
2365
|
+
const date = `${d.getFullYear()}-${pad(2, d.getMonth() + 1)}-${pad(2, d.getDate())}`;
|
|
2366
|
+
const time = `${pad(2, d.getHours())}:${pad(2, d.getMinutes())}:${pad(2, d.getSeconds())}.${pad(3, d.getMilliseconds())}`;
|
|
2367
|
+
return `${date}T${time}`
|
|
2368
|
+
}
|
|
2369
|
+
focus(options) {
|
|
2370
|
+
this.#value1.focus(options);
|
|
2371
|
+
}
|
|
2372
|
+
setCustomValidity(error) {
|
|
2373
|
+
if (!error) {
|
|
2374
|
+
this.internals.setValidity({});
|
|
2375
|
+
this.#fieldError.innerText = "";
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2379
|
+
this.#fieldError.innerText = error;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
class LocalDateFilter extends ftl.ParsedElement {
|
|
2384
|
+
static observed = ["value:json"];
|
|
2385
|
+
static slots = true;
|
|
2386
|
+
static template = `
|
|
2387
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2388
|
+
<div class="input-group">
|
|
2389
|
+
<button data-ref="operator" class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" value="EQ" form="">=</button>
|
|
2390
|
+
<ul class="dropdown-menu">
|
|
2391
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2392
|
+
<li><a class="dropdown-item" role="button" value="NEQ">≠</a></li>
|
|
2393
|
+
<li><a class="dropdown-item" role="button" value="LT">≺</a></li>
|
|
2394
|
+
<li><a class="dropdown-item" role="button" value="GT">≻</a></li>
|
|
2395
|
+
<li><a class="dropdown-item" role="button" value="LTE">≼</a></li>
|
|
2396
|
+
<li><a class="dropdown-item" role="button" value="GTE">≽</a></li>
|
|
2397
|
+
<li><a class="dropdown-item" role="button" value="BETWEEN">↔</a></li>
|
|
2398
|
+
</ul>
|
|
2399
|
+
<input data-tpl-id="id" data-ref="value1" type="date" class="form-control" form="">
|
|
2400
|
+
<input data-ref="value2" type="date" class="form-control" form="" hidden>
|
|
2401
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2402
|
+
|
|
2403
|
+
</div>
|
|
2404
|
+
<ful-field-error></ful-field-error>
|
|
2405
|
+
`;
|
|
2406
|
+
static formAssociated = true;
|
|
2407
|
+
#operator;
|
|
2408
|
+
#value1;
|
|
2409
|
+
#value2;
|
|
2410
|
+
#fieldError;
|
|
2411
|
+
constructor() {
|
|
2412
|
+
super();
|
|
2413
|
+
this.internals = this.attachInternals();
|
|
2414
|
+
}
|
|
2415
|
+
render({ slots }) {
|
|
2416
|
+
const id = ftl.Attributes.uid('instant-filter');
|
|
2417
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2418
|
+
const name = this.getAttribute("name");
|
|
2419
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2420
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2421
|
+
this.#value1 = fragment.querySelector('[data-ref=value1]');
|
|
2422
|
+
this.#value2 = fragment.querySelector('[data-ref=value2]');
|
|
2423
|
+
this.replaceChildren(fragment);
|
|
2424
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2425
|
+
this.addEventListener('click', (evt) => {
|
|
2426
|
+
const target = /** @type HTMLElement */(evt.target);
|
|
2427
|
+
if (!target.matches('ul > li > a')) {
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2431
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2432
|
+
ftl.Attributes.toggle(this.#value2, 'hidden', value !== 'BETWEEN');
|
|
2433
|
+
btn.setAttribute('value', value);
|
|
2434
|
+
btn.innerHTML = target.innerHTML;
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
get value() {
|
|
2438
|
+
const operator = this.#operator.getAttribute('value');
|
|
2439
|
+
const values = operator == 'BETWEEN' ? [this.#value1.value, this.#value2.value] : [this.#value1.value];
|
|
2440
|
+
return values.some(v => v === '') ? undefined : [operator, "ISO_8601", ...values];
|
|
2441
|
+
}
|
|
2442
|
+
set value(v) {
|
|
2443
|
+
if (v === null || v === undefined) {
|
|
2444
|
+
this.#value1.value = '';
|
|
2445
|
+
this.#value2.value = '';
|
|
2446
|
+
this.reflect(() => {
|
|
2447
|
+
this.removeAttribute('value');
|
|
2448
|
+
});
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
const [operator, ...values] = v;
|
|
2452
|
+
this.#operator.setAttibute('value', operator);
|
|
2453
|
+
this.#value1.value = values[0];
|
|
2454
|
+
this.#value2.value = values[1];
|
|
2455
|
+
this.reflect(() => {
|
|
2456
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
focus(options) {
|
|
2460
|
+
this.#value1.focus(options);
|
|
2461
|
+
}
|
|
2462
|
+
setCustomValidity(error) {
|
|
2463
|
+
if (!error) {
|
|
2464
|
+
this.internals.setValidity({});
|
|
2465
|
+
this.#fieldError.innerText = "";
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2469
|
+
this.#fieldError.innerText = error;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
class TextFilter extends ftl.ParsedElement {
|
|
2474
|
+
static observed = ["value:json"];
|
|
2475
|
+
static slots = true;
|
|
2476
|
+
static template = `
|
|
2477
|
+
<label data-tpl-for="id" class="form-label" data-tpl-if="label">{{{{ label }}}}</label>
|
|
2478
|
+
<div class="input-group">
|
|
2479
|
+
<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>
|
|
2480
|
+
<ul class="dropdown-menu">
|
|
2481
|
+
<li><a class="dropdown-item" role="button" value="CONTAINS">…a…</a></li>
|
|
2482
|
+
<li><a class="dropdown-item" role="button" value="STARTS_WITH">a…</a></li>
|
|
2483
|
+
<li><a class="dropdown-item" role="button" value="ENDS_WITH">…a</a></li>
|
|
2484
|
+
<li><a class="dropdown-item" role="button" value="EQ">=</a></li>
|
|
2485
|
+
</ul>
|
|
2486
|
+
<input data-tpl-id="id" data-ref="value" type="text" class="form-control" form="">
|
|
2487
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
2488
|
+
</div>
|
|
2489
|
+
<ful-field-error></ful-field-error>
|
|
2490
|
+
`;
|
|
2491
|
+
static formAssociated = true;
|
|
2492
|
+
#operator;
|
|
2493
|
+
#value;
|
|
2494
|
+
#fieldError;
|
|
2495
|
+
constructor() {
|
|
2496
|
+
super();
|
|
2497
|
+
this.internals = this.attachInternals();
|
|
2498
|
+
}
|
|
2499
|
+
render({ slots }) {
|
|
2500
|
+
const id = ftl.Attributes.uid('string-filter');
|
|
2501
|
+
const label = ftl.Fragments.toHtml(slots.default.cloneNode(true)).trim().length === 0 ? null : slots.default;
|
|
2502
|
+
const name = this.getAttribute("name");
|
|
2503
|
+
const fragment = this.template().withOverlay({ id, label, name }).render(this);
|
|
2504
|
+
this.#operator = fragment.querySelector('[data-ref=operator]');
|
|
2505
|
+
this.#value = fragment.querySelector('[data-ref=value]');
|
|
2506
|
+
this.replaceChildren(fragment);
|
|
2507
|
+
this.#fieldError = this.querySelector('ful-field-error');
|
|
2508
|
+
this.addEventListener('click', (evt) => {
|
|
2509
|
+
const target = /** @type HTMLElement */(evt.target);
|
|
2510
|
+
if (!target.matches('ul > li > a')) {
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const btn = /** @type HTMLButtonElement */ (target.closest('ul')?.previousElementSibling);
|
|
2514
|
+
const value = /** @type String */ (target.getAttribute("value"));
|
|
2515
|
+
btn.setAttribute('value', value);
|
|
2516
|
+
btn.innerHTML = target.innerHTML;
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
get value() {
|
|
2521
|
+
const operator = this.#operator.getAttribute('value');
|
|
2522
|
+
return this.#value.value === '' ? undefined : [operator, 'IGNORE_CASE', this.#value.value];
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
set value(v) {
|
|
2526
|
+
if (v === null || v === undefined) {
|
|
2527
|
+
this.#value.value = '';
|
|
2528
|
+
this.reflect(() => {
|
|
2529
|
+
this.removeAttribute('value');
|
|
2530
|
+
});
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const [operator, sensitivity, value] = v;
|
|
2534
|
+
this.#operator.setAttribute('value', operator);
|
|
2535
|
+
this.#value.value = value;
|
|
2536
|
+
this.reflect(() => {
|
|
2537
|
+
this.setAttribute('value', JSON.stringify(v));
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
focus(options) {
|
|
2541
|
+
this.#value.focus(options);
|
|
2542
|
+
}
|
|
2543
|
+
setCustomValidity(error) {
|
|
2544
|
+
if (!error) {
|
|
2545
|
+
this.internals.setValidity({});
|
|
2546
|
+
this.#fieldError.innerText = "";
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
this.internals.setValidity({ customError: true }, " ");
|
|
2550
|
+
this.#fieldError.innerText = error;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
class Plugin {
|
|
2555
|
+
configure(registry) {
|
|
2556
|
+
const httpClient = HttpClient.builder()
|
|
2557
|
+
.withCsrfToken()
|
|
2558
|
+
.withRedirectOnUnauthorized("/")
|
|
2559
|
+
.build();
|
|
2560
|
+
registry
|
|
2561
|
+
.defineComponent('http-client', httpClient)
|
|
2562
|
+
.defineElement('ful-spinner', Spinner)
|
|
2563
|
+
.defineElement('ful-form', Form)
|
|
2564
|
+
.defineElement('ful-checkbox', Checkbox)
|
|
2565
|
+
.defineElement('ful-input', Input)
|
|
2566
|
+
.defineElement('ful-radio-group', RadioGroup)
|
|
2567
|
+
.defineElement('ful-table', Table)
|
|
2568
|
+
.defineElement('ful-pagination', Pagination)
|
|
2569
|
+
.defineElement('ful-sorter', SortButton)
|
|
2570
|
+
.defineElement('ful-filter-instant', InstantFilter)
|
|
2571
|
+
.defineElement('ful-filter-local-date', LocalDateFilter)
|
|
2572
|
+
.defineElement('ful-filter-text', TextFilter)
|
|
2573
|
+
.defineElement('ful-select', Select)
|
|
2574
|
+
.defineElement('ful-dropdown', Dropdown)
|
|
2575
|
+
.defineComponent("loaders:select", SelectLoader)
|
|
2576
|
+
.defineComponent("loaders:form", FormLoader)
|
|
2577
|
+
.defineComponent("loaders:table", TableLoader);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
1538
2581
|
exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
|
|
1539
2582
|
exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
|
|
1540
2583
|
exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
|
|
1541
2584
|
exports.Base64 = Base64;
|
|
1542
2585
|
exports.Bindings = Bindings;
|
|
2586
|
+
exports.Checkbox = Checkbox;
|
|
2587
|
+
exports.Dropdown = Dropdown;
|
|
1543
2588
|
exports.Failure = Failure;
|
|
1544
2589
|
exports.Form = Form;
|
|
2590
|
+
exports.FormLoader = FormLoader;
|
|
1545
2591
|
exports.Hex = Hex;
|
|
1546
2592
|
exports.HttpClient = HttpClient;
|
|
1547
2593
|
exports.HttpClientError = HttpClientError;
|
|
1548
|
-
exports.INPUT_TEMPLATE = INPUT_TEMPLATE;
|
|
1549
2594
|
exports.Input = Input;
|
|
2595
|
+
exports.InstantFilter = InstantFilter;
|
|
2596
|
+
exports.Loaders = Loaders;
|
|
2597
|
+
exports.LocalDateFilter = LocalDateFilter;
|
|
1550
2598
|
exports.LocalStorage = LocalStorage;
|
|
1551
2599
|
exports.MediaType = MediaType;
|
|
2600
|
+
exports.Pagination = Pagination;
|
|
2601
|
+
exports.Plugin = Plugin;
|
|
1552
2602
|
exports.RadioGroup = RadioGroup;
|
|
1553
2603
|
exports.Select = Select;
|
|
2604
|
+
exports.SelectLoader = SelectLoader;
|
|
1554
2605
|
exports.SessionStorage = SessionStorage;
|
|
2606
|
+
exports.SortButton = SortButton;
|
|
1555
2607
|
exports.Spinner = Spinner;
|
|
2608
|
+
exports.Table = Table;
|
|
2609
|
+
exports.TableSchemaParser = TableSchemaParser;
|
|
2610
|
+
exports.TextFilter = TextFilter;
|
|
1556
2611
|
exports.VersionedStorage = VersionedStorage;
|
|
1557
|
-
exports.makeInputFragment = makeInputFragment;
|
|
1558
2612
|
exports.timing = timing;
|
|
1559
2613
|
|
|
1560
2614
|
return exports;
|
|
1561
2615
|
|
|
1562
|
-
})({}, ftl
|
|
2616
|
+
})({}, ftl);
|
|
1563
2617
|
//# sourceMappingURL=ful.iife.js.map
|