@optionfactory/ful 0.18.0 → 0.20.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
@@ -1,102 +1,3 @@
1
- /* global CSS */
2
-
3
- function extract(extractors, el) {
4
- const maybeExtractor = extractors[el.dataset['bindExtractor']] || extractors[el.dataset['bindProvide']];
5
- if (maybeExtractor) {
6
- return maybeExtractor(el);
7
- }
8
- if (el.getAttribute('type') === 'radio') {
9
- if (!el.checked) {
10
- return undefined;
11
- }
12
- return el.dataset['bindType'] === 'boolean' ? el.value === 'true' : el.value;
13
- }
14
- if (el.getAttribute('type') === 'checkbox') {
15
- return el.checked;
16
- }
17
- if (el.dataset['bindType'] === 'boolean') {
18
- return !el.value ? null : el.value === 'true';
19
- }
20
- return el.value || null;
21
- }
22
-
23
- function mutate(mutators, el, raw, key, values) {
24
- const maybeMutator = mutators[el.dataset['bindMutator']] || mutators[el.dataset['bindProvide']];
25
- if (maybeMutator) {
26
- maybeMutator(el, raw, key, values);
27
- return;
28
- }
29
- if (el.getAttribute('type') === 'radio') {
30
- el.checked = el.getAttribute('value') === raw;
31
- return;
32
- }
33
- if (el.getAttribute('type') === 'checkbox') {
34
- el.checked = raw;
35
- return;
36
- }
37
- el.value = raw;
38
- }
39
-
40
-
41
- function providePath(result, path, value) {
42
- const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
43
- let current = result;
44
- let previous = null;
45
- for (let i = 0; ; ++i) {
46
- const ckey = keys[i];
47
- const pkey = keys[i - 1];
48
- if (Number.isInteger(ckey) && !Array.isArray(current)) {
49
- if (previous !== null) {
50
- previous[pkey] = current = [];
51
- } else {
52
- result = current = [];
53
- }
54
- }
55
- if (i === keys.length - 1) {
56
- //when value is undefined we only want to define the property if it's not defined
57
- current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
58
- return result;
59
- }
60
- if (current[ckey] === undefined) {
61
- current[ckey] = {};
62
- }
63
- previous = current;
64
- current = current[ckey];
65
- }
66
- }
67
-
68
- class Bindings {
69
-
70
- constructor( {extractors, mutators, ignoredChildrenSelector, valueHoldersSelector}) {
71
- this.extractors = extractors || {};
72
- this.mutators = mutators || {};
73
- this.valueHoldersSelector = valueHoldersSelector || 'input[name], select[name], textarea[name]';
74
- this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
75
- }
76
- setValues(el, values) {
77
- for (let k in values) {
78
- if (!values.hasOwnProperty(k)) {
79
- continue;
80
- }
81
- Array.from(el.querySelectorAll(`[name='${CSS.escape(k)}']`)).forEach((el) => {
82
- mutate(this.mutators, el, values[k], k, values);
83
- });
84
- }
85
- }
86
- getValues(el) {
87
- return Array.from(el.querySelectorAll(this.valueHoldersSelector))
88
- .filter((el) => {
89
- if (el.dataset['bindInclude'] === 'never') {
90
- return false;
91
- }
92
- return el.dataset['bindInclude'] === 'always' || el.closest(this.ignoredChildrenSelector) === null;
93
- })
94
- .reduce((result, el) => {
95
- return providePath(result, el.getAttribute('name'), extract(this.extractors, el));
96
- }, {});
97
- }
98
- }
99
-
100
1
  class Base64 {
101
2
  static encode(arrayBuffer, dialect) {
102
3
  const d = dialect || Base64.URL_SAFE;
@@ -169,78 +70,54 @@ class Hex {
169
70
  }
170
71
  }
171
72
 
172
- /* global Infinity, CSS */
173
-
174
-
175
- class Form {
176
- constructor(el, bindings, {globalErrorsEl, fieldContainerSelector, errorClass, hideClass}) {
177
- this.el = el;
178
- this.bindings = bindings;
179
- this.globalErrorsEl = globalErrorsEl;
180
- this.fieldContainerSelector = fieldContainerSelector !== undefined ? fieldContainerSelector : Form.DEFAULT_FIELD_CONTAINER_SELECTOR;
181
- this.errorClass = errorClass || Form.DEFAULT_ERROR_CLASS;
182
- this.hideClass = hideClass || Form.DEFAULT_HIDE_CLASS;
183
- }
184
- setValues(values) {
185
- return this.bindings.setValues(this.el, values);
186
- }
187
- getValues() {
188
- return this.bindings.getValues(this.el);
73
+ class Observable {
74
+ constructor() {
75
+ this.listeners = {};
189
76
  }
190
- setErrors(errors, scrollFirstErrorIntoView, context) {
191
-
192
- this.clearErrors();
193
- errors
194
- .map(this.mapError ? this.mapError : (e) => e)
195
- .filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
196
- .forEach((e) => {
197
- const name = e.context.replace("[", ".").replace("].", ".");
198
- Array.from(this.el.querySelectorAll(`[name='${CSS.escape(name)}']`))
199
- .map(el => this.fieldContainerSelector ? el.closest(this.fieldContainerSelector) : el)
200
- .filter(el => el !== null)
201
- .forEach(label => {
202
- label.classList.add(this.errorClass);
203
- label.dataset['error'] = e.reason;
204
- });
205
- });
206
- if (this.globalErrorsEl) {
207
- const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
208
- this.globalErrorsEl.innerHTML = globalErrors.map(e => e.reason).join("\n");
209
- if (globalErrors.length !== 0) {
210
- this.globalErrorsEl.classList.remove(this.hideClass);
211
- }
212
- }
213
- if (!scrollFirstErrorIntoView) {
214
- return;
215
- }
216
- const yOffsets = Array.from(this.el.querySelectorAll('.${CSS.escape(this.errorClass)}'))
217
- .map((label) => label.getBoundingClientRect().y + window.scrollY);
218
- const firstErrorScrollY = Math.min(...yOffsets);
219
- if (firstErrorScrollY !== Infinity) {
220
- window.scroll(window.scrollX, firstErrorScrollY > 100 ? firstErrorScrollY - 100 : 0);
77
+ fireSync(event, data, initialAcc) {
78
+ const listeners = this.listeners[event] || [];
79
+ let acc = initialAcc;
80
+ for (const l of listeners) {
81
+ acc = l(data, this, acc);
221
82
  }
83
+ return acc;
222
84
  }
223
- clearErrors() {
224
- this.el.querySelectorAll(`.${CSS.escape(this.errorClass)}`).forEach(l => l.classList.remove(this.errorClass));
225
- if (this.globalErrorsEl) {
226
- this.globalErrorsEl.innerHTML = '';
227
- this.globalErrorsEl.classList.add(this.hideClass);
85
+ async fire(event, data, initialAcc) {
86
+ const listeners = this.listeners[event] || [];
87
+ let acc = initialAcc;
88
+ for (const l of listeners) {
89
+ acc = await l(data, this, acc);
228
90
  }
91
+ return acc;
92
+ }
93
+ on(event, listener) {
94
+ this.listeners[event] = this.listeners[event] || [];
95
+ this.listeners[event].push(listener);
96
+ }
97
+ un(event, listener) {
98
+ const listeners = this.listeners[event] || [];
99
+ const idx = listeners.indexOf(listener);
100
+ return idx === -1 ? [] : listeners.splice(idx, 1);
101
+ }
102
+ static mixin(self) {
103
+ self.listeners = {};
104
+ self.fireSync = Observable.prototype.fireSync;
105
+ self.fire = Observable.prototype.fire;
106
+ self.on = Observable.prototype.on;
107
+ self.un = Observable.prototype.un;
229
108
  }
230
- }
231
109
 
232
- Form.DEFAULT_FIELD_CONTAINER_SELECTOR = 'label';
233
- Form.DEFAULT_ERROR_CLASS = 'has-error';
234
- Form.DEFAULT_HIDE_CLASS = 'd-none';
110
+ }
235
111
 
236
112
  class ContextInterceptor {
237
113
  constructor() {
238
114
  const context = document.querySelector("meta[name='context']").getAttribute("content");
239
115
  this.context = context.endsWith("/") ? context.substring(0, context.length - 1) : context;
240
116
  }
241
- before(request) {
117
+ async intercept(request, chain){
242
118
  const separator = request.resource.startsWith("/") ? "" : "/";
243
119
  request.resource = this.context + separator + request.resource;
120
+ return await chain.proceed(request);
244
121
  }
245
122
  }
246
123
 
@@ -249,10 +126,11 @@ class CsrfTokenInterceptor {
249
126
  this.k = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
250
127
  this.v = document.querySelector("meta[name='_csrf']").getAttribute("content");
251
128
  }
252
- before(request) {
129
+ async intercept(request, chain){
253
130
  const headers = new Headers(request.options.headers);
254
131
  headers.set(this.k, this.v);
255
132
  request.options.headers = headers;
133
+ return await chain.proceed(request);
256
134
  }
257
135
  }
258
136
 
@@ -260,9 +138,10 @@ class RedirectOnUnauthorizedInterceptor {
260
138
  constructor(redirectUri) {
261
139
  this.redirectUri = redirectUri;
262
140
  }
263
- after(request, response) {
141
+ async intercept(request, chain){
142
+ const response = await chain.proceed(request);
264
143
  if (response.status !== 401) {
265
- return;
144
+ return response;
266
145
  }
267
146
  window.location.href = redirectUri;
268
147
  }
@@ -319,31 +198,35 @@ class HttpClientBuilder {
319
198
  }
320
199
  }
321
200
 
201
+ class HttpCall {
202
+ async intercept(request, chain){
203
+ return await fetch(request.resource, request.options);
204
+ }
205
+ }
206
+
207
+ class HttpInterceptorChain {
208
+ constructor(interceptors, current){
209
+ this.interceptors = interceptors;
210
+ this.current = current;
211
+ }
212
+ async proceed(request){
213
+ const interceptor = this.interceptors[this.current];
214
+ return await interceptor.intercept(request, new HttpInterceptorChain(this.interceptors, this.current + 1));
215
+ }
216
+ }
217
+
218
+
322
219
  class HttpClient {
323
220
  static builder() {
324
221
  return new HttpClientBuilder();
325
222
  }
326
- constructor( {interceptors}){
223
+ constructor({interceptors}){
327
224
  this.interceptors = interceptors || [];
328
225
  }
329
226
  async fetch(resource, options) {
330
- const is = this.interceptors.concat(options.interceptors || []);
331
- const request = {resource, options};
332
- await is.forEach(async (i) => {
333
- if (!i.before) {
334
- return;
335
- }
336
- await i.before(request);
337
- });
338
- const response = await fetch(request.resource, request.options);
339
- await is.forEach(async (i) => {
340
- if (!i.after) {
341
- return;
342
- }
343
- await i.after(request, response);
344
- });
345
-
346
- return response;
227
+ const interceptors = [...this.interceptors, ...options.interceptors || [], new HttpCall()];
228
+ const chain = new HttpInterceptorChain(interceptors, 0);
229
+ return await chain.proceed({resource, options});
347
230
  }
348
231
  async json(resource, options) {
349
232
  try {
@@ -366,30 +249,456 @@ class HttpClient {
366
249
  }]);
367
250
  }
368
251
  }
369
- async form(resource, options, uiOptions) {
370
- const ui = uiOptions || {};
371
- ui.buttons?.forEach(el => {
372
- el.setAttribute("disabled", "disabled");
373
- if (ui.loader) {
374
- el.dataset['oldContent'] = el.innerHTML;
375
- el.innerHTML = ui.loader;
252
+ }
253
+
254
+ /* global Infinity, CSS */
255
+
256
+ class CustomElements {
257
+ static id = 0;
258
+ static uid(prefix) {
259
+ return `${prefix}-${++CustomElements.id}`;
260
+ }
261
+ static forwardAttributes(from, to, except) {
262
+ from.getAttributeNames().filter(a => except.indexOf(a) === -1)
263
+ .filter(a => a[0] === '@')
264
+ .forEach(a => {
265
+ if (a === '@class') {
266
+ to.classList.add(...from.getAttribute("@class").split(" ").filter(a => a.length));
267
+ return;
268
+ }
269
+ to.setAttribute(a.substring(1), from.getAttribute(a));
270
+ });
271
+ }
272
+ static extractSlots(el) {
273
+ const slotted = Object.fromEntries([...el.querySelectorAll("[slot]")].map(el => {
274
+ el.parentElement.removeChild(el);
275
+ const slot = el.getAttribute("slot");
276
+ el.removeAttribute("slot");
277
+ return [slot, el];
278
+ }));
279
+ slotted.default = new DocumentFragment();
280
+ slotted.default.append(...el.childNodes);
281
+ return slotted;
282
+ }
283
+ static labelAndInputGroup(id, name, isFloating, slotted) {
284
+ if (isFloating) {
285
+ /**
286
+ * <div class="input-group has-validation">
287
+ * <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
288
+ * <div class="form-floating">
289
+ * {{{{ slotted.input }}}}
290
+ * <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
291
+ * </div>
292
+ * <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
293
+ * <ful-field-error data-tpl-field="name"></ful-field-error>
294
+ * </div>
295
+ */
296
+ const label = document.createElement("label");
297
+ label.setAttribute("for", id);
298
+ label.classList.add('form-label');
299
+ label.append(slotted.default);
300
+
301
+ const ff = document.createElement('div');
302
+ ff.classList.add("form-floating");
303
+ ff.append(slotted.input, label);
304
+
305
+ const ffe = document.createElement('ful-field-error');
306
+ ffe.setAttribute("field", name);
307
+
308
+ const ig = document.createElement("div");
309
+ ig.classList.add('input-group', 'has-validtion');
310
+
311
+ if (slotted.before) {
312
+ ig.append(slotted.before);
313
+ } else if (slotted.ibefore) {
314
+ const igt = document.createElement('div');
315
+ igt.classList.add('input-group-text');
316
+ igt.append(slotted.ibefore);
317
+ ig.append(igt);
318
+ }
319
+ ig.append(ff);
320
+ if (slotted.after) {
321
+ ig.append(slotted.after);
322
+ } else if (slotted.iafter) {
323
+ const igt = document.createElement('div');
324
+ igt.classList.add('input-group-text');
325
+ igt.append(slotted.iafter);
326
+ ig.append(igt);
327
+ }
328
+ ig.append(ffe);
329
+ return ig;
330
+ }
331
+ /**
332
+ <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
333
+ <div class="input-group has-validation">
334
+ <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
335
+ {{{{ slotted.input }}}}
336
+ <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
337
+ <ful-field-error data-tpl-field="name"></ful-field-error>
338
+ </div>
339
+ */
340
+
341
+ const label = document.createElement("label");
342
+ label.setAttribute("for", name);
343
+ label.classList.add('form-label');
344
+ label.append(slotted.default);
345
+
346
+ const ffe = document.createElement('ful-field-error');
347
+ ffe.setAttribute("field", name);
348
+
349
+ const ig = document.createElement("div");
350
+ ig.classList.add('input-group', 'has-validation');
351
+
352
+ if (slotted.before) {
353
+ ig.append(slotted.before);
354
+ } else if (slotted.ibefore) {
355
+ const igt = document.createElement('div');
356
+ igt.classList.add('input-group-text');
357
+ igt.append(slotted.ibefore);
358
+ ig.append(igt);
359
+ }
360
+ ig.append(slotted.input);
361
+ if (slotted.after) {
362
+ ig.append(slotted.after);
363
+ } else if (slotted.iafter) {
364
+ const igt = document.createElement('div');
365
+ igt.classList.add('input-group-text');
366
+ igt.append(slotted.iafter);
367
+ ig.append(igt);
368
+ }
369
+ ig.append(ffe);
370
+
371
+ const fragment = new DocumentFragment();
372
+ fragment.append(label, ig);
373
+ return fragment;
374
+ }
375
+
376
+ }
377
+
378
+
379
+ class FieldError extends HTMLElement {
380
+ constructor() {
381
+ super();
382
+ }
383
+ connectedCallback() {
384
+ this.classList.add('invalid-feedback');
385
+ }
386
+ static configure() {
387
+ customElements.define('ful-field-error', FieldError);
388
+ }
389
+ }
390
+
391
+ class Errors extends HTMLElement {
392
+ constructor() {
393
+ super();
394
+ }
395
+ connectedCallback() {
396
+ this.classList.add('alert', 'alert-danger', 'd-none');
397
+ }
398
+ static configure() {
399
+ customElements.define('ful-errors', Errors);
400
+ }
401
+
402
+ }
403
+
404
+ class Spinner extends HTMLElement {
405
+ constructor() {
406
+ super();
407
+ }
408
+ connectedCallback() {
409
+ this.classList.add('spinner-border', 'spinner-border-sm', 'd-none');
410
+ this.setAttribute("aria-hidden", "true");
411
+ }
412
+ show() {
413
+ this.classList.remove("d-none");
414
+ }
415
+ hide() {
416
+ this.classList.add("d-none");
417
+ }
418
+ static configure() {
419
+ customElements.define('ful-spinner', Spinner);
420
+ }
421
+ }
422
+
423
+
424
+
425
+ class Input extends HTMLElement {
426
+ constructor() {
427
+ super();
428
+ const id = CustomElements.uid('ful-input');
429
+ const name = this.getAttribute('@name');
430
+ const floating = this.hasAttribute('@floating');
431
+ const slotted = CustomElements.extractSlots(this);
432
+ slotted.input = slotted.input || (() => {
433
+ const el = document.createElement("input");
434
+ el.classList.add("form-control");
435
+ return el;
436
+ })();
437
+ CustomElements.forwardAttributes(this, slotted.input, ['@floating']);
438
+ const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
439
+ attrIfMissing(slotted.input, "name", id);
440
+ attrIfMissing(slotted.input, "id", id);
441
+ attrIfMissing(slotted.input, "type", "text");
442
+ attrIfMissing(slotted.input, "placeholder", " ");
443
+ this.innerHTML = '';
444
+ this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
445
+ }
446
+ static configure() {
447
+ customElements.define('ful-input', Input);
448
+ }
449
+ }
450
+
451
+
452
+
453
+ /**
454
+ * <script src="tom-select.complete.js"></script>
455
+ * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
456
+ */
457
+ class Select extends HTMLElement {
458
+ constructor(tsConfig) {
459
+ super();
460
+ Observable.mixin(this);
461
+ const id = CustomElements.uid('ful-select');
462
+ const name = this.getAttribute('@name');
463
+ const floating = this.hasAttribute('@floating');
464
+ const remote = this.hasAttribute('@remote');
465
+ const slotted = CustomElements.extractSlots(this);
466
+ slotted.input = slotted.input || (() => {
467
+ return document.createElement("select");
468
+ })();
469
+ CustomElements.forwardAttributes(this, slotted.input, ['@floating', '@remote']);
470
+ const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
471
+ attrIfMissing(slotted.input, "name", id);
472
+ attrIfMissing(slotted.input, "id", id);
473
+ attrIfMissing(slotted.input, "placeholder", " ");
474
+ this.innerHTML = '';
475
+ this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
476
+ this.loaded = !remote;
477
+ this.ts = new TomSelect(slotted.input, Object.assign(remote ? {
478
+ preload: 'focus',
479
+ load: async (query, callback) => {
480
+ if (this.loaded) {
481
+ callback();
482
+ return;
483
+ }
484
+ const data = await this.fire('load', query, []);
485
+ this.loaded = true;
486
+ callback(data);
487
+ }
488
+ } : {}, tsConfig));
489
+ slotted.input.setValue = this.setValue.bind(this);
490
+ slotted.input.getValue = this.getValue.bind(this);
491
+ }
492
+ async setValue(v){
493
+ if(!this.loaded){
494
+ await this.ts.load();
495
+ }
496
+ this.ts.setValue(v);
497
+ }
498
+ getValue(){
499
+ const v = this.ts.getValue();
500
+ return v === '' ? null : v;
501
+ }
502
+ static custom(tagName, configuration) {
503
+ customElements.define(tagName, class extends Select {
504
+ constructor() {
505
+ super(configuration);
376
506
  }
377
507
  });
378
- try {
379
- const r = await this.json(resource, options);
380
- ui.form?.clearErrors();
381
- return r;
382
- } catch (e) {
383
- ui.form?.setErrors(e.problems);
384
- throw e;
385
- } finally {
386
- ui.buttons?.forEach(el => {
387
- el.removeAttribute("disabled");
388
- el.innerHTML = el.dataset['oldContent'];
389
- delete el.dataset['oldContent'];
508
+ }
509
+ static configure() {
510
+ return Select.custom('ful-select');
511
+ }
512
+
513
+ }
514
+
515
+ class Form extends HTMLElement {
516
+ constructor({ mutators, extractors, valueHoldersSelector, ignoredChildrenSelector }) {
517
+ super();
518
+ Observable.mixin(this);
519
+ this.mutators = mutators || {};
520
+ this.extractors = extractors || {};
521
+ this.valueHoldersSelector = valueHoldersSelector || '[name]';
522
+ this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
523
+
524
+ const form = document.createElement('form');
525
+ form.append(...this.childNodes);
526
+ this.appendChild(form);
527
+
528
+ form.addEventListener('submit', async (e) => {
529
+ e.preventDefault();
530
+ this.spinner(true);
531
+ try {
532
+ await this.fire('submit', this.getValues(), this);
533
+ } catch (e) {
534
+ if (e instanceof Failure) {
535
+ this.setErrors(e.problems);
536
+ return;
537
+ }
538
+ throw e;
539
+ } finally {
540
+ this.spinner(false);
541
+ }
542
+ });
543
+ }
544
+ spinner(spin) {
545
+ this.querySelectorAll('ful-spinner').forEach(el => {
546
+ el[spin ? 'show' : 'hide']();
547
+ });
548
+ this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
549
+ el.disabled = spin;
550
+ });
551
+ }
552
+ setValues(values) {
553
+ for (let k in values) {
554
+ if (!values.hasOwnProperty(k)) {
555
+ continue;
556
+ }
557
+ Array.from(this.querySelectorAll(`[name='${CSS.escape(k)}']`)).forEach((el) => {
558
+ Form.mutate(this.mutators, el, values[k], k, values);
390
559
  });
391
560
  }
392
561
  }
562
+ getValues() {
563
+ return Array.from(this.querySelectorAll(this.valueHoldersSelector))
564
+ .filter((el) => {
565
+ if (el.dataset['fulBindInclude'] === 'never') {
566
+ return false;
567
+ }
568
+ return el.dataset['fulBindInclude'] === 'always' || el.closest(this.ignoredChildrenSelector) === null;
569
+ })
570
+ .reduce((result, el) => {
571
+ return Form.providePath(result, el.getAttribute('name'), Form.extract(this.extractors, el));
572
+ }, {});
573
+ }
574
+ setErrors(errors, scroll) {
575
+ this.clearErrors();
576
+ errors
577
+ .filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
578
+ .forEach((e) => {
579
+ const name = e.context.replace("[", ".").replace("].", ".");
580
+ this.querySelectorAll(`[name='${CSS.escape(name)}']`)
581
+ .forEach(input => {
582
+ input.classList.add('is-invalid');
583
+ if (input.parentElement.classList.contains("form-floating")) {
584
+ input.parentElement.classList.add('is-invalid');
585
+ }
586
+ });
587
+ this.querySelectorAll(`ful-field-error[field='${CSS.escape(name)}']`)
588
+ .forEach(el => el.innerText = e.reason);
589
+ });
590
+ this.querySelectorAll("ful-errors")
591
+ .forEach(el => {
592
+ const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
593
+ el.innerHTML = globalErrors.map(e => e.reason).join("\n");
594
+ if (globalErrors.length !== 0) {
595
+ el.classList.remove('d-none');
596
+ }
597
+ });
598
+
599
+ if (!scroll) {
600
+ return;
601
+ }
602
+ const ys = Array.from(this.querySelectorAll('ful-field-error:not(.d-none)'))
603
+ .map(el => el.getBoundingClientRect().y + window.scrollY);
604
+ const miny = Math.min(...ys);
605
+ if (miny !== Infinity) {
606
+ window.scroll(window.scrollX, miny > 100 ? miny - 100 : 0);
607
+ }
608
+ }
609
+ clearErrors() {
610
+ this.querySelectorAll('[name].is-invalid, .form-floating.is-invalid')
611
+ .forEach(el => el.classList.remove('is-invalid'));
612
+ this.querySelectorAll("ful-errors")
613
+ .forEach(el => {
614
+ el.innerHTML = '';
615
+ el.classList.add('d-none');
616
+ });
617
+ }
618
+ static extract(extractors, el) {
619
+ const maybeExtractor = extractors[el.dataset['fulBindExtractor']] || extractors[el.dataset['fulBindProvide']];
620
+ if (maybeExtractor) {
621
+ return maybeExtractor(el);
622
+ }
623
+ if (el.getAttribute('type') === 'radio') {
624
+ if (!el.checked) {
625
+ return undefined;
626
+ }
627
+ return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
628
+ }
629
+ if (el.getAttribute('type') === 'checkbox') {
630
+ return el.checked;
631
+ }
632
+ if (el.dataset['fulBindType'] === 'boolean') {
633
+ return !el.value ? null : el.value === 'true';
634
+ }
635
+ if (el.getValue) {
636
+ return el.getValue();
637
+ }
638
+ return el.value || null;
639
+ }
640
+ static mutate(mutators, el, raw, key, values) {
641
+ const maybeMutator = mutators[el.dataset['fulBindMutator']] || mutators[el.dataset['fulBindProvide']];
642
+ if (maybeMutator) {
643
+ maybeMutator(el, raw, key, values);
644
+ return;
645
+ }
646
+ if (el.getAttribute('type') === 'radio') {
647
+ el.checked = el.getAttribute('value') === raw;
648
+ return;
649
+ }
650
+ if (el.getAttribute('type') === 'checkbox') {
651
+ el.checked = raw;
652
+ return;
653
+ }
654
+ if (el.setValue) {
655
+ el.setValue(raw);
656
+ return;
657
+ }
658
+ el.value = raw;
659
+ }
660
+
661
+ static providePath(result, path, value) {
662
+ const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
663
+ let current = result;
664
+ let previous = null;
665
+ for (let i = 0; ; ++i) {
666
+ const ckey = keys[i];
667
+ const pkey = keys[i - 1];
668
+ if (Number.isInteger(ckey) && !Array.isArray(current)) {
669
+ if (previous !== null) {
670
+ previous[pkey] = current = [];
671
+ } else {
672
+ result = current = [];
673
+ }
674
+ }
675
+ if (i === keys.length - 1) {
676
+ //when value is undefined we only want to define the property if it's not defined
677
+ current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
678
+ return result;
679
+ }
680
+ if (current[ckey] === undefined) {
681
+ current[ckey] = {};
682
+ }
683
+ previous = current;
684
+ current = current[ckey];
685
+ }
686
+ }
687
+ static custom(tagName, configuration) {
688
+ customElements.define(tagName, class extends Form {
689
+ constructor() {
690
+ super(configuration);
691
+ }
692
+ });
693
+ }
694
+ static configure(configuration) {
695
+ FieldError.configure();
696
+ Errors.configure();
697
+ Spinner.configure();
698
+ Input.configure();
699
+ Select.configure();
700
+ Form.custom('ful-form', configuration || {});
701
+ }
393
702
  }
394
703
 
395
704
  class Storage {
@@ -454,22 +763,22 @@ class VersionedStorage {
454
763
 
455
764
  class AuthorizationCodeFlow {
456
765
  static forKeycloak(clientId, realmBaseUrl, redirectUri){
457
- const authUri = new URL("protocol/openid-connect/auth", realmBaseUrl);
458
- const tokenUri = new URL("protocol/openid-connect/token", realmBaseUrl);
459
- const logoutUri = new URL("protocol/openid-connect/logout", realmBaseUrl);
460
766
  const scope = "openid profile";
461
- return new AuthorizationCodeFlow(clientId, scope, authUri, tokenUri, logoutUri, redirectUri);
462
- }
463
- constructor(clientId, scope, authUri, tokenUri, logoutUri, redirectUri) {
767
+ return new AuthorizationCodeFlow(clientId, scope, {
768
+ auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
769
+ token: new URL("protocol/openid-connect/token", realmBaseUrl),
770
+ logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
771
+ registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
772
+ redirect: redirectUri
773
+ });
774
+ }
775
+ constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
776
+ this.storage = new SessionStorage(clientId);
464
777
  this.clientId = clientId;
465
778
  this.scope = scope;
466
- this.authUri = authUri;
467
- this.tokenUri = tokenUri;
468
- this.logoutUri = logoutUri;
469
- this.redirectUri = redirectUri;
470
- this.storage = new SessionStorage(clientId);
779
+ this.uri = {auth, token, registration, logout, redirect};
471
780
  }
472
- async _auth() {
781
+ async action(uri, additionalParams){
473
782
  const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
474
783
  const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
475
784
  const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
@@ -477,23 +786,34 @@ class AuthorizationCodeFlow {
477
786
  state: state,
478
787
  verifier: pkceVerifier
479
788
  });
480
- const url = new URL(this.authUri);
789
+ const url = new URL(uri);
481
790
  url.searchParams.set("client_id", this.clientId);
482
- url.searchParams.set("redirect_uri", this.redirectUri);
791
+ url.searchParams.set("redirect_uri", this.uri.redirect);
483
792
  url.searchParams.set("response_type", 'code');
484
793
  url.searchParams.set("scope", this.scope);
485
794
  url.searchParams.set("state", state);
486
795
  url.searchParams.set("code_challenge", pkceChallenge);
487
796
  url.searchParams.set("code_challenge_method", 'S256');
797
+ Object.entries(additionalParams || {}).forEach(kv => {
798
+ url.searchParams.set(kv[0], kv[1]);
799
+ });
488
800
  window.location = url;
489
801
  }
802
+ async registration(additionalParams){
803
+ await this.action(this.uri.registration, additionalParams);
804
+ }
805
+ async applicationInitiatedAction(kcAction){
806
+ await this.action(this.uri.auth, {
807
+ kc_action: kcAction
808
+ });
809
+ }
490
810
  async _tokenExchange(code, state) {
491
- window.history.replaceState('', "", this.redirectUri);
811
+ window.history.replaceState('', "", this.uri.redirect);
492
812
  const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
493
813
  if (stateAndVerifier.state !== state) {
494
814
  throw new Error("State mismatch");
495
815
  }
496
- const response = await fetch(this.tokenUri, {
816
+ const response = await fetch(this.uri.token, {
497
817
  method: "POST",
498
818
  headers: {
499
819
  "Content-Type": 'application/x-www-form-urlencoded'
@@ -504,7 +824,7 @@ class AuthorizationCodeFlow {
504
824
  ["grant_type", "authorization_code"],
505
825
  ["code_verifier", stateAndVerifier.verifier],
506
826
  ["state", stateAndVerifier.state],
507
- ["redirect_uri", this.redirectUri]
827
+ ["redirect_uri", this.uri.redirect]
508
828
  ])
509
829
  });
510
830
  if (!response.ok) {
@@ -512,7 +832,7 @@ class AuthorizationCodeFlow {
512
832
  throw new Error("Error:" + response.status + ": " + text);
513
833
  }
514
834
  const token = await response.json();
515
- return new AuthorizationCodeFlowSession(this.clientId, token, this.tokenUri, this.logoutUri, this.redirectUri);
835
+ return new AuthorizationCodeFlowSession(this.clientId, token, this.uri);
516
836
  }
517
837
  async ensureLoggedIn() {
518
838
  const url = new URL(window.location.href);
@@ -523,7 +843,7 @@ class AuthorizationCodeFlow {
523
843
  return await this._tokenExchange(code, state);
524
844
  }
525
845
  //if not authorized
526
- await this._auth();
846
+ await this.action(this.uri.auth, {});
527
847
  return null;
528
848
  }
529
849
  }
@@ -532,27 +852,26 @@ AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
532
852
  class AuthorizationCodeFlowSession {
533
853
  static parseToken(token) {
534
854
  const [rawHeader, rawPayload, signature] = token.split(".");
855
+ const ut8decoder = new TextDecoder("utf-8");
535
856
  return {
536
- header: JSON.parse(atob(rawHeader)),
537
- payload: JSON.parse(atob(rawPayload)),
857
+ header: JSON.parse(ut8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
858
+ payload: JSON.parse(ut8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
538
859
  signature: signature
539
860
  };
540
861
  }
541
- constructor(clientId, token, tokenUri, logoutUri, redirectUri) {
862
+ constructor(clientId, t, {token, logout, redirect}) {
542
863
  this.clientId = clientId;
543
- this.token = token;
544
- this.tokenUri = tokenUri;
545
- this.logoutUri = logoutUri;
546
- this.redirectUri = redirectUri;
547
- this.accessToken = AuthorizationCodeFlowSession.parseToken(token.access_token);
548
- this.refreshToken = AuthorizationCodeFlowSession.parseToken(token.refresh_token);
864
+ this.token = t;
865
+ this.accessToken = AuthorizationCodeFlowSession.parseToken(t.access_token);
866
+ this.refreshToken = AuthorizationCodeFlowSession.parseToken(t.refresh_token);
867
+ this.uri = { token, logout, redirect };
549
868
  this.refreshCallback = null;
550
869
  }
551
870
  onRefresh(callback) {
552
871
  this.refreshCallback = callback;
553
872
  }
554
873
  async refresh() {
555
- const response = await fetch(this.tokenUri, {
874
+ const response = await fetch(this.uri.token, {
556
875
  method: "POST",
557
876
  headers: {
558
877
  "Content-Type": 'application/x-www-form-urlencoded'
@@ -564,7 +883,7 @@ class AuthorizationCodeFlowSession {
564
883
  ])
565
884
  });
566
885
  if (!response.ok) {
567
- throw new Error("Error:" + response.code + ": " + response.text());
886
+ throw new Error("Error:" + response.status + ": " + response.text());
568
887
  }
569
888
  const token = await response.json();
570
889
  this.token = token;
@@ -588,8 +907,8 @@ class AuthorizationCodeFlowSession {
588
907
  await this.refresh();
589
908
  }
590
909
  logout() {
591
- const url = new URL(this.logoutUri);
592
- url.searchParams.set("post_logout_redirect_uri", this.redirectUri);
910
+ const url = new URL(this.uri.logout);
911
+ url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
593
912
  url.searchParams.set("id_token_hint", this.token.id_token);
594
913
  window.location = url;
595
914
  }
@@ -609,13 +928,12 @@ class AuthorizationCodeFlowInterceptor {
609
928
  this.gracePeriodBefore = gracePeriodBefore || 2000;
610
929
  this.gracePeriodAfter = gracePeriodAfter || 30000;
611
930
  }
612
- async before(request) {
931
+ async intercept(request, chain) {
613
932
  await this.session.refreshIf(this.gracePeriodBefore);
614
933
  const headers = new Headers(request.options.headers);
615
934
  headers.set("Authorization", this.session.bearerToken());
616
- return request;
617
- }
618
- async after(request, response) {
935
+ request.options.headers = headers;
936
+ const response = await chain.proceed(request);
619
937
  await this.session.refreshIf(this.gracePeriodAfter);
620
938
  return response;
621
939
  }
@@ -703,10 +1021,10 @@ const timing = {
703
1021
  }
704
1022
  };
705
1023
 
706
- class Wizard {
707
- constructor(el) {
708
- this.el = el;
709
- this.progress = [...el.children].filter(e => e.matches("header,ol,ul"));
1024
+ class Wizard extends HTMLElement {
1025
+ constructor() {
1026
+ super();
1027
+ this.progress = [...this.children].filter(e => e.matches("header,ol,ul"));
710
1028
 
711
1029
  this.progress.forEach(p => {
712
1030
  const children = [...p.children];
@@ -715,8 +1033,8 @@ class Wizard {
715
1033
  children[0].classList.add('active');
716
1034
  }
717
1035
  });
718
- if (this.el.querySelector('section.current') === null) {
719
- const firstSection = this.el.querySelector('section:first-of-type');
1036
+ if (this.querySelector('section.current') === null) {
1037
+ const firstSection = this.querySelector('section:first-of-type');
720
1038
  if (firstSection !== null) {
721
1039
  firstSection.classList.add('current');
722
1040
  }
@@ -729,11 +1047,11 @@ class Wizard {
729
1047
  current?.classList.remove('active');
730
1048
  current?.nextElementSibling?.classList.add('active');
731
1049
  });
732
- const currentSection = this.el.querySelector('section.current');
1050
+ const currentSection = this.querySelector('section.current');
733
1051
  currentSection.classList.remove("current");
734
1052
  currentSection.nextElementSibling.classList.add('current');
735
1053
 
736
- this.el.dispatchEvent(new CustomEvent('wizard:activate', {
1054
+ this.dispatchEvent(new CustomEvent('wizard:activate', {
737
1055
  bubbles: true,
738
1056
  cancelable: true
739
1057
  }));
@@ -746,10 +1064,10 @@ class Wizard {
746
1064
  current?.classList.remove('active');
747
1065
  current?.previousElementSibling?.classList.add('active');
748
1066
  });
749
- const currentSection = this.el.querySelector('section.current');
1067
+ const currentSection = this.querySelector('section.current');
750
1068
  currentSection.classList.remove("current");
751
1069
  currentSection.previousElementSibling.classList.add('current');
752
- this.el.dispatchEvent(new CustomEvent('wizard:activate', {
1070
+ this.dispatchEvent(new CustomEvent('wizard:activate', {
753
1071
  bubbles: true,
754
1072
  cancelable: true
755
1073
  }));
@@ -761,15 +1079,25 @@ class Wizard {
761
1079
  current?.classList.remove('active');
762
1080
  p.children[+n]?.classList.add('active');
763
1081
  });
764
- const currentSection = this.el.querySelector('section.current');
1082
+ const currentSection = this.querySelector('section.current');
765
1083
  currentSection?.classList.remove("current");
766
- const nthSection = this.el.querySelector(`section:nth-child(${+n})`);
1084
+ const nthSection = this.querySelector(`section:nth-child(${+n})`);
767
1085
  nthSection.classList.add('current');
768
- this.el.dispatchEvent(new CustomEvent('wizard:activate', {
1086
+ this.dispatchEvent(new CustomEvent('wizard:activate', {
769
1087
  bubbles: true,
770
1088
  cancelable: true
771
1089
  }));
772
1090
  }
1091
+ static custom(tagName, configuration) {
1092
+ customElements.define(tagName, class extends Wizard {
1093
+ constructor() {
1094
+ super(configuration);
1095
+ }
1096
+ });
1097
+ }
1098
+ static configure() {
1099
+ return Wizard.custom('ful-wizard');
1100
+ }
773
1101
  }
774
1102
 
775
1103
  class App {
@@ -798,5 +1126,5 @@ class App {
798
1126
  }
799
1127
  }
800
1128
 
801
- export { App, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Bindings, Failure, Form, Hex, HttpClient, LocalStorage, SessionStorage, VersionedStorage, Wizard, timing };
1129
+ export { App, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, CustomElements, Errors, Failure, FieldError, Form, Hex, HttpClient, Input, LocalStorage, Observable, Select, SessionStorage, Spinner, VersionedStorage, Wizard, timing };
802
1130
  //# sourceMappingURL=ful.mjs.map