@brandup/ui-app 1.0.43 → 2.0.1
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/README.md +5 -4
- package/dist/cjs/index.js +234 -148
- package/dist/cjs/index.js.map +1 -1
- package/dist/mjs/index.js +234 -148
- package/dist/mjs/index.js.map +1 -1
- package/dist/types.d.ts +151 -6
- package/package.json +3 -4
package/dist/mjs/index.js
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import { UIElement } from '@brandup/ui';
|
|
2
2
|
import { Guid } from '@brandup/ui-helpers';
|
|
3
3
|
|
|
4
|
+
/** Runs a chain of middlewares, invoking the named method on each in registration order. */
|
|
4
5
|
class MiddlewareInvoker {
|
|
6
|
+
/** Middleware handled by this invoker node. */
|
|
5
7
|
middleware;
|
|
6
8
|
__next;
|
|
9
|
+
__tail = this;
|
|
10
|
+
/**
|
|
11
|
+
* @param middleware Middleware handled by this invoker node.
|
|
12
|
+
*/
|
|
7
13
|
constructor(middleware) {
|
|
8
14
|
this.middleware = middleware;
|
|
9
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Append a middleware to the end of the chain.
|
|
18
|
+
* @param middleware Middleware to add.
|
|
19
|
+
*/
|
|
10
20
|
next(middleware) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.__next = new MiddlewareInvoker(middleware);
|
|
21
|
+
const invoker = new MiddlewareInvoker(middleware);
|
|
22
|
+
this.__tail.__next = invoker;
|
|
23
|
+
this.__tail = invoker;
|
|
15
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Invoke the given method across the whole middleware chain.
|
|
27
|
+
* @param method Name of the middleware method to call (e.g. "start", "navigate").
|
|
28
|
+
* @param context Invocation context.
|
|
29
|
+
* @returns Promise resolved when the chain has completed.
|
|
30
|
+
*/
|
|
16
31
|
invoke(method, context) {
|
|
17
32
|
return this.__exec(method, context);
|
|
18
33
|
}
|
|
19
34
|
async __exec(method, context) {
|
|
20
|
-
|
|
35
|
+
let nextCalled = false;
|
|
36
|
+
const nextFunc = () => {
|
|
37
|
+
if (nextCalled)
|
|
38
|
+
throw new Error(`Middleware "${this.middleware.name}" called next() more than once for method "${method}".`);
|
|
39
|
+
nextCalled = true;
|
|
40
|
+
return this.__next ? this.__next.__exec(method, context) : Promise.resolve();
|
|
41
|
+
};
|
|
21
42
|
context.abort.throwIfAborted();
|
|
22
43
|
const methodFunc = this.middleware[method];
|
|
23
44
|
if (typeof methodFunc === "function") {
|
|
@@ -51,14 +72,18 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
51
72
|
default: result
|
|
52
73
|
});
|
|
53
74
|
|
|
75
|
+
/** Unique name of the built-in state middleware. */
|
|
54
76
|
const STATE_MIDDLEWARE_NAME = "app-state";
|
|
77
|
+
/**
|
|
78
|
+
* Create the built-in state middleware that toggles loading/loaded/ready CSS state
|
|
79
|
+
* classes on the application element during start, load, navigation and submit.
|
|
80
|
+
* @returns The state middleware instance.
|
|
81
|
+
*/
|
|
55
82
|
const StateMiddlewareFactory = () => {
|
|
56
83
|
let counter = 0;
|
|
57
|
-
//let beginTime: number;
|
|
58
84
|
const begin = (context) => {
|
|
59
85
|
const prev = counter++;
|
|
60
86
|
if (prev === 0) {
|
|
61
|
-
//beginTime = Date.now();
|
|
62
87
|
context.app.element?.classList.remove(result.STATE_CLASS.LOADED);
|
|
63
88
|
context.app.element?.classList.add(result.STATE_CLASS.LOADING);
|
|
64
89
|
}
|
|
@@ -77,6 +102,9 @@ const StateMiddlewareFactory = () => {
|
|
|
77
102
|
begin(context);
|
|
78
103
|
try {
|
|
79
104
|
await next();
|
|
105
|
+
// on success the begin() is left open on purpose: the loading state
|
|
106
|
+
// must persist through "loaded" and the first navigation, which closes
|
|
107
|
+
// it (see the "first" branch in navigate). On error we close it here.
|
|
80
108
|
}
|
|
81
109
|
catch (reason) {
|
|
82
110
|
end(context);
|
|
@@ -99,12 +127,15 @@ const StateMiddlewareFactory = () => {
|
|
|
99
127
|
await next();
|
|
100
128
|
}
|
|
101
129
|
finally {
|
|
130
|
+
end(context); // close the begin() of this navigation
|
|
131
|
+
// the first navigation also closes the loading opened by start(),
|
|
132
|
+
// which intentionally leaves its begin() open until the page is rendered
|
|
102
133
|
if (context.source == "first")
|
|
103
134
|
end(context);
|
|
104
|
-
end(context);
|
|
105
135
|
}
|
|
106
136
|
},
|
|
107
137
|
submit: async (context, next) => {
|
|
138
|
+
begin(context);
|
|
108
139
|
try {
|
|
109
140
|
await next();
|
|
110
141
|
}
|
|
@@ -118,10 +149,15 @@ const StateMiddlewareFactory = () => {
|
|
|
118
149
|
};
|
|
119
150
|
};
|
|
120
151
|
|
|
152
|
+
/** Unique name of the built-in hyperlink middleware. */
|
|
121
153
|
const HYPERLINK_MIDDLEWARE_NAME = "app-hyperlink";
|
|
154
|
+
/**
|
|
155
|
+
* Create the built-in hyperlink middleware that intercepts clicks on application links
|
|
156
|
+
* (anchors with the `applink` class or elements with `data-nav-url`) and routes them through
|
|
157
|
+
* application navigation. Honors meta/ctrl-click and `target="_blank"` to open in a new tab.
|
|
158
|
+
* @returns The hyperlink middleware instance.
|
|
159
|
+
*/
|
|
122
160
|
const HyperLinkMiddlewareFactory = () => {
|
|
123
|
-
let __ctrlPressed = false;
|
|
124
|
-
const onKeyDownUp = (e) => __ctrlPressed = e.ctrlKey;
|
|
125
161
|
let onClick;
|
|
126
162
|
return {
|
|
127
163
|
name: HYPERLINK_MIDDLEWARE_NAME,
|
|
@@ -139,11 +175,9 @@ const HyperLinkMiddlewareFactory = () => {
|
|
|
139
175
|
if (elem.classList.contains(result.NavUrlClassName) || elem.hasAttribute(result.NavUrlAttributeName))
|
|
140
176
|
break;
|
|
141
177
|
}
|
|
142
|
-
if (elem === e.currentTarget)
|
|
143
|
-
return;
|
|
144
178
|
elem = elem.parentElement;
|
|
145
179
|
}
|
|
146
|
-
if (!elem ||
|
|
180
|
+
if (!elem || e.ctrlKey || e.metaKey || elem.getAttribute("target") === "_blank")
|
|
147
181
|
return;
|
|
148
182
|
e.preventDefault();
|
|
149
183
|
e.stopPropagation();
|
|
@@ -154,8 +188,12 @@ const HyperLinkMiddlewareFactory = () => {
|
|
|
154
188
|
url = elem.getAttribute("href");
|
|
155
189
|
else if (elem.hasAttribute(result.NavUrlAttributeName))
|
|
156
190
|
url = elem.getAttribute(result.NavUrlAttributeName);
|
|
157
|
-
else
|
|
158
|
-
|
|
191
|
+
else {
|
|
192
|
+
// matched an applink element with no resolvable url (malformed markup);
|
|
193
|
+
// swallow the click rather than throwing inside a global event listener
|
|
194
|
+
console.warn("Application hyperlink: clicked element has no navigation url.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
159
197
|
if (elem.classList.contains(result.LoadingElementClass))
|
|
160
198
|
return;
|
|
161
199
|
elem.classList.add(result.LoadingElementClass);
|
|
@@ -169,18 +207,20 @@ const HyperLinkMiddlewareFactory = () => {
|
|
|
169
207
|
.catch(() => { })
|
|
170
208
|
.finally(() => elem.classList.remove(result.LoadingElementClass));
|
|
171
209
|
}, false);
|
|
172
|
-
window.addEventListener("keydown", onKeyDownUp, false);
|
|
173
|
-
window.addEventListener("keyup", onKeyDownUp, false);
|
|
174
210
|
},
|
|
175
211
|
stop: (_context, next) => {
|
|
176
212
|
window.removeEventListener("click", onClick, false);
|
|
177
|
-
window.removeEventListener("keydown", onKeyDownUp, false);
|
|
178
|
-
window.removeEventListener("keyup", onKeyDownUp, false);
|
|
179
213
|
return next();
|
|
180
214
|
}
|
|
181
215
|
};
|
|
182
216
|
};
|
|
183
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Parse a url into its components relative to the application base path.
|
|
220
|
+
* @param basePath Application base path.
|
|
221
|
+
* @param url Url to parse. If null, the current `window.location` is used.
|
|
222
|
+
* @returns Parsed url parts.
|
|
223
|
+
*/
|
|
184
224
|
const parseUrl = (basePath, url) => {
|
|
185
225
|
const loc = window.location;
|
|
186
226
|
let origin = loc.origin;
|
|
@@ -212,7 +252,7 @@ const parseUrl = (basePath, url) => {
|
|
|
212
252
|
path = loc.pathname;
|
|
213
253
|
query = new URLSearchParams(url);
|
|
214
254
|
}
|
|
215
|
-
else if (
|
|
255
|
+
else if (/^https?:\/\//i.test(url)) {
|
|
216
256
|
const u = new URL(url);
|
|
217
257
|
if (u.origin != origin) {
|
|
218
258
|
origin = u.origin;
|
|
@@ -248,7 +288,7 @@ const parseUrl = (basePath, url) => {
|
|
|
248
288
|
path = path.substring(0, path.length - 1);
|
|
249
289
|
path = path.toLowerCase();
|
|
250
290
|
if (basePath) {
|
|
251
|
-
if (path.
|
|
291
|
+
if (path.startsWith(basePath.toLowerCase())) {
|
|
252
292
|
path = path.substring(basePath.length);
|
|
253
293
|
if (!path)
|
|
254
294
|
path = '/';
|
|
@@ -291,8 +331,10 @@ const extendQuery = (url, query) => {
|
|
|
291
331
|
query.forEach((v, k) => url.query.append(k, v.toString()));
|
|
292
332
|
}
|
|
293
333
|
else {
|
|
294
|
-
for (
|
|
334
|
+
for (const key in query) {
|
|
295
335
|
const value = query[key];
|
|
336
|
+
if (value === null || typeof value === "undefined")
|
|
337
|
+
continue;
|
|
296
338
|
if (!Array.isArray(value)) {
|
|
297
339
|
url.query.set(key, value);
|
|
298
340
|
}
|
|
@@ -314,6 +356,14 @@ const rebuildUrl = (parsedUrl) => {
|
|
|
314
356
|
parsedUrl.relative = relativeUrl;
|
|
315
357
|
parsedUrl.full = parsedUrl.hash ? `${parsedUrl.url}#${parsedUrl.hash}` : parsedUrl.url;
|
|
316
358
|
};
|
|
359
|
+
/**
|
|
360
|
+
* Build a relative url from a base path with optional path, query and hash.
|
|
361
|
+
* @param basePath Application base path.
|
|
362
|
+
* @param path Optional path appended to the base path.
|
|
363
|
+
* @param query Optional query parameters.
|
|
364
|
+
* @param hash Optional hash.
|
|
365
|
+
* @returns Relative url with base path.
|
|
366
|
+
*/
|
|
317
367
|
const buildUrl = (basePath, path, query, hash) => {
|
|
318
368
|
let url = basePath;
|
|
319
369
|
if (url == '/')
|
|
@@ -358,6 +408,7 @@ const buildUrl = (basePath, path, query, hash) => {
|
|
|
358
408
|
}
|
|
359
409
|
return url;
|
|
360
410
|
};
|
|
411
|
+
/** Url helper functions. */
|
|
361
412
|
var urlHelper = {
|
|
362
413
|
parseUrl,
|
|
363
414
|
extendQuery,
|
|
@@ -369,7 +420,9 @@ var url = /*#__PURE__*/Object.freeze({
|
|
|
369
420
|
default: urlHelper
|
|
370
421
|
});
|
|
371
422
|
|
|
423
|
+
/** Type name of the base {@link Application} class. */
|
|
372
424
|
const APP_TYPENAME = "brandup-ui-app";
|
|
425
|
+
/** Reason value thrown when a navigation is overridden (e.g. by a redirect). */
|
|
373
426
|
const NAV_OVERIDE_ERROR = "NavigationOveride";
|
|
374
427
|
/**
|
|
375
428
|
* Base application class.
|
|
@@ -386,8 +439,13 @@ class Application extends UIElement {
|
|
|
386
439
|
__isRuned;
|
|
387
440
|
__middlewares = {};
|
|
388
441
|
__globalSubmit;
|
|
442
|
+
__onPopStateHandler;
|
|
389
443
|
__execNav; // current navigation invoking
|
|
390
444
|
__lastNav; // last success navigation
|
|
445
|
+
/**
|
|
446
|
+
* @param env Application environment.
|
|
447
|
+
* @param model Application model.
|
|
448
|
+
*/
|
|
391
449
|
constructor(env, model, ..._args) {
|
|
392
450
|
super();
|
|
393
451
|
this.env = env;
|
|
@@ -396,6 +454,7 @@ class Application extends UIElement {
|
|
|
396
454
|
this.invoker = new MiddlewareInvoker(core);
|
|
397
455
|
this.__abort = new AbortController();
|
|
398
456
|
}
|
|
457
|
+
/** Type name of the application. */
|
|
399
458
|
get typeName() { return APP_TYPENAME; }
|
|
400
459
|
/** Current navigation context. */
|
|
401
460
|
get current() { return this.__lastNav?.context; }
|
|
@@ -408,25 +467,36 @@ class Application extends UIElement {
|
|
|
408
467
|
this.__isInited = true;
|
|
409
468
|
this.onInitialize();
|
|
410
469
|
middlewares.forEach(middleware => {
|
|
411
|
-
|
|
470
|
+
const name = middleware.name;
|
|
412
471
|
if (this.__middlewares.hasOwnProperty(name))
|
|
413
472
|
throw new Error(`Middleware "${name}" already registered.`);
|
|
414
473
|
this.__middlewares[name] = middleware;
|
|
415
474
|
this.invoker.next(middleware);
|
|
416
475
|
});
|
|
417
476
|
}
|
|
418
|
-
/**
|
|
477
|
+
/**
|
|
478
|
+
* Initialize application instance. Override to register additional middlewares.
|
|
479
|
+
* Registers the built-in state and hyperlink middlewares by default.
|
|
480
|
+
*/
|
|
419
481
|
onInitialize() {
|
|
420
482
|
this.invoker.next(StateMiddlewareFactory());
|
|
421
483
|
this.invoker.next(HyperLinkMiddlewareFactory());
|
|
422
484
|
}
|
|
423
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* Called at the beginning of application run, before middlewares start.
|
|
487
|
+
* Override to perform async setup work.
|
|
488
|
+
* @returns Promise resolved when starting work is complete.
|
|
489
|
+
*/
|
|
424
490
|
onStarting() { return Promise.resolve(); }
|
|
425
|
-
/**
|
|
491
|
+
/**
|
|
492
|
+
* Called after the application has fully started.
|
|
493
|
+
* Override to perform async work after start.
|
|
494
|
+
* @returns Promise resolved when post-start work is complete.
|
|
495
|
+
*/
|
|
426
496
|
onStared() { return Promise.resolve(); }
|
|
427
497
|
/**
|
|
428
|
-
* Get middleware by
|
|
429
|
-
* @param
|
|
498
|
+
* Get registered middleware by its unique name.
|
|
499
|
+
* @param name Unique name of middleware.
|
|
430
500
|
* @returns Middleware instance.
|
|
431
501
|
*/
|
|
432
502
|
middleware(name) {
|
|
@@ -465,7 +535,7 @@ class Application extends UIElement {
|
|
|
465
535
|
await this.invoker.invoke("loaded", context);
|
|
466
536
|
console.info("app load success");
|
|
467
537
|
this.__abort.signal.throwIfAborted();
|
|
468
|
-
window.addEventListener("popstate", (e) => this.__onPopState(context, e));
|
|
538
|
+
window.addEventListener("popstate", this.__onPopStateHandler = (e) => this.__onPopState(context, e));
|
|
469
539
|
element.addEventListener("submit", this.__globalSubmit = (e) => {
|
|
470
540
|
const form = e.target;
|
|
471
541
|
if (!form.classList.contains(result.FormClassName))
|
|
@@ -474,23 +544,24 @@ class Application extends UIElement {
|
|
|
474
544
|
this.__onSubmit({ form, button: e.submitter instanceof HTMLButtonElement ? e.submitter : null })
|
|
475
545
|
.catch(() => { });
|
|
476
546
|
}, false);
|
|
477
|
-
await this.onStared();
|
|
478
|
-
console.info("app runned");
|
|
479
547
|
}
|
|
480
548
|
catch (reason) {
|
|
481
549
|
console.error(`app run error: ${reason}`);
|
|
482
550
|
throw reason;
|
|
483
551
|
}
|
|
552
|
+
// Run the first navigation before onStared(): its navigate middleware always
|
|
553
|
+
// closes the startup loading state (in a finally, even on error), so the app
|
|
554
|
+
// can never get stuck "loading" if onStared throws.
|
|
484
555
|
try {
|
|
485
556
|
await this.nav({ data: context.data, abort: this.__abort.signal });
|
|
486
557
|
}
|
|
487
558
|
catch (reason) {
|
|
488
|
-
if (reason
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
throw reason;
|
|
559
|
+
if (reason !== NAV_OVERIDE_ERROR)
|
|
560
|
+
throw reason;
|
|
561
|
+
console.info(`app run nav overided`);
|
|
493
562
|
}
|
|
563
|
+
await this.onStared();
|
|
564
|
+
console.info("app runned");
|
|
494
565
|
return context;
|
|
495
566
|
}
|
|
496
567
|
/**
|
|
@@ -510,54 +581,19 @@ class Application extends UIElement {
|
|
|
510
581
|
action = "first";
|
|
511
582
|
else {
|
|
512
583
|
const isChangedUrl = this.__lastNav?.context.url.toLowerCase() !== navUrl.url.toLowerCase();
|
|
513
|
-
const hasHash = !!
|
|
584
|
+
const hasHash = !!navUrl.hash;
|
|
514
585
|
if (isChangedUrl)
|
|
515
|
-
action = "url-change"; //
|
|
586
|
+
action = "url-change"; // url changed
|
|
516
587
|
else
|
|
517
588
|
action = hasHash ? "hash" : "url-no-change";
|
|
518
589
|
}
|
|
519
|
-
|
|
520
|
-
if (this.__execNav && this.__execNav.status === "work") {
|
|
521
|
-
parentNav = this.__execNav;
|
|
522
|
-
parentNav.abort.abort(NAV_OVERIDE_ERROR);
|
|
523
|
-
parentNav.context.overided = true;
|
|
524
|
-
}
|
|
525
|
-
const navIndex = parentNav ? parentNav.context.index + 1 : 1;
|
|
526
|
-
const navAbort = new AbortController();
|
|
527
|
-
const aborts = [this.__abort.signal, navAbort.signal];
|
|
528
|
-
if (abort)
|
|
529
|
-
aborts.push(abort);
|
|
530
|
-
const complextAbort = AbortSignal.any(aborts);
|
|
590
|
+
const base = this.__beginNav(abort);
|
|
531
591
|
const context = {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
source: isFirst ? "first" : "nav",
|
|
535
|
-
app: this,
|
|
536
|
-
abort: complextAbort,
|
|
537
|
-
current: this.__lastNav?.context,
|
|
538
|
-
parent: parentNav?.context,
|
|
539
|
-
overided: false,
|
|
540
|
-
action: action,
|
|
541
|
-
data,
|
|
542
|
-
url: navUrl.url,
|
|
543
|
-
origin: navUrl.origin,
|
|
544
|
-
pathAndQuery: navUrl.relative,
|
|
545
|
-
basePath: navUrl.basePath,
|
|
546
|
-
path: navUrl.path,
|
|
547
|
-
query: navUrl.query,
|
|
548
|
-
hash: navUrl.hash,
|
|
549
|
-
external: navUrl.external,
|
|
550
|
-
replace,
|
|
551
|
-
scope,
|
|
552
|
-
redirect: async (options) => {
|
|
553
|
-
complextAbort.throwIfAborted();
|
|
554
|
-
const result = await this.nav(options);
|
|
555
|
-
complextAbort.throwIfAborted();
|
|
556
|
-
return result;
|
|
557
|
-
}
|
|
592
|
+
...this.__createContext(base, navUrl, isFirst ? "first" : "nav", action, data, replace),
|
|
593
|
+
scope
|
|
558
594
|
};
|
|
559
|
-
const currentNav = { method: "navigate", context, abort: navAbort, status: "work" };
|
|
560
|
-
return await this.__execNavigate(currentNav, parentNav);
|
|
595
|
+
const currentNav = { method: "navigate", context, abort: base.navAbort, status: "work" };
|
|
596
|
+
return await this.__execNavigate(currentNav, base.parentNav);
|
|
561
597
|
}
|
|
562
598
|
/**
|
|
563
599
|
* Reload page with nav.
|
|
@@ -573,9 +609,14 @@ class Application extends UIElement {
|
|
|
573
609
|
this.__abort.signal.throwIfAborted();
|
|
574
610
|
window.location.reload();
|
|
575
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Destroy application: abort pending navigations, run the middleware "stop" chain and release listeners.
|
|
614
|
+
* @param contextData Stop context data.
|
|
615
|
+
* @returns Promise of the stop context.
|
|
616
|
+
*/
|
|
576
617
|
async destroy(contextData) {
|
|
577
618
|
if (this.__abort.signal.aborted)
|
|
578
|
-
return Promise.reject('Application already destroyed.');
|
|
619
|
+
return Promise.reject(new Error('Application already destroyed.'));
|
|
579
620
|
this.__abort.abort();
|
|
580
621
|
console.info("app destroy begin");
|
|
581
622
|
if (this.__execNav)
|
|
@@ -584,6 +625,8 @@ class Application extends UIElement {
|
|
|
584
625
|
this.__lastNav.abort.abort();
|
|
585
626
|
if (this.__globalSubmit)
|
|
586
627
|
this.element?.removeEventListener("submit", this.__globalSubmit);
|
|
628
|
+
if (this.__onPopStateHandler)
|
|
629
|
+
window.removeEventListener("popstate", this.__onPopStateHandler);
|
|
587
630
|
const destroyAbort = new AbortController();
|
|
588
631
|
const context = {
|
|
589
632
|
abort: destroyAbort.signal,
|
|
@@ -619,6 +662,10 @@ class Application extends UIElement {
|
|
|
619
662
|
const { form, button = null, query, data = {} } = opt;
|
|
620
663
|
if ((!button || !button.formNoValidate) && !form.checkValidity())
|
|
621
664
|
throw new Error('Form is invalid.');
|
|
665
|
+
// guard before applying any loading class, otherwise a rejected re-entrant
|
|
666
|
+
// submit would leave the button stuck in the loading state
|
|
667
|
+
if (form.classList.contains(result.LoadingElementClass))
|
|
668
|
+
throw new Error('Form already submitting.');
|
|
622
669
|
let replace = form.hasAttribute(result.NavUrlReplaceAttributeName);
|
|
623
670
|
let method = form.method;
|
|
624
671
|
let enctype = form.enctype;
|
|
@@ -634,62 +681,30 @@ class Application extends UIElement {
|
|
|
634
681
|
if (button.hasAttribute(result.NavUrlReplaceAttributeName))
|
|
635
682
|
replace = true;
|
|
636
683
|
}
|
|
637
|
-
if (form.classList.contains(result.LoadingElementClass))
|
|
638
|
-
throw new Error('Form already submitting.');
|
|
639
684
|
form.classList.add(result.LoadingElementClass);
|
|
640
685
|
method = method.toUpperCase();
|
|
641
686
|
try {
|
|
642
|
-
if (method === "GET")
|
|
643
|
-
|
|
687
|
+
if (method === "GET") {
|
|
688
|
+
const getUrl = urlHelper.parseUrl(this.env.basePath, url);
|
|
689
|
+
urlHelper.extendQuery(getUrl, new FormData(form));
|
|
690
|
+
if (query)
|
|
691
|
+
urlHelper.extendQuery(getUrl, query);
|
|
692
|
+
await this.nav({ url: getUrl.url, data: data, replace, abort: opt.abort });
|
|
693
|
+
}
|
|
644
694
|
else {
|
|
645
695
|
const navUrl = urlHelper.parseUrl(this.env.basePath, url);
|
|
646
696
|
if (query)
|
|
647
697
|
urlHelper.extendQuery(navUrl, query);
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
parentNav.abort.abort(NAV_OVERIDE_ERROR);
|
|
652
|
-
parentNav.context.overided = true;
|
|
653
|
-
}
|
|
654
|
-
const navIndex = parentNav ? parentNav.context.index + 1 : 1;
|
|
655
|
-
const submitAbort = new AbortController();
|
|
656
|
-
const aborts = [this.__abort.signal, submitAbort.signal];
|
|
657
|
-
if (opt.abort)
|
|
658
|
-
aborts.push(opt.abort);
|
|
659
|
-
const complextAbort = AbortSignal.any(aborts);
|
|
660
|
-
let context = {
|
|
661
|
-
index: navIndex,
|
|
662
|
-
id: Guid.createGuid(),
|
|
663
|
-
source: "submit",
|
|
664
|
-
app: this,
|
|
665
|
-
abort: complextAbort,
|
|
666
|
-
current: this.__lastNav?.context,
|
|
667
|
-
parent: parentNav?.context,
|
|
668
|
-
overided: false,
|
|
669
|
-
action: "submit",
|
|
670
|
-
data,
|
|
698
|
+
const base = this.__beginNav(opt.abort);
|
|
699
|
+
const context = {
|
|
700
|
+
...this.__createContext(base, navUrl, "submit", "submit", data, replace),
|
|
671
701
|
form,
|
|
672
702
|
button,
|
|
673
703
|
method,
|
|
674
|
-
enctype
|
|
675
|
-
url: navUrl.url,
|
|
676
|
-
origin: navUrl.origin,
|
|
677
|
-
pathAndQuery: navUrl.relative,
|
|
678
|
-
basePath: navUrl.basePath,
|
|
679
|
-
path: navUrl.path,
|
|
680
|
-
query: navUrl.query,
|
|
681
|
-
hash: navUrl.hash,
|
|
682
|
-
external: navUrl.external,
|
|
683
|
-
replace,
|
|
684
|
-
redirect: async (options) => {
|
|
685
|
-
complextAbort.throwIfAborted();
|
|
686
|
-
const result = await this.nav(options);
|
|
687
|
-
complextAbort.throwIfAborted();
|
|
688
|
-
return result;
|
|
689
|
-
}
|
|
704
|
+
enctype
|
|
690
705
|
};
|
|
691
|
-
const currentNav = { method: "submit", context, abort:
|
|
692
|
-
await this.__execNavigate(currentNav);
|
|
706
|
+
const currentNav = { method: "submit", context, abort: base.navAbort, status: "work" };
|
|
707
|
+
await this.__execNavigate(currentNav, base.parentNav);
|
|
693
708
|
}
|
|
694
709
|
}
|
|
695
710
|
finally {
|
|
@@ -701,7 +716,53 @@ class Application extends UIElement {
|
|
|
701
716
|
__onPopState(_context, event) {
|
|
702
717
|
const popUrl = location.href;
|
|
703
718
|
console.log(`popstate: ${popUrl}`, event.state);
|
|
704
|
-
this.nav({ url: popUrl, data: { popstate: event.state } })
|
|
719
|
+
this.nav({ url: popUrl, data: { popstate: event.state } })
|
|
720
|
+
.catch(() => { });
|
|
721
|
+
}
|
|
722
|
+
/** Detect parent (overriding) navigation and compose the abort signal shared by a new navigation. */
|
|
723
|
+
__beginNav(abort) {
|
|
724
|
+
let parentNav;
|
|
725
|
+
if (this.__execNav && this.__execNav.status === "work") {
|
|
726
|
+
parentNav = this.__execNav;
|
|
727
|
+
parentNav.abort.abort(NAV_OVERIDE_ERROR);
|
|
728
|
+
parentNav.context.overided = true;
|
|
729
|
+
}
|
|
730
|
+
const navAbort = new AbortController();
|
|
731
|
+
const aborts = [this.__abort.signal, navAbort.signal];
|
|
732
|
+
if (abort)
|
|
733
|
+
aborts.push(abort);
|
|
734
|
+
return { parentNav, navAbort, abort: AbortSignal.any(aborts) };
|
|
735
|
+
}
|
|
736
|
+
/** Build the navigation context fields shared by nav and submit. */
|
|
737
|
+
__createContext(base, navUrl, source, action, data, replace) {
|
|
738
|
+
const { parentNav, abort } = base;
|
|
739
|
+
return {
|
|
740
|
+
index: parentNav ? parentNav.context.index + 1 : 1,
|
|
741
|
+
id: Guid.createGuid(),
|
|
742
|
+
source,
|
|
743
|
+
app: this,
|
|
744
|
+
abort,
|
|
745
|
+
current: this.__lastNav?.context,
|
|
746
|
+
parent: parentNav?.context,
|
|
747
|
+
overided: false,
|
|
748
|
+
action,
|
|
749
|
+
data,
|
|
750
|
+
url: navUrl.url,
|
|
751
|
+
origin: navUrl.origin,
|
|
752
|
+
pathAndQuery: navUrl.relative,
|
|
753
|
+
basePath: navUrl.basePath,
|
|
754
|
+
path: navUrl.path,
|
|
755
|
+
query: navUrl.query,
|
|
756
|
+
hash: navUrl.hash,
|
|
757
|
+
external: navUrl.external,
|
|
758
|
+
replace,
|
|
759
|
+
redirect: async (options) => {
|
|
760
|
+
abort.throwIfAborted();
|
|
761
|
+
const result = await this.nav(options);
|
|
762
|
+
abort.throwIfAborted();
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
705
766
|
}
|
|
706
767
|
async __execNavigate(nav, parent) {
|
|
707
768
|
if (parent)
|
|
@@ -737,51 +798,76 @@ class Application extends UIElement {
|
|
|
737
798
|
}
|
|
738
799
|
}
|
|
739
800
|
|
|
801
|
+
/** Fluent builder that configures the application type, middlewares and model, then constructs an {@link Application}. */
|
|
740
802
|
class ApplicationBuilder {
|
|
741
803
|
__model;
|
|
742
804
|
__appType = (Application);
|
|
743
805
|
__middlewares = [];
|
|
806
|
+
/**
|
|
807
|
+
* @param model Application model used when building the application.
|
|
808
|
+
*/
|
|
744
809
|
constructor(model) {
|
|
745
810
|
this.__model = model;
|
|
746
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Set a custom application type to instantiate instead of the base {@link Application}.
|
|
814
|
+
* @param appType Application class constructor.
|
|
815
|
+
* @returns The builder, for chaining.
|
|
816
|
+
*/
|
|
747
817
|
useApp(appType) {
|
|
748
818
|
this.__appType = appType;
|
|
749
819
|
return this;
|
|
750
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Register a middleware via its factory function. Middlewares run in registration order.
|
|
823
|
+
* @param createFunc Factory that creates the middleware instance.
|
|
824
|
+
* @param params Optional arguments passed to the factory.
|
|
825
|
+
* @returns The builder, for chaining.
|
|
826
|
+
*/
|
|
751
827
|
useMiddleware(createFunc, ...params) {
|
|
752
828
|
let midl = createFunc(...params);
|
|
753
829
|
this.__middlewares.push(midl);
|
|
754
830
|
return this;
|
|
755
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Build and initialize the application instance.
|
|
834
|
+
* @param env Application environment (a base path of `/` is normalized to empty).
|
|
835
|
+
* @param args Extra arguments forwarded to the application constructor.
|
|
836
|
+
* @returns The initialized application instance.
|
|
837
|
+
*/
|
|
756
838
|
build(env, ...args) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
839
|
+
const appEnv = { ...env };
|
|
840
|
+
if (!appEnv.basePath || appEnv.basePath == '/')
|
|
841
|
+
appEnv.basePath = '';
|
|
842
|
+
const app = new this.__appType(appEnv, this.__model, ...args);
|
|
760
843
|
app.initialize(this.__middlewares);
|
|
761
844
|
return app;
|
|
762
845
|
}
|
|
763
846
|
}
|
|
764
847
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
848
|
+
// Guarded so importing the module does not throw in a non-DOM environment (SSR/Node).
|
|
849
|
+
if (typeof HTMLElement !== "undefined") {
|
|
850
|
+
HTMLElement.prototype.navUrl = function (url) {
|
|
851
|
+
if (this instanceof HTMLAnchorElement)
|
|
852
|
+
this.href = url;
|
|
853
|
+
else
|
|
854
|
+
this.dataset.navUrl = url;
|
|
855
|
+
this.classList.add(result.NavUrlClassName);
|
|
856
|
+
return this;
|
|
857
|
+
};
|
|
858
|
+
HTMLElement.prototype.nav = function (app, path, query, hash) {
|
|
859
|
+
const url = app.buildUrl(path, query, hash);
|
|
860
|
+
return this.navUrl(url);
|
|
861
|
+
};
|
|
862
|
+
HTMLElement.prototype.navReplace = function () {
|
|
863
|
+
this.setAttribute(result.NavUrlReplaceAttributeName, "");
|
|
864
|
+
return this;
|
|
865
|
+
};
|
|
866
|
+
HTMLElement.prototype.navScope = function (scope) {
|
|
867
|
+
this.setAttribute(result.NavUrlScopeAttributeName, scope);
|
|
868
|
+
return this;
|
|
869
|
+
};
|
|
870
|
+
}
|
|
785
871
|
|
|
786
872
|
export { APP_TYPENAME, Application, ApplicationBuilder, constants as CONSTANTS, NAV_OVERIDE_ERROR, url as UrlHelper };
|
|
787
873
|
//# sourceMappingURL=index.js.map
|