@hybridly/core 0.8.3 → 0.10.0-beta.1

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