@optionfactory/ful 0.25.0 → 0.27.0

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