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