@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.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,491 @@ 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
+ //see https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet#using_double_dash_prefixed_idents
706
+ if (Attributes.asBoolean(value)) {
707
+ this.internals_.states.add(`--${flag}`);
708
+ this.setAttribute(flag, '');
709
+ return;
710
+ }
711
+ this.internals_.states.delete(`--${flag}`);
712
+ this.removeAttribute(flag);
713
+ }
714
+ });
715
+ }
716
+ }
717
+ attributeChangedCallback(name, oldValue, newValue) {
718
+ if (oldValue === newValue) {
719
+ return;
720
+ }
721
+ this[name] = newValue;
722
+ const method = this[`on${name.charAt(0).toUpperCase()}${name.substr(1).toLowerCase()}Changed`];
723
+ method?.call(this, newValue, oldValue);
724
+ }
725
+ };
726
+ };
727
+
728
+ class FieldError extends Templated(HTMLElement) {
729
+ render(slotted, template) {
730
+ this.classList.add('invalid-feedback');
731
+ }
732
+ static configure() {
733
+ customElements.define('ful-field-error', FieldError);
734
+ }
735
+ }
736
+
737
+ class Errors extends Templated(HTMLElement) {
738
+ render(slotted, template) {
739
+ this.classList.add('alert', 'alert-danger', 'd-none');
740
+ }
741
+ static configure() {
742
+ customElements.define('ful-errors', Errors);
743
+ }
547
744
 
745
+ }
548
746
 
747
+ /* global Infinity, CSS */
549
748
 
550
- class Form extends HTMLElement {
749
+ class Form extends Templated(Observable(HTMLElement)) {
551
750
  constructor({ mutators, extractors, valueHoldersSelector, ignoredChildrenSelector }) {
552
751
  super();
553
- Observable.init(this);
554
752
  this.mutators = mutators || {};
555
753
  this.extractors = extractors || {};
556
754
  this.valueHoldersSelector = valueHoldersSelector || '[name]';
557
755
  this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
558
-
756
+ }
757
+ render(slotted, template) {
559
758
  const form = document.createElement('form');
560
- form.append(...this.childNodes);
561
- this.appendChild(form);
562
-
759
+ form.append(slotted.default);
563
760
  form.addEventListener('submit', async (e) => {
564
761
  e.preventDefault();
565
762
  this.spinner(true);
@@ -575,10 +772,11 @@ var ful = (function (exports) {
575
772
  this.spinner(false);
576
773
  }
577
774
  });
775
+ return form;
578
776
  }
579
777
  spinner(spin) {
580
778
  this.querySelectorAll('ful-spinner').forEach(el => {
581
- el[spin ? 'show' : 'hide']();
779
+ el.hidden = !spin;
582
780
  });
583
781
  this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
584
782
  el.disabled = spin;
@@ -726,338 +924,214 @@ var ful = (function (exports) {
726
924
  }
727
925
  });
728
926
  }
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
927
  }
738
928
 
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}`);
929
+ const ful_input_ec = globalThis.ec || ftl.EvaluationContext.configure({
930
+
931
+ });
932
+
933
+ const ful_input_template_ = globalThis.ful_input_template || ftl.Template.fromHtml(`
934
+ <div data-tpl-if="floating" class="input-group has-validation">
935
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
936
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
937
+ <div class="form-floating">
938
+ {{{{ slotted.input }}}}
939
+ <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
940
+ </div>
941
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
942
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
943
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
944
+ </div>
945
+ <div data-tpl-if="!floating" data-tpl-remove="tag">
946
+ <label data-tpl-for="id" class="form-label">{{{{ slotted.default }}}}</label>
947
+ <div class="input-group has-validation">
948
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
949
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
950
+ {{{{ slotted.input }}}}
951
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
952
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
953
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
954
+ </div>
955
+ </div>
956
+ `, ful_input_ec);
957
+
958
+
959
+
960
+ class Input extends Templated(HTMLElement, ful_input_template_) {
961
+ render(slotted, template) {
962
+ const floating = this.hasAttribute('floating');
963
+ const input = slotted.input = slotted.input || (() => {
964
+ const el = document.createElement("input");
965
+ el.classList.add("form-control");
966
+ return el;
967
+ })();
968
+ const id = input.getAttribute('id') || this.getAttribute('input-id') || Attributes.uid('ful-input');
969
+ Attributes.forward('input-', this, slotted.input);
970
+ Attributes.defaultValue(slotted.input, "id", id);
971
+ Attributes.defaultValue(slotted.input, "type", "text");
972
+ Attributes.defaultValue(slotted.input, "placeholder", " ");
973
+ const name = input.getAttribute('name');
974
+ return template.render({ id, name, floating, slotted });
755
975
  }
756
- pop(k) {
757
- const decoded = this.load(k);
758
- this.remove(k);
759
- return decoded;
976
+ static configure() {
977
+ customElements.define('ful-input', Input);
760
978
  }
761
979
  }
762
980
 
763
- class LocalStorage extends Storage {
764
- constructor(prefix) {
765
- super(prefix, localStorage);
981
+ /**
982
+ * <script src="tom-select.complete.js"></script>
983
+ * <link href="tom-select.bootstrap5.css" rel="stylesheet" />
984
+ */
985
+ const ful_select_ec = globalThis.ec || ftl.EvaluationContext.configure({
986
+
987
+ });
988
+
989
+ const ful_select_template_ = globalThis.ful_select_template || ftl.Template.fromHtml(`
990
+ <div data-tpl-if="floating" class="input-group has-validation">
991
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
992
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
993
+ <div class="form-floating">
994
+ {{{{ slotted.input }}}}
995
+ <label data-tpl-for="name" class="form-label">{{{{ slotted.default }}}}</label>
996
+ </div>
997
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
998
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
999
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1000
+ </div>
1001
+ <div data-tpl-if="!floating" data-tpl-remove="tag">
1002
+ <label data-tpl-for="id" class="form-label">{{{{ slotted.default }}}}</label>
1003
+ <div class="input-group has-validation">
1004
+ <span data-tpl-if="slotted.ibefore" class="input-group-text">{{{{ slotted.ibefore }}}}</span>
1005
+ <div data-tpl-if="slotted.before" data-tpl-remove="tag">{{{{ slotted.before }}}}</div>
1006
+ {{{{ slotted.input }}}}
1007
+ <div data-tpl-if="slotted.after" data-tpl-remove="tag">{{{{ slotted.after }}}}</div>
1008
+ <span data-tpl-if="slotted.iafter" class="input-group-text">{{{{ slotted.iafter }}}}</span>
1009
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1010
+ </div>
1011
+ </div>
1012
+ `, ful_select_ec);
1013
+
1014
+
1015
+ class Select extends Templated(Observable(HTMLElement), ful_select_template_) {
1016
+ constructor(tsConfig) {
1017
+ super();
1018
+ this.tsConfig = tsConfig;
766
1019
  }
767
- }
1020
+ render(slotted, template) {
1021
+ const floating = this.hasAttribute('floating');
1022
+ const remote = this.hasAttribute('remote');
1023
+ const input = slotted.input = slotted.input || (() => {
1024
+ return document.createElement("select");
1025
+ })();
1026
+ const id = input.getAttribute('id') || this.getAttribute('input-id') || Attributes.uid('ful-select');
1027
+ Attributes.forward('input-', this, input);
1028
+ Attributes.defaultValue(input, "id", id);
1029
+ Attributes.defaultValue(input, "placeholder", " ");
1030
+ const name = input.getAttribute('name');
1031
+ input.setValue = this.setValue.bind(this);
1032
+ input.getValue = this.getValue.bind(this);
1033
+
1034
+ //tomselect needs the input to have a parent.
1035
+ //se we move the input to a fragment
1036
+ slotted.input = Fragments.from(input);
768
1037
 
769
- class SessionStorage extends Storage {
770
- constructor(prefix) {
771
- super(prefix, sessionStorage);
772
- }
773
- }
1038
+ this.loaded = !remote;
1039
+ this.ts = new TomSelect(input, Object.assign(remote ? {
1040
+ preload: 'focus',
1041
+ load: async (query, callback) => {
1042
+ if (this.loaded) {
1043
+ callback();
1044
+ return;
1045
+ }
1046
+ const data = await this.fire('load', query, []);
1047
+ this.loaded = true;
1048
+ callback(data);
1049
+ }
1050
+ } : {}, this.tsConfig));
774
1051
 
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
-
1052
+ return template.render({ id, name, floating, slotted });
782
1053
  }
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;
1054
+ async setValue(v) {
1055
+ if (!this.loaded) {
1056
+ await this.ts.load();
788
1057
  }
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);
1058
+ this.ts.setValue(v);
841
1059
  }
842
- async applicationInitiatedAction(kcAction){
843
- await this.action(this.uri.auth, {
844
- kc_action: kcAction
845
- });
1060
+ getValue() {
1061
+ const v = this.ts.getValue();
1062
+ return v === '' ? null : v;
846
1063
  }
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
- ])
1064
+ static custom(tagName, configuration) {
1065
+ customElements.define(tagName, class extends Select {
1066
+ constructor() {
1067
+ super(configuration);
1068
+ }
866
1069
  });
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);
873
1070
  }
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);
881
- }
882
- //if not authorized
883
- await this.action(this.uri.auth, {});
884
- return null;
1071
+ static configure() {
1072
+ return Select.custom('ful-select');
885
1073
  }
1074
+
886
1075
  }
887
- AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
888
1076
 
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;
906
- }
907
- onRefresh(callback) {
908
- this.refreshCallback = callback;
909
- }
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
- ])
1077
+ const ful_radiogroup_ec = globalThis.ec || ftl.EvaluationContext.configure({
1078
+
1079
+ });
1080
+
1081
+ const ful_radiougroup_template_ = globalThis.ful_radiogroup_template || ftl.Template.fromHtml(`
1082
+ <fieldset>
1083
+ <legend class="form-label">
1084
+ {{{{ slotted.default }}}}
1085
+ </legend>
1086
+ <section>
1087
+ <label data-tpl-each="inputsAndLabels" data-tpl-var="ial">
1088
+ {{{{ ial[0] }}}}
1089
+ {{{{ ial[1] }}}}
1090
+ </label>
1091
+ </section>
1092
+ <ful-field-error data-tpl-if="name" data-tpl-field="name"></ful-field-error>
1093
+ </fieldset>
1094
+ `, ful_radiogroup_ec);
1095
+
1096
+
1097
+ class RadioGroup extends Stateful(Templated(HTMLElement, ful_radiougroup_template_), ['readonly']) {
1098
+ render(slotted, template) {
1099
+ const name = this.getAttribute('input-name') || Attributes.uid('ful-radiogroup');
1100
+ const radioEls = Array.from(slotted.default.querySelectorAll('ful-radio'));
1101
+ const inputsAndLabels = radioEls.map(el => {
1102
+ const input = document.createElement('input');
1103
+ input.setAttribute('type', 'radio');
1104
+ Attributes.forward('input-', this, input);
1105
+ Attributes.forward('', el, input);
1106
+ Attributes.defaultValue(input, 'name', name);
1107
+ const label = Fragments.fromChildNodes(el);
1108
+ return [input, label];
921
1109
  });
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
- }
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;
951
- }
952
-
953
- bearerToken() {
954
- return `Bearer ${this.token.access_token}`;
955
- }
956
-
957
- interceptor(gracePeriodBefore, gracePeriodAfter){
958
- return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
1110
+ radioEls.forEach(el => el.remove());
1111
+
1112
+ const fragment = template.render({
1113
+ name: name,
1114
+ slotted: slotted,
1115
+ inputsAndLabels: inputsAndLabels
1116
+ });
1117
+ return fragment;
959
1118
  }
1119
+ static configure() {
1120
+ customElements.define('ful-radio-group', RadioGroup);
1121
+ }
960
1122
  }
961
1123
 
962
- class AuthorizationCodeFlowInterceptor {
963
- constructor(session, gracePeriodBefore, gracePeriodAfter) {
964
- this.session = session;
965
- this.gracePeriodBefore = gracePeriodBefore || 2000;
966
- this.gracePeriodAfter = gracePeriodAfter || 30000;
1124
+ class Spinner extends Templated(HTMLElement) {
1125
+ render(slotted, template) {
1126
+ return Fragments.fromHtml(`
1127
+ <div class="spinner-border spinner-border-sm" aria-hidden="true"></div>
1128
+ `);
967
1129
  }
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;
1130
+ static configure() {
1131
+ customElements.define('ful-spinner', Spinner);
976
1132
  }
977
1133
  }
978
1134
 
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
1135
  class Wizard extends HTMLElement {
1062
1136
  constructor() {
1063
1137
  super();
@@ -1164,23 +1238,28 @@ var ful = (function (exports) {
1164
1238
  }
1165
1239
 
1166
1240
  exports.App = App;
1241
+ exports.Attributes = Attributes;
1167
1242
  exports.AuthorizationCodeFlow = AuthorizationCodeFlow;
1168
1243
  exports.AuthorizationCodeFlowInterceptor = AuthorizationCodeFlowInterceptor;
1169
1244
  exports.AuthorizationCodeFlowSession = AuthorizationCodeFlowSession;
1170
1245
  exports.Base64 = Base64;
1171
- exports.CustomElements = CustomElements;
1172
1246
  exports.Errors = Errors;
1173
1247
  exports.Failure = Failure;
1174
1248
  exports.FieldError = FieldError;
1175
1249
  exports.Form = Form;
1250
+ exports.Fragments = Fragments;
1176
1251
  exports.Hex = Hex;
1177
1252
  exports.HttpClient = HttpClient;
1178
1253
  exports.Input = Input;
1179
1254
  exports.LocalStorage = LocalStorage;
1180
1255
  exports.Observable = Observable;
1256
+ exports.RadioGroup = RadioGroup;
1181
1257
  exports.Select = Select;
1182
1258
  exports.SessionStorage = SessionStorage;
1259
+ exports.Slots = Slots;
1183
1260
  exports.Spinner = Spinner;
1261
+ exports.Stateful = Stateful;
1262
+ exports.Templated = Templated;
1184
1263
  exports.VersionedStorage = VersionedStorage;
1185
1264
  exports.Wizard = Wizard;
1186
1265
  exports.jsonPatch = jsonPatch;