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