@hybridly/core 0.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +642 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.mjs +624 -0
- package/package.json +50 -0
- package/properties.d.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Anthony Fu <https://github.com/antfu>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const qs = require('qs');
|
|
7
|
+
const utils = require('@hybridly/utils');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
|
|
10
|
+
|
|
11
|
+
const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
12
|
+
const qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
|
|
13
|
+
|
|
14
|
+
const STORAGE_EXTERNAL_KEY = "hybridly:external";
|
|
15
|
+
const HYBRIDLY_HEADER = "x-hybridly";
|
|
16
|
+
const EXTERNAL_VISIT_HEADER = `${HYBRIDLY_HEADER}-external`;
|
|
17
|
+
const PARTIAL_COMPONENT_HEADER = `${HYBRIDLY_HEADER}-partial-component`;
|
|
18
|
+
const ONLY_DATA_HEADER = `${HYBRIDLY_HEADER}-only-data`;
|
|
19
|
+
const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
|
|
20
|
+
const CONTEXT_HEADER = `${HYBRIDLY_HEADER}-context`;
|
|
21
|
+
const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
|
|
22
|
+
const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
|
|
23
|
+
const SCROLL_REGION_ATTRIBUTE = "scroll-region";
|
|
24
|
+
|
|
25
|
+
const constants = {
|
|
26
|
+
__proto__: null,
|
|
27
|
+
STORAGE_EXTERNAL_KEY: STORAGE_EXTERNAL_KEY,
|
|
28
|
+
HYBRIDLY_HEADER: HYBRIDLY_HEADER,
|
|
29
|
+
EXTERNAL_VISIT_HEADER: EXTERNAL_VISIT_HEADER,
|
|
30
|
+
PARTIAL_COMPONENT_HEADER: PARTIAL_COMPONENT_HEADER,
|
|
31
|
+
ONLY_DATA_HEADER: ONLY_DATA_HEADER,
|
|
32
|
+
EXCEPT_DATA_HEADER: EXCEPT_DATA_HEADER,
|
|
33
|
+
CONTEXT_HEADER: CONTEXT_HEADER,
|
|
34
|
+
VERSION_HEADER: VERSION_HEADER,
|
|
35
|
+
ERROR_BAG_HEADER: ERROR_BAG_HEADER,
|
|
36
|
+
SCROLL_REGION_ATTRIBUTE: SCROLL_REGION_ATTRIBUTE
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
class NotAHybridlyResponseError extends Error {
|
|
40
|
+
constructor(response) {
|
|
41
|
+
super();
|
|
42
|
+
this.response = response;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
class VisitCancelledError extends Error {
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function saveScrollPositions() {
|
|
49
|
+
const regions = getScrollRegions();
|
|
50
|
+
utils.debug.scroll("Saving scroll positions of:", regions);
|
|
51
|
+
setContext({
|
|
52
|
+
scrollRegions: regions.map(({ scrollTop, scrollLeft }) => ({
|
|
53
|
+
top: scrollTop,
|
|
54
|
+
left: scrollLeft
|
|
55
|
+
}))
|
|
56
|
+
});
|
|
57
|
+
setHistoryState({ replace: true });
|
|
58
|
+
}
|
|
59
|
+
function getScrollRegions() {
|
|
60
|
+
return Array.from(document?.querySelectorAll(`[${SCROLL_REGION_ATTRIBUTE}]`) ?? []);
|
|
61
|
+
}
|
|
62
|
+
function resetScrollPositions() {
|
|
63
|
+
utils.debug.scroll("Resetting scroll positions.");
|
|
64
|
+
getScrollRegions().concat(document.documentElement, document.body).forEach((element) => {
|
|
65
|
+
element.scrollTop = 0;
|
|
66
|
+
element.scrollLeft = 0;
|
|
67
|
+
});
|
|
68
|
+
saveScrollPositions();
|
|
69
|
+
if (window.location.hash) {
|
|
70
|
+
utils.debug.scroll(`Hash is present, scrolling to the element of ID ${window.location.hash}.`);
|
|
71
|
+
document.getElementById(window.location.hash.slice(1))?.scrollIntoView();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function restoreScrollPositions() {
|
|
75
|
+
utils.debug.scroll("Restoring scroll positions stored in the context.");
|
|
76
|
+
const context = getRouterContext();
|
|
77
|
+
const regions = getScrollRegions();
|
|
78
|
+
if (!context.scrollRegions) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let tries = 0;
|
|
82
|
+
const timer = setInterval(() => {
|
|
83
|
+
if (context.scrollRegions.length !== regions.length) {
|
|
84
|
+
if (++tries > 20) {
|
|
85
|
+
utils.debug.scroll("The limit of tries has been reached. Cancelling scroll restoration.");
|
|
86
|
+
clearInterval(timer);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
utils.debug.scroll(`The scroll regions count do not match. Waiting for page to fully load (try #${tries}).`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
clearInterval(timer);
|
|
93
|
+
regions.forEach((el, i) => el.scrollTo({
|
|
94
|
+
top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
|
|
95
|
+
left: context.scrollRegions.at(i)?.top ?? el.scrollLeft
|
|
96
|
+
}));
|
|
97
|
+
}, 50);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeUrl(href) {
|
|
101
|
+
return makeUrl(href).toString();
|
|
102
|
+
}
|
|
103
|
+
function makeUrl(href, transformations = {}) {
|
|
104
|
+
try {
|
|
105
|
+
const base = document?.location?.href === "//" ? void 0 : document.location.href;
|
|
106
|
+
const url = new URL(String(href), base);
|
|
107
|
+
Object.entries(transformations ?? {}).forEach(([key, value]) => Reflect.set(url, key, value));
|
|
108
|
+
return url;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new TypeError(`${href} is not resolvable to a valid URL.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function sameUrls(...hrefs) {
|
|
114
|
+
if (hrefs.length < 2) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return hrefs.every((href) => {
|
|
119
|
+
return makeUrl(href, { hash: "" }).toJSON() === makeUrl(hrefs.at(0), { hash: "" }).toJSON();
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function fillHash(currentUrl, targetUrl) {
|
|
126
|
+
currentUrl = makeUrl(currentUrl);
|
|
127
|
+
targetUrl = makeUrl(targetUrl);
|
|
128
|
+
if (currentUrl.hash && !targetUrl.hash && sameUrls(targetUrl, currentUrl)) {
|
|
129
|
+
targetUrl.hash = currentUrl.hash;
|
|
130
|
+
}
|
|
131
|
+
return targetUrl.toString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setHistoryState(options = {}) {
|
|
135
|
+
if (!window?.history) {
|
|
136
|
+
throw new Error("The history API is not available, so Hybridly cannot operate.");
|
|
137
|
+
}
|
|
138
|
+
const context = getRouterContext();
|
|
139
|
+
const method = options.replace ? "replaceState" : "pushState";
|
|
140
|
+
const serialized = serializeContext(context);
|
|
141
|
+
utils.debug.history("Setting history state:", {
|
|
142
|
+
method,
|
|
143
|
+
context,
|
|
144
|
+
serialized
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
window.history[method](serialized, "", context.url);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error("Hybridly could not save its current state in the history. This is most likely due to a property being non-serializable, such as a proxy or a reference.");
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function getHistoryState(key) {
|
|
154
|
+
const state = key ? window.history.state?.state?.[key] : window.history.state?.state;
|
|
155
|
+
return getRouterContext().serializer.unserialize(state);
|
|
156
|
+
}
|
|
157
|
+
async function registerEventListeners() {
|
|
158
|
+
const context = getRouterContext();
|
|
159
|
+
utils.debug.history("Registering [popstate] and [scroll] event listeners.");
|
|
160
|
+
window?.addEventListener("popstate", async (event) => {
|
|
161
|
+
utils.debug.history("Navigation detected (popstate event). State:", { state: event.state });
|
|
162
|
+
if (!event.state) {
|
|
163
|
+
utils.debug.history("There is no state. Adding hash if any and restoring scroll positions.");
|
|
164
|
+
return await navigate({
|
|
165
|
+
payload: {
|
|
166
|
+
...context,
|
|
167
|
+
url: makeUrl(context.url, { hash: window.location.hash }).toString()
|
|
168
|
+
},
|
|
169
|
+
preserveScroll: true,
|
|
170
|
+
preserveState: true,
|
|
171
|
+
replace: true
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
await navigate({
|
|
175
|
+
payload: event.state,
|
|
176
|
+
preserveScroll: true,
|
|
177
|
+
preserveState: false,
|
|
178
|
+
updateHistoryState: false,
|
|
179
|
+
isBackForward: true
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
window?.addEventListener("scroll", (event) => utils.debounce(() => {
|
|
183
|
+
if (event?.target?.hasAttribute?.(SCROLL_REGION_ATTRIBUTE)) {
|
|
184
|
+
saveScrollPositions();
|
|
185
|
+
}
|
|
186
|
+
}, 100), true);
|
|
187
|
+
}
|
|
188
|
+
function isBackForwardVisit() {
|
|
189
|
+
if (!window.history.state) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return window.performance?.getEntriesByType("navigation").at(0)?.type === "back_forward";
|
|
193
|
+
}
|
|
194
|
+
async function handleBackForwardVisit() {
|
|
195
|
+
utils.debug.router("Handling a back/forward visit.");
|
|
196
|
+
window.history.state.version = getRouterContext().version;
|
|
197
|
+
await navigate({
|
|
198
|
+
preserveScroll: true,
|
|
199
|
+
preserveState: true
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function remember(key, value) {
|
|
203
|
+
utils.debug.history(`Remembering key "${key}" with value`, value);
|
|
204
|
+
setContext({
|
|
205
|
+
state: {
|
|
206
|
+
...getRouterContext().state,
|
|
207
|
+
[key]: value
|
|
208
|
+
}
|
|
209
|
+
}, { propagate: false });
|
|
210
|
+
setHistoryState({ replace: true });
|
|
211
|
+
}
|
|
212
|
+
function getKeyFromHistory(key) {
|
|
213
|
+
return getHistoryState(key);
|
|
214
|
+
}
|
|
215
|
+
function serializeContext(context) {
|
|
216
|
+
return {
|
|
217
|
+
url: context.url,
|
|
218
|
+
version: context.version,
|
|
219
|
+
view: context.serializer.serialize(context.view),
|
|
220
|
+
dialog: context.dialog,
|
|
221
|
+
scrollRegions: context.scrollRegions,
|
|
222
|
+
state: context.serializer.serialize(context.state)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function createSerializer(options) {
|
|
226
|
+
if (options.serializer) {
|
|
227
|
+
return options.serializer;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
serialize: (view) => JSON.parse(JSON.stringify(view)),
|
|
231
|
+
unserialize: (state) => state
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const state = {
|
|
236
|
+
initialized: false,
|
|
237
|
+
context: {}
|
|
238
|
+
};
|
|
239
|
+
function getRouterContext() {
|
|
240
|
+
return getInternalRouterContext();
|
|
241
|
+
}
|
|
242
|
+
function getInternalRouterContext() {
|
|
243
|
+
if (!state.initialized) {
|
|
244
|
+
throw new Error("Hybridly is not initialized.");
|
|
245
|
+
}
|
|
246
|
+
return state.context;
|
|
247
|
+
}
|
|
248
|
+
async function initializeContext(options) {
|
|
249
|
+
state.initialized = true;
|
|
250
|
+
state.context = {
|
|
251
|
+
...options.payload,
|
|
252
|
+
serializer: createSerializer(options),
|
|
253
|
+
url: makeUrl(options.payload.url).toString(),
|
|
254
|
+
adapter: options.adapter,
|
|
255
|
+
scrollRegions: [],
|
|
256
|
+
plugins: options.plugins ?? [],
|
|
257
|
+
hooks: {},
|
|
258
|
+
state: {}
|
|
259
|
+
};
|
|
260
|
+
for (const plugin of state.context.plugins) {
|
|
261
|
+
utils.debug.plugin(plugin.name, 'Calling "initialized" hook.');
|
|
262
|
+
await plugin.initialized?.(state.context);
|
|
263
|
+
}
|
|
264
|
+
return getInternalRouterContext();
|
|
265
|
+
}
|
|
266
|
+
function setContext(merge = {}, options = {}) {
|
|
267
|
+
Object.keys(merge).forEach((key) => {
|
|
268
|
+
Reflect.set(state.context, key, merge[key]);
|
|
269
|
+
});
|
|
270
|
+
if (options.propagate !== false) {
|
|
271
|
+
state.context.adapter.update?.(state.context);
|
|
272
|
+
}
|
|
273
|
+
utils.debug.context("Updated context:", { context: state.context, added: merge });
|
|
274
|
+
}
|
|
275
|
+
function payloadFromContext() {
|
|
276
|
+
return {
|
|
277
|
+
url: getRouterContext().url,
|
|
278
|
+
version: getRouterContext().version,
|
|
279
|
+
view: getRouterContext().view,
|
|
280
|
+
dialog: getRouterContext().dialog
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function performExternalVisit(options) {
|
|
285
|
+
utils.debug.external("Making a hard navigation for an external visit:", options);
|
|
286
|
+
window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
|
|
287
|
+
window.location.href = options.url;
|
|
288
|
+
if (sameUrls(window.location, options.url)) {
|
|
289
|
+
utils.debug.external("Manually reloading due to the external URL being the same.");
|
|
290
|
+
window.location.reload();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function isExternalResponse(response) {
|
|
294
|
+
return response?.status === 409 && !!response?.headers?.[EXTERNAL_VISIT_HEADER];
|
|
295
|
+
}
|
|
296
|
+
async function handleExternalVisit() {
|
|
297
|
+
utils.debug.external("Handling an external visit.");
|
|
298
|
+
const options = JSON.parse(window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) || "{}");
|
|
299
|
+
window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
|
|
300
|
+
utils.debug.external("Options from the session storage:", options);
|
|
301
|
+
setContext({
|
|
302
|
+
url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString()
|
|
303
|
+
});
|
|
304
|
+
await navigate({
|
|
305
|
+
preserveScroll: options.preserveScroll,
|
|
306
|
+
preserveState: true
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function isExternalVisit() {
|
|
310
|
+
try {
|
|
311
|
+
return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function definePlugin(plugin) {
|
|
318
|
+
return plugin;
|
|
319
|
+
}
|
|
320
|
+
async function runPluginHooks(hook, ...args) {
|
|
321
|
+
const { plugins } = getRouterContext();
|
|
322
|
+
let result = true;
|
|
323
|
+
for (const plugin of plugins) {
|
|
324
|
+
if (plugin.hooks[hook]) {
|
|
325
|
+
utils.debug.plugin(plugin.name, `Calling "${hook}" hooks.`);
|
|
326
|
+
result = await plugin.hooks[hook]?.(...args) ?? result;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
async function runGlobalHooks(hook, ...args) {
|
|
332
|
+
const { hooks } = getRouterContext();
|
|
333
|
+
if (!hooks[hook]) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
let result = true;
|
|
337
|
+
for (const fn of hooks[hook]) {
|
|
338
|
+
utils.debug.hook(`Calling global "${hook}" hooks.`);
|
|
339
|
+
result = await fn(...args) ?? result;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
async function runHooks(hook, requestHooks, ...args) {
|
|
344
|
+
const result = await Promise.all([
|
|
345
|
+
requestHooks?.[hook]?.(...args),
|
|
346
|
+
runGlobalHooks(hook, ...args),
|
|
347
|
+
runPluginHooks(hook, ...args)
|
|
348
|
+
]);
|
|
349
|
+
return !result.includes(false);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function registerHook(hook, fn) {
|
|
353
|
+
const hooks = getRouterContext().hooks;
|
|
354
|
+
hooks[hook] = [...hooks[hook] ?? [], fn];
|
|
355
|
+
return () => hooks[hook]?.splice(hooks[hook].indexOf(fn), 1);
|
|
356
|
+
}
|
|
357
|
+
function registerHookOnce(hook, fn) {
|
|
358
|
+
const unregister = registerHook(hook, async (...args) => {
|
|
359
|
+
await fn(...args);
|
|
360
|
+
unregister();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const router = {
|
|
365
|
+
abort: async () => getRouterContext().activeVisit?.controller.abort(),
|
|
366
|
+
active: () => !!getRouterContext().activeVisit,
|
|
367
|
+
visit: async (options) => await visit(options),
|
|
368
|
+
reload: async (options) => await visit({ preserveScroll: true, preserveState: true, ...options }),
|
|
369
|
+
get: async (url, options = {}) => await visit({ ...options, url, method: "GET" }),
|
|
370
|
+
post: async (url, options = {}) => await visit({ preserveState: true, ...options, url, method: "POST" }),
|
|
371
|
+
put: async (url, options = {}) => await visit({ preserveState: true, ...options, url, method: "PUT" }),
|
|
372
|
+
patch: async (url, options = {}) => await visit({ preserveState: true, ...options, url, method: "PATCH" }),
|
|
373
|
+
delete: async (url, options = {}) => await visit({ preserveState: true, ...options, url, method: "DELETE" }),
|
|
374
|
+
local: async (url, options) => await performLocalComponentVisit(url, options),
|
|
375
|
+
external: (url, data = {}) => performLocalExternalVisit(url, data),
|
|
376
|
+
history: {
|
|
377
|
+
get: (key) => getKeyFromHistory(key),
|
|
378
|
+
remember: (key, value) => remember(key, value)
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
async function createRouter(options) {
|
|
382
|
+
await initializeContext(options);
|
|
383
|
+
return await initializeRouter();
|
|
384
|
+
}
|
|
385
|
+
async function visit(options) {
|
|
386
|
+
const visitId = utils.random();
|
|
387
|
+
const context = getRouterContext();
|
|
388
|
+
utils.debug.router("Making a visit:", { context, options, visitId });
|
|
389
|
+
try {
|
|
390
|
+
if ((utils.hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
|
|
391
|
+
options.data = utils.objectToFormData(options.data);
|
|
392
|
+
utils.debug.router("Converted data to FormData.", options.data);
|
|
393
|
+
}
|
|
394
|
+
if (!await runHooks("before", options.hooks, options)) {
|
|
395
|
+
utils.debug.router('"before" event returned false, aborting the visit.');
|
|
396
|
+
throw new VisitCancelledError('The visit was cancelled by the "before" event.');
|
|
397
|
+
}
|
|
398
|
+
if (context.activeVisit) {
|
|
399
|
+
utils.debug.router("Aborting current visit.", context.activeVisit);
|
|
400
|
+
context.activeVisit?.controller.abort();
|
|
401
|
+
}
|
|
402
|
+
saveScrollPositions();
|
|
403
|
+
if (options.url && options.transformUrl) {
|
|
404
|
+
options.url = makeUrl(options.url, options.transformUrl);
|
|
405
|
+
}
|
|
406
|
+
setContext({
|
|
407
|
+
activeVisit: {
|
|
408
|
+
id: visitId,
|
|
409
|
+
url: makeUrl(options.url ?? context.url),
|
|
410
|
+
controller: new AbortController(),
|
|
411
|
+
options
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
await runHooks("start", options.hooks, context);
|
|
415
|
+
utils.debug.router("Making request with axios.");
|
|
416
|
+
const response = await axios__default.request({
|
|
417
|
+
url: context.activeVisit.url.toString(),
|
|
418
|
+
method: options.method ?? "GET",
|
|
419
|
+
data: options.method === "GET" ? {} : options.data,
|
|
420
|
+
params: options.method === "GET" ? options.data : {},
|
|
421
|
+
signal: context.activeVisit.controller.signal,
|
|
422
|
+
headers: {
|
|
423
|
+
...options.headers,
|
|
424
|
+
...utils.when(options.only !== void 0 || options.except !== void 0, {
|
|
425
|
+
[PARTIAL_COMPONENT_HEADER]: context.view.name,
|
|
426
|
+
...utils.when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
|
|
427
|
+
...utils.when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
|
|
428
|
+
}, {}),
|
|
429
|
+
...utils.when(options.errorBag, { [ERROR_BAG_HEADER]: options.errorBag }, {}),
|
|
430
|
+
...utils.when(context.version, { [VERSION_HEADER]: context.version }, {}),
|
|
431
|
+
[HYBRIDLY_HEADER]: true,
|
|
432
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
433
|
+
"Accept": "text/html, application/xhtml+xml"
|
|
434
|
+
},
|
|
435
|
+
validateStatus: () => true,
|
|
436
|
+
onUploadProgress: async (event) => {
|
|
437
|
+
await runHooks("progress", options.hooks, {
|
|
438
|
+
event,
|
|
439
|
+
percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
await runHooks("data", options.hooks, response);
|
|
444
|
+
if (isExternalResponse(response)) {
|
|
445
|
+
utils.debug.router("The response is explicitely external.");
|
|
446
|
+
await performExternalVisit({
|
|
447
|
+
url: fillHash(context.activeVisit.url, response.headers[EXTERNAL_VISIT_HEADER]),
|
|
448
|
+
preserveScroll: options.preserveScroll === true
|
|
449
|
+
});
|
|
450
|
+
return { response };
|
|
451
|
+
}
|
|
452
|
+
if (!isHybridlyResponse(response)) {
|
|
453
|
+
throw new NotAHybridlyResponseError(response);
|
|
454
|
+
}
|
|
455
|
+
utils.debug.router("The response respects the hybridly protocol.");
|
|
456
|
+
const payload = response.data;
|
|
457
|
+
if ((options.only?.length ?? options.except?.length) && payload.view.name === context.view.name) {
|
|
458
|
+
utils.debug.router(`Merging ${options.only ? '"only"' : '"except"'} properties.`, payload.view.properties);
|
|
459
|
+
payload.view.properties = utils.merge(context.view.properties, payload.view.properties);
|
|
460
|
+
utils.debug.router("Merged properties:", payload.view.properties);
|
|
461
|
+
}
|
|
462
|
+
await navigate({
|
|
463
|
+
payload: {
|
|
464
|
+
...payload,
|
|
465
|
+
url: fillHash(context.activeVisit.url, payload.url)
|
|
466
|
+
},
|
|
467
|
+
preserveScroll: options.preserveScroll === true,
|
|
468
|
+
preserveState: options.preserveState,
|
|
469
|
+
preserveUrl: options.preserveUrl,
|
|
470
|
+
replace: options.replace === true || sameUrls(payload.url, window.location.href) || options.preserveUrl
|
|
471
|
+
});
|
|
472
|
+
if (Object.keys(context.view.properties.errors ?? {}).length > 0) {
|
|
473
|
+
const errors = (() => {
|
|
474
|
+
if (options.errorBag && typeof context.view.properties.errors === "object") {
|
|
475
|
+
return context.view.properties.errors[options.errorBag] ?? {};
|
|
476
|
+
}
|
|
477
|
+
return context.view.properties.errors;
|
|
478
|
+
})();
|
|
479
|
+
utils.debug.router("The request returned validation errors.", errors);
|
|
480
|
+
await runHooks("error", options.hooks, errors);
|
|
481
|
+
setContext({
|
|
482
|
+
activeVisit: {
|
|
483
|
+
...context.activeVisit,
|
|
484
|
+
status: "error"
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
await runHooks("success", options.hooks, payload);
|
|
489
|
+
setContext({
|
|
490
|
+
activeVisit: {
|
|
491
|
+
...context.activeVisit,
|
|
492
|
+
status: "success"
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return { response };
|
|
497
|
+
} catch (error) {
|
|
498
|
+
await utils.match(error.constructor.name, {
|
|
499
|
+
VisitCancelledError: async () => {
|
|
500
|
+
utils.debug.router('The request was cancelled through the "before" hook.', error);
|
|
501
|
+
console.warn(error);
|
|
502
|
+
await runHooks("abort", options.hooks, context);
|
|
503
|
+
},
|
|
504
|
+
AbortError: async () => {
|
|
505
|
+
utils.debug.router("The request was cancelled.", error);
|
|
506
|
+
console.warn(error);
|
|
507
|
+
await runHooks("abort", options.hooks, context);
|
|
508
|
+
},
|
|
509
|
+
NotAHybridlyResponseError: async () => {
|
|
510
|
+
utils.debug.router("The request was not hybridly.");
|
|
511
|
+
console.error(error);
|
|
512
|
+
await runHooks("invalid", options.hooks, error);
|
|
513
|
+
utils.showResponseErrorModal(error.response.data);
|
|
514
|
+
},
|
|
515
|
+
default: async () => {
|
|
516
|
+
utils.debug.router("An unknown error occured.", error);
|
|
517
|
+
console.error(error);
|
|
518
|
+
await runHooks("exception", options.hooks, error);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
await runHooks("fail", options.hooks, context);
|
|
522
|
+
return {
|
|
523
|
+
error: {
|
|
524
|
+
type: error.constructor.name,
|
|
525
|
+
actual: error
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
} finally {
|
|
529
|
+
utils.debug.router("Ending visit.");
|
|
530
|
+
await runHooks("after", options.hooks, context);
|
|
531
|
+
if (context.activeVisit?.id === visitId) {
|
|
532
|
+
setContext({ activeVisit: void 0 });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function isHybridlyResponse(response) {
|
|
537
|
+
return !!response?.headers[HYBRIDLY_HEADER];
|
|
538
|
+
}
|
|
539
|
+
async function navigate(options) {
|
|
540
|
+
const context = getRouterContext();
|
|
541
|
+
utils.debug.router("Making an internal navigation:", { context, options });
|
|
542
|
+
options.payload ?? (options.payload = payloadFromContext());
|
|
543
|
+
const evaluateConditionalOption = (option) => typeof option === "function" ? option(options.payload) : option;
|
|
544
|
+
const shouldPreserveState = evaluateConditionalOption(options.preserveState);
|
|
545
|
+
const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
|
|
546
|
+
const shouldReplaceHistory = evaluateConditionalOption(options.replace);
|
|
547
|
+
const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
|
|
548
|
+
if (shouldPreserveState && getHistoryState() && options.payload.view.name === context.view.name) {
|
|
549
|
+
setContext({ state: getHistoryState() });
|
|
550
|
+
}
|
|
551
|
+
if (shouldReplaceUrl) {
|
|
552
|
+
utils.debug.router(`Preserving the current URL (${context.url}) instead of navigating to ${options.payload.url}`);
|
|
553
|
+
options.payload.url = context.url;
|
|
554
|
+
}
|
|
555
|
+
setContext({
|
|
556
|
+
...options.payload,
|
|
557
|
+
state: {}
|
|
558
|
+
}, { propagate: false });
|
|
559
|
+
if (options.updateHistoryState !== false) {
|
|
560
|
+
utils.debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
|
|
561
|
+
setHistoryState({ replace: shouldReplaceHistory });
|
|
562
|
+
}
|
|
563
|
+
const viewComponent = await context.adapter.resolveComponent(context.view.name);
|
|
564
|
+
utils.debug.router(`Component [${context.view.name}] resolved to:`, viewComponent);
|
|
565
|
+
await context.adapter.swapView({
|
|
566
|
+
component: viewComponent,
|
|
567
|
+
preserveState: shouldPreserveState
|
|
568
|
+
});
|
|
569
|
+
if (context.dialog) {
|
|
570
|
+
const dialogComponent = await context.adapter.resolveComponent(context.dialog.name);
|
|
571
|
+
utils.debug.router(`Dialog [${context.view.name}] resolved to:`, dialogComponent);
|
|
572
|
+
await context.adapter.swapDialog({
|
|
573
|
+
component: dialogComponent,
|
|
574
|
+
preserveState: shouldPreserveState
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
setContext();
|
|
578
|
+
if (!shouldPreserveScroll) {
|
|
579
|
+
resetScrollPositions();
|
|
580
|
+
} else {
|
|
581
|
+
restoreScrollPositions();
|
|
582
|
+
}
|
|
583
|
+
await runHooks("navigate", {}, options);
|
|
584
|
+
}
|
|
585
|
+
async function initializeRouter() {
|
|
586
|
+
const context = getRouterContext();
|
|
587
|
+
if (isBackForwardVisit()) {
|
|
588
|
+
handleBackForwardVisit();
|
|
589
|
+
} else if (isExternalVisit()) {
|
|
590
|
+
handleExternalVisit();
|
|
591
|
+
} else {
|
|
592
|
+
utils.debug.router("Handling the initial page visit.");
|
|
593
|
+
setContext({
|
|
594
|
+
url: makeUrl(context.url, { hash: window.location.hash }).toString()
|
|
595
|
+
});
|
|
596
|
+
await navigate({
|
|
597
|
+
preserveState: true,
|
|
598
|
+
replace: sameUrls(context.url, window.location.href)
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
registerEventListeners();
|
|
602
|
+
return context;
|
|
603
|
+
}
|
|
604
|
+
async function performLocalComponentVisit(targetUrl, options) {
|
|
605
|
+
const context = getRouterContext();
|
|
606
|
+
const url = normalizeUrl(targetUrl);
|
|
607
|
+
return await navigate({
|
|
608
|
+
...options,
|
|
609
|
+
payload: {
|
|
610
|
+
version: context.version,
|
|
611
|
+
dialog: context.dialog,
|
|
612
|
+
url,
|
|
613
|
+
view: {
|
|
614
|
+
name: options.component ?? context.view.name,
|
|
615
|
+
properties: options.properties
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function performLocalExternalVisit(url, data) {
|
|
621
|
+
document.location.href = makeUrl(url, {
|
|
622
|
+
search: qs__default.stringify(data, {
|
|
623
|
+
encodeValuesOnly: true,
|
|
624
|
+
arrayFormat: "brackets"
|
|
625
|
+
})
|
|
626
|
+
}).toString();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function can(resource, action) {
|
|
630
|
+
return resource.authorization?.[action] ?? false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
exports.can = can;
|
|
634
|
+
exports.constants = constants;
|
|
635
|
+
exports.createRouter = createRouter;
|
|
636
|
+
exports.definePlugin = definePlugin;
|
|
637
|
+
exports.getRouterContext = getRouterContext;
|
|
638
|
+
exports.makeUrl = makeUrl;
|
|
639
|
+
exports.registerHook = registerHook;
|
|
640
|
+
exports.registerHookOnce = registerHookOnce;
|
|
641
|
+
exports.router = router;
|
|
642
|
+
exports.sameUrls = sameUrls;
|