@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.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ParsedElement, Attributes, Fragments } from '@optionfactory/ftl';
|
|
2
|
-
import TomSelect from 'tom-select';
|
|
1
|
+
import { registry, ParsedElement, Attributes, Fragments, Nodes } from '@optionfactory/ftl';
|
|
3
2
|
|
|
4
3
|
class Base64 {
|
|
5
4
|
static encode(arrayBuffer, dialect) {
|
|
@@ -66,10 +65,10 @@ class Hex {
|
|
|
66
65
|
}
|
|
67
66
|
static encode(bytes, upper) {
|
|
68
67
|
return Array.from(bytes)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
.map(b => b.toString(16))
|
|
69
|
+
.map(b => upper ? b.toUpperCase() : b)
|
|
70
|
+
.map(o => o.padStart(2, 0))
|
|
71
|
+
.join('');
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
|
|
@@ -121,7 +120,6 @@ class MediaType {
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
|
|
125
123
|
/**
|
|
126
124
|
* @typedef {Int8Array| Uint8Array| Uint8ClampedArray| Int16Array| Uint16Array| Int32Array| Uint32Array| Float32Array| Float64Array| BigInt64Array| BigUint64Array} TypedArray
|
|
127
125
|
*/
|
|
@@ -390,7 +388,6 @@ const unmarshal = async (response, type) => {
|
|
|
390
388
|
}
|
|
391
389
|
};
|
|
392
390
|
|
|
393
|
-
|
|
394
391
|
class HttpRequestBuilder {
|
|
395
392
|
#client;
|
|
396
393
|
#method;
|
|
@@ -451,7 +448,7 @@ class HttpRequestBuilder {
|
|
|
451
448
|
this.#headers.delete(k);
|
|
452
449
|
} else {
|
|
453
450
|
this.#headers.set(k, v);
|
|
454
|
-
}
|
|
451
|
+
}
|
|
455
452
|
}
|
|
456
453
|
return this;
|
|
457
454
|
}
|
|
@@ -485,16 +482,17 @@ class HttpRequestBuilder {
|
|
|
485
482
|
return this;
|
|
486
483
|
}
|
|
487
484
|
/**
|
|
488
|
-
* Adds a query parameter to the request, overriding it if it already exists.
|
|
485
|
+
* 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
486
|
* @param {string} k
|
|
490
|
-
* @param {string}
|
|
487
|
+
* @param {...string} vs
|
|
491
488
|
* @returns {HttpRequestBuilder} this builder
|
|
492
489
|
*/
|
|
493
|
-
param(k,
|
|
494
|
-
if (
|
|
490
|
+
param(k, ...vs) {
|
|
491
|
+
if (vs.length === 0 || vs[0] === null || vs[0] === undefined) {
|
|
495
492
|
this.#params.delete(k);
|
|
496
|
-
}
|
|
497
|
-
|
|
493
|
+
}
|
|
494
|
+
for (const v of vs) {
|
|
495
|
+
this.#params.append(k, v);
|
|
498
496
|
}
|
|
499
497
|
return this;
|
|
500
498
|
}
|
|
@@ -539,7 +537,6 @@ class HttpRequestBuilder {
|
|
|
539
537
|
*/
|
|
540
538
|
options(kvs) {
|
|
541
539
|
for (const [k, v] of Object.entries(kvs)) {
|
|
542
|
-
// @ts-ignore
|
|
543
540
|
this.#options[k] = v;
|
|
544
541
|
}
|
|
545
542
|
return this;
|
|
@@ -647,7 +644,6 @@ class HttpRequestBuilder {
|
|
|
647
644
|
}
|
|
648
645
|
}
|
|
649
646
|
|
|
650
|
-
|
|
651
647
|
class HttpMultipartRequestCustomizer {
|
|
652
648
|
#formData;
|
|
653
649
|
/**
|
|
@@ -744,14 +740,14 @@ class SessionStorage extends Storage {
|
|
|
744
740
|
}
|
|
745
741
|
|
|
746
742
|
class VersionedStorage {
|
|
747
|
-
constructor(storage, key, dataSupplier){
|
|
743
|
+
constructor(storage, key, dataSupplier) {
|
|
748
744
|
this.storage = storage;
|
|
749
745
|
this.key = key;
|
|
750
746
|
this.dataSupplier = dataSupplier;
|
|
751
747
|
this.cache = null;
|
|
752
|
-
|
|
748
|
+
|
|
753
749
|
}
|
|
754
|
-
async load(revision){
|
|
750
|
+
async load(revision) {
|
|
755
751
|
const saved = this.storage.load(this.key);
|
|
756
752
|
if (!!saved && saved.revision === revision) {
|
|
757
753
|
this.cache = saved.value;
|
|
@@ -764,13 +760,13 @@ class VersionedStorage {
|
|
|
764
760
|
});
|
|
765
761
|
this.cache = freshData;
|
|
766
762
|
}
|
|
767
|
-
data(){
|
|
763
|
+
data() {
|
|
768
764
|
return this.cache;
|
|
769
765
|
}
|
|
770
766
|
}
|
|
771
767
|
|
|
772
768
|
class AuthorizationCodeFlow {
|
|
773
|
-
static forKeycloak(clientId, realmBaseUrl, redirectUri){
|
|
769
|
+
static forKeycloak(clientId, realmBaseUrl, redirectUri) {
|
|
774
770
|
const scope = "openid profile";
|
|
775
771
|
return new AuthorizationCodeFlow(clientId, scope, {
|
|
776
772
|
auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
|
|
@@ -778,15 +774,15 @@ class AuthorizationCodeFlow {
|
|
|
778
774
|
logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
|
|
779
775
|
registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
|
|
780
776
|
redirect: redirectUri
|
|
781
|
-
});
|
|
777
|
+
});
|
|
782
778
|
}
|
|
783
|
-
constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
|
|
779
|
+
constructor(clientId, scope, { auth, token, registration, logout, redirect }) {
|
|
784
780
|
this.storage = new SessionStorage(clientId);
|
|
785
781
|
this.clientId = clientId;
|
|
786
782
|
this.scope = scope;
|
|
787
|
-
this.uri = {auth, token, registration, logout, redirect};
|
|
783
|
+
this.uri = { auth, token, registration, logout, redirect };
|
|
788
784
|
}
|
|
789
|
-
async action(uri, additionalParams){
|
|
785
|
+
async action(uri, additionalParams) {
|
|
790
786
|
const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
|
|
791
787
|
const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
|
|
792
788
|
const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
|
|
@@ -807,10 +803,10 @@ class AuthorizationCodeFlow {
|
|
|
807
803
|
});
|
|
808
804
|
window.location.href = url.toString();
|
|
809
805
|
}
|
|
810
|
-
async registration(additionalParams){
|
|
806
|
+
async registration(additionalParams) {
|
|
811
807
|
await this.action(this.uri.registration, additionalParams);
|
|
812
808
|
}
|
|
813
|
-
async applicationInitiatedAction(kcAction, additionalParams){
|
|
809
|
+
async applicationInitiatedAction(kcAction, additionalParams) {
|
|
814
810
|
await this.action(this.uri.auth, {
|
|
815
811
|
...additionalParams,
|
|
816
812
|
kc_action: kcAction,
|
|
@@ -867,8 +863,8 @@ class AuthorizationCodeFlowSession {
|
|
|
867
863
|
payload: JSON.parse(utf8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
|
|
868
864
|
signature: signature
|
|
869
865
|
};
|
|
870
|
-
}
|
|
871
|
-
constructor(clientId, t, {token, logout, redirect}) {
|
|
866
|
+
}
|
|
867
|
+
constructor(clientId, t, { token, logout, redirect }) {
|
|
872
868
|
this.clientId = clientId;
|
|
873
869
|
this.token = t;
|
|
874
870
|
this.idToken = AuthorizationCodeFlowSession.parseToken(t.id_token);
|
|
@@ -928,9 +924,9 @@ class AuthorizationCodeFlowSession {
|
|
|
928
924
|
bearerToken() {
|
|
929
925
|
return `Bearer ${this.token.access_token}`;
|
|
930
926
|
}
|
|
931
|
-
|
|
932
|
-
interceptor(gracePeriodBefore, gracePeriodAfter){
|
|
933
|
-
return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
|
|
927
|
+
|
|
928
|
+
interceptor(gracePeriodBefore, gracePeriodAfter) {
|
|
929
|
+
return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
|
|
934
930
|
}
|
|
935
931
|
}
|
|
936
932
|
|
|
@@ -958,11 +954,21 @@ const timing = {
|
|
|
958
954
|
},
|
|
959
955
|
DEBOUNCE_DEFAULT: 0,
|
|
960
956
|
DEBOUNCE_IMMEDIATE: 1,
|
|
957
|
+
/**
|
|
958
|
+
* Executes only after a period of inactivity (pause in events).
|
|
959
|
+
* Delays execution until events stop for a set duration.
|
|
960
|
+
* Consolidates multiple rapid events into a single execution.
|
|
961
|
+
* Respond to the "end" of a series of events.
|
|
962
|
+
* @param {*} timeoutMs
|
|
963
|
+
* @param {*} func
|
|
964
|
+
* @param {*} [options]
|
|
965
|
+
* @returns {[function, function]}
|
|
966
|
+
*/
|
|
961
967
|
debounce(timeoutMs, func, options) {
|
|
968
|
+
const opts = options ?? timing.DEBOUNCE_DEFAULT;
|
|
962
969
|
let tid = null;
|
|
963
970
|
let args = [];
|
|
964
971
|
let previousTimestamp = 0;
|
|
965
|
-
let opts = options || timing.DEBOUNCE_DEFAULT;
|
|
966
972
|
|
|
967
973
|
const later = () => {
|
|
968
974
|
const elapsed = new Date().getTime() - previousTimestamp;
|
|
@@ -980,7 +986,7 @@ const timing = {
|
|
|
980
986
|
}
|
|
981
987
|
};
|
|
982
988
|
|
|
983
|
-
|
|
989
|
+
const debounced = function () {
|
|
984
990
|
args = [...arguments];
|
|
985
991
|
previousTimestamp = new Date().getTime();
|
|
986
992
|
if (tid === null) {
|
|
@@ -990,15 +996,23 @@ const timing = {
|
|
|
990
996
|
}
|
|
991
997
|
}
|
|
992
998
|
};
|
|
999
|
+
const abort = () => clearTimeout(tid);
|
|
1000
|
+
return [debounced, abort];
|
|
993
1001
|
},
|
|
994
1002
|
THROTTLE_DEFAULT: 0,
|
|
995
1003
|
THROTTLE_NO_LEADING: 1,
|
|
996
1004
|
THROTTLE_NO_TRAILING: 2,
|
|
1005
|
+
/**
|
|
1006
|
+
* Executes at most once per specified time interval, regardless of ongoing events.
|
|
1007
|
+
* Executes regularly as long as events are firing, but at a controlled rate.
|
|
1008
|
+
* Allows execution periodically during a burst of events.
|
|
1009
|
+
* Ensure a function doesn't fire too frequently during continuous events.
|
|
1010
|
+
*/
|
|
997
1011
|
throttle(timeoutMs, func, options) {
|
|
1012
|
+
const opts = options ?? timing.THROTTLE_DEFAULT;
|
|
998
1013
|
let tid = null;
|
|
999
1014
|
let args = [];
|
|
1000
1015
|
let previousTimestamp = 0;
|
|
1001
|
-
let opts = options || timing.THROTTLE_DEFAULT;
|
|
1002
1016
|
|
|
1003
1017
|
const later = () => {
|
|
1004
1018
|
previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
|
|
@@ -1008,8 +1022,7 @@ const timing = {
|
|
|
1008
1022
|
args = [];
|
|
1009
1023
|
}
|
|
1010
1024
|
};
|
|
1011
|
-
|
|
1012
|
-
return function () {
|
|
1025
|
+
const throttled = function () {
|
|
1013
1026
|
const now = new Date().getTime();
|
|
1014
1027
|
if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
|
|
1015
1028
|
previousTimestamp = now;
|
|
@@ -1030,10 +1043,27 @@ const timing = {
|
|
|
1030
1043
|
tid = setTimeout(later, remaining);
|
|
1031
1044
|
}
|
|
1032
1045
|
};
|
|
1033
|
-
|
|
1046
|
+
const abort = () => clearTimeout(tid);
|
|
1047
|
+
return [throttled, abort];
|
|
1034
1048
|
}
|
|
1035
1049
|
};
|
|
1036
1050
|
|
|
1051
|
+
class Loaders {
|
|
1052
|
+
static fromAttributes(el, defaultLoader, options) {
|
|
1053
|
+
const http = registry.component("http-client");
|
|
1054
|
+
const requestMapper = el.hasAttribute("request-mapper") ? registry.component(el.getAttribute("request-mapper")) : v => v;
|
|
1055
|
+
const responseMapper = el.hasAttribute("response-mapper") ? registry.component(el.getAttribute("response-mapper")) : v => v;
|
|
1056
|
+
const loaderClass = registry.component(el.getAttribute("loader") ?? defaultLoader);
|
|
1057
|
+
return loaderClass.create({
|
|
1058
|
+
el,
|
|
1059
|
+
http,
|
|
1060
|
+
requestMapper,
|
|
1061
|
+
responseMapper,
|
|
1062
|
+
options: options ?? {}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1037
1067
|
class Bindings {
|
|
1038
1068
|
|
|
1039
1069
|
/**
|
|
@@ -1108,13 +1138,15 @@ class Bindings {
|
|
|
1108
1138
|
return el.value;
|
|
1109
1139
|
}
|
|
1110
1140
|
|
|
1111
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
*
|
|
1143
|
+
* @param {HTMLFormElement} form
|
|
1144
|
+
* @returns
|
|
1145
|
+
*/
|
|
1146
|
+
static extractFrom(form){
|
|
1112
1147
|
let result = {};
|
|
1113
|
-
for(const el of
|
|
1114
|
-
if
|
|
1115
|
-
continue;
|
|
1116
|
-
}
|
|
1117
|
-
if(ignoredChildrenSelector && el.dataset['fulBindInclude'] !== 'always' && el.closest(ignoredChildrenSelector) !== null){
|
|
1148
|
+
for(const el of form.elements){
|
|
1149
|
+
if(!el.hasAttribute("name") || el.matches(":disabled")){
|
|
1118
1150
|
continue;
|
|
1119
1151
|
}
|
|
1120
1152
|
result = Bindings.providePath(result, /** @type {string} */(el.getAttribute('name')), Bindings.extract(el));
|
|
@@ -1139,70 +1171,140 @@ class Bindings {
|
|
|
1139
1171
|
el.value = raw;
|
|
1140
1172
|
}
|
|
1141
1173
|
|
|
1142
|
-
static mutateIn(
|
|
1174
|
+
static mutateIn(form, values){
|
|
1143
1175
|
for (const [flattenedKey, value] of Object.entries(Bindings.flatten(values, ''))) {
|
|
1144
|
-
for(const el of
|
|
1176
|
+
for(const el of form.querySelectorAll(`[name='${CSS.escape(flattenedKey)}']`)){
|
|
1145
1177
|
Bindings.mutate(el, value);
|
|
1146
1178
|
}
|
|
1147
1179
|
}
|
|
1148
1180
|
}
|
|
1149
1181
|
|
|
1150
1182
|
|
|
1151
|
-
static errors(
|
|
1183
|
+
static errors(form, es, scrollOnError){
|
|
1152
1184
|
const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
1153
1185
|
const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
1154
|
-
|
|
1155
|
-
|
|
1186
|
+
form.querySelectorAll(`[name]`).forEach(el => el.setCustomValidity?.(""));
|
|
1187
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1156
1188
|
el.replaceChildren();
|
|
1157
1189
|
el.setAttribute('hidden', '');
|
|
1158
1190
|
});
|
|
1159
1191
|
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
|
-
});
|
|
1192
|
+
const name = e.context.replace("[", ".").replace("].", ".").replace("]", "");
|
|
1193
|
+
form.querySelectorAll(`[name='${CSS.escape(name)}']`).forEach(input => input.setCustomValidity?.(e.reason));
|
|
1168
1194
|
});
|
|
1169
|
-
|
|
1195
|
+
form.querySelectorAll("ful-errors").forEach(el => {
|
|
1170
1196
|
const hel = /** @type HTMLElement} */ (el);
|
|
1171
1197
|
hel.innerText = globalErrors.map(e => e.reason).join("\n");
|
|
1172
1198
|
if (globalErrors.length !== 0) {
|
|
1173
1199
|
el.removeAttribute('hidden');
|
|
1174
1200
|
}
|
|
1175
1201
|
});
|
|
1202
|
+
if (es.length == 0 || !scrollOnError) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
Array.from(form.querySelectorAll(`:invalid`)).sort((a,b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y)[0]?.focus();
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1176
1208
|
|
|
1209
|
+
class RemoteJsonFormLoader {
|
|
1210
|
+
#http;
|
|
1211
|
+
#url;
|
|
1212
|
+
#method;
|
|
1213
|
+
#requestMapper;
|
|
1214
|
+
#responseMapper;
|
|
1215
|
+
constructor(http, url, method, requestMapper, responseMapper) {
|
|
1216
|
+
this.#http = http;
|
|
1217
|
+
this.#url = url;
|
|
1218
|
+
this.#method = method;
|
|
1219
|
+
this.#requestMapper = requestMapper;
|
|
1220
|
+
this.#responseMapper = responseMapper;
|
|
1221
|
+
}
|
|
1222
|
+
prepare(values, form) {
|
|
1223
|
+
return this.#requestMapper(values, form);
|
|
1224
|
+
}
|
|
1225
|
+
async submit(values, form) {
|
|
1226
|
+
return await this.#http.request(this.#method, this.#url)
|
|
1227
|
+
.json(values)
|
|
1228
|
+
.fetch()
|
|
1229
|
+
}
|
|
1230
|
+
transform(response, form) {
|
|
1231
|
+
return this.#responseMapper(response, form);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1177
1234
|
|
|
1235
|
+
class LocalFormLoader {
|
|
1236
|
+
#requestMapper;
|
|
1237
|
+
#responseMapper;
|
|
1238
|
+
constructor(requestMapper, responseMapper) {
|
|
1239
|
+
this.#requestMapper = requestMapper;
|
|
1240
|
+
this.#responseMapper = responseMapper;
|
|
1241
|
+
}
|
|
1242
|
+
async prepare(values, form) {
|
|
1243
|
+
return await this.#requestMapper(values, form);
|
|
1244
|
+
}
|
|
1245
|
+
async submit(values, form) {
|
|
1246
|
+
return values;
|
|
1247
|
+
}
|
|
1248
|
+
async transform(response, form) {
|
|
1249
|
+
return await this.#responseMapper(response, form);
|
|
1178
1250
|
}
|
|
1179
1251
|
}
|
|
1180
1252
|
|
|
1181
|
-
class
|
|
1182
|
-
static
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1253
|
+
class FormLoader {
|
|
1254
|
+
static create({ el, http, requestMapper, responseMapper }) {
|
|
1255
|
+
const url = el.getAttribute("action");
|
|
1256
|
+
if (!url) {
|
|
1257
|
+
return new LocalFormLoader(requestMapper, responseMapper);
|
|
1258
|
+
}
|
|
1259
|
+
const method = el.getAttribute("method") ?? 'POST';
|
|
1260
|
+
return new RemoteJsonFormLoader(http, url, method, requestMapper, responseMapper);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
class Form extends ParsedElement {
|
|
1265
|
+
form;
|
|
1186
1266
|
render() {
|
|
1187
|
-
const form = document.createElement('form');
|
|
1267
|
+
const form = this.form = document.createElement('form');
|
|
1268
|
+
form.setAttribute("novalidate", "");
|
|
1188
1269
|
Attributes.forward('form-', this, form);
|
|
1189
1270
|
form.replaceChildren(...this.childNodes);
|
|
1190
1271
|
form.addEventListener('submit', async (e) => {
|
|
1191
1272
|
e.preventDefault();
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
});
|
|
1273
|
+
e.stopPropagation();
|
|
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 @@ class Form extends ParsedElement() {
|
|
|
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 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 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 = 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
|
+
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 = 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 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 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 = 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 ParsedElement
|
|
1444
|
-
observed
|
|
1445
|
-
slots
|
|
1446
|
-
template
|
|
1447
|
-
<fieldset
|
|
1759
|
+
class RadioGroup extends 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>
|
|
@@ -1455,18 +1771,24 @@ class RadioGroup extends ParsedElement({
|
|
|
1455
1771
|
<div class="label-wrapper" data-tpl-each="inputsAndLabels" data-tpl-var="ial">
|
|
1456
1772
|
<label>
|
|
1457
1773
|
{{{{ ial[0] }}}}
|
|
1458
|
-
{{{{ ial[1] }}}}
|
|
1774
|
+
<div>{{{{ ial[1] }}}}</div>
|
|
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') ?? 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 @@ class RadioGroup extends ParsedElement({
|
|
|
1475
1797
|
Attributes.forward('input-', this, input);
|
|
1476
1798
|
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 @@ class RadioGroup extends ParsedElement({
|
|
|
1493
1814
|
});
|
|
1494
1815
|
|
|
1495
1816
|
radioEls.forEach(el => el.remove());
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1817
|
+
const fieldErrorId = 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(() => 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,21 +1837,745 @@ class RadioGroup extends ParsedElement({
|
|
|
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 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 = 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
|
+
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 ParsedElement
|
|
1525
|
-
slots
|
|
1526
|
-
template
|
|
1911
|
+
class Spinner extends 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
|
|
|
1538
|
-
|
|
1924
|
+
class SortButton extends 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 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 = Nodes.queryChildren(nodeOrFragment, "schema");
|
|
2051
|
+
if (!schema) {
|
|
2052
|
+
throw new Error(`missing expected <schema> in ${nodeOrFragment}`);
|
|
2053
|
+
}
|
|
2054
|
+
return 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 = Nodes.queryChildren(el, 'title');
|
|
2067
|
+
if (maybeTitleTag) {
|
|
2068
|
+
maybeTitleTag.remove();
|
|
2069
|
+
}
|
|
2070
|
+
const fragment = maybeTitleTag ? template.withFragment(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(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 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 */ (Nodes.queryChildren(fragment, 'table'));
|
|
2191
|
+
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 = Nodes.queryChildren(fragment, 'ful-pagination');
|
|
2198
|
+
this.#sorters = table.querySelectorAll(':scope > thead ful-sorter') ?? [];
|
|
2199
|
+
this.replaceChildren(fragment);
|
|
2200
|
+
await Nodes.waitForUpgrades();
|
|
2201
|
+
const orderFromSchema = schema.find(v => v.order);
|
|
2202
|
+
|
|
2203
|
+
const maybeForm = /** @type any */(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 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 = Attributes.uid('instant-filter');
|
|
2316
|
+
const label = 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
|
+
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 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 = Attributes.uid('instant-filter');
|
|
2416
|
+
const label = 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
|
+
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 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 = Attributes.uid('string-filter');
|
|
2500
|
+
const label = 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
|
+
|
|
2580
|
+
export { AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Bindings, Checkbox, Dropdown, Failure, Form, FormLoader, Hex, HttpClient, HttpClientError, Input, InstantFilter, Loaders, LocalDateFilter, LocalStorage, MediaType, Pagination, Plugin, RadioGroup, Select, SelectLoader, SessionStorage, SortButton, Spinner, Table, TableSchemaParser, TextFilter, VersionedStorage, timing };
|
|
1539
2581
|
//# sourceMappingURL=ful.mjs.map
|