@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 CHANGED
@@ -73,21 +73,51 @@ var ful = (function (exports) {
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
78
+ */
76
79
  class Failure extends Error {
77
- constructor(name, problems, cause) {
78
- super(JSON.stringify(problems), { cause });
79
- this.name = name;
80
+ /**
81
+ *
82
+ * @param {string} message
83
+ * @param {Problem[]} problems
84
+ * @param {*} cause
85
+ */
86
+ constructor(message, problems, cause) {
87
+ super(message, { cause });
88
+ this.name = 'Failure';
80
89
  this.problems = problems;
81
90
  }
82
91
  }
83
92
 
93
+ /**
94
+ * @typedef {Int8Array| Uint8Array| Uint8ClampedArray| Int16Array| Uint16Array| Int32Array| Uint32Array| Float32Array| Float64Array| BigInt64Array| BigUint64Array} TypedArray
95
+ */
96
+ /**
97
+ * @typedef HttpInterceptor
98
+ * @property {function(Request,HttpInterceptorChain):Promise<Response>} intercept
99
+ */
100
+
84
101
  class HttpClientError extends Failure {
85
- constructor(status, problems, cause) {
86
- super(`HttpClientError:${status}`, problems, cause);
102
+ /**
103
+ * @param {string} message
104
+ * @param {number} status
105
+ * @param {{ type: string; context: string?; reason: string; details: any?; }[]} problems
106
+ * @param {Error|undefined} [cause]
107
+ */
108
+ constructor(message, status, problems, cause) {
109
+ super(message, problems, cause);
110
+ this.name = 'HttpClientError';
87
111
  this.status = status;
88
112
  }
113
+ /**
114
+ *
115
+ * @param {string} type
116
+ * @param {any} cause
117
+ * @returns
118
+ */
89
119
  static of(type, cause) {
90
- return new HttpClientError(0, [{
120
+ return new HttpClientError(cause.message, 0, [{
91
121
  type,
92
122
  context: null,
93
123
  reason: cause.message,
@@ -101,47 +131,61 @@ var ful = (function (exports) {
101
131
  */
102
132
  static async fromResponse(response) {
103
133
  const text = await response.text();
104
- const def = [{
134
+ const message = `${response.status} ${response.statusText}: ${text}`;
135
+ const fallback = [{
105
136
  type: "GENERIC_PROBLEM",
106
137
  context: null,
107
- reason: `${response.status} ${response.statusText}: ${text}`,
138
+ reason: message,
108
139
  details: null
109
140
  }];
110
141
  try {
111
- return new HttpClientError(response.status, text ? JSON.parse(text) : def);
142
+ return new HttpClientError(message, response.status, text ? JSON.parse(text) : fallback);
112
143
  } catch (e) {
113
- return new HttpClientError(response.status, def);
144
+ return new HttpClientError(message, response.status, fallback);
114
145
  }
115
146
  }
116
147
  }
117
148
 
149
+ /**
150
+ * @implements {HttpInterceptor}
151
+ */
118
152
  class CsrfTokenInterceptor {
119
153
  #k; #v;
120
154
  constructor() {
121
- this.#k = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
122
- this.#v = document.querySelector("meta[name='_csrf']").getAttribute("content");
123
- }
155
+ this.#k = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
156
+ this.#v = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
157
+ }
124
158
  async intercept(request, chain) {
125
- request.headers.set(this.#k, this.#v);
159
+ if(this.#k && this.#v) {
160
+ request.headers.set(this.#k, this.#v);
161
+ }
126
162
  return await chain.proceed(request);
127
163
  }
128
164
  }
129
-
165
+ /**
166
+ * @implements {HttpInterceptor}
167
+ */
130
168
  class RedirectOnUnauthorizedInterceptor {
131
169
  #redirectUri;
170
+ /**
171
+ * @param {string} redirectUri
172
+ */
132
173
  constructor(redirectUri) {
133
174
  this.#redirectUri = redirectUri;
134
175
  }
135
176
  async intercept(request, chain) {
136
177
  const response = await chain.proceed(request);
137
- if (response.status !== 401) {
138
- return response;
178
+ if (response.status === 401) {
179
+ window.location.href = this.#redirectUri;
139
180
  }
140
- window.location.href = this.#redirectUri;
181
+ return response;
141
182
  }
142
183
  }
143
184
 
144
185
  class HttpClientBuilder {
186
+ /**
187
+ * @type {HttpInterceptor[]}
188
+ */
145
189
  #interceptors;
146
190
  constructor() {
147
191
  this.#interceptors = [];
@@ -154,6 +198,9 @@ var ful = (function (exports) {
154
198
  this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
155
199
  return this;
156
200
  }
201
+ /**
202
+ * @param {...HttpInterceptor} interceptors
203
+ */
157
204
  withInterceptors(...interceptors) {
158
205
  this.#interceptors.push(...interceptors);
159
206
  return this;
@@ -163,27 +210,35 @@ var ful = (function (exports) {
163
210
  }
164
211
  }
165
212
 
213
+ /**
214
+ * @implements {HttpInterceptor}
215
+ */
166
216
  class HttpCall {
167
- /**
168
- *
169
- * @async
170
- * @param {Request} request
171
- * @param {HttpInterceptorChain} chain
172
- * @returns {Promise<Response>} the response
173
- */
174
217
  async intercept(request, chain) {
175
218
  return await fetch(request);
176
219
  }
177
220
  }
178
221
 
179
222
  class HttpInterceptorChain {
223
+ #interceptors;
224
+ #current;
225
+ /**
226
+ *
227
+ * @param {HttpInterceptor[]} interceptors
228
+ * @param {number} current
229
+ */
180
230
  constructor(interceptors, current) {
181
- this.interceptors = interceptors;
182
- this.current = current;
231
+ this.#interceptors = interceptors;
232
+ this.#current = current;
183
233
  }
234
+ /**
235
+ *
236
+ * @param {Request} request
237
+ * @returns {Promise<Response>} the response
238
+ */
184
239
  async proceed(request) {
185
- const interceptor = this.interceptors[this.current];
186
- return await interceptor.intercept(request, new HttpInterceptorChain(this.interceptors, this.current + 1));
240
+ const interceptor = this.#interceptors[this.#current];
241
+ return await interceptor.intercept(request, new HttpInterceptorChain(this.#interceptors, this.#current + 1));
187
242
  }
188
243
  }
189
244
 
@@ -191,14 +246,14 @@ var ful = (function (exports) {
191
246
  #interceptors;
192
247
  /**
193
248
  * Creates a builder for an HttpClient.
194
- * @returns {HttpRequestBuilder} the client builder
249
+ * @returns {HttpClientBuilder} the client builder
195
250
  */
196
251
  static builder() {
197
252
  return new HttpClientBuilder();
198
253
  }
199
254
  /**
200
255
  * Creates an HttpClient.
201
- * @returns {[HttpInterceptor]} interceptors - a list of interceptors to be registered for every request performed by the created client.
256
+ * @param {HttpInterceptor[]|undefined} interceptors - a list of interceptors to be registered for every request performed by the created client.
202
257
  */
203
258
  constructor(interceptors) {
204
259
  this.#interceptors = interceptors || [];
@@ -208,7 +263,7 @@ var ful = (function (exports) {
208
263
  * @async
209
264
  * @param {string} uri - the (possibly relative) request url
210
265
  * @param {RequestInit|undefined} options - fetch options
211
- * @param {[any]|undefined} interceptors - the HttpInterceptors to be registered for this request.
266
+ * @param {HttpInterceptor[]|undefined} interceptors - the HttpInterceptors to be registered for this exchange.
212
267
  * @returns {Promise<Response>} the response
213
268
  */
214
269
  async exchange(uri, options, interceptors) {
@@ -275,12 +330,17 @@ var ful = (function (exports) {
275
330
  }
276
331
  }
277
332
 
278
-
333
+ /**
334
+ *
335
+ * @param {Response} response
336
+ * @param {'text'|'json'|'blob'|'arrayBuffer'} type
337
+ * @returns
338
+ */
279
339
  const unmarshal = async (response, type) => {
280
340
  try {
281
341
  return await response[type]();
282
- } catch (e) {
283
- throw HttpClientError.of("UNMARSHALING_PROBLEM", e);
342
+ } catch (ex) {
343
+ throw HttpClientError.of("UNMARSHALING_PROBLEM", ex);
284
344
  }
285
345
  };
286
346
 
@@ -322,7 +382,7 @@ var ful = (function (exports) {
322
382
  * @param {Headers} headers
323
383
  * @param {any} body
324
384
  * @param {Omit<RequestInit,"headers"|"method"|"body">} options
325
- * @param {[HttpInterceptor]} interceptors
385
+ * @param {HttpInterceptor[]} interceptors
326
386
  */
327
387
  constructor(client, method, uri, params, headers, body, options, interceptors) {
328
388
  this.#client = client;
@@ -336,7 +396,7 @@ var ful = (function (exports) {
336
396
  }
337
397
  /**
338
398
  * Add all passed headers to the request, overriding existing ones if that key already exists.
339
- * @param {headersInit} hs
399
+ * @param {HeadersInit} hs
340
400
  * @returns {HttpRequestBuilder} this builder
341
401
  */
342
402
  headers(hs) {
@@ -398,6 +458,17 @@ var ful = (function (exports) {
398
458
  this.#body = JSON.stringify(body);
399
459
  return this;
400
460
  }
461
+ /**
462
+ * Sets the request body as a FormData configured using the callback.
463
+ * `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
464
+ * @param {function(HttpMultipartRequestCustomizer):void} callback
465
+ */
466
+ multipart(callback) {
467
+ const formData = new FormData();
468
+ const builder = new HttpMultipartRequestCustomizer(formData);
469
+ callback(builder);
470
+ this.#body = formData;
471
+ }
401
472
  /**
402
473
  * Sets a fetch options for the request.
403
474
  * @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
@@ -405,6 +476,7 @@ var ful = (function (exports) {
405
476
  */
406
477
  options(kvs) {
407
478
  for (const [k, v] of Object.entries(kvs)) {
479
+ // @ts-ignore
408
480
  this.#options[k] = v;
409
481
  }
410
482
  return this;
@@ -471,11 +543,11 @@ var ful = (function (exports) {
471
543
  throw await HttpClientError.fromResponse(response);
472
544
  }
473
545
  return response;
474
- } catch (e) {
475
- if (e instanceof Failure) {
476
- throw e;
546
+ } catch (ex) {
547
+ if (ex instanceof Failure) {
548
+ throw ex;
477
549
  }
478
- throw HttpClientError.of("CONNECTION_PROBLEM", e);
550
+ throw HttpClientError.of("CONNECTION_PROBLEM", ex);
479
551
  }
480
552
  }
481
553
  /**
@@ -494,14 +566,6 @@ var ful = (function (exports) {
494
566
  const response = await this.fetch();
495
567
  return await unmarshal(response, 'json');
496
568
  }
497
- /**
498
- * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
499
- * @returns {Promise<Uint8Array>} the response body, as an Uint8Array
500
- */
501
- async fetchBytes() {
502
- const response = await this.fetch();
503
- return await unmarshal(response, 'bytes');
504
- }
505
569
  /**
506
570
  * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
507
571
  * @returns {Promise<Blob>} the response body, as a Blob
@@ -520,6 +584,54 @@ var ful = (function (exports) {
520
584
  }
521
585
  }
522
586
 
587
+
588
+ class HttpMultipartRequestCustomizer {
589
+ #formData;
590
+ /**
591
+ *
592
+ * @param {FormData} formData
593
+ */
594
+ constructor(formData){
595
+ this.#formData = formData;
596
+ }
597
+ /**
598
+ * Appends a value to the FormData.
599
+ * @param {string} name
600
+ * @param {*} value
601
+ * @returns this builder
602
+ */
603
+ field(name, value){
604
+ this.#formData.append(name, value);
605
+ return this;
606
+ }
607
+ /**
608
+ * Appends a Blob to the FormData.
609
+ * If `filename` is omitted, FormData defaults are applied:
610
+ * The default filename for Blob objects is "blob";
611
+ * The default filename for File objects is the file's filename.
612
+ * @param {string} name
613
+ * @param {Blob} value
614
+ * @param {string|undefined} filename
615
+ * @returns this builder
616
+ */
617
+ blob(name, value, filename){
618
+ this.#formData.append(name, value, filename);
619
+ return this;
620
+ }
621
+ /**
622
+ * Appends a JSON serialized blob to the FormData.
623
+ * @param {string} name
624
+ * @param {any} value
625
+ * @param {string|undefined} filename
626
+ * @returns this builder
627
+ */
628
+ json(name, value, filename){
629
+ const blob = new Blob([JSON.stringify(value)], {type: 'application/json'});
630
+ this.#formData.append(name, blob, filename);
631
+ return this;
632
+ }
633
+ }
634
+
523
635
  class Storage {
524
636
  constructor(prefix, storage) {
525
637
  this.prefix = prefix;
@@ -616,7 +728,7 @@ var ful = (function (exports) {
616
728
  Object.entries(additionalParams || {}).forEach(kv => {
617
729
  url.searchParams.set(kv[0], kv[1]);
618
730
  });
619
- window.location = url;
731
+ window.location.href = url.toString();
620
732
  }
621
733
  async registration(additionalParams){
622
734
  await this.action(this.uri.registration, additionalParams);
@@ -626,7 +738,7 @@ var ful = (function (exports) {
626
738
  kc_action: kcAction
627
739
  });
628
740
  }
629
- async _tokenExchange(code, state) {
741
+ async #tokenExchange(code, state) {
630
742
  window.history.replaceState('', "", this.uri.redirect);
631
743
  const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
632
744
  if (stateAndVerifier.state !== state) {
@@ -659,7 +771,7 @@ var ful = (function (exports) {
659
771
  if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
660
772
  //if callback from keycloak and we have our state still stored
661
773
  const state = url.searchParams.get("state");
662
- return await this._tokenExchange(code, state);
774
+ return await this.#tokenExchange(code, state);
663
775
  }
664
776
  //if not authorized
665
777
  await this.action(this.uri.auth, {});
@@ -730,7 +842,7 @@ var ful = (function (exports) {
730
842
  const url = new URL(this.uri.logout);
731
843
  url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
732
844
  url.searchParams.set("id_token_hint", this.token.id_token);
733
- window.location = url;
845
+ window.location.href = url.toString();
734
846
  }
735
847
 
736
848
  bearerToken() {
@@ -789,7 +901,7 @@ var ful = (function (exports) {
789
901
  };
790
902
 
791
903
  return function () {
792
- args = arguments;
904
+ args = [...arguments];
793
905
  previousTimestamp = new Date().getTime();
794
906
  if (tid === null) {
795
907
  tid = setTimeout(later, timeoutMs);
@@ -823,7 +935,7 @@ var ful = (function (exports) {
823
935
  previousTimestamp = now;
824
936
  }
825
937
  const remaining = timeoutMs - (now - previousTimestamp);
826
- args = arguments;
938
+ args = [...arguments];
827
939
  if (remaining <= 0 || remaining > timeoutMs) {
828
940
  if (tid !== null) {
829
941
  clearTimeout(tid);
@@ -929,16 +1041,18 @@ var ful = (function (exports) {
929
1041
  .forEach(a => {
930
1042
  const target = a.substring(prefix.length);
931
1043
  if (target === 'class') {
932
- to.classList.add(...from.getAttribute(prefix + "class").split(" ").filter(a => a.length));
1044
+ const classes = from.getAttribute(prefix + "class")?.split(" ").filter(a => a.length) ?? [];
1045
+ to.classList.add(...classes);
933
1046
  return;
934
1047
  }
1048
+ // @ts-ignore
935
1049
  to.setAttribute(target, from.getAttribute(a));
936
1050
  });
937
1051
  }
938
1052
  /**
939
1053
  *
940
1054
  * @param {HTMLElement} el
941
- * @param {stirng} attr
1055
+ * @param {string} attr
942
1056
  * @param {boolean} value
943
1057
  */
944
1058
  static toggle(el, attr, value) {
@@ -965,19 +1079,21 @@ var ful = (function (exports) {
965
1079
  * @returns the slots
966
1080
  */
967
1081
  static from(el) {
1082
+ /** @type [string, Element][] */
968
1083
  const namedSlots = Array.from(el.childNodes)
969
- .filter(el => el.matches && el.matches('[slot]'))
1084
+ .filter(el => el instanceof Element)
1085
+ .filter(el => el.matches('[slot]'))
970
1086
  .map(el => {
971
1087
  el.remove();
972
1088
  const slot = el.getAttribute("slot");
973
1089
  el.removeAttribute("slot");
974
- return [slot, el];
1090
+ return [slot ?? 'unnamed', el];
975
1091
  });
976
1092
  const slots = {};
977
1093
  slots.default = new DocumentFragment();
978
1094
  slots.default.append(...el.childNodes);
979
- for(const [name,el] of namedSlots){
980
- if(!(name in slots)){
1095
+ for (const [name, el] of namedSlots) {
1096
+ if (!(name in slots)) {
981
1097
  slots[name] = new DocumentFragment();
982
1098
  }
983
1099
  slots[name].append(el);
@@ -1003,7 +1119,8 @@ var ful = (function (exports) {
1003
1119
  #ec;
1004
1120
  put(k, fragment) {
1005
1121
  if (this.#ec) {
1006
- this.#idToTemplate[k] = Template.fromFragment(fragment, ec);
1122
+ // @ts-ignore
1123
+ this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, this.#ec);
1007
1124
  return;
1008
1125
  }
1009
1126
  this.#idToFragment[k] = fragment;
@@ -1022,6 +1139,7 @@ var ful = (function (exports) {
1022
1139
  this.#ec = ec;
1023
1140
  for (const [k, fragment] of Object.entries(this.#idToFragment)) {
1024
1141
  delete this.#idToFragment[k];
1142
+ // @ts-ignore
1025
1143
  this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, ec, ...data);
1026
1144
  }
1027
1145
  }
@@ -1125,8 +1243,8 @@ var ful = (function (exports) {
1125
1243
  #initialized;
1126
1244
  #reflecting;
1127
1245
  #internals;
1128
- constructor(...args) {
1129
- super(...args);
1246
+ constructor() {
1247
+ super();
1130
1248
  this.#internals = this.attachInternals();
1131
1249
  }
1132
1250
  get initialized() {
@@ -1154,6 +1272,7 @@ var ful = (function (exports) {
1154
1272
  observer.disconnect();
1155
1273
  upgradeQueue.enqueue(this);
1156
1274
  });
1275
+ // @ts-ignore
1157
1276
  observer.observe(this.parentNode, { childList: true, subtree: true });
1158
1277
  }
1159
1278
  attributeChangedCallback(attr, oldValue, newValue) {
@@ -1179,6 +1298,7 @@ var ful = (function (exports) {
1179
1298
  return;
1180
1299
  }
1181
1300
  this.#parsed = true;
1301
+ // @ts-ignore
1182
1302
  await this.render(elements.template(templateId), slots ? LightSlots.from(this) : undefined);
1183
1303
 
1184
1304
  for (const [attr, mapper] of attrsAndMappers) {
@@ -1330,7 +1450,7 @@ var ful = (function (exports) {
1330
1450
  }
1331
1451
  get values() {
1332
1452
  return Array.from(this.querySelectorAll('[name]'))
1333
- .filter((el) => {
1453
+ .filter(el => {
1334
1454
  if (el.dataset['fulBindInclude'] === 'never') {
1335
1455
  return false;
1336
1456
  }
@@ -1341,14 +1461,14 @@ var ful = (function (exports) {
1341
1461
  }, {});
1342
1462
  }
1343
1463
  set errors(es) {
1344
- const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
1345
- const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
1464
+ const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
1465
+ const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
1346
1466
  this.querySelectorAll(`.${Form.INVALID_CLASS}`).forEach(el => el.classList.remove(Form.INVALID_CLASS));
1347
1467
  this.querySelectorAll("ful-errors").forEach(el => {
1348
1468
  el.replaceChildren();
1349
1469
  el.setAttribute('hidden', '');
1350
1470
  });
1351
- fieldErrors.forEach((e) => {
1471
+ fieldErrors.forEach(e => {
1352
1472
  const name = e.context.replace("[", ".").replace("].", ".");
1353
1473
  const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
1354
1474
  this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(Form.INVALID_CLASS));
@@ -1419,6 +1539,7 @@ var ful = (function (exports) {
1419
1539
  slots: true,
1420
1540
  template: INPUT_TEMPLATE
1421
1541
  }){
1542
+ input;
1422
1543
  render(template, slots) {
1423
1544
  const fragment = makeInputFragment(this, template, slots);
1424
1545
  this.replaceChildren(fragment);
@@ -1589,27 +1710,40 @@ var ful = (function (exports) {
1589
1710
  input.addEventListener('change', evt => {
1590
1711
  evt.stopPropagation();
1591
1712
  //change is not cancelable
1592
- this.dispatchEvent(new CustomEvent('change', {
1593
- bubbles: true,
1594
- cancelable: false,
1713
+ this.dispatchEvent(new CustomEvent('change', {
1714
+ bubbles: true,
1715
+ cancelable: false,
1595
1716
  detail: {
1596
1717
  value: this.value
1597
1718
  }
1598
- }));
1599
- });
1719
+ }));
1720
+ });
1600
1721
  const label = Fragments.fromChildNodes(el);
1601
1722
  return [input, label];
1602
1723
  });
1603
1724
 
1604
1725
  radioEls.forEach(el => el.remove());
1605
- template.renderTo(this, {name, slots, inputsAndLabels});
1726
+ template.renderTo(this, { name, slots, inputsAndLabels });
1606
1727
  }
1607
1728
  get value() {
1729
+ /** @type {HTMLInputElement|null} */
1608
1730
  const checked = this.querySelector('input[type=radio]:checked');
1609
1731
  return checked ? checked.value : null;
1610
1732
  }
1611
1733
  set value(value) {
1612
- this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`).checked = true;
1734
+ if (value === null) {
1735
+ /** @type {HTMLInputElement[]} */
1736
+ this.querySelectorAll(`input[type=radio]`).forEach(el => {
1737
+ // @ts-ignore
1738
+ el.checked = false;
1739
+ });
1740
+ return;
1741
+ }
1742
+ /** @type {HTMLInputElement|null} */
1743
+ const el = this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`);
1744
+ if (el) {
1745
+ el.checked = true;
1746
+ }
1613
1747
  }
1614
1748
  }
1615
1749