@brandup/ui-app 2.0.1 → 2.0.2
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 +2 -0
- package/dist/cjs/app.js +397 -0
- package/dist/cjs/app.js.map +1 -0
- package/dist/cjs/builder.js +53 -0
- package/dist/cjs/builder.js.map +1 -0
- package/dist/cjs/constants.js +21 -0
- package/dist/cjs/constants.js.map +1 -0
- package/dist/cjs/ext.js +39 -0
- package/dist/cjs/ext.js.map +1 -0
- package/dist/cjs/helpers/url.js +206 -0
- package/dist/cjs/helpers/url.js.map +1 -0
- package/dist/cjs/index.js +11 -873
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/middlewares/hyperlink.js +75 -0
- package/dist/cjs/middlewares/hyperlink.js.map +1 -0
- package/dist/cjs/middlewares/invoker.js +55 -0
- package/dist/cjs/middlewares/invoker.js.map +1 -0
- package/dist/cjs/middlewares/state.js +86 -0
- package/dist/cjs/middlewares/state.js.map +1 -0
- package/dist/mjs/app.js +393 -0
- package/dist/mjs/app.js.map +1 -0
- package/dist/mjs/builder.js +51 -0
- package/dist/mjs/builder.js.map +1 -0
- package/dist/mjs/constants.js +17 -0
- package/dist/mjs/constants.js.map +1 -0
- package/dist/mjs/ext.js +37 -0
- package/dist/mjs/ext.js.map +1 -0
- package/dist/mjs/helpers/url.js +202 -0
- package/dist/mjs/helpers/url.js.map +1 -0
- package/dist/mjs/index.js +7 -872
- package/dist/mjs/index.js.map +1 -1
- package/dist/mjs/middlewares/hyperlink.js +70 -0
- package/dist/mjs/middlewares/hyperlink.js.map +1 -0
- package/dist/mjs/middlewares/invoker.js +53 -0
- package/dist/mjs/middlewares/invoker.js.map +1 -0
- package/dist/mjs/middlewares/state.js +81 -0
- package/dist/mjs/middlewares/state.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/package.json +11 -3
package/dist/mjs/index.js
CHANGED
|
@@ -1,873 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
__next;
|
|
9
|
-
__tail = this;
|
|
10
|
-
/**
|
|
11
|
-
* @param middleware Middleware handled by this invoker node.
|
|
12
|
-
*/
|
|
13
|
-
constructor(middleware) {
|
|
14
|
-
this.middleware = middleware;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Append a middleware to the end of the chain.
|
|
18
|
-
* @param middleware Middleware to add.
|
|
19
|
-
*/
|
|
20
|
-
next(middleware) {
|
|
21
|
-
const invoker = new MiddlewareInvoker(middleware);
|
|
22
|
-
this.__tail.__next = invoker;
|
|
23
|
-
this.__tail = invoker;
|
|
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
|
-
*/
|
|
31
|
-
invoke(method, context) {
|
|
32
|
-
return this.__exec(method, context);
|
|
33
|
-
}
|
|
34
|
-
async __exec(method, context) {
|
|
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
|
-
};
|
|
42
|
-
context.abort.throwIfAborted();
|
|
43
|
-
const methodFunc = this.middleware[method];
|
|
44
|
-
if (typeof methodFunc === "function") {
|
|
45
|
-
const methodResult = methodFunc.call(this.middleware, context, nextFunc);
|
|
46
|
-
if (!methodResult || !(methodResult instanceof Promise))
|
|
47
|
-
throw new Error(`Middleware "${this.middleware.name}" method "${method}" is not async.`);
|
|
48
|
-
await methodResult;
|
|
49
|
-
}
|
|
50
|
-
else
|
|
51
|
-
await nextFunc();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const result = {
|
|
56
|
-
LoadingElementClass: "loading",
|
|
57
|
-
NavUrlClassName: "applink",
|
|
58
|
-
FormClassName: "appform",
|
|
59
|
-
NavUrlAttributeName: "data-nav-url",
|
|
60
|
-
NavUrlReplaceAttributeName: "data-nav-replace",
|
|
61
|
-
NavUrlScopeAttributeName: "data-nav-scope",
|
|
62
|
-
NavIgnoreAttributeName: "data-nav-ignore",
|
|
63
|
-
STATE_CLASS: {
|
|
64
|
-
LOADING: "bp-state-loading",
|
|
65
|
-
LOADED: "bp-state-loaded",
|
|
66
|
-
READY: "bp-state-ready"
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
var constants = /*#__PURE__*/Object.freeze({
|
|
71
|
-
__proto__: null,
|
|
72
|
-
default: result
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
/** Unique name of the built-in state middleware. */
|
|
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
|
-
*/
|
|
82
|
-
const StateMiddlewareFactory = () => {
|
|
83
|
-
let counter = 0;
|
|
84
|
-
const begin = (context) => {
|
|
85
|
-
const prev = counter++;
|
|
86
|
-
if (prev === 0) {
|
|
87
|
-
context.app.element?.classList.remove(result.STATE_CLASS.LOADED);
|
|
88
|
-
context.app.element?.classList.add(result.STATE_CLASS.LOADING);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
const end = (context) => {
|
|
92
|
-
counter--;
|
|
93
|
-
if (counter <= 0) {
|
|
94
|
-
counter = 0;
|
|
95
|
-
context.app.element?.classList.add(result.STATE_CLASS.LOADED);
|
|
96
|
-
context.app.element?.classList.remove(result.STATE_CLASS.LOADING);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
return {
|
|
100
|
-
name: STATE_MIDDLEWARE_NAME,
|
|
101
|
-
start: async (context, next) => {
|
|
102
|
-
begin(context);
|
|
103
|
-
try {
|
|
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.
|
|
108
|
-
}
|
|
109
|
-
catch (reason) {
|
|
110
|
-
end(context);
|
|
111
|
-
throw reason;
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
loaded: async (context, next) => {
|
|
115
|
-
try {
|
|
116
|
-
await next();
|
|
117
|
-
context.app.element?.classList.add(result.STATE_CLASS.READY);
|
|
118
|
-
}
|
|
119
|
-
catch (reason) {
|
|
120
|
-
end(context);
|
|
121
|
-
throw reason;
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
navigate: async (context, next) => {
|
|
125
|
-
begin(context);
|
|
126
|
-
try {
|
|
127
|
-
await next();
|
|
128
|
-
}
|
|
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
|
|
133
|
-
if (context.source == "first")
|
|
134
|
-
end(context);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
submit: async (context, next) => {
|
|
138
|
-
begin(context);
|
|
139
|
-
try {
|
|
140
|
-
await next();
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
end(context);
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
stop: async (_context, next) => {
|
|
147
|
-
await next();
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/** Unique name of the built-in hyperlink middleware. */
|
|
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
|
-
*/
|
|
160
|
-
const HyperLinkMiddlewareFactory = () => {
|
|
161
|
-
let onClick;
|
|
162
|
-
return {
|
|
163
|
-
name: HYPERLINK_MIDDLEWARE_NAME,
|
|
164
|
-
start: async (context, next) => {
|
|
165
|
-
await next();
|
|
166
|
-
window.addEventListener("click", onClick = (e) => {
|
|
167
|
-
let elem = e.target;
|
|
168
|
-
let ignore = false;
|
|
169
|
-
while (elem) {
|
|
170
|
-
if (elem instanceof HTMLElement) {
|
|
171
|
-
if (elem.hasAttribute(result.NavIgnoreAttributeName)) {
|
|
172
|
-
ignore = true;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
if (elem.classList.contains(result.NavUrlClassName) || elem.hasAttribute(result.NavUrlAttributeName))
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
elem = elem.parentElement;
|
|
179
|
-
}
|
|
180
|
-
if (!elem || e.ctrlKey || e.metaKey || elem.getAttribute("target") === "_blank")
|
|
181
|
-
return;
|
|
182
|
-
e.preventDefault();
|
|
183
|
-
e.stopPropagation();
|
|
184
|
-
if (ignore)
|
|
185
|
-
return;
|
|
186
|
-
let url;
|
|
187
|
-
if (elem.tagName === "A")
|
|
188
|
-
url = elem.getAttribute("href");
|
|
189
|
-
else if (elem.hasAttribute(result.NavUrlAttributeName))
|
|
190
|
-
url = elem.getAttribute(result.NavUrlAttributeName);
|
|
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
|
-
}
|
|
197
|
-
if (elem.classList.contains(result.LoadingElementClass))
|
|
198
|
-
return;
|
|
199
|
-
elem.classList.add(result.LoadingElementClass);
|
|
200
|
-
context.app
|
|
201
|
-
.nav({
|
|
202
|
-
url,
|
|
203
|
-
replace: elem.hasAttribute(result.NavUrlReplaceAttributeName),
|
|
204
|
-
scope: elem.getAttribute(result.NavUrlScopeAttributeName),
|
|
205
|
-
data: { clickElem: elem }
|
|
206
|
-
})
|
|
207
|
-
.catch(() => { })
|
|
208
|
-
.finally(() => elem.classList.remove(result.LoadingElementClass));
|
|
209
|
-
}, false);
|
|
210
|
-
},
|
|
211
|
-
stop: (_context, next) => {
|
|
212
|
-
window.removeEventListener("click", onClick, false);
|
|
213
|
-
return next();
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
};
|
|
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
|
-
*/
|
|
224
|
-
const parseUrl = (basePath, url) => {
|
|
225
|
-
const loc = window.location;
|
|
226
|
-
let origin = loc.origin;
|
|
227
|
-
let path;
|
|
228
|
-
let query = null;
|
|
229
|
-
let hash = null;
|
|
230
|
-
let isExternal = false;
|
|
231
|
-
if (basePath === '/')
|
|
232
|
-
basePath = '';
|
|
233
|
-
if (!url) {
|
|
234
|
-
path = loc.pathname;
|
|
235
|
-
if (loc.search)
|
|
236
|
-
query = new URLSearchParams(loc.search);
|
|
237
|
-
if (loc.hash)
|
|
238
|
-
hash = loc.hash;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
if (url.startsWith("#")) {
|
|
242
|
-
path = loc.pathname;
|
|
243
|
-
query = new URLSearchParams(loc.search);
|
|
244
|
-
hash = url;
|
|
245
|
-
}
|
|
246
|
-
else if (url.startsWith("?")) {
|
|
247
|
-
const hastIndex = url.lastIndexOf("#");
|
|
248
|
-
if (hastIndex !== -1) {
|
|
249
|
-
hash = url.substring(hastIndex);
|
|
250
|
-
url = url.substring(0, hastIndex);
|
|
251
|
-
}
|
|
252
|
-
path = loc.pathname;
|
|
253
|
-
query = new URLSearchParams(url);
|
|
254
|
-
}
|
|
255
|
-
else if (/^https?:\/\//i.test(url)) {
|
|
256
|
-
const u = new URL(url);
|
|
257
|
-
if (u.origin != origin) {
|
|
258
|
-
origin = u.origin;
|
|
259
|
-
isExternal = true;
|
|
260
|
-
}
|
|
261
|
-
path = u.pathname;
|
|
262
|
-
query = u.searchParams;
|
|
263
|
-
hash = u.hash || null;
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
const hastIndex = url.lastIndexOf("#");
|
|
267
|
-
if (hastIndex !== -1) {
|
|
268
|
-
hash = url.substring(hastIndex);
|
|
269
|
-
url = url.substring(0, hastIndex);
|
|
270
|
-
}
|
|
271
|
-
const queryIndex = url.lastIndexOf("?");
|
|
272
|
-
if (queryIndex !== -1) {
|
|
273
|
-
query = new URLSearchParams(url.substring(queryIndex));
|
|
274
|
-
url = url.substring(0, queryIndex);
|
|
275
|
-
}
|
|
276
|
-
path = url;
|
|
277
|
-
if (!path.startsWith("/")) {
|
|
278
|
-
let curPath = loc.pathname;
|
|
279
|
-
if (curPath.endsWith("/"))
|
|
280
|
-
curPath = curPath.substring(0, curPath.length - 1);
|
|
281
|
-
path = curPath + "/" + path;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (!path)
|
|
286
|
-
path = '/';
|
|
287
|
-
else if (path.length > 1 && path.endsWith('/'))
|
|
288
|
-
path = path.substring(0, path.length - 1);
|
|
289
|
-
path = path.toLowerCase();
|
|
290
|
-
if (basePath) {
|
|
291
|
-
if (path.startsWith(basePath.toLowerCase())) {
|
|
292
|
-
path = path.substring(basePath.length);
|
|
293
|
-
if (!path)
|
|
294
|
-
path = '/';
|
|
295
|
-
}
|
|
296
|
-
else
|
|
297
|
-
basePath = '';
|
|
298
|
-
}
|
|
299
|
-
if (!query)
|
|
300
|
-
query = new URLSearchParams();
|
|
301
|
-
if (hash === '#')
|
|
302
|
-
hash = null;
|
|
303
|
-
else if (hash)
|
|
304
|
-
hash = hash.substring(1);
|
|
305
|
-
var result = {
|
|
306
|
-
full: '',
|
|
307
|
-
url: '',
|
|
308
|
-
relative: '',
|
|
309
|
-
origin,
|
|
310
|
-
basePath,
|
|
311
|
-
path,
|
|
312
|
-
query,
|
|
313
|
-
hash,
|
|
314
|
-
external: isExternal
|
|
315
|
-
};
|
|
316
|
-
rebuildUrl(result);
|
|
317
|
-
return result;
|
|
318
|
-
};
|
|
319
|
-
/**
|
|
320
|
-
* Add or replace query parameters.
|
|
321
|
-
* @param url Source url for extending query.
|
|
322
|
-
* @param query New or update parameters.
|
|
323
|
-
*/
|
|
324
|
-
const extendQuery = (url, query) => {
|
|
325
|
-
if (query instanceof URLSearchParams) {
|
|
326
|
-
query.forEach((_v, k) => url.query.delete(k));
|
|
327
|
-
query.forEach((v, k) => url.query.append(k, v));
|
|
328
|
-
}
|
|
329
|
-
else if (query instanceof FormData) {
|
|
330
|
-
query.forEach((_v, k) => url.query.delete(k));
|
|
331
|
-
query.forEach((v, k) => url.query.append(k, v.toString()));
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
for (const key in query) {
|
|
335
|
-
const value = query[key];
|
|
336
|
-
if (value === null || typeof value === "undefined")
|
|
337
|
-
continue;
|
|
338
|
-
if (!Array.isArray(value)) {
|
|
339
|
-
url.query.set(key, value);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
url.query.delete(key);
|
|
343
|
-
value.forEach(val => url.query.append(key, val));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
rebuildUrl(url);
|
|
348
|
-
};
|
|
349
|
-
const rebuildUrl = (parsedUrl) => {
|
|
350
|
-
let relativeUrl = parsedUrl.basePath + parsedUrl.path;
|
|
351
|
-
if (relativeUrl.length > 1 && relativeUrl.endsWith('/'))
|
|
352
|
-
relativeUrl = relativeUrl.substring(0, relativeUrl.length - 1);
|
|
353
|
-
if (parsedUrl.query.size)
|
|
354
|
-
relativeUrl += "?" + parsedUrl.query.toString();
|
|
355
|
-
parsedUrl.url = parsedUrl.origin + relativeUrl;
|
|
356
|
-
parsedUrl.relative = relativeUrl;
|
|
357
|
-
parsedUrl.full = parsedUrl.hash ? `${parsedUrl.url}#${parsedUrl.hash}` : parsedUrl.url;
|
|
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
|
-
*/
|
|
367
|
-
const buildUrl = (basePath, path, query, hash) => {
|
|
368
|
-
let url = basePath;
|
|
369
|
-
if (url == '/')
|
|
370
|
-
url = '';
|
|
371
|
-
if (path) {
|
|
372
|
-
if (!path.startsWith("/"))
|
|
373
|
-
path = '/' + path;
|
|
374
|
-
url += path;
|
|
375
|
-
}
|
|
376
|
-
if (!url)
|
|
377
|
-
url = '/';
|
|
378
|
-
else if (url.endsWith('/'))
|
|
379
|
-
url = url.substring(0, url.length - 1);
|
|
380
|
-
if (query) {
|
|
381
|
-
let params;
|
|
382
|
-
if (query instanceof URLSearchParams)
|
|
383
|
-
params = query;
|
|
384
|
-
else if (query instanceof FormData) {
|
|
385
|
-
params = new URLSearchParams();
|
|
386
|
-
query.forEach((value, key) => params.append(key, value.toString()));
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
params = new URLSearchParams();
|
|
390
|
-
for (const key in query) {
|
|
391
|
-
const value = query[key];
|
|
392
|
-
if (value === null || typeof value === "undefined")
|
|
393
|
-
continue;
|
|
394
|
-
if (Array.isArray(value))
|
|
395
|
-
value.forEach(v => params.append(key, v));
|
|
396
|
-
else
|
|
397
|
-
params.append(key, value);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (params.size)
|
|
401
|
-
url += "?" + params.toString();
|
|
402
|
-
}
|
|
403
|
-
if (hash) {
|
|
404
|
-
if (!hash.startsWith("#"))
|
|
405
|
-
hash = "#" + hash;
|
|
406
|
-
if (hash != "#")
|
|
407
|
-
url += hash;
|
|
408
|
-
}
|
|
409
|
-
return url;
|
|
410
|
-
};
|
|
411
|
-
/** Url helper functions. */
|
|
412
|
-
var urlHelper = {
|
|
413
|
-
parseUrl,
|
|
414
|
-
extendQuery,
|
|
415
|
-
buildUrl
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
var url = /*#__PURE__*/Object.freeze({
|
|
419
|
-
__proto__: null,
|
|
420
|
-
default: urlHelper
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
/** Type name of the base {@link Application} class. */
|
|
424
|
-
const APP_TYPENAME = "brandup-ui-app";
|
|
425
|
-
/** Reason value thrown when a navigation is overridden (e.g. by a redirect). */
|
|
426
|
-
const NAV_OVERIDE_ERROR = "NavigationOveride";
|
|
427
|
-
/**
|
|
428
|
-
* Base application class.
|
|
429
|
-
*/
|
|
430
|
-
class Application extends UIElement {
|
|
431
|
-
/** Application environment. */
|
|
432
|
-
env;
|
|
433
|
-
/** Application model. */
|
|
434
|
-
model;
|
|
435
|
-
/** Application middleware invoker. */
|
|
436
|
-
invoker;
|
|
437
|
-
__abort;
|
|
438
|
-
__isInited;
|
|
439
|
-
__isRuned;
|
|
440
|
-
__middlewares = {};
|
|
441
|
-
__globalSubmit;
|
|
442
|
-
__onPopStateHandler;
|
|
443
|
-
__execNav; // current navigation invoking
|
|
444
|
-
__lastNav; // last success navigation
|
|
445
|
-
/**
|
|
446
|
-
* @param env Application environment.
|
|
447
|
-
* @param model Application model.
|
|
448
|
-
*/
|
|
449
|
-
constructor(env, model, ..._args) {
|
|
450
|
-
super();
|
|
451
|
-
this.env = env;
|
|
452
|
-
this.model = model;
|
|
453
|
-
const core = { name: "app-root" };
|
|
454
|
-
this.invoker = new MiddlewareInvoker(core);
|
|
455
|
-
this.__abort = new AbortController();
|
|
456
|
-
}
|
|
457
|
-
/** Type name of the application. */
|
|
458
|
-
get typeName() { return APP_TYPENAME; }
|
|
459
|
-
/** Current navigation context. */
|
|
460
|
-
get current() { return this.__lastNav?.context; }
|
|
461
|
-
/** Application destroy signal. */
|
|
462
|
-
get abort() { return this.__abort.signal; }
|
|
463
|
-
/** @internal */
|
|
464
|
-
initialize(middlewares) {
|
|
465
|
-
if (this.__isInited)
|
|
466
|
-
throw new Error('Application already initialized.');
|
|
467
|
-
this.__isInited = true;
|
|
468
|
-
this.onInitialize();
|
|
469
|
-
middlewares.forEach(middleware => {
|
|
470
|
-
const name = middleware.name;
|
|
471
|
-
if (this.__middlewares.hasOwnProperty(name))
|
|
472
|
-
throw new Error(`Middleware "${name}" already registered.`);
|
|
473
|
-
this.__middlewares[name] = middleware;
|
|
474
|
-
this.invoker.next(middleware);
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Initialize application instance. Override to register additional middlewares.
|
|
479
|
-
* Registers the built-in state and hyperlink middlewares by default.
|
|
480
|
-
*/
|
|
481
|
-
onInitialize() {
|
|
482
|
-
this.invoker.next(StateMiddlewareFactory());
|
|
483
|
-
this.invoker.next(HyperLinkMiddlewareFactory());
|
|
484
|
-
}
|
|
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
|
-
*/
|
|
490
|
-
onStarting() { return Promise.resolve(); }
|
|
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
|
-
*/
|
|
496
|
-
onStared() { return Promise.resolve(); }
|
|
497
|
-
/**
|
|
498
|
-
* Get registered middleware by its unique name.
|
|
499
|
-
* @param name Unique name of middleware.
|
|
500
|
-
* @returns Middleware instance.
|
|
501
|
-
*/
|
|
502
|
-
middleware(name) {
|
|
503
|
-
this.__abort.signal.throwIfAborted();
|
|
504
|
-
const middleware = this.__middlewares[name];
|
|
505
|
-
if (!middleware)
|
|
506
|
-
throw new Error(`Middleware ${name} is not registered.`);
|
|
507
|
-
return middleware;
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Run application.
|
|
511
|
-
* @param contextData Run context data.
|
|
512
|
-
* @param element HTMLElement of application. Default is document.body.
|
|
513
|
-
* @returns Promise of runned result.
|
|
514
|
-
*/
|
|
515
|
-
async run(contextData, element) {
|
|
516
|
-
if (this.__abort.signal.aborted)
|
|
517
|
-
throw new Error('Application is destroyed.');
|
|
518
|
-
if (this.__isRuned)
|
|
519
|
-
throw new Error('Application already run.');
|
|
520
|
-
this.__isRuned = true;
|
|
521
|
-
if (!contextData)
|
|
522
|
-
contextData = {};
|
|
523
|
-
element = element || document.body;
|
|
524
|
-
this.setElement(element);
|
|
525
|
-
const context = {
|
|
526
|
-
abort: this.__abort.signal,
|
|
527
|
-
app: this,
|
|
528
|
-
data: contextData
|
|
529
|
-
};
|
|
530
|
-
try {
|
|
531
|
-
await this.onStarting();
|
|
532
|
-
await this.invoker.invoke("start", context);
|
|
533
|
-
console.info("app start success");
|
|
534
|
-
this.__abort.signal.throwIfAborted();
|
|
535
|
-
await this.invoker.invoke("loaded", context);
|
|
536
|
-
console.info("app load success");
|
|
537
|
-
this.__abort.signal.throwIfAborted();
|
|
538
|
-
window.addEventListener("popstate", this.__onPopStateHandler = (e) => this.__onPopState(context, e));
|
|
539
|
-
element.addEventListener("submit", this.__globalSubmit = (e) => {
|
|
540
|
-
const form = e.target;
|
|
541
|
-
if (!form.classList.contains(result.FormClassName))
|
|
542
|
-
return;
|
|
543
|
-
e.preventDefault();
|
|
544
|
-
this.__onSubmit({ form, button: e.submitter instanceof HTMLButtonElement ? e.submitter : null })
|
|
545
|
-
.catch(() => { });
|
|
546
|
-
}, false);
|
|
547
|
-
}
|
|
548
|
-
catch (reason) {
|
|
549
|
-
console.error(`app run error: ${reason}`);
|
|
550
|
-
throw reason;
|
|
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.
|
|
555
|
-
try {
|
|
556
|
-
await this.nav({ data: context.data, abort: this.__abort.signal });
|
|
557
|
-
}
|
|
558
|
-
catch (reason) {
|
|
559
|
-
if (reason !== NAV_OVERIDE_ERROR)
|
|
560
|
-
throw reason;
|
|
561
|
-
console.info(`app run nav overided`);
|
|
562
|
-
}
|
|
563
|
-
await this.onStared();
|
|
564
|
-
console.info("app runned");
|
|
565
|
-
return context;
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Navigate application to url.
|
|
569
|
-
* @param options Navigate options.
|
|
570
|
-
* @returns Promise of navigated result.
|
|
571
|
-
*/
|
|
572
|
-
async nav(options) {
|
|
573
|
-
const opt = (!options || typeof options === "string") ? { url: options } : options;
|
|
574
|
-
let { url = null, query, replace = false, scope = null, data = {}, abort } = opt;
|
|
575
|
-
const navUrl = urlHelper.parseUrl(this.env.basePath, url);
|
|
576
|
-
if (query)
|
|
577
|
-
urlHelper.extendQuery(navUrl, query);
|
|
578
|
-
let isFirst = !this.__lastNav && !this.__execNav;
|
|
579
|
-
let action;
|
|
580
|
-
if (isFirst)
|
|
581
|
-
action = "first";
|
|
582
|
-
else {
|
|
583
|
-
const isChangedUrl = this.__lastNav?.context.url.toLowerCase() !== navUrl.url.toLowerCase();
|
|
584
|
-
const hasHash = !!navUrl.hash;
|
|
585
|
-
if (isChangedUrl)
|
|
586
|
-
action = "url-change"; // url changed
|
|
587
|
-
else
|
|
588
|
-
action = hasHash ? "hash" : "url-no-change";
|
|
589
|
-
}
|
|
590
|
-
const base = this.__beginNav(abort);
|
|
591
|
-
const context = {
|
|
592
|
-
...this.__createContext(base, navUrl, isFirst ? "first" : "nav", action, data, replace),
|
|
593
|
-
scope
|
|
594
|
-
};
|
|
595
|
-
const currentNav = { method: "navigate", context, abort: base.navAbort, status: "work" };
|
|
596
|
-
return await this.__execNavigate(currentNav, base.parentNav);
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Reload page with nav.
|
|
600
|
-
*/
|
|
601
|
-
reload() {
|
|
602
|
-
this.__abort.signal.throwIfAborted();
|
|
603
|
-
return this.nav({ replace: true });
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Global reload page in browser.
|
|
607
|
-
*/
|
|
608
|
-
restart() {
|
|
609
|
-
this.__abort.signal.throwIfAborted();
|
|
610
|
-
window.location.reload();
|
|
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
|
-
*/
|
|
617
|
-
async destroy(contextData) {
|
|
618
|
-
if (this.__abort.signal.aborted)
|
|
619
|
-
return Promise.reject(new Error('Application already destroyed.'));
|
|
620
|
-
this.__abort.abort();
|
|
621
|
-
console.info("app destroy begin");
|
|
622
|
-
if (this.__execNav)
|
|
623
|
-
this.__execNav.abort.abort();
|
|
624
|
-
if (this.__lastNav)
|
|
625
|
-
this.__lastNav.abort.abort();
|
|
626
|
-
if (this.__globalSubmit)
|
|
627
|
-
this.element?.removeEventListener("submit", this.__globalSubmit);
|
|
628
|
-
if (this.__onPopStateHandler)
|
|
629
|
-
window.removeEventListener("popstate", this.__onPopStateHandler);
|
|
630
|
-
const destroyAbort = new AbortController();
|
|
631
|
-
const context = {
|
|
632
|
-
abort: destroyAbort.signal,
|
|
633
|
-
app: this,
|
|
634
|
-
data: contextData || {}
|
|
635
|
-
};
|
|
636
|
-
try {
|
|
637
|
-
await this.invoker.invoke("stop", context);
|
|
638
|
-
console.info("app destroy success");
|
|
639
|
-
return context;
|
|
640
|
-
}
|
|
641
|
-
catch (reason) {
|
|
642
|
-
console.error(`app destroy error: ${reason}`);
|
|
643
|
-
throw reason;
|
|
644
|
-
}
|
|
645
|
-
finally {
|
|
646
|
-
super.destroy();
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Generate url of application base url.
|
|
651
|
-
* @param path Add optional path of base url.
|
|
652
|
-
* @param query Add optional query params.
|
|
653
|
-
* @param hash Add optional hash.
|
|
654
|
-
* @returns Relative url with base path.
|
|
655
|
-
*/
|
|
656
|
-
buildUrl(path, query, hash) {
|
|
657
|
-
this.__abort.signal.throwIfAborted();
|
|
658
|
-
return urlHelper.buildUrl(this.env.basePath, path, query, hash);
|
|
659
|
-
}
|
|
660
|
-
async __onSubmit(options) {
|
|
661
|
-
const opt = options instanceof HTMLFormElement ? { form: options } : options;
|
|
662
|
-
const { form, button = null, query, data = {} } = opt;
|
|
663
|
-
if ((!button || !button.formNoValidate) && !form.checkValidity())
|
|
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.');
|
|
669
|
-
let replace = form.hasAttribute(result.NavUrlReplaceAttributeName);
|
|
670
|
-
let method = form.method;
|
|
671
|
-
let enctype = form.enctype;
|
|
672
|
-
let url = form.action;
|
|
673
|
-
if (button) {
|
|
674
|
-
if (button.hasAttribute("formmethod"))
|
|
675
|
-
method = button.formMethod;
|
|
676
|
-
if (button.hasAttribute("formenctype"))
|
|
677
|
-
enctype = button.formEnctype;
|
|
678
|
-
if (button.hasAttribute("formaction"))
|
|
679
|
-
url = button.formAction;
|
|
680
|
-
button.classList.add(result.LoadingElementClass);
|
|
681
|
-
if (button.hasAttribute(result.NavUrlReplaceAttributeName))
|
|
682
|
-
replace = true;
|
|
683
|
-
}
|
|
684
|
-
form.classList.add(result.LoadingElementClass);
|
|
685
|
-
method = method.toUpperCase();
|
|
686
|
-
try {
|
|
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
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
const navUrl = urlHelper.parseUrl(this.env.basePath, url);
|
|
696
|
-
if (query)
|
|
697
|
-
urlHelper.extendQuery(navUrl, query);
|
|
698
|
-
const base = this.__beginNav(opt.abort);
|
|
699
|
-
const context = {
|
|
700
|
-
...this.__createContext(base, navUrl, "submit", "submit", data, replace),
|
|
701
|
-
form,
|
|
702
|
-
button,
|
|
703
|
-
method,
|
|
704
|
-
enctype
|
|
705
|
-
};
|
|
706
|
-
const currentNav = { method: "submit", context, abort: base.navAbort, status: "work" };
|
|
707
|
-
await this.__execNavigate(currentNav, base.parentNav);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
finally {
|
|
711
|
-
form.classList.remove(result.LoadingElementClass);
|
|
712
|
-
if (button)
|
|
713
|
-
button.classList.remove(result.LoadingElementClass);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
__onPopState(_context, event) {
|
|
717
|
-
const popUrl = location.href;
|
|
718
|
-
console.log(`popstate: ${popUrl}`, 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
|
-
};
|
|
766
|
-
}
|
|
767
|
-
async __execNavigate(nav, parent) {
|
|
768
|
-
if (parent)
|
|
769
|
-
parent.overide = nav;
|
|
770
|
-
try {
|
|
771
|
-
console.info(`${nav.method} begin`, nav.context);
|
|
772
|
-
nav.context.abort.throwIfAborted();
|
|
773
|
-
this.__execNav = nav;
|
|
774
|
-
await this.invoker.invoke(nav.method, nav.context);
|
|
775
|
-
this.__lastNav = nav;
|
|
776
|
-
nav.status = "success";
|
|
777
|
-
console.info(`${nav.method} ${nav.status} ${nav.context.url}`);
|
|
778
|
-
return nav.context;
|
|
779
|
-
}
|
|
780
|
-
catch (reason) {
|
|
781
|
-
if (reason?.name === "AbortError") {
|
|
782
|
-
nav.status = "cancelled";
|
|
783
|
-
console.warn(`${nav.method} ${nav.status} ${nav.context.url}`);
|
|
784
|
-
}
|
|
785
|
-
else if (reason === NAV_OVERIDE_ERROR) {
|
|
786
|
-
if (!nav.context.overided || !nav.overide)
|
|
787
|
-
throw new Error("Nav is not overided.");
|
|
788
|
-
nav.status = "overided";
|
|
789
|
-
console.warn(`${nav.method} ${nav.status} ${nav.context.url}`);
|
|
790
|
-
return nav.overide.context; // return redirected navigation
|
|
791
|
-
}
|
|
792
|
-
else {
|
|
793
|
-
nav.status = "error";
|
|
794
|
-
console.error(`${nav.method} ${nav.status} ${nav.context.url}: ${reason}`);
|
|
795
|
-
}
|
|
796
|
-
throw reason;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/** Fluent builder that configures the application type, middlewares and model, then constructs an {@link Application}. */
|
|
802
|
-
class ApplicationBuilder {
|
|
803
|
-
__model;
|
|
804
|
-
__appType = (Application);
|
|
805
|
-
__middlewares = [];
|
|
806
|
-
/**
|
|
807
|
-
* @param model Application model used when building the application.
|
|
808
|
-
*/
|
|
809
|
-
constructor(model) {
|
|
810
|
-
this.__model = model;
|
|
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
|
-
*/
|
|
817
|
-
useApp(appType) {
|
|
818
|
-
this.__appType = appType;
|
|
819
|
-
return this;
|
|
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
|
-
*/
|
|
827
|
-
useMiddleware(createFunc, ...params) {
|
|
828
|
-
let midl = createFunc(...params);
|
|
829
|
-
this.__middlewares.push(midl);
|
|
830
|
-
return this;
|
|
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
|
-
*/
|
|
838
|
-
build(env, ...args) {
|
|
839
|
-
const appEnv = { ...env };
|
|
840
|
-
if (!appEnv.basePath || appEnv.basePath == '/')
|
|
841
|
-
appEnv.basePath = '';
|
|
842
|
-
const app = new this.__appType(appEnv, this.__model, ...args);
|
|
843
|
-
app.initialize(this.__middlewares);
|
|
844
|
-
return app;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
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
|
-
}
|
|
871
|
-
|
|
872
|
-
export { APP_TYPENAME, Application, ApplicationBuilder, constants as CONSTANTS, NAV_OVERIDE_ERROR, url as UrlHelper };
|
|
1
|
+
export { ApplicationBuilder } from './builder.js';
|
|
2
|
+
export { APP_TYPENAME, Application, NAV_OVERIDE_ERROR } from './app.js';
|
|
3
|
+
export { enableNavExtensions } from './ext.js';
|
|
4
|
+
import * as url from './helpers/url.js';
|
|
5
|
+
export { url as UrlHelper };
|
|
6
|
+
import * as constants from './constants.js';
|
|
7
|
+
export { constants as CONSTANTS };
|
|
873
8
|
//# sourceMappingURL=index.js.map
|