@hybridly/core 0.10.0-beta.14 → 0.10.0-beta.16
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.d.mts +270 -141
- package/dist/index.mjs +1020 -562
- package/package.json +49 -51
package/dist/index.mjs
CHANGED
|
@@ -1,59 +1,355 @@
|
|
|
1
1
|
import { t as __exportAll } from "./_chunks/chunk.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { parse, stringify } from "
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
2
|
+
import { debug, hasFiles, merge, mergeObject, objectToFormData, random, showResponseErrorModal, wrap } from "@hybridly/utils";
|
|
3
|
+
import { trimEnd } from "es-toolkit/string";
|
|
4
|
+
import { parse, stringify } from "picoquery";
|
|
5
|
+
import { debounce } from "es-toolkit/function";
|
|
6
|
+
import { parse as parse$1, stringify as stringify$1 } from "superjson";
|
|
7
|
+
import { get, set, uniqBy } from "es-toolkit/compat";
|
|
8
|
+
//#region src/query.ts
|
|
9
|
+
function parseQueryString(query) {
|
|
10
|
+
const source = query.startsWith("?") ? query.slice(1) : query;
|
|
11
|
+
if (!source) return {};
|
|
12
|
+
return parse(source, {
|
|
13
|
+
nesting: true,
|
|
14
|
+
nestingSyntax: "index",
|
|
15
|
+
arrayRepeat: true,
|
|
16
|
+
arrayRepeatSyntax: "bracket"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function stringifyQueryString(value, options = {}) {
|
|
20
|
+
const normalizedQuery = unescapeBracketSyntaxInKeys(stringify(normalizeQueryValue(value), {
|
|
21
|
+
nesting: true,
|
|
22
|
+
nestingSyntax: "index",
|
|
23
|
+
arrayRepeat: (options.arrayFormat ?? "brackets") === "brackets",
|
|
24
|
+
arrayRepeatSyntax: "bracket"
|
|
25
|
+
}));
|
|
26
|
+
if (!normalizedQuery) return "";
|
|
27
|
+
return options.addQueryPrefix ? `?${normalizedQuery}` : normalizedQuery;
|
|
28
|
+
}
|
|
29
|
+
function unescapeBracketSyntaxInKeys(query) {
|
|
30
|
+
if (!query) return query;
|
|
31
|
+
return query.split("&").map((entry) => {
|
|
32
|
+
const separator = entry.indexOf("=");
|
|
33
|
+
if (separator < 0) return decodeBracketSyntax(entry);
|
|
34
|
+
const key = entry.slice(0, separator);
|
|
35
|
+
const value = entry.slice(separator);
|
|
36
|
+
return `${decodeBracketSyntax(key)}${value}`;
|
|
37
|
+
}).join("&");
|
|
38
|
+
}
|
|
39
|
+
function decodeBracketSyntax(value) {
|
|
40
|
+
return value.replace(/%5B/gi, "[").replace(/%5D/gi, "]");
|
|
41
|
+
}
|
|
42
|
+
function normalizeQueryValue(value) {
|
|
43
|
+
if (value instanceof Set) return [...value].map((entry) => normalizeQueryValue(entry));
|
|
44
|
+
if (Array.isArray(value)) return value.map((entry) => normalizeQueryValue(entry));
|
|
45
|
+
if (isPlainObject(value)) return Object.entries(value).reduce((result, [key, entry]) => ({
|
|
46
|
+
...result,
|
|
47
|
+
[key]: normalizeQueryValue(entry)
|
|
48
|
+
}), {});
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function isPlainObject(value) {
|
|
52
|
+
if (typeof value !== "object" || value === null) return false;
|
|
53
|
+
const prototype = Object.getPrototypeOf(value);
|
|
54
|
+
return prototype === null || prototype === Object.prototype;
|
|
55
|
+
}
|
|
33
56
|
//#endregion
|
|
34
|
-
//#region src/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
//#region src/url.ts
|
|
58
|
+
/** Normalizes the given input to an URL. */
|
|
59
|
+
function normalizeUrl(href, trailingSlash) {
|
|
60
|
+
return makeUrl(href, { trailingSlash }).toString();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Converts an input to an URL, optionally changing its properties after initialization.
|
|
64
|
+
*/
|
|
65
|
+
function makeUrl(href, transformations = {}) {
|
|
66
|
+
try {
|
|
67
|
+
const base = document?.location?.href === "//" ? void 0 : document.location.href;
|
|
68
|
+
const url = new URL(String(href), base);
|
|
69
|
+
transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
|
|
70
|
+
Object.entries(transformations).forEach(([key, value]) => {
|
|
71
|
+
if (key === "query") {
|
|
72
|
+
const currentQueryParameters = merge(parseQueryString(url.search), value, { mergePlainObjects: true });
|
|
73
|
+
key = "search";
|
|
74
|
+
value = stringifyQueryString(currentQueryParameters, { arrayFormat: "brackets" });
|
|
75
|
+
}
|
|
76
|
+
Reflect.set(url, key, value);
|
|
77
|
+
});
|
|
78
|
+
if (transformations.trailingSlash === false) {
|
|
79
|
+
const _url = trimEnd(url.toString().replace(/\/\?/, "?"), "/");
|
|
80
|
+
url.toString = () => _url;
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new TypeError(`${href} is not resolvable to a valid URL.`);
|
|
39
85
|
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checks if the given URLs have the same origin and path.
|
|
89
|
+
*/
|
|
90
|
+
function sameUrls(...hrefs) {
|
|
91
|
+
if (hrefs.length < 2) return true;
|
|
92
|
+
try {
|
|
93
|
+
return hrefs.every((href) => {
|
|
94
|
+
return makeUrl(href, { hash: "" }).toJSON() === makeUrl(hrefs.at(0), { hash: "" }).toJSON();
|
|
95
|
+
});
|
|
96
|
+
} catch {}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks if the given URLs have the same origin, path, and hash.
|
|
101
|
+
*/
|
|
102
|
+
function sameHashes(...hrefs) {
|
|
103
|
+
if (hrefs.length < 2) return true;
|
|
104
|
+
try {
|
|
105
|
+
return hrefs.every((href) => {
|
|
106
|
+
return makeUrl(href).toJSON() === makeUrl(hrefs.at(0)).toJSON();
|
|
107
|
+
});
|
|
108
|
+
} catch {}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* If the back-end did not specify a hash, if the navigation specified one,
|
|
113
|
+
* and both URLs lead to the same endpoint, we update the target URL
|
|
114
|
+
* to use the hash of the initially-requested URL.
|
|
115
|
+
*/
|
|
116
|
+
function fillHash(currentUrl, targetUrl) {
|
|
117
|
+
currentUrl = makeUrl(currentUrl);
|
|
118
|
+
targetUrl = makeUrl(targetUrl);
|
|
119
|
+
if (currentUrl.hash && !targetUrl.hash && sameUrls(targetUrl, currentUrl)) targetUrl.hash = currentUrl.hash;
|
|
120
|
+
return targetUrl.toString();
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/http/client.ts
|
|
124
|
+
var HttpError = class extends Error {
|
|
125
|
+
constructor(message, options) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.isHttpError = true;
|
|
128
|
+
this.name = "HttpError";
|
|
129
|
+
this.config = options.config;
|
|
130
|
+
this.request = options.request;
|
|
131
|
+
this.response = options.response;
|
|
132
|
+
this.code = options.code ?? "ERR_NETWORK";
|
|
133
|
+
this.kind = options.kind ?? "network";
|
|
134
|
+
this.reason = options.reason;
|
|
45
135
|
}
|
|
46
136
|
};
|
|
47
|
-
var
|
|
48
|
-
constructor(
|
|
49
|
-
super(
|
|
137
|
+
var HttpAbortError = class extends HttpError {
|
|
138
|
+
constructor(options) {
|
|
139
|
+
super("The request was aborted.", {
|
|
140
|
+
...options,
|
|
141
|
+
code: options.code ?? "ERR_ABORTED",
|
|
142
|
+
kind: options.kind ?? "abort"
|
|
143
|
+
});
|
|
144
|
+
this.name = "AbortError";
|
|
50
145
|
}
|
|
51
146
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
147
|
+
function isHttpError(error) {
|
|
148
|
+
return error instanceof Error && error.isHttpError === true;
|
|
149
|
+
}
|
|
150
|
+
function isHttpAbortError(error) {
|
|
151
|
+
return isHttpError(error) && error.kind === "abort";
|
|
152
|
+
}
|
|
153
|
+
function createXhrHttpClient() {
|
|
154
|
+
return { request: async (config) => await requestWithXhr(config) };
|
|
155
|
+
}
|
|
156
|
+
function requestWithXhr(config) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
const resolveOnce = (response) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
cleanupAbort?.();
|
|
163
|
+
resolve(response);
|
|
164
|
+
};
|
|
165
|
+
const rejectOnce = (error) => {
|
|
166
|
+
if (settled) return;
|
|
167
|
+
settled = true;
|
|
168
|
+
cleanupAbort?.();
|
|
169
|
+
reject(error);
|
|
170
|
+
};
|
|
171
|
+
const xhr = new XMLHttpRequest();
|
|
172
|
+
const method = config.method ?? "GET";
|
|
173
|
+
const url = buildUrl(config.url, config.params);
|
|
174
|
+
const headers = normalizeHeaders(config.headers);
|
|
175
|
+
const body = toRequestBody(config.data, headers);
|
|
176
|
+
const cleanupAbort = registerAbortSignal(config.signal, xhr, (reason) => {
|
|
177
|
+
rejectOnce(new HttpAbortError({
|
|
178
|
+
config,
|
|
179
|
+
request: xhr,
|
|
180
|
+
reason
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
if (config.signal?.aborted) {
|
|
184
|
+
rejectOnce(new HttpAbortError({
|
|
185
|
+
config,
|
|
186
|
+
request: xhr,
|
|
187
|
+
reason: config.signal.reason
|
|
188
|
+
}));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
xhr.open(method, url, true);
|
|
192
|
+
xhr.responseType = "arraybuffer";
|
|
193
|
+
for (const [key, value] of Object.entries(headers)) xhr.setRequestHeader(key, value);
|
|
194
|
+
if (config.onUploadProgress) xhr.upload.onprogress = (event) => {
|
|
195
|
+
const total = event.lengthComputable ? event.total : void 0;
|
|
196
|
+
const percentage = total && total > 0 ? Math.round(event.loaded / total * 100) : 0;
|
|
197
|
+
config.onUploadProgress?.({
|
|
198
|
+
loaded: event.loaded,
|
|
199
|
+
total,
|
|
200
|
+
lengthComputable: event.lengthComputable,
|
|
201
|
+
percentage
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
xhr.onload = () => {
|
|
205
|
+
resolveOnce(toResponse(xhr, config));
|
|
206
|
+
};
|
|
207
|
+
xhr.onerror = () => {
|
|
208
|
+
rejectOnce(new HttpError("Network Error", {
|
|
209
|
+
config,
|
|
210
|
+
request: xhr,
|
|
211
|
+
code: "ERR_NETWORK",
|
|
212
|
+
kind: "network"
|
|
213
|
+
}));
|
|
214
|
+
};
|
|
215
|
+
xhr.onabort = () => {
|
|
216
|
+
rejectOnce(new HttpAbortError({
|
|
217
|
+
config,
|
|
218
|
+
request: xhr,
|
|
219
|
+
reason: config.signal?.reason
|
|
220
|
+
}));
|
|
221
|
+
};
|
|
222
|
+
xhr.ontimeout = () => {
|
|
223
|
+
rejectOnce(new HttpError("The request timed out.", {
|
|
224
|
+
config,
|
|
225
|
+
request: xhr,
|
|
226
|
+
code: "ECONNABORTED",
|
|
227
|
+
kind: "timeout"
|
|
228
|
+
}));
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
xhr.send(body);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
rejectOnce(new HttpError("Network Error", {
|
|
234
|
+
config,
|
|
235
|
+
request: xhr,
|
|
236
|
+
code: "ERR_NETWORK",
|
|
237
|
+
kind: "network",
|
|
238
|
+
reason: error
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function registerAbortSignal(signal, xhr, onAbort) {
|
|
244
|
+
if (!signal) return;
|
|
245
|
+
if (signal.aborted) {
|
|
246
|
+
onAbort(signal.reason);
|
|
247
|
+
return;
|
|
55
248
|
}
|
|
56
|
-
|
|
249
|
+
const abort = () => {
|
|
250
|
+
onAbort(signal.reason);
|
|
251
|
+
xhr.abort();
|
|
252
|
+
};
|
|
253
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
254
|
+
return () => signal.removeEventListener("abort", abort);
|
|
255
|
+
}
|
|
256
|
+
function buildUrl(url, params) {
|
|
257
|
+
if (!params || Object.keys(params).length === 0) return url;
|
|
258
|
+
return makeUrl(url, { query: params }).toString();
|
|
259
|
+
}
|
|
260
|
+
function normalizeHeaders(headers) {
|
|
261
|
+
if (!headers) return {};
|
|
262
|
+
return Object.entries(headers).reduce((result, [key, value]) => ({
|
|
263
|
+
...result,
|
|
264
|
+
[key]: String(value)
|
|
265
|
+
}), {});
|
|
266
|
+
}
|
|
267
|
+
function toRequestBody(data, headers) {
|
|
268
|
+
if (data === void 0 || data === null) return null;
|
|
269
|
+
if (typeof FormData !== "undefined" && data instanceof FormData) return data;
|
|
270
|
+
if (typeof data === "string" || data instanceof Blob || data instanceof URLSearchParams || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data;
|
|
271
|
+
if (typeof data === "object") {
|
|
272
|
+
if (!hasHeader(headers, "content-type")) headers["Content-Type"] = "application/json";
|
|
273
|
+
return JSON.stringify(data);
|
|
274
|
+
}
|
|
275
|
+
return String(data);
|
|
276
|
+
}
|
|
277
|
+
function hasHeader(headers, headerName) {
|
|
278
|
+
const expected = headerName.toLowerCase();
|
|
279
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === expected);
|
|
280
|
+
}
|
|
281
|
+
function toResponse(xhr, config) {
|
|
282
|
+
const headers = createHttpHeaders(parseResponseHeaders(xhr.getAllResponseHeaders()));
|
|
283
|
+
const rawData = toArrayBuffer(xhr.response);
|
|
284
|
+
return {
|
|
285
|
+
data: decodeResponseBody(rawData, headers),
|
|
286
|
+
rawData,
|
|
287
|
+
status: xhr.status,
|
|
288
|
+
statusText: xhr.statusText,
|
|
289
|
+
headers,
|
|
290
|
+
request: xhr,
|
|
291
|
+
config,
|
|
292
|
+
toBlob: (type) => {
|
|
293
|
+
const resolvedType = type ?? headers.get("content-type");
|
|
294
|
+
return new Blob([rawData], { ...resolvedType ? { type: resolvedType } : {} });
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function createHttpHeaders(values) {
|
|
299
|
+
const all = Object.freeze({ ...values });
|
|
300
|
+
const get = (name) => all[name.toLowerCase()];
|
|
301
|
+
const has = (name) => get(name) !== void 0;
|
|
302
|
+
const isContentType = (matcher) => {
|
|
303
|
+
const contentType = get("content-type");
|
|
304
|
+
if (!contentType) return false;
|
|
305
|
+
if (typeof matcher === "string") return contentType.toLowerCase().includes(matcher.toLowerCase());
|
|
306
|
+
return matcher.test(contentType);
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
all,
|
|
310
|
+
get,
|
|
311
|
+
has,
|
|
312
|
+
isContentType
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function toArrayBuffer(data) {
|
|
316
|
+
if (data instanceof ArrayBuffer) return data;
|
|
317
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice().buffer;
|
|
318
|
+
if (typeof data === "string") return new TextEncoder().encode(data).buffer;
|
|
319
|
+
return /* @__PURE__ */ new ArrayBuffer(0);
|
|
320
|
+
}
|
|
321
|
+
function decodeResponseBody(rawData, headers) {
|
|
322
|
+
if (headers.isContentType(/(^|\b|\+)json(\b|;|$)/i)) {
|
|
323
|
+
const text = decodeText(rawData, headers);
|
|
324
|
+
try {
|
|
325
|
+
return JSON.parse(text);
|
|
326
|
+
} catch {
|
|
327
|
+
return text;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (headers.isContentType(/^text\//i) || headers.isContentType(/xml|javascript|x-www-form-urlencoded/i)) return decodeText(rawData, headers);
|
|
331
|
+
return rawData;
|
|
332
|
+
}
|
|
333
|
+
function decodeText(rawData, headers) {
|
|
334
|
+
const charset = headers.get("content-type")?.match(/charset=([^;]+)/i)?.[1]?.trim();
|
|
335
|
+
try {
|
|
336
|
+
return new TextDecoder(charset).decode(rawData);
|
|
337
|
+
} catch {
|
|
338
|
+
return new TextDecoder().decode(rawData);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function parseResponseHeaders(rawHeaders) {
|
|
342
|
+
const lines = rawHeaders.split("\r\n").filter(Boolean);
|
|
343
|
+
const headers = {};
|
|
344
|
+
for (const line of lines) {
|
|
345
|
+
const separator = line.indexOf(":");
|
|
346
|
+
if (separator < 0) continue;
|
|
347
|
+
const name = line.slice(0, separator).trim().toLowerCase();
|
|
348
|
+
const value = line.slice(separator + 1).trim();
|
|
349
|
+
headers[name] = name in headers ? `${headers[name]}, ${value}` : value;
|
|
350
|
+
}
|
|
351
|
+
return headers;
|
|
352
|
+
}
|
|
57
353
|
//#endregion
|
|
58
354
|
//#region src/plugins/plugin.ts
|
|
59
355
|
function definePlugin(plugin) {
|
|
@@ -119,6 +415,36 @@ function registerHook(hook, fn, options) {
|
|
|
119
415
|
return appendCallbackToHooks(hook, fn);
|
|
120
416
|
}
|
|
121
417
|
//#endregion
|
|
418
|
+
//#region src/constants.ts
|
|
419
|
+
var constants_exports = /* @__PURE__ */ __exportAll({
|
|
420
|
+
DIALOG_KEY_HEADER: () => DIALOG_KEY_HEADER,
|
|
421
|
+
DIALOG_REDIRECT_HEADER: () => DIALOG_REDIRECT_HEADER,
|
|
422
|
+
ERROR_BAG_HEADER: () => ERROR_BAG_HEADER,
|
|
423
|
+
EXCEPT_DATA_HEADER: () => EXCEPT_DATA_HEADER,
|
|
424
|
+
EXTERNAL_NAVIGATION_HEADER: () => EXTERNAL_NAVIGATION_HEADER,
|
|
425
|
+
EXTERNAL_NAVIGATION_TARGET_HEADER: () => EXTERNAL_NAVIGATION_TARGET_HEADER,
|
|
426
|
+
HYBRIDLY_HEADER: () => HYBRIDLY_HEADER,
|
|
427
|
+
ONLY_DATA_HEADER: () => ONLY_DATA_HEADER,
|
|
428
|
+
PARTIAL_COMPONENT_HEADER: () => PARTIAL_COMPONENT_HEADER,
|
|
429
|
+
RESET_HEADER: () => RESET_HEADER,
|
|
430
|
+
SCROLL_REGION_ATTRIBUTE: () => SCROLL_REGION_ATTRIBUTE,
|
|
431
|
+
STORAGE_EXTERNAL_KEY: () => STORAGE_EXTERNAL_KEY,
|
|
432
|
+
VERSION_HEADER: () => VERSION_HEADER
|
|
433
|
+
});
|
|
434
|
+
const STORAGE_EXTERNAL_KEY = "hybridly:external";
|
|
435
|
+
const HYBRIDLY_HEADER = "x-hybrid";
|
|
436
|
+
const EXTERNAL_NAVIGATION_HEADER = `${HYBRIDLY_HEADER}-external`;
|
|
437
|
+
const EXTERNAL_NAVIGATION_TARGET_HEADER = `${HYBRIDLY_HEADER}-external-target`;
|
|
438
|
+
const PARTIAL_COMPONENT_HEADER = `${HYBRIDLY_HEADER}-partial-component`;
|
|
439
|
+
const RESET_HEADER = `${HYBRIDLY_HEADER}-reset`;
|
|
440
|
+
const ONLY_DATA_HEADER = `${HYBRIDLY_HEADER}-only-data`;
|
|
441
|
+
const DIALOG_KEY_HEADER = `${HYBRIDLY_HEADER}-dialog-key`;
|
|
442
|
+
const DIALOG_REDIRECT_HEADER = `${HYBRIDLY_HEADER}-dialog-redirect`;
|
|
443
|
+
const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
|
|
444
|
+
const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
|
|
445
|
+
const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
|
|
446
|
+
const SCROLL_REGION_ATTRIBUTE = "scroll-region";
|
|
447
|
+
//#endregion
|
|
122
448
|
//#region src/scroll.ts
|
|
123
449
|
/** Saves the current view's scrollbar positions into the history state. */
|
|
124
450
|
function saveScrollPositions() {
|
|
@@ -158,6 +484,7 @@ function resetScrollPositions() {
|
|
|
158
484
|
}
|
|
159
485
|
/** Restores the scroll positions stored in the context. */
|
|
160
486
|
async function restoreScrollPositions() {
|
|
487
|
+
debug.scroll("Restoring scroll positions.");
|
|
161
488
|
const context = getRouterContext();
|
|
162
489
|
const regions = getScrollRegions();
|
|
163
490
|
if (!context.scrollRegions) {
|
|
@@ -173,74 +500,654 @@ async function restoreScrollPositions() {
|
|
|
173
500
|
});
|
|
174
501
|
}
|
|
175
502
|
//#endregion
|
|
176
|
-
//#region src/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
503
|
+
//#region src/errors.ts
|
|
504
|
+
var HybridlyError = class extends Error {
|
|
505
|
+
constructor(message, options) {
|
|
506
|
+
super(message);
|
|
507
|
+
this.isHybridlyError = true;
|
|
508
|
+
this.name = "HybridlyError";
|
|
509
|
+
this.kind = options.kind;
|
|
510
|
+
this.code = options.code;
|
|
511
|
+
this.details = options.details;
|
|
512
|
+
this.cause = options.cause;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
var InvalidResponseError = class extends HybridlyError {
|
|
516
|
+
constructor(options = {}) {
|
|
517
|
+
super("The response was not a valid hybrid response.", {
|
|
518
|
+
...options,
|
|
519
|
+
kind: "response.invalid",
|
|
520
|
+
code: "HYB_INVALID_RESPONSE"
|
|
521
|
+
});
|
|
522
|
+
this.name = "InvalidResponseError";
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var NavigationCancelledError = class extends HybridlyError {
|
|
526
|
+
constructor(message = "The navigation was cancelled.", options = {}) {
|
|
527
|
+
super(message, {
|
|
528
|
+
...options,
|
|
529
|
+
kind: "navigation.cancelled",
|
|
530
|
+
code: "HYB_NAVIGATION_CANCELLED"
|
|
531
|
+
});
|
|
532
|
+
this.name = "NavigationCancelledError";
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
var RoutingNotInitialized = class extends HybridlyError {
|
|
536
|
+
constructor(options = {}) {
|
|
537
|
+
super("Routing is not initialized. Make sure the Vite plugin is enabled and that `php artisan route:list` returns no error.", {
|
|
538
|
+
...options,
|
|
539
|
+
kind: "routing.not-initialized",
|
|
540
|
+
code: "HYB_ROUTING_NOT_INITIALIZED"
|
|
541
|
+
});
|
|
542
|
+
this.name = "RoutingNotInitialized";
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
var RouteNotFound = class extends HybridlyError {
|
|
546
|
+
constructor(name, options = {}) {
|
|
547
|
+
super(`Route [${name}] does not exist.`, {
|
|
548
|
+
...options,
|
|
549
|
+
kind: "routing.route-not-found",
|
|
550
|
+
code: "HYB_ROUTE_NOT_FOUND",
|
|
551
|
+
details: { name }
|
|
552
|
+
});
|
|
553
|
+
this.name = "RouteNotFound";
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var MissingRouteParameter = class extends HybridlyError {
|
|
557
|
+
constructor(parameter, routeName, options = {}) {
|
|
558
|
+
super(`Parameter [${parameter}] is required for route [${routeName}].`, {
|
|
559
|
+
...options,
|
|
560
|
+
kind: "routing.missing-parameter",
|
|
561
|
+
code: "HYB_MISSING_ROUTE_PARAMETER",
|
|
562
|
+
details: {
|
|
563
|
+
parameter,
|
|
564
|
+
routeName
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
this.name = "MissingRouteParameter";
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
function isHybridlyError(error) {
|
|
571
|
+
return error instanceof Error && error.isHybridlyError === true;
|
|
572
|
+
}
|
|
573
|
+
function isNavigationCancelledError(error) {
|
|
574
|
+
return isHybridlyError(error) && error.kind === "navigation.cancelled";
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/download.ts
|
|
578
|
+
/** Checks if the response wants to redirect to an external URL. */
|
|
579
|
+
function isDownloadResponse(response) {
|
|
580
|
+
return response.status === 200 && response.headers.has("content-disposition");
|
|
581
|
+
}
|
|
582
|
+
/** Handles a download. */
|
|
583
|
+
async function handleDownloadResponse(response) {
|
|
584
|
+
const blob = response.toBlob();
|
|
585
|
+
const urlObject = window.webkitURL || window.URL;
|
|
586
|
+
const link = document.createElement("a");
|
|
587
|
+
link.style.display = "none";
|
|
588
|
+
link.href = urlObject.createObjectURL(blob);
|
|
589
|
+
link.download = getFileNameFromContentDispositionHeader(response.headers.get("content-disposition") ?? "");
|
|
590
|
+
link.click();
|
|
591
|
+
setTimeout(() => {
|
|
592
|
+
urlObject.revokeObjectURL(link.href);
|
|
593
|
+
link.remove();
|
|
594
|
+
}, 0);
|
|
595
|
+
}
|
|
596
|
+
function getFileNameFromContentDispositionHeader(header) {
|
|
597
|
+
return (header.split(";")[1]?.trim().split("=")[1])?.replace(/^"(.*)"$/, "$1") ?? "";
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/utils.ts
|
|
601
|
+
function createPromiseWithResolvers() {
|
|
602
|
+
let resolve;
|
|
603
|
+
let reject;
|
|
604
|
+
return {
|
|
605
|
+
promise: new Promise((_resolve, _reject) => {
|
|
606
|
+
resolve = _resolve;
|
|
607
|
+
reject = _reject;
|
|
608
|
+
}),
|
|
609
|
+
resolve,
|
|
610
|
+
reject
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/router/request/request.ts
|
|
615
|
+
function createPendingHybridRequest(options) {
|
|
616
|
+
const context = getRouterContext();
|
|
617
|
+
const url = makeUrl(options.url ?? context.url, options.transformUrl);
|
|
618
|
+
const { promise, resolve } = createPromiseWithResolvers();
|
|
619
|
+
return {
|
|
620
|
+
url,
|
|
621
|
+
options,
|
|
622
|
+
promise,
|
|
623
|
+
resolve,
|
|
624
|
+
id: random(),
|
|
625
|
+
controller: options.abortController ?? new AbortController(),
|
|
626
|
+
cancelled: false,
|
|
627
|
+
completed: false,
|
|
628
|
+
interrupted: false,
|
|
629
|
+
view: context.view
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function sendHybridRequest(request) {
|
|
633
|
+
const context = getInternalRouterContext();
|
|
634
|
+
return await context.http.request({
|
|
635
|
+
url: request.url.toString(),
|
|
636
|
+
method: request.options.method,
|
|
637
|
+
data: request.options.method === "GET" ? {} : request.options.data,
|
|
638
|
+
params: request.options.method === "GET" ? request.options.data : {},
|
|
639
|
+
signal: request.controller?.signal,
|
|
640
|
+
headers: {
|
|
641
|
+
...request.options.headers,
|
|
642
|
+
...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
|
|
643
|
+
...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
|
|
644
|
+
...mergeObject(request.options.only || request.options.except || request.options.reset, {
|
|
645
|
+
[PARTIAL_COMPONENT_HEADER]: context.view.component,
|
|
646
|
+
...mergeObject(request.options.reset, { [RESET_HEADER]: JSON.stringify(wrap(request.options.reset)) }),
|
|
647
|
+
...mergeObject(request.options.only, { [ONLY_DATA_HEADER]: JSON.stringify(wrap(request.options.only)) }),
|
|
648
|
+
...mergeObject(request.options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(wrap(request.options.except)) })
|
|
649
|
+
}),
|
|
650
|
+
...mergeObject(request.options.errorBag, { [ERROR_BAG_HEADER]: request.options.errorBag }),
|
|
651
|
+
...mergeObject(context.version, { [VERSION_HEADER]: context.version }),
|
|
652
|
+
[HYBRIDLY_HEADER]: "true",
|
|
653
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
654
|
+
"Accept": "text/html, application/xhtml+xml"
|
|
655
|
+
},
|
|
656
|
+
onUploadProgress: async (progress) => {
|
|
657
|
+
await runHooks("progress", request.options.hooks, progress, request, context);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
async function performHybridRequest(request) {
|
|
662
|
+
enqueueRequest(request);
|
|
663
|
+
return request.promise;
|
|
664
|
+
}
|
|
665
|
+
/** Performs every action necessary to make a hybrid navigation. */
|
|
666
|
+
async function performHybridNavigation(options) {
|
|
667
|
+
const context = getRouterContext();
|
|
668
|
+
debug.router("Making a hybrid navigation:", {
|
|
669
|
+
context,
|
|
670
|
+
options
|
|
671
|
+
});
|
|
672
|
+
await transformOptions(options);
|
|
673
|
+
const request = createPendingHybridRequest(options);
|
|
674
|
+
if (!await runHooks("before", options.hooks, request, context)) {
|
|
675
|
+
debug.router("\"before\" event returned false, aborting the navigation.");
|
|
676
|
+
return { error: new NavigationCancelledError("The navigation was cancelled by the \"before\" event.") };
|
|
677
|
+
}
|
|
678
|
+
await runHooks("start", options.hooks, request, context);
|
|
679
|
+
debug.router("Making request with the configured HTTP client.");
|
|
680
|
+
return await performHybridRequest(request);
|
|
180
681
|
}
|
|
181
682
|
/**
|
|
182
|
-
*
|
|
683
|
+
* Transform the options object with convenience changes.
|
|
183
684
|
*/
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
685
|
+
async function transformOptions(options) {
|
|
686
|
+
const context = getRouterContext();
|
|
687
|
+
if (!options.method) {
|
|
688
|
+
debug.router("Setting method to GET because none was provided.");
|
|
689
|
+
options.method = "GET";
|
|
690
|
+
}
|
|
691
|
+
options.method = options.method.toUpperCase();
|
|
692
|
+
options.mode ??= "navigation";
|
|
693
|
+
options.cancelOnNavigation ??= false;
|
|
694
|
+
options.interruptAsyncOnStart ??= "none";
|
|
695
|
+
if (options.mode === "async" && options.progress === void 0) options.progress = false;
|
|
696
|
+
if (options.mode === "async" && options.replace === void 0) options.replace = true;
|
|
697
|
+
if ((hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
|
|
698
|
+
options.data = objectToFormData(options.data);
|
|
699
|
+
debug.router("Converted data to FormData.", options.data);
|
|
700
|
+
}
|
|
701
|
+
if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
|
|
702
|
+
debug.router("Transforming data to query parameters.", options.data);
|
|
703
|
+
options.url = makeUrl(options.url ?? context.url, { query: options.data });
|
|
704
|
+
options.data = {};
|
|
705
|
+
}
|
|
706
|
+
if ([
|
|
707
|
+
"PUT",
|
|
708
|
+
"PATCH",
|
|
709
|
+
"DELETE"
|
|
710
|
+
].includes(options.method) && options.spoof !== false) {
|
|
711
|
+
debug.router(`Automatically spoofing method ${options.method}.`);
|
|
712
|
+
if (options.data instanceof FormData) options.data.append("_method", options.method);
|
|
713
|
+
else if (typeof options.data === "undefined") options.data = { _method: options.method };
|
|
714
|
+
else if (options.data instanceof Object && Object.keys(options.data).length >= 0) Object.assign(options.data, { _method: options.method });
|
|
715
|
+
else debug.router("Could not spoof method because body type is not supported.", options.data);
|
|
716
|
+
options.method = "POST";
|
|
717
|
+
}
|
|
718
|
+
return options;
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/router/view.ts
|
|
722
|
+
/**
|
|
723
|
+
* Makes an internal navigation that swaps the view and updates the context.
|
|
724
|
+
* @internal
|
|
725
|
+
*/
|
|
726
|
+
async function navigate(options) {
|
|
727
|
+
const context = getRouterContext();
|
|
728
|
+
options.hasDialog ??= !!options.payload?.dialog;
|
|
729
|
+
debug.router("Making an internal navigation:", {
|
|
730
|
+
context,
|
|
731
|
+
options
|
|
732
|
+
});
|
|
733
|
+
await runHooks("navigating", {}, options, context);
|
|
734
|
+
options.payload ??= payloadFromContext();
|
|
735
|
+
options.payload.view ??= payloadFromContext().view;
|
|
736
|
+
options.payload.view.properties = options.properties ?? options.payload.view.properties;
|
|
737
|
+
function evaluateConditionalOption(option) {
|
|
738
|
+
return typeof option === "function" ? option(options) : option;
|
|
739
|
+
}
|
|
740
|
+
const shouldPreserveState = evaluateConditionalOption(options.preserveState);
|
|
741
|
+
const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
|
|
742
|
+
const shouldReplaceHistory = evaluateConditionalOption(options.replace);
|
|
743
|
+
const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
|
|
744
|
+
const shouldPreserveView = !options.payload.view.component;
|
|
745
|
+
if (shouldPreserveState && getHistoryMemo() && options.payload.view.component === context.view.component) {
|
|
746
|
+
debug.history("Setting the memo from this history entry into the current context.");
|
|
747
|
+
setContext({ memo: getHistoryMemo() });
|
|
748
|
+
}
|
|
749
|
+
if (shouldReplaceUrl) {
|
|
750
|
+
debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
|
|
751
|
+
options.payload.url = context.url;
|
|
752
|
+
}
|
|
753
|
+
setContext({
|
|
754
|
+
...shouldPreserveView ? {
|
|
755
|
+
view: {
|
|
756
|
+
component: context.view.component,
|
|
757
|
+
properties: merge(context.view.properties, options.payload.view.properties),
|
|
758
|
+
deferred: context.view.deferred,
|
|
759
|
+
mergeable: context.view.mergeable
|
|
760
|
+
},
|
|
761
|
+
url: context.url,
|
|
762
|
+
version: options.payload.version,
|
|
763
|
+
validation: options.payload.validation,
|
|
764
|
+
dialog: context.dialog
|
|
765
|
+
} : options.payload,
|
|
766
|
+
memo: {}
|
|
767
|
+
});
|
|
768
|
+
if (options.updateHistoryState !== false) {
|
|
769
|
+
debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
|
|
770
|
+
setHistoryState({ replace: shouldReplaceHistory });
|
|
771
|
+
}
|
|
772
|
+
if (Object.entries(context.view.deferred ?? {}).length) {
|
|
773
|
+
debug.router("Request has deferred properties, queueing a partial reload:", context.view.deferred);
|
|
774
|
+
context.adapter.executeOnMounted(async () => {
|
|
775
|
+
return Promise.all(Object.entries(context.view.deferred).map(async ([_, properties]) => {
|
|
776
|
+
await performHybridNavigation({
|
|
777
|
+
preserveScroll: true,
|
|
778
|
+
preserveState: true,
|
|
779
|
+
replace: true,
|
|
780
|
+
mode: "async",
|
|
781
|
+
only: properties
|
|
197
782
|
});
|
|
198
|
-
}
|
|
199
|
-
Reflect.set(url, key, value);
|
|
783
|
+
}));
|
|
200
784
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
785
|
+
}
|
|
786
|
+
const viewComponent = !shouldPreserveView ? await context.adapter.resolveComponent(context.view.component) : void 0;
|
|
787
|
+
if (viewComponent) debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
|
|
788
|
+
await context.adapter.onViewSwap({
|
|
789
|
+
component: viewComponent,
|
|
790
|
+
dialog: context.dialog,
|
|
791
|
+
properties: options.payload?.view?.properties,
|
|
792
|
+
preserveState: shouldPreserveState,
|
|
793
|
+
onMounted: (hookOptions) => runHooks("mounted", {}, {
|
|
794
|
+
...options,
|
|
795
|
+
...hookOptions
|
|
796
|
+
}, context)
|
|
797
|
+
});
|
|
798
|
+
if (options.type === "back-forward" || shouldPreserveScroll) restoreScrollPositions();
|
|
799
|
+
else if (!shouldPreserveScroll) resetScrollPositions();
|
|
800
|
+
await runHooks("navigated", {}, options, context);
|
|
801
|
+
}
|
|
802
|
+
/** Performs a local navigation to the given component without a round-trip. */
|
|
803
|
+
async function performLocalNavigation(targetUrl, options) {
|
|
804
|
+
const context = getRouterContext();
|
|
805
|
+
const url = normalizeUrl(targetUrl);
|
|
806
|
+
return await navigate({
|
|
807
|
+
...options,
|
|
808
|
+
type: "local",
|
|
809
|
+
payload: {
|
|
810
|
+
version: context.version,
|
|
811
|
+
validation: context.validation,
|
|
812
|
+
dialog: options?.dialog === false ? void 0 : options?.dialog ?? context.dialog,
|
|
813
|
+
url,
|
|
814
|
+
view: {
|
|
815
|
+
component: options?.component ?? context.view.component,
|
|
816
|
+
properties: options?.properties ?? {},
|
|
817
|
+
deferred: {},
|
|
818
|
+
mergeable: []
|
|
819
|
+
}
|
|
204
820
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/router/response/external.ts
|
|
825
|
+
/**
|
|
826
|
+
* Performs an external navigation by saving options to the storage and
|
|
827
|
+
* making a full page reload. Upon loading, the navigation options
|
|
828
|
+
* will be pulled and a hybrid navigation will be made.
|
|
829
|
+
*/
|
|
830
|
+
async function performExternalNavigation(options) {
|
|
831
|
+
debug.external("Navigating to an external URL:", options);
|
|
832
|
+
if (options.target === "new-tab") {
|
|
833
|
+
const link = document.createElement("a");
|
|
834
|
+
link.style.display = "none";
|
|
835
|
+
link.target = "_blank";
|
|
836
|
+
link.href = options.url;
|
|
837
|
+
link.click();
|
|
838
|
+
setTimeout(() => link.remove(), 0);
|
|
839
|
+
return;
|
|
208
840
|
}
|
|
841
|
+
window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
|
|
842
|
+
window.location.href = options.url;
|
|
843
|
+
if (sameUrls(window.location, options.url)) {
|
|
844
|
+
debug.external("Manually reloading due to the external URL being the same.");
|
|
845
|
+
window.location.reload();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/** Navigates to the given URL without the hybrid protocol. */
|
|
849
|
+
function navigateToExternalUrl(url, data) {
|
|
850
|
+
document.location.href = makeUrl(url, { search: stringifyQueryString(data, { arrayFormat: "brackets" }) }).toString();
|
|
851
|
+
}
|
|
852
|
+
/** Checks if the response wants to redirect to an external URL. */
|
|
853
|
+
function isExternalResponse(response) {
|
|
854
|
+
return response.status === 409 && response.headers.has(EXTERNAL_NAVIGATION_HEADER);
|
|
209
855
|
}
|
|
210
856
|
/**
|
|
211
|
-
*
|
|
857
|
+
* Performs the internal navigation when an external navigation to a hybrid view has been made.
|
|
858
|
+
* This method is meant to be called on router creation.
|
|
212
859
|
*/
|
|
213
|
-
function
|
|
214
|
-
|
|
860
|
+
async function handleExternalNavigation() {
|
|
861
|
+
debug.external("Handling an external navigation.");
|
|
862
|
+
const options = JSON.parse(window.sessionStorage.getItem("hybridly:external") || "{}");
|
|
863
|
+
window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
|
|
864
|
+
debug.external("Options from the session storage:", options);
|
|
865
|
+
setContext({ url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString() });
|
|
866
|
+
await navigate({
|
|
867
|
+
type: "initial",
|
|
868
|
+
preserveState: true,
|
|
869
|
+
preserveScroll: options.preserveScroll
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/** Checks if the navigation being initialized points to an external location. */
|
|
873
|
+
function isExternalNavigation() {
|
|
215
874
|
try {
|
|
216
|
-
return
|
|
217
|
-
return makeUrl(href, { hash: "" }).toJSON() === makeUrl(hrefs.at(0), { hash: "" }).toJSON();
|
|
218
|
-
});
|
|
875
|
+
return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
|
|
219
876
|
} catch {}
|
|
220
877
|
return false;
|
|
221
878
|
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/router/response/response.ts
|
|
881
|
+
async function handleHybridRequestResponse({ request, response }) {
|
|
882
|
+
debug.router("Handling response", response);
|
|
883
|
+
const context = getInternalRouterContext();
|
|
884
|
+
const options = request.options;
|
|
885
|
+
saveScrollPositions();
|
|
886
|
+
if (await runHooks("data", options.hooks, request, response, context) === false) return { response };
|
|
887
|
+
if (isExternalResponse(response)) {
|
|
888
|
+
debug.router("The response is explicitely external.");
|
|
889
|
+
await performExternalNavigation({
|
|
890
|
+
url: fillHash(request.url, response.headers.get(EXTERNAL_NAVIGATION_HEADER)),
|
|
891
|
+
preserveScroll: options.preserveScroll === true,
|
|
892
|
+
target: "current"
|
|
893
|
+
});
|
|
894
|
+
return { response };
|
|
895
|
+
}
|
|
896
|
+
if (isDownloadResponse(response)) {
|
|
897
|
+
debug.router("The response returns a file to download.");
|
|
898
|
+
await handleDownloadResponse(response);
|
|
899
|
+
return { response };
|
|
900
|
+
}
|
|
901
|
+
if (!isHybridResponse(response)) {
|
|
902
|
+
debug.router("The response was not hybrid.");
|
|
903
|
+
console.warn("Hybridly received an invalid response.", response);
|
|
904
|
+
await runHooks("fail", request.options.hooks, new InvalidResponseError(), request, context);
|
|
905
|
+
const prevented = !await runHooks("invalid", request.options.hooks, request, response, context);
|
|
906
|
+
if (context.responseErrorModals && !prevented) showResponseErrorModal(typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2));
|
|
907
|
+
return { response };
|
|
908
|
+
}
|
|
909
|
+
debug.router("The response respects the Hybridly protocol.");
|
|
910
|
+
const payload = response.data;
|
|
911
|
+
const mergedValidation = mergeValidation(context.validation, payload.validation, options.errorBag);
|
|
912
|
+
const incomingErrors = resolveErrors(payload.validation, options.errorBag);
|
|
913
|
+
if (options.mode !== "async" || context.view.component === request.view.component) {
|
|
914
|
+
const properties = (() => {
|
|
915
|
+
if (!payload.view && !isPartial(options)) return;
|
|
916
|
+
if (!payload.view.component || payload.view.component === context.view.component) return resolveProperties(context.view.properties, payload.view);
|
|
917
|
+
})();
|
|
918
|
+
if (properties) debug.router("Merged properties:", properties);
|
|
919
|
+
await navigate({
|
|
920
|
+
type: "server",
|
|
921
|
+
properties,
|
|
922
|
+
payload: {
|
|
923
|
+
...payload,
|
|
924
|
+
validation: mergedValidation,
|
|
925
|
+
url: fillHash(request.url, payload.url)
|
|
926
|
+
},
|
|
927
|
+
preserveScroll: options.preserveScroll,
|
|
928
|
+
preserveState: options.preserveState,
|
|
929
|
+
preserveUrl: options.preserveUrl,
|
|
930
|
+
replace: options.replace === true || options.preserveUrl || sameUrls(payload.url, window.location.href) && !sameHashes(payload.url, window.location.href),
|
|
931
|
+
viewTransition: options.viewTransition
|
|
932
|
+
});
|
|
933
|
+
} else debug.router("Discarding navigation from an asynchronous request initiated on a previous page.");
|
|
934
|
+
if (Object.keys(incomingErrors).length > 0) {
|
|
935
|
+
debug.router("The request returned validation errors.", incomingErrors);
|
|
936
|
+
const errors = resolveErrors(context.validation, options.errorBag);
|
|
937
|
+
const resolvedErrors = Object.keys(errors).length > 0 ? errors : incomingErrors;
|
|
938
|
+
await runHooks("validation-error", options.hooks, resolvedErrors, request, context);
|
|
939
|
+
} else await runHooks("success", options.hooks, payload, request, response, context);
|
|
940
|
+
return { response };
|
|
941
|
+
}
|
|
942
|
+
/** Checks if the response contains a hybrid header. */
|
|
943
|
+
function isHybridResponse(response) {
|
|
944
|
+
return response.headers.has(HYBRIDLY_HEADER);
|
|
945
|
+
}
|
|
946
|
+
function isPartial(options) {
|
|
947
|
+
return options.only !== void 0 || options.except !== void 0;
|
|
948
|
+
}
|
|
949
|
+
function resolveProperties(original, payload) {
|
|
950
|
+
const mergedPayloadProperties = merge(original, payload.properties);
|
|
951
|
+
(payload.mergeable ?? []).forEach(([mergeableProperty, prepends, uniqueBy]) => {
|
|
952
|
+
const originalValue = get(original, mergeableProperty);
|
|
953
|
+
const newValue = get(payload.properties, mergeableProperty);
|
|
954
|
+
const mergeArrays = (current, incoming) => {
|
|
955
|
+
const merged = prepends === true ? [...incoming, ...current] : [...current, ...incoming];
|
|
956
|
+
if (typeof uniqueBy !== "string") return merged;
|
|
957
|
+
const getUniqueKey = (entry) => {
|
|
958
|
+
const key = get(entry, uniqueBy);
|
|
959
|
+
return key === void 0 ? Symbol() : key;
|
|
960
|
+
};
|
|
961
|
+
if (prepends === true) return uniqBy(merged, getUniqueKey);
|
|
962
|
+
const orderedKeys = [];
|
|
963
|
+
const valuesByKey = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const entry of merged) {
|
|
965
|
+
const key = getUniqueKey(entry);
|
|
966
|
+
if (!valuesByKey.has(key)) orderedKeys.push(key);
|
|
967
|
+
valuesByKey.set(key, entry);
|
|
968
|
+
}
|
|
969
|
+
return orderedKeys.map((key) => valuesByKey.get(key));
|
|
970
|
+
};
|
|
971
|
+
let value = newValue;
|
|
972
|
+
if (Array.isArray(originalValue)) value = mergeArrays(originalValue, Array.isArray(newValue) ? newValue : newValue === void 0 ? [] : [newValue]);
|
|
973
|
+
else if (originalValue instanceof Object && newValue instanceof Object) value = merge(originalValue, newValue, {
|
|
974
|
+
overwriteArray: false,
|
|
975
|
+
arrayMerge: (current, incoming) => mergeArrays(current, incoming)
|
|
976
|
+
});
|
|
977
|
+
set(mergedPayloadProperties, mergeableProperty, value);
|
|
978
|
+
});
|
|
979
|
+
return mergedPayloadProperties;
|
|
980
|
+
}
|
|
981
|
+
function mergeValidation(current, next, errorBag) {
|
|
982
|
+
if (!errorBag) return next;
|
|
983
|
+
return {
|
|
984
|
+
...current,
|
|
985
|
+
[errorBag]: next[errorBag] ?? {}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function resolveErrors(validation, errorBag) {
|
|
989
|
+
if (errorBag) return validation[errorBag] ?? {};
|
|
990
|
+
return validation.default ?? {};
|
|
991
|
+
}
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/router/response/response-manager.ts
|
|
994
|
+
const queue = [];
|
|
995
|
+
let processing = false;
|
|
222
996
|
/**
|
|
223
|
-
*
|
|
997
|
+
* Adds a response to the queue and starts processing it if not already doing so.
|
|
224
998
|
*/
|
|
225
|
-
function
|
|
226
|
-
|
|
999
|
+
function enqueueResponse(response) {
|
|
1000
|
+
debug.queue("Enqueuing response", response);
|
|
1001
|
+
queue.push(response);
|
|
1002
|
+
processResponseQueue();
|
|
1003
|
+
}
|
|
1004
|
+
async function processResponseQueue() {
|
|
1005
|
+
if (processing) return;
|
|
1006
|
+
processing = true;
|
|
1007
|
+
await processNextResponse();
|
|
1008
|
+
processing = false;
|
|
1009
|
+
}
|
|
1010
|
+
async function processNextResponse() {
|
|
1011
|
+
const response = queue.shift();
|
|
1012
|
+
if (!response) {
|
|
1013
|
+
debug.queue("End of response queue.");
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
debug.queue("Processing response", response);
|
|
227
1017
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
1018
|
+
response.request.resolve(await handleHybridRequestResponse(response));
|
|
1019
|
+
} finally {
|
|
1020
|
+
debug.router("Ended navigation.", response.request);
|
|
1021
|
+
await runHooks("after", response.request.options.hooks, response.request, getInternalRouterContext());
|
|
1022
|
+
}
|
|
1023
|
+
return await processNextResponse();
|
|
1024
|
+
}
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/router/request/request-manager.ts
|
|
1027
|
+
const activeRequests = {
|
|
1028
|
+
navigation: void 0,
|
|
1029
|
+
async: /* @__PURE__ */ new Map(),
|
|
1030
|
+
asyncGroups: /* @__PURE__ */ new Map()
|
|
1031
|
+
};
|
|
1032
|
+
/**
|
|
1033
|
+
* Registers and starts a request.
|
|
1034
|
+
*/
|
|
1035
|
+
function enqueueRequest(request) {
|
|
1036
|
+
debug.queue("Enqueuing request", request);
|
|
1037
|
+
interruptRequestIfNeeded(request);
|
|
1038
|
+
if (isAsyncRequest(request)) {
|
|
1039
|
+
registerAsyncRequest(request);
|
|
1040
|
+
processRequest(request, () => unregisterAsyncRequest(request));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
activeRequests.navigation = request;
|
|
1044
|
+
processRequest(request, () => {
|
|
1045
|
+
if (activeRequests.navigation?.id === request.id) activeRequests.navigation = void 0;
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function isAsyncRequest(request) {
|
|
1049
|
+
return request.options.mode === "async";
|
|
1050
|
+
}
|
|
1051
|
+
function registerAsyncRequest(request) {
|
|
1052
|
+
activeRequests.async.set(request.id, request);
|
|
1053
|
+
const group = request.options.group;
|
|
1054
|
+
if (!group) return;
|
|
1055
|
+
if (!activeRequests.asyncGroups.has(group)) activeRequests.asyncGroups.set(group, /* @__PURE__ */ new Set());
|
|
1056
|
+
activeRequests.asyncGroups.get(group)?.add(request.id);
|
|
1057
|
+
}
|
|
1058
|
+
function unregisterAsyncRequest(request) {
|
|
1059
|
+
activeRequests.async.delete(request.id);
|
|
1060
|
+
const group = request.options.group;
|
|
1061
|
+
if (!group) return;
|
|
1062
|
+
const requests = activeRequests.asyncGroups.get(group);
|
|
1063
|
+
if (!requests) return;
|
|
1064
|
+
requests.delete(request.id);
|
|
1065
|
+
if (requests.size === 0) activeRequests.asyncGroups.delete(group);
|
|
1066
|
+
}
|
|
1067
|
+
async function processRequest(request, onFinally) {
|
|
1068
|
+
debug.queue("Processing request", request);
|
|
1069
|
+
try {
|
|
1070
|
+
enqueueResponse({
|
|
1071
|
+
request,
|
|
1072
|
+
response: await sendHybridRequest(request)
|
|
230
1073
|
});
|
|
231
|
-
} catch {
|
|
232
|
-
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (!(error instanceof Error)) error = /* @__PURE__ */ new Error("Unknown error during request processing.");
|
|
1076
|
+
await handleTransportError(request, error);
|
|
1077
|
+
} finally {
|
|
1078
|
+
request.completed = true;
|
|
1079
|
+
onFinally();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async function handleTransportError(request, error) {
|
|
1083
|
+
const context = getRouterContext();
|
|
1084
|
+
if (isHttpAbortError(error)) {
|
|
1085
|
+
debug.router("The request was aborted.", error);
|
|
1086
|
+
await runHooks("abort", request.options.hooks, request, context);
|
|
1087
|
+
await runHooks("fail", request.options.hooks, error, request, context);
|
|
1088
|
+
request.resolve({ error });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (isNavigationCancelledError(error)) {
|
|
1092
|
+
debug.router("The request was cancelled through the \"before\" hook.", error);
|
|
1093
|
+
await runHooks("abort", request.options.hooks, request, context);
|
|
1094
|
+
} else {
|
|
1095
|
+
debug.router("An unknown error occured.", error);
|
|
1096
|
+
console.error(error);
|
|
1097
|
+
await runHooks("exception", request.options.hooks, error, request, context);
|
|
1098
|
+
}
|
|
1099
|
+
await runHooks("fail", request.options.hooks, error, request, context);
|
|
1100
|
+
request.resolve({ error });
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Interrupts active requests according to the starting request's policy.
|
|
1104
|
+
*/
|
|
1105
|
+
function interruptRequestIfNeeded(request) {
|
|
1106
|
+
if (isAsyncRequest(request)) {
|
|
1107
|
+
interruptAsyncRequestsOnStart(request);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
interruptNavigationRequest({ interrupted: true });
|
|
1111
|
+
interruptAsyncRequestsCancelledByNavigation();
|
|
1112
|
+
interruptAsyncRequestsOnStart(request);
|
|
1113
|
+
}
|
|
1114
|
+
function interruptNavigationRequest(options) {
|
|
1115
|
+
const request = activeRequests.navigation;
|
|
1116
|
+
if (!request) return;
|
|
1117
|
+
activeRequests.navigation = void 0;
|
|
1118
|
+
cancelRequest(request, options);
|
|
1119
|
+
}
|
|
1120
|
+
function interruptAsyncRequestsCancelledByNavigation() {
|
|
1121
|
+
for (const request of activeRequests.async.values()) if (request.options.cancelOnNavigation) interruptAsyncRequest(request, { interrupted: true });
|
|
1122
|
+
}
|
|
1123
|
+
function interruptAsyncRequestsOnStart(request) {
|
|
1124
|
+
debug.queue("Interrupting async requests", request);
|
|
1125
|
+
if (request.options.interruptAsyncOnStart === "none") return;
|
|
1126
|
+
if (request.options.interruptAsyncOnStart === "all") {
|
|
1127
|
+
for (const current of activeRequests.async.values()) interruptAsyncRequest(current, { interrupted: true });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (!request.options.group) return;
|
|
1131
|
+
for (const requestId of activeRequests.asyncGroups.get(request.options.group) ?? []) {
|
|
1132
|
+
const current = activeRequests.async.get(requestId);
|
|
1133
|
+
if (current) interruptAsyncRequest(current, { interrupted: true });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function interruptAsyncRequest(request, options) {
|
|
1137
|
+
unregisterAsyncRequest(request);
|
|
1138
|
+
cancelRequest(request, options);
|
|
233
1139
|
}
|
|
234
1140
|
/**
|
|
235
|
-
*
|
|
236
|
-
* and both URLs lead to the same endpoint, we update the target URL
|
|
237
|
-
* to use the hash of the initially-requested URL.
|
|
1141
|
+
* Cancels the current navigation request.
|
|
238
1142
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1143
|
+
function cancelNavigationRequest() {
|
|
1144
|
+
interruptNavigationRequest({ cancelled: true });
|
|
1145
|
+
}
|
|
1146
|
+
function cancelRequest(request, options) {
|
|
1147
|
+
request.completed = false;
|
|
1148
|
+
request.cancelled = options.cancelled ?? false;
|
|
1149
|
+
request.interrupted = options.interrupted ?? false;
|
|
1150
|
+
request.controller.abort();
|
|
244
1151
|
}
|
|
245
1152
|
//#endregion
|
|
246
1153
|
//#region src/router/history.ts
|
|
@@ -276,10 +1183,7 @@ async function registerEventListeners() {
|
|
|
276
1183
|
debug.history("Registering [popstate] and [scroll] event listeners.");
|
|
277
1184
|
window?.addEventListener("popstate", async (event) => {
|
|
278
1185
|
debug.history("Navigation detected (popstate event). State:", { state: event.state });
|
|
279
|
-
|
|
280
|
-
debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
281
|
-
context.pendingNavigation?.controller?.abort();
|
|
282
|
-
}
|
|
1186
|
+
cancelNavigationRequest();
|
|
283
1187
|
const state = context.serializer.unserialize(event.state);
|
|
284
1188
|
await runHooks("backForward", {}, state, context);
|
|
285
1189
|
if (!state) {
|
|
@@ -303,9 +1207,10 @@ async function registerEventListeners() {
|
|
|
303
1207
|
updateHistoryState: false
|
|
304
1208
|
});
|
|
305
1209
|
});
|
|
306
|
-
|
|
1210
|
+
const onScroll = debounce((event) => {
|
|
307
1211
|
if ((event?.target)?.hasAttribute?.("scroll-region")) saveScrollPositions();
|
|
308
|
-
}
|
|
1212
|
+
}, 100);
|
|
1213
|
+
window?.addEventListener("scroll", (event) => onScroll(event), true);
|
|
309
1214
|
}
|
|
310
1215
|
/** Checks if the current navigation was made by going back or forward. */
|
|
311
1216
|
function isBackForwardNavigation() {
|
|
@@ -343,6 +1248,7 @@ function serializeContext(context) {
|
|
|
343
1248
|
return context.serializer.serialize({
|
|
344
1249
|
url: context.url,
|
|
345
1250
|
version: context.version,
|
|
1251
|
+
validation: context.validation,
|
|
346
1252
|
view: context.view,
|
|
347
1253
|
dialog: context.dialog,
|
|
348
1254
|
scrollRegions: context.scrollRegions,
|
|
@@ -354,14 +1260,14 @@ function createSerializer(options) {
|
|
|
354
1260
|
return {
|
|
355
1261
|
serialize: (data) => {
|
|
356
1262
|
debug.history("Serializing data.", data);
|
|
357
|
-
return stringify(data);
|
|
1263
|
+
return stringify$1(data);
|
|
358
1264
|
},
|
|
359
1265
|
unserialize: (data) => {
|
|
360
1266
|
if (!data) {
|
|
361
1267
|
debug.history("No data to unserialize.");
|
|
362
1268
|
return;
|
|
363
1269
|
}
|
|
364
|
-
return parse(data);
|
|
1270
|
+
return parse$1(data);
|
|
365
1271
|
}
|
|
366
1272
|
};
|
|
367
1273
|
}
|
|
@@ -471,8 +1377,7 @@ function getRouteTransformable(routeName, routeParameters, shouldThrow) {
|
|
|
471
1377
|
return {
|
|
472
1378
|
...domain && { hostname: domain },
|
|
473
1379
|
pathname: path,
|
|
474
|
-
search:
|
|
475
|
-
encodeValuesOnly: true,
|
|
1380
|
+
search: stringifyQueryString(remaining, {
|
|
476
1381
|
arrayFormat: "indices",
|
|
477
1382
|
addQueryPrefix: true
|
|
478
1383
|
})
|
|
@@ -534,29 +1439,6 @@ function updateRoutingConfiguration(routing) {
|
|
|
534
1439
|
setContext({ routing });
|
|
535
1440
|
}
|
|
536
1441
|
//#endregion
|
|
537
|
-
//#region src/download.ts
|
|
538
|
-
/** Checks if the response wants to redirect to an external URL. */
|
|
539
|
-
function isDownloadResponse(response) {
|
|
540
|
-
return response.status === 200 && !!response.headers["content-disposition"];
|
|
541
|
-
}
|
|
542
|
-
/** Handles a download. */
|
|
543
|
-
async function handleDownloadResponse(response) {
|
|
544
|
-
const blob = new Blob([response.data], { type: response.headers["content-type"] });
|
|
545
|
-
const urlObject = window.webkitURL || window.URL;
|
|
546
|
-
const link = document.createElement("a");
|
|
547
|
-
link.style.display = "none";
|
|
548
|
-
link.href = urlObject.createObjectURL(blob);
|
|
549
|
-
link.download = getFileNameFromContentDispositionHeader(response.headers["content-disposition"]);
|
|
550
|
-
link.click();
|
|
551
|
-
setTimeout(() => {
|
|
552
|
-
urlObject.revokeObjectURL(link.href);
|
|
553
|
-
link.remove();
|
|
554
|
-
}, 0);
|
|
555
|
-
}
|
|
556
|
-
function getFileNameFromContentDispositionHeader(header) {
|
|
557
|
-
return (header.split(";")[1]?.trim().split("=")[1])?.replace(/^"(.*)"$/, "$1") ?? "";
|
|
558
|
-
}
|
|
559
|
-
//#endregion
|
|
560
1442
|
//#region src/context/context.ts
|
|
561
1443
|
const state = {
|
|
562
1444
|
initialized: false,
|
|
@@ -576,6 +1458,7 @@ async function initializeContext(options) {
|
|
|
576
1458
|
state.initialized = true;
|
|
577
1459
|
state.context = {
|
|
578
1460
|
...options.payload,
|
|
1461
|
+
validation: options.payload.validation ?? {},
|
|
579
1462
|
responseErrorModals: options.responseErrorModals,
|
|
580
1463
|
serializer: createSerializer(options),
|
|
581
1464
|
url: makeUrl(options.payload.url).toString(),
|
|
@@ -585,9 +1468,8 @@ async function initializeContext(options) {
|
|
|
585
1468
|
},
|
|
586
1469
|
scrollRegions: [],
|
|
587
1470
|
plugins: options.plugins ?? [],
|
|
588
|
-
|
|
1471
|
+
http: options.http ?? createXhrHttpClient(),
|
|
589
1472
|
routing: options.routing,
|
|
590
|
-
preloadCache: /* @__PURE__ */ new Map(),
|
|
591
1473
|
hooks: {},
|
|
592
1474
|
memo: {}
|
|
593
1475
|
};
|
|
@@ -595,24 +1477,6 @@ async function initializeContext(options) {
|
|
|
595
1477
|
return getInternalRouterContext();
|
|
596
1478
|
}
|
|
597
1479
|
/**
|
|
598
|
-
* Registers an interceptor that assumes `arraybuffer`
|
|
599
|
-
* responses and converts responses to JSON or text.
|
|
600
|
-
*/
|
|
601
|
-
function registerAxios(axios) {
|
|
602
|
-
axios.interceptors.response.use((response) => {
|
|
603
|
-
if (!isDownloadResponse(response)) {
|
|
604
|
-
const text = new TextDecoder().decode(response.data);
|
|
605
|
-
try {
|
|
606
|
-
response.data = JSON.parse(text);
|
|
607
|
-
} catch {
|
|
608
|
-
response.data = text;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return response;
|
|
612
|
-
}, (error) => Promise.reject(error));
|
|
613
|
-
return axios;
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
1480
|
* Mutates properties at the top-level of the context.
|
|
617
1481
|
*/
|
|
618
1482
|
function setContext(merge = {}, options = {}) {
|
|
@@ -630,70 +1494,12 @@ function payloadFromContext() {
|
|
|
630
1494
|
return {
|
|
631
1495
|
url: getRouterContext().url,
|
|
632
1496
|
version: getRouterContext().version,
|
|
1497
|
+
validation: getRouterContext().validation,
|
|
633
1498
|
view: getRouterContext().view,
|
|
634
1499
|
dialog: getRouterContext().dialog
|
|
635
1500
|
};
|
|
636
1501
|
}
|
|
637
1502
|
//#endregion
|
|
638
|
-
//#region src/external.ts
|
|
639
|
-
/**
|
|
640
|
-
* Performs an external navigation by saving options to the storage and
|
|
641
|
-
* making a full page reload. Upon loading, the navigation options
|
|
642
|
-
* will be pulled and a hybrid navigation will be made.
|
|
643
|
-
*/
|
|
644
|
-
async function performExternalNavigation(options) {
|
|
645
|
-
debug.external("Navigating to an external URL:", options);
|
|
646
|
-
if (options.target === "new-tab") {
|
|
647
|
-
const link = document.createElement("a");
|
|
648
|
-
link.style.display = "none";
|
|
649
|
-
link.target = "_blank";
|
|
650
|
-
link.href = options.url;
|
|
651
|
-
link.click();
|
|
652
|
-
setTimeout(() => link.remove(), 0);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
|
|
656
|
-
window.location.href = options.url;
|
|
657
|
-
if (sameUrls(window.location, options.url)) {
|
|
658
|
-
debug.external("Manually reloading due to the external URL being the same.");
|
|
659
|
-
window.location.reload();
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
/** Navigates to the given URL without the hybrid protocol. */
|
|
663
|
-
function navigateToExternalUrl(url, data) {
|
|
664
|
-
document.location.href = makeUrl(url, { search: qs.stringify(data, {
|
|
665
|
-
encodeValuesOnly: true,
|
|
666
|
-
arrayFormat: "brackets"
|
|
667
|
-
}) }).toString();
|
|
668
|
-
}
|
|
669
|
-
/** Checks if the response wants to redirect to an external URL. */
|
|
670
|
-
function isExternalResponse(response) {
|
|
671
|
-
return response?.status === 409 && !!response?.headers?.[EXTERNAL_NAVIGATION_HEADER];
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Performs the internal navigation when an external navigation to a hybrid view has been made.
|
|
675
|
-
* This method is meant to be called on router creation.
|
|
676
|
-
*/
|
|
677
|
-
async function handleExternalNavigation() {
|
|
678
|
-
debug.external("Handling an external navigation.");
|
|
679
|
-
const options = JSON.parse(window.sessionStorage.getItem("hybridly:external") || "{}");
|
|
680
|
-
window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
|
|
681
|
-
debug.external("Options from the session storage:", options);
|
|
682
|
-
setContext({ url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString() });
|
|
683
|
-
await navigate({
|
|
684
|
-
type: "initial",
|
|
685
|
-
preserveState: true,
|
|
686
|
-
preserveScroll: options.preserveScroll
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
/** Checks if the navigation being initialized points to an external location. */
|
|
690
|
-
function isExternalNavigation() {
|
|
691
|
-
try {
|
|
692
|
-
return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
|
|
693
|
-
} catch {}
|
|
694
|
-
return false;
|
|
695
|
-
}
|
|
696
|
-
//#endregion
|
|
697
1503
|
//#region src/dialog/index.ts
|
|
698
1504
|
/**
|
|
699
1505
|
* Closes the dialog.
|
|
@@ -719,62 +1525,6 @@ async function closeDialog(options) {
|
|
|
719
1525
|
});
|
|
720
1526
|
}
|
|
721
1527
|
//#endregion
|
|
722
|
-
//#region src/router/preload.ts
|
|
723
|
-
/**
|
|
724
|
-
* Checks if there is a preloaded request for the given URL.
|
|
725
|
-
*/
|
|
726
|
-
function isPreloaded(targetUrl) {
|
|
727
|
-
return getInternalRouterContext().preloadCache.has(targetUrl.toString()) ?? false;
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Gets the response of a preloaded request.
|
|
731
|
-
*/
|
|
732
|
-
function getPreloadedRequest(targetUrl) {
|
|
733
|
-
return getInternalRouterContext().preloadCache.get(targetUrl.toString());
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Stores the response of a preloaded request.
|
|
737
|
-
*/
|
|
738
|
-
function storePreloadRequest(targetUrl, response) {
|
|
739
|
-
getInternalRouterContext().preloadCache.set(targetUrl.toString(), response);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Discards a preloaded request.
|
|
743
|
-
*/
|
|
744
|
-
function discardPreloadedRequest(targetUrl) {
|
|
745
|
-
return getInternalRouterContext().preloadCache.delete(targetUrl.toString());
|
|
746
|
-
}
|
|
747
|
-
/** Preloads a hybrid request. */
|
|
748
|
-
async function performPreloadRequest(options) {
|
|
749
|
-
const context = getRouterContext();
|
|
750
|
-
const url = makeUrl(options.url ?? context.url);
|
|
751
|
-
if (isPreloaded(url)) {
|
|
752
|
-
debug.router("This request is already preloaded.");
|
|
753
|
-
return false;
|
|
754
|
-
}
|
|
755
|
-
if (context.pendingNavigation) {
|
|
756
|
-
debug.router("A navigation is pending, preload aborted.");
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
if (options.method !== "GET") {
|
|
760
|
-
debug.router("Cannot preload non-GET requests.");
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
debug.router(`Preloading response for [${url.toString()}]`);
|
|
764
|
-
try {
|
|
765
|
-
const response = await performHybridRequest(url, options);
|
|
766
|
-
if (!isHybridResponse(response)) {
|
|
767
|
-
debug.router("Preload result was invalid.");
|
|
768
|
-
return false;
|
|
769
|
-
}
|
|
770
|
-
storePreloadRequest(url, response);
|
|
771
|
-
return true;
|
|
772
|
-
} catch (error) {
|
|
773
|
-
debug.router("Preloading failed.");
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
//#endregion
|
|
778
1528
|
//#region src/router/router.ts
|
|
779
1529
|
/**
|
|
780
1530
|
* The hybridly router.
|
|
@@ -786,13 +1536,13 @@ async function performPreloadRequest(options) {
|
|
|
786
1536
|
* router.get('/posts/edit', { post })
|
|
787
1537
|
*/
|
|
788
1538
|
const router = {
|
|
789
|
-
abort:
|
|
790
|
-
active: () => !!getRouterContext().pendingNavigation,
|
|
1539
|
+
abort: () => cancelNavigationRequest(),
|
|
791
1540
|
navigate: async (options) => await performHybridNavigation(options),
|
|
792
|
-
reload: async (options) => await performHybridNavigation({
|
|
1541
|
+
reload: async (options = {}) => await performHybridNavigation({
|
|
793
1542
|
preserveScroll: true,
|
|
794
1543
|
preserveState: true,
|
|
795
1544
|
replace: true,
|
|
1545
|
+
mode: "async",
|
|
796
1546
|
...options
|
|
797
1547
|
}),
|
|
798
1548
|
get: async (url, options = {}) => await performHybridNavigation({
|
|
@@ -825,13 +1575,8 @@ const router = {
|
|
|
825
1575
|
method: "DELETE"
|
|
826
1576
|
}),
|
|
827
1577
|
local: async (url, options = {}) => await performLocalNavigation(url, options),
|
|
828
|
-
preload: async (url, options = {}) => await performPreloadRequest({
|
|
829
|
-
...options,
|
|
830
|
-
url,
|
|
831
|
-
method: "GET"
|
|
832
|
-
}),
|
|
833
1578
|
external: (url, data = {}) => navigateToExternalUrl(url, data),
|
|
834
|
-
to: async (name, parameters, options) => {
|
|
1579
|
+
to: async (name, parameters = void 0, options = {}) => {
|
|
835
1580
|
const url = generateRouteFromName(name, parameters);
|
|
836
1581
|
const method = getRouteDefinition(name).method.at(0);
|
|
837
1582
|
return await performHybridNavigation({
|
|
@@ -840,9 +1585,9 @@ const router = {
|
|
|
840
1585
|
method
|
|
841
1586
|
});
|
|
842
1587
|
},
|
|
843
|
-
matches: (name, parameters) => currentRouteMatches(name, parameters),
|
|
1588
|
+
matches: (name, parameters = void 0) => currentRouteMatches(name, parameters),
|
|
844
1589
|
current: () => getCurrentRouteName(),
|
|
845
|
-
dialog: { close: (options) => closeDialog(options) },
|
|
1590
|
+
dialog: { close: (options = {}) => closeDialog(options) },
|
|
846
1591
|
history: {
|
|
847
1592
|
get: (key) => getHistoryMemo(key),
|
|
848
1593
|
remember: (key, value) => remember(key, value)
|
|
@@ -853,274 +1598,6 @@ async function createRouter(options) {
|
|
|
853
1598
|
await initializeContext(options);
|
|
854
1599
|
return await initializeRouter();
|
|
855
1600
|
}
|
|
856
|
-
/** Performs every action necessary to make a hybrid navigation. */
|
|
857
|
-
async function performHybridNavigation(options) {
|
|
858
|
-
const navigationId = random();
|
|
859
|
-
const context = getRouterContext();
|
|
860
|
-
debug.router("Making a hybrid navigation:", {
|
|
861
|
-
context,
|
|
862
|
-
options,
|
|
863
|
-
navigationId
|
|
864
|
-
});
|
|
865
|
-
try {
|
|
866
|
-
if (!options.method) {
|
|
867
|
-
debug.router("Setting method to GET because none was provided.");
|
|
868
|
-
options.method = "GET";
|
|
869
|
-
}
|
|
870
|
-
options.method = options.method.toUpperCase();
|
|
871
|
-
if ((hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
|
|
872
|
-
options.data = objectToFormData(options.data);
|
|
873
|
-
debug.router("Converted data to FormData.", options.data);
|
|
874
|
-
}
|
|
875
|
-
if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
|
|
876
|
-
debug.router("Transforming data to query parameters.", options.data);
|
|
877
|
-
options.url = makeUrl(options.url ?? context.url, { query: options.data });
|
|
878
|
-
options.data = {};
|
|
879
|
-
}
|
|
880
|
-
if ([
|
|
881
|
-
"PUT",
|
|
882
|
-
"PATCH",
|
|
883
|
-
"DELETE"
|
|
884
|
-
].includes(options.method) && options.spoof !== false) {
|
|
885
|
-
debug.router(`Automatically spoofing method ${options.method}.`);
|
|
886
|
-
if (options.data instanceof FormData) options.data.append("_method", options.method);
|
|
887
|
-
else if (typeof options.data === "undefined") options.data = { _method: options.method };
|
|
888
|
-
else if (options.data instanceof Object && Object.keys(options.data).length >= 0) Object.assign(options.data, { _method: options.method });
|
|
889
|
-
else debug.router("Could not spoof method because body type is not supported.", options.data);
|
|
890
|
-
options.method = "POST";
|
|
891
|
-
}
|
|
892
|
-
if (!await runHooks("before", options.hooks, options, context)) {
|
|
893
|
-
debug.router("\"before\" event returned false, aborting the navigation.");
|
|
894
|
-
throw new NavigationCancelledError("The navigation was cancelled by the \"before\" event.");
|
|
895
|
-
}
|
|
896
|
-
if (context.pendingNavigation) {
|
|
897
|
-
debug.router("Aborting current navigation.", context.pendingNavigation);
|
|
898
|
-
context.pendingNavigation?.controller?.abort();
|
|
899
|
-
}
|
|
900
|
-
saveScrollPositions();
|
|
901
|
-
const targetUrl = makeUrl(options.url ?? context.url, options.transformUrl);
|
|
902
|
-
const abortController = new AbortController();
|
|
903
|
-
setContext({ pendingNavigation: {
|
|
904
|
-
id: navigationId,
|
|
905
|
-
url: targetUrl,
|
|
906
|
-
controller: abortController,
|
|
907
|
-
status: "pending",
|
|
908
|
-
options
|
|
909
|
-
} });
|
|
910
|
-
await runHooks("start", options.hooks, context);
|
|
911
|
-
debug.router("Making request with axios.");
|
|
912
|
-
const response = await performHybridRequest(targetUrl, options, abortController);
|
|
913
|
-
if (await runHooks("data", options.hooks, response, context) === false) return { response };
|
|
914
|
-
if (isExternalResponse(response)) {
|
|
915
|
-
debug.router("The response is explicitely external.");
|
|
916
|
-
await performExternalNavigation({
|
|
917
|
-
url: fillHash(targetUrl, response.headers[EXTERNAL_NAVIGATION_HEADER]),
|
|
918
|
-
preserveScroll: options.preserveScroll === true,
|
|
919
|
-
target: response.headers[EXTERNAL_NAVIGATION_TARGET_HEADER] ?? "current"
|
|
920
|
-
});
|
|
921
|
-
return { response };
|
|
922
|
-
}
|
|
923
|
-
if (isDownloadResponse(response)) {
|
|
924
|
-
debug.router("The response returns a file to download.");
|
|
925
|
-
await handleDownloadResponse(response);
|
|
926
|
-
return { response };
|
|
927
|
-
}
|
|
928
|
-
if (!isHybridResponse(response)) throw new NotAHybridResponseError(response);
|
|
929
|
-
debug.router("The response respects the Hybridly protocol.");
|
|
930
|
-
const payload = response.data;
|
|
931
|
-
if (payload.view && (options.only?.length ?? options.except?.length) && payload.view.component === context.view.component) {
|
|
932
|
-
debug.router(`Merging ${options.only ? "\"only\"" : "\"except\""} properties.`, payload.view.properties);
|
|
933
|
-
const mergedPayloadProperties = merge(context.view.properties, payload.view.properties);
|
|
934
|
-
if (options.errorBag) mergedPayloadProperties.errors[options.errorBag] = payload.view.properties.errors[options.errorBag] ?? {};
|
|
935
|
-
else mergedPayloadProperties.errors = payload.view.properties.errors;
|
|
936
|
-
payload.view.properties = mergedPayloadProperties;
|
|
937
|
-
debug.router("Merged properties:", payload.view.properties);
|
|
938
|
-
}
|
|
939
|
-
await navigate({
|
|
940
|
-
type: "server",
|
|
941
|
-
payload: {
|
|
942
|
-
...payload,
|
|
943
|
-
url: fillHash(targetUrl, payload.url)
|
|
944
|
-
},
|
|
945
|
-
preserveScroll: options.preserveScroll,
|
|
946
|
-
preserveState: options.preserveState,
|
|
947
|
-
preserveUrl: options.preserveUrl,
|
|
948
|
-
replace: options.replace === true || options.preserveUrl || sameUrls(payload.url, window.location.href) && !sameHashes(payload.url, window.location.href)
|
|
949
|
-
});
|
|
950
|
-
if (Object.keys(context.view.properties.errors ?? {}).length > 0) {
|
|
951
|
-
const errors = (() => {
|
|
952
|
-
if (options.errorBag && typeof context.view.properties.errors === "object") return context.view.properties.errors[options.errorBag] ?? {};
|
|
953
|
-
return context.view.properties.errors;
|
|
954
|
-
})();
|
|
955
|
-
debug.router("The request returned validation errors.", errors);
|
|
956
|
-
setContext({ pendingNavigation: {
|
|
957
|
-
...context.pendingNavigation,
|
|
958
|
-
status: "error"
|
|
959
|
-
} });
|
|
960
|
-
await runHooks("error", options.hooks, errors, context);
|
|
961
|
-
} else {
|
|
962
|
-
setContext({ pendingNavigation: {
|
|
963
|
-
...context.pendingNavigation,
|
|
964
|
-
status: "success"
|
|
965
|
-
} });
|
|
966
|
-
await runHooks("success", options.hooks, payload, context);
|
|
967
|
-
}
|
|
968
|
-
return { response };
|
|
969
|
-
} catch (error) {
|
|
970
|
-
await match(error.constructor.name, {
|
|
971
|
-
NavigationCancelledError: async () => {
|
|
972
|
-
debug.router("The request was cancelled through the \"before\" hook.", error);
|
|
973
|
-
await runHooks("abort", options.hooks, context);
|
|
974
|
-
},
|
|
975
|
-
AbortError: async () => {
|
|
976
|
-
debug.router("The request was aborted.", error);
|
|
977
|
-
await runHooks("abort", options.hooks, context);
|
|
978
|
-
},
|
|
979
|
-
NotAHybridResponseError: async () => {
|
|
980
|
-
debug.router("The response was not hybrid.");
|
|
981
|
-
console.error(error);
|
|
982
|
-
await runHooks("invalid", options.hooks, error, context);
|
|
983
|
-
if (context.responseErrorModals) showResponseErrorModal(error.response.data);
|
|
984
|
-
},
|
|
985
|
-
default: async () => {
|
|
986
|
-
if (error?.name === "CanceledError") {
|
|
987
|
-
debug.router("The request was cancelled.", error);
|
|
988
|
-
await runHooks("abort", options.hooks, context);
|
|
989
|
-
} else {
|
|
990
|
-
debug.router("An unknown error occured.", error);
|
|
991
|
-
console.error(error);
|
|
992
|
-
await runHooks("exception", options.hooks, error, context);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
await runHooks("fail", options.hooks, context);
|
|
997
|
-
return { error: {
|
|
998
|
-
type: error.constructor.name,
|
|
999
|
-
actual: error
|
|
1000
|
-
} };
|
|
1001
|
-
} finally {
|
|
1002
|
-
debug.router("Ending navigation.");
|
|
1003
|
-
await runHooks("after", options.hooks, context);
|
|
1004
|
-
if (context.pendingNavigation?.id === navigationId) setContext({ pendingNavigation: void 0 });
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
/** Checks if the response contains a hybrid header. */
|
|
1008
|
-
function isHybridResponse(response) {
|
|
1009
|
-
return !!response?.headers[HYBRIDLY_HEADER];
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Makes an internal navigation that swaps the view and updates the context.
|
|
1013
|
-
* @internal
|
|
1014
|
-
*/
|
|
1015
|
-
async function navigate(options) {
|
|
1016
|
-
const context = getRouterContext();
|
|
1017
|
-
options.hasDialog ??= !!options.payload?.dialog;
|
|
1018
|
-
debug.router("Making an internal navigation:", {
|
|
1019
|
-
context,
|
|
1020
|
-
options
|
|
1021
|
-
});
|
|
1022
|
-
await runHooks("navigating", {}, options, context);
|
|
1023
|
-
options.payload ??= payloadFromContext();
|
|
1024
|
-
options.payload.view ??= payloadFromContext().view;
|
|
1025
|
-
function evaluateConditionalOption(option) {
|
|
1026
|
-
return typeof option === "function" ? option(options) : option;
|
|
1027
|
-
}
|
|
1028
|
-
const shouldPreserveState = evaluateConditionalOption(options.preserveState);
|
|
1029
|
-
const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
|
|
1030
|
-
const shouldReplaceHistory = evaluateConditionalOption(options.replace);
|
|
1031
|
-
const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
|
|
1032
|
-
const shouldPreserveView = !options.payload.view.component;
|
|
1033
|
-
if (shouldPreserveState && getHistoryMemo() && options.payload.view.component === context.view.component) {
|
|
1034
|
-
debug.history("Setting the memo from this history entry into the current context.");
|
|
1035
|
-
setContext({ memo: getHistoryMemo() });
|
|
1036
|
-
}
|
|
1037
|
-
if (shouldReplaceUrl) {
|
|
1038
|
-
debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
|
|
1039
|
-
options.payload.url = context.url;
|
|
1040
|
-
}
|
|
1041
|
-
setContext({
|
|
1042
|
-
...shouldPreserveView ? {
|
|
1043
|
-
view: {
|
|
1044
|
-
component: context.view.component,
|
|
1045
|
-
properties: merge(context.view.properties, options.payload.view.properties),
|
|
1046
|
-
deferred: context.view.deferred
|
|
1047
|
-
},
|
|
1048
|
-
url: context.url,
|
|
1049
|
-
version: options.payload.version,
|
|
1050
|
-
dialog: context.dialog
|
|
1051
|
-
} : options.payload,
|
|
1052
|
-
memo: {}
|
|
1053
|
-
});
|
|
1054
|
-
if (options.updateHistoryState !== false) {
|
|
1055
|
-
debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
|
|
1056
|
-
setHistoryState({ replace: shouldReplaceHistory });
|
|
1057
|
-
}
|
|
1058
|
-
if (context.view.deferred?.length) {
|
|
1059
|
-
debug.router("Request has deferred properties, queueing a partial reload:", context.view.deferred);
|
|
1060
|
-
context.adapter.executeOnMounted(async () => {
|
|
1061
|
-
await performHybridNavigation({
|
|
1062
|
-
preserveScroll: true,
|
|
1063
|
-
preserveState: true,
|
|
1064
|
-
replace: true,
|
|
1065
|
-
only: context.view.deferred
|
|
1066
|
-
});
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
const viewComponent = !shouldPreserveView ? await context.adapter.resolveComponent(context.view.component) : void 0;
|
|
1070
|
-
if (viewComponent) debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
|
|
1071
|
-
await context.adapter.onViewSwap({
|
|
1072
|
-
component: viewComponent,
|
|
1073
|
-
dialog: context.dialog,
|
|
1074
|
-
properties: options.payload?.view?.properties,
|
|
1075
|
-
preserveState: shouldPreserveState,
|
|
1076
|
-
onMounted: (hookOptions) => runHooks("mounted", {}, {
|
|
1077
|
-
...options,
|
|
1078
|
-
...hookOptions
|
|
1079
|
-
}, context)
|
|
1080
|
-
});
|
|
1081
|
-
if (options.type === "back-forward") restoreScrollPositions();
|
|
1082
|
-
else if (!shouldPreserveScroll) resetScrollPositions();
|
|
1083
|
-
await runHooks("navigated", {}, options, context);
|
|
1084
|
-
}
|
|
1085
|
-
async function performHybridRequest(targetUrl, options, abortController) {
|
|
1086
|
-
const context = getInternalRouterContext();
|
|
1087
|
-
const preloaded = options.method === "GET" ? getPreloadedRequest(targetUrl) : false;
|
|
1088
|
-
if (preloaded) {
|
|
1089
|
-
debug.router(`Found a pre-loaded request for [${targetUrl}]`);
|
|
1090
|
-
discardPreloadedRequest(targetUrl);
|
|
1091
|
-
return preloaded;
|
|
1092
|
-
}
|
|
1093
|
-
return await context.axios.request({
|
|
1094
|
-
url: targetUrl.toString(),
|
|
1095
|
-
method: options.method,
|
|
1096
|
-
data: options.method === "GET" ? {} : options.data,
|
|
1097
|
-
params: options.method === "GET" ? options.data : {},
|
|
1098
|
-
signal: abortController?.signal,
|
|
1099
|
-
headers: {
|
|
1100
|
-
...options.headers,
|
|
1101
|
-
...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
|
|
1102
|
-
...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
|
|
1103
|
-
...when(options.only !== void 0 || options.except !== void 0, {
|
|
1104
|
-
[PARTIAL_COMPONENT_HEADER]: context.view.component,
|
|
1105
|
-
...when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
|
|
1106
|
-
...when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
|
|
1107
|
-
}, {}),
|
|
1108
|
-
...when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
|
|
1109
|
-
...when(context.version, { [VERSION_HEADER]: context.version }, {}),
|
|
1110
|
-
[HYBRIDLY_HEADER]: true,
|
|
1111
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
1112
|
-
"Accept": "text/html, application/xhtml+xml"
|
|
1113
|
-
},
|
|
1114
|
-
responseType: "arraybuffer",
|
|
1115
|
-
validateStatus: () => true,
|
|
1116
|
-
onUploadProgress: async (event) => {
|
|
1117
|
-
await runHooks("progress", options.hooks, {
|
|
1118
|
-
event,
|
|
1119
|
-
percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
|
|
1120
|
-
}, context);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
1601
|
/** Initializes the router by reading the context and registering events if necessary. */
|
|
1125
1602
|
async function initializeRouter() {
|
|
1126
1603
|
const context = getRouterContext();
|
|
@@ -1139,25 +1616,6 @@ async function initializeRouter() {
|
|
|
1139
1616
|
await runHooks("ready", {}, context);
|
|
1140
1617
|
return context;
|
|
1141
1618
|
}
|
|
1142
|
-
/** Performs a local navigation to the given component without a round-trip. */
|
|
1143
|
-
async function performLocalNavigation(targetUrl, options) {
|
|
1144
|
-
const context = getRouterContext();
|
|
1145
|
-
const url = normalizeUrl(targetUrl);
|
|
1146
|
-
return await navigate({
|
|
1147
|
-
...options,
|
|
1148
|
-
type: "local",
|
|
1149
|
-
payload: {
|
|
1150
|
-
version: context.version,
|
|
1151
|
-
dialog: options?.dialog === false ? void 0 : options?.dialog ?? context.dialog,
|
|
1152
|
-
url,
|
|
1153
|
-
view: {
|
|
1154
|
-
component: options?.component ?? context.view.component,
|
|
1155
|
-
properties: options?.properties ?? context.view.properties,
|
|
1156
|
-
deferred: []
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
1619
|
//#endregion
|
|
1162
1620
|
//#region src/authorization.ts
|
|
1163
1621
|
/**
|
|
@@ -1168,4 +1626,4 @@ function can(resource, action) {
|
|
|
1168
1626
|
return resource.authorization?.[action] ?? false;
|
|
1169
1627
|
}
|
|
1170
1628
|
//#endregion
|
|
1171
|
-
export { can, constants_exports as constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
|
|
1629
|
+
export { can, constants_exports as constants, createRouter, createXhrHttpClient, definePlugin, getRouterContext, isHttpAbortError, isHttpError, makeUrl, parseQueryString, registerHook, route, router, sameUrls, stringifyQueryString };
|