@optionfactory/ful 0.90.0 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ful.iife.js +209 -75
- 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 +209 -75
- package/dist/ful.mjs.map +1 -1
- package/package.json +7 -4
package/dist/ful.mjs
CHANGED
|
@@ -70,21 +70,51 @@ class Hex {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
|
|
75
|
+
*/
|
|
73
76
|
class Failure extends Error {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
*
|
|
79
|
+
* @param {string} message
|
|
80
|
+
* @param {Problem[]} problems
|
|
81
|
+
* @param {*} cause
|
|
82
|
+
*/
|
|
83
|
+
constructor(message, problems, cause) {
|
|
84
|
+
super(message, { cause });
|
|
85
|
+
this.name = 'Failure';
|
|
77
86
|
this.problems = problems;
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {Int8Array| Uint8Array| Uint8ClampedArray| Int16Array| Uint16Array| Int32Array| Uint32Array| Float32Array| Float64Array| BigInt64Array| BigUint64Array} TypedArray
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* @typedef HttpInterceptor
|
|
95
|
+
* @property {function(Request,HttpInterceptorChain):Promise<Response>} intercept
|
|
96
|
+
*/
|
|
97
|
+
|
|
81
98
|
class HttpClientError extends Failure {
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} message
|
|
101
|
+
* @param {number} status
|
|
102
|
+
* @param {{ type: string; context: string?; reason: string; details: any?; }[]} problems
|
|
103
|
+
* @param {Error|undefined} [cause]
|
|
104
|
+
*/
|
|
105
|
+
constructor(message, status, problems, cause) {
|
|
106
|
+
super(message, problems, cause);
|
|
107
|
+
this.name = 'HttpClientError';
|
|
84
108
|
this.status = status;
|
|
85
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
*
|
|
112
|
+
* @param {string} type
|
|
113
|
+
* @param {any} cause
|
|
114
|
+
* @returns
|
|
115
|
+
*/
|
|
86
116
|
static of(type, cause) {
|
|
87
|
-
return new HttpClientError(0, [{
|
|
117
|
+
return new HttpClientError(cause.message, 0, [{
|
|
88
118
|
type,
|
|
89
119
|
context: null,
|
|
90
120
|
reason: cause.message,
|
|
@@ -98,47 +128,61 @@ class HttpClientError extends Failure {
|
|
|
98
128
|
*/
|
|
99
129
|
static async fromResponse(response) {
|
|
100
130
|
const text = await response.text();
|
|
101
|
-
const
|
|
131
|
+
const message = `${response.status} ${response.statusText}: ${text}`;
|
|
132
|
+
const fallback = [{
|
|
102
133
|
type: "GENERIC_PROBLEM",
|
|
103
134
|
context: null,
|
|
104
|
-
reason:
|
|
135
|
+
reason: message,
|
|
105
136
|
details: null
|
|
106
137
|
}];
|
|
107
138
|
try {
|
|
108
|
-
return new HttpClientError(response.status, text ? JSON.parse(text) :
|
|
139
|
+
return new HttpClientError(message, response.status, text ? JSON.parse(text) : fallback);
|
|
109
140
|
} catch (e) {
|
|
110
|
-
return new HttpClientError(response.status,
|
|
141
|
+
return new HttpClientError(message, response.status, fallback);
|
|
111
142
|
}
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @implements {HttpInterceptor}
|
|
148
|
+
*/
|
|
115
149
|
class CsrfTokenInterceptor {
|
|
116
150
|
#k; #v;
|
|
117
151
|
constructor() {
|
|
118
|
-
this.#k = document.querySelector("meta[name='_csrf_header']")
|
|
119
|
-
this.#v = document.querySelector("meta[name='_csrf']")
|
|
120
|
-
}
|
|
152
|
+
this.#k = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
|
|
153
|
+
this.#v = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
|
|
154
|
+
}
|
|
121
155
|
async intercept(request, chain) {
|
|
122
|
-
|
|
156
|
+
if(this.#k && this.#v) {
|
|
157
|
+
request.headers.set(this.#k, this.#v);
|
|
158
|
+
}
|
|
123
159
|
return await chain.proceed(request);
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
|
-
|
|
162
|
+
/**
|
|
163
|
+
* @implements {HttpInterceptor}
|
|
164
|
+
*/
|
|
127
165
|
class RedirectOnUnauthorizedInterceptor {
|
|
128
166
|
#redirectUri;
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} redirectUri
|
|
169
|
+
*/
|
|
129
170
|
constructor(redirectUri) {
|
|
130
171
|
this.#redirectUri = redirectUri;
|
|
131
172
|
}
|
|
132
173
|
async intercept(request, chain) {
|
|
133
174
|
const response = await chain.proceed(request);
|
|
134
|
-
if (response.status
|
|
135
|
-
|
|
175
|
+
if (response.status === 401) {
|
|
176
|
+
window.location.href = this.#redirectUri;
|
|
136
177
|
}
|
|
137
|
-
|
|
178
|
+
return response;
|
|
138
179
|
}
|
|
139
180
|
}
|
|
140
181
|
|
|
141
182
|
class HttpClientBuilder {
|
|
183
|
+
/**
|
|
184
|
+
* @type {HttpInterceptor[]}
|
|
185
|
+
*/
|
|
142
186
|
#interceptors;
|
|
143
187
|
constructor() {
|
|
144
188
|
this.#interceptors = [];
|
|
@@ -151,6 +195,9 @@ class HttpClientBuilder {
|
|
|
151
195
|
this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
|
|
152
196
|
return this;
|
|
153
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* @param {...HttpInterceptor} interceptors
|
|
200
|
+
*/
|
|
154
201
|
withInterceptors(...interceptors) {
|
|
155
202
|
this.#interceptors.push(...interceptors);
|
|
156
203
|
return this;
|
|
@@ -160,27 +207,35 @@ class HttpClientBuilder {
|
|
|
160
207
|
}
|
|
161
208
|
}
|
|
162
209
|
|
|
210
|
+
/**
|
|
211
|
+
* @implements {HttpInterceptor}
|
|
212
|
+
*/
|
|
163
213
|
class HttpCall {
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
* @async
|
|
167
|
-
* @param {Request} request
|
|
168
|
-
* @param {HttpInterceptorChain} chain
|
|
169
|
-
* @returns {Promise<Response>} the response
|
|
170
|
-
*/
|
|
171
214
|
async intercept(request, chain) {
|
|
172
215
|
return await fetch(request);
|
|
173
216
|
}
|
|
174
217
|
}
|
|
175
218
|
|
|
176
219
|
class HttpInterceptorChain {
|
|
220
|
+
#interceptors;
|
|
221
|
+
#current;
|
|
222
|
+
/**
|
|
223
|
+
*
|
|
224
|
+
* @param {HttpInterceptor[]} interceptors
|
|
225
|
+
* @param {number} current
|
|
226
|
+
*/
|
|
177
227
|
constructor(interceptors, current) {
|
|
178
|
-
this
|
|
179
|
-
this
|
|
228
|
+
this.#interceptors = interceptors;
|
|
229
|
+
this.#current = current;
|
|
180
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
*
|
|
233
|
+
* @param {Request} request
|
|
234
|
+
* @returns {Promise<Response>} the response
|
|
235
|
+
*/
|
|
181
236
|
async proceed(request) {
|
|
182
|
-
const interceptor = this
|
|
183
|
-
return await interceptor.intercept(request, new HttpInterceptorChain(this
|
|
237
|
+
const interceptor = this.#interceptors[this.#current];
|
|
238
|
+
return await interceptor.intercept(request, new HttpInterceptorChain(this.#interceptors, this.#current + 1));
|
|
184
239
|
}
|
|
185
240
|
}
|
|
186
241
|
|
|
@@ -188,14 +243,14 @@ class HttpClient {
|
|
|
188
243
|
#interceptors;
|
|
189
244
|
/**
|
|
190
245
|
* Creates a builder for an HttpClient.
|
|
191
|
-
* @returns {
|
|
246
|
+
* @returns {HttpClientBuilder} the client builder
|
|
192
247
|
*/
|
|
193
248
|
static builder() {
|
|
194
249
|
return new HttpClientBuilder();
|
|
195
250
|
}
|
|
196
251
|
/**
|
|
197
252
|
* Creates an HttpClient.
|
|
198
|
-
* @
|
|
253
|
+
* @param {HttpInterceptor[]|undefined} interceptors - a list of interceptors to be registered for every request performed by the created client.
|
|
199
254
|
*/
|
|
200
255
|
constructor(interceptors) {
|
|
201
256
|
this.#interceptors = interceptors || [];
|
|
@@ -205,7 +260,7 @@ class HttpClient {
|
|
|
205
260
|
* @async
|
|
206
261
|
* @param {string} uri - the (possibly relative) request url
|
|
207
262
|
* @param {RequestInit|undefined} options - fetch options
|
|
208
|
-
* @param {[
|
|
263
|
+
* @param {HttpInterceptor[]|undefined} interceptors - the HttpInterceptors to be registered for this exchange.
|
|
209
264
|
* @returns {Promise<Response>} the response
|
|
210
265
|
*/
|
|
211
266
|
async exchange(uri, options, interceptors) {
|
|
@@ -272,12 +327,17 @@ class HttpClient {
|
|
|
272
327
|
}
|
|
273
328
|
}
|
|
274
329
|
|
|
275
|
-
|
|
330
|
+
/**
|
|
331
|
+
*
|
|
332
|
+
* @param {Response} response
|
|
333
|
+
* @param {'text'|'json'|'blob'|'arrayBuffer'} type
|
|
334
|
+
* @returns
|
|
335
|
+
*/
|
|
276
336
|
const unmarshal = async (response, type) => {
|
|
277
337
|
try {
|
|
278
338
|
return await response[type]();
|
|
279
|
-
} catch (
|
|
280
|
-
throw HttpClientError.of("UNMARSHALING_PROBLEM",
|
|
339
|
+
} catch (ex) {
|
|
340
|
+
throw HttpClientError.of("UNMARSHALING_PROBLEM", ex);
|
|
281
341
|
}
|
|
282
342
|
};
|
|
283
343
|
|
|
@@ -319,7 +379,7 @@ class HttpRequestBuilder {
|
|
|
319
379
|
* @param {Headers} headers
|
|
320
380
|
* @param {any} body
|
|
321
381
|
* @param {Omit<RequestInit,"headers"|"method"|"body">} options
|
|
322
|
-
* @param {[
|
|
382
|
+
* @param {HttpInterceptor[]} interceptors
|
|
323
383
|
*/
|
|
324
384
|
constructor(client, method, uri, params, headers, body, options, interceptors) {
|
|
325
385
|
this.#client = client;
|
|
@@ -333,7 +393,7 @@ class HttpRequestBuilder {
|
|
|
333
393
|
}
|
|
334
394
|
/**
|
|
335
395
|
* Add all passed headers to the request, overriding existing ones if that key already exists.
|
|
336
|
-
* @param {
|
|
396
|
+
* @param {HeadersInit} hs
|
|
337
397
|
* @returns {HttpRequestBuilder} this builder
|
|
338
398
|
*/
|
|
339
399
|
headers(hs) {
|
|
@@ -395,6 +455,17 @@ class HttpRequestBuilder {
|
|
|
395
455
|
this.#body = JSON.stringify(body);
|
|
396
456
|
return this;
|
|
397
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Sets the request body as a FormData configured using the callback.
|
|
460
|
+
* `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
|
|
461
|
+
* @param {function(HttpMultipartRequestCustomizer):void} callback
|
|
462
|
+
*/
|
|
463
|
+
multipart(callback) {
|
|
464
|
+
const formData = new FormData();
|
|
465
|
+
const builder = new HttpMultipartRequestCustomizer(formData);
|
|
466
|
+
callback(builder);
|
|
467
|
+
this.#body = formData;
|
|
468
|
+
}
|
|
398
469
|
/**
|
|
399
470
|
* Sets a fetch options for the request.
|
|
400
471
|
* @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
|
|
@@ -402,6 +473,7 @@ class HttpRequestBuilder {
|
|
|
402
473
|
*/
|
|
403
474
|
options(kvs) {
|
|
404
475
|
for (const [k, v] of Object.entries(kvs)) {
|
|
476
|
+
// @ts-ignore
|
|
405
477
|
this.#options[k] = v;
|
|
406
478
|
}
|
|
407
479
|
return this;
|
|
@@ -468,11 +540,11 @@ class HttpRequestBuilder {
|
|
|
468
540
|
throw await HttpClientError.fromResponse(response);
|
|
469
541
|
}
|
|
470
542
|
return response;
|
|
471
|
-
} catch (
|
|
472
|
-
if (
|
|
473
|
-
throw
|
|
543
|
+
} catch (ex) {
|
|
544
|
+
if (ex instanceof Failure) {
|
|
545
|
+
throw ex;
|
|
474
546
|
}
|
|
475
|
-
throw HttpClientError.of("CONNECTION_PROBLEM",
|
|
547
|
+
throw HttpClientError.of("CONNECTION_PROBLEM", ex);
|
|
476
548
|
}
|
|
477
549
|
}
|
|
478
550
|
/**
|
|
@@ -491,14 +563,6 @@ class HttpRequestBuilder {
|
|
|
491
563
|
const response = await this.fetch();
|
|
492
564
|
return await unmarshal(response, 'json');
|
|
493
565
|
}
|
|
494
|
-
/**
|
|
495
|
-
* Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
|
|
496
|
-
* @returns {Promise<Uint8Array>} the response body, as an Uint8Array
|
|
497
|
-
*/
|
|
498
|
-
async fetchBytes() {
|
|
499
|
-
const response = await this.fetch();
|
|
500
|
-
return await unmarshal(response, 'bytes');
|
|
501
|
-
}
|
|
502
566
|
/**
|
|
503
567
|
* Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
|
|
504
568
|
* @returns {Promise<Blob>} the response body, as a Blob
|
|
@@ -517,6 +581,54 @@ class HttpRequestBuilder {
|
|
|
517
581
|
}
|
|
518
582
|
}
|
|
519
583
|
|
|
584
|
+
|
|
585
|
+
class HttpMultipartRequestCustomizer {
|
|
586
|
+
#formData;
|
|
587
|
+
/**
|
|
588
|
+
*
|
|
589
|
+
* @param {FormData} formData
|
|
590
|
+
*/
|
|
591
|
+
constructor(formData){
|
|
592
|
+
this.#formData = formData;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Appends a value to the FormData.
|
|
596
|
+
* @param {string} name
|
|
597
|
+
* @param {*} value
|
|
598
|
+
* @returns this builder
|
|
599
|
+
*/
|
|
600
|
+
field(name, value){
|
|
601
|
+
this.#formData.append(name, value);
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Appends a Blob to the FormData.
|
|
606
|
+
* If `filename` is omitted, FormData defaults are applied:
|
|
607
|
+
* The default filename for Blob objects is "blob";
|
|
608
|
+
* The default filename for File objects is the file's filename.
|
|
609
|
+
* @param {string} name
|
|
610
|
+
* @param {Blob} value
|
|
611
|
+
* @param {string|undefined} filename
|
|
612
|
+
* @returns this builder
|
|
613
|
+
*/
|
|
614
|
+
blob(name, value, filename){
|
|
615
|
+
this.#formData.append(name, value, filename);
|
|
616
|
+
return this;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Appends a JSON serialized blob to the FormData.
|
|
620
|
+
* @param {string} name
|
|
621
|
+
* @param {any} value
|
|
622
|
+
* @param {string|undefined} filename
|
|
623
|
+
* @returns this builder
|
|
624
|
+
*/
|
|
625
|
+
json(name, value, filename){
|
|
626
|
+
const blob = new Blob([JSON.stringify(value)], {type: 'application/json'});
|
|
627
|
+
this.#formData.append(name, blob, filename);
|
|
628
|
+
return this;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
520
632
|
class Storage {
|
|
521
633
|
constructor(prefix, storage) {
|
|
522
634
|
this.prefix = prefix;
|
|
@@ -613,7 +725,7 @@ class AuthorizationCodeFlow {
|
|
|
613
725
|
Object.entries(additionalParams || {}).forEach(kv => {
|
|
614
726
|
url.searchParams.set(kv[0], kv[1]);
|
|
615
727
|
});
|
|
616
|
-
window.location = url;
|
|
728
|
+
window.location.href = url.toString();
|
|
617
729
|
}
|
|
618
730
|
async registration(additionalParams){
|
|
619
731
|
await this.action(this.uri.registration, additionalParams);
|
|
@@ -623,7 +735,7 @@ class AuthorizationCodeFlow {
|
|
|
623
735
|
kc_action: kcAction
|
|
624
736
|
});
|
|
625
737
|
}
|
|
626
|
-
async
|
|
738
|
+
async #tokenExchange(code, state) {
|
|
627
739
|
window.history.replaceState('', "", this.uri.redirect);
|
|
628
740
|
const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
|
|
629
741
|
if (stateAndVerifier.state !== state) {
|
|
@@ -656,7 +768,7 @@ class AuthorizationCodeFlow {
|
|
|
656
768
|
if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
|
|
657
769
|
//if callback from keycloak and we have our state still stored
|
|
658
770
|
const state = url.searchParams.get("state");
|
|
659
|
-
return await this
|
|
771
|
+
return await this.#tokenExchange(code, state);
|
|
660
772
|
}
|
|
661
773
|
//if not authorized
|
|
662
774
|
await this.action(this.uri.auth, {});
|
|
@@ -727,7 +839,7 @@ class AuthorizationCodeFlowSession {
|
|
|
727
839
|
const url = new URL(this.uri.logout);
|
|
728
840
|
url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
|
|
729
841
|
url.searchParams.set("id_token_hint", this.token.id_token);
|
|
730
|
-
window.location = url;
|
|
842
|
+
window.location.href = url.toString();
|
|
731
843
|
}
|
|
732
844
|
|
|
733
845
|
bearerToken() {
|
|
@@ -786,7 +898,7 @@ const timing = {
|
|
|
786
898
|
};
|
|
787
899
|
|
|
788
900
|
return function () {
|
|
789
|
-
args = arguments;
|
|
901
|
+
args = [...arguments];
|
|
790
902
|
previousTimestamp = new Date().getTime();
|
|
791
903
|
if (tid === null) {
|
|
792
904
|
tid = setTimeout(later, timeoutMs);
|
|
@@ -820,7 +932,7 @@ const timing = {
|
|
|
820
932
|
previousTimestamp = now;
|
|
821
933
|
}
|
|
822
934
|
const remaining = timeoutMs - (now - previousTimestamp);
|
|
823
|
-
args = arguments;
|
|
935
|
+
args = [...arguments];
|
|
824
936
|
if (remaining <= 0 || remaining > timeoutMs) {
|
|
825
937
|
if (tid !== null) {
|
|
826
938
|
clearTimeout(tid);
|
|
@@ -926,16 +1038,18 @@ class Attributes {
|
|
|
926
1038
|
.forEach(a => {
|
|
927
1039
|
const target = a.substring(prefix.length);
|
|
928
1040
|
if (target === 'class') {
|
|
929
|
-
|
|
1041
|
+
const classes = from.getAttribute(prefix + "class")?.split(" ").filter(a => a.length) ?? [];
|
|
1042
|
+
to.classList.add(...classes);
|
|
930
1043
|
return;
|
|
931
1044
|
}
|
|
1045
|
+
// @ts-ignore
|
|
932
1046
|
to.setAttribute(target, from.getAttribute(a));
|
|
933
1047
|
});
|
|
934
1048
|
}
|
|
935
1049
|
/**
|
|
936
1050
|
*
|
|
937
1051
|
* @param {HTMLElement} el
|
|
938
|
-
* @param {
|
|
1052
|
+
* @param {string} attr
|
|
939
1053
|
* @param {boolean} value
|
|
940
1054
|
*/
|
|
941
1055
|
static toggle(el, attr, value) {
|
|
@@ -962,19 +1076,21 @@ class LightSlots {
|
|
|
962
1076
|
* @returns the slots
|
|
963
1077
|
*/
|
|
964
1078
|
static from(el) {
|
|
1079
|
+
/** @type [string, Element][] */
|
|
965
1080
|
const namedSlots = Array.from(el.childNodes)
|
|
966
|
-
.filter(el => el
|
|
1081
|
+
.filter(el => el instanceof Element)
|
|
1082
|
+
.filter(el => el.matches('[slot]'))
|
|
967
1083
|
.map(el => {
|
|
968
1084
|
el.remove();
|
|
969
1085
|
const slot = el.getAttribute("slot");
|
|
970
1086
|
el.removeAttribute("slot");
|
|
971
|
-
return [slot, el];
|
|
1087
|
+
return [slot ?? 'unnamed', el];
|
|
972
1088
|
});
|
|
973
1089
|
const slots = {};
|
|
974
1090
|
slots.default = new DocumentFragment();
|
|
975
1091
|
slots.default.append(...el.childNodes);
|
|
976
|
-
for(const [name,el] of namedSlots){
|
|
977
|
-
if(!(name in slots)){
|
|
1092
|
+
for (const [name, el] of namedSlots) {
|
|
1093
|
+
if (!(name in slots)) {
|
|
978
1094
|
slots[name] = new DocumentFragment();
|
|
979
1095
|
}
|
|
980
1096
|
slots[name].append(el);
|
|
@@ -1000,7 +1116,8 @@ class TemplatesRegistry {
|
|
|
1000
1116
|
#ec;
|
|
1001
1117
|
put(k, fragment) {
|
|
1002
1118
|
if (this.#ec) {
|
|
1003
|
-
|
|
1119
|
+
// @ts-ignore
|
|
1120
|
+
this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, this.#ec);
|
|
1004
1121
|
return;
|
|
1005
1122
|
}
|
|
1006
1123
|
this.#idToFragment[k] = fragment;
|
|
@@ -1019,6 +1136,7 @@ class TemplatesRegistry {
|
|
|
1019
1136
|
this.#ec = ec;
|
|
1020
1137
|
for (const [k, fragment] of Object.entries(this.#idToFragment)) {
|
|
1021
1138
|
delete this.#idToFragment[k];
|
|
1139
|
+
// @ts-ignore
|
|
1022
1140
|
this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, ec, ...data);
|
|
1023
1141
|
}
|
|
1024
1142
|
}
|
|
@@ -1122,8 +1240,8 @@ const ParsedElement = (conf) => {
|
|
|
1122
1240
|
#initialized;
|
|
1123
1241
|
#reflecting;
|
|
1124
1242
|
#internals;
|
|
1125
|
-
constructor(
|
|
1126
|
-
super(
|
|
1243
|
+
constructor() {
|
|
1244
|
+
super();
|
|
1127
1245
|
this.#internals = this.attachInternals();
|
|
1128
1246
|
}
|
|
1129
1247
|
get initialized() {
|
|
@@ -1151,6 +1269,7 @@ const ParsedElement = (conf) => {
|
|
|
1151
1269
|
observer.disconnect();
|
|
1152
1270
|
upgradeQueue.enqueue(this);
|
|
1153
1271
|
});
|
|
1272
|
+
// @ts-ignore
|
|
1154
1273
|
observer.observe(this.parentNode, { childList: true, subtree: true });
|
|
1155
1274
|
}
|
|
1156
1275
|
attributeChangedCallback(attr, oldValue, newValue) {
|
|
@@ -1176,6 +1295,7 @@ const ParsedElement = (conf) => {
|
|
|
1176
1295
|
return;
|
|
1177
1296
|
}
|
|
1178
1297
|
this.#parsed = true;
|
|
1298
|
+
// @ts-ignore
|
|
1179
1299
|
await this.render(elements.template(templateId), slots ? LightSlots.from(this) : undefined);
|
|
1180
1300
|
|
|
1181
1301
|
for (const [attr, mapper] of attrsAndMappers) {
|
|
@@ -1327,7 +1447,7 @@ class Form extends ParsedElement() {
|
|
|
1327
1447
|
}
|
|
1328
1448
|
get values() {
|
|
1329
1449
|
return Array.from(this.querySelectorAll('[name]'))
|
|
1330
|
-
.filter(
|
|
1450
|
+
.filter(el => {
|
|
1331
1451
|
if (el.dataset['fulBindInclude'] === 'never') {
|
|
1332
1452
|
return false;
|
|
1333
1453
|
}
|
|
@@ -1338,14 +1458,14 @@ class Form extends ParsedElement() {
|
|
|
1338
1458
|
}, {});
|
|
1339
1459
|
}
|
|
1340
1460
|
set errors(es) {
|
|
1341
|
-
const fieldErrors = es.filter(
|
|
1342
|
-
const globalErrors = es.filter(
|
|
1461
|
+
const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
|
|
1462
|
+
const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
1343
1463
|
this.querySelectorAll(`.${Form.INVALID_CLASS}`).forEach(el => el.classList.remove(Form.INVALID_CLASS));
|
|
1344
1464
|
this.querySelectorAll("ful-errors").forEach(el => {
|
|
1345
1465
|
el.replaceChildren();
|
|
1346
1466
|
el.setAttribute('hidden', '');
|
|
1347
1467
|
});
|
|
1348
|
-
fieldErrors.forEach(
|
|
1468
|
+
fieldErrors.forEach(e => {
|
|
1349
1469
|
const name = e.context.replace("[", ".").replace("].", ".");
|
|
1350
1470
|
const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
|
|
1351
1471
|
this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(Form.INVALID_CLASS));
|
|
@@ -1416,6 +1536,7 @@ class Input extends ParsedElement({
|
|
|
1416
1536
|
slots: true,
|
|
1417
1537
|
template: INPUT_TEMPLATE
|
|
1418
1538
|
}){
|
|
1539
|
+
input;
|
|
1419
1540
|
render(template, slots) {
|
|
1420
1541
|
const fragment = makeInputFragment(this, template, slots);
|
|
1421
1542
|
this.replaceChildren(fragment);
|
|
@@ -1586,27 +1707,40 @@ class RadioGroup extends ParsedElement({
|
|
|
1586
1707
|
input.addEventListener('change', evt => {
|
|
1587
1708
|
evt.stopPropagation();
|
|
1588
1709
|
//change is not cancelable
|
|
1589
|
-
this.dispatchEvent(new CustomEvent('change', {
|
|
1590
|
-
bubbles: true,
|
|
1591
|
-
cancelable: false,
|
|
1710
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
1711
|
+
bubbles: true,
|
|
1712
|
+
cancelable: false,
|
|
1592
1713
|
detail: {
|
|
1593
1714
|
value: this.value
|
|
1594
1715
|
}
|
|
1595
|
-
}));
|
|
1596
|
-
});
|
|
1716
|
+
}));
|
|
1717
|
+
});
|
|
1597
1718
|
const label = Fragments.fromChildNodes(el);
|
|
1598
1719
|
return [input, label];
|
|
1599
1720
|
});
|
|
1600
1721
|
|
|
1601
1722
|
radioEls.forEach(el => el.remove());
|
|
1602
|
-
template.renderTo(this, {name, slots, inputsAndLabels});
|
|
1723
|
+
template.renderTo(this, { name, slots, inputsAndLabels });
|
|
1603
1724
|
}
|
|
1604
1725
|
get value() {
|
|
1726
|
+
/** @type {HTMLInputElement|null} */
|
|
1605
1727
|
const checked = this.querySelector('input[type=radio]:checked');
|
|
1606
1728
|
return checked ? checked.value : null;
|
|
1607
1729
|
}
|
|
1608
1730
|
set value(value) {
|
|
1609
|
-
|
|
1731
|
+
if (value === null) {
|
|
1732
|
+
/** @type {HTMLInputElement[]} */
|
|
1733
|
+
this.querySelectorAll(`input[type=radio]`).forEach(el => {
|
|
1734
|
+
// @ts-ignore
|
|
1735
|
+
el.checked = false;
|
|
1736
|
+
});
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
/** @type {HTMLInputElement|null} */
|
|
1740
|
+
const el = this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`);
|
|
1741
|
+
if (el) {
|
|
1742
|
+
el.checked = true;
|
|
1743
|
+
}
|
|
1610
1744
|
}
|
|
1611
1745
|
}
|
|
1612
1746
|
|