swup 3.1.0 → 4.0.0-rc.14

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 (71) 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 +20 -20
  10. package/dist/types/helpers/delegateEvent.d.ts +4 -2
  11. package/dist/types/helpers/fetch.d.ts +2 -2
  12. package/dist/types/helpers.d.ts +10 -10
  13. package/dist/types/index.d.ts +5 -5
  14. package/dist/types/modules/Cache.d.ts +2 -2
  15. package/dist/types/modules/__test__/fetchPage.test.d.ts +1 -0
  16. package/dist/types/modules/destroy.d.ts +2 -0
  17. package/dist/types/modules/enable.d.ts +2 -0
  18. package/dist/types/modules/enterPage.d.ts +2 -2
  19. package/dist/types/modules/events.d.ts +6 -6
  20. package/dist/types/modules/fetchPage.d.ts +3 -3
  21. package/dist/types/modules/getAnchorElement.d.ts +0 -7
  22. package/dist/types/modules/getAnimationPromises.d.ts +1 -1
  23. package/dist/types/modules/getPageData.d.ts +2 -2
  24. package/dist/types/modules/handleLinkToSamePage.d.ts +2 -0
  25. package/dist/types/modules/isSameResolvedUrl.d.ts +8 -0
  26. package/dist/types/modules/leavePage.d.ts +2 -2
  27. package/dist/types/modules/linkClickHandler.d.ts +3 -0
  28. package/dist/types/modules/loadPage.d.ts +2 -5
  29. package/dist/types/modules/off.d.ts +3 -0
  30. package/dist/types/modules/on.d.ts +5 -0
  31. package/dist/types/modules/plugins.d.ts +1 -1
  32. package/dist/types/modules/popStateHandler.d.ts +2 -0
  33. package/dist/types/modules/renderPage.d.ts +2 -2
  34. package/dist/types/modules/resolveUrl.d.ts +7 -0
  35. package/dist/types/modules/shouldIgnoreVisit.d.ts +4 -0
  36. package/dist/types/modules/transitions.d.ts +1 -1
  37. package/dist/types/modules/triggerEvent.d.ts +3 -0
  38. package/dist/types/modules/triggerWillOpenNewWindow.d.ts +2 -0
  39. package/dist/types/modules/updateTransition.d.ts +2 -0
  40. package/dist/types/utils.d.ts +1 -1
  41. package/package.json +8 -5
  42. package/readme.md +30 -12
  43. package/src/Swup.ts +115 -143
  44. package/src/__test__/index.test.ts +6 -1
  45. package/src/helpers/Location.ts +10 -7
  46. package/src/helpers/__test__/matchPath.test.ts +54 -0
  47. package/src/helpers/delegateEvent.ts +1 -0
  48. package/src/helpers/matchPath.ts +22 -0
  49. package/src/helpers.ts +2 -4
  50. package/src/index.ts +7 -4
  51. package/src/modules/Cache.ts +43 -33
  52. package/src/modules/Context.ts +91 -0
  53. package/src/modules/Hooks.ts +393 -0
  54. package/src/modules/__test__/cache.test.ts +142 -0
  55. package/src/modules/__test__/hooks.test.ts +192 -0
  56. package/src/modules/enterPage.ts +19 -17
  57. package/src/modules/fetchPage.ts +74 -29
  58. package/src/modules/getAnchorElement.ts +8 -4
  59. package/src/modules/getAnimationPromises.ts +66 -75
  60. package/src/modules/leavePage.ts +22 -20
  61. package/src/modules/loadPage.ts +72 -54
  62. package/src/modules/renderPage.ts +41 -32
  63. package/src/modules/replaceContent.ts +26 -17
  64. package/src/utils/index.ts +24 -3
  65. package/src/helpers/fetch.ts +0 -33
  66. package/src/helpers/getDataFromHtml.ts +0 -39
  67. package/src/helpers/markSwupElements.ts +0 -16
  68. package/src/modules/__test__/events.test.ts +0 -72
  69. package/src/modules/events.ts +0 -92
  70. package/src/modules/getPageData.ts +0 -24
  71. package/src/modules/transitions.ts +0 -10
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import Swup from '../../Swup.js';
3
+ import { Handler, Hooks } from '../Hooks.js';
4
+ import { Context } from '../Context.js';
5
+
6
+ describe('Hook registry', () => {
7
+ it('should add custom handlers', () => {
8
+ const swup = new Swup();
9
+ const handler = vi.fn();
10
+
11
+ // Make private fields public for this test
12
+ const HooksWithAccess = class extends Hooks {
13
+ getRegistry() {
14
+ return this.registry;
15
+ }
16
+ };
17
+ const hooks = new HooksWithAccess(swup);
18
+
19
+ hooks.on('enabled', handler);
20
+ const ledger = hooks.getRegistry().get('enabled');
21
+
22
+ expect(ledger).toBeDefined();
23
+ expect(ledger).toBeInstanceOf(Map);
24
+ expect(ledger!.size).toBe(1);
25
+
26
+ const registrations = Array.from(ledger!.values());
27
+ const registration = registrations.find((reg) => reg.handler === handler);
28
+
29
+ expect(registration?.handler).toEqual(handler);
30
+ });
31
+
32
+ it('should return the passed handler', () => {
33
+ const swup = new Swup();
34
+ const handler = vi.fn();
35
+
36
+ const handlerReturned = swup.hooks.on('enabled', handler);
37
+
38
+ expect(handlerReturned).toEqual(handler);
39
+ });
40
+
41
+ it('should trigger custom handlers', async () => {
42
+ const swup = new Swup();
43
+ const handler = vi.fn();
44
+
45
+ swup.hooks.on('enabled', handler);
46
+
47
+ await swup.hooks.trigger('enabled');
48
+
49
+ expect(handler).toBeCalledTimes(1);
50
+ });
51
+
52
+ it('should only trigger custom handlers once if requested', async () => {
53
+ const swup = new Swup();
54
+ const handler = vi.fn();
55
+
56
+ swup.hooks.on('enabled', handler, { once: true });
57
+
58
+ await swup.hooks.trigger('enabled', undefined, () => {});
59
+ await swup.hooks.trigger('enabled', undefined, () => {});
60
+
61
+ expect(handler).toBeCalledTimes(1);
62
+ });
63
+
64
+ it('should only trigger custom handlers once if using alias', async () => {
65
+ const swup = new Swup();
66
+ const handler = vi.fn();
67
+
68
+ swup.hooks.once('enabled', handler);
69
+
70
+ await swup.hooks.trigger('enabled', undefined, () => {});
71
+ await swup.hooks.trigger('enabled', undefined, () => {});
72
+
73
+ expect(handler).toBeCalledTimes(1);
74
+ });
75
+
76
+ it('should trigger original handlers', async () => {
77
+ const swup = new Swup();
78
+ const handler = vi.fn();
79
+
80
+ await swup.hooks.trigger('enabled', undefined, handler);
81
+
82
+ expect(handler).toBeCalledTimes(1);
83
+ });
84
+
85
+ it('should allow triggering custom handlers before original handler', async () => {
86
+ const swup = new Swup();
87
+
88
+ let called: Array<string> = [];
89
+ const handlers = {
90
+ before: () => {
91
+ called.push('before');
92
+ },
93
+ original: () => {
94
+ called.push('original');
95
+ },
96
+ normal: () => {
97
+ called.push('normal');
98
+ },
99
+ after: () => {
100
+ called.push('after');
101
+ }
102
+ };
103
+
104
+ swup.hooks.on('disabled', handlers.before, { before: true });
105
+ swup.hooks.on('disabled', handlers.normal, {});
106
+ swup.hooks.on('disabled', handlers.after, {});
107
+
108
+ await swup.hooks.trigger('disabled', undefined, handlers.original);
109
+
110
+ expect(called).toEqual(['before', 'original', 'normal', 'after']);
111
+ });
112
+
113
+ it('should sort custom handlers by priority', async () => {
114
+ const swup = new Swup();
115
+
116
+ let called: Array<number> = [];
117
+ const handlers = {
118
+ 1: () => {
119
+ called.push(1);
120
+ },
121
+ 2: () => {
122
+ called.push(2);
123
+ },
124
+ 3: () => {
125
+ called.push(3);
126
+ },
127
+ 4: () => {
128
+ called.push(4);
129
+ },
130
+ 5: () => {
131
+ called.push(5);
132
+ },
133
+ 6: () => {
134
+ called.push(6);
135
+ }
136
+ };
137
+
138
+ swup.hooks.on('disabled', handlers['1'], { priority: 1, before: true });
139
+ swup.hooks.on('disabled', handlers['2'], { priority: 2, before: true });
140
+ swup.hooks.on('disabled', handlers['4'], { priority: 5 });
141
+ swup.hooks.on('disabled', handlers['6'], { priority: 4 });
142
+ swup.hooks.on('disabled', handlers['5'], { priority: 4 });
143
+
144
+ await swup.hooks.trigger('disabled', undefined, handlers['3']);
145
+
146
+ expect(called).toEqual([2, 1, 3, 4, 6, 5]);
147
+ });
148
+
149
+ it('should allow replacing original handlers', async () => {
150
+ const swup = new Swup();
151
+ const listener = vi.fn();
152
+ const handler = vi.fn();
153
+
154
+ swup.hooks.on('enabled', listener, { replace: true });
155
+
156
+ await swup.hooks.trigger('enabled', undefined, handler);
157
+
158
+ expect(handler).toBeCalledTimes(0);
159
+ expect(listener).toBeCalledTimes(1);
160
+ });
161
+
162
+ it('should trigger event handler with context and args', async () => {
163
+ const swup = new Swup();
164
+ const handler: Handler<'popState'> = vi.fn();
165
+ const ctx = swup.context;
166
+ const args = { event: new PopStateEvent('') };
167
+
168
+ swup.hooks.on('popState', handler);
169
+ await swup.hooks.trigger('popState', args);
170
+
171
+ expect(handler).toBeCalledTimes(1);
172
+ expect(handler).toBeCalledWith(ctx, args);
173
+ });
174
+ });
175
+
176
+ describe('Types', () => {
177
+ it('error when necessary', async () => {
178
+ const swup = new Swup();
179
+
180
+ // @ts-expect-no-error
181
+ swup.hooks.on('popState', (ctx: Context, { event }: { event: PopStateEvent }) => {});
182
+ // @ts-expect-no-error
183
+ await swup.hooks.trigger('popState', { event: new PopStateEvent('') });
184
+
185
+ // @ts-expect-error
186
+ swup.hooks.on('popState', ({ event: MouseEvent }) => {});
187
+ // @ts-expect-error
188
+ swup.hooks.on('popState', (ctx: Context, { event }: { event: MouseEvent }) => {});
189
+ // @ts-expect-error
190
+ await swup.hooks.trigger('popState', { event: new MouseEvent('') });
191
+ });
192
+ });
@@ -1,24 +1,26 @@
1
- import { nextTick } from '../utils.js';
2
1
  import Swup from '../Swup.js';
3
- import { PageRenderOptions } from './renderPage.js';
2
+ import { nextTick } from '../utils.js';
4
3
 
5
- export const enterPage = function (this: Swup, { event, skipTransition }: PageRenderOptions = {}) {
6
- if (skipTransition) {
7
- this.triggerEvent('transitionEnd', event);
8
- this.cleanupAnimationClasses();
9
- return [Promise.resolve()];
4
+ export const enterPage = async function (this: Swup) {
5
+ if (this.context.transition.animate) {
6
+ const animation = this.hooks.trigger(
7
+ 'awaitAnimation',
8
+ { selector: this.options.animationSelector, direction: 'in' },
9
+ async (context, { selector, direction }) => {
10
+ await Promise.all(this.getAnimationPromises({ selector, direction }));
11
+ }
12
+ );
13
+ await nextTick();
14
+ await this.hooks.trigger('animationInStart', undefined, () => {
15
+ document.documentElement.classList.remove('is-animating');
16
+ });
17
+ await animation;
18
+ await this.hooks.trigger('animationInDone');
10
19
  }
11
20
 
12
- nextTick(() => {
13
- this.triggerEvent('animationInStart');
14
- document.documentElement.classList.remove('is-animating');
15
- });
16
-
17
- const animationPromises = this.getAnimationPromises('in');
18
- Promise.all(animationPromises).then(() => {
19
- this.triggerEvent('animationInDone');
20
- this.triggerEvent('transitionEnd', event);
21
+ await this.hooks.trigger('transitionEnd', undefined, () => {
21
22
  this.cleanupAnimationClasses();
22
23
  });
23
- return animationPromises;
24
+
25
+ this.context = this.createContext({ to: undefined });
24
26
  };
@@ -1,35 +1,80 @@
1
1
  import Swup from '../Swup.js';
2
- import { fetch } from '../helpers.js';
3
- import { TransitionOptions } from './loadPage.js';
4
- import { PageRecord } from './Cache.js';
2
+ import { Location } from '../helpers.js';
5
3
 
6
- export function fetchPage(this: Swup, data: TransitionOptions): Promise<PageRecord> {
7
- const headers = this.options.requestHeaders;
8
- const { url } = data;
4
+ export interface PageData {
5
+ url: string;
6
+ html: string;
7
+ }
8
+
9
+ export interface FetchOptions extends RequestInit {
10
+ method?: 'GET' | 'POST';
11
+ body?: string | FormData | URLSearchParams;
12
+ headers?: Record<string, string>;
13
+ }
14
+
15
+ export class FetchError extends Error {
16
+ url: string;
17
+ status: number;
18
+ constructor(message: string, details: { url: string; status: number }) {
19
+ super(message);
20
+ this.name = 'FetchError';
21
+ this.url = details.url;
22
+ this.status = details.status;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Fetch a page from the server, return it and cache it.
28
+ */
29
+ export async function fetchPage(
30
+ this: Swup,
31
+ url: URL | string,
32
+ options: FetchOptions & { triggerHooks?: boolean } = {}
33
+ ): Promise<PageData> {
34
+ const { url: requestUrl } = Location.fromUrl(url);
35
+
36
+ if (this.cache.has(requestUrl)) {
37
+ const page = this.cache.get(requestUrl) as PageData;
38
+ if (options.triggerHooks !== false) {
39
+ await this.hooks.trigger('pageLoaded', { page, cache: true });
40
+ }
41
+ return page;
42
+ }
43
+
44
+ const headers = { ...this.options.requestHeaders, ...options.headers };
45
+ options = { ...options, headers };
46
+
47
+ // Allow hooking before this and returning a custom response-like object (e.g. custom fetch implementation)
48
+ const response = await this.hooks.trigger(
49
+ 'fetchPage',
50
+ { url: requestUrl, options },
51
+ async (context, { url, options, response }) => await (response || fetch(url, options))
52
+ );
53
+
54
+ const { status, url: responseUrl } = response;
55
+ const html = await response.text();
56
+
57
+ if (status === 500) {
58
+ this.hooks.trigger('serverError', { status, response, url: responseUrl });
59
+ throw new FetchError(`Server error: ${responseUrl}`, { status, url: responseUrl });
60
+ }
61
+
62
+ if (!html) {
63
+ throw new FetchError(`Empty response: ${responseUrl}`, { status, url: responseUrl });
64
+ }
65
+
66
+ // Resolve real url after potential redirect
67
+ const { url: finalUrl } = new Location(responseUrl);
68
+ const page = { url: finalUrl, html };
69
+
70
+ // Only save cache entry for non-redirects
71
+ if (requestUrl === finalUrl) {
72
+ this.cache.set(page.url, page);
73
+ }
9
74
 
10
- if (this.cache.exists(url)) {
11
- this.triggerEvent('pageRetrievedFromCache');
12
- return Promise.resolve(this.cache.getPage(url));
75
+ if (options.triggerHooks !== false) {
76
+ await this.hooks.trigger('pageLoaded', { page, cache: false });
13
77
  }
14
78
 
15
- return new Promise((resolve, reject) => {
16
- fetch({ ...data, headers }, (response) => {
17
- if (response.status === 500) {
18
- this.triggerEvent('serverError');
19
- reject(url);
20
- return;
21
- }
22
- // get json data
23
- const page = this.getPageData(response);
24
- if (!page || !page.blocks.length) {
25
- reject(url);
26
- return;
27
- }
28
- // render page
29
- const cacheablePageData = { ...page, url };
30
- this.cache.cacheUrl(cacheablePageData);
31
- this.triggerEvent('pageLoaded');
32
- resolve(cacheablePageData);
33
- });
34
- });
79
+ return page;
35
80
  }
@@ -17,11 +17,15 @@ export const getAnchorElement = (hash: string): Element | null => {
17
17
  }
18
18
 
19
19
  const decoded = decodeURIComponent(hash);
20
-
21
- return (
20
+ let element =
22
21
  document.getElementById(hash) ||
23
22
  document.getElementById(decoded) ||
24
23
  query(`a[name='${escape(hash)}']`) ||
25
- query(`a[name='${escape(decoded)}']`)
26
- );
24
+ query(`a[name='${escape(decoded)}']`);
25
+
26
+ if (!element && hash === 'top') {
27
+ element = document.body;
28
+ }
29
+
30
+ return element;
27
31
  };
@@ -1,44 +1,51 @@
1
1
  import { queryAll, toMs } from '../utils.js';
2
- import Swup from '../Swup.js';
2
+ import Swup, { Options } from '../Swup.js';
3
3
 
4
- // Transition property/event sniffing
5
- let transitionProp = 'transition';
6
- let transitionEndEvent = 'transitionend';
7
- let animationProp = 'animation';
8
- let animationEndEvent = 'animationend';
4
+ const TRANSITION = 'transition';
5
+ const ANIMATION = 'animation';
9
6
 
10
- if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
11
- transitionProp = 'WebkitTransition';
12
- transitionEndEvent = 'webkitTransitionEnd';
13
- }
7
+ type AnimationTypes = typeof TRANSITION | typeof ANIMATION;
8
+ type AnimationProperties = 'Delay' | 'Duration';
9
+ type AnimationStyleKeys = `${AnimationTypes}${AnimationProperties}` | 'transitionProperty';
10
+ type AnimationStyleDeclarations = Pick<CSSStyleDeclaration, AnimationStyleKeys>;
14
11
 
15
- if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
16
- animationProp = 'WebkitAnimation';
17
- animationEndEvent = 'webkitAnimationEnd';
18
- }
12
+ export type AnimationDirection = 'in' | 'out';
19
13
 
14
+ /**
15
+ * Get an array of Promises that resolve when all animations are done on the page.
16
+ * @note We don't make use of the `direction` argument, but it's required by JS plugin
17
+ */
20
18
  export function getAnimationPromises(
21
19
  this: Swup,
22
- // we don't use this argument, but JS plugin depends on it with
23
- // its own version of getAnimationPromises, so it must be specified when
24
- // getAnimationPromises is being used
25
- animationType: 'in' | 'out'
20
+ {
21
+ elements,
22
+ selector
23
+ }: {
24
+ selector: Options['animationSelector'];
25
+ elements?: NodeListOf<HTMLElement> | HTMLElement[];
26
+ direction?: AnimationDirection;
27
+ }
26
28
  ): Promise<void>[] {
27
- const selector = this.options.animationSelector;
29
+ // Use array of a single resolved promise instead of an empty array to allow
30
+ // possible future use with Promise.race() which requires an actual value
31
+ const resolved = [Promise.resolve()];
28
32
 
29
33
  // Allow usage of swup without animations
30
- if (selector === false) {
31
- // Use array of a single resolved promise instead of an empty array to allow
32
- // possible future use with Promise.race() which requires an actual value
33
- return [Promise.resolve()];
34
+ if (selector === false && !elements) {
35
+ return resolved;
34
36
  }
35
37
 
36
- const animatedElements = queryAll(selector, document.body);
37
-
38
- // Warn if no elements match the animationSelector, but keep things going
39
- if (!animatedElements.length) {
40
- console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
41
- return [Promise.resolve()];
38
+ // Allow passing in elements
39
+ let animatedElements: HTMLElement[] = [];
40
+ if (elements) {
41
+ animatedElements = Array.from(elements);
42
+ } else if (selector) {
43
+ animatedElements = queryAll(selector, document.body);
44
+ // Warn if no elements match the selector, but keep things going
45
+ if (!animatedElements.length) {
46
+ console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
47
+ return resolved;
48
+ }
42
49
  }
43
50
 
44
51
  const animationPromises = animatedElements
@@ -46,18 +53,17 @@ export function getAnimationPromises(
46
53
  .filter(Boolean) as Promise<void>[];
47
54
 
48
55
  if (!animationPromises.length) {
49
- console.warn(
50
- `[swup] No CSS animation duration defined on elements matching \`${selector}\``
51
- );
52
- return [Promise.resolve()];
56
+ if (selector) {
57
+ console.warn(
58
+ `[swup] No CSS animation duration defined on elements matching \`${selector}\``
59
+ );
60
+ }
61
+ return resolved;
53
62
  }
54
63
 
55
64
  return animationPromises;
56
65
  }
57
66
 
58
- const isTransitionOrAnimationEvent = (event: any): event is TransitionEvent | AnimationEvent =>
59
- [transitionEndEvent, animationEndEvent].includes(event.type);
60
-
61
67
  function getAnimationPromiseForElement(element: Element): Promise<void> | undefined {
62
68
  const { type, timeout, propCount } = getTransitionInfo(element);
63
69
 
@@ -67,7 +73,7 @@ function getAnimationPromiseForElement(element: Element): Promise<void> | undefi
67
73
  }
68
74
 
69
75
  return new Promise((resolve) => {
70
- const endEvent = type === 'transition' ? transitionEndEvent : animationEndEvent;
76
+ const endEvent = `${type}end`;
71
77
  const startTime = performance.now();
72
78
  let propsTransitioned = 0;
73
79
 
@@ -108,60 +114,37 @@ function getAnimationPromiseForElement(element: Element): Promise<void> | undefi
108
114
  });
109
115
  }
110
116
 
111
- export function getTransitionInfo(
112
- element: Element,
113
- expectedType: 'animation' | 'transition' | null = null
114
- ) {
115
- const styles = window.getComputedStyle(element);
116
-
117
- // not sure what to do about the below mess other than casting, but it's a mess
118
- const transitionDelay = `${transitionProp}Delay` as keyof CSSStyleDeclaration;
119
- const transitionDuration = `${transitionProp}Duration` as keyof CSSStyleDeclaration;
120
- const animationDelay = `${animationProp}Delay` as keyof CSSStyleDeclaration;
121
- const animationDuration = `${animationProp}Duration` as keyof CSSStyleDeclaration;
122
-
123
- const transitionDelays = (
124
- styles[transitionDelay] as CSSStyleDeclaration['transitionDelay']
125
- ).split(', ');
126
- const transitionDurations = (
127
- (styles[transitionDuration] || '') as CSSStyleDeclaration['transitionDuration']
128
- ).split(', ');
129
- const transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);
117
+ export function getTransitionInfo(element: Element, expectedType?: AnimationTypes) {
118
+ const styles = window.getComputedStyle(element) as AnimationStyleDeclarations;
130
119
 
131
- const animationDelays = (
132
- (styles[animationDelay] || '') as CSSStyleDeclaration['animationDelay']
133
- ).split(', ');
134
- const animationDurations = (
135
- (styles[animationDuration] || '') as CSSStyleDeclaration['animationDuration']
136
- ).split(', ');
120
+ const transitionDelays = getStyleProperties(styles, `${TRANSITION}Delay`);
121
+ const transitionDurations = getStyleProperties(styles, `${TRANSITION}Duration`);
122
+ const transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);
123
+ const animationDelays = getStyleProperties(styles, `${ANIMATION}Delay`);
124
+ const animationDurations = getStyleProperties(styles, `${ANIMATION}Duration`);
137
125
  const animationTimeout = calculateTimeout(animationDelays, animationDurations);
138
126
 
139
- let type: string | null = '';
127
+ let type: AnimationTypes | null = null;
140
128
  let timeout = 0;
141
129
  let propCount = 0;
142
130
 
143
- if (expectedType === 'transition') {
131
+ if (expectedType === TRANSITION) {
144
132
  if (transitionTimeout > 0) {
145
- type = 'transition';
133
+ type = TRANSITION;
146
134
  timeout = transitionTimeout;
147
135
  propCount = transitionDurations.length;
148
136
  }
149
- } else if (expectedType === 'animation') {
137
+ } else if (expectedType === ANIMATION) {
150
138
  if (animationTimeout > 0) {
151
- type = 'animation';
139
+ type = ANIMATION;
152
140
  timeout = animationTimeout;
153
141
  propCount = animationDurations.length;
154
142
  }
155
143
  } else {
156
144
  timeout = Math.max(transitionTimeout, animationTimeout);
157
- type =
158
- timeout > 0
159
- ? transitionTimeout > animationTimeout
160
- ? 'transition'
161
- : 'animation'
162
- : null;
145
+ type = timeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null;
163
146
  propCount = type
164
- ? type === 'transition'
147
+ ? type === TRANSITION
165
148
  ? transitionDurations.length
166
149
  : animationDurations.length
167
150
  : 0;
@@ -174,7 +157,15 @@ export function getTransitionInfo(
174
157
  };
175
158
  }
176
159
 
177
- function calculateTimeout(delays: string[], durations: string[]) {
160
+ function isTransitionOrAnimationEvent(event: any): event is TransitionEvent | AnimationEvent {
161
+ return [`${TRANSITION}end`, `${ANIMATION}end`].includes(event.type);
162
+ }
163
+
164
+ function getStyleProperties(styles: AnimationStyleDeclarations, key: AnimationStyleKeys): string[] {
165
+ return (styles[key] || '').split(', ');
166
+ }
167
+
168
+ function calculateTimeout(delays: string[], durations: string[]): number {
178
169
  while (delays.length < durations.length) {
179
170
  delays = delays.concat(delays);
180
171
  }
@@ -1,27 +1,29 @@
1
1
  import Swup from '../Swup.js';
2
- import { PageRenderOptions } from './renderPage.js';
2
+ import { classify } from '../helpers.js';
3
3
 
4
- export const leavePage = function (this: Swup, { event, skipTransition }: PageRenderOptions = {}) {
5
- const isHistoryVisit = event instanceof PopStateEvent;
6
-
7
- if (skipTransition) {
8
- this.triggerEvent('animationSkipped');
9
- return [Promise.resolve()];
4
+ export const leavePage = async function (this: Swup) {
5
+ if (!this.context.transition.animate) {
6
+ await this.hooks.trigger('animationSkipped');
7
+ return;
10
8
  }
11
9
 
12
- this.triggerEvent('animationOutStart');
13
-
14
- // handle classes
15
- document.documentElement.classList.add('is-changing', 'is-leaving', 'is-animating');
16
- if (isHistoryVisit) {
17
- document.documentElement.classList.add('is-popstate');
18
- }
19
-
20
- // animation promise stuff
21
- const animationPromises: Promise<void>[] = this.getAnimationPromises('out');
22
- Promise.all(animationPromises).then(() => {
23
- this.triggerEvent('animationOutDone');
10
+ await this.hooks.trigger('animationOutStart', undefined, () => {
11
+ document.documentElement.classList.add('is-changing', 'is-leaving', 'is-animating');
12
+ if (this.context.history.popstate) {
13
+ document.documentElement.classList.add('is-popstate');
14
+ }
15
+ if (this.context.transition.name) {
16
+ document.documentElement.classList.add(`to-${classify(this.context.transition.name)}`);
17
+ }
24
18
  });
25
19
 
26
- return animationPromises;
20
+ await this.hooks.trigger(
21
+ 'awaitAnimation',
22
+ { selector: this.options.animationSelector, direction: 'out' },
23
+ async (context, { selector, direction }) => {
24
+ await Promise.all(this.getAnimationPromises({ selector, direction }));
25
+ }
26
+ );
27
+
28
+ await this.hooks.trigger('animationOutDone');
27
29
  };