@optionfactory/ful 0.25.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,18 +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 init(self){
106
- self.listeners = {};
107
- }
108
- static mixin(ctor) {
109
- ctor.prototype.fireSync = Observable.prototype.fireSync;
110
- ctor.prototype.fire = Observable.prototype.fire;
111
- ctor.prototype.on = Observable.prototype.on;
112
- ctor.prototype.un = Observable.prototype.un;
113
- }
114
-
115
- }
105
+ }
106
+ };
116
107
 
117
108
  class ContextInterceptor {
118
109
  constructor() {
@@ -281,285 +272,490 @@ var ful = (function (exports) {
281
272
  return jsonRequest('PATCH', body, headers);
282
273
  }
283
274
 
284
- /* global Infinity, CSS */
285
-
286
- class CustomElements {
287
- static id = 0;
288
- static uid(prefix) {
289
- return `${prefix}-${++CustomElements.id}`;
275
+ class Storage {
276
+ constructor(prefix, storage) {
277
+ this.prefix = prefix;
278
+ this.storage = storage;
290
279
  }
291
- static forwardAttributes(from, to, except) {
292
- from.getAttributeNames()
293
- .filter(a => except.indexOf(a) === -1)
294
- .filter(a => a[0] === '@')
295
- .forEach(a => {
296
- if (a === '@class') {
297
- to.classList.add(...from.getAttribute("@class").split(" ").filter(a => a.length));
298
- return;
299
- }
300
- to.setAttribute(a.substring(1), from.getAttribute(a));
301
- });
280
+ save(k, v) {
281
+ this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
302
282
  }
303
- static extractSlots(el) {
304
- const slotted = Object.fromEntries([...el.querySelectorAll("[slot]")].map(el => {
305
- el.parentElement.removeChild(el);
306
- const slot = el.getAttribute("slot");
307
- el.removeAttribute("slot");
308
- return [slot, el];
309
- }));
310
- slotted.default = new DocumentFragment();
311
- slotted.default.append(...el.childNodes);
312
- return slotted;
283
+ load(k) {
284
+ const got = this.storage.getItem(`${this.prefix}-${k}`);
285
+ return got === undefined ? undefined : JSON.parse(got);
313
286
  }
314
- static labelAndInputGroup(id, name, isFloating, slotted) {
315
- if (isFloating) {
316
- /**
317
- * <div class="input-group has-validation">
318
- * <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
319
- * <div class="form-floating">
320
- * {{{{ slotted.input }}}}
321
- * <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
322
- * </div>
323
- * <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
324
- * <ful-field-error data-tpl-field="name"></ful-field-error>
325
- * </div>
326
- */
327
- const label = document.createElement("label");
328
- label.setAttribute("for", id);
329
- label.classList.add('form-label');
330
- label.append(slotted.default);
331
-
332
- const ff = document.createElement('div');
333
- ff.classList.add("form-floating");
334
- ff.append(slotted.input, label);
335
-
336
- const ffe = document.createElement('ful-field-error');
337
- ffe.setAttribute("field", name);
338
-
339
- const ig = document.createElement("div");
340
- ig.classList.add('input-group', 'has-validtion');
341
-
342
- if (slotted.before) {
343
- ig.append(slotted.before);
344
- } else if (slotted.ibefore) {
345
- const igt = document.createElement('div');
346
- igt.classList.add('input-group-text');
347
- igt.append(slotted.ibefore);
348
- ig.append(igt);
349
- }
350
- ig.append(ff);
351
- if (slotted.after) {
352
- ig.append(slotted.after);
353
- } else if (slotted.iafter) {
354
- const igt = document.createElement('div');
355
- igt.classList.add('input-group-text');
356
- igt.append(slotted.iafter);
357
- ig.append(igt);
358
- }
359
- ig.append(ffe);
360
- return ig;
361
- }
362
- /**
363
- <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
364
- <div class="input-group has-validation">
365
- <span data-tpl-if="slotted.before" class="input-group-text">{{{{ slotted.before }}}}</span>
366
- {{{{ slotted.input }}}}
367
- <span data-tpl-if="slotted.after" class="input-group-text">{{{{ slotted.after }}}}</span>
368
- <ful-field-error data-tpl-field="name"></ful-field-error>
369
- </div>
370
- */
371
-
372
- const label = document.createElement("label");
373
- label.setAttribute("for", name);
374
- label.classList.add('form-label');
375
- label.append(slotted.default);
376
-
377
- const ffe = document.createElement('ful-field-error');
378
- ffe.setAttribute("field", name);
379
-
380
- const ig = document.createElement("div");
381
- ig.classList.add('input-group', 'has-validation');
382
-
383
- if (slotted.before) {
384
- ig.append(slotted.before);
385
- } else if (slotted.ibefore) {
386
- const igt = document.createElement('div');
387
- igt.classList.add('input-group-text');
388
- igt.append(slotted.ibefore);
389
- ig.append(igt);
390
- }
391
- ig.append(slotted.input);
392
- if (slotted.after) {
393
- ig.append(slotted.after);
394
- } else if (slotted.iafter) {
395
- const igt = document.createElement('div');
396
- igt.classList.add('input-group-text');
397
- igt.append(slotted.iafter);
398
- ig.append(igt);
399
- }
400
- ig.append(ffe);
401
-
402
- const fragment = new DocumentFragment();
403
- fragment.append(label, ig);
404
- return fragment;
287
+ remove(k) {
288
+ this.storage.removeItem(`${this.prefix}-${k}`);
289
+ }
290
+ pop(k) {
291
+ const decoded = this.load(k);
292
+ this.remove(k);
293
+ return decoded;
405
294
  }
406
-
407
295
  }
408
296
 
409
-
410
- class FieldError extends HTMLElement {
411
- constructor() {
412
- super();
413
- }
414
- connectedCallback() {
415
- this.classList.add('invalid-feedback');
297
+ class LocalStorage extends Storage {
298
+ constructor(prefix) {
299
+ super(prefix, localStorage);
416
300
  }
417
- static configure() {
418
- customElements.define('ful-field-error', FieldError);
301
+ }
302
+
303
+ class SessionStorage extends Storage {
304
+ constructor(prefix) {
305
+ super(prefix, sessionStorage);
419
306
  }
420
307
  }
421
308
 
422
- class Errors extends HTMLElement {
423
- constructor() {
424
- super();
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
+
425
316
  }
426
- connectedCallback() {
427
- this.classList.add('alert', 'alert-danger', 'd-none');
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;
428
329
  }
429
- static configure() {
430
- customElements.define('ful-errors', Errors);
330
+ data(){
331
+ return this.cache;
431
332
  }
432
-
433
333
  }
434
334
 
435
- class Spinner extends HTMLElement {
436
- constructor() {
437
- super();
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
+ });
438
345
  }
439
- connectedCallback() {
440
- this.classList.add('spinner-border', 'spinner-border-sm', 'd-none');
441
- this.setAttribute("aria-hidden", "true");
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};
442
351
  }
443
- show() {
444
- this.classList.remove("d-none");
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]);
370
+ });
371
+ window.location = url;
445
372
  }
446
- hide() {
447
- this.classList.add("d-none");
373
+ async registration(additionalParams){
374
+ await this.action(this.uri.registration, additionalParams);
448
375
  }
449
- static configure() {
450
- customElements.define('ful-spinner', Spinner);
376
+ async applicationInitiatedAction(kcAction){
377
+ await this.action(this.uri.auth, {
378
+ kc_action: kcAction
379
+ });
451
380
  }
452
- }
453
-
454
-
455
-
456
- class Input extends HTMLElement {
457
- constructor() {
458
- super();
459
- const id = CustomElements.uid('ful-input');
460
- const name = this.getAttribute('@name');
461
- const floating = this.hasAttribute('@floating');
462
- const slotted = CustomElements.extractSlots(this);
463
- slotted.input = slotted.input || (() => {
464
- const el = document.createElement("input");
465
- el.classList.add("form-control");
466
- return el;
467
- })();
468
- CustomElements.forwardAttributes(this, slotted.input, ['@floating']);
469
- const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
470
- attrIfMissing(slotted.input, "name", id);
471
- attrIfMissing(slotted.input, "id", id);
472
- attrIfMissing(slotted.input, "type", "text");
473
- attrIfMissing(slotted.input, "placeholder", " ");
474
- this.innerHTML = '';
475
- this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
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
+ ])
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);
476
407
  }
477
- static configure() {
478
- customElements.define('ful-input', Input);
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);
415
+ }
416
+ //if not authorized
417
+ await this.action(this.uri.auth, {});
418
+ return null;
479
419
  }
480
420
  }
421
+ AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
481
422
 
482
-
483
-
484
- /**
485
- * <script src="tom-select.complete.js"></script>
486
- * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
487
- */
488
- class Select extends HTMLElement {
489
- constructor(tsConfig) {
490
- super();
491
- Observable.init(this);
492
- const id = CustomElements.uid('ful-select');
493
- const name = this.getAttribute('@name');
494
- const floating = this.hasAttribute('@floating');
495
- const remote = this.hasAttribute('@remote');
496
- const slotted = CustomElements.extractSlots(this);
497
- slotted.input = slotted.input || (() => {
498
- return document.createElement("select");
499
- })();
500
- CustomElements.forwardAttributes(this, slotted.input, ['@floating', '@remote']);
501
- const attrIfMissing = (el, k, v) => !el.hasAttribute(k) && el.setAttribute(k, v);
502
- attrIfMissing(slotted.input, "name", id);
503
- attrIfMissing(slotted.input, "id", id);
504
- attrIfMissing(slotted.input, "placeholder", " ");
505
- this.innerHTML = '';
506
- this.append(CustomElements.labelAndInputGroup(id, name || id, floating, slotted));
507
- this.loaded = !remote;
508
- this.ts = new TomSelect(slotted.input, Object.assign(remote ? {
509
- preload: 'focus',
510
- load: async (query, callback) => {
511
- if (this.loaded) {
512
- callback();
513
- return;
514
- }
515
- const data = await this.fire('load', query, []);
516
- this.loaded = true;
517
- callback(data);
518
- }
519
- } : {}, tsConfig));
520
- slotted.input.setValue = this.setValue.bind(this);
521
- slotted.input.getValue = this.getValue.bind(this);
522
- }
523
- async setValue(v){
524
- if(!this.loaded){
525
- await this.ts.load();
526
- }
527
- this.ts.setValue(v);
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;
528
440
  }
529
- getValue(){
530
- const v = this.ts.getValue();
531
- return v === '' ? null : v;
441
+ onRefresh(callback) {
442
+ this.refreshCallback = callback;
532
443
  }
533
- static custom(tagName, configuration) {
534
- customElements.define(tagName, class extends Select {
535
- constructor() {
536
- super(configuration);
537
- }
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
+ ])
538
455
  });
456
+ if (!response.ok) {
457
+ throw new Error("Error:" + response.status + ": " + response.text());
458
+ }
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);
465
+ }
539
466
  }
540
- static configure() {
541
- return Select.custom('ful-select');
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)) {
476
+ return;
477
+ }
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;
485
+ }
486
+
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);
548
+ }
549
+ }
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 = [];
567
+ }
568
+ };
569
+
570
+ return function () {
571
+ const now = new Date().getTime();
572
+ if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
573
+ previousTimestamp = now;
574
+ }
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);
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);
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;
616
+ }
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;
624
+ }
625
+ }
626
+
627
+ class Attributes {
628
+ static id = 0;
629
+ static uid(prefix) {
630
+ return `${prefix}-${++Attributes.id}`;
631
+ }
632
+ static asBoolean(value) {
633
+ return value !== null && value !== undefined && value !== false;
634
+ }
635
+ static defaultValue(el, k, v) {
636
+ if (!el.hasAttribute(k)) {
637
+ el.setAttribute(k, v);
638
+ }
639
+ return el.getAttribute(k);
640
+ }
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
+ });
652
+ }
653
+ }
654
+
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;
542
666
  }
543
667
 
544
668
  }
545
669
 
546
- Observable.mixin(Select);
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;
684
+ }
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');
730
+ }
731
+ static configure() {
732
+ customElements.define('ful-field-error', FieldError);
733
+ }
734
+ }
735
+
736
+ class Errors extends Templated(HTMLElement) {
737
+ render(slotted, template) {
738
+ this.classList.add('alert', 'alert-danger', 'd-none');
739
+ }
740
+ static configure() {
741
+ customElements.define('ful-errors', Errors);
742
+ }
547
743
 
744
+ }
548
745
 
746
+ /* global Infinity, CSS */
549
747
 
550
- class Form extends HTMLElement {
748
+ class Form extends Templated(Observable(HTMLElement)) {
551
749
  constructor({ mutators, extractors, valueHoldersSelector, ignoredChildrenSelector }) {
552
750
  super();
553
- Observable.init(this);
554
751
  this.mutators = mutators || {};
555
752
  this.extractors = extractors || {};
556
753
  this.valueHoldersSelector = valueHoldersSelector || '[name]';
557
754
  this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
558
-
755
+ }
756
+ render(slotted, template) {
559
757
  const form = document.createElement('form');
560
- form.append(...this.childNodes);
561
- this.appendChild(form);
562
-
758
+ form.append(slotted.default);
563
759
  form.addEventListener('submit', async (e) => {
564
760
  e.preventDefault();
565
761
  this.spinner(true);
@@ -575,10 +771,11 @@ var ful = (function (exports) {
575
771
  this.spinner(false);
576
772
  }
577
773
  });
774
+ return form;
578
775
  }
579
776
  spinner(spin) {
580
777
  this.querySelectorAll('ful-spinner').forEach(el => {
581
- el[spin ? 'show' : 'hide']();
778
+ el.hidden = !spin;
582
779
  });
583
780
  this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
584
781
  el.disabled = spin;
@@ -726,338 +923,163 @@ var ful = (function (exports) {
726
923
  }
727
924
  });
728
925
  }
729
- static configure(configuration) {
730
- FieldError.configure();
731
- Errors.configure();
732
- Spinner.configure();
733
- Input.configure();
734
- Select.configure();
735
- Form.custom('ful-form', configuration || {});
736
- }
737
926
  }
738
927
 
739
- Observable.mixin(Form);
740
-
741
- class Storage {
742
- constructor(prefix, storage) {
743
- this.prefix = prefix;
744
- this.storage = storage;
745
- }
746
- save(k, v) {
747
- this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
748
- }
749
- load(k) {
750
- const got = this.storage.getItem(`${this.prefix}-${k}`);
751
- return got === undefined ? undefined : JSON.parse(got);
752
- }
753
- remove(k) {
754
- this.storage.removeItem(`${this.prefix}-${k}`);
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 });
755
972
  }
756
- pop(k) {
757
- const decoded = this.load(k);
758
- this.remove(k);
759
- return decoded;
973
+ static configure() {
974
+ customElements.define('ful-input', Input);
760
975
  }
761
976
  }
762
977
 
763
- class LocalStorage extends Storage {
764
- constructor(prefix) {
765
- super(prefix, localStorage);
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;
766
1014
  }
767
- }
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);
768
1032
 
769
- class SessionStorage extends Storage {
770
- constructor(prefix) {
771
- super(prefix, sessionStorage);
772
- }
773
- }
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;
1040
+ }
1041
+ const data = await this.fire('load', query, []);
1042
+ this.loaded = true;
1043
+ callback(data);
1044
+ }
1045
+ } : {}, this.tsConfig));
774
1046
 
775
- class VersionedStorage {
776
- constructor(storage, key, dataSupplier){
777
- this.storage = storage;
778
- this.key = key;
779
- this.dataSupplier = dataSupplier;
780
- this.cache = null;
781
-
782
- }
783
- async load(revision){
784
- const saved = this.storage.load(this.key);
785
- if (!!saved && saved.revision === revision) {
786
- this.cache = saved.value;
787
- return;
788
- }
789
- const freshData = await this.dataSupplier(revision, this.key);
790
- this.storage.save(this.key, {
791
- revision: revision,
792
- value: freshData
793
- });
794
- this.cache = freshData;
795
- }
796
- data(){
797
- return this.cache;
798
- }
799
- }
800
-
801
- class AuthorizationCodeFlow {
802
- static forKeycloak(clientId, realmBaseUrl, redirectUri){
803
- const scope = "openid profile";
804
- return new AuthorizationCodeFlow(clientId, scope, {
805
- auth: new URL("protocol/openid-connect/auth", realmBaseUrl),
806
- token: new URL("protocol/openid-connect/token", realmBaseUrl),
807
- logout: new URL("protocol/openid-connect/logout", realmBaseUrl),
808
- registration: new URL("protocol/openid-connect/registrations", realmBaseUrl),
809
- redirect: redirectUri
810
- });
811
- }
812
- constructor(clientId, scope, {auth, token, registration, logout, redirect}) {
813
- this.storage = new SessionStorage(clientId);
814
- this.clientId = clientId;
815
- this.scope = scope;
816
- this.uri = {auth, token, registration, logout, redirect};
817
- }
818
- async action(uri, additionalParams){
819
- const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
820
- const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
821
- const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
822
- this.storage.save(AuthorizationCodeFlow.PKCE_AND_STATE_KEY, {
823
- state: state,
824
- verifier: pkceVerifier
825
- });
826
- const url = new URL(uri);
827
- url.searchParams.set("client_id", this.clientId);
828
- url.searchParams.set("redirect_uri", this.uri.redirect);
829
- url.searchParams.set("response_type", 'code');
830
- url.searchParams.set("scope", this.scope);
831
- url.searchParams.set("state", state);
832
- url.searchParams.set("code_challenge", pkceChallenge);
833
- url.searchParams.set("code_challenge_method", 'S256');
834
- Object.entries(additionalParams || {}).forEach(kv => {
835
- url.searchParams.set(kv[0], kv[1]);
836
- });
837
- window.location = url;
838
- }
839
- async registration(additionalParams){
840
- await this.action(this.uri.registration, additionalParams);
841
- }
842
- async applicationInitiatedAction(kcAction){
843
- await this.action(this.uri.auth, {
844
- kc_action: kcAction
845
- });
846
- }
847
- async _tokenExchange(code, state) {
848
- window.history.replaceState('', "", this.uri.redirect);
849
- const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
850
- if (stateAndVerifier.state !== state) {
851
- throw new Error("State mismatch");
852
- }
853
- const response = await fetch(this.uri.token, {
854
- method: "POST",
855
- headers: {
856
- "Content-Type": 'application/x-www-form-urlencoded'
857
- },
858
- body: new URLSearchParams([
859
- ["client_id", this.clientId],
860
- ["code", code],
861
- ["grant_type", "authorization_code"],
862
- ["code_verifier", stateAndVerifier.verifier],
863
- ["state", stateAndVerifier.state],
864
- ["redirect_uri", this.uri.redirect]
865
- ])
866
- });
867
- if (!response.ok) {
868
- const text = await response.text();
869
- throw new Error("Error:" + response.status + ": " + text);
870
- }
871
- const token = await response.json();
872
- return new AuthorizationCodeFlowSession(this.clientId, token, this.uri);
1047
+ return template.render({ id, name, floating, slotted });
873
1048
  }
874
- async ensureLoggedIn() {
875
- const url = new URL(window.location.href);
876
- const code = url.searchParams.get("code");
877
- if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
878
- //if callback from keycloak and we have our state still stored
879
- const state = url.searchParams.get("state");
880
- return await this._tokenExchange(code, state);
1049
+ async setValue(v) {
1050
+ if (!this.loaded) {
1051
+ await this.ts.load();
881
1052
  }
882
- //if not authorized
883
- await this.action(this.uri.auth, {});
884
- return null;
885
- }
886
- }
887
- AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
888
-
889
- class AuthorizationCodeFlowSession {
890
- static parseToken(token) {
891
- const [rawHeader, rawPayload, signature] = token.split(".");
892
- const ut8decoder = new TextDecoder("utf-8");
893
- return {
894
- header: JSON.parse(ut8decoder.decode(Base64.decode(rawHeader, Base64.STANDARD))),
895
- payload: JSON.parse(ut8decoder.decode(Base64.decode(rawPayload, Base64.STANDARD))),
896
- signature: signature
897
- };
898
- }
899
- constructor(clientId, t, {token, logout, redirect}) {
900
- this.clientId = clientId;
901
- this.token = t;
902
- this.accessToken = AuthorizationCodeFlowSession.parseToken(t.access_token);
903
- this.refreshToken = AuthorizationCodeFlowSession.parseToken(t.refresh_token);
904
- this.uri = { token, logout, redirect };
905
- this.refreshCallback = null;
1053
+ this.ts.setValue(v);
906
1054
  }
907
- onRefresh(callback) {
908
- this.refreshCallback = callback;
1055
+ getValue() {
1056
+ const v = this.ts.getValue();
1057
+ return v === '' ? null : v;
909
1058
  }
910
- async refresh() {
911
- const response = await fetch(this.uri.token, {
912
- method: "POST",
913
- headers: {
914
- "Content-Type": 'application/x-www-form-urlencoded'
915
- },
916
- body: new URLSearchParams([
917
- ["client_id", this.clientId],
918
- ["grant_type", "refresh_token"],
919
- ["refresh_token", this.token.refresh_token]
920
- ])
1059
+ static custom(tagName, configuration) {
1060
+ customElements.define(tagName, class extends Select {
1061
+ constructor() {
1062
+ super(configuration);
1063
+ }
921
1064
  });
922
- if (!response.ok) {
923
- throw new Error("Error:" + response.status + ": " + response.text());
924
- }
925
- const token = await response.json();
926
- this.token = token;
927
- this.accessToken = AuthorizationCodeFlowSession.parseToken(token.access_token);
928
- this.refreshToken = AuthorizationCodeFlowSession.parseToken(token.refresh_token);
929
- if (this.refreshCallback) {
930
- this.refreshCallback(this.token, this.accessToken, this.refreshToken);
931
- }
932
- }
933
- shouldBeRefreshed(gracePeriod) {
934
- const now = new Date().getTime();
935
- const refreshTokenExpiresAt = this.refreshToken.payload.exp * 1000;
936
- const expired = now > refreshTokenExpiresAt;
937
- const shouldRefresh = now - gracePeriod > refreshTokenExpiresAt;
938
- return !expired && shouldRefresh;
939
- }
940
- async refreshIf(gracePeriod) {
941
- if (!this.shouldBeRefreshed(gracePeriod)) {
942
- return;
943
- }
944
- await this.refresh();
945
1065
  }
946
- logout() {
947
- const url = new URL(this.uri.logout);
948
- url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
949
- url.searchParams.set("id_token_hint", this.token.id_token);
950
- window.location = url;
1066
+ static configure() {
1067
+ return Select.custom('ful-select');
951
1068
  }
952
1069
 
953
- bearerToken() {
954
- return `Bearer ${this.token.access_token}`;
955
- }
956
-
957
- interceptor(gracePeriodBefore, gracePeriodAfter){
958
- return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
959
- }
960
1070
  }
961
1071
 
962
- class AuthorizationCodeFlowInterceptor {
963
- constructor(session, gracePeriodBefore, gracePeriodAfter) {
964
- this.session = session;
965
- this.gracePeriodBefore = gracePeriodBefore || 2000;
966
- this.gracePeriodAfter = gracePeriodAfter || 30000;
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
+ `);
967
1077
  }
968
- async intercept(request, chain) {
969
- await this.session.refreshIf(this.gracePeriodBefore);
970
- const headers = new Headers(request.options.headers);
971
- headers.set("Authorization", this.session.bearerToken());
972
- request.options.headers = headers;
973
- const response = await chain.proceed(request);
974
- await this.session.refreshIf(this.gracePeriodAfter);
975
- return response;
1078
+ static configure() {
1079
+ customElements.define('ful-spinner', Spinner);
976
1080
  }
977
1081
  }
978
1082
 
979
- const timing = {
980
- sleep(ms) {
981
- return new Promise(resolve => setTimeout(resolve, ms));
982
- },
983
- DEBOUNCE_DEFAULT: 0,
984
- DEBOUNCE_IMMEDIATE: 1,
985
- debounce(timeoutMs, func, options) {
986
- let tid = null;
987
- let args = [];
988
- let previousTimestamp = 0;
989
- let opts = options || timing.DEBOUNCE_DEFAULT;
990
-
991
- const later = () => {
992
- const elapsed = new Date().getTime() - previousTimestamp;
993
- if (timeoutMs > elapsed) {
994
- tid = setTimeout(later, timeoutMs - elapsed);
995
- return;
996
- }
997
- tid = null;
998
- if (opts !== timing.DEBOUNCE_IMMEDIATE) {
999
- func(...args);
1000
- }
1001
- // This check is needed because `func` can recursively invoke `debounced`.
1002
- if (tid === null) {
1003
- args = [];
1004
- }
1005
- };
1006
-
1007
- return function () {
1008
- args = arguments;
1009
- previousTimestamp = new Date().getTime();
1010
- if (tid === null) {
1011
- tid = setTimeout(later, timeoutMs);
1012
- if (opts === timing.DEBOUNCE_IMMEDIATE) {
1013
- func(...args);
1014
- }
1015
- }
1016
- };
1017
- },
1018
- THROTTLE_DEFAULT: 0,
1019
- THROTTLE_NO_LEADING: 1,
1020
- THROTTLE_NO_TRAILING: 2,
1021
- throttle(timeoutMs, func, options) {
1022
- let tid = null;
1023
- let args = [];
1024
- let previousTimestamp = 0;
1025
- let opts = options || timing.THROTTLE_DEFAULT;
1026
-
1027
- const later = () => {
1028
- previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
1029
- tid = null;
1030
- func(...args);
1031
- if (tid === null) {
1032
- args = [];
1033
- }
1034
- };
1035
-
1036
- return function () {
1037
- const now = new Date().getTime();
1038
- if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
1039
- previousTimestamp = now;
1040
- }
1041
- const remaining = timeoutMs - (now - previousTimestamp);
1042
- args = arguments;
1043
- if (remaining <= 0 || remaining > timeoutMs) {
1044
- if (tid !== null) {
1045
- clearTimeout(tid);
1046
- tid = null;
1047
- }
1048
- previousTimestamp = now;
1049
- func(...args);
1050
- if (tid === null) {
1051
- args = [];
1052
- }
1053
- } else if (tid === null && !(opts & timing.THROTTLE_NO_TRAILING)) {
1054
- tid = setTimeout(later, remaining);
1055
- }
1056
- };
1057
-
1058
- }
1059
- };
1060
-
1061
1083
  class Wizard extends HTMLElement {
1062
1084
  constructor() {
1063
1085
  super();
@@ -1164,15 +1186,16 @@ var ful = (function (exports) {
1164
1186
  }
1165
1187
 
1166
1188
  exports.App = App;
1189
+ exports.Attributes = Attributes;
1167
1190
  exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
1168
1191
  exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
1169
1192
  exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
1170
1193
  exports.Base64 = Base64;
1171
- exports.CustomElements = CustomElements;
1172
1194
  exports.Errors = Errors;
1173
1195
  exports.Failure = Failure;
1174
1196
  exports.FieldError = FieldError;
1175
1197
  exports.Form = Form;
1198
+ exports.Fragments = Fragments;
1176
1199
  exports.Hex = Hex;
1177
1200
  exports.HttpClient = HttpClient;
1178
1201
  exports.Input = Input;
@@ -1180,7 +1203,10 @@ var ful = (function (exports) {
1180
1203
  exports.Observable = Observable;
1181
1204
  exports.Select = Select;
1182
1205
  exports.SessionStorage = SessionStorage;
1206
+ exports.Slots = Slots;
1183
1207
  exports.Spinner = Spinner;
1208
+ exports.Stateful = Stateful;
1209
+ exports.Templated = Templated;
1184
1210
  exports.VersionedStorage = VersionedStorage;
1185
1211
  exports.Wizard = Wizard;
1186
1212
  exports.jsonPatch = jsonPatch;