@hybridly/core 0.0.1-dev.3 → 0.1.0-alpha.0

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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ const utils = require('@hybridly/utils');
5
6
  const axios = require('axios');
6
7
  const qs = require('qs');
7
- const utils = require('@hybridly/utils');
8
8
 
9
9
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
10
10
 
@@ -12,10 +12,12 @@ const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
12
12
  const qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
13
13
 
14
14
  const STORAGE_EXTERNAL_KEY = "hybridly:external";
15
- const HYBRIDLY_HEADER = "x-hybridly";
16
- const EXTERNAL_VISIT_HEADER = `${HYBRIDLY_HEADER}-external`;
15
+ const HYBRIDLY_HEADER = "x-hybrid";
16
+ const EXTERNAL_NAVIGATION_HEADER = `${HYBRIDLY_HEADER}-external`;
17
17
  const PARTIAL_COMPONENT_HEADER = `${HYBRIDLY_HEADER}-partial-component`;
18
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`;
19
21
  const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
20
22
  const CONTEXT_HEADER = `${HYBRIDLY_HEADER}-context`;
21
23
  const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
@@ -26,9 +28,11 @@ const constants = {
26
28
  __proto__: null,
27
29
  STORAGE_EXTERNAL_KEY: STORAGE_EXTERNAL_KEY,
28
30
  HYBRIDLY_HEADER: HYBRIDLY_HEADER,
29
- EXTERNAL_VISIT_HEADER: EXTERNAL_VISIT_HEADER,
31
+ EXTERNAL_NAVIGATION_HEADER: EXTERNAL_NAVIGATION_HEADER,
30
32
  PARTIAL_COMPONENT_HEADER: PARTIAL_COMPONENT_HEADER,
31
33
  ONLY_DATA_HEADER: ONLY_DATA_HEADER,
34
+ DIALOG_KEY_HEADER: DIALOG_KEY_HEADER,
35
+ DIALOG_REDIRECT_HEADER: DIALOG_REDIRECT_HEADER,
32
36
  EXCEPT_DATA_HEADER: EXCEPT_DATA_HEADER,
33
37
  CONTEXT_HEADER: CONTEXT_HEADER,
34
38
  VERSION_HEADER: VERSION_HEADER,
@@ -36,13 +40,90 @@ const constants = {
36
40
  SCROLL_REGION_ATTRIBUTE: SCROLL_REGION_ATTRIBUTE
37
41
  };
38
42
 
39
- class NotAHybridlyResponseError extends Error {
43
+ class NotAHybridResponseError extends Error {
40
44
  constructor(response) {
41
45
  super();
42
46
  this.response = response;
43
47
  }
44
48
  }
45
- class VisitCancelledError extends Error {
49
+ class NavigationCancelledError extends Error {
50
+ }
51
+ class RoutingNotInitialized extends Error {
52
+ constructor() {
53
+ super("Routing is not initialized. Make sure the Vite plugin is enabled and that `php artisan route:list` returns no error.");
54
+ }
55
+ }
56
+ class RouteNotFound extends Error {
57
+ constructor(name) {
58
+ super(`Route [${name}] does not exist.`);
59
+ }
60
+ }
61
+ class MissingRouteParameter extends Error {
62
+ constructor(parameter, routeName) {
63
+ super(`Parameter [${parameter}] is required for route [${routeName}].`);
64
+ }
65
+ }
66
+
67
+ function definePlugin(plugin) {
68
+ return plugin;
69
+ }
70
+ async function forEachPlugin(cb) {
71
+ const { plugins } = getRouterContext();
72
+ for (const plugin of plugins) {
73
+ await cb(plugin);
74
+ }
75
+ }
76
+ async function runPluginHooks(hook, ...args) {
77
+ let result = true;
78
+ await forEachPlugin(async (plugin) => {
79
+ if (plugin[hook]) {
80
+ utils.debug.plugin(plugin.name, `Calling "${hook}" hook.`);
81
+ result && (result = await plugin[hook]?.(...args) !== false);
82
+ }
83
+ });
84
+ return result;
85
+ }
86
+ async function runGlobalHooks(hook, ...args) {
87
+ const { hooks } = getRouterContext();
88
+ if (!hooks[hook]) {
89
+ return true;
90
+ }
91
+ let result = true;
92
+ for (const fn of hooks[hook]) {
93
+ utils.debug.hook(`Calling global "${hook}" hooks.`);
94
+ result = await fn(...args) ?? result;
95
+ }
96
+ return result;
97
+ }
98
+ async function runHooks(hook, requestHooks, ...args) {
99
+ const result = await Promise.all([
100
+ requestHooks?.[hook]?.(...args),
101
+ runGlobalHooks(hook, ...args),
102
+ runPluginHooks(hook, ...args)
103
+ ]);
104
+ utils.debug.hook(`Called all hooks for [${hook}],`, result);
105
+ return !result.includes(false);
106
+ }
107
+
108
+ function appendCallbackToHooks(hook, fn) {
109
+ const hooks = getRouterContext().hooks;
110
+ hooks[hook] = [...hooks[hook] ?? [], fn];
111
+ return () => {
112
+ const index = hooks[hook].indexOf(fn);
113
+ if (index !== -1) {
114
+ hooks[hook]?.splice(index, 1);
115
+ }
116
+ };
117
+ }
118
+ function registerHook(hook, fn, options) {
119
+ if (options?.once) {
120
+ const unregister = appendCallbackToHooks(hook, async (...args) => {
121
+ await fn(...args);
122
+ unregister();
123
+ });
124
+ return unregister;
125
+ }
126
+ return appendCallbackToHooks(hook, fn);
46
127
  }
47
128
 
48
129
  function saveScrollPositions() {
@@ -61,10 +142,10 @@ function getScrollRegions() {
61
142
  }
62
143
  function resetScrollPositions() {
63
144
  utils.debug.scroll("Resetting scroll positions.");
64
- getScrollRegions().concat(document.documentElement, document.body).forEach((element) => {
65
- element.scrollTop = 0;
66
- element.scrollLeft = 0;
67
- });
145
+ getScrollRegions().concat(document.documentElement, document.body).forEach((element) => element.scrollTo({
146
+ top: 0,
147
+ left: 0
148
+ }));
68
149
  saveScrollPositions();
69
150
  if (window.location.hash) {
70
151
  utils.debug.scroll(`Hash is present, scrolling to the element of ID ${window.location.hash}.`);
@@ -76,10 +157,14 @@ async function restoreScrollPositions() {
76
157
  const context = getRouterContext();
77
158
  const regions = getScrollRegions();
78
159
  if (!context.scrollRegions) {
160
+ utils.debug.scroll("No region found to restore.");
79
161
  return;
80
162
  }
81
163
  let tries = 0;
82
- const timer = setInterval(() => {
164
+ const timer = setInterval(restore, 50);
165
+ restore();
166
+ function restore() {
167
+ utils.debug.scroll(`Restoring ${regions.length}/${context.scrollRegions.length} region(s).`);
83
168
  if (context.scrollRegions.length !== regions.length) {
84
169
  if (++tries > 20) {
85
170
  utils.debug.scroll("The limit of tries has been reached. Cancelling scroll restoration.");
@@ -94,17 +179,31 @@ async function restoreScrollPositions() {
94
179
  top: context.scrollRegions.at(i)?.top ?? el.scrollTop,
95
180
  left: context.scrollRegions.at(i)?.top ?? el.scrollLeft
96
181
  }));
97
- }, 50);
182
+ }
98
183
  }
99
184
 
100
- function normalizeUrl(href) {
101
- return makeUrl(href).toString();
185
+ function normalizeUrl(href, trailingSlash) {
186
+ return makeUrl(href, { trailingSlash }).toString();
102
187
  }
103
188
  function makeUrl(href, transformations = {}) {
104
189
  try {
105
190
  const base = document?.location?.href === "//" ? void 0 : document.location.href;
106
191
  const url = new URL(String(href), base);
107
- Object.entries(transformations ?? {}).forEach(([key, value]) => Reflect.set(url, key, value));
192
+ transformations = typeof transformations === "function" ? transformations(url) ?? {} : transformations ?? {};
193
+ Object.entries(transformations).forEach(([key, value]) => {
194
+ if (key === "query") {
195
+ key = "search";
196
+ value = qs__default.stringify(utils.merge(qs__default.parse(url.search, { ignoreQueryPrefix: true }), value), {
197
+ encodeValuesOnly: true,
198
+ arrayFormat: "brackets"
199
+ });
200
+ }
201
+ Reflect.set(url, key, value);
202
+ });
203
+ if (transformations.trailingSlash === false) {
204
+ const _url = utils.removeTrailingSlash(url.toString().replace(/\/\?/, "?"));
205
+ url.toString = () => _url;
206
+ }
108
207
  return url;
109
208
  } catch (error) {
110
209
  throw new TypeError(`${href} is not resolvable to a valid URL.`);
@@ -159,6 +258,11 @@ async function registerEventListeners() {
159
258
  utils.debug.history("Registering [popstate] and [scroll] event listeners.");
160
259
  window?.addEventListener("popstate", async (event) => {
161
260
  utils.debug.history("Navigation detected (popstate event). State:", { state: event.state });
261
+ if (context.pendingNavigation) {
262
+ utils.debug.router("Aborting current navigation.", context.pendingNavigation);
263
+ context.pendingNavigation?.controller?.abort();
264
+ }
265
+ await runHooks("backForward", {}, event.state, context);
162
266
  if (!event.state) {
163
267
  utils.debug.history("There is no state. Adding hash if any and restoring scroll positions.");
164
268
  return await navigate({
@@ -174,7 +278,7 @@ async function registerEventListeners() {
174
278
  await navigate({
175
279
  payload: event.state,
176
280
  preserveScroll: true,
177
- preserveState: false,
281
+ preserveState: !!getInternalRouterContext().dialog || !!event.state.dialog,
178
282
  updateHistoryState: false,
179
283
  isBackForward: true
180
284
  });
@@ -185,18 +289,21 @@ async function registerEventListeners() {
185
289
  }
186
290
  }, 100), true);
187
291
  }
188
- function isBackForwardVisit() {
292
+ function isBackForwardNavigation() {
189
293
  if (!window.history.state) {
190
294
  return false;
191
295
  }
192
296
  return window.performance?.getEntriesByType("navigation").at(0)?.type === "back_forward";
193
297
  }
194
- async function handleBackForwardVisit() {
195
- utils.debug.router("Handling a back/forward visit.");
298
+ async function handleBackForwardNavigation() {
299
+ utils.debug.router("Handling a back/forward navigation.");
196
300
  window.history.state.version = getRouterContext().version;
197
301
  await navigate({
302
+ payload: window.history.state,
198
303
  preserveScroll: true,
199
- preserveState: true
304
+ preserveState: false,
305
+ updateHistoryState: false,
306
+ isBackForward: true
200
307
  });
201
308
  }
202
309
  function remember(key, value) {
@@ -232,6 +339,118 @@ function createSerializer(options) {
232
339
  };
233
340
  }
234
341
 
342
+ function generateRouteFromName(name, parameters, absolute, shouldThrow) {
343
+ const url = getUrlFromName(name, parameters, shouldThrow);
344
+ return absolute === false ? url.toString().replace(url.origin, "") : url.toString();
345
+ }
346
+ function getUrlFromName(name, parameters, shouldThrow) {
347
+ const routing = getRouting();
348
+ const definition = getRouteDefinition(name);
349
+ const transforms = getRouteTransformable(name, parameters, shouldThrow);
350
+ const url = makeUrl(routing.url, (url2) => ({
351
+ hostname: definition.domain || url2.hostname,
352
+ port: routing.port?.toString() || url2.port,
353
+ trailingSlash: false,
354
+ ...transforms
355
+ }));
356
+ return url;
357
+ }
358
+ function getRouteTransformable(routeName, routeParameters, shouldThrow) {
359
+ const routing = getRouting();
360
+ const definition = getRouteDefinition(routeName);
361
+ const parameters = routeParameters || {};
362
+ const missing = Object.keys(parameters);
363
+ const replaceParameter = (match, parameterName) => {
364
+ const optional = /\?}$/.test(match);
365
+ const value = (() => {
366
+ const value2 = parameters[parameterName];
367
+ const bindingProperty = definition.bindings?.[parameterName];
368
+ if (bindingProperty && typeof value2 === "object") {
369
+ return value2[bindingProperty];
370
+ }
371
+ return value2;
372
+ })();
373
+ missing.splice(missing.indexOf(parameterName), 1);
374
+ if (value) {
375
+ const where = definition.wheres?.[parameterName];
376
+ if (where && !new RegExp(where).test(value)) {
377
+ console.warn(`[hybridly:routing] Parameter [${parameterName}] does not match the required format [${where}] for route [${routeName}].`);
378
+ }
379
+ return value;
380
+ }
381
+ if (routing.defaults?.[parameterName]) {
382
+ return routing.defaults?.[parameterName];
383
+ }
384
+ if (optional) {
385
+ return "";
386
+ }
387
+ if (shouldThrow === false) {
388
+ return "";
389
+ }
390
+ throw new MissingRouteParameter(parameterName, routeName);
391
+ };
392
+ const path = definition.uri.replace(/{([^}?]+)\??}/g, replaceParameter);
393
+ const domain = definition.domain?.replace(/{([^}?]+)\??}/g, replaceParameter);
394
+ const remaining = Object.keys(parameters).filter((key) => missing.includes(key)).reduce((obj, key) => ({
395
+ ...obj,
396
+ [key]: parameters[key]
397
+ }), {});
398
+ return {
399
+ ...domain && { hostname: domain },
400
+ pathname: path,
401
+ search: qs__default.stringify(remaining, {
402
+ encodeValuesOnly: true,
403
+ arrayFormat: "indices",
404
+ addQueryPrefix: true
405
+ })
406
+ };
407
+ }
408
+ function getRouteDefinition(name) {
409
+ const routing = getRouting();
410
+ const definition = routing.routes[name];
411
+ if (!definition) {
412
+ throw new RouteNotFound(name);
413
+ }
414
+ return definition;
415
+ }
416
+ function getRouting() {
417
+ const { routing } = getInternalRouterContext();
418
+ if (!routing) {
419
+ throw new RoutingNotInitialized();
420
+ }
421
+ return routing;
422
+ }
423
+
424
+ function isCurrentFromName(name, parameters, mode = "loose") {
425
+ const location = window.location;
426
+ const matchee = (() => {
427
+ try {
428
+ return makeUrl(generateRouteFromName(name, parameters, true, false));
429
+ } catch (error) {
430
+ }
431
+ })();
432
+ if (!matchee) {
433
+ return false;
434
+ }
435
+ if (mode === "strict") {
436
+ return location.href === matchee.href;
437
+ }
438
+ return location.href.startsWith(matchee.href);
439
+ }
440
+
441
+ function route(name, parameters, absolute) {
442
+ return generateRouteFromName(name, parameters, absolute);
443
+ }
444
+ function current(name, parameters, mode = "loose") {
445
+ return isCurrentFromName(name, parameters, mode);
446
+ }
447
+ function updateRoutingConfiguration(routing) {
448
+ if (!routing) {
449
+ return;
450
+ }
451
+ setContext({ routing });
452
+ }
453
+
235
454
  const state = {
236
455
  initialized: false,
237
456
  context: {}
@@ -249,18 +468,21 @@ async function initializeContext(options) {
249
468
  state.initialized = true;
250
469
  state.context = {
251
470
  ...options.payload,
471
+ responseErrorModals: options.responseErrorModals,
252
472
  serializer: createSerializer(options),
253
473
  url: makeUrl(options.payload.url).toString(),
254
- adapter: options.adapter,
474
+ adapter: {
475
+ ...options.adapter,
476
+ updateRoutingConfiguration
477
+ },
255
478
  scrollRegions: [],
256
479
  plugins: options.plugins ?? [],
480
+ axios: options.axios ?? axios__default.create(),
481
+ routing: options.routing,
257
482
  hooks: {},
258
483
  state: {}
259
484
  };
260
- for (const plugin of state.context.plugins) {
261
- utils.debug.plugin(plugin.name, 'Calling "initialized" hook.');
262
- await plugin.initialized?.(state.context);
263
- }
485
+ await runHooks("initialized", {}, state.context);
264
486
  return getInternalRouterContext();
265
487
  }
266
488
  function setContext(merge = {}, options = {}) {
@@ -268,7 +490,7 @@ function setContext(merge = {}, options = {}) {
268
490
  Reflect.set(state.context, key, merge[key]);
269
491
  });
270
492
  if (options.propagate !== false) {
271
- state.context.adapter.update?.(state.context);
493
+ state.context.adapter.onContextUpdate?.(state.context);
272
494
  }
273
495
  utils.debug.context("Updated context:", { context: state.context, added: merge });
274
496
  }
@@ -281,8 +503,8 @@ function payloadFromContext() {
281
503
  };
282
504
  }
283
505
 
284
- async function performExternalVisit(options) {
285
- utils.debug.external("Making a hard navigation for an external visit:", options);
506
+ async function performExternalNavigation(options) {
507
+ utils.debug.external("Navigating to an external URL:", options);
286
508
  window.sessionStorage.setItem(STORAGE_EXTERNAL_KEY, JSON.stringify(options));
287
509
  window.location.href = options.url;
288
510
  if (sameUrls(window.location, options.url)) {
@@ -290,11 +512,19 @@ async function performExternalVisit(options) {
290
512
  window.location.reload();
291
513
  }
292
514
  }
515
+ function navigateToExternalUrl(url, data) {
516
+ document.location.href = makeUrl(url, {
517
+ search: qs__default.stringify(data, {
518
+ encodeValuesOnly: true,
519
+ arrayFormat: "brackets"
520
+ })
521
+ }).toString();
522
+ }
293
523
  function isExternalResponse(response) {
294
- return response?.status === 409 && !!response?.headers?.[EXTERNAL_VISIT_HEADER];
524
+ return response?.status === 409 && !!response?.headers?.[EXTERNAL_NAVIGATION_HEADER];
295
525
  }
296
- async function handleExternalVisit() {
297
- utils.debug.external("Handling an external visit.");
526
+ async function handleExternalNavigation() {
527
+ utils.debug.external("Handling an external navigation.");
298
528
  const options = JSON.parse(window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) || "{}");
299
529
  window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
300
530
  utils.debug.external("Options from the session storage:", options);
@@ -306,7 +536,7 @@ async function handleExternalVisit() {
306
536
  preserveState: true
307
537
  });
308
538
  }
309
- function isExternalVisit() {
539
+ function isExternalNavigation() {
310
540
  try {
311
541
  return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
312
542
  } catch {
@@ -314,65 +544,41 @@ function isExternalVisit() {
314
544
  return false;
315
545
  }
316
546
 
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;
547
+ async function closeDialog(options) {
548
+ const context = getInternalRouterContext();
549
+ const url = context.dialog?.redirectUrl ?? context.dialog?.baseUrl;
550
+ if (!url) {
551
+ return;
340
552
  }
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();
553
+ context.adapter.onDialogClose?.(context);
554
+ return await performHybridNavigation({
555
+ url,
556
+ preserveScroll: true,
557
+ preserveState: true,
558
+ ...options
361
559
  });
362
560
  }
363
561
 
364
562
  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),
563
+ abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
564
+ active: () => !!getRouterContext().pendingNavigation,
565
+ navigate: async (options) => await performHybridNavigation(options),
566
+ reload: async (options) => await performHybridNavigation({ preserveScroll: true, preserveState: true, ...options }),
567
+ get: async (url, options = {}) => await performHybridNavigation({ ...options, url, method: "GET" }),
568
+ post: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "POST" }),
569
+ put: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PUT" }),
570
+ patch: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "PATCH" }),
571
+ delete: async (url, options = {}) => await performHybridNavigation({ preserveState: true, ...options, url, method: "DELETE" }),
572
+ local: async (url, options) => await performLocalNavigation(url, options),
573
+ external: (url, data = {}) => navigateToExternalUrl(url, data),
574
+ to: async (name, parameters, options) => {
575
+ const url = generateRouteFromName(name, parameters);
576
+ const method = getRouteDefinition(name).method.at(0);
577
+ return await performHybridNavigation({ url, ...options, method });
578
+ },
579
+ dialog: {
580
+ close: (options) => closeDialog(options)
581
+ },
376
582
  history: {
377
583
  get: (key) => getKeyFromHistory(key),
378
584
  remember: (key, value) => remember(key, value)
@@ -382,47 +588,69 @@ async function createRouter(options) {
382
588
  await initializeContext(options);
383
589
  return await initializeRouter();
384
590
  }
385
- async function visit(options) {
386
- const visitId = utils.random();
591
+ async function performHybridNavigation(options) {
592
+ const navigationId = utils.random();
387
593
  const context = getRouterContext();
388
- utils.debug.router("Making a visit:", { context, options, visitId });
594
+ utils.debug.router("Making a hybrid navigation:", { context, options, navigationId });
389
595
  try {
596
+ if (!options.method) {
597
+ utils.debug.router("Setting method to GET because none was provided.");
598
+ options.method = "GET";
599
+ }
600
+ options.method = options.method.toUpperCase();
390
601
  if ((utils.hasFiles(options.data) || options.useFormData) && !(options.data instanceof FormData)) {
391
602
  options.data = utils.objectToFormData(options.data);
392
603
  utils.debug.router("Converted data to FormData.", options.data);
604
+ if (options.method && ["PUT", "PATCH", "DELETE"].includes(options.method) && options.spoof !== false) {
605
+ utils.debug.router(`Automatically spoofing method ${options.method}.`);
606
+ options.data.append("_method", options.method);
607
+ options.method = "POST";
608
+ }
609
+ }
610
+ if (!(options.data instanceof FormData) && options.method === "GET" && Object.keys(options.data ?? {}).length) {
611
+ utils.debug.router("Transforming data to query parameters.", options.data);
612
+ options.url = makeUrl(options.url ?? context.url, {
613
+ query: options.data
614
+ });
615
+ options.data = {};
393
616
  }
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.');
617
+ if (!await runHooks("before", options.hooks, options, context)) {
618
+ utils.debug.router('"before" event returned false, aborting the navigation.');
619
+ throw new NavigationCancelledError('The navigation was cancelled by the "before" event.');
397
620
  }
398
- if (context.activeVisit) {
399
- utils.debug.router("Aborting current visit.", context.activeVisit);
400
- context.activeVisit?.controller.abort();
621
+ if (context.pendingNavigation) {
622
+ utils.debug.router("Aborting current navigation.", context.pendingNavigation);
623
+ context.pendingNavigation?.controller?.abort();
401
624
  }
402
625
  saveScrollPositions();
403
626
  if (options.url && options.transformUrl) {
404
627
  options.url = makeUrl(options.url, options.transformUrl);
405
628
  }
629
+ const targetUrl = makeUrl(options.url ?? context.url);
630
+ const abortController = new AbortController();
406
631
  setContext({
407
- activeVisit: {
408
- id: visitId,
409
- url: makeUrl(options.url ?? context.url),
410
- controller: new AbortController(),
632
+ pendingNavigation: {
633
+ id: navigationId,
634
+ url: targetUrl,
635
+ controller: abortController,
636
+ status: "pending",
411
637
  options
412
638
  }
413
639
  });
414
640
  await runHooks("start", options.hooks, context);
415
641
  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",
642
+ const response = await context.axios.request({
643
+ url: targetUrl.toString(),
644
+ method: options.method,
419
645
  data: options.method === "GET" ? {} : options.data,
420
646
  params: options.method === "GET" ? options.data : {},
421
- signal: context.activeVisit.controller.signal,
647
+ signal: abortController.signal,
422
648
  headers: {
423
649
  ...options.headers,
650
+ ...context.dialog ? { [DIALOG_KEY_HEADER]: context.dialog.key } : {},
651
+ ...context.dialog ? { [DIALOG_REDIRECT_HEADER]: context.dialog.redirectUrl ?? "" } : {},
424
652
  ...utils.when(options.only !== void 0 || options.except !== void 0, {
425
- [PARTIAL_COMPONENT_HEADER]: context.view.name,
653
+ [PARTIAL_COMPONENT_HEADER]: context.view.component,
426
654
  ...utils.when(options.only, { [ONLY_DATA_HEADER]: JSON.stringify(options.only) }, {}),
427
655
  ...utils.when(options.except, { [EXCEPT_DATA_HEADER]: JSON.stringify(options.except) }, {})
428
656
  }, {}),
@@ -437,24 +665,24 @@ async function visit(options) {
437
665
  await runHooks("progress", options.hooks, {
438
666
  event,
439
667
  percentage: Math.round(event.loaded / (event.total ?? 0) * 100)
440
- });
668
+ }, context);
441
669
  }
442
670
  });
443
- await runHooks("data", options.hooks, response);
671
+ await runHooks("data", options.hooks, response, context);
444
672
  if (isExternalResponse(response)) {
445
673
  utils.debug.router("The response is explicitely external.");
446
- await performExternalVisit({
447
- url: fillHash(context.activeVisit.url, response.headers[EXTERNAL_VISIT_HEADER]),
674
+ await performExternalNavigation({
675
+ url: fillHash(targetUrl, response.headers[EXTERNAL_NAVIGATION_HEADER]),
448
676
  preserveScroll: options.preserveScroll === true
449
677
  });
450
678
  return { response };
451
679
  }
452
- if (!isHybridlyResponse(response)) {
453
- throw new NotAHybridlyResponseError(response);
680
+ if (!isHybridResponse(response)) {
681
+ throw new NotAHybridResponseError(response);
454
682
  }
455
- utils.debug.router("The response respects the hybridly protocol.");
683
+ utils.debug.router("The response respects the Hybridly protocol.");
456
684
  const payload = response.data;
457
- if ((options.only?.length ?? options.except?.length) && payload.view.name === context.view.name) {
685
+ if ((options.only?.length ?? options.except?.length) && payload.view.component === context.view.component) {
458
686
  utils.debug.router(`Merging ${options.only ? '"only"' : '"except"'} properties.`, payload.view.properties);
459
687
  payload.view.properties = utils.merge(context.view.properties, payload.view.properties);
460
688
  utils.debug.router("Merged properties:", payload.view.properties);
@@ -462,7 +690,7 @@ async function visit(options) {
462
690
  await navigate({
463
691
  payload: {
464
692
  ...payload,
465
- url: fillHash(context.activeVisit.url, payload.url)
693
+ url: fillHash(targetUrl, payload.url)
466
694
  },
467
695
  preserveScroll: options.preserveScroll === true,
468
696
  preserveState: options.preserveState,
@@ -477,45 +705,50 @@ async function visit(options) {
477
705
  return context.view.properties.errors;
478
706
  })();
479
707
  utils.debug.router("The request returned validation errors.", errors);
480
- await runHooks("error", options.hooks, errors);
481
708
  setContext({
482
- activeVisit: {
483
- ...context.activeVisit,
709
+ pendingNavigation: {
710
+ ...context.pendingNavigation,
484
711
  status: "error"
485
712
  }
486
713
  });
714
+ await runHooks("error", options.hooks, errors, context);
487
715
  } else {
488
- await runHooks("success", options.hooks, payload);
489
716
  setContext({
490
- activeVisit: {
491
- ...context.activeVisit,
717
+ pendingNavigation: {
718
+ ...context.pendingNavigation,
492
719
  status: "success"
493
720
  }
494
721
  });
722
+ await runHooks("success", options.hooks, payload, context);
495
723
  }
496
724
  return { response };
497
725
  } catch (error) {
498
726
  await utils.match(error.constructor.name, {
499
- VisitCancelledError: async () => {
727
+ NavigationCancelledError: async () => {
500
728
  utils.debug.router('The request was cancelled through the "before" hook.', error);
501
- console.warn(error);
502
729
  await runHooks("abort", options.hooks, context);
503
730
  },
504
731
  AbortError: async () => {
505
- utils.debug.router("The request was cancelled.", error);
506
- console.warn(error);
732
+ utils.debug.router("The request was aborted.", error);
507
733
  await runHooks("abort", options.hooks, context);
508
734
  },
509
- NotAHybridlyResponseError: async () => {
735
+ NotAHybridResponseError: async () => {
510
736
  utils.debug.router("The request was not hybridly.");
511
737
  console.error(error);
512
- await runHooks("invalid", options.hooks, error);
513
- utils.showResponseErrorModal(error.response.data);
738
+ await runHooks("invalid", options.hooks, error, context);
739
+ if (context.responseErrorModals) {
740
+ utils.showResponseErrorModal(error.response.data);
741
+ }
514
742
  },
515
743
  default: async () => {
516
- utils.debug.router("An unknown error occured.", error);
517
- console.error(error);
518
- await runHooks("exception", options.hooks, error);
744
+ if (error?.name === "CanceledError") {
745
+ utils.debug.router("The request was cancelled.", error);
746
+ await runHooks("abort", options.hooks, context);
747
+ } else {
748
+ utils.debug.router("An unknown error occured.", error);
749
+ console.error(error);
750
+ await runHooks("exception", options.hooks, error, context);
751
+ }
519
752
  }
520
753
  });
521
754
  await runHooks("fail", options.hooks, context);
@@ -526,26 +759,28 @@ async function visit(options) {
526
759
  }
527
760
  };
528
761
  } finally {
529
- utils.debug.router("Ending visit.");
762
+ utils.debug.router("Ending navigation.");
530
763
  await runHooks("after", options.hooks, context);
531
- if (context.activeVisit?.id === visitId) {
532
- setContext({ activeVisit: void 0 });
764
+ if (context.pendingNavigation?.id === navigationId) {
765
+ setContext({ pendingNavigation: void 0 });
533
766
  }
534
767
  }
535
768
  }
536
- function isHybridlyResponse(response) {
769
+ function isHybridResponse(response) {
537
770
  return !!response?.headers[HYBRIDLY_HEADER];
538
771
  }
539
772
  async function navigate(options) {
540
773
  const context = getRouterContext();
541
774
  utils.debug.router("Making an internal navigation:", { context, options });
775
+ await runHooks("navigating", {}, options, context);
542
776
  options.payload ?? (options.payload = payloadFromContext());
543
777
  const evaluateConditionalOption = (option) => typeof option === "function" ? option(options.payload) : option;
544
778
  const shouldPreserveState = evaluateConditionalOption(options.preserveState);
545
779
  const shouldPreserveScroll = evaluateConditionalOption(options.preserveScroll);
546
780
  const shouldReplaceHistory = evaluateConditionalOption(options.replace);
547
781
  const shouldReplaceUrl = evaluateConditionalOption(options.preserveUrl);
548
- if (shouldPreserveState && getHistoryState() && options.payload.view.name === context.view.name) {
782
+ if (shouldPreserveState && getHistoryState() && options.payload.view.component === context.view.component) {
783
+ utils.debug.history("Setting the history from this entry into the context.");
549
784
  setContext({ state: getHistoryState() });
550
785
  }
551
786
  if (shouldReplaceUrl) {
@@ -555,41 +790,34 @@ async function navigate(options) {
555
790
  setContext({
556
791
  ...options.payload,
557
792
  state: {}
558
- }, { propagate: false });
793
+ });
559
794
  if (options.updateHistoryState !== false) {
560
795
  utils.debug.router(`Target URL is ${context.url}, current window URL is ${window.location.href}.`, { shouldReplaceHistory });
561
796
  setHistoryState({ replace: shouldReplaceHistory });
562
797
  }
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({
798
+ const viewComponent = await context.adapter.resolveComponent(context.view.component);
799
+ utils.debug.router(`Component [${context.view.component}] resolved to:`, viewComponent);
800
+ await context.adapter.onViewSwap({
566
801
  component: viewComponent,
802
+ dialog: context.dialog,
803
+ properties: options.payload?.view.properties,
567
804
  preserveState: shouldPreserveState
568
805
  });
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
806
  if (!shouldPreserveScroll) {
579
807
  resetScrollPositions();
580
808
  } else {
581
809
  restoreScrollPositions();
582
810
  }
583
- await runHooks("navigate", {}, options);
811
+ await runHooks("navigated", {}, options, context);
584
812
  }
585
813
  async function initializeRouter() {
586
814
  const context = getRouterContext();
587
- if (isBackForwardVisit()) {
588
- handleBackForwardVisit();
589
- } else if (isExternalVisit()) {
590
- handleExternalVisit();
815
+ if (isBackForwardNavigation()) {
816
+ handleBackForwardNavigation();
817
+ } else if (isExternalNavigation()) {
818
+ handleExternalNavigation();
591
819
  } else {
592
- utils.debug.router("Handling the initial page visit.");
820
+ utils.debug.router("Handling the initial page navigation.");
593
821
  setContext({
594
822
  url: makeUrl(context.url, { hash: window.location.hash }).toString()
595
823
  });
@@ -599,9 +827,10 @@ async function initializeRouter() {
599
827
  });
600
828
  }
601
829
  registerEventListeners();
830
+ await runHooks("ready", {}, context);
602
831
  return context;
603
832
  }
604
- async function performLocalComponentVisit(targetUrl, options) {
833
+ async function performLocalNavigation(targetUrl, options) {
605
834
  const context = getRouterContext();
606
835
  const url = normalizeUrl(targetUrl);
607
836
  return await navigate({
@@ -611,20 +840,12 @@ async function performLocalComponentVisit(targetUrl, options) {
611
840
  dialog: context.dialog,
612
841
  url,
613
842
  view: {
614
- name: options.component ?? context.view.name,
615
- properties: options.properties
843
+ component: options.component ?? context.view.component,
844
+ properties: options.properties ?? {}
616
845
  }
617
846
  }
618
847
  });
619
848
  }
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
849
 
629
850
  function can(resource, action) {
630
851
  return resource.authorization?.[action] ?? false;
@@ -633,10 +854,11 @@ function can(resource, action) {
633
854
  exports.can = can;
634
855
  exports.constants = constants;
635
856
  exports.createRouter = createRouter;
857
+ exports.current = current;
636
858
  exports.definePlugin = definePlugin;
637
859
  exports.getRouterContext = getRouterContext;
638
860
  exports.makeUrl = makeUrl;
639
861
  exports.registerHook = registerHook;
640
- exports.registerHookOnce = registerHookOnce;
862
+ exports.route = route;
641
863
  exports.router = router;
642
864
  exports.sameUrls = sameUrls;