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