@hybridly/core 0.0.1-dev.3 → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +387 -165
- package/dist/index.d.ts +240 -138
- package/dist/index.mjs +386 -165
- package/package.json +5 -6
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
const utils = require('@hybridly/utils');
|
|
5
6
|
const axios = require('axios');
|
|
6
7
|
const qs = require('qs');
|
|
7
|
-
const utils = require('@hybridly/utils');
|
|
8
8
|
|
|
9
9
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
|
|
10
10
|
|
|
@@ -12,10 +12,12 @@ const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
|
12
12
|
const qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
|
|
13
13
|
|
|
14
14
|
const STORAGE_EXTERNAL_KEY = "hybridly:external";
|
|
15
|
-
const HYBRIDLY_HEADER = "x-
|
|
16
|
-
const
|
|
15
|
+
const HYBRIDLY_HEADER = "x-hybrid";
|
|
16
|
+
const EXTERNAL_NAVIGATION_HEADER = `${HYBRIDLY_HEADER}-external`;
|
|
17
17
|
const PARTIAL_COMPONENT_HEADER = `${HYBRIDLY_HEADER}-partial-component`;
|
|
18
18
|
const ONLY_DATA_HEADER = `${HYBRIDLY_HEADER}-only-data`;
|
|
19
|
+
const DIALOG_KEY_HEADER = `${HYBRIDLY_HEADER}-dialog-key`;
|
|
20
|
+
const DIALOG_REDIRECT_HEADER = `${HYBRIDLY_HEADER}-dialog-redirect`;
|
|
19
21
|
const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
|
|
20
22
|
const CONTEXT_HEADER = `${HYBRIDLY_HEADER}-context`;
|
|
21
23
|
const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
|
|
@@ -26,9 +28,11 @@ const constants = {
|
|
|
26
28
|
__proto__: null,
|
|
27
29
|
STORAGE_EXTERNAL_KEY: STORAGE_EXTERNAL_KEY,
|
|
28
30
|
HYBRIDLY_HEADER: HYBRIDLY_HEADER,
|
|
29
|
-
|
|
31
|
+
EXTERNAL_NAVIGATION_HEADER: EXTERNAL_NAVIGATION_HEADER,
|
|
30
32
|
PARTIAL_COMPONENT_HEADER: PARTIAL_COMPONENT_HEADER,
|
|
31
33
|
ONLY_DATA_HEADER: ONLY_DATA_HEADER,
|
|
34
|
+
DIALOG_KEY_HEADER: DIALOG_KEY_HEADER,
|
|
35
|
+
DIALOG_REDIRECT_HEADER: DIALOG_REDIRECT_HEADER,
|
|
32
36
|
EXCEPT_DATA_HEADER: EXCEPT_DATA_HEADER,
|
|
33
37
|
CONTEXT_HEADER: CONTEXT_HEADER,
|
|
34
38
|
VERSION_HEADER: VERSION_HEADER,
|
|
@@ -36,13 +40,90 @@ const constants = {
|
|
|
36
40
|
SCROLL_REGION_ATTRIBUTE: SCROLL_REGION_ATTRIBUTE
|
|
37
41
|
};
|
|
38
42
|
|
|
39
|
-
class
|
|
43
|
+
class NotAHybridResponseError extends Error {
|
|
40
44
|
constructor(response) {
|
|
41
45
|
super();
|
|
42
46
|
this.response = response;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
class
|
|
49
|
+
class NavigationCancelledError extends Error {
|
|
50
|
+
}
|
|
51
|
+
class RoutingNotInitialized extends Error {
|
|
52
|
+
constructor() {
|
|
53
|
+
super("Routing is not initialized. Make sure the Vite plugin is enabled and that `php artisan route:list` returns no error.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
class RouteNotFound extends Error {
|
|
57
|
+
constructor(name) {
|
|
58
|
+
super(`Route [${name}] does not exist.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
class MissingRouteParameter extends Error {
|
|
62
|
+
constructor(parameter, routeName) {
|
|
63
|
+
super(`Parameter [${parameter}] is required for route [${routeName}].`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function definePlugin(plugin) {
|
|
68
|
+
return plugin;
|
|
69
|
+
}
|
|
70
|
+
async function forEachPlugin(cb) {
|
|
71
|
+
const { plugins } = getRouterContext();
|
|
72
|
+
for (const plugin of plugins) {
|
|
73
|
+
await cb(plugin);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function runPluginHooks(hook, ...args) {
|
|
77
|
+
let result = true;
|
|
78
|
+
await forEachPlugin(async (plugin) => {
|
|
79
|
+
if (plugin[hook]) {
|
|
80
|
+
utils.debug.plugin(plugin.name, `Calling "${hook}" hook.`);
|
|
81
|
+
result && (result = await plugin[hook]?.(...args) !== false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
async function runGlobalHooks(hook, ...args) {
|
|
87
|
+
const { hooks } = getRouterContext();
|
|
88
|
+
if (!hooks[hook]) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
let result = true;
|
|
92
|
+
for (const fn of hooks[hook]) {
|
|
93
|
+
utils.debug.hook(`Calling global "${hook}" hooks.`);
|
|
94
|
+
result = await fn(...args) ?? result;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
async function runHooks(hook, requestHooks, ...args) {
|
|
99
|
+
const result = await Promise.all([
|
|
100
|
+
requestHooks?.[hook]?.(...args),
|
|
101
|
+
runGlobalHooks(hook, ...args),
|
|
102
|
+
runPluginHooks(hook, ...args)
|
|
103
|
+
]);
|
|
104
|
+
utils.debug.hook(`Called all hooks for [${hook}],`, result);
|
|
105
|
+
return !result.includes(false);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function appendCallbackToHooks(hook, fn) {
|
|
109
|
+
const hooks = getRouterContext().hooks;
|
|
110
|
+
hooks[hook] = [...hooks[hook] ?? [], fn];
|
|
111
|
+
return () => {
|
|
112
|
+
const index = hooks[hook].indexOf(fn);
|
|
113
|
+
if (index !== -1) {
|
|
114
|
+
hooks[hook]?.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function registerHook(hook, fn, options) {
|
|
119
|
+
if (options?.once) {
|
|
120
|
+
const unregister = appendCallbackToHooks(hook, async (...args) => {
|
|
121
|
+
await fn(...args);
|
|
122
|
+
unregister();
|
|
123
|
+
});
|
|
124
|
+
return unregister;
|
|
125
|
+
}
|
|
126
|
+
return appendCallbackToHooks(hook, fn);
|
|
46
127
|
}
|
|
47
128
|
|
|
48
129
|
function saveScrollPositions() {
|
|
@@ -61,10 +142,10 @@ function getScrollRegions() {
|
|
|
61
142
|
}
|
|
62
143
|
function resetScrollPositions() {
|
|
63
144
|
utils.debug.scroll("Resetting scroll positions.");
|
|
64
|
-
getScrollRegions().concat(document.documentElement, document.body).forEach((element) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
145
|
+
getScrollRegions().concat(document.documentElement, document.body).forEach((element) => element.scrollTo({
|
|
146
|
+
top: 0,
|
|
147
|
+
left: 0
|
|
148
|
+
}));
|
|
68
149
|
saveScrollPositions();
|
|
69
150
|
if (window.location.hash) {
|
|
70
151
|
utils.debug.scroll(`Hash is present, scrolling to the element of ID ${window.location.hash}.`);
|
|
@@ -76,10 +157,14 @@ async function restoreScrollPositions() {
|
|
|
76
157
|
const context = getRouterContext();
|
|
77
158
|
const regions = getScrollRegions();
|
|
78
159
|
if (!context.scrollRegions) {
|
|
160
|
+
utils.debug.scroll("No region found to restore.");
|
|
79
161
|
return;
|
|
80
162
|
}
|
|
81
163
|
let tries = 0;
|
|
82
|
-
const timer = setInterval(
|
|
164
|
+
const timer = setInterval(restore, 50);
|
|
165
|
+
restore();
|
|
166
|
+
function restore() {
|
|
167
|
+
utils.debug.scroll(`Restoring ${regions.length}/${context.scrollRegions.length} region(s).`);
|
|
83
168
|
if (context.scrollRegions.length !== regions.length) {
|
|
84
169
|
if (++tries > 20) {
|
|
85
170
|
utils.debug.scroll("The limit of tries has been reached. Cancelling scroll restoration.");
|
|
@@ -94,17 +179,31 @@ async function restoreScrollPositions() {
|
|
|
94
179
|
top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
|
|
95
180
|
left: context.scrollRegions.at(i)?.top ?? el.scrollLeft
|
|
96
181
|
}));
|
|
97
|
-
}
|
|
182
|
+
}
|
|
98
183
|
}
|
|
99
184
|
|
|
100
|
-
function normalizeUrl(href) {
|
|
101
|
-
return makeUrl(href).toString();
|
|
185
|
+
function normalizeUrl(href, trailingSlash) {
|
|
186
|
+
return makeUrl(href, { trailingSlash }).toString();
|
|
102
187
|
}
|
|
103
188
|
function makeUrl(href, transformations = {}) {
|
|
104
189
|
try {
|
|
105
190
|
const base = document?.location?.href === "//" ? void 0 : document.location.href;
|
|
106
191
|
const url = new URL(String(href), base);
|
|
107
|
-
|
|
192
|
+
transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
|
|
193
|
+
Object.entries(transformations).forEach(([key, value]) => {
|
|
194
|
+
if (key === "query") {
|
|
195
|
+
key = "search";
|
|
196
|
+
value = qs__default.stringify(utils.merge(qs__default.parse(url.search, { ignoreQueryPrefix: true }), value), {
|
|
197
|
+
encodeValuesOnly: true,
|
|
198
|
+
arrayFormat: "brackets"
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
Reflect.set(url, key, value);
|
|
202
|
+
});
|
|
203
|
+
if (transformations.trailingSlash === false) {
|
|
204
|
+
const _url = utils.removeTrailingSlash(url.toString().replace(/\/\?/, "?"));
|
|
205
|
+
url.toString = () => _url;
|
|
206
|
+
}
|
|
108
207
|
return url;
|
|
109
208
|
} catch (error) {
|
|
110
209
|
throw new TypeError(`${href} is not resolvable to a valid URL.`);
|
|
@@ -159,6 +258,11 @@ async function registerEventListeners() {
|
|
|
159
258
|
utils.debug.history("Registering [popstate] and [scroll] event listeners.");
|
|
160
259
|
window?.addEventListener("popstate", async (event) => {
|
|
161
260
|
utils.debug.history("Navigation detected (popstate event). State:", { state: event.state });
|
|
261
|
+
if (context.pendingNavigation) {
|
|
262
|
+
utils.debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
263
|
+
context.pendingNavigation?.controller?.abort();
|
|
264
|
+
}
|
|
265
|
+
await runHooks("backForward", {}, event.state, context);
|
|
162
266
|
if (!event.state) {
|
|
163
267
|
utils.debug.history("There is no state. Adding hash if any and restoring scroll positions.");
|
|
164
268
|
return await navigate({
|
|
@@ -174,7 +278,7 @@ async function registerEventListeners() {
|
|
|
174
278
|
await navigate({
|
|
175
279
|
payload: event.state,
|
|
176
280
|
preserveScroll: true,
|
|
177
|
-
preserveState:
|
|
281
|
+
preserveState: !!getInternalRouterContext().dialog || !!event.state.dialog,
|
|
178
282
|
updateHistoryState: false,
|
|
179
283
|
isBackForward: true
|
|
180
284
|
});
|
|
@@ -185,18 +289,21 @@ async function registerEventListeners() {
|
|
|
185
289
|
}
|
|
186
290
|
}, 100), true);
|
|
187
291
|
}
|
|
188
|
-
function
|
|
292
|
+
function isBackForwardNavigation() {
|
|
189
293
|
if (!window.history.state) {
|
|
190
294
|
return false;
|
|
191
295
|
}
|
|
192
296
|
return window.performance?.getEntriesByType("navigation").at(0)?.type === "back_forward";
|
|
193
297
|
}
|
|
194
|
-
async function
|
|
195
|
-
utils.debug.router("Handling a back/forward
|
|
298
|
+
async function handleBackForwardNavigation() {
|
|
299
|
+
utils.debug.router("Handling a back/forward navigation.");
|
|
196
300
|
window.history.state.version = getRouterContext().version;
|
|
197
301
|
await navigate({
|
|
302
|
+
payload: window.history.state,
|
|
198
303
|
preserveScroll: true,
|
|
199
|
-
preserveState:
|
|
304
|
+
preserveState: false,
|
|
305
|
+
updateHistoryState: false,
|
|
306
|
+
isBackForward: true
|
|
200
307
|
});
|
|
201
308
|
}
|
|
202
309
|
function remember(key, value) {
|
|
@@ -232,6 +339,118 @@ function createSerializer(options) {
|
|
|
232
339
|
};
|
|
233
340
|
}
|
|
234
341
|
|
|
342
|
+
function generateRouteFromName(name, parameters, absolute, shouldThrow) {
|
|
343
|
+
const url = getUrlFromName(name, parameters, shouldThrow);
|
|
344
|
+
return absolute === false ? url.toString().replace(url.origin, "") : url.toString();
|
|
345
|
+
}
|
|
346
|
+
function getUrlFromName(name, parameters, shouldThrow) {
|
|
347
|
+
const routing = getRouting();
|
|
348
|
+
const definition = getRouteDefinition(name);
|
|
349
|
+
const transforms = getRouteTransformable(name, parameters, shouldThrow);
|
|
350
|
+
const url = makeUrl(routing.url, (url2) => ({
|
|
351
|
+
hostname: definition.domain || url2.hostname,
|
|
352
|
+
port: routing.port?.toString() || url2.port,
|
|
353
|
+
trailingSlash: false,
|
|
354
|
+
...transforms
|
|
355
|
+
}));
|
|
356
|
+
return url;
|
|
357
|
+
}
|
|
358
|
+
function getRouteTransformable(routeName, routeParameters, shouldThrow) {
|
|
359
|
+
const routing = getRouting();
|
|
360
|
+
const definition = getRouteDefinition(routeName);
|
|
361
|
+
const parameters = routeParameters || {};
|
|
362
|
+
const missing = Object.keys(parameters);
|
|
363
|
+
const replaceParameter = (match, parameterName) => {
|
|
364
|
+
const optional = /\?}$/.test(match);
|
|
365
|
+
const value = (() => {
|
|
366
|
+
const value2 = parameters[parameterName];
|
|
367
|
+
const bindingProperty = definition.bindings?.[parameterName];
|
|
368
|
+
if (bindingProperty && typeof value2 === "object") {
|
|
369
|
+
return value2[bindingProperty];
|
|
370
|
+
}
|
|
371
|
+
return value2;
|
|
372
|
+
})();
|
|
373
|
+
missing.splice(missing.indexOf(parameterName), 1);
|
|
374
|
+
if (value) {
|
|
375
|
+
const where = definition.wheres?.[parameterName];
|
|
376
|
+
if (where && !new RegExp(where).test(value)) {
|
|
377
|
+
console.warn(`[hybridly:routing] Parameter [${parameterName}] does not match the required format [${where}] for route [${routeName}].`);
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
if (routing.defaults?.[parameterName]) {
|
|
382
|
+
return routing.defaults?.[parameterName];
|
|
383
|
+
}
|
|
384
|
+
if (optional) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
if (shouldThrow === false) {
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
throw new MissingRouteParameter(parameterName, routeName);
|
|
391
|
+
};
|
|
392
|
+
const path = definition.uri.replace(/{([^}?]+)\??}/g, replaceParameter);
|
|
393
|
+
const domain = definition.domain?.replace(/{([^}?]+)\??}/g, replaceParameter);
|
|
394
|
+
const remaining = Object.keys(parameters).filter((key) => missing.includes(key)).reduce((obj, key) => ({
|
|
395
|
+
...obj,
|
|
396
|
+
[key]: parameters[key]
|
|
397
|
+
}), {});
|
|
398
|
+
return {
|
|
399
|
+
...domain && { hostname: domain },
|
|
400
|
+
pathname: path,
|
|
401
|
+
search: qs__default.stringify(remaining, {
|
|
402
|
+
encodeValuesOnly: true,
|
|
403
|
+
arrayFormat: "indices",
|
|
404
|
+
addQueryPrefix: true
|
|
405
|
+
})
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function getRouteDefinition(name) {
|
|
409
|
+
const routing = getRouting();
|
|
410
|
+
const definition = routing.routes[name];
|
|
411
|
+
if (!definition) {
|
|
412
|
+
throw new RouteNotFound(name);
|
|
413
|
+
}
|
|
414
|
+
return definition;
|
|
415
|
+
}
|
|
416
|
+
function getRouting() {
|
|
417
|
+
const { routing } = getInternalRouterContext();
|
|
418
|
+
if (!routing) {
|
|
419
|
+
throw new RoutingNotInitialized();
|
|
420
|
+
}
|
|
421
|
+
return routing;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isCurrentFromName(name, parameters, mode = "loose") {
|
|
425
|
+
const location = window.location;
|
|
426
|
+
const matchee = (() => {
|
|
427
|
+
try {
|
|
428
|
+
return makeUrl(generateRouteFromName(name, parameters, true, false));
|
|
429
|
+
} catch (error) {
|
|
430
|
+
}
|
|
431
|
+
})();
|
|
432
|
+
if (!matchee) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
if (mode === "strict") {
|
|
436
|
+
return location.href === matchee.href;
|
|
437
|
+
}
|
|
438
|
+
return location.href.startsWith(matchee.href);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function route(name, parameters, absolute) {
|
|
442
|
+
return generateRouteFromName(name, parameters, absolute);
|
|
443
|
+
}
|
|
444
|
+
function current(name, parameters, mode = "loose") {
|
|
445
|
+
return isCurrentFromName(name, parameters, mode);
|
|
446
|
+
}
|
|
447
|
+
function updateRoutingConfiguration(routing) {
|
|
448
|
+
if (!routing) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
setContext({ routing });
|
|
452
|
+
}
|
|
453
|
+
|
|
235
454
|
const state = {
|
|
236
455
|
initialized: false,
|
|
237
456
|
context: {}
|
|
@@ -249,18 +468,21 @@ async function initializeContext(options) {
|
|
|
249
468
|
state.initialized = true;
|
|
250
469
|
state.context = {
|
|
251
470
|
...options.payload,
|
|
471
|
+
responseErrorModals: options.responseErrorModals,
|
|
252
472
|
serializer: createSerializer(options),
|
|
253
473
|
url: makeUrl(options.payload.url).toString(),
|
|
254
|
-
adapter:
|
|
474
|
+
adapter: {
|
|
475
|
+
...options.adapter,
|
|
476
|
+
updateRoutingConfiguration
|
|
477
|
+
},
|
|
255
478
|
scrollRegions: [],
|
|
256
479
|
plugins: options.plugins ?? [],
|
|
480
|
+
axios: options.axios ?? axios__default.create(),
|
|
481
|
+
routing: options.routing,
|
|
257
482
|
hooks: {},
|
|
258
483
|
state: {}
|
|
259
484
|
};
|
|
260
|
-
|
|
261
|
-
utils.debug.plugin(plugin.name, 'Calling "initialized" hook.');
|
|
262
|
-
await plugin.initialized?.(state.context);
|
|
263
|
-
}
|
|
485
|
+
await runHooks("initialized", {}, state.context);
|
|
264
486
|
return getInternalRouterContext();
|
|
265
487
|
}
|
|
266
488
|
function setContext(merge = {}, options = {}) {
|
|
@@ -268,7 +490,7 @@ function setContext(merge = {}, options = {}) {
|
|
|
268
490
|
Reflect.set(state.context, key, merge[key]);
|
|
269
491
|
});
|
|
270
492
|
if (options.propagate !== false) {
|
|
271
|
-
state.context.adapter.
|
|
493
|
+
state.context.adapter.onContextUpdate?.(state.context);
|
|
272
494
|
}
|
|
273
495
|
utils.debug.context("Updated context:", { context: state.context, added: merge });
|
|
274
496
|
}
|
|
@@ -281,8 +503,8 @@ function payloadFromContext() {
|
|
|
281
503
|
};
|
|
282
504
|
}
|
|
283
505
|
|
|
284
|
-
async function
|
|
285
|
-
utils.debug.external("
|
|
506
|
+
async function performExternalNavigation(options) {
|
|
507
|
+
utils.debug.external("Navigating to an external URL:", options);
|
|
286
508
|
window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
|
|
287
509
|
window.location.href = options.url;
|
|
288
510
|
if (sameUrls(window.location, options.url)) {
|
|
@@ -290,11 +512,19 @@ async function performExternalVisit(options) {
|
|
|
290
512
|
window.location.reload();
|
|
291
513
|
}
|
|
292
514
|
}
|
|
515
|
+
function navigateToExternalUrl(url, data) {
|
|
516
|
+
document.location.href = makeUrl(url, {
|
|
517
|
+
search: qs__default.stringify(data, {
|
|
518
|
+
encodeValuesOnly: true,
|
|
519
|
+
arrayFormat: "brackets"
|
|
520
|
+
})
|
|
521
|
+
}).toString();
|
|
522
|
+
}
|
|
293
523
|
function isExternalResponse(response) {
|
|
294
|
-
return response?.status === 409 && !!response?.headers?.[
|
|
524
|
+
return response?.status === 409 && !!response?.headers?.[EXTERNAL_NAVIGATION_HEADER];
|
|
295
525
|
}
|
|
296
|
-
async function
|
|
297
|
-
utils.debug.external("Handling an external
|
|
526
|
+
async function handleExternalNavigation() {
|
|
527
|
+
utils.debug.external("Handling an external navigation.");
|
|
298
528
|
const options = JSON.parse(window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) || "{}");
|
|
299
529
|
window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
|
|
300
530
|
utils.debug.external("Options from the session storage:", options);
|
|
@@ -306,7 +536,7 @@ async function handleExternalVisit() {
|
|
|
306
536
|
preserveState: true
|
|
307
537
|
});
|
|
308
538
|
}
|
|
309
|
-
function
|
|
539
|
+
function isExternalNavigation() {
|
|
310
540
|
try {
|
|
311
541
|
return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
|
|
312
542
|
} catch {
|
|
@@ -314,65 +544,41 @@ function isExternalVisit() {
|
|
|
314
544
|
return false;
|
|
315
545
|
}
|
|
316
546
|
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
let result = true;
|
|
323
|
-
for (const plugin of plugins) {
|
|
324
|
-
if (plugin.hooks[hook]) {
|
|
325
|
-
utils.debug.plugin(plugin.name, `Calling "${hook}" hooks.`);
|
|
326
|
-
result = await plugin.hooks[hook]?.(...args) ?? result;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return result;
|
|
330
|
-
}
|
|
331
|
-
async function runGlobalHooks(hook, ...args) {
|
|
332
|
-
const { hooks } = getRouterContext();
|
|
333
|
-
if (!hooks[hook]) {
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
let result = true;
|
|
337
|
-
for (const fn of hooks[hook]) {
|
|
338
|
-
utils.debug.hook(`Calling global "${hook}" hooks.`);
|
|
339
|
-
result = await fn(...args) ?? result;
|
|
547
|
+
async function closeDialog(options) {
|
|
548
|
+
const context = getInternalRouterContext();
|
|
549
|
+
const url = context.dialog?.redirectUrl ?? context.dialog?.baseUrl;
|
|
550
|
+
if (!url) {
|
|
551
|
+
return;
|
|
340
552
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
runPluginHooks(hook, ...args)
|
|
348
|
-
]);
|
|
349
|
-
return !result.includes(false);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function registerHook(hook, fn) {
|
|
353
|
-
const hooks = getRouterContext().hooks;
|
|
354
|
-
hooks[hook] = [...hooks[hook] ?? [], fn];
|
|
355
|
-
return () => hooks[hook]?.splice(hooks[hook].indexOf(fn), 1);
|
|
356
|
-
}
|
|
357
|
-
function registerHookOnce(hook, fn) {
|
|
358
|
-
const unregister = registerHook(hook, async (...args) => {
|
|
359
|
-
await fn(...args);
|
|
360
|
-
unregister();
|
|
553
|
+
context.adapter.onDialogClose?.(context);
|
|
554
|
+
return await performHybridNavigation({
|
|
555
|
+
url,
|
|
556
|
+
preserveScroll: true,
|
|
557
|
+
preserveState: true,
|
|
558
|
+
...options
|
|
361
559
|
});
|
|
362
560
|
}
|
|
363
561
|
|
|
364
562
|
const router = {
|
|
365
|
-
abort: async () => getRouterContext().
|
|
366
|
-
active: () => !!getRouterContext().
|
|
367
|
-
|
|
368
|
-
reload: async (options) => await
|
|
369
|
-
get: async (url, options = {}) => await
|
|
370
|
-
post: async (url, options = {}) => await
|
|
371
|
-
put: async (url, options = {}) => await
|
|
372
|
-
patch: async (url, options = {}) => await
|
|
373
|
-
delete: async (url, options = {}) => await
|
|
374
|
-
local: async (url, options) => await
|
|
375
|
-
external: (url, data = {}) =>
|
|
563
|
+
abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
|
|
564
|
+
active: () => !!getRouterContext().pendingNavigation,
|
|
565
|
+
navigate: async (options) => await performHybridNavigation(options),
|
|
566
|
+
reload: async (options) => await performHybridNavigation({ preserveScroll: true, preserveState: true, ...options }),
|
|
567
|
+
get: async (url, options = {}) => await performHybridNavigation({ ...options, url, method: "GET" }),
|
|
568
|
+
post: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "POST" }),
|
|
569
|
+
put: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PUT" }),
|
|
570
|
+
patch: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PATCH" }),
|
|
571
|
+
delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
|
|
572
|
+
local: async (url, options) => await performLocalNavigation(url, options),
|
|
573
|
+
external: (url, data = {}) => navigateToExternalUrl(url, data),
|
|
574
|
+
to: async (name, parameters, options) => {
|
|
575
|
+
const url = generateRouteFromName(name, parameters);
|
|
576
|
+
const method = getRouteDefinition(name).method.at(0);
|
|
577
|
+
return await performHybridNavigation({ url, ...options, method });
|
|
578
|
+
},
|
|
579
|
+
dialog: {
|
|
580
|
+
close: (options) => closeDialog(options)
|
|
581
|
+
},
|
|
376
582
|
history: {
|
|
377
583
|
get: (key) => getKeyFromHistory(key),
|
|
378
584
|
remember: (key, value) => remember(key, value)
|
|
@@ -382,47 +588,69 @@ async function createRouter(options) {
|
|
|
382
588
|
await initializeContext(options);
|
|
383
589
|
return await initializeRouter();
|
|
384
590
|
}
|
|
385
|
-
async function
|
|
386
|
-
const
|
|
591
|
+
async function performHybridNavigation(options) {
|
|
592
|
+
const navigationId = utils.random();
|
|
387
593
|
const context = getRouterContext();
|
|
388
|
-
utils.debug.router("Making a
|
|
594
|
+
utils.debug.router("Making a hybrid navigation:", { context, options, navigationId });
|
|
389
595
|
try {
|
|
596
|
+
if (!options.method) {
|
|
597
|
+
utils.debug.router("Setting method to GET because none was provided.");
|
|
598
|
+
options.method = "GET";
|
|
599
|
+
}
|
|
600
|
+
options.method = options.method.toUpperCase();
|
|
390
601
|
if ((utils.hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
|
|
391
602
|
options.data = utils.objectToFormData(options.data);
|
|
392
603
|
utils.debug.router("Converted data to FormData.", options.data);
|
|
604
|
+
if (options.method && ["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
|
|
605
|
+
utils.debug.router(`Automatically spoofing method ${options.method}.`);
|
|
606
|
+
options.data.append("_method", options.method);
|
|
607
|
+
options.method = "POST";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
|
|
611
|
+
utils.debug.router("Transforming data to query parameters.", options.data);
|
|
612
|
+
options.url = makeUrl(options.url ?? context.url, {
|
|
613
|
+
query: options.data
|
|
614
|
+
});
|
|
615
|
+
options.data = {};
|
|
393
616
|
}
|
|
394
|
-
if (!await runHooks("before", options.hooks, options)) {
|
|
395
|
-
utils.debug.router('"before" event returned false, aborting the
|
|
396
|
-
throw new
|
|
617
|
+
if (!await runHooks("before", options.hooks, options, context)) {
|
|
618
|
+
utils.debug.router('"before" event returned false, aborting the navigation.');
|
|
619
|
+
throw new NavigationCancelledError('The navigation was cancelled by the "before" event.');
|
|
397
620
|
}
|
|
398
|
-
if (context.
|
|
399
|
-
utils.debug.router("Aborting current
|
|
400
|
-
context.
|
|
621
|
+
if (context.pendingNavigation) {
|
|
622
|
+
utils.debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
623
|
+
context.pendingNavigation?.controller?.abort();
|
|
401
624
|
}
|
|
402
625
|
saveScrollPositions();
|
|
403
626
|
if (options.url && options.transformUrl) {
|
|
404
627
|
options.url = makeUrl(options.url, options.transformUrl);
|
|
405
628
|
}
|
|
629
|
+
const targetUrl = makeUrl(options.url ?? context.url);
|
|
630
|
+
const abortController = new AbortController();
|
|
406
631
|
setContext({
|
|
407
|
-
|
|
408
|
-
id:
|
|
409
|
-
url:
|
|
410
|
-
controller:
|
|
632
|
+
pendingNavigation: {
|
|
633
|
+
id: navigationId,
|
|
634
|
+
url: targetUrl,
|
|
635
|
+
controller: abortController,
|
|
636
|
+
status: "pending",
|
|
411
637
|
options
|
|
412
638
|
}
|
|
413
639
|
});
|
|
414
640
|
await runHooks("start", options.hooks, context);
|
|
415
641
|
utils.debug.router("Making request with axios.");
|
|
416
|
-
const response = await
|
|
417
|
-
url:
|
|
418
|
-
method: options.method
|
|
642
|
+
const response = await context.axios.request({
|
|
643
|
+
url: targetUrl.toString(),
|
|
644
|
+
method: options.method,
|
|
419
645
|
data: options.method === "GET" ? {} : options.data,
|
|
420
646
|
params: options.method === "GET" ? options.data : {},
|
|
421
|
-
signal:
|
|
647
|
+
signal: abortController.signal,
|
|
422
648
|
headers: {
|
|
423
649
|
...options.headers,
|
|
650
|
+
...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
|
|
651
|
+
...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
|
|
424
652
|
...utils.when(options.only !== void 0 || options.except !== void 0, {
|
|
425
|
-
[PARTIAL_COMPONENT_HEADER]: context.view.
|
|
653
|
+
[PARTIAL_COMPONENT_HEADER]: context.view.component,
|
|
426
654
|
...utils.when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
|
|
427
655
|
...utils.when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
|
|
428
656
|
}, {}),
|
|
@@ -437,24 +665,24 @@ async function visit(options) {
|
|
|
437
665
|
await runHooks("progress", options.hooks, {
|
|
438
666
|
event,
|
|
439
667
|
percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
|
|
440
|
-
});
|
|
668
|
+
}, context);
|
|
441
669
|
}
|
|
442
670
|
});
|
|
443
|
-
await runHooks("data", options.hooks, response);
|
|
671
|
+
await runHooks("data", options.hooks, response, context);
|
|
444
672
|
if (isExternalResponse(response)) {
|
|
445
673
|
utils.debug.router("The response is explicitely external.");
|
|
446
|
-
await
|
|
447
|
-
url: fillHash(
|
|
674
|
+
await performExternalNavigation({
|
|
675
|
+
url: fillHash(targetUrl, response.headers[EXTERNAL_NAVIGATION_HEADER]),
|
|
448
676
|
preserveScroll: options.preserveScroll === true
|
|
449
677
|
});
|
|
450
678
|
return { response };
|
|
451
679
|
}
|
|
452
|
-
if (!
|
|
453
|
-
throw new
|
|
680
|
+
if (!isHybridResponse(response)) {
|
|
681
|
+
throw new NotAHybridResponseError(response);
|
|
454
682
|
}
|
|
455
|
-
utils.debug.router("The response respects the
|
|
683
|
+
utils.debug.router("The response respects the Hybridly protocol.");
|
|
456
684
|
const payload = response.data;
|
|
457
|
-
if ((options.only?.length ?? options.except?.length) && payload.view.
|
|
685
|
+
if ((options.only?.length ?? options.except?.length) && payload.view.component === context.view.component) {
|
|
458
686
|
utils.debug.router(`Merging ${options.only ? '"only"' : '"except"'} properties.`, payload.view.properties);
|
|
459
687
|
payload.view.properties = utils.merge(context.view.properties, payload.view.properties);
|
|
460
688
|
utils.debug.router("Merged properties:", payload.view.properties);
|
|
@@ -462,7 +690,7 @@ async function visit(options) {
|
|
|
462
690
|
await navigate({
|
|
463
691
|
payload: {
|
|
464
692
|
...payload,
|
|
465
|
-
url: fillHash(
|
|
693
|
+
url: fillHash(targetUrl, payload.url)
|
|
466
694
|
},
|
|
467
695
|
preserveScroll: options.preserveScroll === true,
|
|
468
696
|
preserveState: options.preserveState,
|
|
@@ -477,45 +705,50 @@ async function visit(options) {
|
|
|
477
705
|
return context.view.properties.errors;
|
|
478
706
|
})();
|
|
479
707
|
utils.debug.router("The request returned validation errors.", errors);
|
|
480
|
-
await runHooks("error", options.hooks, errors);
|
|
481
708
|
setContext({
|
|
482
|
-
|
|
483
|
-
...context.
|
|
709
|
+
pendingNavigation: {
|
|
710
|
+
...context.pendingNavigation,
|
|
484
711
|
status: "error"
|
|
485
712
|
}
|
|
486
713
|
});
|
|
714
|
+
await runHooks("error", options.hooks, errors, context);
|
|
487
715
|
} else {
|
|
488
|
-
await runHooks("success", options.hooks, payload);
|
|
489
716
|
setContext({
|
|
490
|
-
|
|
491
|
-
...context.
|
|
717
|
+
pendingNavigation: {
|
|
718
|
+
...context.pendingNavigation,
|
|
492
719
|
status: "success"
|
|
493
720
|
}
|
|
494
721
|
});
|
|
722
|
+
await runHooks("success", options.hooks, payload, context);
|
|
495
723
|
}
|
|
496
724
|
return { response };
|
|
497
725
|
} catch (error) {
|
|
498
726
|
await utils.match(error.constructor.name, {
|
|
499
|
-
|
|
727
|
+
NavigationCancelledError: async () => {
|
|
500
728
|
utils.debug.router('The request was cancelled through the "before" hook.', error);
|
|
501
|
-
console.warn(error);
|
|
502
729
|
await runHooks("abort", options.hooks, context);
|
|
503
730
|
},
|
|
504
731
|
AbortError: async () => {
|
|
505
|
-
utils.debug.router("The request was
|
|
506
|
-
console.warn(error);
|
|
732
|
+
utils.debug.router("The request was aborted.", error);
|
|
507
733
|
await runHooks("abort", options.hooks, context);
|
|
508
734
|
},
|
|
509
|
-
|
|
735
|
+
NotAHybridResponseError: async () => {
|
|
510
736
|
utils.debug.router("The request was not hybridly.");
|
|
511
737
|
console.error(error);
|
|
512
|
-
await runHooks("invalid", options.hooks, error);
|
|
513
|
-
|
|
738
|
+
await runHooks("invalid", options.hooks, error, context);
|
|
739
|
+
if (context.responseErrorModals) {
|
|
740
|
+
utils.showResponseErrorModal(error.response.data);
|
|
741
|
+
}
|
|
514
742
|
},
|
|
515
743
|
default: async () => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
744
|
+
if (error?.name === "CanceledError") {
|
|
745
|
+
utils.debug.router("The request was cancelled.", error);
|
|
746
|
+
await runHooks("abort", options.hooks, context);
|
|
747
|
+
} else {
|
|
748
|
+
utils.debug.router("An unknown error occured.", error);
|
|
749
|
+
console.error(error);
|
|
750
|
+
await runHooks("exception", options.hooks, error, context);
|
|
751
|
+
}
|
|
519
752
|
}
|
|
520
753
|
});
|
|
521
754
|
await runHooks("fail", options.hooks, context);
|
|
@@ -526,26 +759,28 @@ async function visit(options) {
|
|
|
526
759
|
}
|
|
527
760
|
};
|
|
528
761
|
} finally {
|
|
529
|
-
utils.debug.router("Ending
|
|
762
|
+
utils.debug.router("Ending navigation.");
|
|
530
763
|
await runHooks("after", options.hooks, context);
|
|
531
|
-
if (context.
|
|
532
|
-
setContext({
|
|
764
|
+
if (context.pendingNavigation?.id === navigationId) {
|
|
765
|
+
setContext({ pendingNavigation: void 0 });
|
|
533
766
|
}
|
|
534
767
|
}
|
|
535
768
|
}
|
|
536
|
-
function
|
|
769
|
+
function isHybridResponse(response) {
|
|
537
770
|
return !!response?.headers[HYBRIDLY_HEADER];
|
|
538
771
|
}
|
|
539
772
|
async function navigate(options) {
|
|
540
773
|
const context = getRouterContext();
|
|
541
774
|
utils.debug.router("Making an internal navigation:", { context, options });
|
|
775
|
+
await runHooks("navigating", {}, options, context);
|
|
542
776
|
options.payload ?? (options.payload = payloadFromContext());
|
|
543
777
|
const evaluateConditionalOption = (option) => typeof option === "function" ? option(options.payload) : option;
|
|
544
778
|
const shouldPreserveState = evaluateConditionalOption(options.preserveState);
|
|
545
779
|
const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
|
|
546
780
|
const shouldReplaceHistory = evaluateConditionalOption(options.replace);
|
|
547
781
|
const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
|
|
548
|
-
if (shouldPreserveState && getHistoryState() && options.payload.view.
|
|
782
|
+
if (shouldPreserveState && getHistoryState() && options.payload.view.component === context.view.component) {
|
|
783
|
+
utils.debug.history("Setting the history from this entry into the context.");
|
|
549
784
|
setContext({ state: getHistoryState() });
|
|
550
785
|
}
|
|
551
786
|
if (shouldReplaceUrl) {
|
|
@@ -555,41 +790,34 @@ async function navigate(options) {
|
|
|
555
790
|
setContext({
|
|
556
791
|
...options.payload,
|
|
557
792
|
state: {}
|
|
558
|
-
}
|
|
793
|
+
});
|
|
559
794
|
if (options.updateHistoryState !== false) {
|
|
560
795
|
utils.debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
|
|
561
796
|
setHistoryState({ replace: shouldReplaceHistory });
|
|
562
797
|
}
|
|
563
|
-
const viewComponent = await context.adapter.resolveComponent(context.view.
|
|
564
|
-
utils.debug.router(`Component [${context.view.
|
|
565
|
-
await context.adapter.
|
|
798
|
+
const viewComponent = await context.adapter.resolveComponent(context.view.component);
|
|
799
|
+
utils.debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
|
|
800
|
+
await context.adapter.onViewSwap({
|
|
566
801
|
component: viewComponent,
|
|
802
|
+
dialog: context.dialog,
|
|
803
|
+
properties: options.payload?.view.properties,
|
|
567
804
|
preserveState: shouldPreserveState
|
|
568
805
|
});
|
|
569
|
-
if (context.dialog) {
|
|
570
|
-
const dialogComponent = await context.adapter.resolveComponent(context.dialog.name);
|
|
571
|
-
utils.debug.router(`Dialog [${context.view.name}] resolved to:`, dialogComponent);
|
|
572
|
-
await context.adapter.swapDialog({
|
|
573
|
-
component: dialogComponent,
|
|
574
|
-
preserveState: shouldPreserveState
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
setContext();
|
|
578
806
|
if (!shouldPreserveScroll) {
|
|
579
807
|
resetScrollPositions();
|
|
580
808
|
} else {
|
|
581
809
|
restoreScrollPositions();
|
|
582
810
|
}
|
|
583
|
-
await runHooks("
|
|
811
|
+
await runHooks("navigated", {}, options, context);
|
|
584
812
|
}
|
|
585
813
|
async function initializeRouter() {
|
|
586
814
|
const context = getRouterContext();
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
} else if (
|
|
590
|
-
|
|
815
|
+
if (isBackForwardNavigation()) {
|
|
816
|
+
handleBackForwardNavigation();
|
|
817
|
+
} else if (isExternalNavigation()) {
|
|
818
|
+
handleExternalNavigation();
|
|
591
819
|
} else {
|
|
592
|
-
utils.debug.router("Handling the initial page
|
|
820
|
+
utils.debug.router("Handling the initial page navigation.");
|
|
593
821
|
setContext({
|
|
594
822
|
url: makeUrl(context.url, { hash: window.location.hash }).toString()
|
|
595
823
|
});
|
|
@@ -599,9 +827,10 @@ async function initializeRouter() {
|
|
|
599
827
|
});
|
|
600
828
|
}
|
|
601
829
|
registerEventListeners();
|
|
830
|
+
await runHooks("ready", {}, context);
|
|
602
831
|
return context;
|
|
603
832
|
}
|
|
604
|
-
async function
|
|
833
|
+
async function performLocalNavigation(targetUrl, options) {
|
|
605
834
|
const context = getRouterContext();
|
|
606
835
|
const url = normalizeUrl(targetUrl);
|
|
607
836
|
return await navigate({
|
|
@@ -611,20 +840,12 @@ async function performLocalComponentVisit(targetUrl, options) {
|
|
|
611
840
|
dialog: context.dialog,
|
|
612
841
|
url,
|
|
613
842
|
view: {
|
|
614
|
-
|
|
615
|
-
properties: options.properties
|
|
843
|
+
component: options.component ?? context.view.component,
|
|
844
|
+
properties: options.properties ?? {}
|
|
616
845
|
}
|
|
617
846
|
}
|
|
618
847
|
});
|
|
619
848
|
}
|
|
620
|
-
function performLocalExternalVisit(url, data) {
|
|
621
|
-
document.location.href = makeUrl(url, {
|
|
622
|
-
search: qs__default.stringify(data, {
|
|
623
|
-
encodeValuesOnly: true,
|
|
624
|
-
arrayFormat: "brackets"
|
|
625
|
-
})
|
|
626
|
-
}).toString();
|
|
627
|
-
}
|
|
628
849
|
|
|
629
850
|
function can(resource, action) {
|
|
630
851
|
return resource.authorization?.[action] ?? false;
|
|
@@ -633,10 +854,11 @@ function can(resource, action) {
|
|
|
633
854
|
exports.can = can;
|
|
634
855
|
exports.constants = constants;
|
|
635
856
|
exports.createRouter = createRouter;
|
|
857
|
+
exports.current = current;
|
|
636
858
|
exports.definePlugin = definePlugin;
|
|
637
859
|
exports.getRouterContext = getRouterContext;
|
|
638
860
|
exports.makeUrl = makeUrl;
|
|
639
861
|
exports.registerHook = registerHook;
|
|
640
|
-
exports.
|
|
862
|
+
exports.route = route;
|
|
641
863
|
exports.router = router;
|
|
642
864
|
exports.sameUrls = sameUrls;
|