@hybridly/core 0.8.3 → 0.10.0-beta.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/dist/_chunks/chunk.mjs +20 -0
- package/dist/index.d.mts +391 -411
- package/dist/index.mjs +1045 -948
- package/package.json +11 -13
- package/LICENSE +0 -19
- package/dist/index.cjs +0 -1108
- package/dist/index.d.cts +0 -543
- package/dist/index.d.ts +0 -543
- package/properties.d.ts +0 -6
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import axios from
|
|
4
|
-
import { parse, stringify } from
|
|
1
|
+
import { t as __exportAll } from "./_chunks/chunk.mjs";
|
|
2
|
+
import { debounce, debug, hasFiles, match, merge, objectToFormData, random, removeTrailingSlash, showResponseErrorModal, when } from "@hybridly/utils";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { parse, stringify } from "superjson";
|
|
5
|
+
import qs from "qs";
|
|
5
6
|
|
|
7
|
+
//#region src/constants.ts
|
|
8
|
+
var constants_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
DIALOG_KEY_HEADER: () => DIALOG_KEY_HEADER,
|
|
10
|
+
DIALOG_REDIRECT_HEADER: () => DIALOG_REDIRECT_HEADER,
|
|
11
|
+
ERROR_BAG_HEADER: () => ERROR_BAG_HEADER,
|
|
12
|
+
EXCEPT_DATA_HEADER: () => EXCEPT_DATA_HEADER,
|
|
13
|
+
EXTERNAL_NAVIGATION_HEADER: () => EXTERNAL_NAVIGATION_HEADER,
|
|
14
|
+
EXTERNAL_NAVIGATION_TARGET_HEADER: () => EXTERNAL_NAVIGATION_TARGET_HEADER,
|
|
15
|
+
HYBRIDLY_HEADER: () => HYBRIDLY_HEADER,
|
|
16
|
+
ONLY_DATA_HEADER: () => ONLY_DATA_HEADER,
|
|
17
|
+
PARTIAL_COMPONENT_HEADER: () => PARTIAL_COMPONENT_HEADER,
|
|
18
|
+
SCROLL_REGION_ATTRIBUTE: () => SCROLL_REGION_ATTRIBUTE,
|
|
19
|
+
STORAGE_EXTERNAL_KEY: () => STORAGE_EXTERNAL_KEY,
|
|
20
|
+
VERSION_HEADER: () => VERSION_HEADER
|
|
21
|
+
});
|
|
6
22
|
const STORAGE_EXTERNAL_KEY = "hybridly:external";
|
|
7
23
|
const HYBRIDLY_HEADER = "x-hybrid";
|
|
8
24
|
const EXTERNAL_NAVIGATION_HEADER = `${HYBRIDLY_HEADER}-external`;
|
|
@@ -16,1077 +32,1158 @@ const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
|
|
|
16
32
|
const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
|
|
17
33
|
const SCROLL_REGION_ATTRIBUTE = "scroll-region";
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/errors.ts
|
|
37
|
+
var NotAHybridResponseError = class extends Error {
|
|
38
|
+
constructor(response) {
|
|
39
|
+
super();
|
|
40
|
+
this.response = response;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var NavigationCancelledError = class extends Error {};
|
|
44
|
+
var RoutingNotInitialized = class extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super("Routing is not initialized. Make sure the Vite plugin is enabled and that `php artisan route:list` returns no error.");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var RouteNotFound = class extends Error {
|
|
50
|
+
constructor(name) {
|
|
51
|
+
super(`Route [${name}] does not exist.`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var MissingRouteParameter = class extends Error {
|
|
55
|
+
constructor(parameter, routeName) {
|
|
56
|
+
super(`Parameter [${parameter}] is required for route [${routeName}].`);
|
|
57
|
+
}
|
|
33
58
|
};
|
|
34
59
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
super();
|
|
38
|
-
this.response = response;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
class NavigationCancelledError extends Error {
|
|
42
|
-
}
|
|
43
|
-
class RoutingNotInitialized extends Error {
|
|
44
|
-
constructor() {
|
|
45
|
-
super("Routing is not initialized. Make sure the Vite plugin is enabled and that `php artisan route:list` returns no error.");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
class RouteNotFound extends Error {
|
|
49
|
-
constructor(name) {
|
|
50
|
-
super(`Route [${name}] does not exist.`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
class MissingRouteParameter extends Error {
|
|
54
|
-
constructor(parameter, routeName) {
|
|
55
|
-
super(`Parameter [${parameter}] is required for route [${routeName}].`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/plugins/plugin.ts
|
|
59
62
|
function definePlugin(plugin) {
|
|
60
|
-
|
|
63
|
+
return plugin;
|
|
61
64
|
}
|
|
62
65
|
async function forEachPlugin(cb) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await cb(plugin);
|
|
66
|
-
}
|
|
66
|
+
const { plugins } = getRouterContext();
|
|
67
|
+
for (const plugin of plugins) await cb(plugin);
|
|
67
68
|
}
|
|
68
69
|
async function runPluginHooks(hook, ...args) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
let result = true;
|
|
71
|
+
await forEachPlugin(async (plugin) => {
|
|
72
|
+
if (plugin[hook]) {
|
|
73
|
+
debug.plugin(plugin.name, `Calling "${hook}" hook.`);
|
|
74
|
+
result &&= await plugin[hook]?.(...args) !== false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return result;
|
|
77
78
|
}
|
|
78
79
|
async function runGlobalHooks(hook, ...args) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
80
|
+
const { hooks } = getRouterContext();
|
|
81
|
+
if (!hooks[hook]) return true;
|
|
82
|
+
let result = true;
|
|
83
|
+
for (const fn of hooks[hook]) {
|
|
84
|
+
debug.hook(`Calling global "${hook}" hooks.`);
|
|
85
|
+
result = await fn(...args) ?? result;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
89
88
|
}
|
|
90
89
|
async function runHooks(hook, requestHooks, ...args) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
const result = await Promise.all([
|
|
91
|
+
requestHooks?.[hook]?.(...args),
|
|
92
|
+
runGlobalHooks(hook, ...args),
|
|
93
|
+
runPluginHooks(hook, ...args)
|
|
94
|
+
]);
|
|
95
|
+
debug.hook(`Called all hooks for [${hook}],`, result);
|
|
96
|
+
return !result.includes(false);
|
|
98
97
|
}
|
|
99
98
|
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/plugins/hooks.ts
|
|
101
|
+
/**
|
|
102
|
+
* Registers a global hook.
|
|
103
|
+
*/
|
|
100
104
|
function appendCallbackToHooks(hook, fn) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
const hooks = getRouterContext().hooks;
|
|
106
|
+
hooks[hook] = [...hooks[hook] ?? [], fn];
|
|
107
|
+
return () => {
|
|
108
|
+
const index = hooks[hook].indexOf(fn);
|
|
109
|
+
if (index !== -1) hooks[hook]?.splice(index, 1);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Registers a global hook.
|
|
114
|
+
*/
|
|
110
115
|
function registerHook(hook, fn, options) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
if (options?.once) {
|
|
117
|
+
const unregister = appendCallbackToHooks(hook, async (...args) => {
|
|
118
|
+
await fn(...args);
|
|
119
|
+
unregister();
|
|
120
|
+
});
|
|
121
|
+
return unregister;
|
|
122
|
+
}
|
|
123
|
+
return appendCallbackToHooks(hook, fn);
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/scroll.ts
|
|
128
|
+
/** Saves the current view's scrollbar positions into the history state. */
|
|
121
129
|
function saveScrollPositions() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const regions = getScrollRegions();
|
|
131
|
+
debug.scroll("Saving scroll positions of:", regions.map((el) => ({
|
|
132
|
+
el,
|
|
133
|
+
scroll: {
|
|
134
|
+
top: el.scrollTop,
|
|
135
|
+
left: el.scrollLeft
|
|
136
|
+
}
|
|
137
|
+
})));
|
|
138
|
+
setContext({ scrollRegions: regions.map(({ scrollTop, scrollLeft }) => ({
|
|
139
|
+
top: scrollTop,
|
|
140
|
+
left: scrollLeft
|
|
141
|
+
})) });
|
|
142
|
+
setHistoryState({ replace: true });
|
|
143
|
+
}
|
|
144
|
+
/** Gets DOM elements which scroll positions should be saved. */
|
|
132
145
|
function getScrollRegions() {
|
|
133
|
-
|
|
146
|
+
return Array.from(document?.querySelectorAll(`[${SCROLL_REGION_ATTRIBUTE}]`) ?? []).concat(document.documentElement, document.body);
|
|
134
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Resets the current view's scrollbars positions to the top, and save them
|
|
150
|
+
* in the history state.
|
|
151
|
+
*/
|
|
135
152
|
function resetScrollPositions() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
153
|
+
debug.scroll("Resetting scroll positions.");
|
|
154
|
+
getScrollRegions().forEach((element) => element.scrollTo({
|
|
155
|
+
top: 0,
|
|
156
|
+
left: 0
|
|
157
|
+
}));
|
|
158
|
+
saveScrollPositions();
|
|
159
|
+
if (window.location.hash) {
|
|
160
|
+
debug.scroll(`Hash is present, scrolling to the element of ID ${window.location.hash}.`);
|
|
161
|
+
document.getElementById(window.location.hash.slice(1))?.scrollIntoView();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Restores the scroll positions stored in the context. */
|
|
147
165
|
async function restoreScrollPositions() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
const context = getRouterContext();
|
|
167
|
+
const regions = getScrollRegions();
|
|
168
|
+
if (!context.scrollRegions) {
|
|
169
|
+
debug.scroll("No region found to restore.");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
context.adapter.executeOnMounted(() => {
|
|
173
|
+
debug.scroll(`Restoring ${regions.length}/${context.scrollRegions.length} region(s).`);
|
|
174
|
+
regions.forEach((el, i) => el.scrollTo({
|
|
175
|
+
top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
|
|
176
|
+
left: context.scrollRegions.at(i)?.top ?? el.scrollLeft
|
|
177
|
+
}));
|
|
178
|
+
});
|
|
161
179
|
}
|
|
162
180
|
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/url.ts
|
|
183
|
+
/** Normalizes the given input to an URL. */
|
|
163
184
|
function normalizeUrl(href, trailingSlash) {
|
|
164
|
-
|
|
185
|
+
return makeUrl(href, { trailingSlash }).toString();
|
|
165
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Converts an input to an URL, optionally changing its properties after initialization.
|
|
189
|
+
*/
|
|
166
190
|
function makeUrl(href, transformations = {}) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
191
|
+
try {
|
|
192
|
+
const base = document?.location?.href === "//" ? void 0 : document.location.href;
|
|
193
|
+
const url = new URL(String(href), base);
|
|
194
|
+
transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
|
|
195
|
+
Object.entries(transformations).forEach(([key, value]) => {
|
|
196
|
+
if (key === "query") {
|
|
197
|
+
const currentQueryParameters = merge(qs.parse(url.search, { ignoreQueryPrefix: true }), value, { mergePlainObjects: true });
|
|
198
|
+
key = "search";
|
|
199
|
+
value = qs.stringify(currentQueryParameters, {
|
|
200
|
+
encodeValuesOnly: true,
|
|
201
|
+
arrayFormat: "brackets",
|
|
202
|
+
filter: (_, object) => object instanceof Set ? [...object] : object
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
Reflect.set(url, key, value);
|
|
206
|
+
});
|
|
207
|
+
if (transformations.trailingSlash === false) {
|
|
208
|
+
const _url = removeTrailingSlash(url.toString().replace(/\/\?/, "?"));
|
|
209
|
+
url.toString = () => _url;
|
|
210
|
+
}
|
|
211
|
+
return url;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new TypeError(`${href} is not resolvable to a valid URL.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Checks if the given URLs have the same origin and path.
|
|
218
|
+
*/
|
|
196
219
|
function sameUrls(...hrefs) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
if (hrefs.length < 2) return true;
|
|
221
|
+
try {
|
|
222
|
+
return hrefs.every((href) => {
|
|
223
|
+
return makeUrl(href, { hash: "" }).toJSON() === makeUrl(hrefs.at(0), { hash: "" }).toJSON();
|
|
224
|
+
});
|
|
225
|
+
} catch {}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Checks if the given URLs have the same origin, path, and hash.
|
|
230
|
+
*/
|
|
208
231
|
function sameHashes(...hrefs) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
if (hrefs.length < 2) return true;
|
|
233
|
+
try {
|
|
234
|
+
return hrefs.every((href) => {
|
|
235
|
+
return makeUrl(href).toJSON() === makeUrl(hrefs.at(0)).toJSON();
|
|
236
|
+
});
|
|
237
|
+
} catch {}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* If the back-end did not specify a hash, if the navigation specified one,
|
|
242
|
+
* and both URLs lead to the same endpoint, we update the target URL
|
|
243
|
+
* to use the hash of the initially-requested URL.
|
|
244
|
+
*/
|
|
220
245
|
function fillHash(currentUrl, targetUrl) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
return targetUrl.toString();
|
|
246
|
+
currentUrl = makeUrl(currentUrl);
|
|
247
|
+
targetUrl = makeUrl(targetUrl);
|
|
248
|
+
if (currentUrl.hash && !targetUrl.hash && sameUrls(targetUrl, currentUrl)) targetUrl.hash = currentUrl.hash;
|
|
249
|
+
return targetUrl.toString();
|
|
227
250
|
}
|
|
228
251
|
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/router/history.ts
|
|
254
|
+
/** Puts the given context into the history state. */
|
|
229
255
|
function setHistoryState(options = {}) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}
|
|
256
|
+
if (!window?.history) throw new Error("The history API is not available, so Hybridly cannot operate.");
|
|
257
|
+
const context = getRouterContext();
|
|
258
|
+
const method = options.replace ? "replaceState" : "pushState";
|
|
259
|
+
const serialized = serializeContext(context);
|
|
260
|
+
debug.history("Setting history state:", {
|
|
261
|
+
method,
|
|
262
|
+
context
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
window.history[method](serialized, "", context.url);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error("Hybridly could not save its current state in the history. This is most likely due to a property being non-serializable, such as a proxy or a reference.");
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Gets the current history data if it exists. */
|
|
248
272
|
function getHistoryState() {
|
|
249
|
-
|
|
273
|
+
return getRouterContext().serializer.unserialize(window.history.state);
|
|
250
274
|
}
|
|
275
|
+
/** Gets the current history state if it exists. */
|
|
251
276
|
function getHistoryMemo(key) {
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
const state$1 = getHistoryState();
|
|
278
|
+
return key ? state$1?.memo?.[key] : state$1?.memo;
|
|
254
279
|
}
|
|
280
|
+
/** Register history-related event listeneners. */
|
|
255
281
|
async function registerEventListeners() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
282
|
+
const context = getRouterContext();
|
|
283
|
+
debug.history("Registering [popstate] and [scroll] event listeners.");
|
|
284
|
+
window?.addEventListener("popstate", async (event) => {
|
|
285
|
+
debug.history("Navigation detected (popstate event). State:", { state: event.state });
|
|
286
|
+
if (context.pendingNavigation) {
|
|
287
|
+
debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
288
|
+
context.pendingNavigation?.controller?.abort();
|
|
289
|
+
}
|
|
290
|
+
const state$1 = context.serializer.unserialize(event.state);
|
|
291
|
+
await runHooks("backForward", {}, state$1, context);
|
|
292
|
+
if (!state$1) {
|
|
293
|
+
debug.history("There is no state. Adding hash if any and restoring scroll positions.");
|
|
294
|
+
return await navigate({
|
|
295
|
+
type: "initial",
|
|
296
|
+
payload: {
|
|
297
|
+
...context,
|
|
298
|
+
url: makeUrl(context.url, { hash: window.location.hash }).toString()
|
|
299
|
+
},
|
|
300
|
+
preserveScroll: true,
|
|
301
|
+
preserveState: true,
|
|
302
|
+
replace: true
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
await navigate({
|
|
306
|
+
type: "back-forward",
|
|
307
|
+
payload: state$1,
|
|
308
|
+
preserveScroll: true,
|
|
309
|
+
preserveState: !!getInternalRouterContext().dialog || !!state$1.dialog,
|
|
310
|
+
updateHistoryState: false
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
window?.addEventListener("scroll", (event) => debounce(100, () => {
|
|
314
|
+
if ((event?.target)?.hasAttribute?.(SCROLL_REGION_ATTRIBUTE)) saveScrollPositions();
|
|
315
|
+
}), true);
|
|
316
|
+
}
|
|
317
|
+
/** Checks if the current navigation was made by going back or forward. */
|
|
293
318
|
function isBackForwardNavigation() {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
return window.performance?.getEntriesByType("navigation").at(0)?.type === "back_forward";
|
|
319
|
+
if (!window.history.state) return false;
|
|
320
|
+
return (window.performance?.getEntriesByType("navigation").at(0))?.type === "back_forward";
|
|
298
321
|
}
|
|
322
|
+
/** Handles a navigation which was going back or forward. */
|
|
299
323
|
async function handleBackForwardNavigation() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
324
|
+
debug.router("Handling a back/forward navigation from an external URL.");
|
|
325
|
+
const context = getRouterContext();
|
|
326
|
+
const state$1 = getHistoryState();
|
|
327
|
+
if (!state$1) throw new Error("Tried to handling a back/forward navigation, but there was no state in the history. This should not happen.");
|
|
328
|
+
await navigate({
|
|
329
|
+
type: "back-forward",
|
|
330
|
+
payload: {
|
|
331
|
+
...state$1,
|
|
332
|
+
version: context.version
|
|
333
|
+
},
|
|
334
|
+
preserveScroll: true,
|
|
335
|
+
preserveState: false,
|
|
336
|
+
updateHistoryState: false
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Saves a value into the current history state. */
|
|
317
340
|
function remember(key, value) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
341
|
+
debug.history(`Remembering key "${key}" with value`, value);
|
|
342
|
+
setContext({ memo: {
|
|
343
|
+
...getHistoryMemo(),
|
|
344
|
+
[key]: value
|
|
345
|
+
} }, { propagate: false });
|
|
346
|
+
setHistoryState({ replace: true });
|
|
347
|
+
}
|
|
348
|
+
/** Serializes the context so it can be written to the history state. */
|
|
327
349
|
function serializeContext(context) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
return context.serializer.serialize({
|
|
351
|
+
url: context.url,
|
|
352
|
+
version: context.version,
|
|
353
|
+
view: context.view,
|
|
354
|
+
dialog: context.dialog,
|
|
355
|
+
scrollRegions: context.scrollRegions,
|
|
356
|
+
memo: context.memo
|
|
357
|
+
});
|
|
336
358
|
}
|
|
337
359
|
function createSerializer(options) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
};
|
|
360
|
+
if (options.serializer) return options.serializer;
|
|
361
|
+
return {
|
|
362
|
+
serialize: (data) => {
|
|
363
|
+
debug.history("Serializing data.", data);
|
|
364
|
+
return stringify(data);
|
|
365
|
+
},
|
|
366
|
+
unserialize: (data) => {
|
|
367
|
+
if (!data) {
|
|
368
|
+
debug.history("No data to unserialize.");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
return parse(data);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
354
374
|
}
|
|
355
375
|
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/routing/route.ts
|
|
356
378
|
function getUrlRegexForRoute(name) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
379
|
+
const routing = getRouting();
|
|
380
|
+
const definition = getRouteDefinition(name);
|
|
381
|
+
const path = definition.uri.replaceAll("/", "\\/");
|
|
382
|
+
const domain = definition.domain;
|
|
383
|
+
const protocolPrefix = routing.url.match(/^\w+:\/\//)?.[0];
|
|
384
|
+
const origin = domain ? `${protocolPrefix}${domain}${routing.port ? `:${routing.port}` : ""}`.replaceAll("/", "\\/") : routing.url.replaceAll("/", "\\/");
|
|
385
|
+
const urlPathRegexPattern = path.length > 0 ? `\\/${path.replace(/\/$/g, "")}` : "";
|
|
386
|
+
let urlRegexPattern = `^${origin.replaceAll(".", "\\.")}${urlPathRegexPattern}\\/?(\\?.*)?$`;
|
|
387
|
+
urlRegexPattern = urlRegexPattern.replace(/(\\\/?){([^}?]+)(\??)}/g, (_, slash, parameterName, optional) => {
|
|
388
|
+
let regexTemplate = (definition.wheres?.[parameterName])?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+";
|
|
389
|
+
regexTemplate = `(?<${parameterName}>${regexTemplate})`;
|
|
390
|
+
if (optional) return `(${slash ? "\\/?" : ""}${regexTemplate})?`;
|
|
391
|
+
return (slash ? "\\/" : "") + regexTemplate;
|
|
392
|
+
});
|
|
393
|
+
return RegExp(urlRegexPattern);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if a given URL matches a route based on its name.
|
|
397
|
+
* Additionally you can pass an object of parameters to check if the URL matches the route with the given parameters.
|
|
398
|
+
* Otherwise it will accept and thus return true for any values for the parameters defined by the route.
|
|
399
|
+
* Note: passing additional parameters that are not defined by the route or included in the current URL will cause this to return false.
|
|
400
|
+
*/
|
|
376
401
|
function urlMatchesRoute(fullUrl, name, routeParameters) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return Object.keys(parameters).every((parameterName) => {
|
|
391
|
-
let value = parameters[parameterName];
|
|
392
|
-
const bindingProperty = definition.bindings?.[parameterName];
|
|
393
|
-
if (bindingProperty && typeof value === "object") {
|
|
394
|
-
value = value[bindingProperty];
|
|
395
|
-
}
|
|
396
|
-
return matches.groups?.[parameterName] === value.toString();
|
|
397
|
-
});
|
|
402
|
+
const url = makeUrl(fullUrl, { hash: "" }).toString();
|
|
403
|
+
const parameters = routeParameters || {};
|
|
404
|
+
const definition = getRouting().routes[name];
|
|
405
|
+
if (!definition) return false;
|
|
406
|
+
const matches = getUrlRegexForRoute(name).exec(url);
|
|
407
|
+
if (!matches) return false;
|
|
408
|
+
for (const k in matches.groups) matches.groups[k] = typeof matches.groups[k] === "string" ? decodeURIComponent(matches.groups[k]) : matches.groups[k];
|
|
409
|
+
return Object.keys(parameters).every((parameterName) => {
|
|
410
|
+
let value = parameters[parameterName];
|
|
411
|
+
const bindingProperty = definition.bindings?.[parameterName];
|
|
412
|
+
if (bindingProperty && typeof value === "object") value = value[bindingProperty];
|
|
413
|
+
return matches.groups?.[parameterName] === value.toString();
|
|
414
|
+
});
|
|
398
415
|
}
|
|
399
416
|
function generateRouteFromName(name, parameters, absolute, shouldThrow) {
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
const url = getUrlFromName(name, parameters, shouldThrow);
|
|
418
|
+
return absolute === false ? url.toString().replace(url.origin, "") || "/" : url.toString();
|
|
402
419
|
}
|
|
403
420
|
function getNameFromUrl(url, parameters) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
});
|
|
421
|
+
const routing = getRouting();
|
|
422
|
+
return Object.values(routing.routes).map((x) => x.name).find((routeName) => {
|
|
423
|
+
return urlMatchesRoute(url, routeName, parameters);
|
|
424
|
+
});
|
|
409
425
|
}
|
|
410
426
|
function getUrlFromName(name, parameters, shouldThrow) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
427
|
+
const routing = getRouting();
|
|
428
|
+
const definition = getRouteDefinition(name);
|
|
429
|
+
const transforms = getRouteTransformable(name, parameters, shouldThrow);
|
|
430
|
+
return makeUrl(routing.url, (url) => ({
|
|
431
|
+
hostname: definition.domain || url.hostname,
|
|
432
|
+
port: routing.port?.toString() || url.port,
|
|
433
|
+
trailingSlash: false,
|
|
434
|
+
...transforms
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Resolved the value of a route parameter from either the passed parameters or the default parameters.
|
|
439
|
+
*/
|
|
422
440
|
function getRouteParameterValue(routeName, parameterName, routeParameters) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return routing.defaults?.[parameterName];
|
|
443
|
-
}
|
|
444
|
-
}
|
|
441
|
+
const routing = getRouting();
|
|
442
|
+
const definition = getRouteDefinition(routeName);
|
|
443
|
+
const parameters = routeParameters || {};
|
|
444
|
+
const value = (() => {
|
|
445
|
+
const value$1 = parameters[parameterName];
|
|
446
|
+
const bindingProperty = definition.bindings?.[parameterName];
|
|
447
|
+
if (bindingProperty && value$1 != null && typeof value$1 === "object") return value$1[bindingProperty];
|
|
448
|
+
return value$1;
|
|
449
|
+
})();
|
|
450
|
+
if (value) {
|
|
451
|
+
const where = definition.wheres?.[parameterName];
|
|
452
|
+
if (where && !new RegExp(where).test(value)) console.warn(`[hybridly:routing] Parameter [${parameterName}] does not match the required format [${where}] for route [${routeName}].`);
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
if (routing.defaults?.[parameterName]) return routing.defaults?.[parameterName];
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Gets the `UrlTransformable` object for the given route and parameters.
|
|
459
|
+
*/
|
|
445
460
|
function getRouteTransformable(routeName, routeParameters, shouldThrow) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
};
|
|
478
|
-
}
|
|
461
|
+
const definition = getRouteDefinition(routeName);
|
|
462
|
+
const parameters = routeParameters || {};
|
|
463
|
+
const missing = Object.keys(parameters);
|
|
464
|
+
const replaceParameter = (match$1, parameterName, optional) => {
|
|
465
|
+
const value = getRouteParameterValue(routeName, parameterName, parameters);
|
|
466
|
+
const found = missing.indexOf(parameterName);
|
|
467
|
+
if (found >= 0) missing.splice(found, 1);
|
|
468
|
+
if (value) return value;
|
|
469
|
+
if (optional) return "";
|
|
470
|
+
if (shouldThrow === false) return "";
|
|
471
|
+
throw new MissingRouteParameter(parameterName, routeName);
|
|
472
|
+
};
|
|
473
|
+
const path = definition.uri.replace(/{([^}?]+)(\??)}/g, replaceParameter);
|
|
474
|
+
const domain = definition.domain?.replace(/{([^}?]+)(\??)}/g, replaceParameter);
|
|
475
|
+
const remaining = Object.keys(parameters).filter((key) => missing.includes(key)).reduce((obj, key) => ({
|
|
476
|
+
...obj,
|
|
477
|
+
[key]: parameters[key]
|
|
478
|
+
}), {});
|
|
479
|
+
return {
|
|
480
|
+
...domain && { hostname: domain },
|
|
481
|
+
pathname: path,
|
|
482
|
+
search: qs.stringify(remaining, {
|
|
483
|
+
encodeValuesOnly: true,
|
|
484
|
+
arrayFormat: "indices",
|
|
485
|
+
addQueryPrefix: true
|
|
486
|
+
})
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Gets the route definition.
|
|
491
|
+
*/
|
|
479
492
|
function getRouteDefinition(name) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
throw new RouteNotFound(name);
|
|
484
|
-
}
|
|
485
|
-
return definition;
|
|
493
|
+
const definition = getRouting().routes[name];
|
|
494
|
+
if (!definition) throw new RouteNotFound(name);
|
|
495
|
+
return definition;
|
|
486
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Gets the routing configuration from the current context.
|
|
499
|
+
*/
|
|
487
500
|
function getRouting() {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
return routing;
|
|
501
|
+
const { routing } = getInternalRouterContext();
|
|
502
|
+
if (!routing) throw new RoutingNotInitialized();
|
|
503
|
+
return routing;
|
|
493
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Generates a route from the given route name.
|
|
507
|
+
*/
|
|
494
508
|
function route(name, parameters, absolute) {
|
|
495
|
-
|
|
509
|
+
return generateRouteFromName(name, parameters, absolute ?? getRouting().absolute ?? true);
|
|
496
510
|
}
|
|
497
511
|
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/routing/current.ts
|
|
498
514
|
function getCurrentUrl() {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
515
|
+
if (typeof window === "undefined") return getInternalRouterContext().url;
|
|
516
|
+
return window.location.toString();
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Determines whether the current route matches the given name and parameters.
|
|
520
|
+
* If multiple routes match, the first one will be returned.
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* currentRouteMatches('tenant.*') // matches all routes starting with 'tenant.'
|
|
525
|
+
* currentRouteMatches('tenant.*.admin') // matches all routes starting with 'tenant.' and ending with '.admin'
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
504
528
|
function currentRouteMatches(name, parameters) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
529
|
+
const namePattern = `^${name.replaceAll(".", "\\.").replaceAll("*", ".*")}$`;
|
|
530
|
+
const possibleRoutes = Object.values(getRouting().routes).filter((x) => x.method.includes("GET") && RegExp(namePattern).test(x.name)).map((x) => x.name);
|
|
531
|
+
const currentUrl = getCurrentUrl();
|
|
532
|
+
return possibleRoutes.some((routeName) => {
|
|
533
|
+
return urlMatchesRoute(currentUrl, routeName, parameters);
|
|
534
|
+
});
|
|
511
535
|
}
|
|
512
536
|
function getCurrentRouteName() {
|
|
513
|
-
|
|
537
|
+
return getNameFromUrl(getCurrentUrl());
|
|
514
538
|
}
|
|
515
539
|
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/routing/index.ts
|
|
516
542
|
function updateRoutingConfiguration(routing) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
setContext({ routing });
|
|
543
|
+
if (!routing) return;
|
|
544
|
+
setContext({ routing });
|
|
521
545
|
}
|
|
522
546
|
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/download.ts
|
|
549
|
+
/** Checks if the response wants to redirect to an external URL. */
|
|
523
550
|
function isDownloadResponse(response) {
|
|
524
|
-
|
|
551
|
+
return response.status === 200 && !!response.headers["content-disposition"];
|
|
525
552
|
}
|
|
553
|
+
/** Handles a download. */
|
|
526
554
|
async function handleDownloadResponse(response) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
555
|
+
const blob = new Blob([response.data], { type: response.headers["content-type"] });
|
|
556
|
+
const urlObject = window.webkitURL || window.URL;
|
|
557
|
+
const link = document.createElement("a");
|
|
558
|
+
link.style.display = "none";
|
|
559
|
+
link.href = urlObject.createObjectURL(blob);
|
|
560
|
+
link.download = getFileNameFromContentDispositionHeader(response.headers["content-disposition"]);
|
|
561
|
+
link.click();
|
|
562
|
+
setTimeout(() => {
|
|
563
|
+
urlObject.revokeObjectURL(link.href);
|
|
564
|
+
link.remove();
|
|
565
|
+
}, 0);
|
|
538
566
|
}
|
|
539
567
|
function getFileNameFromContentDispositionHeader(header) {
|
|
540
|
-
|
|
541
|
-
return result?.replace(/^"(.*)"$/, "$1") ?? "";
|
|
568
|
+
return (header.split(";")[1]?.trim().split("=")[1])?.replace(/^"(.*)"$/, "$1") ?? "";
|
|
542
569
|
}
|
|
543
570
|
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/context/context.ts
|
|
544
573
|
const state = {
|
|
545
|
-
|
|
546
|
-
|
|
574
|
+
initialized: false,
|
|
575
|
+
context: {}
|
|
547
576
|
};
|
|
577
|
+
/** Gets the current context. */
|
|
548
578
|
function getRouterContext() {
|
|
549
|
-
|
|
579
|
+
return getInternalRouterContext();
|
|
550
580
|
}
|
|
581
|
+
/** Gets the current context, but not in read-only. */
|
|
551
582
|
function getInternalRouterContext() {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
return state.context;
|
|
583
|
+
if (!state.initialized) throw new Error("Hybridly is not initialized.");
|
|
584
|
+
return state.context;
|
|
556
585
|
}
|
|
586
|
+
/** Initializes the context. */
|
|
557
587
|
async function initializeContext(options) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
588
|
+
state.initialized = true;
|
|
589
|
+
state.context = {
|
|
590
|
+
...options.payload,
|
|
591
|
+
responseErrorModals: options.responseErrorModals,
|
|
592
|
+
serializer: createSerializer(options),
|
|
593
|
+
url: makeUrl(options.payload.url).toString(),
|
|
594
|
+
adapter: {
|
|
595
|
+
...options.adapter,
|
|
596
|
+
updateRoutingConfiguration
|
|
597
|
+
},
|
|
598
|
+
scrollRegions: [],
|
|
599
|
+
plugins: options.plugins ?? [],
|
|
600
|
+
axios: registerAxios(options.axios ?? axios.create()),
|
|
601
|
+
routing: options.routing,
|
|
602
|
+
preloadCache: /* @__PURE__ */ new Map(),
|
|
603
|
+
hooks: {},
|
|
604
|
+
memo: {}
|
|
605
|
+
};
|
|
606
|
+
await runHooks("initialized", {}, state.context);
|
|
607
|
+
return getInternalRouterContext();
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Registers an interceptor that assumes `arraybuffer`
|
|
611
|
+
* responses and converts responses to JSON or text.
|
|
612
|
+
*/
|
|
613
|
+
function registerAxios(axios$1) {
|
|
614
|
+
axios$1.interceptors.response.use((response) => {
|
|
615
|
+
if (!isDownloadResponse(response)) {
|
|
616
|
+
const text = new TextDecoder().decode(response.data);
|
|
617
|
+
try {
|
|
618
|
+
response.data = JSON.parse(text);
|
|
619
|
+
} catch {
|
|
620
|
+
response.data = text;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return response;
|
|
624
|
+
}, (error) => Promise.reject(error));
|
|
625
|
+
return axios$1;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Mutates properties at the top-level of the context.
|
|
629
|
+
*/
|
|
630
|
+
function setContext(merge$1 = {}, options = {}) {
|
|
631
|
+
Object.keys(merge$1).forEach((key) => {
|
|
632
|
+
Reflect.set(state.context, key, merge$1[key]);
|
|
633
|
+
});
|
|
634
|
+
if (options.propagate !== false) state.context.adapter.onContextUpdate?.(state.context);
|
|
635
|
+
debug.context("Updated context:", {
|
|
636
|
+
context: state.context,
|
|
637
|
+
added: merge$1
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/** Gets a payload from the current context. */
|
|
605
641
|
function payloadFromContext() {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
642
|
+
return {
|
|
643
|
+
url: getRouterContext().url,
|
|
644
|
+
version: getRouterContext().version,
|
|
645
|
+
view: getRouterContext().view,
|
|
646
|
+
dialog: getRouterContext().dialog
|
|
647
|
+
};
|
|
612
648
|
}
|
|
613
649
|
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/external.ts
|
|
652
|
+
/**
|
|
653
|
+
* Performs an external navigation by saving options to the storage and
|
|
654
|
+
* making a full page reload. Upon loading, the navigation options
|
|
655
|
+
* will be pulled and a hybrid navigation will be made.
|
|
656
|
+
*/
|
|
614
657
|
async function performExternalNavigation(options) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
658
|
+
debug.external("Navigating to an external URL:", options);
|
|
659
|
+
if (options.target === "new-tab") {
|
|
660
|
+
const link = document.createElement("a");
|
|
661
|
+
link.style.display = "none";
|
|
662
|
+
link.target = "_blank";
|
|
663
|
+
link.href = options.url;
|
|
664
|
+
link.click();
|
|
665
|
+
setTimeout(() => link.remove(), 0);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
|
|
669
|
+
window.location.href = options.url;
|
|
670
|
+
if (sameUrls(window.location, options.url)) {
|
|
671
|
+
debug.external("Manually reloading due to the external URL being the same.");
|
|
672
|
+
window.location.reload();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Navigates to the given URL without the hybrid protocol. */
|
|
632
676
|
function navigateToExternalUrl(url, data) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
})
|
|
638
|
-
}).toString();
|
|
677
|
+
document.location.href = makeUrl(url, { search: qs.stringify(data, {
|
|
678
|
+
encodeValuesOnly: true,
|
|
679
|
+
arrayFormat: "brackets"
|
|
680
|
+
}) }).toString();
|
|
639
681
|
}
|
|
682
|
+
/** Checks if the response wants to redirect to an external URL. */
|
|
640
683
|
function isExternalResponse(response) {
|
|
641
|
-
|
|
684
|
+
return response?.status === 409 && !!response?.headers?.[EXTERNAL_NAVIGATION_HEADER];
|
|
642
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Performs the internal navigation when an external navigation to a hybrid view has been made.
|
|
688
|
+
* This method is meant to be called on router creation.
|
|
689
|
+
*/
|
|
643
690
|
async function handleExternalNavigation() {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
691
|
+
debug.external("Handling an external navigation.");
|
|
692
|
+
const options = JSON.parse(window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) || "{}");
|
|
693
|
+
window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
|
|
694
|
+
debug.external("Options from the session storage:", options);
|
|
695
|
+
setContext({ url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString() });
|
|
696
|
+
await navigate({
|
|
697
|
+
type: "initial",
|
|
698
|
+
preserveState: true,
|
|
699
|
+
preserveScroll: options.preserveScroll
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
/** Checks if the navigation being initialized points to an external location. */
|
|
657
703
|
function isExternalNavigation() {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return false;
|
|
704
|
+
try {
|
|
705
|
+
return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
|
|
706
|
+
} catch {}
|
|
707
|
+
return false;
|
|
663
708
|
}
|
|
664
709
|
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/dialog/index.ts
|
|
712
|
+
/**
|
|
713
|
+
* Closes the dialog.
|
|
714
|
+
*/
|
|
665
715
|
async function closeDialog(options) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
preserveScroll: true,
|
|
685
|
-
preserveState: true,
|
|
686
|
-
...options
|
|
687
|
-
});
|
|
716
|
+
const context = getInternalRouterContext();
|
|
717
|
+
const url = context.dialog?.redirectUrl ?? context.dialog?.baseUrl;
|
|
718
|
+
if (!url) return;
|
|
719
|
+
context.adapter.onDialogClose?.(context);
|
|
720
|
+
if (options?.local === true) return await performLocalNavigation(url, {
|
|
721
|
+
preserveScroll: true,
|
|
722
|
+
preserveState: true,
|
|
723
|
+
dialog: false,
|
|
724
|
+
component: context.view.component,
|
|
725
|
+
properties: context.view.properties,
|
|
726
|
+
...options
|
|
727
|
+
});
|
|
728
|
+
return await performHybridNavigation({
|
|
729
|
+
url,
|
|
730
|
+
preserveScroll: true,
|
|
731
|
+
preserveState: true,
|
|
732
|
+
...options
|
|
733
|
+
});
|
|
688
734
|
}
|
|
689
735
|
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/router/preload.ts
|
|
738
|
+
/**
|
|
739
|
+
* Checks if there is a preloaded request for the given URL.
|
|
740
|
+
*/
|
|
690
741
|
function isPreloaded(targetUrl) {
|
|
691
|
-
|
|
692
|
-
return context.preloadCache.has(targetUrl.toString()) ?? false;
|
|
742
|
+
return getInternalRouterContext().preloadCache.has(targetUrl.toString()) ?? false;
|
|
693
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Gets the response of a preloaded request.
|
|
746
|
+
*/
|
|
694
747
|
function getPreloadedRequest(targetUrl) {
|
|
695
|
-
|
|
696
|
-
return context.preloadCache.get(targetUrl.toString());
|
|
748
|
+
return getInternalRouterContext().preloadCache.get(targetUrl.toString());
|
|
697
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Stores the response of a preloaded request.
|
|
752
|
+
*/
|
|
698
753
|
function storePreloadRequest(targetUrl, response) {
|
|
699
|
-
|
|
700
|
-
context.preloadCache.set(targetUrl.toString(), response);
|
|
754
|
+
getInternalRouterContext().preloadCache.set(targetUrl.toString(), response);
|
|
701
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Discards a preloaded request.
|
|
758
|
+
*/
|
|
702
759
|
function discardPreloadedRequest(targetUrl) {
|
|
703
|
-
|
|
704
|
-
return context.preloadCache.delete(targetUrl.toString());
|
|
760
|
+
return getInternalRouterContext().preloadCache.delete(targetUrl.toString());
|
|
705
761
|
}
|
|
762
|
+
/** Preloads a hybrid request. */
|
|
706
763
|
async function performPreloadRequest(options) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
764
|
+
const context = getRouterContext();
|
|
765
|
+
const url = makeUrl(options.url ?? context.url);
|
|
766
|
+
if (isPreloaded(url)) {
|
|
767
|
+
debug.router("This request is already preloaded.");
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
if (context.pendingNavigation) {
|
|
771
|
+
debug.router("A navigation is pending, preload aborted.");
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
if (options.method !== "GET") {
|
|
775
|
+
debug.router("Cannot preload non-GET requests.");
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
debug.router(`Preloading response for [${url.toString()}]`);
|
|
779
|
+
try {
|
|
780
|
+
const response = await performHybridRequest(url, options);
|
|
781
|
+
if (!isHybridResponse(response)) {
|
|
782
|
+
debug.router("Preload result was invalid.");
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
storePreloadRequest(url, response);
|
|
786
|
+
return true;
|
|
787
|
+
} catch (error) {
|
|
788
|
+
debug.router("Preloading failed.");
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
734
791
|
}
|
|
735
792
|
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/router/router.ts
|
|
795
|
+
/**
|
|
796
|
+
* The hybridly router.
|
|
797
|
+
* This is the core function that you can use to navigate in
|
|
798
|
+
* your application. Make sure the routes you call return a
|
|
799
|
+
* hybrid response, otherwise you need to call `external`.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* router.get('/posts/edit', { post })
|
|
803
|
+
*/
|
|
736
804
|
const router = {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
805
|
+
abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
|
|
806
|
+
active: () => !!getRouterContext().pendingNavigation,
|
|
807
|
+
navigate: async (options) => await performHybridNavigation(options),
|
|
808
|
+
reload: async (options) => await performHybridNavigation({
|
|
809
|
+
preserveScroll: true,
|
|
810
|
+
preserveState: true,
|
|
811
|
+
replace: true,
|
|
812
|
+
...options
|
|
813
|
+
}),
|
|
814
|
+
get: async (url, options = {}) => await performHybridNavigation({
|
|
815
|
+
...options,
|
|
816
|
+
url,
|
|
817
|
+
method: "GET"
|
|
818
|
+
}),
|
|
819
|
+
post: async (url, options = {}) => await performHybridNavigation({
|
|
820
|
+
preserveState: true,
|
|
821
|
+
...options,
|
|
822
|
+
url,
|
|
823
|
+
method: "POST"
|
|
824
|
+
}),
|
|
825
|
+
put: async (url, options = {}) => await performHybridNavigation({
|
|
826
|
+
preserveState: true,
|
|
827
|
+
...options,
|
|
828
|
+
url,
|
|
829
|
+
method: "PUT"
|
|
830
|
+
}),
|
|
831
|
+
patch: async (url, options = {}) => await performHybridNavigation({
|
|
832
|
+
preserveState: true,
|
|
833
|
+
...options,
|
|
834
|
+
url,
|
|
835
|
+
method: "PATCH"
|
|
836
|
+
}),
|
|
837
|
+
delete: async (url, options = {}) => await performHybridNavigation({
|
|
838
|
+
preserveState: true,
|
|
839
|
+
...options,
|
|
840
|
+
url,
|
|
841
|
+
method: "DELETE"
|
|
842
|
+
}),
|
|
843
|
+
local: async (url, options = {}) => await performLocalNavigation(url, options),
|
|
844
|
+
preload: async (url, options = {}) => await performPreloadRequest({
|
|
845
|
+
...options,
|
|
846
|
+
url,
|
|
847
|
+
method: "GET"
|
|
848
|
+
}),
|
|
849
|
+
external: (url, data = {}) => navigateToExternalUrl(url, data),
|
|
850
|
+
to: async (name, parameters, options) => {
|
|
851
|
+
const url = generateRouteFromName(name, parameters);
|
|
852
|
+
const method = getRouteDefinition(name).method.at(0);
|
|
853
|
+
return await performHybridNavigation({
|
|
854
|
+
url,
|
|
855
|
+
...options,
|
|
856
|
+
method
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
matches: (name, parameters) => currentRouteMatches(name, parameters),
|
|
860
|
+
current: () => getCurrentRouteName(),
|
|
861
|
+
dialog: { close: (options) => closeDialog(options) },
|
|
862
|
+
history: {
|
|
863
|
+
get: (key) => getHistoryMemo(key),
|
|
864
|
+
remember: (key, value) => remember(key, value)
|
|
865
|
+
}
|
|
763
866
|
};
|
|
867
|
+
/** Creates the hybridly router. */
|
|
764
868
|
async function createRouter(options) {
|
|
765
|
-
|
|
766
|
-
|
|
869
|
+
await initializeContext(options);
|
|
870
|
+
return await initializeRouter();
|
|
767
871
|
}
|
|
872
|
+
/** Performs every action necessary to make a hybrid navigation. */
|
|
768
873
|
async function performHybridNavigation(options) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
console.error(error);
|
|
920
|
-
await runHooks("exception", options.hooks, error, context);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
});
|
|
924
|
-
await runHooks("fail", options.hooks, context);
|
|
925
|
-
return {
|
|
926
|
-
error: {
|
|
927
|
-
type: error.constructor.name,
|
|
928
|
-
actual: error
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
} finally {
|
|
932
|
-
debug.router("Ending navigation.");
|
|
933
|
-
await runHooks("after", options.hooks, context);
|
|
934
|
-
if (context.pendingNavigation?.id === navigationId) {
|
|
935
|
-
setContext({ pendingNavigation: void 0 });
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
}
|
|
874
|
+
const navigationId = random();
|
|
875
|
+
const context = getRouterContext();
|
|
876
|
+
debug.router("Making a hybrid navigation:", {
|
|
877
|
+
context,
|
|
878
|
+
options,
|
|
879
|
+
navigationId
|
|
880
|
+
});
|
|
881
|
+
try {
|
|
882
|
+
if (!options.method) {
|
|
883
|
+
debug.router("Setting method to GET because none was provided.");
|
|
884
|
+
options.method = "GET";
|
|
885
|
+
}
|
|
886
|
+
options.method = options.method.toUpperCase();
|
|
887
|
+
if ((hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
|
|
888
|
+
options.data = objectToFormData(options.data);
|
|
889
|
+
debug.router("Converted data to FormData.", options.data);
|
|
890
|
+
}
|
|
891
|
+
if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
|
|
892
|
+
debug.router("Transforming data to query parameters.", options.data);
|
|
893
|
+
options.url = makeUrl(options.url ?? context.url, { query: options.data });
|
|
894
|
+
options.data = {};
|
|
895
|
+
}
|
|
896
|
+
if ([
|
|
897
|
+
"PUT",
|
|
898
|
+
"PATCH",
|
|
899
|
+
"DELETE"
|
|
900
|
+
].includes(options.method) && options.spoof !== false) {
|
|
901
|
+
debug.router(`Automatically spoofing method ${options.method}.`);
|
|
902
|
+
if (options.data instanceof FormData) options.data.append("_method", options.method);
|
|
903
|
+
else if (typeof options.data === "undefined") options.data = { _method: options.method };
|
|
904
|
+
else if (options.data instanceof Object && Object.keys(options.data).length >= 0) Object.assign(options.data, { _method: options.method });
|
|
905
|
+
else debug.router("Could not spoof method because body type is not supported.", options.data);
|
|
906
|
+
options.method = "POST";
|
|
907
|
+
}
|
|
908
|
+
if (!await runHooks("before", options.hooks, options, context)) {
|
|
909
|
+
debug.router("\"before\" event returned false, aborting the navigation.");
|
|
910
|
+
throw new NavigationCancelledError("The navigation was cancelled by the \"before\" event.");
|
|
911
|
+
}
|
|
912
|
+
if (context.pendingNavigation) {
|
|
913
|
+
debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
914
|
+
context.pendingNavigation?.controller?.abort();
|
|
915
|
+
}
|
|
916
|
+
saveScrollPositions();
|
|
917
|
+
const targetUrl = makeUrl(options.url ?? context.url, options.transformUrl);
|
|
918
|
+
const abortController = new AbortController();
|
|
919
|
+
setContext({ pendingNavigation: {
|
|
920
|
+
id: navigationId,
|
|
921
|
+
url: targetUrl,
|
|
922
|
+
controller: abortController,
|
|
923
|
+
status: "pending",
|
|
924
|
+
options
|
|
925
|
+
} });
|
|
926
|
+
await runHooks("start", options.hooks, context);
|
|
927
|
+
debug.router("Making request with axios.");
|
|
928
|
+
const response = await performHybridRequest(targetUrl, options, abortController);
|
|
929
|
+
if (await runHooks("data", options.hooks, response, context) === false) return { response };
|
|
930
|
+
if (isExternalResponse(response)) {
|
|
931
|
+
debug.router("The response is explicitely external.");
|
|
932
|
+
await performExternalNavigation({
|
|
933
|
+
url: fillHash(targetUrl, response.headers[EXTERNAL_NAVIGATION_HEADER]),
|
|
934
|
+
preserveScroll: options.preserveScroll === true,
|
|
935
|
+
target: response.headers[EXTERNAL_NAVIGATION_TARGET_HEADER] ?? "current"
|
|
936
|
+
});
|
|
937
|
+
return { response };
|
|
938
|
+
}
|
|
939
|
+
if (isDownloadResponse(response)) {
|
|
940
|
+
debug.router("The response returns a file to download.");
|
|
941
|
+
await handleDownloadResponse(response);
|
|
942
|
+
return { response };
|
|
943
|
+
}
|
|
944
|
+
if (!isHybridResponse(response)) throw new NotAHybridResponseError(response);
|
|
945
|
+
debug.router("The response respects the Hybridly protocol.");
|
|
946
|
+
const payload = response.data;
|
|
947
|
+
if (payload.view && (options.only?.length ?? options.except?.length) && payload.view.component === context.view.component) {
|
|
948
|
+
debug.router(`Merging ${options.only ? "\"only\"" : "\"except\""} properties.`, payload.view.properties);
|
|
949
|
+
const mergedPayloadProperties = merge(context.view.properties, payload.view.properties);
|
|
950
|
+
if (options.errorBag) mergedPayloadProperties.errors[options.errorBag] = payload.view.properties.errors[options.errorBag] ?? {};
|
|
951
|
+
else mergedPayloadProperties.errors = payload.view.properties.errors;
|
|
952
|
+
payload.view.properties = mergedPayloadProperties;
|
|
953
|
+
debug.router("Merged properties:", payload.view.properties);
|
|
954
|
+
}
|
|
955
|
+
await navigate({
|
|
956
|
+
type: "server",
|
|
957
|
+
payload: {
|
|
958
|
+
...payload,
|
|
959
|
+
url: fillHash(targetUrl, payload.url)
|
|
960
|
+
},
|
|
961
|
+
preserveScroll: options.preserveScroll,
|
|
962
|
+
preserveState: options.preserveState,
|
|
963
|
+
preserveUrl: options.preserveUrl,
|
|
964
|
+
replace: options.replace === true || options.preserveUrl || sameUrls(payload.url, window.location.href) && !sameHashes(payload.url, window.location.href)
|
|
965
|
+
});
|
|
966
|
+
if (Object.keys(context.view.properties.errors ?? {}).length > 0) {
|
|
967
|
+
const errors = (() => {
|
|
968
|
+
if (options.errorBag && typeof context.view.properties.errors === "object") return context.view.properties.errors[options.errorBag] ?? {};
|
|
969
|
+
return context.view.properties.errors;
|
|
970
|
+
})();
|
|
971
|
+
debug.router("The request returned validation errors.", errors);
|
|
972
|
+
setContext({ pendingNavigation: {
|
|
973
|
+
...context.pendingNavigation,
|
|
974
|
+
status: "error"
|
|
975
|
+
} });
|
|
976
|
+
await runHooks("error", options.hooks, errors, context);
|
|
977
|
+
} else {
|
|
978
|
+
setContext({ pendingNavigation: {
|
|
979
|
+
...context.pendingNavigation,
|
|
980
|
+
status: "success"
|
|
981
|
+
} });
|
|
982
|
+
await runHooks("success", options.hooks, payload, context);
|
|
983
|
+
}
|
|
984
|
+
return { response };
|
|
985
|
+
} catch (error) {
|
|
986
|
+
await match(error.constructor.name, {
|
|
987
|
+
NavigationCancelledError: async () => {
|
|
988
|
+
debug.router("The request was cancelled through the \"before\" hook.", error);
|
|
989
|
+
await runHooks("abort", options.hooks, context);
|
|
990
|
+
},
|
|
991
|
+
AbortError: async () => {
|
|
992
|
+
debug.router("The request was aborted.", error);
|
|
993
|
+
await runHooks("abort", options.hooks, context);
|
|
994
|
+
},
|
|
995
|
+
NotAHybridResponseError: async () => {
|
|
996
|
+
debug.router("The response was not hybrid.");
|
|
997
|
+
console.error(error);
|
|
998
|
+
await runHooks("invalid", options.hooks, error, context);
|
|
999
|
+
if (context.responseErrorModals) showResponseErrorModal(error.response.data);
|
|
1000
|
+
},
|
|
1001
|
+
default: async () => {
|
|
1002
|
+
if (error?.name === "CanceledError") {
|
|
1003
|
+
debug.router("The request was cancelled.", error);
|
|
1004
|
+
await runHooks("abort", options.hooks, context);
|
|
1005
|
+
} else {
|
|
1006
|
+
debug.router("An unknown error occured.", error);
|
|
1007
|
+
console.error(error);
|
|
1008
|
+
await runHooks("exception", options.hooks, error, context);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
await runHooks("fail", options.hooks, context);
|
|
1013
|
+
return { error: {
|
|
1014
|
+
type: error.constructor.name,
|
|
1015
|
+
actual: error
|
|
1016
|
+
} };
|
|
1017
|
+
} finally {
|
|
1018
|
+
debug.router("Ending navigation.");
|
|
1019
|
+
await runHooks("after", options.hooks, context);
|
|
1020
|
+
if (context.pendingNavigation?.id === navigationId) setContext({ pendingNavigation: void 0 });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/** Checks if the response contains a hybrid header. */
|
|
939
1024
|
function isHybridResponse(response) {
|
|
940
|
-
|
|
1025
|
+
return !!response?.headers[HYBRIDLY_HEADER];
|
|
941
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Makes an internal navigation that swaps the view and updates the context.
|
|
1029
|
+
* @internal
|
|
1030
|
+
*/
|
|
942
1031
|
async function navigate(options) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1032
|
+
const context = getRouterContext();
|
|
1033
|
+
options.hasDialog ??= !!options.payload?.dialog;
|
|
1034
|
+
debug.router("Making an internal navigation:", {
|
|
1035
|
+
context,
|
|
1036
|
+
options
|
|
1037
|
+
});
|
|
1038
|
+
await runHooks("navigating", {}, options, context);
|
|
1039
|
+
options.payload ??= payloadFromContext();
|
|
1040
|
+
options.payload.view ??= payloadFromContext().view;
|
|
1041
|
+
function evaluateConditionalOption(option) {
|
|
1042
|
+
return typeof option === "function" ? option(options) : option;
|
|
1043
|
+
}
|
|
1044
|
+
const shouldPreserveState = evaluateConditionalOption(options.preserveState);
|
|
1045
|
+
const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
|
|
1046
|
+
const shouldReplaceHistory = evaluateConditionalOption(options.replace);
|
|
1047
|
+
const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
|
|
1048
|
+
const shouldPreserveView = !options.payload.view.component;
|
|
1049
|
+
if (shouldPreserveState && getHistoryMemo() && options.payload.view.component === context.view.component) {
|
|
1050
|
+
debug.history("Setting the memo from this history entry into the current context.");
|
|
1051
|
+
setContext({ memo: getHistoryMemo() });
|
|
1052
|
+
}
|
|
1053
|
+
if (shouldReplaceUrl) {
|
|
1054
|
+
debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
|
|
1055
|
+
options.payload.url = context.url;
|
|
1056
|
+
}
|
|
1057
|
+
setContext({
|
|
1058
|
+
...shouldPreserveView ? {
|
|
1059
|
+
view: {
|
|
1060
|
+
component: context.view.component,
|
|
1061
|
+
properties: merge(context.view.properties, options.payload.view.properties),
|
|
1062
|
+
deferred: context.view.deferred
|
|
1063
|
+
},
|
|
1064
|
+
url: context.url,
|
|
1065
|
+
version: options.payload.version,
|
|
1066
|
+
dialog: context.dialog
|
|
1067
|
+
} : options.payload,
|
|
1068
|
+
memo: {}
|
|
1069
|
+
});
|
|
1070
|
+
if (options.updateHistoryState !== false) {
|
|
1071
|
+
debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
|
|
1072
|
+
setHistoryState({ replace: shouldReplaceHistory });
|
|
1073
|
+
}
|
|
1074
|
+
if (context.view.deferred?.length) {
|
|
1075
|
+
debug.router("Request has deferred properties, queueing a partial reload:", context.view.deferred);
|
|
1076
|
+
context.adapter.executeOnMounted(async () => {
|
|
1077
|
+
await performHybridNavigation({
|
|
1078
|
+
preserveScroll: true,
|
|
1079
|
+
preserveState: true,
|
|
1080
|
+
replace: true,
|
|
1081
|
+
only: context.view.deferred
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
const viewComponent = !shouldPreserveView ? await context.adapter.resolveComponent(context.view.component) : void 0;
|
|
1086
|
+
if (viewComponent) debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
|
|
1087
|
+
await context.adapter.onViewSwap({
|
|
1088
|
+
component: viewComponent,
|
|
1089
|
+
dialog: context.dialog,
|
|
1090
|
+
properties: options.payload?.view?.properties,
|
|
1091
|
+
preserveState: shouldPreserveState,
|
|
1092
|
+
onMounted: (hookOptions) => runHooks("mounted", {}, {
|
|
1093
|
+
...options,
|
|
1094
|
+
...hookOptions
|
|
1095
|
+
}, context)
|
|
1096
|
+
});
|
|
1097
|
+
if (options.type === "back-forward") restoreScrollPositions();
|
|
1098
|
+
else if (!shouldPreserveScroll) resetScrollPositions();
|
|
1099
|
+
await runHooks("navigated", {}, options, context);
|
|
1008
1100
|
}
|
|
1009
1101
|
async function performHybridRequest(targetUrl, options, abortController) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1102
|
+
const context = getInternalRouterContext();
|
|
1103
|
+
const preloaded = options.method === "GET" ? getPreloadedRequest(targetUrl) : false;
|
|
1104
|
+
if (preloaded) {
|
|
1105
|
+
debug.router(`Found a pre-loaded request for [${targetUrl}]`);
|
|
1106
|
+
discardPreloadedRequest(targetUrl);
|
|
1107
|
+
return preloaded;
|
|
1108
|
+
}
|
|
1109
|
+
return await context.axios.request({
|
|
1110
|
+
url: targetUrl.toString(),
|
|
1111
|
+
method: options.method,
|
|
1112
|
+
data: options.method === "GET" ? {} : options.data,
|
|
1113
|
+
params: options.method === "GET" ? options.data : {},
|
|
1114
|
+
signal: abortController?.signal,
|
|
1115
|
+
headers: {
|
|
1116
|
+
...options.headers,
|
|
1117
|
+
...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
|
|
1118
|
+
...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
|
|
1119
|
+
...when(options.only !== void 0 || options.except !== void 0, {
|
|
1120
|
+
[PARTIAL_COMPONENT_HEADER]: context.view.component,
|
|
1121
|
+
...when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
|
|
1122
|
+
...when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
|
|
1123
|
+
}, {}),
|
|
1124
|
+
...when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
|
|
1125
|
+
...when(context.version, { [VERSION_HEADER]: context.version }, {}),
|
|
1126
|
+
[HYBRIDLY_HEADER]: true,
|
|
1127
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
1128
|
+
"Accept": "text/html, application/xhtml+xml"
|
|
1129
|
+
},
|
|
1130
|
+
responseType: "arraybuffer",
|
|
1131
|
+
validateStatus: () => true,
|
|
1132
|
+
onUploadProgress: async (event) => {
|
|
1133
|
+
await runHooks("progress", options.hooks, {
|
|
1134
|
+
event,
|
|
1135
|
+
percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
|
|
1136
|
+
}, context);
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
/** Initializes the router by reading the context and registering events if necessary. */
|
|
1048
1141
|
async function initializeRouter() {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
await runHooks("ready", {}, context);
|
|
1067
|
-
return context;
|
|
1068
|
-
}
|
|
1142
|
+
const context = getRouterContext();
|
|
1143
|
+
if (isBackForwardNavigation()) handleBackForwardNavigation();
|
|
1144
|
+
else if (isExternalNavigation()) handleExternalNavigation();
|
|
1145
|
+
else {
|
|
1146
|
+
debug.router("Handling the initial navigation.");
|
|
1147
|
+
setContext({ url: makeUrl(context.url, { hash: window.location.hash }).toString() });
|
|
1148
|
+
await navigate({
|
|
1149
|
+
type: "initial",
|
|
1150
|
+
preserveState: true,
|
|
1151
|
+
replace: sameUrls(context.url, window.location.href)
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
registerEventListeners();
|
|
1155
|
+
await runHooks("ready", {}, context);
|
|
1156
|
+
return context;
|
|
1157
|
+
}
|
|
1158
|
+
/** Performs a local navigation to the given component without a round-trip. */
|
|
1069
1159
|
async function performLocalNavigation(targetUrl, options) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1160
|
+
const context = getRouterContext();
|
|
1161
|
+
const url = normalizeUrl(targetUrl);
|
|
1162
|
+
return await navigate({
|
|
1163
|
+
...options,
|
|
1164
|
+
type: "local",
|
|
1165
|
+
payload: {
|
|
1166
|
+
version: context.version,
|
|
1167
|
+
dialog: options?.dialog === false ? void 0 : options?.dialog ?? context.dialog,
|
|
1168
|
+
url,
|
|
1169
|
+
view: {
|
|
1170
|
+
component: options?.component ?? context.view.component,
|
|
1171
|
+
properties: options?.properties ?? context.view.properties,
|
|
1172
|
+
deferred: []
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1086
1176
|
}
|
|
1087
1177
|
|
|
1178
|
+
//#endregion
|
|
1179
|
+
//#region src/authorization.ts
|
|
1180
|
+
/**
|
|
1181
|
+
* Checks whether the given data has the authorization for the given action.
|
|
1182
|
+
* If the data object has no authorization definition corresponding to the given action, this method will return `false`.
|
|
1183
|
+
*/
|
|
1088
1184
|
function can(resource, action) {
|
|
1089
|
-
|
|
1185
|
+
return resource.authorization?.[action] ?? false;
|
|
1090
1186
|
}
|
|
1091
1187
|
|
|
1092
|
-
|
|
1188
|
+
//#endregion
|
|
1189
|
+
export { can, constants_exports as constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
|