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