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