swup 3.1.1 → 4.0.0-rc.20

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 (79) hide show
  1. package/README.md +94 -0
  2. package/dist/Swup.cjs +1 -1
  3. package/dist/Swup.cjs.map +1 -1
  4. package/dist/Swup.modern.js +1 -1
  5. package/dist/Swup.modern.js.map +1 -1
  6. package/dist/Swup.module.js +1 -1
  7. package/dist/Swup.module.js.map +1 -1
  8. package/dist/Swup.umd.js +1 -1
  9. package/dist/Swup.umd.js.map +1 -1
  10. package/dist/types/Swup.d.ts +53 -45
  11. package/dist/types/helpers/Location.d.ts +10 -7
  12. package/dist/types/helpers/delegateEvent.d.ts +2 -2
  13. package/dist/types/helpers/matchPath.d.ts +3 -0
  14. package/dist/types/helpers.d.ts +1 -4
  15. package/dist/types/index.d.ts +7 -4
  16. package/dist/types/modules/Cache.d.ts +14 -14
  17. package/dist/types/modules/Classes.d.ts +13 -0
  18. package/dist/types/modules/Context.d.ts +73 -0
  19. package/dist/types/modules/Hooks.d.ts +241 -0
  20. package/dist/types/modules/__test__/cache.test.d.ts +1 -0
  21. package/dist/types/modules/__test__/hooks.test.d.ts +1 -0
  22. package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -0
  23. package/dist/types/modules/awaitAnimations.d.ts +21 -0
  24. package/dist/types/modules/enterPage.d.ts +5 -2
  25. package/dist/types/modules/fetchPage.d.ts +23 -3
  26. package/dist/types/modules/getAnchorElement.d.ts +2 -1
  27. package/dist/types/modules/leavePage.d.ts +5 -2
  28. package/dist/types/modules/plugins.d.ts +7 -0
  29. package/dist/types/modules/renderPage.d.ts +6 -6
  30. package/dist/types/modules/replaceContent.d.ts +8 -11
  31. package/dist/types/modules/visit.d.ts +33 -0
  32. package/dist/types/utils/index.d.ts +3 -1
  33. package/package.json +13 -9
  34. package/src/Swup.ts +172 -182
  35. package/src/__test__/index.test.ts +8 -3
  36. package/src/helpers/Location.ts +12 -9
  37. package/src/helpers/__test__/matchPath.test.ts +54 -0
  38. package/src/helpers/delegateEvent.ts +3 -2
  39. package/src/helpers/matchPath.ts +22 -0
  40. package/src/helpers.ts +2 -5
  41. package/src/index.ts +36 -4
  42. package/src/modules/Cache.ts +43 -33
  43. package/src/modules/Classes.ts +48 -0
  44. package/src/modules/Context.ts +121 -0
  45. package/src/modules/Hooks.ts +413 -0
  46. package/src/modules/__test__/cache.test.ts +142 -0
  47. package/src/modules/__test__/hooks.test.ts +263 -0
  48. package/src/modules/__test__/replaceContent.test.ts +92 -0
  49. package/src/modules/awaitAnimations.ts +169 -0
  50. package/src/modules/enterPage.ts +23 -17
  51. package/src/modules/fetchPage.ts +74 -29
  52. package/src/modules/getAnchorElement.ts +2 -1
  53. package/src/modules/leavePage.ts +26 -20
  54. package/src/modules/plugins.ts +7 -2
  55. package/src/modules/renderPage.ts +52 -33
  56. package/src/modules/replaceContent.ts +33 -16
  57. package/src/modules/visit.ts +143 -0
  58. package/src/utils/index.ts +25 -5
  59. package/dist/types/helpers/cleanupAnimationClasses.d.ts +0 -2
  60. package/dist/types/helpers/fetch.d.ts +0 -5
  61. package/dist/types/helpers/getDataFromHtml.d.ts +0 -7
  62. package/dist/types/helpers/markSwupElements.d.ts +0 -1
  63. package/dist/types/modules/events.d.ts +0 -33
  64. package/dist/types/modules/getAnimationPromises.d.ts +0 -7
  65. package/dist/types/modules/getPageData.d.ts +0 -6
  66. package/dist/types/modules/loadPage.d.ts +0 -15
  67. package/dist/types/modules/transitions.d.ts +0 -6
  68. package/readme.md +0 -60
  69. package/src/helpers/cleanupAnimationClasses.ts +0 -8
  70. package/src/helpers/fetch.ts +0 -33
  71. package/src/helpers/getDataFromHtml.ts +0 -39
  72. package/src/helpers/markSwupElements.ts +0 -16
  73. package/src/modules/__test__/events.test.ts +0 -72
  74. package/src/modules/events.ts +0 -92
  75. package/src/modules/getAnimationPromises.ts +0 -183
  76. package/src/modules/getPageData.ts +0 -24
  77. package/src/modules/loadPage.ts +0 -81
  78. package/src/modules/transitions.ts +0 -10
  79. /package/dist/types/{modules/__test__/events.test.d.ts → helpers/__test__/matchPath.test.d.ts} +0 -0
@@ -0,0 +1,263 @@
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 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('enable', handler);
20
+ const ledger = hooks.getRegistry().get('enable');
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 remove handlers', async () => {
33
+ const swup = new Swup();
34
+ const handler1 = vi.fn();
35
+ const handler2 = vi.fn();
36
+
37
+ swup.hooks.on('enable', handler1);
38
+ swup.hooks.on('enable', handler2);
39
+
40
+ await swup.hooks.trigger('enable');
41
+
42
+ expect(handler1).toBeCalledTimes(1);
43
+ expect(handler2).toBeCalledTimes(1);
44
+
45
+ swup.hooks.off('enable', handler2);
46
+
47
+ await swup.hooks.trigger('enable');
48
+
49
+ expect(handler1).toBeCalledTimes(2);
50
+ expect(handler2).toBeCalledTimes(1);
51
+ });
52
+
53
+ it('should return a function to unregister the handler', async () => {
54
+ const swup = new Swup();
55
+ const handler1 = vi.fn();
56
+ const handler2 = vi.fn();
57
+
58
+ const unregister1 = swup.hooks.on('enable', handler1);
59
+ const unregister2 = swup.hooks.on('enable', handler2);
60
+
61
+ expect(unregister1).toBeTypeOf('function');
62
+
63
+ await swup.hooks.trigger('enable');
64
+
65
+ expect(handler1).toBeCalledTimes(1);
66
+ expect(handler2).toBeCalledTimes(1);
67
+
68
+ unregister2();
69
+
70
+ await swup.hooks.trigger('enable');
71
+
72
+ expect(handler1).toBeCalledTimes(2);
73
+ expect(handler2).toBeCalledTimes(1);
74
+ });
75
+
76
+ it('should trigger custom handlers', async () => {
77
+ const swup = new Swup();
78
+ const handler = vi.fn();
79
+
80
+ swup.hooks.on('enable', handler);
81
+
82
+ await swup.hooks.trigger('enable');
83
+
84
+ expect(handler).toBeCalledTimes(1);
85
+ });
86
+
87
+ it('should only trigger custom handlers once if requested', async () => {
88
+ const swup = new Swup();
89
+ const handler = vi.fn();
90
+
91
+ swup.hooks.on('enable', handler, { once: true });
92
+
93
+ await swup.hooks.trigger('enable', undefined, () => {});
94
+ await swup.hooks.trigger('enable', undefined, () => {});
95
+
96
+ expect(handler).toBeCalledTimes(1);
97
+ });
98
+
99
+ it('should only trigger custom handlers once if using alias', async () => {
100
+ const swup = new Swup();
101
+ const handler = vi.fn();
102
+
103
+ swup.hooks.once('enable', handler);
104
+
105
+ await swup.hooks.trigger('enable', undefined, () => {});
106
+ await swup.hooks.trigger('enable', undefined, () => {});
107
+
108
+ expect(handler).toBeCalledTimes(1);
109
+ });
110
+
111
+ it('should trigger original handlers', async () => {
112
+ const swup = new Swup();
113
+ const handler = vi.fn();
114
+
115
+ await swup.hooks.trigger('enable', undefined, handler);
116
+
117
+ expect(handler).toBeCalledTimes(1);
118
+ });
119
+
120
+ it('should allow triggering custom handlers before original handler', async () => {
121
+ const swup = new Swup();
122
+
123
+ let called: Array<string> = [];
124
+ const handlers = {
125
+ before: () => {
126
+ called.push('before');
127
+ },
128
+ original: () => {
129
+ called.push('original');
130
+ },
131
+ normal: () => {
132
+ called.push('normal');
133
+ },
134
+ after: () => {
135
+ called.push('after');
136
+ }
137
+ };
138
+
139
+ swup.hooks.on('disable', handlers.before, { before: true });
140
+ swup.hooks.on('disable', handlers.normal, {});
141
+ swup.hooks.on('disable', handlers.after, {});
142
+
143
+ await swup.hooks.trigger('disable', undefined, handlers.original);
144
+
145
+ expect(called).toEqual(['before', 'original', 'normal', 'after']);
146
+ });
147
+
148
+ it('should sort custom handlers by priority', async () => {
149
+ const swup = new Swup();
150
+
151
+ let called: Array<number> = [];
152
+ const handlers = {
153
+ 1: () => {
154
+ called.push(1);
155
+ },
156
+ 2: () => {
157
+ called.push(2);
158
+ },
159
+ 3: () => {
160
+ called.push(3);
161
+ },
162
+ 4: () => {
163
+ called.push(4);
164
+ },
165
+ 5: () => {
166
+ called.push(5);
167
+ },
168
+ 6: () => {
169
+ called.push(6);
170
+ },
171
+ 7: () => {
172
+ called.push(7);
173
+ },
174
+ 8: () => {
175
+ called.push(8);
176
+ }
177
+ };
178
+
179
+ swup.hooks.on('disable', handlers['1'], { priority: 2, before: true });
180
+ swup.hooks.on('disable', handlers['2'], { priority: -1, before: true });
181
+ swup.hooks.on('disable', handlers['3'], { priority: 1 });
182
+ swup.hooks.on('disable', handlers['4']);
183
+ swup.hooks.on('disable', handlers['8'], { priority: 4 });
184
+ swup.hooks.on('disable', handlers['7'], { priority: 4 });
185
+
186
+ await swup.hooks.trigger('disable', undefined, handlers['5']);
187
+
188
+ expect(called).toEqual([2, 1, 5, 4, 3, 8, 7]);
189
+ });
190
+
191
+ it('should allow replacing original handlers', async () => {
192
+ const swup = new Swup();
193
+ const customHandler = vi.fn();
194
+ const defaultHandler = vi.fn();
195
+
196
+ swup.hooks.on('enable', customHandler, { replace: true });
197
+
198
+ await swup.hooks.trigger('enable', undefined, defaultHandler);
199
+
200
+ expect(customHandler).toBeCalledTimes(1);
201
+ expect(defaultHandler).toBeCalledTimes(0);
202
+ });
203
+
204
+ it('should pass original handler into replacing handlers', async () => {
205
+ const swup = new Swup();
206
+ const customHandler = vi.fn();
207
+ const defaultHandler = vi.fn();
208
+ const ctx = swup.context;
209
+
210
+ swup.hooks.on('enable', customHandler, { replace: true });
211
+
212
+ await swup.hooks.trigger('enable', undefined, defaultHandler);
213
+
214
+ expect(customHandler).toBeCalledWith(ctx, undefined, defaultHandler);
215
+ });
216
+
217
+ it('should not pass original handler into normal handlers', async () => {
218
+ const swup = new Swup();
219
+ const listener = vi.fn();
220
+ const handler = vi.fn();
221
+ const ctx = swup.context;
222
+
223
+ swup.hooks.on('enable', listener);
224
+
225
+ await swup.hooks.trigger('enable', undefined, handler);
226
+
227
+ expect(listener).toBeCalledWith(ctx, undefined, undefined);
228
+ });
229
+
230
+ it('should trigger event handler with context and args', async () => {
231
+ const swup = new Swup();
232
+ const handler: Handler<'history:popstate'> = vi.fn();
233
+ const ctx = swup.context;
234
+ const args = { event: new PopStateEvent('') };
235
+
236
+ swup.hooks.on('history:popstate', handler);
237
+ await swup.hooks.trigger('history:popstate', args);
238
+
239
+ expect(handler).toBeCalledTimes(1);
240
+ expect(handler).toBeCalledWith(ctx, args, undefined);
241
+ });
242
+ });
243
+
244
+ describe('Types', () => {
245
+ it('error when necessary', async () => {
246
+ const swup = new Swup();
247
+
248
+ // @ts-expect-no-error
249
+ swup.hooks.on(
250
+ 'history:popstate',
251
+ (ctx: Context, { event }: { event: PopStateEvent }) => {}
252
+ );
253
+ // @ts-expect-no-error
254
+ await swup.hooks.trigger('history:popstate', { event: new PopStateEvent('') });
255
+
256
+ // @ts-expect-error
257
+ swup.hooks.on('history:popstate', ({ event: MouseEvent }) => {});
258
+ // @ts-expect-error
259
+ swup.hooks.on('history:popstate', (ctx: Context, { event }: { event: MouseEvent }) => {});
260
+ // @ts-expect-error
261
+ await swup.hooks.trigger('history:popstate', { event: new MouseEvent('') });
262
+ });
263
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import Swup from '../../Swup.js';
3
+ import type { PageData } from '../fetchPage.js';
4
+ import { JSDOM } from 'jsdom';
5
+
6
+ const getHtml = (body: string): string => {
7
+ return /*html*/ `
8
+ <!DOCTYPE html>
9
+ <body>
10
+ ${body}
11
+ </body>
12
+ `;
13
+ };
14
+
15
+ const mockPage = (body: string): PageData => {
16
+ return {
17
+ url: '',
18
+ html: getHtml(body)
19
+ };
20
+ };
21
+
22
+ const stubGlobalDocument = (body: string): void => {
23
+ const dom = new JSDOM(getHtml(body));
24
+ vi.stubGlobal('document', dom.window.document);
25
+ };
26
+
27
+ describe('replaceContent', () => {
28
+ afterEach(() => {
29
+ vi.unstubAllGlobals();
30
+ });
31
+
32
+ it('should replace containers', () => {
33
+ stubGlobalDocument(/*html*/ `
34
+ <div id="container-1" data-from="current"></div>
35
+ <div id="container-2" data-from="current"></div>
36
+ <div id="container-3" data-from="current"></div>
37
+ `);
38
+
39
+ console.debug(document.documentElement.querySelector('#container-1'));
40
+ const page = mockPage(/*html*/ `
41
+ <div id="container-1" data-from="incoming"></div>
42
+ <div id="container-2" data-from="incoming"></div>`);
43
+ const swup = new Swup();
44
+
45
+ const result = swup.replaceContent(page, { containers: ['#container-1', '#container-2'] });
46
+
47
+ expect(result).toBe(true);
48
+ expect(document.querySelector('#container-1')?.getAttribute('data-from')).toBe('incoming');
49
+ expect(document.querySelector('#container-2')?.getAttribute('data-from')).toBe('incoming');
50
+ expect(document.querySelector('#container-3')?.getAttribute('data-from')).toBe('current');
51
+ });
52
+
53
+ it('should handle missing containers in current DOM', () => {
54
+ stubGlobalDocument(/*html*/ `
55
+ <div id="container-1" data-from="current"></div>
56
+ `);
57
+ const warn = vi.spyOn(console, 'warn');
58
+ const page = mockPage(/*html*/ `
59
+ <div id="container-1" data-from="incoming"></div>
60
+ <div id="container-2" data-from="incoming"></div>
61
+ `);
62
+
63
+ const swup = new Swup();
64
+ const result = swup.replaceContent(page, { containers: ['#container-1', '#missing'] });
65
+
66
+ expect(result).toBe(false);
67
+ expect(warn).not.toBeCalledWith(
68
+ '[swup] Container missing in current document: #container-1'
69
+ );
70
+ expect(warn).toBeCalledWith('[swup] Container missing in current document: #missing');
71
+ });
72
+
73
+ it('should handle missing containers in incoming DOM', () => {
74
+ stubGlobalDocument(/*html*/ `
75
+ <div id="container-1" data-from="current"></div>
76
+ <div id="container-2" data-from="current"></div>
77
+ <div id="container-3" data-from="current"></div>
78
+ `);
79
+ const warn = vi.spyOn(console, 'warn');
80
+ const page = mockPage(/*html*/ `
81
+ <div id="container-1" data-from="incoming"></div>`);
82
+
83
+ const swup = new Swup();
84
+ const result = swup.replaceContent(page, { containers: ['#container-1', '#missing'] });
85
+
86
+ expect(result).toBe(false);
87
+ expect(warn).not.toBeCalledWith(
88
+ '[swup] Container missing in incoming document: #container-1'
89
+ );
90
+ expect(warn).toBeCalledWith('[swup] Container missing in incoming document: #missing');
91
+ });
92
+ });
@@ -0,0 +1,169 @@
1
+ import { queryAll, toMs } from '../utils.js';
2
+ import Swup, { Options } from '../Swup.js';
3
+
4
+ const TRANSITION = 'transition';
5
+ const ANIMATION = 'animation';
6
+
7
+ type AnimationTypes = typeof TRANSITION | typeof ANIMATION;
8
+ type AnimationProperties = 'Delay' | 'Duration';
9
+ type AnimationStyleKeys = `${AnimationTypes}${AnimationProperties}` | 'transitionProperty';
10
+ type AnimationStyleDeclarations = Pick<CSSStyleDeclaration, AnimationStyleKeys>;
11
+
12
+ export type AnimationDirection = 'in' | 'out';
13
+
14
+ /**
15
+ * Return a Promise that resolves when all animations are done on the page.
16
+ *
17
+ * @note We don't make use of the `direction` argument, but it's required by JS plugin
18
+ */
19
+ export async function awaitAnimations(
20
+ this: Swup,
21
+ {
22
+ elements,
23
+ selector
24
+ }: {
25
+ selector: Options['animationSelector'];
26
+ elements?: NodeListOf<HTMLElement> | HTMLElement[];
27
+ direction?: AnimationDirection;
28
+ }
29
+ ): Promise<void> {
30
+ // Allow usage of swup without animations
31
+ if (selector === false && !elements) {
32
+ return;
33
+ }
34
+
35
+ // Allow passing in elements
36
+ let animatedElements: HTMLElement[] = [];
37
+ if (elements) {
38
+ animatedElements = Array.from(elements);
39
+ } else if (selector) {
40
+ animatedElements = queryAll(selector, document.body);
41
+ // Warn if no elements match the selector, but keep things going
42
+ if (!animatedElements.length) {
43
+ console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
44
+ return;
45
+ }
46
+ }
47
+
48
+ const awaitedAnimations = animatedElements.map((el) => awaitAnimationsOnElement(el));
49
+ const hasAnimations = awaitedAnimations.filter(Boolean).length > 0;
50
+ if (!hasAnimations) {
51
+ if (selector) {
52
+ console.warn(
53
+ `[swup] No CSS animation duration defined on elements matching \`${selector}\``
54
+ );
55
+ }
56
+ return;
57
+ }
58
+
59
+ await Promise.all(awaitedAnimations);
60
+ }
61
+
62
+ function awaitAnimationsOnElement(element: Element): Promise<void> | false {
63
+ const { type, timeout, propCount } = getTransitionInfo(element);
64
+
65
+ // Resolve immediately if no transition defined
66
+ if (!type || !timeout) {
67
+ return false;
68
+ }
69
+
70
+ return new Promise((resolve) => {
71
+ const endEvent = `${type}end`;
72
+ const startTime = performance.now();
73
+ let propsTransitioned = 0;
74
+
75
+ const end = () => {
76
+ element.removeEventListener(endEvent, onEnd);
77
+ resolve();
78
+ };
79
+
80
+ const onEnd: EventListener = (event) => {
81
+ // Skip transitions on child elements
82
+ if (event.target !== element) {
83
+ return;
84
+ }
85
+
86
+ if (!isTransitionOrAnimationEvent(event)) {
87
+ throw new Error('Not a transition or animation event.');
88
+ }
89
+
90
+ // Skip transitions that happened before we started listening
91
+ const elapsedTime = (performance.now() - startTime) / 1000;
92
+ if (elapsedTime < event.elapsedTime) {
93
+ return;
94
+ }
95
+
96
+ // End if all properties have transitioned
97
+ if (++propsTransitioned >= propCount) {
98
+ end();
99
+ }
100
+ };
101
+
102
+ setTimeout(() => {
103
+ if (propsTransitioned < propCount) {
104
+ end();
105
+ }
106
+ }, timeout + 1);
107
+
108
+ element.addEventListener(endEvent, onEnd);
109
+ });
110
+ }
111
+
112
+ export function getTransitionInfo(element: Element, expectedType?: AnimationTypes) {
113
+ const styles = window.getComputedStyle(element) as AnimationStyleDeclarations;
114
+
115
+ const transitionDelays = getStyleProperties(styles, `${TRANSITION}Delay`);
116
+ const transitionDurations = getStyleProperties(styles, `${TRANSITION}Duration`);
117
+ const transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);
118
+ const animationDelays = getStyleProperties(styles, `${ANIMATION}Delay`);
119
+ const animationDurations = getStyleProperties(styles, `${ANIMATION}Duration`);
120
+ const animationTimeout = calculateTimeout(animationDelays, animationDurations);
121
+
122
+ let type: AnimationTypes | null = null;
123
+ let timeout = 0;
124
+ let propCount = 0;
125
+
126
+ if (expectedType === TRANSITION) {
127
+ if (transitionTimeout > 0) {
128
+ type = TRANSITION;
129
+ timeout = transitionTimeout;
130
+ propCount = transitionDurations.length;
131
+ }
132
+ } else if (expectedType === ANIMATION) {
133
+ if (animationTimeout > 0) {
134
+ type = ANIMATION;
135
+ timeout = animationTimeout;
136
+ propCount = animationDurations.length;
137
+ }
138
+ } else {
139
+ timeout = Math.max(transitionTimeout, animationTimeout);
140
+ type = timeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null;
141
+ propCount = type
142
+ ? type === TRANSITION
143
+ ? transitionDurations.length
144
+ : animationDurations.length
145
+ : 0;
146
+ }
147
+
148
+ return {
149
+ type,
150
+ timeout,
151
+ propCount
152
+ };
153
+ }
154
+
155
+ function isTransitionOrAnimationEvent(event: any): event is TransitionEvent | AnimationEvent {
156
+ return [`${TRANSITION}end`, `${ANIMATION}end`].includes(event.type);
157
+ }
158
+
159
+ function getStyleProperties(styles: AnimationStyleDeclarations, key: AnimationStyleKeys): string[] {
160
+ return (styles[key] || '').split(', ');
161
+ }
162
+
163
+ function calculateTimeout(delays: string[], durations: string[]): number {
164
+ while (delays.length < durations.length) {
165
+ delays = delays.concat(delays);
166
+ }
167
+
168
+ return Math.max(...durations.map((duration, i) => toMs(duration) + toMs(delays[i])));
169
+ }
@@ -1,24 +1,30 @@
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
+ /**
5
+ * Perform the in/enter animation of the next page.
6
+ * @returns Promise<void>
7
+ */
8
+ export const enterPage = async function (this: Swup) {
9
+ if (!this.context.animation.animate) {
10
+ return;
10
11
  }
11
12
 
12
- nextTick(() => {
13
- this.triggerEvent('animationInStart');
14
- document.documentElement.classList.remove('is-animating');
15
- });
13
+ const animation = this.hooks.trigger(
14
+ 'animation:await',
15
+ { direction: 'in' },
16
+ async (context, { direction }) => {
17
+ await this.awaitAnimations({ selector: context.animation.selector, direction });
18
+ }
19
+ );
16
20
 
17
- const animationPromises = this.getAnimationPromises('in');
18
- Promise.all(animationPromises).then(() => {
19
- this.triggerEvent('animationInDone');
20
- this.triggerEvent('transitionEnd', event);
21
- this.cleanupAnimationClasses();
21
+ await nextTick();
22
+
23
+ await this.hooks.trigger('animation:in:start', undefined, () => {
24
+ this.classes.remove('is-animating');
22
25
  });
23
- return animationPromises;
26
+
27
+ await animation;
28
+
29
+ await this.hooks.trigger('animation:in:end');
24
30
  };
@@ -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
+ url = Location.fromUrl(url).url;
35
+
36
+ if (this.cache.has(url)) {
37
+ const page = this.cache.get(url) as PageData;
38
+ if (options.triggerHooks !== false) {
39
+ await this.hooks.trigger('page:load', { 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: Response = await this.hooks.trigger(
49
+ 'fetch:request',
50
+ { url, options },
51
+ (context, { url, options }) => 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('fetch:error', { 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 } = Location.fromUrl(responseUrl);
68
+ const page = { url: finalUrl, html };
69
+
70
+ // Only save cache entry for non-redirects
71
+ if (url === 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('page:load', { 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
  }
@@ -2,10 +2,11 @@ import { escapeCssIdentifier as escape, query } from '../utils.js';
2
2
 
3
3
  /**
4
4
  * Find the anchor element for a given hash.
5
- * @see https://html.spec.whatwg.org/#find-a-potential-indicated-element
6
5
  *
7
6
  * @param hash Hash with or without leading '#'
8
7
  * @returns The element, if found, or null.
8
+ *
9
+ * @see https://html.spec.whatwg.org/#find-a-potential-indicated-element
9
10
  */
10
11
  export const getAnchorElement = (hash: string): Element | null => {
11
12
  if (hash && hash.charAt(0) === '#') {