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.
- package/README.md +94 -0
- package/dist/Swup.cjs +1 -1
- package/dist/Swup.cjs.map +1 -1
- package/dist/Swup.modern.js +1 -1
- package/dist/Swup.modern.js.map +1 -1
- package/dist/Swup.module.js +1 -1
- package/dist/Swup.module.js.map +1 -1
- package/dist/Swup.umd.js +1 -1
- package/dist/Swup.umd.js.map +1 -1
- package/dist/types/Swup.d.ts +53 -45
- package/dist/types/helpers/Location.d.ts +10 -7
- package/dist/types/helpers/delegateEvent.d.ts +2 -2
- package/dist/types/helpers/matchPath.d.ts +3 -0
- package/dist/types/helpers.d.ts +1 -4
- package/dist/types/index.d.ts +7 -4
- package/dist/types/modules/Cache.d.ts +14 -14
- package/dist/types/modules/Classes.d.ts +13 -0
- package/dist/types/modules/Context.d.ts +73 -0
- package/dist/types/modules/Hooks.d.ts +241 -0
- package/dist/types/modules/__test__/cache.test.d.ts +1 -0
- package/dist/types/modules/__test__/hooks.test.d.ts +1 -0
- package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -0
- package/dist/types/modules/awaitAnimations.d.ts +21 -0
- package/dist/types/modules/enterPage.d.ts +5 -2
- package/dist/types/modules/fetchPage.d.ts +23 -3
- package/dist/types/modules/getAnchorElement.d.ts +2 -1
- package/dist/types/modules/leavePage.d.ts +5 -2
- package/dist/types/modules/plugins.d.ts +7 -0
- package/dist/types/modules/renderPage.d.ts +6 -6
- package/dist/types/modules/replaceContent.d.ts +8 -11
- package/dist/types/modules/visit.d.ts +33 -0
- package/dist/types/utils/index.d.ts +3 -1
- package/package.json +13 -9
- package/src/Swup.ts +172 -182
- package/src/__test__/index.test.ts +8 -3
- package/src/helpers/Location.ts +12 -9
- package/src/helpers/__test__/matchPath.test.ts +54 -0
- package/src/helpers/delegateEvent.ts +3 -2
- package/src/helpers/matchPath.ts +22 -0
- package/src/helpers.ts +2 -5
- package/src/index.ts +36 -4
- package/src/modules/Cache.ts +43 -33
- package/src/modules/Classes.ts +48 -0
- package/src/modules/Context.ts +121 -0
- package/src/modules/Hooks.ts +413 -0
- package/src/modules/__test__/cache.test.ts +142 -0
- package/src/modules/__test__/hooks.test.ts +263 -0
- package/src/modules/__test__/replaceContent.test.ts +92 -0
- package/src/modules/awaitAnimations.ts +169 -0
- package/src/modules/enterPage.ts +23 -17
- package/src/modules/fetchPage.ts +74 -29
- package/src/modules/getAnchorElement.ts +2 -1
- package/src/modules/leavePage.ts +26 -20
- package/src/modules/plugins.ts +7 -2
- package/src/modules/renderPage.ts +52 -33
- package/src/modules/replaceContent.ts +33 -16
- package/src/modules/visit.ts +143 -0
- package/src/utils/index.ts +25 -5
- package/dist/types/helpers/cleanupAnimationClasses.d.ts +0 -2
- package/dist/types/helpers/fetch.d.ts +0 -5
- package/dist/types/helpers/getDataFromHtml.d.ts +0 -7
- package/dist/types/helpers/markSwupElements.d.ts +0 -1
- package/dist/types/modules/events.d.ts +0 -33
- package/dist/types/modules/getAnimationPromises.d.ts +0 -7
- package/dist/types/modules/getPageData.d.ts +0 -6
- package/dist/types/modules/loadPage.d.ts +0 -15
- package/dist/types/modules/transitions.d.ts +0 -6
- package/readme.md +0 -60
- package/src/helpers/cleanupAnimationClasses.ts +0 -8
- package/src/helpers/fetch.ts +0 -33
- package/src/helpers/getDataFromHtml.ts +0 -39
- package/src/helpers/markSwupElements.ts +0 -16
- package/src/modules/__test__/events.test.ts +0 -72
- package/src/modules/events.ts +0 -92
- package/src/modules/getAnimationPromises.ts +0 -183
- package/src/modules/getPageData.ts +0 -24
- package/src/modules/loadPage.ts +0 -81
- package/src/modules/transitions.ts +0 -10
- /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
|
+
}
|
package/src/modules/enterPage.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import { nextTick } from '../utils.js';
|
|
2
1
|
import Swup from '../Swup.js';
|
|
3
|
-
import {
|
|
2
|
+
import { nextTick } from '../utils.js';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.
|
|
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
|
-
|
|
26
|
+
|
|
27
|
+
await animation;
|
|
28
|
+
|
|
29
|
+
await this.hooks.trigger('animation:in:end');
|
|
24
30
|
};
|
package/src/modules/fetchPage.ts
CHANGED
|
@@ -1,35 +1,80 @@
|
|
|
1
1
|
import Swup from '../Swup.js';
|
|
2
|
-
import {
|
|
3
|
-
import { TransitionOptions } from './loadPage.js';
|
|
4
|
-
import { PageRecord } from './Cache.js';
|
|
2
|
+
import { Location } from '../helpers.js';
|
|
5
3
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
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 (
|
|
11
|
-
this.
|
|
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
|
|
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) === '#') {
|