@optionfactory/ful 0.80.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.mjs CHANGED
@@ -70,171 +70,563 @@ class Hex {
70
70
  }
71
71
  }
72
72
 
73
- class ContextInterceptor {
74
- constructor() {
75
- const context = document.querySelector("meta[name='context']").getAttribute("content");
76
- this.context = context.endsWith("/") ? context.substring(0, context.length - 1) : context;
73
+ /**
74
+ * @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
75
+ */
76
+ class Failure extends Error {
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';
86
+ this.problems = problems;
77
87
  }
78
- async intercept(request, chain){
79
- const separator = request.resource.startsWith("/") ? "" : "/";
80
- request.resource = this.context + separator + request.resource;
81
- return await chain.proceed(request);
88
+ }
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
+
98
+ class HttpClientError extends Failure {
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';
108
+ this.status = status;
109
+ }
110
+ /**
111
+ *
112
+ * @param {string} type
113
+ * @param {any} cause
114
+ * @returns
115
+ */
116
+ static of(type, cause) {
117
+ return new HttpClientError(cause.message, 0, [{
118
+ type,
119
+ context: null,
120
+ reason: cause.message,
121
+ details: null
122
+ }], cause);
123
+ }
124
+ /**
125
+ * Creates an HttpClientError from a Response.
126
+ * @param {Response} response
127
+ * @returns an HttpClientError
128
+ */
129
+ static async fromResponse(response) {
130
+ const text = await response.text();
131
+ const message = `${response.status} ${response.statusText}: ${text}`;
132
+ const fallback = [{
133
+ type: "GENERIC_PROBLEM",
134
+ context: null,
135
+ reason: message,
136
+ details: null
137
+ }];
138
+ try {
139
+ return new HttpClientError(message, response.status, text ? JSON.parse(text) : fallback);
140
+ } catch (e) {
141
+ return new HttpClientError(message, response.status, fallback);
142
+ }
82
143
  }
83
144
  }
84
145
 
146
+ /**
147
+ * @implements {HttpInterceptor}
148
+ */
85
149
  class CsrfTokenInterceptor {
150
+ #k; #v;
86
151
  constructor() {
87
- this.k = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
88
- this.v = document.querySelector("meta[name='_csrf']").getAttribute("content");
89
- }
90
- async intercept(request, chain){
91
- const headers = new Headers(request.options.headers);
92
- headers.set(this.k, this.v);
93
- request.options.headers = headers;
152
+ this.#k = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
153
+ this.#v = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
154
+ }
155
+ async intercept(request, chain) {
156
+ if(this.#k && this.#v) {
157
+ request.headers.set(this.#k, this.#v);
158
+ }
94
159
  return await chain.proceed(request);
95
160
  }
96
161
  }
97
-
162
+ /**
163
+ * @implements {HttpInterceptor}
164
+ */
98
165
  class RedirectOnUnauthorizedInterceptor {
166
+ #redirectUri;
167
+ /**
168
+ * @param {string} redirectUri
169
+ */
99
170
  constructor(redirectUri) {
100
- this.redirectUri = redirectUri;
171
+ this.#redirectUri = redirectUri;
101
172
  }
102
- async intercept(request, chain){
103
- const response = await chain.proceed(request);
104
- if (response.status !== 401) {
105
- return response;
106
- }
107
- window.location.href = this.redirectUri;
108
- }
109
- }
110
-
111
- class Failure extends Error {
112
- static parseProblems(status, text) {
113
- const def = [{
114
- type: "GENERIC_PROBLEM",
115
- context: null,
116
- reason: `${status}: ${text}`,
117
- details: null
118
- }];
119
- try {
120
- return text ? JSON.parse(text) : def;
121
- } catch (e) {
122
- return def;
173
+ async intercept(request, chain) {
174
+ const response = await chain.proceed(request);
175
+ if (response.status === 401) {
176
+ window.location.href = this.#redirectUri;
123
177
  }
124
- }
125
- static fromResponse(status, text) {
126
- return new Failure(status, Failure.parseProblems(status, text));
127
- }
128
- constructor(status, problems) {
129
- super(JSON.stringify(problems));
130
- this.name = `Failure:${status}`;
131
- this.status = status;
132
- this.problems = problems;
178
+ return response;
133
179
  }
134
180
  }
135
181
 
136
182
  class HttpClientBuilder {
183
+ /**
184
+ * @type {HttpInterceptor[]}
185
+ */
186
+ #interceptors;
137
187
  constructor() {
138
- this.interceptors = [];
139
- }
140
- withContext() {
141
- this.interceptors.push(new ContextInterceptor());
142
- return this;
188
+ this.#interceptors = [];
143
189
  }
144
190
  withCsrfToken() {
145
- this.interceptors.push(new CsrfTokenInterceptor());
191
+ this.#interceptors.push(new CsrfTokenInterceptor());
146
192
  return this;
147
193
  }
148
194
  withRedirectOnUnauthorized(redirectUri) {
149
- this.interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
195
+ this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
150
196
  return this;
151
197
  }
198
+ /**
199
+ * @param {...HttpInterceptor} interceptors
200
+ */
152
201
  withInterceptors(...interceptors) {
153
- this.interceptors.push(...interceptors);
202
+ this.#interceptors.push(...interceptors);
154
203
  return this;
155
204
  }
156
205
  build() {
157
- const interceptors = this.interceptors;
158
- return new HttpClient({interceptors});
206
+ return new HttpClient(this.#interceptors);
159
207
  }
160
208
  }
161
209
 
210
+ /**
211
+ * @implements {HttpInterceptor}
212
+ */
162
213
  class HttpCall {
163
- async intercept(request, chain){
164
- return await fetch(request.resource, request.options);
165
- }
214
+ async intercept(request, chain) {
215
+ return await fetch(request);
216
+ }
166
217
  }
167
218
 
168
219
  class HttpInterceptorChain {
169
- constructor(interceptors, current){
170
- this.interceptors = interceptors;
171
- this.current = current;
220
+ #interceptors;
221
+ #current;
222
+ /**
223
+ *
224
+ * @param {HttpInterceptor[]} interceptors
225
+ * @param {number} current
226
+ */
227
+ constructor(interceptors, current) {
228
+ this.#interceptors = interceptors;
229
+ this.#current = current;
172
230
  }
173
- async proceed(request){
174
- const interceptor = this.interceptors[this.current];
175
- return await interceptor.intercept(request, new HttpInterceptorChain(this.interceptors, this.current + 1));
231
+ /**
232
+ *
233
+ * @param {Request} request
234
+ * @returns {Promise<Response>} the response
235
+ */
236
+ async proceed(request) {
237
+ const interceptor = this.#interceptors[this.#current];
238
+ return await interceptor.intercept(request, new HttpInterceptorChain(this.#interceptors, this.#current + 1));
176
239
  }
177
240
  }
178
241
 
179
-
180
242
  class HttpClient {
243
+ #interceptors;
244
+ /**
245
+ * Creates a builder for an HttpClient.
246
+ * @returns {HttpClientBuilder} the client builder
247
+ */
181
248
  static builder() {
182
249
  return new HttpClientBuilder();
183
250
  }
184
- constructor({interceptors}){
185
- this.interceptors = interceptors || [];
251
+ /**
252
+ * Creates an HttpClient.
253
+ * @param {HttpInterceptor[]|undefined} interceptors - a list of interceptors to be registered for every request performed by the created client.
254
+ */
255
+ constructor(interceptors) {
256
+ this.#interceptors = interceptors || [];
186
257
  }
187
- async exchange(resource, options) {
188
- const opts = options || {};
189
- const interceptors = [...this.interceptors, ...opts.interceptors || [], new HttpCall()];
190
- const chain = new HttpInterceptorChain(interceptors, 0);
191
- return await chain.proceed({resource, options: opts});
258
+ /**
259
+ * Performs an HTTP exchange.
260
+ * @async
261
+ * @param {string} uri - the (possibly relative) request url
262
+ * @param {RequestInit|undefined} options - fetch options
263
+ * @param {HttpInterceptor[]|undefined} interceptors - the HttpInterceptors to be registered for this exchange.
264
+ * @returns {Promise<Response>} the response
265
+ */
266
+ async exchange(uri, options, interceptors) {
267
+ const is = [...this.#interceptors, ...interceptors || [], new HttpCall()];
268
+ const chain = new HttpInterceptorChain(is, 0);
269
+ return await chain.proceed(new Request(uri, options));
192
270
  }
193
- async fetch(resource, options) {
194
- const response = await this.exchange(resource, options);
195
- if (!response.ok) {
196
- const message = await response.text();
197
- throw Failure.fromResponse(response.status, message);
198
- }
199
- return response;
271
+ /**
272
+ * Creates a request builder.
273
+ * @param {string} method - the HTTP method to be used
274
+ * @param {string} uri - the (possibly relative) request url
275
+ * @returns {HttpRequestBuilder} the request builder
276
+ */
277
+ request(method, uri) {
278
+ return HttpRequestBuilder.create(this, method, uri);
279
+ }
280
+ /**
281
+ * Creates a request builder.
282
+ * @param {string} uri - the (possibly relative) request url
283
+ * @returns {HttpRequestBuilder} the request builder
284
+ */
285
+ get(uri) {
286
+ return HttpRequestBuilder.create(this, 'GET', uri);
287
+ }
288
+ /**
289
+ * Creates a request builder.
290
+ * @param {string} uri - the (possibly relative) request url
291
+ * @returns {HttpRequestBuilder} the request builder
292
+ */
293
+ head(uri) {
294
+ return HttpRequestBuilder.create(this, 'HEAD', uri);
200
295
  }
201
- async json(resource, options) {
296
+ /**
297
+ * Creates a request builder.
298
+ * @param {string} uri - the (possibly relative) request url
299
+ * @returns {HttpRequestBuilder} the request builder
300
+ */
301
+ post(uri) {
302
+ return HttpRequestBuilder.create(this, 'POST', uri);
303
+ }
304
+ /**
305
+ * Creates a request builder.
306
+ * @param {string} uri - the (possibly relative) request url
307
+ * @returns {HttpRequestBuilder} the request builder
308
+ */
309
+ put(uri) {
310
+ return HttpRequestBuilder.create(this, 'PUT', uri);
311
+ }
312
+ /**
313
+ * Creates a request builder.
314
+ * @param {string} uri - the (possibly relative) request url
315
+ * @returns {HttpRequestBuilder} the request builder
316
+ */
317
+ patch(uri) {
318
+ return HttpRequestBuilder.create(this, 'PATCH', uri);
319
+ }
320
+ /**
321
+ * Creates a request builder.
322
+ * @param {string} uri - the (possibly relative) request url
323
+ * @returns {HttpRequestBuilder} the request builder
324
+ */
325
+ delete(uri) {
326
+ return HttpRequestBuilder.create(this, 'DELETE', uri);
327
+ }
328
+ }
329
+
330
+ /**
331
+ *
332
+ * @param {Response} response
333
+ * @param {'text'|'json'|'blob'|'arrayBuffer'} type
334
+ * @returns
335
+ */
336
+ const unmarshal = async (response, type) => {
337
+ try {
338
+ return await response[type]();
339
+ } catch (ex) {
340
+ throw HttpClientError.of("UNMARSHALING_PROBLEM", ex);
341
+ }
342
+ };
343
+
344
+
345
+ class HttpRequestBuilder {
346
+ #client;
347
+ #method;
348
+ #uri;
349
+ #params;
350
+ #headers;
351
+ #body;
352
+ #options;
353
+ #interceptors;
354
+ /**
355
+ * Creates an HttpRequestBuilder.
356
+ * @param {HttpClient} client
357
+ * @param {string} method - the HTTP method to be used
358
+ * @param {string} uri - the (possibly relative) request url
359
+ * @returns {HttpRequestBuilder} the builder
360
+ */
361
+ static create(client, method, uri) {
362
+ return new HttpRequestBuilder(
363
+ client,
364
+ method,
365
+ uri,
366
+ new URLSearchParams(),
367
+ new Headers(),
368
+ undefined,
369
+ {},
370
+ []
371
+ );
372
+ }
373
+ /**
374
+ * Creates an HttpRequestBuilder.
375
+ * @param {HttpClient} client
376
+ * @param {string} method - the HTTP method to be used
377
+ * @param {string} uri - the (possibly relative) request url
378
+ * @param {URLSearchParams} params
379
+ * @param {Headers} headers
380
+ * @param {any} body
381
+ * @param {Omit<RequestInit,"headers"|"method"|"body">} options
382
+ * @param {HttpInterceptor[]} interceptors
383
+ */
384
+ constructor(client, method, uri, params, headers, body, options, interceptors) {
385
+ this.#client = client;
386
+ this.#method = method;
387
+ this.#uri = uri;
388
+ this.#params = params;
389
+ this.#body = body;
390
+ this.#headers = headers;
391
+ this.#options = options;
392
+ this.#interceptors = interceptors;
393
+ }
394
+ /**
395
+ * Add all passed headers to the request, overriding existing ones if that key already exists.
396
+ * @param {HeadersInit} hs
397
+ * @returns {HttpRequestBuilder} this builder
398
+ */
399
+ headers(hs) {
400
+ for (const [k, v] of new Headers(hs).entries()) {
401
+ this.#headers.set(k, v);
402
+ }
403
+ return this;
404
+ }
405
+ /**
406
+ * Adds an header to the request, overriding it if it already exists.
407
+ * @param {string} k
408
+ * @param {string} v
409
+ * @returns {HttpRequestBuilder} this builder
410
+ */
411
+ header(k, v) {
412
+ this.#headers.set(k, v);
413
+ return this;
414
+ }
415
+ /**
416
+ * Add all query parameters to the request, overriding existing ones if that key already exists.
417
+ * @param {URLSearchParams|Record<string,string>|string[][]|string} ps
418
+ * @returns {HttpRequestBuilder} this builder
419
+ */
420
+ params(ps) {
421
+ for (const [k, v] of new URLSearchParams(ps).entries()) {
422
+ this.#params.set(k, v);
423
+ }
424
+ return this;
425
+ }
426
+ /**
427
+ * Adds a query parameter to the request, overriding it if it already exists.
428
+ * @param {string} k
429
+ * @param {string} v
430
+ * @returns {HttpRequestBuilder} this builder
431
+ */
432
+ param(k, v) {
433
+ this.#params.set(k, v);
434
+ return this;
435
+ }
436
+ /**
437
+ * Sets the request body.
438
+ * `Content-Type: multipart/form-data` header is automatically added by fetch when data is a FormData instance if not explicitly set.
439
+ * `Content-Type: application/x-www-form-urlencoded` header is automatically added by fetch when data is an URLSearchParams instance if not explicitly set.
440
+ * `Content-Type: text/plain` header is automatically added by fetch when data is a string instance if not explicitly set.
441
+ * @param {string|ArrayBuffer|Blob|DataView|File|FormData|TypedArray|URLSearchParams|ReadableStream} data
442
+ * @returns {HttpRequestBuilder} this builder
443
+ */
444
+ body(data) {
445
+ this.#body = data;
446
+ return this;
447
+ }
448
+ /**
449
+ * Sets the request body that will be serialized as json. Calling this method adds the `Content-Type application/json` header for the request.
450
+ * @param {any} body - the body to be serialized as json
451
+ * @returns {HttpRequestBuilder} this builder
452
+ */
453
+ json(body) {
454
+ this.#headers.set("Content-Type", "application/json");
455
+ this.#body = JSON.stringify(body);
456
+ return this;
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
+ }
469
+ /**
470
+ * Sets a fetch options for the request.
471
+ * @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
472
+ * @returns {HttpRequestBuilder} this builder
473
+ */
474
+ options(kvs) {
475
+ for (const [k, v] of Object.entries(kvs)) {
476
+ // @ts-ignore
477
+ this.#options[k] = v;
478
+ }
479
+ return this;
480
+ }
481
+ /**
482
+ * Sets a fetch option for the request.
483
+ * @param {keyof Omit<RequestInit,"headers"|"method"|"body">} k
484
+ * @param {*} v
485
+ * @returns {HttpRequestBuilder} this builder
486
+ */
487
+ option(k, v) {
488
+ this.#options[k] = v;
489
+ return this;
490
+ }
491
+ /**
492
+ * Adds interceptors to the request.
493
+ * @param {[HttpInterceptor]} is - the interceptor to be regisered
494
+ * @returns {HttpRequestBuilder} this builder
495
+ */
496
+ interceptors(is) {
497
+ for (const i of is) {
498
+ this.#interceptors.push(i);
499
+ }
500
+ return this;
501
+ }
502
+ /**
503
+ * Adds an interceptor to the request.
504
+ * @param {HttpInterceptor} i - the interceptor to be regisered
505
+ * @returns {HttpRequestBuilder} this builder
506
+ */
507
+ interceptor(i) {
508
+ this.#interceptors.push(i);
509
+ return this;
510
+ }
511
+ /**
512
+ * Performs an HTTP exchange using the configured client, request and interceptors.
513
+ * @returns {Promise<Response>} the response
514
+ */
515
+ async exchange() {
516
+ const uri = this.#params.size ? `${this.#uri}?${this.#params}` : this.#uri;
517
+ const opts = {
518
+ ...this.#options,
519
+ headers: this.#headers,
520
+ method: this.#method,
521
+ body: this.#body,
522
+ };
523
+ return await this.#client.exchange(uri, opts, this.#interceptors);
524
+ }
525
+ /**
526
+ * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
527
+ * @returns {Promise<Response>} the response
528
+ */
529
+ async fetch() {
530
+ const uri = this.#params.size ? `${this.#uri}?${this.#params}` : this.#uri;
531
+ const opts = {
532
+ ...this.#options,
533
+ headers: this.#headers,
534
+ method: this.#method,
535
+ body: this.#body,
536
+ };
202
537
  try {
203
- const response = await this.fetch(resource, options);
204
- const text = await response.text();
205
- return text ? JSON.parse(text) : undefined;
206
- } catch (e) {
207
- if (e instanceof Failure) {
208
- throw e;
538
+ const response = await this.#client.exchange(uri, opts, this.#interceptors);
539
+ if (!response.ok) {
540
+ throw await HttpClientError.fromResponse(response);
209
541
  }
210
- throw new Failure(0, [{
211
- type: "CONNECTION_PROBLEM",
212
- context: null,
213
- reason: e.message,
214
- details: null
215
- }]);
542
+ return response;
543
+ } catch (ex) {
544
+ if (ex instanceof Failure) {
545
+ throw ex;
546
+ }
547
+ throw HttpClientError.of("CONNECTION_PROBLEM", ex);
216
548
  }
217
549
  }
550
+ /**
551
+ * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
552
+ * @returns {Promise<string>} the response body, as text
553
+ */
554
+ async fetchText() {
555
+ const response = await this.fetch();
556
+ return await unmarshal(response, 'text');
557
+ }
558
+ /**
559
+ * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
560
+ * @returns {Promise<any>} the response body, deserialized as JSON
561
+ */
562
+ async fetchJson() {
563
+ const response = await this.fetch();
564
+ return await unmarshal(response, 'json');
565
+ }
566
+ /**
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.
568
+ * @returns {Promise<Blob>} the response body, as a Blob
569
+ */
570
+ async fetchBlob() {
571
+ const response = await this.fetch();
572
+ return await unmarshal(response, 'blob');
573
+ }
574
+ /**
575
+ * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
576
+ * @returns {Promise<ArrayBuffer>} the response body, as an ArrayBuffer
577
+ */
578
+ async fetchArrayBuffer() {
579
+ const response = await this.fetch();
580
+ return await unmarshal(response, 'arrayBuffer');
581
+ }
218
582
  }
219
583
 
220
- function jsonRequest(method, body, headers){
221
- return {
222
- headers: {
223
- "Content-Type": "application/json",
224
- ...headers
225
- },
226
- method: method,
227
- body: JSON.stringify(body)
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;
228
629
  }
229
- }
230
- function jsonPost(body, headers){
231
- return jsonRequest('POST', body, headers);
232
- }
233
- function jsonPut(body, headers){
234
- return jsonRequest('PUT', body, headers);
235
- }
236
- function jsonPatch(body, headers){
237
- return jsonRequest('PATCH', body, headers);
238
630
  }
239
631
 
240
632
  class Storage {
@@ -333,7 +725,7 @@ class AuthorizationCodeFlow {
333
725
  Object.entries(additionalParams || {}).forEach(kv => {
334
726
  url.searchParams.set(kv[0], kv[1]);
335
727
  });
336
- window.location = url;
728
+ window.location.href = url.toString();
337
729
  }
338
730
  async registration(additionalParams){
339
731
  await this.action(this.uri.registration, additionalParams);
@@ -343,7 +735,7 @@ class AuthorizationCodeFlow {
343
735
  kc_action: kcAction
344
736
  });
345
737
  }
346
- async _tokenExchange(code, state) {
738
+ async #tokenExchange(code, state) {
347
739
  window.history.replaceState('', "", this.uri.redirect);
348
740
  const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
349
741
  if (stateAndVerifier.state !== state) {
@@ -376,7 +768,7 @@ class AuthorizationCodeFlow {
376
768
  if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
377
769
  //if callback from keycloak and we have our state still stored
378
770
  const state = url.searchParams.get("state");
379
- return await this._tokenExchange(code, state);
771
+ return await this.#tokenExchange(code, state);
380
772
  }
381
773
  //if not authorized
382
774
  await this.action(this.uri.auth, {});
@@ -388,10 +780,10 @@ AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
388
780
  class AuthorizationCodeFlowSession {
389
781
  static parseToken(token) {
390
782
  const [rawHeader, rawPayload, signature] = token.split(".");
391
- const ut8decoder = new TextDecoder("utf-8");
783
+ const utf8decoder = new TextDecoder("utf-8");
392
784
  return {
393
- header: JSON.parse(ut8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
394
- payload: JSON.parse(ut8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
785
+ header: JSON.parse(utf8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
786
+ payload: JSON.parse(utf8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
395
787
  signature: signature
396
788
  };
397
789
  }
@@ -419,7 +811,8 @@ class AuthorizationCodeFlowSession {
419
811
  ])
420
812
  });
421
813
  if (!response.ok) {
422
- throw new Error("Error:" + response.status + ": " + response.text());
814
+ const text = await response.text();
815
+ throw new Error("Error:" + response.status + ": " + text);
423
816
  }
424
817
  const token = await response.json();
425
818
  this.token = token;
@@ -446,7 +839,7 @@ class AuthorizationCodeFlowSession {
446
839
  const url = new URL(this.uri.logout);
447
840
  url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
448
841
  url.searchParams.set("id_token_hint", this.token.id_token);
449
- window.location = url;
842
+ window.location.href = url.toString();
450
843
  }
451
844
 
452
845
  bearerToken() {
@@ -459,18 +852,19 @@ class AuthorizationCodeFlowSession {
459
852
  }
460
853
 
461
854
  class AuthorizationCodeFlowInterceptor {
855
+ #session;
856
+ #gracePeriodBefore;
857
+ #gracePeriodAfter;
462
858
  constructor(session, gracePeriodBefore, gracePeriodAfter) {
463
- this.session = session;
464
- this.gracePeriodBefore = gracePeriodBefore || 2000;
465
- this.gracePeriodAfter = gracePeriodAfter || 30000;
859
+ this.#session = session;
860
+ this.#gracePeriodBefore = gracePeriodBefore || 2000;
861
+ this.#gracePeriodAfter = gracePeriodAfter || 30000;
466
862
  }
467
863
  async intercept(request, chain) {
468
- await this.session.refreshIf(this.gracePeriodBefore);
469
- const headers = new Headers(request.options.headers);
470
- headers.set("Authorization", this.session.bearerToken());
471
- request.options.headers = headers;
864
+ await this.#session.refreshIf(this.#gracePeriodBefore);
865
+ request.headers.set("Authorization", this.#session.bearerToken());
472
866
  const response = await chain.proceed(request);
473
- await this.session.refreshIf(this.gracePeriodAfter);
867
+ await this.#session.refreshIf(this.#gracePeriodAfter);
474
868
  return response;
475
869
  }
476
870
  }
@@ -504,7 +898,7 @@ const timing = {
504
898
  };
505
899
 
506
900
  return function () {
507
- args = arguments;
901
+ args = [...arguments];
508
902
  previousTimestamp = new Date().getTime();
509
903
  if (tid === null) {
510
904
  tid = setTimeout(later, timeoutMs);
@@ -538,7 +932,7 @@ const timing = {
538
932
  previousTimestamp = now;
539
933
  }
540
934
  const remaining = timeoutMs - (now - previousTimestamp);
541
- args = arguments;
935
+ args = [...arguments];
542
936
  if (remaining <= 0 || remaining > timeoutMs) {
543
937
  if (tid !== null) {
544
938
  clearTimeout(tid);
@@ -644,16 +1038,18 @@ class Attributes {
644
1038
  .forEach(a => {
645
1039
  const target = a.substring(prefix.length);
646
1040
  if (target === 'class') {
647
- to.classList.add(...from.getAttribute(prefix + "class").split(" ").filter(a => a.length));
1041
+ const classes = from.getAttribute(prefix + "class")?.split(" ").filter(a => a.length) ?? [];
1042
+ to.classList.add(...classes);
648
1043
  return;
649
1044
  }
1045
+ // @ts-ignore
650
1046
  to.setAttribute(target, from.getAttribute(a));
651
1047
  });
652
1048
  }
653
1049
  /**
654
1050
  *
655
1051
  * @param {HTMLElement} el
656
- * @param {stirng} attr
1052
+ * @param {string} attr
657
1053
  * @param {boolean} value
658
1054
  */
659
1055
  static toggle(el, attr, value) {
@@ -680,19 +1076,21 @@ class LightSlots {
680
1076
  * @returns the slots
681
1077
  */
682
1078
  static from(el) {
1079
+ /** @type [string, Element][] */
683
1080
  const namedSlots = Array.from(el.childNodes)
684
- .filter(el => el.matches && el.matches('[slot]'))
1081
+ .filter(el => el instanceof Element)
1082
+ .filter(el => el.matches('[slot]'))
685
1083
  .map(el => {
686
1084
  el.remove();
687
1085
  const slot = el.getAttribute("slot");
688
1086
  el.removeAttribute("slot");
689
- return [slot, el];
1087
+ return [slot ?? 'unnamed', el];
690
1088
  });
691
1089
  const slots = {};
692
1090
  slots.default = new DocumentFragment();
693
1091
  slots.default.append(...el.childNodes);
694
- for(const [name,el] of namedSlots){
695
- if(!(name in slots)){
1092
+ for (const [name, el] of namedSlots) {
1093
+ if (!(name in slots)) {
696
1094
  slots[name] = new DocumentFragment();
697
1095
  }
698
1096
  slots[name].append(el);
@@ -718,7 +1116,8 @@ class TemplatesRegistry {
718
1116
  #ec;
719
1117
  put(k, fragment) {
720
1118
  if (this.#ec) {
721
- this.#idToTemplate[k] = Template.fromFragment(fragment, ec);
1119
+ // @ts-ignore
1120
+ this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, this.#ec);
722
1121
  return;
723
1122
  }
724
1123
  this.#idToFragment[k] = fragment;
@@ -737,6 +1136,7 @@ class TemplatesRegistry {
737
1136
  this.#ec = ec;
738
1137
  for (const [k, fragment] of Object.entries(this.#idToFragment)) {
739
1138
  delete this.#idToFragment[k];
1139
+ // @ts-ignore
740
1140
  this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, ec, ...data);
741
1141
  }
742
1142
  }
@@ -840,8 +1240,8 @@ const ParsedElement = (conf) => {
840
1240
  #initialized;
841
1241
  #reflecting;
842
1242
  #internals;
843
- constructor(...args) {
844
- super(...args);
1243
+ constructor() {
1244
+ super();
845
1245
  this.#internals = this.attachInternals();
846
1246
  }
847
1247
  get initialized() {
@@ -869,6 +1269,7 @@ const ParsedElement = (conf) => {
869
1269
  observer.disconnect();
870
1270
  upgradeQueue.enqueue(this);
871
1271
  });
1272
+ // @ts-ignore
872
1273
  observer.observe(this.parentNode, { childList: true, subtree: true });
873
1274
  }
874
1275
  attributeChangedCallback(attr, oldValue, newValue) {
@@ -894,6 +1295,7 @@ const ParsedElement = (conf) => {
894
1295
  return;
895
1296
  }
896
1297
  this.#parsed = true;
1298
+ // @ts-ignore
897
1299
  await this.render(elements.template(templateId), slots ? LightSlots.from(this) : undefined);
898
1300
 
899
1301
  for (const [attr, mapper] of attrsAndMappers) {
@@ -922,8 +1324,6 @@ const ParsedElement = (conf) => {
922
1324
  return k;
923
1325
  };
924
1326
 
925
- /* global Infinity, CSS */
926
-
927
1327
  function flatten(obj, prefix) {
928
1328
  return Object.keys(obj).reduce((acc, k) => {
929
1329
  const pre = prefix.length ? prefix + '.' : '';
@@ -1047,7 +1447,7 @@ class Form extends ParsedElement() {
1047
1447
  }
1048
1448
  get values() {
1049
1449
  return Array.from(this.querySelectorAll('[name]'))
1050
- .filter((el) => {
1450
+ .filter(el => {
1051
1451
  if (el.dataset['fulBindInclude'] === 'never') {
1052
1452
  return false;
1053
1453
  }
@@ -1058,14 +1458,14 @@ class Form extends ParsedElement() {
1058
1458
  }, {});
1059
1459
  }
1060
1460
  set errors(es) {
1061
- const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
1062
- const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
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');
1063
1463
  this.querySelectorAll(`.${Form.INVALID_CLASS}`).forEach(el => el.classList.remove(Form.INVALID_CLASS));
1064
1464
  this.querySelectorAll("ful-errors").forEach(el => {
1065
1465
  el.replaceChildren();
1066
1466
  el.setAttribute('hidden', '');
1067
1467
  });
1068
- fieldErrors.forEach((e) => {
1468
+ fieldErrors.forEach(e => {
1069
1469
  const name = e.context.replace("[", ".").replace("].", ".");
1070
1470
  const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
1071
1471
  this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(Form.INVALID_CLASS));
@@ -1081,7 +1481,7 @@ class Form extends ParsedElement() {
1081
1481
  if (!this.hasAttribute('scroll-on-error')) {
1082
1482
  return;
1083
1483
  }
1084
- const ys = Array.from(this.querySelectorAll(`[ful-validated-field]:has(.${Form.INVALID_CLASS}) ful-field-error`))
1484
+ const ys = Array.from(this.querySelectorAll(`ful-errors:not([hidden]), [ful-validated-field]:has(.${Form.INVALID_CLASS}) ful-field-error`))
1085
1485
  .map(el => el.parentElement ? el.parentElement : el)
1086
1486
  .map(el => el.getBoundingClientRect().y + window.scrollY);
1087
1487
  const miny = Math.min(...ys);
@@ -1136,6 +1536,7 @@ class Input extends ParsedElement({
1136
1536
  slots: true,
1137
1537
  template: INPUT_TEMPLATE
1138
1538
  }){
1539
+ input;
1139
1540
  render(template, slots) {
1140
1541
  const fragment = makeInputFragment(this, template, slots);
1141
1542
  this.replaceChildren(fragment);
@@ -1306,27 +1707,40 @@ class RadioGroup extends ParsedElement({
1306
1707
  input.addEventListener('change', evt => {
1307
1708
  evt.stopPropagation();
1308
1709
  //change is not cancelable
1309
- this.dispatchEvent(new CustomEvent('change', {
1310
- bubbles: true,
1311
- cancelable: false,
1710
+ this.dispatchEvent(new CustomEvent('change', {
1711
+ bubbles: true,
1712
+ cancelable: false,
1312
1713
  detail: {
1313
1714
  value: this.value
1314
1715
  }
1315
- }));
1316
- });
1716
+ }));
1717
+ });
1317
1718
  const label = Fragments.fromChildNodes(el);
1318
1719
  return [input, label];
1319
1720
  });
1320
1721
 
1321
1722
  radioEls.forEach(el => el.remove());
1322
- template.renderTo(this, {name, slots, inputsAndLabels});
1723
+ template.renderTo(this, { name, slots, inputsAndLabels });
1323
1724
  }
1324
1725
  get value() {
1726
+ /** @type {HTMLInputElement|null} */
1325
1727
  const checked = this.querySelector('input[type=radio]:checked');
1326
1728
  return checked ? checked.value : null;
1327
1729
  }
1328
1730
  set value(value) {
1329
- this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`).checked = true;
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
+ }
1330
1744
  }
1331
1745
  }
1332
1746
 
@@ -1344,5 +1758,5 @@ class Spinner extends ParsedElement({
1344
1758
  }
1345
1759
  }
1346
1760
 
1347
- export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Deferred, ElementsRegistry, Failure, Form, Fragments, Hex, HttpClient, INPUT_TEMPLATE, Input, LightSlots, LocalStorage, Nodes, ParsedElement, RadioGroup, Select, SessionStorage, Spinner, TemplatesRegistry, VersionedStorage, elements, jsonPatch, jsonPost, jsonPut, jsonRequest, makeInputFragment, timing };
1761
+ export { Attributes, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Deferred, ElementsRegistry, Failure, Form, Fragments, Hex, HttpClient, HttpClientError, INPUT_TEMPLATE, Input, LightSlots, LocalStorage, Nodes, ParsedElement, RadioGroup, Select, SessionStorage, Spinner, TemplatesRegistry, VersionedStorage, elements, makeInputFragment, timing };
1348
1762
  //# sourceMappingURL=ful.mjs.map