swup 4.4.4 → 4.5.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.
Files changed (55) hide show
  1. package/dist/Swup.cjs +1 -1
  2. package/dist/Swup.cjs.map +1 -1
  3. package/dist/Swup.modern.js +1 -1
  4. package/dist/Swup.modern.js.map +1 -1
  5. package/dist/Swup.module.js +1 -1
  6. package/dist/Swup.module.js.map +1 -1
  7. package/dist/Swup.umd.js +1 -1
  8. package/dist/Swup.umd.js.map +1 -1
  9. package/dist/types/Swup.d.ts +8 -4
  10. package/dist/types/Swup.d.ts.map +1 -1
  11. package/dist/types/helpers/Location.d.ts.map +1 -1
  12. package/dist/types/helpers/history.d.ts +14 -0
  13. package/dist/types/helpers/history.d.ts.map +1 -0
  14. package/dist/types/helpers.d.ts +1 -2
  15. package/dist/types/helpers.d.ts.map +1 -1
  16. package/dist/types/modules/Classes.d.ts.map +1 -1
  17. package/dist/types/modules/Hooks.d.ts +16 -8
  18. package/dist/types/modules/Hooks.d.ts.map +1 -1
  19. package/dist/types/modules/Visit.d.ts +28 -22
  20. package/dist/types/modules/Visit.d.ts.map +1 -1
  21. package/dist/types/modules/animatePageIn.d.ts +2 -1
  22. package/dist/types/modules/animatePageIn.d.ts.map +1 -1
  23. package/dist/types/modules/animatePageOut.d.ts +2 -1
  24. package/dist/types/modules/animatePageOut.d.ts.map +1 -1
  25. package/dist/types/modules/fetchPage.d.ts.map +1 -1
  26. package/dist/types/modules/navigate.d.ts +2 -2
  27. package/dist/types/modules/navigate.d.ts.map +1 -1
  28. package/dist/types/modules/renderPage.d.ts +2 -1
  29. package/dist/types/modules/renderPage.d.ts.map +1 -1
  30. package/dist/types/modules/replaceContent.d.ts.map +1 -1
  31. package/dist/types/modules/scrollToContent.d.ts +2 -1
  32. package/dist/types/modules/scrollToContent.d.ts.map +1 -1
  33. package/package.json +6 -5
  34. package/src/Swup.ts +43 -40
  35. package/src/helpers/Location.ts +1 -0
  36. package/src/helpers/getCurrentUrl.ts +1 -1
  37. package/src/helpers/history.ts +37 -0
  38. package/src/helpers.ts +1 -2
  39. package/src/modules/Cache.ts +2 -2
  40. package/src/modules/Classes.ts +8 -1
  41. package/src/modules/Hooks.ts +98 -37
  42. package/src/modules/Visit.ts +86 -47
  43. package/src/modules/animatePageIn.ts +9 -8
  44. package/src/modules/animatePageOut.ts +7 -18
  45. package/src/modules/fetchPage.ts +9 -11
  46. package/src/modules/navigate.ts +83 -39
  47. package/src/modules/renderPage.ts +18 -18
  48. package/src/modules/replaceContent.ts +1 -0
  49. package/src/modules/scrollToContent.ts +6 -4
  50. package/dist/types/helpers/createHistoryRecord.d.ts +0 -10
  51. package/dist/types/helpers/createHistoryRecord.d.ts.map +0 -1
  52. package/dist/types/helpers/updateHistoryRecord.d.ts +0 -3
  53. package/dist/types/helpers/updateHistoryRecord.d.ts.map +0 -1
  54. package/src/helpers/createHistoryRecord.ts +0 -24
  55. package/src/helpers/updateHistoryRecord.ts +0 -19
package/src/Swup.ts CHANGED
@@ -21,7 +21,7 @@ import { renderPage } from './modules/renderPage.js';
21
21
  import { use, unuse, findPlugin, type Plugin } from './modules/plugins.js';
22
22
  import { isSameResolvedUrl, resolveUrl } from './modules/resolveUrl.js';
23
23
  import { nextTick } from './utils.js';
24
- import { type HistoryState } from './helpers/createHistoryRecord.js';
24
+ import { type HistoryState } from './helpers/history.js';
25
25
 
26
26
  /** Options for customizing swup's behavior. */
27
27
  export type Options = {
@@ -41,6 +41,8 @@ export type Options = {
41
41
  linkSelector: string;
42
42
  /** How swup handles links to the same page. Default: `scroll` */
43
43
  linkToSelf: NavigationToSelfAction;
44
+ /** Enable native animations using the View Transitions API. */
45
+ native: boolean;
44
46
  /** Plugins to register on startup. */
45
47
  plugins: Plugin[];
46
48
  /** Custom headers sent along with fetch requests. */
@@ -62,6 +64,7 @@ const defaults: Options = {
62
64
  ignoreVisit: (url, { el } = {}) => !!el?.closest('[data-no-swup]'),
63
65
  linkSelector: 'a[href]',
64
66
  linkToSelf: 'scroll',
67
+ native: false,
65
68
  plugins: [],
66
69
  resolveUrl: (url) => url,
67
70
  requestHeaders: {
@@ -98,6 +101,8 @@ export default class Swup {
98
101
  protected clickDelegate?: DelegateEventUnsubscribe;
99
102
  /** Navigation status */
100
103
  protected navigating: boolean = false;
104
+ /** Run anytime a visit ends */
105
+ protected onVisitEnd?: () => Promise<unknown>;
101
106
 
102
107
  /** Install a plugin */
103
108
  use = use;
@@ -149,7 +154,7 @@ export default class Swup {
149
154
  this.hooks = new Hooks(this);
150
155
  this.visit = this.createVisit({ to: '' });
151
156
 
152
- this.currentHistoryIndex = (history.state as HistoryState)?.index ?? 1;
157
+ this.currentHistoryIndex = (window.history.state as HistoryState)?.index ?? 1;
153
158
 
154
159
  if (!this.checkRequirements()) {
155
160
  return;
@@ -185,11 +190,14 @@ export default class Swup {
185
190
  // https://github.com/swup/swup/issues/475
186
191
  }
187
192
 
193
+ // Sanitize/check native option
194
+ this.options.native = this.options.native && !!document.startViewTransition;
195
+
188
196
  // Mount plugins
189
197
  this.options.plugins.forEach((plugin) => this.use(plugin));
190
198
 
191
199
  // Create initial history record
192
- if ((history.state as HistoryState)?.source !== 'swup') {
200
+ if ((window.history.state as HistoryState)?.source !== 'swup') {
193
201
  updateHistoryRecord(null, { index: this.currentHistoryIndex });
194
202
  }
195
203
 
@@ -197,9 +205,10 @@ export default class Swup {
197
205
  await nextTick();
198
206
 
199
207
  // Trigger enable hook
200
- await this.hooks.call('enable', undefined, () => {
201
- // Add swup-enabled class to html tag
202
- document.documentElement.classList.add('swup-enabled');
208
+ await this.hooks.call('enable', undefined, undefined, () => {
209
+ const html = document.documentElement;
210
+ html.classList.add('swup-enabled');
211
+ html.classList.toggle('swup-native', this.options.native);
203
212
  });
204
213
  }
205
214
 
@@ -218,9 +227,10 @@ export default class Swup {
218
227
  this.options.plugins.forEach((plugin) => this.unuse(plugin));
219
228
 
220
229
  // trigger disable hook
221
- await this.hooks.call('disable', undefined, () => {
222
- // remove swup-enabled class from html tag
223
- document.documentElement.classList.remove('swup-enabled');
230
+ await this.hooks.call('disable', undefined, undefined, () => {
231
+ const html = document.documentElement;
232
+ html.classList.remove('swup-enabled');
233
+ html.classList.remove('swup-native');
224
234
  });
225
235
 
226
236
  // remove handlers
@@ -265,11 +275,11 @@ export default class Swup {
265
275
  return;
266
276
  }
267
277
 
268
- this.visit = this.createVisit({ to: url, hash, el, event });
278
+ const visit = this.createVisit({ to: url, hash, el, event });
269
279
 
270
280
  // Exit early if control key pressed
271
281
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
272
- this.hooks.call('link:newtab', { href });
282
+ this.hooks.callSync('link:newtab', visit, { href });
273
283
  return;
274
284
  }
275
285
 
@@ -278,8 +288,8 @@ export default class Swup {
278
288
  return;
279
289
  }
280
290
 
281
- this.hooks.callSync('link:click', { el, event }, () => {
282
- const from = this.visit.from.url ?? '';
291
+ this.hooks.callSync('link:click', visit, { el, event }, () => {
292
+ const from = visit.from.url ?? '';
283
293
 
284
294
  event.preventDefault();
285
295
 
@@ -287,20 +297,18 @@ export default class Swup {
287
297
  if (!url || url === from) {
288
298
  if (hash) {
289
299
  // With hash: scroll to anchor
290
- this.hooks.callSync('link:anchor', { hash }, () => {
300
+ this.hooks.callSync('link:anchor', visit, { hash }, () => {
291
301
  updateHistoryRecord(url + hash);
292
- this.scrollToContent();
302
+ this.scrollToContent(visit);
293
303
  });
294
304
  } else {
295
305
  // Without hash: scroll to top or load/reload page
296
- this.hooks.callSync('link:self', undefined, () => {
297
- switch (this.options.linkToSelf) {
298
- case 'navigate':
299
- return this.performNavigation();
300
- case 'scroll':
301
- default:
302
- updateHistoryRecord(url);
303
- return this.scrollToContent();
306
+ this.hooks.callSync('link:self', visit, undefined, () => {
307
+ if (this.options.linkToSelf === 'navigate') {
308
+ this.performNavigation(visit);
309
+ } else {
310
+ updateHistoryRecord(url);
311
+ this.scrollToContent(visit);
304
312
  }
305
313
  });
306
314
  }
@@ -313,12 +321,12 @@ export default class Swup {
313
321
  }
314
322
 
315
323
  // Finally, proceed with loading the page
316
- this.performNavigation();
324
+ this.performNavigation(visit);
317
325
  });
318
326
  }
319
327
 
320
328
  protected handlePopState(event: PopStateEvent) {
321
- const href: string = (event.state as HistoryState)?.url ?? location.href;
329
+ const href: string = (event.state as HistoryState)?.url ?? window.location.href;
322
330
 
323
331
  // Exit early if this event should be ignored
324
332
  if (this.options.skipPopStateHandling(event)) {
@@ -332,37 +340,32 @@ export default class Swup {
332
340
 
333
341
  const { url, hash } = Location.fromUrl(href);
334
342
 
335
- this.visit = this.createVisit({ to: url, hash, event });
343
+ const visit = this.createVisit({ to: url, hash, event });
336
344
 
337
345
  // Mark as history visit
338
- this.visit.history.popstate = true;
346
+ visit.history.popstate = true;
339
347
 
340
348
  // Determine direction of history visit
341
349
  const index = (event.state as HistoryState)?.index ?? 0;
342
350
  if (index && index !== this.currentHistoryIndex) {
343
351
  const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
344
- this.visit.history.direction = direction;
352
+ visit.history.direction = direction;
345
353
  this.currentHistoryIndex = index;
346
354
  }
347
355
 
348
356
  // Disable animation & scrolling for history visits
349
- this.visit.animation.animate = false;
350
- this.visit.scroll.reset = false;
351
- this.visit.scroll.target = false;
357
+ visit.animation.animate = false;
358
+ visit.scroll.reset = false;
359
+ visit.scroll.target = false;
352
360
 
353
361
  // Animated history visit: re-enable animation & scroll reset
354
362
  if (this.options.animateHistoryBrowsing) {
355
- this.visit.animation.animate = true;
356
- this.visit.scroll.reset = true;
363
+ visit.animation.animate = true;
364
+ visit.scroll.reset = true;
357
365
  }
358
366
 
359
- // Does this even do anything?
360
- // if (!hash) {
361
- // event.preventDefault();
362
- // }
363
-
364
- this.hooks.callSync('history:popstate', { event }, () => {
365
- this.performNavigation();
367
+ this.hooks.callSync('history:popstate', visit, { event }, () => {
368
+ this.performNavigation(visit);
366
369
  });
367
370
  }
368
371
 
@@ -6,6 +6,7 @@
6
6
  export class Location extends URL {
7
7
  constructor(url: URL | string, base: string = document.baseURI) {
8
8
  super(url.toString(), base);
9
+ // Fix Safari bug with extending native classes
9
10
  Object.setPrototypeOf(this, Location.prototype);
10
11
  }
11
12
 
@@ -1,4 +1,4 @@
1
1
  /** Get the current page URL: path name + query params. Optionally including hash. */
2
2
  export const getCurrentUrl = ({ hash }: { hash?: boolean } = {}): string => {
3
- return location.pathname + location.search + (hash ? location.hash : '');
3
+ return window.location.pathname + window.location.search + (hash ? window.location.hash : '');
4
4
  };
@@ -0,0 +1,37 @@
1
+ import { getCurrentUrl } from './getCurrentUrl.js';
2
+
3
+ export interface HistoryState {
4
+ url: string;
5
+ source: 'swup';
6
+ random: number;
7
+ index?: number;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ type HistoryData = Record<string, unknown>;
12
+
13
+ /** Create a new history record with a custom swup identifier. */
14
+ export const createHistoryRecord = (url: string, data: HistoryData = {}): void => {
15
+ url = url || getCurrentUrl({ hash: true });
16
+ const state: HistoryState = {
17
+ url,
18
+ random: Math.random(),
19
+ source: 'swup',
20
+ ...data
21
+ };
22
+ window.history.pushState(state, '', url);
23
+ };
24
+
25
+ /** Update the current history record with a custom swup identifier. */
26
+ export const updateHistoryRecord = (url: string | null = null, data: HistoryData = {}): void => {
27
+ url = url || getCurrentUrl({ hash: true });
28
+ const currentState = (window.history.state as HistoryState) || {};
29
+ const state: HistoryState = {
30
+ ...currentState,
31
+ url,
32
+ random: Math.random(),
33
+ source: 'swup',
34
+ ...data
35
+ };
36
+ window.history.replaceState(state, '', url);
37
+ };
package/src/helpers.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  // e.g. import { updateHistoryRecord } from 'swup'
3
3
 
4
4
  export { classify } from './helpers/classify.js';
5
- export { createHistoryRecord } from './helpers/createHistoryRecord.js';
6
- export { updateHistoryRecord } from './helpers/updateHistoryRecord.js';
5
+ export { createHistoryRecord, updateHistoryRecord } from './helpers/history.js';
7
6
  export { delegateEvent } from './helpers/delegateEvent.js';
8
7
  export { getCurrentUrl } from './helpers/getCurrentUrl.js';
9
8
  export { Location } from './helpers/Location.js';
@@ -49,7 +49,7 @@ export class Cache {
49
49
  url = this.resolve(url);
50
50
  page = { ...page, url };
51
51
  this.pages.set(url, page);
52
- this.swup.hooks.callSync('cache:set', { page });
52
+ this.swup.hooks.callSync('cache:set', undefined, { page });
53
53
  }
54
54
 
55
55
  /** Update a cache record, overwriting or adding custom data. */
@@ -67,7 +67,7 @@ export class Cache {
67
67
  /** Empty the cache. */
68
68
  clear(): void {
69
69
  this.pages.clear();
70
- this.swup.hooks.callSync('cache:clear', undefined);
70
+ this.swup.hooks.callSync('cache:clear', undefined, undefined);
71
71
  }
72
72
 
73
73
  /** Remove all cache entries that return true for a given predicate function. */
@@ -3,7 +3,14 @@ import { queryAll } from '../utils.js';
3
3
 
4
4
  export class Classes {
5
5
  protected swup: Swup;
6
- protected swupClasses = ['to-', 'is-changing', 'is-rendering', 'is-popstate', 'is-animating'];
6
+ protected swupClasses = [
7
+ 'to-',
8
+ 'is-changing',
9
+ 'is-rendering',
10
+ 'is-popstate',
11
+ 'is-animating',
12
+ 'is-leaving'
13
+ ];
7
14
 
8
15
  constructor(swup: Swup) {
9
16
  this.swup = swup;
@@ -2,7 +2,7 @@ import type { DelegateEvent } from 'delegate-it';
2
2
 
3
3
  import type Swup from '../Swup.js';
4
4
  import { isPromise, runAsPromise } from '../utils.js';
5
- import type { Visit } from './Visit.js';
5
+ import { Visit } from './Visit.js';
6
6
  import type { FetchOptions, PageData } from './fetchPage.js';
7
7
 
8
8
  export interface HookDefinitions {
@@ -33,16 +33,16 @@ export interface HookDefinitions {
33
33
  'scroll:anchor': { hash: string; options: ScrollIntoViewOptions };
34
34
  'visit:start': undefined;
35
35
  'visit:transition': undefined;
36
+ 'visit:abort': undefined;
36
37
  'visit:end': undefined;
37
38
  }
38
39
 
39
40
  export interface HookReturnValues {
40
- 'content:scroll': Promise<boolean>;
41
+ 'content:scroll': Promise<boolean> | boolean;
41
42
  'fetch:request': Promise<Response>;
42
43
  'page:load': Promise<PageData>;
43
44
  'scroll:top': boolean;
44
45
  'scroll:anchor': boolean;
45
- 'visit:transition': Promise<boolean>;
46
46
  }
47
47
 
48
48
  export type HookArguments<T extends HookName> = HookDefinitions[T];
@@ -154,6 +154,7 @@ export class Hooks {
154
154
  'scroll:anchor',
155
155
  'visit:start',
156
156
  'visit:transition',
157
+ 'visit:abort',
157
158
  'visit:end'
158
159
  ];
159
160
 
@@ -333,20 +334,29 @@ export class Hooks {
333
334
  * Trigger a hook asynchronously, executing its default handler and all registered handlers.
334
335
  * Will execute all handlers in order and `await` any `Promise`s they return.
335
336
  * @param hook Name of the hook to trigger
337
+ * @param visit The visit object this hook belongs to
336
338
  * @param args Arguments to pass to the handler
337
339
  * @param defaultHandler A default implementation of this hook to execute
338
340
  * @returns The resolved return value of the executed default handler
339
341
  */
342
+ // Overload: default order of arguments
343
+ async call<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore
344
+ // Overload: legacy order of arguments, with visit missing
345
+ async call<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore
346
+ // Implementation
340
347
  async call<T extends HookName>(
341
348
  hook: T,
342
- args: HookArguments<T>,
343
- defaultHandler?: HookDefaultHandler<T>
349
+ arg1: Visit | HookArguments<T>,
350
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
351
+ arg3?: HookDefaultHandler<T>
344
352
  ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>> {
353
+ const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);
354
+
345
355
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
346
- await this.run(before, args);
347
- const [result] = await this.run(handler, args);
348
- await this.run(after, args);
349
- this.dispatchDomEvent(hook, args);
356
+ await this.run(before, visit, args);
357
+ const [result] = await this.run(handler, visit, args, true);
358
+ await this.run(after, visit, args);
359
+ this.dispatchDomEvent(hook, visit, args);
350
360
  return result;
351
361
  }
352
362
 
@@ -354,23 +364,51 @@ export class Hooks {
354
364
  * Trigger a hook synchronously, executing its default handler and all registered handlers.
355
365
  * Will execute all handlers in order, but will **not** `await` any `Promise`s they return.
356
366
  * @param hook Name of the hook to trigger
367
+ * @param visit The visit object this hook belongs to
357
368
  * @param args Arguments to pass to the handler
358
369
  * @param defaultHandler A default implementation of this hook to execute
359
370
  * @returns The (possibly unresolved) return value of the executed default handler
360
371
  */
372
+ // Overload: default order of arguments
373
+ callSync<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore
374
+ // Overload: legacy order of arguments, with visit missing
375
+ callSync<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore
376
+ // Implementation
361
377
  callSync<T extends HookName>(
362
378
  hook: T,
363
- args: HookArguments<T>,
364
- defaultHandler?: HookDefaultHandler<T>
379
+ arg1: Visit | HookArguments<T>,
380
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
381
+ arg3?: HookDefaultHandler<T>
365
382
  ): ReturnType<HookDefaultHandler<T>> {
383
+ const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);
366
384
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
367
- this.runSync(before, args);
368
- const [result] = this.runSync(handler, args);
369
- this.runSync(after, args);
370
- this.dispatchDomEvent(hook, args);
385
+ this.runSync(before, visit, args);
386
+ const [result] = this.runSync(handler, visit, args, true);
387
+ this.runSync(after, visit, args);
388
+ this.dispatchDomEvent(hook, visit, args);
371
389
  return result;
372
390
  }
373
391
 
392
+ /**
393
+ * Parse the call arguments for call() and callSync() to allow legacy argument order.
394
+ */
395
+ protected parseCallArgs<T extends HookName>(
396
+ hook: T,
397
+ arg1: Visit | HookArguments<T> | undefined,
398
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
399
+ arg3?: HookDefaultHandler<T>
400
+ ): [Visit | undefined, HookArguments<T>, HookDefaultHandler<T> | undefined] {
401
+ const isLegacyOrder =
402
+ !(arg1 instanceof Visit) && (typeof arg1 === 'object' || typeof arg2 === 'function');
403
+ if (isLegacyOrder) {
404
+ // Legacy positioning: arguments in second or handler passed in third place
405
+ return [undefined, arg1 as HookArguments<T>, arg2 as HookDefaultHandler<T>];
406
+ } else {
407
+ // Default positioning: visit passed in as first argument
408
+ return [arg1, arg2 as HookArguments<T>, arg3];
409
+ }
410
+ }
411
+
374
412
  /**
375
413
  * Execute the handlers for a hook, in order, as `Promise`s that will be `await`ed.
376
414
  * @param registrations The registrations (handler + options) to execute
@@ -378,20 +416,29 @@ export class Hooks {
378
416
  */
379
417
 
380
418
  // Overload: running HookDefaultHandler: expect HookDefaultHandler return type
381
- protected async run<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], args: HookArguments<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>[]>; // prettier-ignore
419
+ protected async run<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>[]>; // prettier-ignore
382
420
  // Overload: running user handler: expect no specific type
383
- protected async run<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore
421
+ protected async run<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore
384
422
  // Implementation
385
423
  protected async run<T extends HookName, R extends HookRegistration<T>[]>(
386
424
  registrations: R,
387
- args: HookArguments<T>
425
+ visit: Visit | undefined = this.swup.visit,
426
+ args: HookArguments<T>,
427
+ rethrow: boolean = false
388
428
  ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>> | unknown[]> {
389
429
  const results = [];
390
430
  for (const { hook, handler, defaultHandler, once } of registrations) {
391
- const result = await runAsPromise(handler, [this.swup.visit, args, defaultHandler]);
392
- results.push(result);
393
- if (once) {
394
- this.off(hook, handler);
431
+ if (visit?.done) continue;
432
+ if (once) this.off(hook, handler);
433
+ try {
434
+ const result = await runAsPromise(handler, [visit, args, defaultHandler]);
435
+ results.push(result);
436
+ } catch (error) {
437
+ if (rethrow) {
438
+ throw error;
439
+ } else {
440
+ console.error(`Error in hook '${hook}':`, error);
441
+ }
395
442
  }
396
443
  }
397
444
  return results;
@@ -404,26 +451,34 @@ export class Hooks {
404
451
  */
405
452
 
406
453
  // Overload: running HookDefaultHandler: expect HookDefaultHandler return type
407
- protected runSync<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], args: HookArguments<T> ): ReturnType<HookDefaultHandler<T>>[]; // prettier-ignore
454
+ protected runSync<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): ReturnType<HookDefaultHandler<T>>[]; // prettier-ignore
408
455
  // Overload: running user handler: expect no specific type
409
- protected runSync<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): unknown[]; // prettier-ignore
456
+ protected runSync<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): unknown[]; // prettier-ignore
410
457
  // Implementation
411
458
  protected runSync<T extends HookName, R extends HookRegistration<T>[]>(
412
459
  registrations: R,
413
- args: HookArguments<T>
460
+ visit: Visit | undefined = this.swup.visit,
461
+ args: HookArguments<T>,
462
+ rethrow: boolean = false
414
463
  ): (ReturnType<HookDefaultHandler<T>> | unknown)[] {
415
464
  const results = [];
416
465
  for (const { hook, handler, defaultHandler, once } of registrations) {
417
- const result = (handler as HookDefaultHandler<T>)(this.swup.visit, args, defaultHandler); // prettier-ignore
418
- results.push(result);
419
- if (isPromise(result)) {
420
- console.warn(
421
- `Promise returned from handler for synchronous hook '${hook}'.` +
422
- `Swup will not wait for it to resolve.`
423
- );
424
- }
425
- if (once) {
426
- this.off(hook, handler);
466
+ if (visit?.done) continue;
467
+ if (once) this.off(hook, handler);
468
+ try {
469
+ const result = (handler as HookDefaultHandler<T>)(visit, args, defaultHandler);
470
+ results.push(result);
471
+ if (isPromise(result)) {
472
+ console.warn(
473
+ `Swup will not await Promises in handler for synchronous hook '${hook}'.`
474
+ );
475
+ }
476
+ } catch (error) {
477
+ if (rethrow) {
478
+ throw error;
479
+ } else {
480
+ console.error(`Error in hook '${hook}':`, error);
481
+ }
427
482
  }
428
483
  }
429
484
  return results;
@@ -500,8 +555,14 @@ export class Hooks {
500
555
  * Dispatch a custom event on the `document` for a hook. Prefixed with `swup:`
501
556
  * @param hook Name of the hook.
502
557
  */
503
- protected dispatchDomEvent<T extends HookName>(hook: T, args?: HookArguments<T>): void {
504
- const detail: HookEventDetail = { hook, args, visit: this.swup.visit };
558
+ protected dispatchDomEvent<T extends HookName>(
559
+ hook: T,
560
+ visit: Visit | undefined,
561
+ args?: HookArguments<T>
562
+ ): void {
563
+ if (visit?.done) return;
564
+
565
+ const detail: HookEventDetail = { hook, args, visit: visit || this.swup.visit };
505
566
  document.dispatchEvent(
506
567
  new CustomEvent<HookEventDetail>(`swup:any`, { detail, bubbles: true })
507
568
  );