@optionfactory/ful 0.24.0 → 0.26.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,8 +73,9 @@ var ful = (function (exports) {
73
73
  }
74
74
  }
75
75
 
76
- class Observable {
77
- constructor() {
76
+ const Observable = (SuperClass) => class extends SuperClass {
77
+ constructor(...args) {
78
+ super(...args);
78
79
  this.listeners = {};
79
80
  }
80
81
  fireSync(event, data, initialAcc) {
@@ -101,16 +102,8 @@ var ful = (function (exports) {
101
102
  const listeners = this.listeners[event] || [];
102
103
  const idx = listeners.indexOf(listener);
103
104
  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;
111
- }
112
-
113
- }
105
+ }
106
+ };
114
107
 
115
108
  class ContextInterceptor {
116
109
  constructor() {
@@ -279,775 +272,813 @@ var ful = (function (exports) {
279
272
  return jsonRequest('PATCH', body, headers);
280
273
  }
281
274
 
282
- /* global Infinity, CSS */
283
-
284
- class CustomElements {
285
- static id = 0;
286
- static uid(prefix) {
287
- return `${prefix}-${++CustomElements.id}`;
288
- }
289
- static forwardAttributes(from, to, except) {
290
- from.getAttributeNames().filter(a => except.indexOf(a) === -1)
291
- .filter(a => a[0] === '@')
292
- .forEach(a => {
293
- if (a === '@class') {
294
- to.classList.add(...from.getAttribute("@class").split(" ").filter(a => a.length));
295
- return;
296
- }
297
- to.setAttribute(a.substring(1), from.getAttribute(a));
298
- });
299
- }
300
- static extractSlots(el) {
301
- const slotted = Object.fromEntries([...el.querySelectorAll("[slot]")].map(el => {
302
- el.parentElement.removeChild(el);
303
- const slot = el.getAttribute("slot");
304
- el.removeAttribute("slot");
305
- return [slot, el];
306
- }));
307
- slotted.default = new DocumentFragment();
308
- slotted.default.append(...el.childNodes);
309
- return slotted;
275
+ class Storage {
276
+ constructor(prefix, storage) {
277
+ this.prefix = prefix;
278
+ this.storage = storage;
310
279
  }
311
- static labelAndInputGroup(id, name, isFloating, slotted) {
312
- if (isFloating) {
313
- /**
314
- * <div class="input-group has-validation">
315
- * <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
316
- * <div class="form-floating">
317
- * {{{{ slotted.input }}}}
318
- * <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
319
- * </div>
320
- * <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
321
- * <ful-field-error data-tpl-field="name"></ful-field-error>
322
- * </div>
323
- */
324
- const label = document.createElement("label");
325
- label.setAttribute("for", id);
326
- label.classList.add('form-label');
327
- label.append(slotted.default);
328
-
329
- const ff = document.createElement('div');
330
- ff.classList.add("form-floating");
331
- ff.append(slotted.input, label);
332
-
333
- const ffe = document.createElement('ful-field-error');
334
- ffe.setAttribute("field", name);
335
-
336
- const ig = document.createElement("div");
337
- ig.classList.add('input-group', 'has-validtion');
338
-
339
- if (slotted.before) {
340
- ig.append(slotted.before);
341
- } else if (slotted.ibefore) {
342
- const igt = document.createElement('div');
343
- igt.classList.add('input-group-text');
344
- igt.append(slotted.ibefore);
345
- ig.append(igt);
346
- }
347
- ig.append(ff);
348
- if (slotted.after) {
349
- ig.append(slotted.after);
350
- } else if (slotted.iafter) {
351
- const igt = document.createElement('div');
352
- igt.classList.add('input-group-text');
353
- igt.append(slotted.iafter);
354
- ig.append(igt);
355
- }
356
- ig.append(ffe);
357
- return ig;
358
- }
359
- /**
360
- <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
361
- <div class="input-group has-validation">
362
- <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
363
- {{{{ slotted.input }}}}
364
- <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
365
- <ful-field-error data-tpl-field="name"></ful-field-error>
366
- </div>
367
- */
368
-
369
- const label = document.createElement("label");
370
- label.setAttribute("for", name);
371
- label.classList.add('form-label');
372
- label.append(slotted.default);
373
-
374
- const ffe = document.createElement('ful-field-error');
375
- ffe.setAttribute("field", name);
376
-
377
- const ig = document.createElement("div");
378
- ig.classList.add('input-group', 'has-validation');
379
-
380
- if (slotted.before) {
381
- ig.append(slotted.before);
382
- } else if (slotted.ibefore) {
383
- const igt = document.createElement('div');
384
- igt.classList.add('input-group-text');
385
- igt.append(slotted.ibefore);
386
- ig.append(igt);
387
- }
388
- ig.append(slotted.input);
389
- if (slotted.after) {
390
- ig.append(slotted.after);
391
- } else if (slotted.iafter) {
392
- const igt = document.createElement('div');
393
- igt.classList.add('input-group-text');
394
- igt.append(slotted.iafter);
395
- ig.append(igt);
396
- }
397
- ig.append(ffe);
398
-
399
- const fragment = new DocumentFragment();
400
- fragment.append(label, ig);
401
- return fragment;
280
+ save(k, v) {
281
+ this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
402
282
  }
403
-
404
- }
405
-
406
-
407
- class FieldError extends HTMLElement {
408
- constructor() {
409
- super();
283
+ load(k) {
284
+ const got = this.storage.getItem(`${this.prefix}-${k}`);
285
+ return got === undefined ? undefined : JSON.parse(got);
410
286
  }
411
- connectedCallback() {
412
- this.classList.add('invalid-feedback');
287
+ remove(k) {
288
+ this.storage.removeItem(`${this.prefix}-${k}`);
413
289
  }
414
- static configure() {
415
- customElements.define('ful-field-error', FieldError);
290
+ pop(k) {
291
+ const decoded = this.load(k);
292
+ this.remove(k);
293
+ return decoded;
416
294
  }
417
295
  }
418
296
 
419
- class Errors extends HTMLElement {
420
- constructor() {
421
- super();
422
- }
423
- connectedCallback() {
424
- this.classList.add('alert', 'alert-danger', 'd-none');
425
- }
426
- static configure() {
427
- customElements.define('ful-errors', Errors);
297
+ class LocalStorage extends Storage {
298
+ constructor(prefix) {
299
+ super(prefix, localStorage);
428
300
  }
429
-
430
301
  }
431
302
 
432
- class Spinner extends HTMLElement {
433
- constructor() {
434
- super();
435
- }
436
- connectedCallback() {
437
- this.classList.add('spinner-border', 'spinner-border-sm', 'd-none');
438
- this.setAttribute("aria-hidden", "true");
439
- }
440
- show() {
441
- this.classList.remove("d-none");
442
- }
443
- hide() {
444
- this.classList.add("d-none");
445
- }
446
- static configure() {
447
- customElements.define('ful-spinner', Spinner);
303
+ class SessionStorage extends Storage {
304
+ constructor(prefix) {
305
+ super(prefix, sessionStorage);
448
306
  }
449
307
  }
450
308
 
451
-
452
-
453
- class Input extends HTMLElement {
454
- constructor() {
455
- super();
456
- const id = CustomElements.uid('ful-input');
457
- const name = this.getAttribute('@name');
458
- const floating = this.hasAttribute('@floating');
459
- const slotted = CustomElements.extractSlots(this);
460
- slotted.input = slotted.input || (() => {
461
- const el = document.createElement("input");
462
- el.classList.add("form-control");
463
- return el;
464
- })();
465
- CustomElements.forwardAttributes(this, slotted.input, ['@floating']);
466
- const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
467
- attrIfMissing(slotted.input, "name", id);
468
- attrIfMissing(slotted.input, "id", id);
469
- attrIfMissing(slotted.input, "type", "text");
470
- attrIfMissing(slotted.input, "placeholder", " ");
471
- this.innerHTML = '';
472
- this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
309
+ class VersionedStorage {
310
+ constructor(storage, key, dataSupplier){
311
+ this.storage = storage;
312
+ this.key = key;
313
+ this.dataSupplier = dataSupplier;
314
+ this.cache = null;
315
+
473
316
  }
474
- static configure() {
475
- customElements.define('ful-input', Input);
317
+ async load(revision){
318
+ const saved = this.storage.load(this.key);
319
+ if (!!saved && saved.revision === revision) {
320
+ this.cache = saved.value;
321
+ return;
322
+ }
323
+ const freshData = await this.dataSupplier(revision, this.key);
324
+ this.storage.save(this.key, {
325
+ revision: revision,
326
+ value: freshData
327
+ });
328
+ this.cache = freshData;
329
+ }
330
+ data(){
331
+ return this.cache;
476
332
  }
477
333
  }
478
334
 
479
-
480
-
481
- /**
482
- * <script src="tom-select.complete.js"></script>
483
- * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
484
- */
485
- class Select extends HTMLElement {
486
- constructor(tsConfig) {
487
- super();
488
- Observable.mixin(this);
489
- const id = CustomElements.uid('ful-select');
490
- const name = this.getAttribute('@name');
491
- const floating = this.hasAttribute('@floating');
492
- const remote = this.hasAttribute('@remote');
493
- const slotted = CustomElements.extractSlots(this);
494
- slotted.input = slotted.input || (() => {
495
- return document.createElement("select");
496
- })();
497
- CustomElements.forwardAttributes(this, slotted.input, ['@floating', '@remote']);
498
- const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
499
- attrIfMissing(slotted.input, "name", id);
500
- attrIfMissing(slotted.input, "id", id);
501
- attrIfMissing(slotted.input, "placeholder", " ");
502
- this.innerHTML = '';
503
- this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
504
- this.loaded = !remote;
505
- this.ts = new TomSelect(slotted.input, Object.assign(remote ? {
506
- preload: 'focus',
507
- load: async (query, callback) => {
508
- if (this.loaded) {
509
- callback();
510
- return;
511
- }
512
- const data = await this.fire('load', query, []);
513
- this.loaded = true;
514
- callback(data);
515
- }
516
- } : {}, tsConfig));
517
- slotted.input.setValue = this.setValue.bind(this);
518
- slotted.input.getValue = this.getValue.bind(this);
519
- }
520
- async setValue(v){
521
- if(!this.loaded){
522
- await this.ts.load();
523
- }
524
- this.ts.setValue(v);
335
+ class AuthorizationCodeFlow {
336
+ static forKeycloak(clientId, realmBaseUrl, redirectUri){
337
+ const scope = "openid profile";
338
+ return new AuthorizationCodeFlow(clientId, scope, {
339
+ auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
340
+ token: new URL("protocol/openid-connect/token", realmBaseUrl),
341
+ logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
342
+ registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
343
+ redirect: redirectUri
344
+ });
525
345
  }
526
- getValue(){
527
- const v = this.ts.getValue();
528
- return v === '' ? null : v;
346
+ constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
347
+ this.storage = new SessionStorage(clientId);
348
+ this.clientId = clientId;
349
+ this.scope = scope;
350
+ this.uri = {auth, token, registration, logout, redirect};
529
351
  }
530
- static custom(tagName, configuration) {
531
- customElements.define(tagName, class extends Select {
532
- constructor() {
533
- super(configuration);
534
- }
352
+ async action(uri, additionalParams){
353
+ const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
354
+ const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
355
+ const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
356
+ this.storage.save(AuthorizationCodeFlow.PKCE_AND_STATE_KEY, {
357
+ state: state,
358
+ verifier: pkceVerifier
359
+ });
360
+ const url = new URL(uri);
361
+ url.searchParams.set("client_id", this.clientId);
362
+ url.searchParams.set("redirect_uri", this.uri.redirect);
363
+ url.searchParams.set("response_type", 'code');
364
+ url.searchParams.set("scope", this.scope);
365
+ url.searchParams.set("state", state);
366
+ url.searchParams.set("code_challenge", pkceChallenge);
367
+ url.searchParams.set("code_challenge_method", 'S256');
368
+ Object.entries(additionalParams || {}).forEach(kv => {
369
+ url.searchParams.set(kv[0], kv[1]);
535
370
  });
371
+ window.location = url;
536
372
  }
537
- static configure() {
538
- return Select.custom('ful-select');
373
+ async registration(additionalParams){
374
+ await this.action(this.uri.registration, additionalParams);
539
375
  }
540
-
541
- }
542
-
543
- class Form extends HTMLElement {
544
- constructor({ mutators, extractors, valueHoldersSelector, ignoredChildrenSelector }) {
545
- super();
546
- Observable.mixin(this);
547
- this.mutators = mutators || {};
548
- this.extractors = extractors || {};
549
- this.valueHoldersSelector = valueHoldersSelector || '[name]';
550
- this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
551
-
552
- const form = document.createElement('form');
553
- form.append(...this.childNodes);
554
- this.appendChild(form);
555
-
556
- form.addEventListener('submit', async (e) => {
557
- e.preventDefault();
558
- this.spinner(true);
559
- try {
560
- await this.fire('submit', this.getValues(), this);
561
- } catch (e) {
562
- if (e instanceof Failure) {
563
- this.setErrors(e.problems);
564
- return;
565
- }
566
- throw e;
567
- } finally {
568
- this.spinner(false);
569
- }
376
+ async applicationInitiatedAction(kcAction){
377
+ await this.action(this.uri.auth, {
378
+ kc_action: kcAction
570
379
  });
571
380
  }
572
- spinner(spin) {
573
- this.querySelectorAll('ful-spinner').forEach(el => {
574
- el[spin ? 'show' : 'hide']();
575
- });
576
- this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
577
- el.disabled = spin;
381
+ async _tokenExchange(code, state) {
382
+ window.history.replaceState('', "", this.uri.redirect);
383
+ const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
384
+ if (stateAndVerifier.state !== state) {
385
+ throw new Error("State mismatch");
386
+ }
387
+ const response = await fetch(this.uri.token, {
388
+ method: "POST",
389
+ headers: {
390
+ "Content-Type": 'application/x-www-form-urlencoded'
391
+ },
392
+ body: new URLSearchParams([
393
+ ["client_id", this.clientId],
394
+ ["code", code],
395
+ ["grant_type", "authorization_code"],
396
+ ["code_verifier", stateAndVerifier.verifier],
397
+ ["state", stateAndVerifier.state],
398
+ ["redirect_uri", this.uri.redirect]
399
+ ])
578
400
  });
401
+ if (!response.ok) {
402
+ const text = await response.text();
403
+ throw new Error("Error:" + response.status + ": " + text);
404
+ }
405
+ const token = await response.json();
406
+ return new AuthorizationCodeFlowSession(this.clientId, token, this.uri);
579
407
  }
580
- setValues(values) {
581
- for (let k in values) {
582
- if (!values.hasOwnProperty(k)) {
583
- continue;
584
- }
585
- Array.from(this.querySelectorAll(`[name='${CSS.escape(k)}']`)).forEach((el) => {
586
- Form.mutate(this.mutators, el, values[k], k, values);
587
- });
408
+ async ensureLoggedIn() {
409
+ const url = new URL(window.location.href);
410
+ const code = url.searchParams.get("code");
411
+ if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
412
+ //if callback from keycloak and we have our state still stored
413
+ const state = url.searchParams.get("state");
414
+ return await this._tokenExchange(code, state);
588
415
  }
416
+ //if not authorized
417
+ await this.action(this.uri.auth, {});
418
+ return null;
589
419
  }
590
- getValues() {
591
- return Array.from(this.querySelectorAll(this.valueHoldersSelector))
592
- .filter((el) => {
593
- if (el.dataset['fulBindInclude'] === 'never') {
594
- return false;
595
- }
596
- return el.dataset['fulBindInclude'] === 'always' || el.closest(this.ignoredChildrenSelector) === null;
597
- })
598
- .reduce((result, el) => {
599
- return Form.providePath(result, el.getAttribute('name'), Form.extract(this.extractors, el));
600
- }, {});
601
- }
602
- setErrors(errors, scroll) {
603
- this.clearErrors();
604
- errors
605
- .filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
606
- .forEach((e) => {
607
- const name = e.context.replace("[", ".").replace("].", ".");
608
- this.querySelectorAll(`[name='${CSS.escape(name)}']`)
609
- .forEach(input => {
610
- input.classList.add('is-invalid');
611
- if (input.parentElement.classList.contains("form-floating")) {
612
- input.parentElement.classList.add('is-invalid');
613
- }
614
- });
615
- this.querySelectorAll(`ful-field-error[field='${CSS.escape(name)}']`)
616
- .forEach(el => el.innerText = e.reason);
617
- });
618
- this.querySelectorAll("ful-errors")
619
- .forEach(el => {
620
- const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
621
- el.innerHTML = globalErrors.map(e => e.reason).join("\n");
622
- if (globalErrors.length !== 0) {
623
- el.classList.remove('d-none');
624
- }
625
- });
420
+ }
421
+ AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
626
422
 
627
- if (!scroll) {
628
- return;
629
- }
630
- const ys = Array.from(this.querySelectorAll('ful-field-error:not(.d-none)'))
631
- .map(el => el.getBoundingClientRect().y + window.scrollY);
632
- const miny = Math.min(...ys);
633
- if (miny !== Infinity) {
634
- window.scroll(window.scrollX, miny > 100 ? miny - 100 : 0);
635
- }
423
+ class AuthorizationCodeFlowSession {
424
+ static parseToken(token) {
425
+ const [rawHeader, rawPayload, signature] = token.split(".");
426
+ const ut8decoder = new TextDecoder("utf-8");
427
+ return {
428
+ header: JSON.parse(ut8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
429
+ payload: JSON.parse(ut8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
430
+ signature: signature
431
+ };
432
+ }
433
+ constructor(clientId, t, {token, logout, redirect}) {
434
+ this.clientId = clientId;
435
+ this.token = t;
436
+ this.accessToken = AuthorizationCodeFlowSession.parseToken(t.access_token);
437
+ this.refreshToken = AuthorizationCodeFlowSession.parseToken(t.refresh_token);
438
+ this.uri = { token, logout, redirect };
439
+ this.refreshCallback = null;
636
440
  }
637
- clearErrors() {
638
- this.querySelectorAll('[name].is-invalid, .form-floating.is-invalid')
639
- .forEach(el => el.classList.remove('is-invalid'));
640
- this.querySelectorAll("ful-errors")
641
- .forEach(el => {
642
- el.innerHTML = '';
643
- el.classList.add('d-none');
644
- });
441
+ onRefresh(callback) {
442
+ this.refreshCallback = callback;
645
443
  }
646
- static extract(extractors, el) {
647
- const maybeExtractor = extractors[el.dataset['fulBindExtractor']] || extractors[el.dataset['fulBindProvide']];
648
- if (maybeExtractor) {
649
- return maybeExtractor(el);
650
- }
651
- if (el.getAttribute('type') === 'radio') {
652
- if (!el.checked) {
653
- return undefined;
654
- }
655
- return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
656
- }
657
- if (el.getAttribute('type') === 'checkbox') {
658
- return el.checked;
659
- }
660
- if (el.dataset['fulBindType'] === 'boolean') {
661
- return !el.value ? null : el.value === 'true';
444
+ async refresh() {
445
+ const response = await fetch(this.uri.token, {
446
+ method: "POST",
447
+ headers: {
448
+ "Content-Type": 'application/x-www-form-urlencoded'
449
+ },
450
+ body: new URLSearchParams([
451
+ ["client_id", this.clientId],
452
+ ["grant_type", "refresh_token"],
453
+ ["refresh_token", this.token.refresh_token]
454
+ ])
455
+ });
456
+ if (!response.ok) {
457
+ throw new Error("Error:" + response.status + ": " + response.text());
662
458
  }
663
- if (el.getValue) {
664
- return el.getValue();
459
+ const token = await response.json();
460
+ this.token = token;
461
+ this.accessToken = AuthorizationCodeFlowSession.parseToken(token.access_token);
462
+ this.refreshToken = AuthorizationCodeFlowSession.parseToken(token.refresh_token);
463
+ if (this.refreshCallback) {
464
+ this.refreshCallback(this.token, this.accessToken, this.refreshToken);
665
465
  }
666
- return el.value || null;
667
466
  }
668
- static mutate(mutators, el, raw, key, values) {
669
- const maybeMutator = mutators[el.dataset['fulBindMutator']] || mutators[el.dataset['fulBindProvide']];
670
- if (maybeMutator) {
671
- maybeMutator(el, raw, key, values);
672
- return;
673
- }
674
- if (el.getAttribute('type') === 'radio') {
675
- el.checked = el.getAttribute('value') === raw;
676
- return;
677
- }
678
- if (el.getAttribute('type') === 'checkbox') {
679
- el.checked = raw;
680
- return;
681
- }
682
- if (el.setValue) {
683
- el.setValue(raw);
467
+ shouldBeRefreshed(gracePeriod) {
468
+ const now = new Date().getTime();
469
+ const refreshTokenExpiresAt = this.refreshToken.payload.exp * 1000;
470
+ const expired = now > refreshTokenExpiresAt;
471
+ const shouldRefresh = now - gracePeriod > refreshTokenExpiresAt;
472
+ return !expired && shouldRefresh;
473
+ }
474
+ async refreshIf(gracePeriod) {
475
+ if (!this.shouldBeRefreshed(gracePeriod)) {
684
476
  return;
685
477
  }
686
- el.value = raw;
478
+ await this.refresh();
479
+ }
480
+ logout() {
481
+ const url = new URL(this.uri.logout);
482
+ url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
483
+ url.searchParams.set("id_token_hint", this.token.id_token);
484
+ window.location = url;
687
485
  }
688
486
 
689
- static providePath(result, path, value) {
690
- const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
691
- let current = result;
692
- let previous = null;
693
- for (let i = 0; ; ++i) {
694
- const ckey = keys[i];
695
- const pkey = keys[i - 1];
696
- if (Number.isInteger(ckey) && !Array.isArray(current)) {
697
- if (previous !== null) {
698
- previous[pkey] = current = [];
699
- } else {
700
- result = current = [];
487
+ bearerToken() {
488
+ return `Bearer ${this.token.access_token}`;
489
+ }
490
+
491
+ interceptor(gracePeriodBefore, gracePeriodAfter){
492
+ return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
493
+ }
494
+ }
495
+
496
+ class AuthorizationCodeFlowInterceptor {
497
+ constructor(session, gracePeriodBefore, gracePeriodAfter) {
498
+ this.session = session;
499
+ this.gracePeriodBefore = gracePeriodBefore || 2000;
500
+ this.gracePeriodAfter = gracePeriodAfter || 30000;
501
+ }
502
+ async intercept(request, chain) {
503
+ await this.session.refreshIf(this.gracePeriodBefore);
504
+ const headers = new Headers(request.options.headers);
505
+ headers.set("Authorization", this.session.bearerToken());
506
+ request.options.headers = headers;
507
+ const response = await chain.proceed(request);
508
+ await this.session.refreshIf(this.gracePeriodAfter);
509
+ return response;
510
+ }
511
+ }
512
+
513
+ const timing = {
514
+ sleep(ms) {
515
+ return new Promise(resolve => setTimeout(resolve, ms));
516
+ },
517
+ DEBOUNCE_DEFAULT: 0,
518
+ DEBOUNCE_IMMEDIATE: 1,
519
+ debounce(timeoutMs, func, options) {
520
+ let tid = null;
521
+ let args = [];
522
+ let previousTimestamp = 0;
523
+ let opts = options || timing.DEBOUNCE_DEFAULT;
524
+
525
+ const later = () => {
526
+ const elapsed = new Date().getTime() - previousTimestamp;
527
+ if (timeoutMs > elapsed) {
528
+ tid = setTimeout(later, timeoutMs - elapsed);
529
+ return;
530
+ }
531
+ tid = null;
532
+ if (opts !== timing.DEBOUNCE_IMMEDIATE) {
533
+ func(...args);
534
+ }
535
+ // This check is needed because `func` can recursively invoke `debounced`.
536
+ if (tid === null) {
537
+ args = [];
538
+ }
539
+ };
540
+
541
+ return function () {
542
+ args = arguments;
543
+ previousTimestamp = new Date().getTime();
544
+ if (tid === null) {
545
+ tid = setTimeout(later, timeoutMs);
546
+ if (opts === timing.DEBOUNCE_IMMEDIATE) {
547
+ func(...args);
701
548
  }
702
549
  }
703
- if (i === keys.length - 1) {
704
- //when value is undefined we only want to define the property if it's not defined
705
- current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
706
- return result;
550
+ };
551
+ },
552
+ THROTTLE_DEFAULT: 0,
553
+ THROTTLE_NO_LEADING: 1,
554
+ THROTTLE_NO_TRAILING: 2,
555
+ throttle(timeoutMs, func, options) {
556
+ let tid = null;
557
+ let args = [];
558
+ let previousTimestamp = 0;
559
+ let opts = options || timing.THROTTLE_DEFAULT;
560
+
561
+ const later = () => {
562
+ previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
563
+ tid = null;
564
+ func(...args);
565
+ if (tid === null) {
566
+ args = [];
707
567
  }
708
- if (current[ckey] === undefined) {
709
- current[ckey] = {};
568
+ };
569
+
570
+ return function () {
571
+ const now = new Date().getTime();
572
+ if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
573
+ previousTimestamp = now;
710
574
  }
711
- previous = current;
712
- current = current[ckey];
713
- }
714
- }
715
- static custom(tagName, configuration) {
716
- customElements.define(tagName, class extends Form {
717
- constructor() {
718
- super(configuration);
575
+ const remaining = timeoutMs - (now - previousTimestamp);
576
+ args = arguments;
577
+ if (remaining <= 0 || remaining > timeoutMs) {
578
+ if (tid !== null) {
579
+ clearTimeout(tid);
580
+ tid = null;
581
+ }
582
+ previousTimestamp = now;
583
+ func(...args);
584
+ if (tid === null) {
585
+ args = [];
586
+ }
587
+ } else if (tid === null && !(opts & timing.THROTTLE_NO_TRAILING)) {
588
+ tid = setTimeout(later, remaining);
719
589
  }
590
+ };
591
+
592
+ }
593
+ };
594
+
595
+ class Fragments {
596
+ static fromHtml(...html) {
597
+ const el = document.createElement('div');
598
+ el.innerHTML = html.join("");
599
+ const fragment = new DocumentFragment();
600
+ Array.from(el.childNodes).forEach(node => {
601
+ fragment.appendChild(node);
720
602
  });
603
+ return fragment;
604
+ }
605
+ static toHtml(fragment) {
606
+ var r = document.createElement("root");
607
+ r.appendChild(fragment);
608
+ return r.innerHTML;
609
+ }
610
+ static from(...nodes) {
611
+ const fragment = new DocumentFragment();
612
+ for (let i = 0; i !== nodes.length; ++i) {
613
+ fragment.appendChild(nodes[i]);
614
+ }
615
+ return fragment;
721
616
  }
722
- static configure(configuration) {
723
- FieldError.configure();
724
- Errors.configure();
725
- Spinner.configure();
726
- Input.configure();
727
- Select.configure();
728
- Form.custom('ful-form', configuration || {});
617
+ static fromChildNodes(el) {
618
+ const nodes = Array.from(el.childNodes);
619
+ const fragment = new DocumentFragment();
620
+ for (let i = 0; i !== nodes.length; ++i) {
621
+ fragment.appendChild(nodes[i]);
622
+ }
623
+ return fragment;
729
624
  }
730
625
  }
731
626
 
732
- class Storage {
733
- constructor(prefix, storage) {
734
- this.prefix = prefix;
735
- this.storage = storage;
736
- }
737
- save(k, v) {
738
- this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
627
+ class Attributes {
628
+ static id = 0;
629
+ static uid(prefix) {
630
+ return `${prefix}-${++Attributes.id}`;
739
631
  }
740
- load(k) {
741
- const got = this.storage.getItem(`${this.prefix}-${k}`);
742
- return got === undefined ? undefined : JSON.parse(got);
632
+ static asBoolean(value) {
633
+ return value !== null && value !== undefined && value !== false;
743
634
  }
744
- remove(k) {
745
- this.storage.removeItem(`${this.prefix}-${k}`);
635
+ static defaultValue(el, k, v) {
636
+ if (!el.hasAttribute(k)) {
637
+ el.setAttribute(k, v);
638
+ }
639
+ return el.getAttribute(k);
746
640
  }
747
- pop(k) {
748
- const decoded = this.load(k);
749
- this.remove(k);
750
- return decoded;
641
+ static forward(prefix, from, to) {
642
+ from.getAttributeNames()
643
+ .filter(a => a.startsWith(prefix))
644
+ .forEach(a => {
645
+ const target = a.substring(prefix.length);
646
+ if (target === 'class') {
647
+ to.classList.add(...from.getAttribute(prefix + "class").split(" ").filter(a => a.length));
648
+ return;
649
+ }
650
+ to.setAttribute(target, from.getAttribute(a));
651
+ });
751
652
  }
752
653
  }
753
654
 
754
- class LocalStorage extends Storage {
755
- constructor(prefix) {
756
- super(prefix, localStorage);
655
+ class Slots {
656
+ static from(el) {
657
+ const slotted = Object.fromEntries(Array.from(el.querySelectorAll("[slot]")).map(el => {
658
+ el.parentElement.removeChild(el);
659
+ const slot = el.getAttribute("slot");
660
+ el.removeAttribute("slot");
661
+ return [slot, el];
662
+ }));
663
+ slotted.default = new DocumentFragment();
664
+ slotted.default.append(...el.childNodes);
665
+ return slotted;
757
666
  }
758
- }
759
667
 
760
- class SessionStorage extends Storage {
761
- constructor(prefix) {
762
- super(prefix, sessionStorage);
763
- }
764
668
  }
765
669
 
766
- class VersionedStorage {
767
- constructor(storage, key, dataSupplier){
768
- this.storage = storage;
769
- this.key = key;
770
- this.dataSupplier = dataSupplier;
771
- this.cache = null;
772
-
773
- }
774
- async load(revision){
775
- const saved = this.storage.load(this.key);
776
- if (!!saved && saved.revision === revision) {
777
- this.cache = saved.value;
778
- return;
670
+ const Templated = (SuperClass, template) => {
671
+ return class extends SuperClass {
672
+ #rendered;
673
+ async connectedCallback() {
674
+ if (this.#rendered) {
675
+ return;
676
+ }
677
+ const slotted = Slots.from(this);
678
+ const fragment = await Promise.resolve(this.render(slotted, template));
679
+ this.innerHTML = '';
680
+ if (fragment) {
681
+ this.appendChild(fragment);
682
+ }
683
+ this.#rendered = true;
779
684
  }
780
- const freshData = await this.dataSupplier(revision, this.key);
781
- this.storage.save(this.key, {
782
- revision: revision,
783
- value: freshData
784
- });
785
- this.cache = freshData;
685
+ };
686
+ };
687
+
688
+ const Stateful = (SuperClass, flags, others) => {
689
+
690
+ const all = [].concat(flags).concat(others || []);
691
+
692
+ return class extends SuperClass {
693
+ static get observedAttributes() {
694
+ return all;
695
+ }
696
+ constructor(...args) {
697
+ super(...args);
698
+ this.internals_ = this.internals_ || this.attachInternals();
699
+ for (const flag of flags) {
700
+ Object.defineProperty(this, flag, {
701
+ get() {
702
+ return this.hasAttribute(flag);
703
+ },
704
+ set(value) {
705
+ if (Attributes.asBoolean(value)) {
706
+ this.internals_.states.add(`--${flag}`);
707
+ this.setAttribute(flag, '');
708
+ return;
709
+ }
710
+ this.internals_.states.delete(`--${flag}`);
711
+ this.removeAttribute(flag);
712
+ }
713
+ });
714
+ }
715
+ }
716
+ attributeChangedCallback(name, oldValue, newValue) {
717
+ if (oldValue === newValue) {
718
+ return;
719
+ }
720
+ this[name] = newValue;
721
+ const method = this[`on${name.charAt(0).toUpperCase()}${name.substr(1).toLowerCase()}Changed`];
722
+ method?.call(this, newValue, oldValue);
723
+ }
724
+ };
725
+ };
726
+
727
+ class FieldError extends Templated(HTMLElement) {
728
+ render(slotted, template) {
729
+ this.classList.add('invalid-feedback');
786
730
  }
787
- data(){
788
- return this.cache;
731
+ static configure() {
732
+ customElements.define('ful-field-error', FieldError);
789
733
  }
790
734
  }
791
735
 
792
- class AuthorizationCodeFlow {
793
- static forKeycloak(clientId, realmBaseUrl, redirectUri){
794
- const scope = "openid profile";
795
- return new AuthorizationCodeFlow(clientId, scope, {
796
- auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
797
- token: new URL("protocol/openid-connect/token", realmBaseUrl),
798
- logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
799
- registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
800
- redirect: redirectUri
801
- });
802
- }
803
- constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
804
- this.storage = new SessionStorage(clientId);
805
- this.clientId = clientId;
806
- this.scope = scope;
807
- this.uri = {auth, token, registration, logout, redirect};
736
+ class Errors extends Templated(HTMLElement) {
737
+ render(slotted, template) {
738
+ this.classList.add('alert', 'alert-danger', 'd-none');
808
739
  }
809
- async action(uri, additionalParams){
810
- const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
811
- const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
812
- const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
813
- this.storage.save(AuthorizationCodeFlow.PKCE_AND_STATE_KEY, {
814
- state: state,
815
- verifier: pkceVerifier
816
- });
817
- const url = new URL(uri);
818
- url.searchParams.set("client_id", this.clientId);
819
- url.searchParams.set("redirect_uri", this.uri.redirect);
820
- url.searchParams.set("response_type", 'code');
821
- url.searchParams.set("scope", this.scope);
822
- url.searchParams.set("state", state);
823
- url.searchParams.set("code_challenge", pkceChallenge);
824
- url.searchParams.set("code_challenge_method", 'S256');
825
- Object.entries(additionalParams || {}).forEach(kv => {
826
- url.searchParams.set(kv[0], kv[1]);
827
- });
828
- window.location = url;
740
+ static configure() {
741
+ customElements.define('ful-errors', Errors);
829
742
  }
830
- async registration(additionalParams){
831
- await this.action(this.uri.registration, additionalParams);
743
+
744
+ }
745
+
746
+ /* global Infinity, CSS */
747
+
748
+ class Form extends Templated(Observable(HTMLElement)) {
749
+ constructor({ mutators, extractors, valueHoldersSelector, ignoredChildrenSelector }) {
750
+ super();
751
+ this.mutators = mutators || {};
752
+ this.extractors = extractors || {};
753
+ this.valueHoldersSelector = valueHoldersSelector || '[name]';
754
+ this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
832
755
  }
833
- async applicationInitiatedAction(kcAction){
834
- await this.action(this.uri.auth, {
835
- kc_action: kcAction
756
+ render(slotted, template) {
757
+ const form = document.createElement('form');
758
+ form.append(slotted.default);
759
+ form.addEventListener('submit', async (e) => {
760
+ e.preventDefault();
761
+ this.spinner(true);
762
+ try {
763
+ await this.fire('submit', this.getValues(), this);
764
+ } catch (e) {
765
+ if (e instanceof Failure) {
766
+ this.setErrors(e.problems);
767
+ return;
768
+ }
769
+ throw e;
770
+ } finally {
771
+ this.spinner(false);
772
+ }
836
773
  });
774
+ return form;
837
775
  }
838
- async _tokenExchange(code, state) {
839
- window.history.replaceState('', "", this.uri.redirect);
840
- const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
841
- if (stateAndVerifier.state !== state) {
842
- throw new Error("State mismatch");
843
- }
844
- const response = await fetch(this.uri.token, {
845
- method: "POST",
846
- headers: {
847
- "Content-Type": 'application/x-www-form-urlencoded'
848
- },
849
- body: new URLSearchParams([
850
- ["client_id", this.clientId],
851
- ["code", code],
852
- ["grant_type", "authorization_code"],
853
- ["code_verifier", stateAndVerifier.verifier],
854
- ["state", stateAndVerifier.state],
855
- ["redirect_uri", this.uri.redirect]
856
- ])
776
+ spinner(spin) {
777
+ this.querySelectorAll('ful-spinner').forEach(el => {
778
+ el.hidden = !spin;
779
+ });
780
+ this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
781
+ el.disabled = spin;
857
782
  });
858
- if (!response.ok) {
859
- const text = await response.text();
860
- throw new Error("Error:" + response.status + ": " + text);
861
- }
862
- const token = await response.json();
863
- return new AuthorizationCodeFlowSession(this.clientId, token, this.uri);
864
783
  }
865
- async ensureLoggedIn() {
866
- const url = new URL(window.location.href);
867
- const code = url.searchParams.get("code");
868
- if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
869
- //if callback from keycloak and we have our state still stored
870
- const state = url.searchParams.get("state");
871
- return await this._tokenExchange(code, state);
784
+ setValues(values) {
785
+ for (let k in values) {
786
+ if (!values.hasOwnProperty(k)) {
787
+ continue;
788
+ }
789
+ Array.from(this.querySelectorAll(`[name='${CSS.escape(k)}']`)).forEach((el) => {
790
+ Form.mutate(this.mutators, el, values[k], k, values);
791
+ });
872
792
  }
873
- //if not authorized
874
- await this.action(this.uri.auth, {});
875
- return null;
876
793
  }
877
- }
878
- AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
794
+ getValues() {
795
+ return Array.from(this.querySelectorAll(this.valueHoldersSelector))
796
+ .filter((el) => {
797
+ if (el.dataset['fulBindInclude'] === 'never') {
798
+ return false;
799
+ }
800
+ return el.dataset['fulBindInclude'] === 'always' || el.closest(this.ignoredChildrenSelector) === null;
801
+ })
802
+ .reduce((result, el) => {
803
+ return Form.providePath(result, el.getAttribute('name'), Form.extract(this.extractors, el));
804
+ }, {});
805
+ }
806
+ setErrors(errors, scroll) {
807
+ this.clearErrors();
808
+ errors
809
+ .filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
810
+ .forEach((e) => {
811
+ const name = e.context.replace("[", ".").replace("].", ".");
812
+ this.querySelectorAll(`[name='${CSS.escape(name)}']`)
813
+ .forEach(input => {
814
+ input.classList.add('is-invalid');
815
+ if (input.parentElement.classList.contains("form-floating")) {
816
+ input.parentElement.classList.add('is-invalid');
817
+ }
818
+ });
819
+ this.querySelectorAll(`ful-field-error[field='${CSS.escape(name)}']`)
820
+ .forEach(el => el.innerText = e.reason);
821
+ });
822
+ this.querySelectorAll("ful-errors")
823
+ .forEach(el => {
824
+ const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
825
+ el.innerHTML = globalErrors.map(e => e.reason).join("\n");
826
+ if (globalErrors.length !== 0) {
827
+ el.classList.remove('d-none');
828
+ }
829
+ });
879
830
 
880
- class AuthorizationCodeFlowSession {
881
- static parseToken(token) {
882
- const [rawHeader, rawPayload, signature] = token.split(".");
883
- const ut8decoder = new TextDecoder("utf-8");
884
- return {
885
- header: JSON.parse(ut8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
886
- payload: JSON.parse(ut8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
887
- signature: signature
888
- };
889
- }
890
- constructor(clientId, t, {token, logout, redirect}) {
891
- this.clientId = clientId;
892
- this.token = t;
893
- this.accessToken = AuthorizationCodeFlowSession.parseToken(t.access_token);
894
- this.refreshToken = AuthorizationCodeFlowSession.parseToken(t.refresh_token);
895
- this.uri = { token, logout, redirect };
896
- this.refreshCallback = null;
831
+ if (!scroll) {
832
+ return;
833
+ }
834
+ const ys = Array.from(this.querySelectorAll('ful-field-error:not(.d-none)'))
835
+ .map(el => el.getBoundingClientRect().y + window.scrollY);
836
+ const miny = Math.min(...ys);
837
+ if (miny !== Infinity) {
838
+ window.scroll(window.scrollX, miny > 100 ? miny - 100 : 0);
839
+ }
897
840
  }
898
- onRefresh(callback) {
899
- this.refreshCallback = callback;
841
+ clearErrors() {
842
+ this.querySelectorAll('[name].is-invalid, .form-floating.is-invalid')
843
+ .forEach(el => el.classList.remove('is-invalid'));
844
+ this.querySelectorAll("ful-errors")
845
+ .forEach(el => {
846
+ el.innerHTML = '';
847
+ el.classList.add('d-none');
848
+ });
900
849
  }
901
- async refresh() {
902
- const response = await fetch(this.uri.token, {
903
- method: "POST",
904
- headers: {
905
- "Content-Type": 'application/x-www-form-urlencoded'
906
- },
907
- body: new URLSearchParams([
908
- ["client_id", this.clientId],
909
- ["grant_type", "refresh_token"],
910
- ["refresh_token", this.token.refresh_token]
911
- ])
912
- });
913
- if (!response.ok) {
914
- throw new Error("Error:" + response.status + ": " + response.text());
850
+ static extract(extractors, el) {
851
+ const maybeExtractor = extractors[el.dataset['fulBindExtractor']] || extractors[el.dataset['fulBindProvide']];
852
+ if (maybeExtractor) {
853
+ return maybeExtractor(el);
915
854
  }
916
- const token = await response.json();
917
- this.token = token;
918
- this.accessToken = this._parseToken(token.access_token);
919
- this.refreshToken = this._parseToken(token.refresh_token);
920
- if (this.refreshCallback) {
921
- this.refreshCallback(this.token, this.accessToken, this.refreshToken);
855
+ if (el.getAttribute('type') === 'radio') {
856
+ if (!el.checked) {
857
+ return undefined;
858
+ }
859
+ return el.dataset['fulBindType'] === 'boolean' ? el.value === 'true' : el.value;
922
860
  }
861
+ if (el.getAttribute('type') === 'checkbox') {
862
+ return el.checked;
863
+ }
864
+ if (el.dataset['fulBindType'] === 'boolean') {
865
+ return !el.value ? null : el.value === 'true';
866
+ }
867
+ if (el.getValue) {
868
+ return el.getValue();
869
+ }
870
+ return el.value || null;
923
871
  }
924
- shouldBeRefreshed(gracePeriod) {
925
- const now = new Date().getTime();
926
- const refreshTokenExpiresAt = this.refreshToken.payload.exp * 1000;
927
- const expired = now > refreshTokenExpiresAt;
928
- const shouldRefresh = now - gracePeriod > refreshTokenExpiresAt;
929
- return !expired && shouldRefresh;
930
- }
931
- async refreshIf(gracePeriod) {
932
- if (!this.shouldBeRefreshed(gracePeriod)) {
872
+ static mutate(mutators, el, raw, key, values) {
873
+ const maybeMutator = mutators[el.dataset['fulBindMutator']] || mutators[el.dataset['fulBindProvide']];
874
+ if (maybeMutator) {
875
+ maybeMutator(el, raw, key, values);
933
876
  return;
934
877
  }
935
- await this.refresh();
936
- }
937
- logout() {
938
- const url = new URL(this.uri.logout);
939
- url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
940
- url.searchParams.set("id_token_hint", this.token.id_token);
941
- window.location = url;
878
+ if (el.getAttribute('type') === 'radio') {
879
+ el.checked = el.getAttribute('value') === raw;
880
+ return;
881
+ }
882
+ if (el.getAttribute('type') === 'checkbox') {
883
+ el.checked = raw;
884
+ return;
885
+ }
886
+ if (el.setValue) {
887
+ el.setValue(raw);
888
+ return;
889
+ }
890
+ el.value = raw;
942
891
  }
943
892
 
944
- bearerToken() {
945
- return `Bearer ${this.token.access_token}`;
893
+ static providePath(result, path, value) {
894
+ const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
895
+ let current = result;
896
+ let previous = null;
897
+ for (let i = 0; ; ++i) {
898
+ const ckey = keys[i];
899
+ const pkey = keys[i - 1];
900
+ if (Number.isInteger(ckey) && !Array.isArray(current)) {
901
+ if (previous !== null) {
902
+ previous[pkey] = current = [];
903
+ } else {
904
+ result = current = [];
905
+ }
906
+ }
907
+ if (i === keys.length - 1) {
908
+ //when value is undefined we only want to define the property if it's not defined
909
+ current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
910
+ return result;
911
+ }
912
+ if (current[ckey] === undefined) {
913
+ current[ckey] = {};
914
+ }
915
+ previous = current;
916
+ current = current[ckey];
917
+ }
946
918
  }
947
-
948
- interceptor(gracePeriodBefore, gracePeriodAfter){
949
- return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
919
+ static custom(tagName, configuration) {
920
+ customElements.define(tagName, class extends Form {
921
+ constructor() {
922
+ super(configuration);
923
+ }
924
+ });
950
925
  }
951
926
  }
952
927
 
953
- class AuthorizationCodeFlowInterceptor {
954
- constructor(session, gracePeriodBefore, gracePeriodAfter) {
955
- this.session = session;
956
- this.gracePeriodBefore = gracePeriodBefore || 2000;
957
- this.gracePeriodAfter = gracePeriodAfter || 30000;
928
+ const ful_input_ec = globalThis.ec || ftl.EvaluationContext.configure({});
929
+
930
+ const ful_input_template = globalThis.template || ftl.Template.fromHtml(`
931
+ <div data-tpl-if="floating" class="input-group has-validation">
932
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
933
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
934
+ <div class="form-floating">
935
+ {{{{ slotted.input }}}}
936
+ <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
937
+ </div>
938
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
939
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
940
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
941
+ </div>
942
+ <div data-tpl-if="!floating" data-tpl-remove="tag">
943
+ <label data-tpl-for="id" class="form-label">{{{{ slotted.default }}}}</label>
944
+ <div class="input-group has-validation">
945
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
946
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
947
+ {{{{ slotted.input }}}}
948
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
949
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
950
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
951
+ </div>
952
+ </div>
953
+ `, ful_input_ec);
954
+
955
+
956
+
957
+ class Input extends Templated(HTMLElement, ful_input_template) {
958
+ render(slotted, template) {
959
+ const floating = this.hasAttribute('floating');
960
+ const input = slotted.input = slotted.input || (() => {
961
+ const el = document.createElement("input");
962
+ el.classList.add("form-control");
963
+ return el;
964
+ })();
965
+ const id = input.getAttribute('id') || this.getAttribute('input-id') || Attributes.uid('ful-input');
966
+ Attributes.forward('input-', this, slotted.input);
967
+ Attributes.defaultValue(slotted.input, "id", id);
968
+ Attributes.defaultValue(slotted.input, "type", "text");
969
+ Attributes.defaultValue(slotted.input, "placeholder", " ");
970
+ const name = input.getAttribute('name');
971
+ return template.render({ id, name, floating, slotted });
958
972
  }
959
- async intercept(request, chain) {
960
- await this.session.refreshIf(this.gracePeriodBefore);
961
- const headers = new Headers(request.options.headers);
962
- headers.set("Authorization", this.session.bearerToken());
963
- request.options.headers = headers;
964
- const response = await chain.proceed(request);
965
- await this.session.refreshIf(this.gracePeriodAfter);
966
- return response;
973
+ static configure() {
974
+ customElements.define('ful-input', Input);
967
975
  }
968
976
  }
969
977
 
970
- const timing = {
971
- sleep(ms) {
972
- return new Promise(resolve => setTimeout(resolve, ms));
973
- },
974
- DEBOUNCE_DEFAULT: 0,
975
- DEBOUNCE_IMMEDIATE: 1,
976
- debounce(timeoutMs, func, options) {
977
- let tid = null;
978
- let args = [];
979
- let previousTimestamp = 0;
980
- let opts = options || timing.DEBOUNCE_DEFAULT;
981
-
982
- const later = () => {
983
- const elapsed = new Date().getTime() - previousTimestamp;
984
- if (timeoutMs > elapsed) {
985
- tid = setTimeout(later, timeoutMs - elapsed);
986
- return;
987
- }
988
- tid = null;
989
- if (opts !== timing.DEBOUNCE_IMMEDIATE) {
990
- func(...args);
991
- }
992
- // This check is needed because `func` can recursively invoke `debounced`.
993
- if (tid === null) {
994
- args = [];
995
- }
996
- };
978
+ /**
979
+ * <script src="tom-select.complete.js"></script>
980
+ * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
981
+ */
982
+ const ful_select_ec = globalThis.ec || ftl.EvaluationContext.configure({});
983
+
984
+ const ful_select_template = globalThis.template || ftl.Template.fromHtml(`
985
+ <div data-tpl-if="floating" class="input-group has-validation">
986
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
987
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
988
+ <div class="form-floating">
989
+ {{{{ slotted.input }}}}
990
+ <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
991
+ </div>
992
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
993
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
994
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
995
+ </div>
996
+ <div data-tpl-if="!floating" data-tpl-remove="tag">
997
+ <label data-tpl-for="id" class="form-label">{{{{ slotted.default }}}}</label>
998
+ <div class="input-group has-validation">
999
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
1000
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
1001
+ {{{{ slotted.input }}}}
1002
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
1003
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
1004
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1005
+ </div>
1006
+ </div>
1007
+ `, ful_select_ec);
1008
+
1009
+
1010
+ class Select extends Templated(Observable(HTMLElement), ful_select_template) {
1011
+ constructor(tsConfig) {
1012
+ super();
1013
+ this.tsConfig = tsConfig;
1014
+ }
1015
+ render(slotted, template) {
1016
+ const floating = this.hasAttribute('floating');
1017
+ const remote = this.hasAttribute('remote');
1018
+ const input = slotted.input = slotted.input || (() => {
1019
+ return document.createElement("select");
1020
+ })();
1021
+ const id = input.getAttribute('id') || this.getAttribute('input-id') || Attributes.uid('ful-select');
1022
+ Attributes.forward('input-', this, input);
1023
+ Attributes.defaultValue(input, "id", id);
1024
+ Attributes.defaultValue(input, "placeholder", " ");
1025
+ const name = input.getAttribute('name');
1026
+ input.setValue = this.setValue.bind(this);
1027
+ input.getValue = this.getValue.bind(this);
1028
+
1029
+ //tomselect needs the input to have a parent.
1030
+ //se we move the input to a fragment
1031
+ slotted.input = Fragments.from(input);
997
1032
 
998
- return function () {
999
- args = arguments;
1000
- previousTimestamp = new Date().getTime();
1001
- if (tid === null) {
1002
- tid = setTimeout(later, timeoutMs);
1003
- if (opts === timing.DEBOUNCE_IMMEDIATE) {
1004
- func(...args);
1033
+ this.loaded = !remote;
1034
+ this.ts = new TomSelect(input, Object.assign(remote ? {
1035
+ preload: 'focus',
1036
+ load: async (query, callback) => {
1037
+ if (this.loaded) {
1038
+ callback();
1039
+ return;
1005
1040
  }
1041
+ const data = await this.fire('load', query, []);
1042
+ this.loaded = true;
1043
+ callback(data);
1006
1044
  }
1007
- };
1008
- },
1009
- THROTTLE_DEFAULT: 0,
1010
- THROTTLE_NO_LEADING: 1,
1011
- THROTTLE_NO_TRAILING: 2,
1012
- throttle(timeoutMs, func, options) {
1013
- let tid = null;
1014
- let args = [];
1015
- let previousTimestamp = 0;
1016
- let opts = options || timing.THROTTLE_DEFAULT;
1045
+ } : {}, this.tsConfig));
1017
1046
 
1018
- const later = () => {
1019
- previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
1020
- tid = null;
1021
- func(...args);
1022
- if (tid === null) {
1023
- args = [];
1047
+ return template.render({ id, name, floating, slotted });
1048
+ }
1049
+ async setValue(v) {
1050
+ if (!this.loaded) {
1051
+ await this.ts.load();
1052
+ }
1053
+ this.ts.setValue(v);
1054
+ }
1055
+ getValue() {
1056
+ const v = this.ts.getValue();
1057
+ return v === '' ? null : v;
1058
+ }
1059
+ static custom(tagName, configuration) {
1060
+ customElements.define(tagName, class extends Select {
1061
+ constructor() {
1062
+ super(configuration);
1024
1063
  }
1025
- };
1064
+ });
1065
+ }
1066
+ static configure() {
1067
+ return Select.custom('ful-select');
1068
+ }
1026
1069
 
1027
- return function () {
1028
- const now = new Date().getTime();
1029
- if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
1030
- previousTimestamp = now;
1031
- }
1032
- const remaining = timeoutMs - (now - previousTimestamp);
1033
- args = arguments;
1034
- if (remaining <= 0 || remaining > timeoutMs) {
1035
- if (tid !== null) {
1036
- clearTimeout(tid);
1037
- tid = null;
1038
- }
1039
- previousTimestamp = now;
1040
- func(...args);
1041
- if (tid === null) {
1042
- args = [];
1043
- }
1044
- } else if (tid === null && !(opts & timing.THROTTLE_NO_TRAILING)) {
1045
- tid = setTimeout(later, remaining);
1046
- }
1047
- };
1070
+ }
1048
1071
 
1072
+ class Spinner extends Templated(HTMLElement) {
1073
+ render(slotted, template) {
1074
+ return Fragments.fromHtml(`
1075
+ <div class="spinner-border spinner-border-sm" aria-hidden="true"></div>
1076
+ `);
1049
1077
  }
1050
- };
1078
+ static configure() {
1079
+ customElements.define('ful-spinner', Spinner);
1080
+ }
1081
+ }
1051
1082
 
1052
1083
  class Wizard extends HTMLElement {
1053
1084
  constructor() {
@@ -1155,15 +1186,16 @@ var ful = (function (exports) {
1155
1186
  }
1156
1187
 
1157
1188
  exports.App = App;
1189
+ exports.Attributes = Attributes;
1158
1190
  exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
1159
1191
  exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
1160
1192
  exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
1161
1193
  exports.Base64 = Base64;
1162
- exports.CustomElements = CustomElements;
1163
1194
  exports.Errors = Errors;
1164
1195
  exports.Failure = Failure;
1165
1196
  exports.FieldError = FieldError;
1166
1197
  exports.Form = Form;
1198
+ exports.Fragments = Fragments;
1167
1199
  exports.Hex = Hex;
1168
1200
  exports.HttpClient = HttpClient;
1169
1201
  exports.Input = Input;
@@ -1171,7 +1203,10 @@ var ful = (function (exports) {
1171
1203
  exports.Observable = Observable;
1172
1204
  exports.Select = Select;
1173
1205
  exports.SessionStorage = SessionStorage;
1206
+ exports.Slots = Slots;
1174
1207
  exports.Spinner = Spinner;
1208
+ exports.Stateful = Stateful;
1209
+ exports.Templated = Templated;
1175
1210
  exports.VersionedStorage = VersionedStorage;
1176
1211
  exports.Wizard = Wizard;
1177
1212
  exports.jsonPatch = jsonPatch;