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