@optionfactory/ful 0.17.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/LICENSE.md +21 -0
- package/README.md +11 -0
- package/dist/ful-client-errors.iife.js +79 -0
- package/dist/ful-client-errors.iife.js.map +1 -0
- package/dist/ful-client-errors.iife.min.js +2 -0
- package/dist/ful-client-errors.iife.min.js.map +1 -0
- package/dist/ful.iife.js +826 -0
- package/dist/ful.iife.js.map +1 -0
- package/dist/ful.iife.min.js +2 -0
- package/dist/ful.iife.min.js.map +1 -0
- package/dist/ful.min.mjs +2 -0
- package/dist/ful.min.mjs.map +1 -0
- package/dist/ful.mjs +803 -0
- package/dist/ful.mjs.map +1 -0
- package/package.json +27 -0
package/dist/ful.mjs
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/* global CSS */
|
|
2
|
+
|
|
3
|
+
function extract(extractors, el) {
|
|
4
|
+
const maybeExtractor = extractors[el.dataset['bindExtractor']] || extractors[el.dataset['bindProvide']];
|
|
5
|
+
if (maybeExtractor) {
|
|
6
|
+
return maybeExtractor(el);
|
|
7
|
+
}
|
|
8
|
+
if (el.getAttribute('type') === 'radio') {
|
|
9
|
+
if (!el.checked) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return el.dataset['bindType'] === 'boolean' ? el.value === 'true' : el.value;
|
|
13
|
+
}
|
|
14
|
+
if (el.getAttribute('type') === 'checkbox') {
|
|
15
|
+
return el.checked;
|
|
16
|
+
}
|
|
17
|
+
if (el.dataset['bindType'] === 'boolean') {
|
|
18
|
+
return !el.value ? null : el.value === 'true';
|
|
19
|
+
}
|
|
20
|
+
return el.value || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mutate(mutators, el, raw, key, values) {
|
|
24
|
+
const maybeMutator = mutators[el.dataset['bindMutator']] || mutators[el.dataset['bindProvide']];
|
|
25
|
+
if (maybeMutator) {
|
|
26
|
+
maybeMutator(el, raw, key, values);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (el.getAttribute('type') === 'radio') {
|
|
30
|
+
el.checked = el.getAttribute('value') === raw;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (el.getAttribute('type') === 'checkbox') {
|
|
34
|
+
el.checked = raw;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
el.value = raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
function providePath(result, path, value) {
|
|
42
|
+
const keys = path.split(".").map((k) => k.match(/^[0-9]+$/) ? +k : k);
|
|
43
|
+
let current = result;
|
|
44
|
+
let previous = null;
|
|
45
|
+
for (let i = 0; ; ++i) {
|
|
46
|
+
const ckey = keys[i];
|
|
47
|
+
const pkey = keys[i - 1];
|
|
48
|
+
if (Number.isInteger(ckey) && !Array.isArray(current)) {
|
|
49
|
+
if (previous !== null) {
|
|
50
|
+
previous[pkey] = current = [];
|
|
51
|
+
} else {
|
|
52
|
+
result = current = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (i === keys.length - 1) {
|
|
56
|
+
//when value is undefined we only want to define the property if it's not defined
|
|
57
|
+
current[ckey] = value !== undefined ? value : (ckey in current ? current[ckey] : null);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
if (current[ckey] === undefined) {
|
|
61
|
+
current[ckey] = {};
|
|
62
|
+
}
|
|
63
|
+
previous = current;
|
|
64
|
+
current = current[ckey];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class Bindings {
|
|
69
|
+
|
|
70
|
+
constructor( {extractors, mutators, ignoredChildrenSelector, valueHoldersSelector}) {
|
|
71
|
+
this.extractors = extractors || {};
|
|
72
|
+
this.mutators = mutators || {};
|
|
73
|
+
this.valueHoldersSelector = valueHoldersSelector || 'input[name], select[name], textarea[name]';
|
|
74
|
+
this.ignoredChildrenSelector = ignoredChildrenSelector || '.d-none';
|
|
75
|
+
}
|
|
76
|
+
setValues(el, values) {
|
|
77
|
+
for (let k in values) {
|
|
78
|
+
if (!values.hasOwnProperty(k)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
Array.from(el.querySelectorAll(`[name='${CSS.escape(k)}']`)).forEach((el) => {
|
|
82
|
+
mutate(this.mutators, el, values[k], k, values);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
getValues(el) {
|
|
87
|
+
return Array.from(el.querySelectorAll(this.valueHoldersSelector))
|
|
88
|
+
.filter((el) => {
|
|
89
|
+
if (el.dataset['bindInclude'] === 'never') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return el.dataset['bindInclude'] === 'always' || el.closest(this.ignoredChildrenSelector) === null;
|
|
93
|
+
})
|
|
94
|
+
.reduce((result, el) => {
|
|
95
|
+
return providePath(result, el.getAttribute('name'), extract(this.extractors, el));
|
|
96
|
+
}, {});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Base64 {
|
|
101
|
+
static encode(arrayBuffer, dialect) {
|
|
102
|
+
const d = dialect || Base64.URL_SAFE;
|
|
103
|
+
const len = arrayBuffer.byteLength;
|
|
104
|
+
const view = new Uint8Array(arrayBuffer);
|
|
105
|
+
let res = '';
|
|
106
|
+
for (let i = 0; i < len; i += 3) {
|
|
107
|
+
const v1 = d[view[i] >> 2];
|
|
108
|
+
const v2 = d[((view[i] & 3) << 4) | (view[i + 1] >> 4)];
|
|
109
|
+
const v3 = d[((view[i + 1] & 15) << 2) | (view[i + 2] >> 6)];
|
|
110
|
+
const v4 = d[view[i + 2] & 63];
|
|
111
|
+
res += v1 + v2 + v3 + v4;
|
|
112
|
+
}
|
|
113
|
+
if (len % 3 === 2) {
|
|
114
|
+
res = res.substring(0, res.length - 1);
|
|
115
|
+
} else if (len % 3 === 1) {
|
|
116
|
+
res = res.substring(0, res.length - 2);
|
|
117
|
+
}
|
|
118
|
+
return res;
|
|
119
|
+
}
|
|
120
|
+
static decode(str, dialect) {
|
|
121
|
+
const d = dialect || Base64.URL_SAFE;
|
|
122
|
+
let nbytes = Math.floor(str.length * 0.75);
|
|
123
|
+
for (let i = 0; i !== str.length; ++i) {
|
|
124
|
+
if (str[str.length - i - 1] !== '=') {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
--nbytes;
|
|
128
|
+
}
|
|
129
|
+
const view = new Uint8Array(nbytes);
|
|
130
|
+
|
|
131
|
+
let vi = 0;
|
|
132
|
+
let si = 0;
|
|
133
|
+
while (vi < str.length * 0.75) {
|
|
134
|
+
const v1 = d.indexOf(str.charAt(si++));
|
|
135
|
+
const v2 = d.indexOf(str.charAt(si++));
|
|
136
|
+
const v3 = d.indexOf(str.charAt(si++));
|
|
137
|
+
const v4 = d.indexOf(str.charAt(si++));
|
|
138
|
+
view[vi++] = (v1 << 2) | (v2 >> 4);
|
|
139
|
+
view[vi++] = ((v2 & 15) << 4) | (v3 >> 2);
|
|
140
|
+
view[vi++] = ((v3 & 3) << 6) | v4;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return view.buffer;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Base64.STANDARD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
148
|
+
Base64.URL_SAFE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Hex {
|
|
152
|
+
static decode(hex) {
|
|
153
|
+
if (hex.length % 2 !== 0) {
|
|
154
|
+
throw new Error("invalid length");
|
|
155
|
+
}
|
|
156
|
+
const lenInBytes = hex.length / 2;
|
|
157
|
+
return new Uint8Array(lenInBytes).map((e, i) => {
|
|
158
|
+
const offset = i * 2;
|
|
159
|
+
const octet = hex.substring(offset, offset + 2);
|
|
160
|
+
return parseInt(octet, 16);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
static encode(bytes, upper) {
|
|
164
|
+
return Array.from(bytes)
|
|
165
|
+
.map(b => b.toString(16))
|
|
166
|
+
.map(b => upper ? b.toUpperCase() : b)
|
|
167
|
+
.map(o => o.padStart(2, 0))
|
|
168
|
+
.join('');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* global Infinity, CSS */
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Form {
|
|
176
|
+
constructor(el, bindings, {globalErrorsEl, fieldContainerSelector, errorClass, hideClass}) {
|
|
177
|
+
this.el = el;
|
|
178
|
+
this.bindings = bindings;
|
|
179
|
+
this.globalErrorsEl = globalErrorsEl;
|
|
180
|
+
this.fieldContainerSelector = fieldContainerSelector !== undefined ? fieldContainerSelector : Form.DEFAULT_FIELD_CONTAINER_SELECTOR;
|
|
181
|
+
this.errorClass = errorClass || Form.DEFAULT_ERROR_CLASS;
|
|
182
|
+
this.hideClass = hideClass || Form.DEFAULT_HIDE_CLASS;
|
|
183
|
+
}
|
|
184
|
+
setValues(values) {
|
|
185
|
+
return this.bindings.setValues(this.el, values);
|
|
186
|
+
}
|
|
187
|
+
getValues() {
|
|
188
|
+
return this.bindings.getValues(this.el);
|
|
189
|
+
}
|
|
190
|
+
setErrors(errors, scrollFirstErrorIntoView, context) {
|
|
191
|
+
|
|
192
|
+
this.clearErrors();
|
|
193
|
+
errors
|
|
194
|
+
.map(this.mapError ? this.mapError : (e) => e)
|
|
195
|
+
.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT')
|
|
196
|
+
.forEach((e) => {
|
|
197
|
+
const name = e.context.replace("[", ".").replace("].", ".");
|
|
198
|
+
Array.from(this.el.querySelectorAll(`[name='${CSS.escape(name)}']`))
|
|
199
|
+
.map(el => this.fieldContainerSelector ? el.closest(this.fieldContainerSelector) : el)
|
|
200
|
+
.filter(el => el !== null)
|
|
201
|
+
.forEach(label => {
|
|
202
|
+
label.classList.add(this.errorClass);
|
|
203
|
+
label.dataset['error'] = e.reason;
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
if (this.globalErrorsEl) {
|
|
207
|
+
const globalErrors = errors.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
|
|
208
|
+
this.globalErrorsEl.innerHTML = globalErrors.map(e => e.reason).join("\n");
|
|
209
|
+
if (globalErrors.length !== 0) {
|
|
210
|
+
this.globalErrorsEl.classList.remove(this.hideClass);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!scrollFirstErrorIntoView) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const yOffsets = Array.from(this.el.querySelectorAll('.${CSS.escape(this.errorClass)}'))
|
|
217
|
+
.map((label) => label.getBoundingClientRect().y + window.scrollY);
|
|
218
|
+
const firstErrorScrollY = Math.min(...yOffsets);
|
|
219
|
+
if (firstErrorScrollY !== Infinity) {
|
|
220
|
+
window.scroll(window.scrollX, firstErrorScrollY > 100 ? firstErrorScrollY - 100 : 0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
clearErrors() {
|
|
224
|
+
this.el.querySelectorAll(`.${CSS.escape(this.errorClass)}`).forEach(l => l.classList.remove(this.errorClass));
|
|
225
|
+
if (this.globalErrorsEl) {
|
|
226
|
+
this.globalErrorsEl.innerHTML = '';
|
|
227
|
+
this.globalErrorsEl.classList.add(this.hideClass);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Form.DEFAULT_FIELD_CONTAINER_SELECTOR = 'label';
|
|
233
|
+
Form.DEFAULT_ERROR_CLASS = 'has-error';
|
|
234
|
+
Form.DEFAULT_HIDE_CLASS = 'd-none';
|
|
235
|
+
|
|
236
|
+
class ContextInterceptor {
|
|
237
|
+
constructor() {
|
|
238
|
+
const context = document.querySelector("meta[name='context']").getAttribute("content");
|
|
239
|
+
this.context = context.endsWith("/") ? context.substring(0, context.length - 1) : context;
|
|
240
|
+
}
|
|
241
|
+
before(request) {
|
|
242
|
+
const separator = request.resource.startsWith("/") ? "" : "/";
|
|
243
|
+
request.resource = this.context + separator + request.resource;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
class CsrfTokenInterceptor {
|
|
248
|
+
constructor() {
|
|
249
|
+
this.k = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
|
|
250
|
+
this.v = document.querySelector("meta[name='_csrf']").getAttribute("content");
|
|
251
|
+
}
|
|
252
|
+
before(request) {
|
|
253
|
+
const headers = new Headers(request.options.headers);
|
|
254
|
+
headers.set(this.k, this.v);
|
|
255
|
+
request.options.headers = headers;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class RedirectOnUnauthorizedInterceptor {
|
|
260
|
+
constructor(redirectUri) {
|
|
261
|
+
this.redirectUri = redirectUri;
|
|
262
|
+
}
|
|
263
|
+
after(request, response) {
|
|
264
|
+
if (response.status !== 401) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
window.location.href = redirectUri;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class Failure extends Error {
|
|
272
|
+
static parseProblems(status, text) {
|
|
273
|
+
const def = [{
|
|
274
|
+
type: "GENERIC_PROBLEM",
|
|
275
|
+
context: null,
|
|
276
|
+
reason: `${status}: ${text}`,
|
|
277
|
+
details: null
|
|
278
|
+
}];
|
|
279
|
+
try {
|
|
280
|
+
return text ? JSON.parse(text) : def;
|
|
281
|
+
} catch (e) {
|
|
282
|
+
return def;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
static fromResponse(status, text) {
|
|
286
|
+
return new Failure(status, Failure.parseProblems(status, text));
|
|
287
|
+
}
|
|
288
|
+
constructor(status, problems) {
|
|
289
|
+
super(JSON.stringify(problems));
|
|
290
|
+
this.name = `Failure:${status}`;
|
|
291
|
+
this.status = status;
|
|
292
|
+
this.problems = problems;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
class HttpClientBuilder {
|
|
297
|
+
constructor() {
|
|
298
|
+
this.interceptors = [];
|
|
299
|
+
}
|
|
300
|
+
withContext() {
|
|
301
|
+
this.interceptors.push(new ContextInterceptor());
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
withCsrfToken() {
|
|
305
|
+
this.interceptors.push(new CsrfTokenInterceptor());
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
withRedirectOnUnauthorized(redirectUri) {
|
|
309
|
+
this.interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
withInterceptors(...interceptors) {
|
|
313
|
+
this.interceptors.push(...interceptors);
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
build() {
|
|
317
|
+
const interceptors = this.interceptors;
|
|
318
|
+
return new HttpClient({interceptors});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
class HttpClient {
|
|
323
|
+
static builder() {
|
|
324
|
+
return new HttpClientBuilder();
|
|
325
|
+
}
|
|
326
|
+
constructor( {interceptors}){
|
|
327
|
+
this.interceptors = interceptors || [];
|
|
328
|
+
}
|
|
329
|
+
async fetch(resource, options) {
|
|
330
|
+
const is = this.interceptors.concat(options.interceptors || []);
|
|
331
|
+
const request = {resource, options};
|
|
332
|
+
await is.forEach(async (i) => {
|
|
333
|
+
if (!i.before) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await i.before(request);
|
|
337
|
+
});
|
|
338
|
+
const response = await fetch(request.resource, request.options);
|
|
339
|
+
await is.forEach(async (i) => {
|
|
340
|
+
if (!i.after) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await i.after(request, response);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return response;
|
|
347
|
+
}
|
|
348
|
+
async json(resource, options) {
|
|
349
|
+
try {
|
|
350
|
+
const response = await this.fetch(resource, options);
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const message = await response.text();
|
|
353
|
+
throw Failure.fromResponse(response.status, message);
|
|
354
|
+
}
|
|
355
|
+
const text = await response.text();
|
|
356
|
+
return text ? JSON.parse(text) : undefined;
|
|
357
|
+
} catch (e) {
|
|
358
|
+
if (e instanceof Failure) {
|
|
359
|
+
throw e;
|
|
360
|
+
}
|
|
361
|
+
throw new Failure(0, [{
|
|
362
|
+
type: "CONNECTION_PROBLEM",
|
|
363
|
+
context: null,
|
|
364
|
+
reason: e.message,
|
|
365
|
+
details: null
|
|
366
|
+
}]);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async form(resource, options, uiOptions) {
|
|
370
|
+
const ui = uiOptions || {};
|
|
371
|
+
ui.buttons?.forEach(el => {
|
|
372
|
+
el.setAttribute("disabled", "disabled");
|
|
373
|
+
if (ui.loader) {
|
|
374
|
+
el.dataset['oldContent'] = el.innerHTML;
|
|
375
|
+
el.innerHTML = ui.loader;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
const r = await this.json(resource, options);
|
|
380
|
+
ui.form?.clearErrors();
|
|
381
|
+
return r;
|
|
382
|
+
} catch (e) {
|
|
383
|
+
ui.form?.setErrors(e.problems);
|
|
384
|
+
throw e;
|
|
385
|
+
} finally {
|
|
386
|
+
ui.buttons?.forEach(el => {
|
|
387
|
+
el.removeAttribute("disabled");
|
|
388
|
+
el.innerHTML = el.dataset['oldContent'];
|
|
389
|
+
delete el.dataset['oldContent'];
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
class Storage {
|
|
396
|
+
constructor(prefix, storage) {
|
|
397
|
+
this.prefix = prefix;
|
|
398
|
+
this.storage = storage;
|
|
399
|
+
}
|
|
400
|
+
save(k, v) {
|
|
401
|
+
this.storage.setItem(`${this.prefix}-${k}`, JSON.stringify(v));
|
|
402
|
+
}
|
|
403
|
+
load(k) {
|
|
404
|
+
const got = this.storage.getItem(`${this.prefix}-${k}`);
|
|
405
|
+
return got === undefined ? undefined : JSON.parse(got);
|
|
406
|
+
}
|
|
407
|
+
remove(k) {
|
|
408
|
+
this.storage.removeItem(`${this.prefix}-${k}`);
|
|
409
|
+
}
|
|
410
|
+
pop(k) {
|
|
411
|
+
const decoded = this.load(k);
|
|
412
|
+
this.remove(k);
|
|
413
|
+
return decoded;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
class LocalStorage extends Storage {
|
|
418
|
+
constructor(prefix) {
|
|
419
|
+
super(prefix, localStorage);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
class SessionStorage extends Storage {
|
|
424
|
+
constructor(prefix) {
|
|
425
|
+
super(prefix, sessionStorage);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
class VersionedStorage {
|
|
430
|
+
constructor(storage, key, dataSupplier){
|
|
431
|
+
this.storage = storage;
|
|
432
|
+
this.key = key;
|
|
433
|
+
this.dataSupplier = dataSupplier;
|
|
434
|
+
this.cache = null;
|
|
435
|
+
|
|
436
|
+
}
|
|
437
|
+
async load(revision){
|
|
438
|
+
const saved = this.storage.load(this.key);
|
|
439
|
+
if (!!saved && saved.revision === revision) {
|
|
440
|
+
this.cache = saved.value;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const freshData = await this.dataSupplier(revision, this.key);
|
|
444
|
+
this.storage.save(this.key, {
|
|
445
|
+
revision: revision,
|
|
446
|
+
value: freshData
|
|
447
|
+
});
|
|
448
|
+
this.cache = freshData;
|
|
449
|
+
}
|
|
450
|
+
data(){
|
|
451
|
+
return this.cache;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
class AuthorizationCodeFlow {
|
|
456
|
+
static forKeycloak(clientId, realmBaseUrl, redirectUri){
|
|
457
|
+
const authUri = new URL("protocol/openid-connect/auth", realmBaseUrl);
|
|
458
|
+
const tokenUri = new URL("protocol/openid-connect/token", realmBaseUrl);
|
|
459
|
+
const logoutUri = new URL("protocol/openid-connect/logout", realmBaseUrl);
|
|
460
|
+
const scope = "openid profile";
|
|
461
|
+
return new AuthorizationCodeFlow(clientId, scope, authUri, tokenUri, logoutUri, redirectUri);
|
|
462
|
+
}
|
|
463
|
+
constructor(clientId, scope, authUri, tokenUri, logoutUri, redirectUri) {
|
|
464
|
+
this.clientId = clientId;
|
|
465
|
+
this.scope = scope;
|
|
466
|
+
this.authUri = authUri;
|
|
467
|
+
this.tokenUri = tokenUri;
|
|
468
|
+
this.logoutUri = logoutUri;
|
|
469
|
+
this.redirectUri = redirectUri;
|
|
470
|
+
this.storage = new SessionStorage(clientId);
|
|
471
|
+
}
|
|
472
|
+
async _auth() {
|
|
473
|
+
const pkceVerifier = Base64.encode(crypto.getRandomValues(new Uint8Array(32)).buffer);
|
|
474
|
+
const pkceChallenge = Base64.encode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pkceVerifier)));
|
|
475
|
+
const state = this.clientId + Base64.encode(crypto.getRandomValues(new Uint8Array(16)).buffer);
|
|
476
|
+
this.storage.save(AuthorizationCodeFlow.PKCE_AND_STATE_KEY, {
|
|
477
|
+
state: state,
|
|
478
|
+
verifier: pkceVerifier
|
|
479
|
+
});
|
|
480
|
+
const url = new URL(this.authUri);
|
|
481
|
+
url.searchParams.set("client_id", this.clientId);
|
|
482
|
+
url.searchParams.set("redirect_uri", this.redirectUri);
|
|
483
|
+
url.searchParams.set("response_type", 'code');
|
|
484
|
+
url.searchParams.set("scope", this.scope);
|
|
485
|
+
url.searchParams.set("state", state);
|
|
486
|
+
url.searchParams.set("code_challenge", pkceChallenge);
|
|
487
|
+
url.searchParams.set("code_challenge_method", 'S256');
|
|
488
|
+
window.location = url;
|
|
489
|
+
}
|
|
490
|
+
async _tokenExchange(code, state) {
|
|
491
|
+
window.history.replaceState('', "", this.redirectUri);
|
|
492
|
+
const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
|
|
493
|
+
if (stateAndVerifier.state !== state) {
|
|
494
|
+
throw new Error("State mismatch");
|
|
495
|
+
}
|
|
496
|
+
const response = await fetch(this.tokenUri, {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: {
|
|
499
|
+
"Content-Type": 'application/x-www-form-urlencoded'
|
|
500
|
+
},
|
|
501
|
+
body: new URLSearchParams([
|
|
502
|
+
["client_id", this.clientId],
|
|
503
|
+
["code", code],
|
|
504
|
+
["grant_type", "authorization_code"],
|
|
505
|
+
["code_verifier", stateAndVerifier.verifier],
|
|
506
|
+
["state", stateAndVerifier.state],
|
|
507
|
+
["redirect_uri", this.redirectUri]
|
|
508
|
+
])
|
|
509
|
+
});
|
|
510
|
+
if (!response.ok) {
|
|
511
|
+
const text = await response.text();
|
|
512
|
+
throw new Error("Error:" + response.status + ": " + text);
|
|
513
|
+
}
|
|
514
|
+
const token = await response.json();
|
|
515
|
+
return new AuthorizationCodeFlowSession(this.clientId, token, this.tokenUri, this.logoutUri, this.redirectUri);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async ensureLoggedIn() {
|
|
519
|
+
const url = new URL(window.location.href);
|
|
520
|
+
const code = url.searchParams.get("code");
|
|
521
|
+
if (code) {
|
|
522
|
+
//if callback from keycloak
|
|
523
|
+
const state = url.searchParams.get("state");
|
|
524
|
+
return await this._tokenExchange(code, state);
|
|
525
|
+
}
|
|
526
|
+
//if not authorized
|
|
527
|
+
await this._auth();
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
AuthorizationCodeFlow.PKCE_AND_STATE_KEY = "state-and-verifier";
|
|
532
|
+
|
|
533
|
+
class AuthorizationCodeFlowSession {
|
|
534
|
+
static parseToken(token) {
|
|
535
|
+
const [rawHeader, rawPayload, signature] = token.split(".");
|
|
536
|
+
return {
|
|
537
|
+
header: JSON.parse(atob(rawHeader)),
|
|
538
|
+
payload: JSON.parse(atob(rawPayload)),
|
|
539
|
+
signature: signature
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
constructor(clientId, token, tokenUri, logoutUri, redirectUri) {
|
|
543
|
+
this.clientId = clientId;
|
|
544
|
+
this.token = token;
|
|
545
|
+
this.tokenUri = tokenUri;
|
|
546
|
+
this.logoutUri = logoutUri;
|
|
547
|
+
this.redirectUri = redirectUri;
|
|
548
|
+
this.accessToken = AuthorizationCodeFlowSession.parseToken(token.access_token);
|
|
549
|
+
this.refreshToken = AuthorizationCodeFlowSession.parseToken(token.refresh_token);
|
|
550
|
+
this.refreshCallback = null;
|
|
551
|
+
}
|
|
552
|
+
onRefresh(callback) {
|
|
553
|
+
this.refreshCallback = callback;
|
|
554
|
+
}
|
|
555
|
+
async refresh() {
|
|
556
|
+
const response = await fetch(this.tokenUri, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: {
|
|
559
|
+
"Content-Type": 'application/x-www-form-urlencoded'
|
|
560
|
+
},
|
|
561
|
+
body: new URLSearchParams([
|
|
562
|
+
["client_id", this.clientId],
|
|
563
|
+
["grant_type", "refresh_token"],
|
|
564
|
+
["refresh_token", this.token.refresh_token]
|
|
565
|
+
])
|
|
566
|
+
});
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
throw new Error("Error:" + response.code + ": " + response.text());
|
|
569
|
+
}
|
|
570
|
+
const token = await response.json();
|
|
571
|
+
this.token = token;
|
|
572
|
+
this.accessToken = this._parseToken(token.access_token);
|
|
573
|
+
this.refreshToken = this._parseToken(token.refresh_token);
|
|
574
|
+
if (this.refreshCallback) {
|
|
575
|
+
this.refreshCallback(this.token, this.accessToken, this.refreshToken);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
shouldBeRefreshed(gracePeriod) {
|
|
579
|
+
const now = new Date().getTime();
|
|
580
|
+
const refreshTokenExpiresAt = this.refreshToken.payload.exp * 1000;
|
|
581
|
+
const expired = now > refreshTokenExpiresAt;
|
|
582
|
+
const shouldRefresh = now - gracePeriod > refreshTokenExpiresAt;
|
|
583
|
+
return !expired && shouldRefresh;
|
|
584
|
+
}
|
|
585
|
+
async refreshIf(gracePeriod) {
|
|
586
|
+
if (!this.shouldBeRefreshed(gracePeriod)) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
await this.refresh();
|
|
590
|
+
}
|
|
591
|
+
logout() {
|
|
592
|
+
const url = new URL(this.logoutUri);
|
|
593
|
+
url.searchParams.set("post_logout_redirect_uri", this.redirectUri);
|
|
594
|
+
url.searchParams.set("id_token_hint", this.token.id_token);
|
|
595
|
+
window.location = url;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
bearerToken() {
|
|
599
|
+
return `Bearer ${this.token.access_token}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
interceptor(gracePeriodBefore, gracePeriodAfter){
|
|
603
|
+
return new AuthorizationCodeFlowInterceptor(this, gracePeriodBefore, gracePeriodAfter);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
class AuthorizationCodeFlowInterceptor {
|
|
608
|
+
constructor(session, gracePeriodBefore, gracePeriodAfter) {
|
|
609
|
+
this.session = session;
|
|
610
|
+
this.gracePeriodBefore = gracePeriodBefore || 2000;
|
|
611
|
+
this.gracePeriodAfter = gracePeriodAfter || 30000;
|
|
612
|
+
}
|
|
613
|
+
async before(request) {
|
|
614
|
+
await this.session.refreshIf(this.gracePeriodBefore);
|
|
615
|
+
const headers = new Headers(request.options.headers);
|
|
616
|
+
headers.set("Authorization", this.session.bearerToken());
|
|
617
|
+
return request;
|
|
618
|
+
}
|
|
619
|
+
async after(request, response) {
|
|
620
|
+
await this.session.refreshIf(this.gracePeriodAfter);
|
|
621
|
+
return response;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const timing = {
|
|
626
|
+
sleep(ms) {
|
|
627
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
628
|
+
},
|
|
629
|
+
DEBOUNCE_DEFAULT: 0,
|
|
630
|
+
DEBOUNCE_IMMEDIATE: 1,
|
|
631
|
+
debounce(timeoutMs, func, options) {
|
|
632
|
+
let tid = null;
|
|
633
|
+
let args = [];
|
|
634
|
+
let previousTimestamp = 0;
|
|
635
|
+
let opts = options || timing.DEBOUNCE_DEFAULT;
|
|
636
|
+
|
|
637
|
+
const later = () => {
|
|
638
|
+
const elapsed = new Date().getTime() - previousTimestamp;
|
|
639
|
+
if (timeoutMs > elapsed) {
|
|
640
|
+
tid = setTimeout(later, timeoutMs - elapsed);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
tid = null;
|
|
644
|
+
if (opts !== timing.DEBOUNCE_IMMEDIATE) {
|
|
645
|
+
func(...args);
|
|
646
|
+
}
|
|
647
|
+
// This check is needed because `func` can recursively invoke `debounced`.
|
|
648
|
+
if (tid === null) {
|
|
649
|
+
args = [];
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
return function () {
|
|
654
|
+
args = arguments;
|
|
655
|
+
previousTimestamp = new Date().getTime();
|
|
656
|
+
if (tid === null) {
|
|
657
|
+
tid = setTimeout(later, timeoutMs);
|
|
658
|
+
if (opts === timing.DEBOUNCE_IMMEDIATE) {
|
|
659
|
+
func(...args);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
THROTTLE_DEFAULT: 0,
|
|
665
|
+
THROTTLE_NO_LEADING: 1,
|
|
666
|
+
THROTTLE_NO_TRAILING: 2,
|
|
667
|
+
throttle(timeoutMs, func, options) {
|
|
668
|
+
let tid = null;
|
|
669
|
+
let args = [];
|
|
670
|
+
let previousTimestamp = 0;
|
|
671
|
+
let opts = options || timing.THROTTLE_DEFAULT;
|
|
672
|
+
|
|
673
|
+
const later = () => {
|
|
674
|
+
previousTimestamp = (opts & timing.THROTTLE_NO_LEADING) ? 0 : new Date().getTime();
|
|
675
|
+
tid = null;
|
|
676
|
+
func(...args);
|
|
677
|
+
if (tid === null) {
|
|
678
|
+
args = [];
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return function () {
|
|
683
|
+
const now = new Date().getTime();
|
|
684
|
+
if (!previousTimestamp && (opts & timing.THROTTLE_NO_LEADING)) {
|
|
685
|
+
previousTimestamp = now;
|
|
686
|
+
}
|
|
687
|
+
const remaining = timeoutMs - (now - previousTimestamp);
|
|
688
|
+
args = arguments;
|
|
689
|
+
if (remaining <= 0 || remaining > timeoutMs) {
|
|
690
|
+
if (tid !== null) {
|
|
691
|
+
clearTimeout(tid);
|
|
692
|
+
tid = null;
|
|
693
|
+
}
|
|
694
|
+
previousTimestamp = now;
|
|
695
|
+
func(...args);
|
|
696
|
+
if (tid === null) {
|
|
697
|
+
args = [];
|
|
698
|
+
}
|
|
699
|
+
} else if (tid === null && !(opts & timing.THROTTLE_NO_TRAILING)) {
|
|
700
|
+
tid = setTimeout(later, remaining);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
class Wizard {
|
|
708
|
+
constructor(el) {
|
|
709
|
+
this.el = el;
|
|
710
|
+
this.progress = [...el.children].filter(e => e.matches("header,ol,ul"));
|
|
711
|
+
|
|
712
|
+
this.progress.forEach(p => {
|
|
713
|
+
const children = [...p.children];
|
|
714
|
+
const current = children.filter(e => e.matches(".active"))[0];
|
|
715
|
+
if (current === undefined && children.length > 0) {
|
|
716
|
+
children[0].classList.add('active');
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
if (this.el.querySelector('section.current') === null) {
|
|
720
|
+
const firstSection = this.el.querySelector('section:first-of-type');
|
|
721
|
+
if (firstSection !== null) {
|
|
722
|
+
firstSection.classList.add('current');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
next() {
|
|
727
|
+
this.progress.forEach(p => {
|
|
728
|
+
const children = [...p.children];
|
|
729
|
+
const current = children.filter(e => e.matches(".active"))[0];
|
|
730
|
+
current?.classList.remove('active');
|
|
731
|
+
current?.nextElementSibling?.classList.add('active');
|
|
732
|
+
});
|
|
733
|
+
const currentSection = this.el.querySelector('section.current');
|
|
734
|
+
currentSection.classList.remove("current");
|
|
735
|
+
currentSection.nextElementSibling.classList.add('current');
|
|
736
|
+
|
|
737
|
+
this.el.dispatchEvent(new CustomEvent('wizard:activate', {
|
|
738
|
+
bubbles: true,
|
|
739
|
+
cancelable: true
|
|
740
|
+
}));
|
|
741
|
+
|
|
742
|
+
}
|
|
743
|
+
prev() {
|
|
744
|
+
this.progress.forEach(p => {
|
|
745
|
+
const children = [...p.children];
|
|
746
|
+
const current = children.filter(e => e.matches(".active"))[0];
|
|
747
|
+
current?.classList.remove('active');
|
|
748
|
+
current?.previousElementSibling?.classList.add('active');
|
|
749
|
+
});
|
|
750
|
+
const currentSection = this.el.querySelector('section.current');
|
|
751
|
+
currentSection.classList.remove("current");
|
|
752
|
+
currentSection.previousElementSibling.classList.add('current');
|
|
753
|
+
this.el.dispatchEvent(new CustomEvent('wizard:activate', {
|
|
754
|
+
bubbles: true,
|
|
755
|
+
cancelable: true
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
moveTo = function (n) {
|
|
759
|
+
this.progress.forEach(p => {
|
|
760
|
+
const children = [...p.children];
|
|
761
|
+
const current = children.filter(e => e.matches(".active"))[0];
|
|
762
|
+
current?.classList.remove('active');
|
|
763
|
+
p.children[+n]?.classList.add('active');
|
|
764
|
+
});
|
|
765
|
+
const currentSection = this.el.querySelector('section.current');
|
|
766
|
+
currentSection?.classList.remove("current");
|
|
767
|
+
const nthSection = this.el.querySelector(`section:nth-child(${+n})`);
|
|
768
|
+
nthSection.classList.add('current');
|
|
769
|
+
this.el.dispatchEvent(new CustomEvent('wizard:activate', {
|
|
770
|
+
bubbles: true,
|
|
771
|
+
cancelable: true
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
class App {
|
|
777
|
+
constructor() {
|
|
778
|
+
this.configurers = [];
|
|
779
|
+
this.initializers = [];
|
|
780
|
+
this.handlers = [];
|
|
781
|
+
this.running = false;
|
|
782
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
783
|
+
await Promise.all(this.configurers);
|
|
784
|
+
await Promise.all(this.initializers.map(h => Promise.resolve(h())));
|
|
785
|
+
await Promise.all(this.handlers.map(h => Promise.resolve(h())));
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
configure(cb) {
|
|
789
|
+
this.configurers.push(Promise.resolve(cb()));
|
|
790
|
+
return this;
|
|
791
|
+
}
|
|
792
|
+
initialize(cb) {
|
|
793
|
+
this.initializers.push(cb);
|
|
794
|
+
return this;
|
|
795
|
+
}
|
|
796
|
+
ready(cb) {
|
|
797
|
+
this.handlers.push(cb);
|
|
798
|
+
return this;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export { App, AuthorizationCodeFlow, AuthorizationCodeFlowInterceptor, AuthorizationCodeFlowSession, Base64, Bindings, Failure, Form, Hex, HttpClient, LocalStorage, SessionStorage, VersionedStorage, Wizard, timing };
|
|
803
|
+
//# sourceMappingURL=ful.mjs.map
|