swup 4.1.0 → 4.3.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.
Files changed (67) 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 +119 -117
  10. package/dist/types/__test__/index.test.d.ts +1 -1
  11. package/dist/types/config/version.d.ts +5 -5
  12. package/dist/types/helpers/Location.d.ts +24 -24
  13. package/dist/types/helpers/__test__/matchPath.test.d.ts +1 -1
  14. package/dist/types/helpers/classify.d.ts +2 -2
  15. package/dist/types/helpers/createHistoryRecord.d.ts +9 -2
  16. package/dist/types/helpers/delegateEvent.d.ts +7 -7
  17. package/dist/types/helpers/getCurrentUrl.d.ts +4 -4
  18. package/dist/types/helpers/matchPath.d.ts +4 -4
  19. package/dist/types/helpers/updateHistoryRecord.d.ts +2 -2
  20. package/dist/types/helpers.d.ts +7 -7
  21. package/dist/types/index.d.ts +14 -14
  22. package/dist/types/modules/Cache.d.ts +34 -34
  23. package/dist/types/modules/Classes.d.ts +13 -13
  24. package/dist/types/modules/Hooks.d.ts +268 -250
  25. package/dist/types/modules/Visit.d.ts +85 -75
  26. package/dist/types/modules/__test__/cache.test.d.ts +1 -1
  27. package/dist/types/modules/__test__/{delegateEvent.d.ts → delegateEvent.test.d.ts} +1 -1
  28. package/dist/types/modules/__test__/hooks.test.d.ts +1 -1
  29. package/dist/types/modules/__test__/plugins.test.d.ts +1 -1
  30. package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -1
  31. package/dist/types/modules/animatePageIn.d.ts +6 -6
  32. package/dist/types/modules/animatePageOut.d.ts +6 -6
  33. package/dist/types/modules/awaitAnimations.d.ts +19 -19
  34. package/dist/types/modules/fetchPage.d.ts +27 -29
  35. package/dist/types/modules/getAnchorElement.d.ts +9 -9
  36. package/dist/types/modules/navigate.d.ts +41 -34
  37. package/dist/types/modules/plugins.d.ts +26 -26
  38. package/dist/types/modules/renderPage.d.ts +7 -7
  39. package/dist/types/modules/replaceContent.d.ts +13 -13
  40. package/dist/types/modules/resolveUrl.d.ts +14 -14
  41. package/dist/types/modules/scrollToContent.d.ts +6 -6
  42. package/dist/types/utils/index.d.ts +20 -20
  43. package/dist/types/utils.d.ts +1 -1
  44. package/package.json +9 -3
  45. package/src/Swup.ts +24 -17
  46. package/src/config/version.ts +1 -2
  47. package/src/helpers/createHistoryRecord.ts +9 -1
  48. package/src/helpers/matchPath.ts +1 -1
  49. package/src/helpers/updateHistoryRecord.ts +4 -2
  50. package/src/modules/Cache.ts +2 -2
  51. package/src/modules/Classes.ts +1 -1
  52. package/src/modules/Hooks.ts +91 -39
  53. package/src/modules/Visit.ts +20 -5
  54. package/src/modules/__test__/cache.test.ts +3 -3
  55. package/src/modules/__test__/hooks.test.ts +12 -13
  56. package/src/modules/animatePageIn.ts +1 -1
  57. package/src/modules/animatePageOut.ts +2 -2
  58. package/src/modules/awaitAnimations.ts +1 -1
  59. package/src/modules/fetchPage.ts +7 -5
  60. package/src/modules/getAnchorElement.ts +1 -1
  61. package/src/modules/navigate.ts +37 -15
  62. package/src/modules/plugins.ts +3 -3
  63. package/src/modules/renderPage.ts +1 -5
  64. package/src/modules/replaceContent.ts +13 -0
  65. package/src/modules/scrollToContent.ts +5 -3
  66. package/src/utils/index.ts +5 -4
  67. /package/src/modules/__test__/{delegateEvent.ts → delegateEvent.test.ts} +0 -0
package/src/Swup.ts CHANGED
@@ -11,7 +11,7 @@ import { Visit, createVisit } from './modules/Visit.js';
11
11
  import { Hooks } from './modules/Hooks.js';
12
12
  import { getAnchorElement } from './modules/getAnchorElement.js';
13
13
  import { awaitAnimations } from './modules/awaitAnimations.js';
14
- import { navigate, performNavigation } from './modules/navigate.js';
14
+ import { navigate, performNavigation, NavigationToSelfAction } from './modules/navigate.js';
15
15
  import { fetchPage } from './modules/fetchPage.js';
16
16
  import { animatePageOut } from './modules/animatePageOut.js';
17
17
  import { replaceContent } from './modules/replaceContent.js';
@@ -21,6 +21,7 @@ import { renderPage } from './modules/renderPage.js';
21
21
  import { use, unuse, findPlugin, Plugin } from './modules/plugins.js';
22
22
  import { isSameResolvedUrl, resolveUrl } from './modules/resolveUrl.js';
23
23
  import { nextTick } from './utils.js';
24
+ import { HistoryState } from './helpers/createHistoryRecord.js';
24
25
 
25
26
  /** Options for customizing swup's behavior. */
26
27
  export type Options = {
@@ -38,6 +39,8 @@ export type Options = {
38
39
  ignoreVisit: (url: string, { el, event }: { el?: Element; event?: Event }) => boolean;
39
40
  /** Selector for links that trigger visits. Default: `'a[href]'` */
40
41
  linkSelector: string;
42
+ /** How swup handles links to the same page. Default: `scroll` */
43
+ linkToSelf: NavigationToSelfAction;
41
44
  /** Plugins to register on startup. */
42
45
  plugins: Plugin[];
43
46
  /** Custom headers sent along with fetch requests. */
@@ -45,7 +48,7 @@ export type Options = {
45
48
  /** Rewrite URLs before loading them. */
46
49
  resolveUrl: (url: string) => string;
47
50
  /** Callback for telling swup to ignore certain popstate events. */
48
- skipPopStateHandling: (event: any) => boolean;
51
+ skipPopStateHandling: (event: PopStateEvent) => boolean;
49
52
  };
50
53
 
51
54
  const defaults: Options = {
@@ -54,15 +57,16 @@ const defaults: Options = {
54
57
  animationScope: 'html',
55
58
  cache: true,
56
59
  containers: ['#swup'],
57
- ignoreVisit: (url, { el, event } = {}) => !!el?.closest('[data-no-swup]'),
60
+ ignoreVisit: (url, { el } = {}) => !!el?.closest('[data-no-swup]'),
58
61
  linkSelector: 'a[href]',
62
+ linkToSelf: 'scroll',
59
63
  plugins: [],
60
64
  resolveUrl: (url) => url,
61
65
  requestHeaders: {
62
66
  'X-Requested-With': 'swup',
63
67
  'Accept': 'text/html, application/xhtml+xml'
64
68
  },
65
- skipPopStateHandling: (event) => event.state?.source !== 'swup'
69
+ skipPopStateHandling: (event) => (event.state as HistoryState)?.source !== 'swup'
66
70
  };
67
71
 
68
72
  /** Swup page transition library. */
@@ -98,7 +102,7 @@ export default class Swup {
98
102
  findPlugin = findPlugin;
99
103
 
100
104
  /** Log a message. Has no effect unless debug plugin is installed */
101
- log: (message: string, context?: any) => void = () => {};
105
+ log: (message: string, context?: unknown) => void = () => {};
102
106
 
103
107
  /** Navigate to a new URL */
104
108
  navigate = navigate;
@@ -138,7 +142,7 @@ export default class Swup {
138
142
  this.cache = new Cache(this);
139
143
  this.classes = new Classes(this);
140
144
  this.hooks = new Hooks(this);
141
- this.visit = this.createVisit({ to: undefined });
145
+ this.visit = this.createVisit({ to: '' });
142
146
 
143
147
  if (!this.checkRequirements()) {
144
148
  return;
@@ -259,16 +263,24 @@ export default class Swup {
259
263
 
260
264
  event.preventDefault();
261
265
 
262
- // Handle links to the same page: with or without hash
266
+ // Handle links to the same page
263
267
  if (!url || url === from) {
264
268
  if (hash) {
269
+ // With hash: scroll to anchor
265
270
  this.hooks.callSync('link:anchor', { hash }, () => {
266
271
  updateHistoryRecord(url + hash);
267
272
  this.scrollToContent();
268
273
  });
269
274
  } else {
275
+ // Without hash: scroll to top or load/reload page
270
276
  this.hooks.callSync('link:self', undefined, () => {
271
- this.scrollToContent();
277
+ switch (this.options.linkToSelf) {
278
+ case 'navigate':
279
+ return this.performNavigation();
280
+ case 'scroll':
281
+ default:
282
+ return this.scrollToContent();
283
+ }
272
284
  });
273
285
  }
274
286
  return;
@@ -280,12 +292,12 @@ export default class Swup {
280
292
  }
281
293
 
282
294
  // Finally, proceed with loading the page
283
- this.performNavigation(url);
295
+ this.performNavigation();
284
296
  });
285
297
  }
286
298
 
287
299
  protected handlePopState(event: PopStateEvent) {
288
- const href = event.state?.url ?? location.href;
300
+ const href: string = (event.state as HistoryState)?.url ?? location.href;
289
301
 
290
302
  // Exit early if this event should be ignored
291
303
  if (this.options.skipPopStateHandling(event)) {
@@ -297,11 +309,6 @@ export default class Swup {
297
309
  return;
298
310
  }
299
311
 
300
- // Exit early if the link should be ignored
301
- if (this.shouldIgnoreVisit(href, { event })) {
302
- return;
303
- }
304
-
305
312
  const { url, hash } = Location.fromUrl(href);
306
313
  const animate = this.options.animateHistoryBrowsing;
307
314
  const resetScroll = this.options.animateHistoryBrowsing;
@@ -318,7 +325,7 @@ export default class Swup {
318
325
  this.visit.history.popstate = true;
319
326
 
320
327
  // Determine direction of history visit
321
- const index = Number(event.state?.index);
328
+ const index = Number((event.state as HistoryState)?.index);
322
329
  if (index) {
323
330
  const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
324
331
  this.visit.history.direction = direction;
@@ -330,7 +337,7 @@ export default class Swup {
330
337
  // }
331
338
 
332
339
  this.hooks.callSync('history:popstate', { event }, () => {
333
- this.performNavigation(url);
340
+ this.performNavigation();
334
341
  });
335
342
  }
336
343
 
@@ -6,8 +6,7 @@
6
6
  // export { version as default } from '../../package.json';
7
7
 
8
8
  // This will work in microbundle + webpack 5, but won't treeshake in webpack 4
9
- // Ignore next line in TypeScript as package.json is outside of rootDir
10
- // @ts-ignore
9
+ // @ts-ignore: package.json is outside of rootDir
11
10
  import pckg from '../../package.json';
12
11
 
13
12
  export default pckg.version;
@@ -1,12 +1,20 @@
1
1
  import { getCurrentUrl } from './getCurrentUrl.js';
2
2
 
3
+ export interface HistoryState {
4
+ url: string;
5
+ source: 'swup';
6
+ random: number;
7
+ index?: number;
8
+ [key: string]: unknown;
9
+ }
10
+
3
11
  /** Create a new history record with a custom swup identifier. */
4
12
  export const createHistoryRecord = (
5
13
  url: string,
6
14
  customData: Record<string, unknown> = {}
7
15
  ): void => {
8
16
  url = url || getCurrentUrl({ hash: true });
9
- const data = {
17
+ const data: HistoryState = {
10
18
  url,
11
19
  random: Math.random(),
12
20
  source: 'swup',
@@ -18,6 +18,6 @@ export const matchPath = <P extends object = object>(
18
18
  try {
19
19
  return match<P>(path, options);
20
20
  } catch (error) {
21
- throw new Error(`[swup] Error parsing path "${path}":\n${error}`);
21
+ throw new Error(`[swup] Error parsing path "${String(path)}":\n${String(error)}`);
22
22
  }
23
23
  };
@@ -1,3 +1,4 @@
1
+ import { HistoryState } from './createHistoryRecord.js';
1
2
  import { getCurrentUrl } from './getCurrentUrl.js';
2
3
 
3
4
  /** Update the current history record with a custom swup identifier. */
@@ -6,8 +7,9 @@ export const updateHistoryRecord = (
6
7
  customData: Record<string, unknown> = {}
7
8
  ): void => {
8
9
  url = url || getCurrentUrl({ hash: true });
9
- const data = {
10
- ...history.state,
10
+ const state = (history.state as HistoryState) || {};
11
+ const data: HistoryState = {
12
+ ...state,
11
13
  url,
12
14
  random: Math.random(),
13
15
  source: 'swup',
@@ -53,7 +53,7 @@ export class Cache {
53
53
  }
54
54
 
55
55
  /** Update a cache record, overwriting or adding custom data. */
56
- update(url: string, payload: Record<string, any>) {
56
+ update(url: string, payload: object) {
57
57
  url = this.resolve(url);
58
58
  const page = { ...this.get(url), ...payload, url } as CacheData;
59
59
  this.pages.set(url, page);
@@ -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');
70
+ this.swup.hooks.callSync('cache:clear', undefined);
71
71
  }
72
72
 
73
73
  /** Remove all cache entries that return true for a given predicate function. */
@@ -23,7 +23,7 @@ export class Classes {
23
23
 
24
24
  protected get targets(): HTMLElement[] {
25
25
  if (!this.selector.trim()) return [];
26
- return queryAll(this.selector) as HTMLElement[];
26
+ return queryAll(this.selector);
27
27
  }
28
28
 
29
29
  add(...classes: string[]): void {
@@ -34,19 +34,35 @@ export interface HookDefinitions {
34
34
  'visit:end': undefined;
35
35
  }
36
36
 
37
+ export interface HookReturnValues {
38
+ 'content:scroll': Promise<boolean>;
39
+ 'fetch:request': Promise<Response>;
40
+ 'page:load': Promise<PageData>;
41
+ 'scroll:top': boolean;
42
+ 'scroll:anchor': boolean;
43
+ }
44
+
37
45
  export type HookArguments<T extends HookName> = HookDefinitions[T];
38
46
 
39
47
  export type HookName = keyof HookDefinitions;
40
48
 
41
- /** A hook handler. */
49
+ /** A generic hook handler. */
42
50
  export type Handler<T extends HookName> = (
51
+ /** Context about the current visit. */
52
+ visit: Visit,
53
+ /** Local arguments passed into the handler. */
54
+ args: HookArguments<T>
55
+ ) => Promise<unknown> | unknown;
56
+
57
+ /** A default hook handler with an expected return type. */
58
+ export type DefaultHandler<T extends HookName> = (
43
59
  /** Context about the current visit. */
44
60
  visit: Visit,
45
61
  /** Local arguments passed into the handler. */
46
62
  args: HookArguments<T>,
47
63
  /** Default handler to be executed. Available if replacing an internal hook handler. */
48
- defaultHandler?: Handler<T>
49
- ) => Promise<any> | any;
64
+ defaultHandler?: DefaultHandler<T>
65
+ ) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise<unknown> | unknown;
50
66
 
51
67
  export type Handlers = {
52
68
  [K in HookName]: Handler<K>[];
@@ -67,11 +83,14 @@ export type HookOptions = {
67
83
  replace?: boolean;
68
84
  };
69
85
 
70
- export type HookRegistration<T extends HookName> = {
86
+ export type HookRegistration<
87
+ T extends HookName,
88
+ H extends Handler<T> | DefaultHandler<T> = Handler<T>
89
+ > = {
71
90
  id: number;
72
91
  hook: T;
73
- handler: Handler<T>;
74
- defaultHandler?: Handler<T>;
92
+ handler: H;
93
+ defaultHandler?: DefaultHandler<T>;
75
94
  } & HookOptions;
76
95
 
77
96
  type HookLedger<T extends HookName> = Map<Handler<T>, HookRegistration<T>>;
@@ -183,12 +202,18 @@ export class Hooks {
183
202
  * - `replace`: Replace the default handler with this handler
184
203
  * @returns A function to unregister the handler
185
204
  */
186
- on<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
187
- on<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
188
- on<T extends HookName>(
205
+
206
+ // Overload: replacing default handler
207
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: DefaultHandler<T>, options: O & { replace: true }): HookUnregister; // prettier-ignore
208
+ // Overload: passed in handler options
209
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: Handler<T>, options: O): HookUnregister; // prettier-ignore
210
+ // Overload: no handler options
211
+ on<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister; // prettier-ignore
212
+ // Implementation
213
+ on<T extends HookName, O extends HookOptions>(
189
214
  hook: T,
190
- handler: Handler<T>,
191
- options: HookOptions = {}
215
+ handler: O['replace'] extends true ? DefaultHandler<T> : Handler<T>,
216
+ options: Partial<O> = {}
192
217
  ): HookUnregister {
193
218
  const ledger = this.get(hook);
194
219
  if (!ledger) {
@@ -212,8 +237,11 @@ export class Hooks {
212
237
  * @returns A function to unregister the handler
213
238
  * @see on
214
239
  */
215
- before<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
240
+ // Overload: passed in handler options
216
241
  before<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
242
+ // Overload: no handler options
243
+ before<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
244
+ // Implementation
217
245
  before<T extends HookName>(
218
246
  hook: T,
219
247
  handler: Handler<T>,
@@ -231,11 +259,14 @@ export class Hooks {
231
259
  * @returns A function to unregister the handler
232
260
  * @see on
233
261
  */
234
- replace<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
235
- replace<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
262
+ // Overload: passed in handler options
263
+ replace<T extends HookName>(hook: T, handler: DefaultHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore
264
+ // Overload: no handler options
265
+ replace<T extends HookName>(hook: T, handler: DefaultHandler<T>): HookUnregister; // prettier-ignore
266
+ // Implementation
236
267
  replace<T extends HookName>(
237
268
  hook: T,
238
- handler: Handler<T>,
269
+ handler: DefaultHandler<T>,
239
270
  options: HookOptions = {}
240
271
  ): HookUnregister {
241
272
  return this.on(hook, handler, { ...options, replace: true });
@@ -249,8 +280,11 @@ export class Hooks {
249
280
  * @param options Any other event options (see `hooks.on()` for details)
250
281
  * @see on
251
282
  */
252
- once<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
283
+ // Overload: passed in handler options
253
284
  once<T extends HookName>(hook: T, handler: Handler<T>, options: HookOptions): HookUnregister;
285
+ // Overload: no handler options
286
+ once<T extends HookName>(hook: T, handler: Handler<T>): HookUnregister;
287
+ // Implementation
254
288
  once<T extends HookName>(
255
289
  hook: T,
256
290
  handler: Handler<T>,
@@ -265,9 +299,12 @@ export class Hooks {
265
299
  * @param handler The handler function that was registered.
266
300
  * If omitted, all handlers for the hook will be removed.
267
301
  */
302
+ // Overload: unregister a specific handler
303
+ off<T extends HookName>(hook: T, handler: Handler<T> | DefaultHandler<T>): void;
304
+ // Overload: unregister all handlers
268
305
  off<T extends HookName>(hook: T): void;
269
- off<T extends HookName>(hook: T, handler: Handler<T>): void;
270
- off<T extends HookName>(hook: T, handler?: Handler<T>): void {
306
+ // Implementation
307
+ off<T extends HookName>(hook: T, handler?: Handler<T> | DefaultHandler<T>): void {
271
308
  const ledger = this.get(hook);
272
309
  if (ledger && handler) {
273
310
  const deleted = ledger.delete(handler);
@@ -289,9 +326,9 @@ export class Hooks {
289
326
  */
290
327
  async call<T extends HookName>(
291
328
  hook: T,
292
- args?: HookArguments<T>,
293
- defaultHandler?: Handler<T>
294
- ): Promise<any> {
329
+ args: HookArguments<T>,
330
+ defaultHandler?: DefaultHandler<T>
331
+ ): Promise<Awaited<ReturnType<DefaultHandler<T>>>> {
295
332
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
296
333
  await this.run(before, args);
297
334
  const [result] = await this.run(handler, args);
@@ -310,9 +347,9 @@ export class Hooks {
310
347
  */
311
348
  callSync<T extends HookName>(
312
349
  hook: T,
313
- args?: HookArguments<T>,
314
- defaultHandler?: Handler<T>
315
- ): any {
350
+ args: HookArguments<T>,
351
+ defaultHandler?: DefaultHandler<T>
352
+ ): ReturnType<DefaultHandler<T>> {
316
353
  const { before, handler, after } = this.getHandlers(hook, defaultHandler);
317
354
  this.runSync(before, args);
318
355
  const [result] = this.runSync(handler, args);
@@ -326,10 +363,16 @@ export class Hooks {
326
363
  * @param registrations The registrations (handler + options) to execute
327
364
  * @param args Arguments to pass to the handler
328
365
  */
329
- protected async run<T extends HookName>(
330
- registrations: HookRegistration<T>[],
331
- args?: HookArguments<T>
332
- ): Promise<any> {
366
+
367
+ // Overload: running DefaultHandler: expect DefaultHandler return type
368
+ protected async run<T extends HookName>(registrations: HookRegistration<T, DefaultHandler<T>>[], args: HookArguments<T>): Promise<Awaited<ReturnType<DefaultHandler<T>>>[]>; // prettier-ignore
369
+ // Overload: running user handler: expect no specific type
370
+ protected async run<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore
371
+ // Implementation
372
+ protected async run<T extends HookName, R extends HookRegistration<T>[]>(
373
+ registrations: R,
374
+ args: HookArguments<T>
375
+ ): Promise<Awaited<ReturnType<DefaultHandler<T>>> | unknown[]> {
333
376
  const results = [];
334
377
  for (const { hook, handler, defaultHandler, once } of registrations) {
335
378
  const result = await runAsPromise(handler, [this.swup.visit, args, defaultHandler]);
@@ -346,13 +389,19 @@ export class Hooks {
346
389
  * @param registrations The registrations (handler + options) to execute
347
390
  * @param args Arguments to pass to the handler
348
391
  */
349
- protected runSync<T extends HookName>(
350
- registrations: HookRegistration<T>[],
351
- args?: HookArguments<T>
352
- ): any[] {
392
+
393
+ // Overload: running DefaultHandler: expect DefaultHandler return type
394
+ protected runSync<T extends HookName>(registrations: HookRegistration<T, DefaultHandler<T>>[], args: HookArguments<T> ): ReturnType<DefaultHandler<T>>[]; // prettier-ignore
395
+ // Overload: running user handler: expect no specific type
396
+ protected runSync<T extends HookName>(registrations: HookRegistration<T>[], args: HookArguments<T>): unknown[]; // prettier-ignore
397
+ // Implementation
398
+ protected runSync<T extends HookName, R extends HookRegistration<T>[]>(
399
+ registrations: R,
400
+ args: HookArguments<T>
401
+ ): (ReturnType<DefaultHandler<T>> | unknown)[] {
353
402
  const results = [];
354
403
  for (const { hook, handler, defaultHandler, once } of registrations) {
355
- const result = handler(this.swup.visit, args as HookArguments<T>, defaultHandler);
404
+ const result = (handler as DefaultHandler<T>)(this.swup.visit, args, defaultHandler);
356
405
  results.push(result);
357
406
  if (isPromise(result)) {
358
407
  console.warn(
@@ -374,30 +423,33 @@ export class Hooks {
374
423
  * @returns An object with the handlers sorted into `before` and `after` arrays,
375
424
  * as well as a flag indicating if the original handler was replaced
376
425
  */
377
- protected getHandlers<T extends HookName>(hook: T, defaultHandler?: Handler<T>) {
426
+ protected getHandlers<T extends HookName>(hook: T, defaultHandler?: DefaultHandler<T>) {
378
427
  const ledger = this.get(hook);
379
428
  if (!ledger) {
380
429
  return { found: false, before: [], handler: [], after: [], replaced: false };
381
430
  }
382
431
 
383
- const sort = this.sortRegistrations;
384
432
  const registrations = Array.from(ledger.values());
385
433
 
434
+ // Let TypeScript know that replaced handlers are default handlers by filtering to true
435
+ const def = (T: HookRegistration<T>): T is HookRegistration<T, DefaultHandler<T>> => true;
436
+ const sort = this.sortRegistrations;
437
+
386
438
  // Filter into before, after, and replace handlers
387
439
  const before = registrations.filter(({ before, replace }) => before && !replace).sort(sort);
388
- const replace = registrations.filter(({ replace }) => replace).sort(sort);
440
+ const replace = registrations.filter(({ replace }) => replace).filter(def).sort(sort); // prettier-ignore
389
441
  const after = registrations.filter(({ before, replace }) => !before && !replace).sort(sort);
390
442
  const replaced = replace.length > 0;
391
443
 
392
444
  // Define main handler registration
393
- // This is an array to allow passing it into hooks.run() directly
394
- let handler: HookRegistration<T>[] = [];
445
+ // Created as HookRegistration[] array to allow passing it into hooks.run() directly
446
+ let handler: HookRegistration<T, DefaultHandler<T>>[] = [];
395
447
  if (defaultHandler) {
396
448
  handler = [{ id: 0, hook, handler: defaultHandler }];
397
449
  if (replaced) {
398
450
  const index = replace.length - 1;
399
451
  const replacingHandler = replace[index].handler;
400
- const createDefaultHandler = (index: number): Handler<T> | undefined => {
452
+ const createDefaultHandler = (index: number): DefaultHandler<T> | undefined => {
401
453
  const next = replace[index - 1];
402
454
  if (next) {
403
455
  return (visit, args) =>
@@ -13,6 +13,8 @@ export interface Visit {
13
13
  animation: VisitAnimation;
14
14
  /** What triggered this visit */
15
15
  trigger: VisitTrigger;
16
+ /** Cache behavior for this visit */
17
+ cache: VisitCache;
16
18
  /** Browser history behavior on this visit */
17
19
  history: VisitHistory;
18
20
  /** Scroll behavior on this visit */
@@ -26,7 +28,9 @@ export interface VisitFrom {
26
28
 
27
29
  export interface VisitTo {
28
30
  /** The URL of the next page */
29
- url?: string;
31
+ url: string;
32
+ /** The hash of the next page */
33
+ hash?: string;
30
34
  /** The HTML content of the next page */
31
35
  html?: string;
32
36
  }
@@ -58,6 +62,13 @@ export interface VisitTrigger {
58
62
  event?: Event;
59
63
  }
60
64
 
65
+ export interface VisitCache {
66
+ /** Whether this visit will try to load the requested page from cache. */
67
+ read: boolean;
68
+ /** Whether this visit will save the loaded page in cache. */
69
+ write: boolean;
70
+ }
71
+
61
72
  export interface VisitHistory {
62
73
  /** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
63
74
  action: HistoryAction;
@@ -68,7 +79,7 @@ export interface VisitHistory {
68
79
  }
69
80
 
70
81
  export interface VisitInitOptions {
71
- to: string | undefined;
82
+ to: string;
72
83
  from?: string;
73
84
  hash?: string;
74
85
  animate?: boolean;
@@ -86,7 +97,7 @@ export function createVisit(
86
97
  {
87
98
  to,
88
99
  from = this.currentPageUrl,
89
- hash: target,
100
+ hash,
90
101
  animate = true,
91
102
  animation: name,
92
103
  el,
@@ -97,7 +108,7 @@ export function createVisit(
97
108
  ): Visit {
98
109
  return {
99
110
  from: { url: from },
100
- to: { url: to },
111
+ to: { url: to, hash },
101
112
  containers: this.options.containers,
102
113
  animation: {
103
114
  animate,
@@ -110,6 +121,10 @@ export function createVisit(
110
121
  el,
111
122
  event
112
123
  },
124
+ cache: {
125
+ read: this.options.cache,
126
+ write: this.options.cache
127
+ },
113
128
  history: {
114
129
  action,
115
130
  popstate: false,
@@ -117,7 +132,7 @@ export function createVisit(
117
132
  },
118
133
  scroll: {
119
134
  reset,
120
- target
135
+ target: undefined
121
136
  }
122
137
  };
123
138
  }
@@ -90,19 +90,19 @@ describe('Cache', () => {
90
90
 
91
91
  swup.hooks.on('cache:set', (_, { page }) => {
92
92
  const ttl: CacheTtlData = { ttl: 1000, created: now };
93
- cache.update(page.url, ttl as AugmentedCacheData);
93
+ cache.update(page.url, ttl);
94
94
  });
95
95
 
96
96
  cache.set('/page', { url: '/page', html: '' });
97
97
 
98
- const page = cache.get('/page') as AugmentedCacheData;
98
+ const page = cache.get('/page');
99
99
 
100
100
  expect(page).toEqual({ url: '/page', html: '', ttl: 1000, created: now });
101
101
  });
102
102
 
103
103
  it('should allow manual pruning', () => {
104
104
  swup.hooks.on('cache:set', (_, { page }) => {
105
- cache.update(page.url, { index: cache.size } as AugmentedCacheData);
105
+ cache.update(page.url, { index: cache.size });
106
106
  });
107
107
 
108
108
  cache.set(page1.url, page1);
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import Swup from '../../Swup.js';
3
- import { Handler, Hooks } from '../Hooks.js';
3
+ import { DefaultHandler, Handler, Hooks } from '../Hooks.js';
4
4
  import { Visit } from '../Visit.js';
5
5
 
6
6
  describe('Hook registry', () => {
@@ -37,14 +37,14 @@ describe('Hook registry', () => {
37
37
  swup.hooks.on('enable', handler1);
38
38
  swup.hooks.on('enable', handler2);
39
39
 
40
- await swup.hooks.call('enable');
40
+ await swup.hooks.call('enable', undefined);
41
41
 
42
42
  expect(handler1).toBeCalledTimes(1);
43
43
  expect(handler2).toBeCalledTimes(1);
44
44
 
45
45
  swup.hooks.off('enable', handler2);
46
46
 
47
- await swup.hooks.call('enable');
47
+ await swup.hooks.call('enable', undefined);
48
48
 
49
49
  expect(handler1).toBeCalledTimes(2);
50
50
  expect(handler2).toBeCalledTimes(1);
@@ -60,14 +60,14 @@ describe('Hook registry', () => {
60
60
 
61
61
  expect(unregister1).toBeTypeOf('function');
62
62
 
63
- await swup.hooks.call('enable');
63
+ await swup.hooks.call('enable', undefined);
64
64
 
65
65
  expect(handler1).toBeCalledTimes(1);
66
66
  expect(handler2).toBeCalledTimes(1);
67
67
 
68
68
  unregister2();
69
69
 
70
- await swup.hooks.call('enable');
70
+ await swup.hooks.call('enable', undefined);
71
71
 
72
72
  expect(handler1).toBeCalledTimes(2);
73
73
  expect(handler2).toBeCalledTimes(1);
@@ -79,7 +79,7 @@ describe('Hook registry', () => {
79
79
 
80
80
  swup.hooks.on('enable', handler);
81
81
 
82
- await swup.hooks.call('enable');
82
+ await swup.hooks.call('enable', undefined);
83
83
 
84
84
  expect(handler).toBeCalledTimes(1);
85
85
  });
@@ -303,18 +303,17 @@ describe('Types', () => {
303
303
  const swup = new Swup();
304
304
 
305
305
  // @ts-expect-no-error
306
- swup.hooks.on(
307
- 'history:popstate',
308
- (visit: Visit, { event }: { event: PopStateEvent }) => {}
309
- );
306
+ swup.hooks.on('history:popstate', (visit: Visit, { event }: { event: PopStateEvent }) => {});
310
307
  // @ts-expect-no-error
311
308
  await swup.hooks.call('history:popstate', { event: new PopStateEvent('') });
312
309
 
313
- // @ts-expect-error
310
+ // @ts-expect-error: first arg must be Visit object
314
311
  swup.hooks.on('history:popstate', ({ event: MouseEvent }) => {});
315
- // @ts-expect-error
312
+ // @ts-expect-error: event arg must be PopStateEvent
316
313
  swup.hooks.on('history:popstate', (visit: Visit, { event }: { event: MouseEvent }) => {});
317
- // @ts-expect-error
314
+ // @ts-expect-error: event arg must be PopStateEvent
318
315
  await swup.hooks.call('history:popstate', { event: new MouseEvent('') });
316
+ // @ts-expect-error: handler arg must be optional: handler?
317
+ swup.hooks.replace('enable', (visit: Visit, args: undefined, handler: DefaultHandler<'enable'>) => {});
319
318
  });
320
319
  });
@@ -27,5 +27,5 @@ export const animatePageIn = async function (this: Swup) {
27
27
 
28
28
  await animation;
29
29
 
30
- await this.hooks.call('animation:in:end');
30
+ await this.hooks.call('animation:in:end', undefined);
31
31
  };