swup 4.0.0-rc.14 → 4.0.0-rc.21
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 +100 -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 +62 -54
- package/dist/types/helpers/Location.d.ts +10 -7
- package/dist/types/helpers/delegateEvent.d.ts +3 -5
- package/dist/types/helpers/matchPath.d.ts +3 -0
- package/dist/types/helpers.d.ts +7 -10
- package/dist/types/index.d.ts +9 -6
- package/dist/types/modules/Cache.d.ts +15 -15
- 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__/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 +6 -3
- package/dist/types/modules/fetchPage.d.ts +24 -4
- package/dist/types/modules/getAnchorElement.d.ts +8 -0
- package/dist/types/modules/leavePage.d.ts +6 -3
- package/dist/types/modules/plugins.d.ts +12 -5
- package/dist/types/modules/renderPage.d.ts +7 -7
- 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/dist/types/utils.d.ts +1 -1
- package/package.json +7 -6
- package/src/Swup.ts +83 -65
- package/src/__test__/index.test.ts +3 -3
- package/src/helpers/Location.ts +2 -2
- package/src/helpers/delegateEvent.ts +2 -2
- package/src/helpers.ts +0 -1
- package/src/index.ts +34 -4
- package/src/modules/Cache.ts +2 -2
- package/src/modules/Classes.ts +48 -0
- package/src/modules/Context.ts +49 -19
- package/src/modules/Hooks.ts +103 -83
- package/src/modules/__test__/cache.test.ts +6 -6
- package/src/modules/__test__/hooks.test.ts +111 -40
- package/src/modules/__test__/replaceContent.test.ts +92 -0
- package/src/modules/{getAnimationPromises.ts → awaitAnimations.ts} +13 -18
- package/src/modules/enterPage.ts +21 -17
- package/src/modules/fetchPage.ts +12 -12
- package/src/modules/getAnchorElement.ts +2 -1
- package/src/modules/leavePage.ts +16 -12
- package/src/modules/plugins.ts +11 -8
- package/src/modules/renderPage.ts +28 -18
- package/src/modules/replaceContent.ts +24 -16
- package/src/modules/visit.ts +143 -0
- package/src/utils/index.ts +1 -2
- 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/destroy.d.ts +0 -2
- package/dist/types/modules/enable.d.ts +0 -2
- 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/handleLinkToSamePage.d.ts +0 -2
- package/dist/types/modules/isSameResolvedUrl.d.ts +0 -8
- package/dist/types/modules/linkClickHandler.d.ts +0 -3
- package/dist/types/modules/loadPage.d.ts +0 -12
- package/dist/types/modules/off.d.ts +0 -3
- package/dist/types/modules/on.d.ts +0 -5
- package/dist/types/modules/popStateHandler.d.ts +0 -2
- package/dist/types/modules/resolveUrl.d.ts +0 -7
- package/dist/types/modules/shouldIgnoreVisit.d.ts +0 -4
- package/dist/types/modules/transitions.d.ts +0 -6
- package/dist/types/modules/triggerEvent.d.ts +0 -3
- package/dist/types/modules/triggerWillOpenNewWindow.d.ts +0 -2
- package/dist/types/modules/updateTransition.d.ts +0 -2
- package/readme.md +0 -78
- package/src/helpers/cleanupAnimationClasses.ts +0 -8
- package/src/modules/loadPage.ts +0 -99
- /package/dist/types/{modules/__test__/events.test.d.ts → helpers/__test__/matchPath.test.d.ts} +0 -0
- /package/dist/types/modules/__test__/{fetchPage.test.d.ts → cache.test.d.ts} +0 -0
|
@@ -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
|
+
});
|
|
@@ -12,10 +12,11 @@ type AnimationStyleDeclarations = Pick<CSSStyleDeclaration, AnimationStyleKeys>;
|
|
|
12
12
|
export type AnimationDirection = 'in' | 'out';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Return a Promise that resolves when all animations are done on the page.
|
|
16
|
+
*
|
|
16
17
|
* @note We don't make use of the `direction` argument, but it's required by JS plugin
|
|
17
18
|
*/
|
|
18
|
-
export function
|
|
19
|
+
export async function awaitAnimations(
|
|
19
20
|
this: Swup,
|
|
20
21
|
{
|
|
21
22
|
elements,
|
|
@@ -25,14 +26,10 @@ export function getAnimationPromises(
|
|
|
25
26
|
elements?: NodeListOf<HTMLElement> | HTMLElement[];
|
|
26
27
|
direction?: AnimationDirection;
|
|
27
28
|
}
|
|
28
|
-
): Promise<void>
|
|
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()];
|
|
32
|
-
|
|
29
|
+
): Promise<void> {
|
|
33
30
|
// Allow usage of swup without animations
|
|
34
31
|
if (selector === false && !elements) {
|
|
35
|
-
return
|
|
32
|
+
return;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
// Allow passing in elements
|
|
@@ -44,32 +41,30 @@ export function getAnimationPromises(
|
|
|
44
41
|
// Warn if no elements match the selector, but keep things going
|
|
45
42
|
if (!animatedElements.length) {
|
|
46
43
|
console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
|
|
47
|
-
return
|
|
44
|
+
return;
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!animationPromises.length) {
|
|
48
|
+
const awaitedAnimations = animatedElements.map((el) => awaitAnimationsOnElement(el));
|
|
49
|
+
const hasAnimations = awaitedAnimations.filter(Boolean).length > 0;
|
|
50
|
+
if (!hasAnimations) {
|
|
56
51
|
if (selector) {
|
|
57
52
|
console.warn(
|
|
58
53
|
`[swup] No CSS animation duration defined on elements matching \`${selector}\``
|
|
59
54
|
);
|
|
60
55
|
}
|
|
61
|
-
return
|
|
56
|
+
return;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
await Promise.all(awaitedAnimations);
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
function
|
|
62
|
+
function awaitAnimationsOnElement(element: Element): Promise<void> | false {
|
|
68
63
|
const { type, timeout, propCount } = getTransitionInfo(element);
|
|
69
64
|
|
|
70
65
|
// Resolve immediately if no transition defined
|
|
71
66
|
if (!type || !timeout) {
|
|
72
|
-
return
|
|
67
|
+
return false;
|
|
73
68
|
}
|
|
74
69
|
|
|
75
70
|
return new Promise((resolve) => {
|
package/src/modules/enterPage.ts
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import Swup from '../Swup.js';
|
|
2
2
|
import { nextTick } from '../utils.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Perform the in/enter animation of the next page.
|
|
6
|
+
* @returns Promise<void>
|
|
7
|
+
*/
|
|
4
8
|
export const enterPage = async function (this: Swup) {
|
|
5
|
-
if (this.context.
|
|
6
|
-
|
|
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');
|
|
9
|
+
if (!this.context.animation.animate) {
|
|
10
|
+
return;
|
|
19
11
|
}
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
);
|
|
20
|
+
|
|
21
|
+
await nextTick();
|
|
22
|
+
|
|
23
|
+
await this.hooks.trigger('animation:in:start', undefined, () => {
|
|
24
|
+
this.classes.remove('is-animating');
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
await animation;
|
|
28
|
+
|
|
29
|
+
await this.hooks.trigger('animation:in:end');
|
|
26
30
|
};
|
package/src/modules/fetchPage.ts
CHANGED
|
@@ -31,12 +31,12 @@ export async function fetchPage(
|
|
|
31
31
|
url: URL | string,
|
|
32
32
|
options: FetchOptions & { triggerHooks?: boolean } = {}
|
|
33
33
|
): Promise<PageData> {
|
|
34
|
-
|
|
34
|
+
url = Location.fromUrl(url).url;
|
|
35
35
|
|
|
36
|
-
if (this.cache.has(
|
|
37
|
-
const page = this.cache.get(
|
|
36
|
+
if (this.cache.has(url)) {
|
|
37
|
+
const page = this.cache.get(url) as PageData;
|
|
38
38
|
if (options.triggerHooks !== false) {
|
|
39
|
-
await this.hooks.trigger('
|
|
39
|
+
await this.hooks.trigger('page:load', { page, cache: true });
|
|
40
40
|
}
|
|
41
41
|
return page;
|
|
42
42
|
}
|
|
@@ -45,17 +45,17 @@ export async function fetchPage(
|
|
|
45
45
|
options = { ...options, headers };
|
|
46
46
|
|
|
47
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
|
-
'
|
|
50
|
-
{ url
|
|
51
|
-
|
|
48
|
+
const response: Response = await this.hooks.trigger(
|
|
49
|
+
'fetch:request',
|
|
50
|
+
{ url, options },
|
|
51
|
+
(context, { url, options }) => fetch(url, options)
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
const { status, url: responseUrl } = response;
|
|
55
55
|
const html = await response.text();
|
|
56
56
|
|
|
57
57
|
if (status === 500) {
|
|
58
|
-
this.hooks.trigger('
|
|
58
|
+
this.hooks.trigger('fetch:error', { status, response, url: responseUrl });
|
|
59
59
|
throw new FetchError(`Server error: ${responseUrl}`, { status, url: responseUrl });
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -64,16 +64,16 @@ export async function fetchPage(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Resolve real url after potential redirect
|
|
67
|
-
const { url: finalUrl } =
|
|
67
|
+
const { url: finalUrl } = Location.fromUrl(responseUrl);
|
|
68
68
|
const page = { url: finalUrl, html };
|
|
69
69
|
|
|
70
70
|
// Only save cache entry for non-redirects
|
|
71
|
-
if (
|
|
71
|
+
if (url === finalUrl) {
|
|
72
72
|
this.cache.set(page.url, page);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
if (options.triggerHooks !== false) {
|
|
76
|
-
await this.hooks.trigger('
|
|
76
|
+
await this.hooks.trigger('page:load', { page, cache: false });
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
return page;
|
|
@@ -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) === '#') {
|
package/src/modules/leavePage.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import Swup from '../Swup.js';
|
|
2
2
|
import { classify } from '../helpers.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Perform the out/leave animation of the current page.
|
|
6
|
+
* @returns Promise<void>
|
|
7
|
+
*/
|
|
4
8
|
export const leavePage = async function (this: Swup) {
|
|
5
|
-
if (!this.context.
|
|
6
|
-
await this.hooks.trigger('
|
|
9
|
+
if (!this.context.animation.animate) {
|
|
10
|
+
await this.hooks.trigger('animation:skip');
|
|
7
11
|
return;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
await this.hooks.trigger('
|
|
11
|
-
|
|
14
|
+
await this.hooks.trigger('animation:out:start', undefined, () => {
|
|
15
|
+
this.classes.add('is-changing', 'is-leaving', 'is-animating');
|
|
12
16
|
if (this.context.history.popstate) {
|
|
13
|
-
|
|
17
|
+
this.classes.add('is-popstate');
|
|
14
18
|
}
|
|
15
|
-
if (this.context.
|
|
16
|
-
|
|
19
|
+
if (this.context.animation.name) {
|
|
20
|
+
this.classes.add(`to-${classify(this.context.animation.name)}`);
|
|
17
21
|
}
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
await this.hooks.trigger(
|
|
21
|
-
'
|
|
22
|
-
{
|
|
23
|
-
async (context, {
|
|
24
|
-
await
|
|
25
|
+
'animation:await',
|
|
26
|
+
{ direction: 'out' },
|
|
27
|
+
async (context, { direction }) => {
|
|
28
|
+
await this.awaitAnimations({ selector: context.animation.selector, direction });
|
|
25
29
|
}
|
|
26
30
|
);
|
|
27
31
|
|
|
28
|
-
await this.hooks.trigger('
|
|
32
|
+
await this.hooks.trigger('animation:out:end');
|
|
29
33
|
};
|
package/src/modules/plugins.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import Swup from '../Swup.js';
|
|
2
2
|
|
|
3
3
|
export type Plugin = {
|
|
4
|
-
|
|
4
|
+
/** Identify as a swup plugin */
|
|
5
5
|
isSwupPlugin: true;
|
|
6
|
+
/** Name of this plugin */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Version of this plugin. Currently not in use, defined here for backward compatiblity. */
|
|
9
|
+
version?: string;
|
|
10
|
+
/** The swup instance that mounted this plugin */
|
|
11
|
+
swup?: Swup;
|
|
12
|
+
/** Version requirements of this plugin. Example: `{ swup: '>=4' }` */
|
|
13
|
+
requires?: Record<string, string | string[]>;
|
|
14
|
+
/** Run on mount */
|
|
6
15
|
mount: () => void;
|
|
16
|
+
/** Run on unmount */
|
|
7
17
|
unmount: () => void;
|
|
8
|
-
|
|
9
|
-
// the instance is assigned later on after passing to swup
|
|
10
|
-
swup?: Swup;
|
|
11
|
-
|
|
12
|
-
// these are possibly undefined for backward compatibility
|
|
13
|
-
version?: string;
|
|
14
|
-
requires?: Record<string, string>;
|
|
15
18
|
_beforeMount?: () => void;
|
|
16
19
|
_afterUnmount?: () => void;
|
|
17
20
|
_checkRequirements?: () => boolean;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { updateHistoryRecord, getCurrentUrl } from '../helpers.js';
|
|
1
|
+
import { updateHistoryRecord, getCurrentUrl, classify } from '../helpers.js';
|
|
2
2
|
import Swup from '../Swup.js';
|
|
3
3
|
import { PageData } from './fetchPage.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Render the next page: replace the content and update scroll position.
|
|
7
|
+
* @returns Promise<void>
|
|
8
|
+
*/
|
|
5
9
|
export const renderPage = async function (this: Swup, requestedUrl: string, page: PageData) {
|
|
6
|
-
const { url } = page;
|
|
10
|
+
const { url, html } = page;
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
this.classes.remove('is-leaving');
|
|
9
13
|
|
|
10
14
|
// do nothing if another page was requested in the meantime
|
|
11
15
|
if (!this.isSameResolvedUrl(getCurrentUrl(), requestedUrl)) {
|
|
@@ -16,25 +20,34 @@ export const renderPage = async function (this: Swup, requestedUrl: string, page
|
|
|
16
20
|
if (!this.isSameResolvedUrl(getCurrentUrl(), url)) {
|
|
17
21
|
updateHistoryRecord(url);
|
|
18
22
|
this.currentPageUrl = getCurrentUrl();
|
|
19
|
-
this.context.to
|
|
23
|
+
this.context.to.url = this.currentPageUrl;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
// only add for page loads
|
|
23
|
-
if (this.context.
|
|
24
|
-
|
|
26
|
+
// only add for animated page loads
|
|
27
|
+
if (this.context.animation.animate) {
|
|
28
|
+
this.classes.add('is-rendering');
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
// save html into context for easier retrieval
|
|
32
|
+
this.context.to.html = html;
|
|
33
|
+
|
|
27
34
|
// replace content: allow handlers and plugins to overwrite paga data and containers
|
|
28
|
-
await this.hooks.trigger(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.replaceContent(page, { containers });
|
|
35
|
+
await this.hooks.trigger('content:replace', { page }, (context, { page }) => {
|
|
36
|
+
const success = this.replaceContent(page, { containers: context.containers });
|
|
37
|
+
if (!success) {
|
|
38
|
+
throw new Error('[swup] Container mismatch, aborting');
|
|
33
39
|
}
|
|
34
|
-
|
|
40
|
+
if (this.context.animation.animate) {
|
|
41
|
+
// Make sure to add these classes to new containers as well
|
|
42
|
+
this.classes.add('is-animating', 'is-changing', 'is-rendering');
|
|
43
|
+
if (this.context.animation.name) {
|
|
44
|
+
this.classes.add(`to-${classify(this.context.animation.name)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
35
48
|
|
|
36
49
|
await this.hooks.trigger(
|
|
37
|
-
'
|
|
50
|
+
'content:scroll',
|
|
38
51
|
{ options: { behavior: 'auto' } },
|
|
39
52
|
(context, { options }) => {
|
|
40
53
|
if (this.context.scroll.target) {
|
|
@@ -50,13 +63,10 @@ export const renderPage = async function (this: Swup, requestedUrl: string, page
|
|
|
50
63
|
}
|
|
51
64
|
);
|
|
52
65
|
|
|
53
|
-
await this.hooks.trigger('
|
|
66
|
+
await this.hooks.trigger('page:view', { url: this.currentPageUrl, title: document.title });
|
|
54
67
|
|
|
55
68
|
// empty cache if it's disabled (in case preload plugin filled it)
|
|
56
69
|
if (!this.options.cache) {
|
|
57
70
|
this.cache.clear();
|
|
58
71
|
}
|
|
59
|
-
|
|
60
|
-
// Perform in transition
|
|
61
|
-
this.enterPage();
|
|
62
72
|
};
|
|
@@ -6,30 +6,38 @@ import { PageData } from './fetchPage.js';
|
|
|
6
6
|
*
|
|
7
7
|
* It takes an object with the page data as returned from `fetchPage` and a list
|
|
8
8
|
* of container selectors to replace.
|
|
9
|
+
*
|
|
10
|
+
* @returns Whether all containers were replaced.
|
|
9
11
|
*/
|
|
10
12
|
export const replaceContent = function (
|
|
11
13
|
this: Swup,
|
|
12
14
|
{ html }: PageData,
|
|
13
15
|
{ containers }: { containers: Options['containers'] } = this.options
|
|
14
|
-
):
|
|
15
|
-
const
|
|
16
|
+
): boolean {
|
|
17
|
+
const incomingDocument = new DOMParser().parseFromString(html, 'text/html');
|
|
16
18
|
|
|
17
19
|
// Update browser title
|
|
18
|
-
const title =
|
|
20
|
+
const title = incomingDocument.querySelector('title')?.innerText || '';
|
|
19
21
|
document.title = title;
|
|
20
22
|
|
|
21
23
|
// Update content containers
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
const replaced = containers
|
|
25
|
+
.map((selector) => {
|
|
26
|
+
const currentEl = document.querySelector(selector);
|
|
27
|
+
const incomingEl = incomingDocument.querySelector(selector);
|
|
28
|
+
if (currentEl && incomingEl) {
|
|
29
|
+
currentEl.replaceWith(incomingEl);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (!currentEl) {
|
|
33
|
+
console.warn(`[swup] Container missing in current document: ${selector}`);
|
|
34
|
+
}
|
|
35
|
+
if (!incomingEl) {
|
|
36
|
+
console.warn(`[swup] Container missing in incoming document: ${selector}`);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
|
|
42
|
+
return replaced.length === containers.length;
|
|
35
43
|
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Swup from '../Swup.js';
|
|
2
|
+
import { createHistoryRecord, updateHistoryRecord, getCurrentUrl, Location } from '../helpers.js';
|
|
3
|
+
import { FetchOptions } from './fetchPage.js';
|
|
4
|
+
import { ContextInitOptions } from './Context.js';
|
|
5
|
+
|
|
6
|
+
export type HistoryAction = 'push' | 'replace';
|
|
7
|
+
export type HistoryDirection = 'forwards' | 'backwards';
|
|
8
|
+
|
|
9
|
+
type VisitOptions = {
|
|
10
|
+
/** Whether this visit is animated. Default: `true` */
|
|
11
|
+
animate?: boolean;
|
|
12
|
+
/** Name of a custom animation to run. */
|
|
13
|
+
animation?: string;
|
|
14
|
+
/** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
|
|
15
|
+
history?: HistoryAction;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Navigate to a new URL.
|
|
20
|
+
* @param url The URL to navigate to.
|
|
21
|
+
* @param options Options for how to perform this visit.
|
|
22
|
+
* @returns Promise<void>
|
|
23
|
+
*/
|
|
24
|
+
export function visit(
|
|
25
|
+
this: Swup,
|
|
26
|
+
url: string,
|
|
27
|
+
options: VisitOptions & FetchOptions = {},
|
|
28
|
+
context: Omit<ContextInitOptions, 'to'> = {}
|
|
29
|
+
) {
|
|
30
|
+
// Check if the visit should be ignored
|
|
31
|
+
if (this.shouldIgnoreVisit(url)) {
|
|
32
|
+
window.location.href = url;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { url: to, hash } = Location.fromUrl(url);
|
|
37
|
+
this.context = this.createContext({ ...context, to, hash });
|
|
38
|
+
this.performVisit(to, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Start a visit to a new URL.
|
|
43
|
+
*
|
|
44
|
+
* Internal method that assumes the global context has already been set up.
|
|
45
|
+
*
|
|
46
|
+
* As a user, you should call `swup.visit(url)` instead.
|
|
47
|
+
*
|
|
48
|
+
* @param url The URL to navigate to.
|
|
49
|
+
* @param options Options for how to perform this visit.
|
|
50
|
+
* @returns Promise<void>
|
|
51
|
+
*/
|
|
52
|
+
export async function performVisit(
|
|
53
|
+
this: Swup,
|
|
54
|
+
url: string,
|
|
55
|
+
options: VisitOptions & FetchOptions = {}
|
|
56
|
+
) {
|
|
57
|
+
if (typeof url !== 'string') {
|
|
58
|
+
throw new Error(`swup.visit() requires a URL parameter`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.context.to.url = Location.fromUrl(url).url;
|
|
62
|
+
const { animation, animate, history: historyAction } = options;
|
|
63
|
+
options.referrer = options.referrer || this.currentPageUrl;
|
|
64
|
+
|
|
65
|
+
if (animate === false) {
|
|
66
|
+
this.context.animation.animate = false;
|
|
67
|
+
}
|
|
68
|
+
if (historyAction) {
|
|
69
|
+
this.context.history.action = historyAction;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Clean up old animation classes and set custom animation name
|
|
73
|
+
if (!this.context.animation.animate) {
|
|
74
|
+
this.classes.clear();
|
|
75
|
+
} else if (animation) {
|
|
76
|
+
this.context.animation.name = animation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await this.hooks.trigger('visit:start');
|
|
81
|
+
|
|
82
|
+
// Begin fetching page
|
|
83
|
+
const pagePromise = this.hooks.trigger(
|
|
84
|
+
'page:request',
|
|
85
|
+
{ url: this.context.to.url, options },
|
|
86
|
+
async (context, { options }) => await this.fetchPage(context.to.url as string, options)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Create history record if this is not a popstate call (with or without anchor)
|
|
90
|
+
if (!this.context.history.popstate) {
|
|
91
|
+
const newUrl = url + (this.context.scroll.target || '');
|
|
92
|
+
if (this.context.history.action === 'replace') {
|
|
93
|
+
updateHistoryRecord(newUrl);
|
|
94
|
+
} else {
|
|
95
|
+
const index = this.currentHistoryIndex + 1;
|
|
96
|
+
createHistoryRecord(newUrl, { index });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.currentPageUrl = getCurrentUrl();
|
|
101
|
+
|
|
102
|
+
// Wait for page before starting to animate out?
|
|
103
|
+
if (this.context.animation.wait) {
|
|
104
|
+
const { html } = await pagePromise;
|
|
105
|
+
this.context.to.html = html;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Wait for page to load and leave animation to finish
|
|
109
|
+
const animationPromise = this.leavePage();
|
|
110
|
+
const [page] = await Promise.all([pagePromise, animationPromise]);
|
|
111
|
+
|
|
112
|
+
// Render page: replace content and scroll to top/fragment
|
|
113
|
+
await this.renderPage(this.context.to.url, page);
|
|
114
|
+
|
|
115
|
+
// Wait for enter animation
|
|
116
|
+
await this.enterPage();
|
|
117
|
+
|
|
118
|
+
// Finalize visit
|
|
119
|
+
await this.hooks.trigger('visit:end', undefined, () => this.classes.clear());
|
|
120
|
+
|
|
121
|
+
// Reset context after visit?
|
|
122
|
+
// if (this.context.to && this.isSameResolvedUrl(this.context.to.url, requestedUrl)) {
|
|
123
|
+
// this.createContext({ to: undefined });
|
|
124
|
+
// }
|
|
125
|
+
} catch (error: unknown) {
|
|
126
|
+
// Return early if error is undefined (probably aborted preload request)
|
|
127
|
+
if (!error) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Log to console as we swallow almost all hook errors
|
|
132
|
+
console.error(error);
|
|
133
|
+
|
|
134
|
+
// Rewrite `skipPopStateHandling` to redirect manually when `history.go` is processed
|
|
135
|
+
this.options.skipPopStateHandling = () => {
|
|
136
|
+
window.location.href = this.context.to.url as string;
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Go back to the actual page we're still at
|
|
141
|
+
history.go(-1);
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -42,9 +42,8 @@ export const escapeCssIdentifier = (ident: string) => {
|
|
|
42
42
|
// @ts-ignore this is for support check, so it's correct that TS complains
|
|
43
43
|
if (window.CSS && window.CSS.escape) {
|
|
44
44
|
return CSS.escape(ident);
|
|
45
|
-
} else {
|
|
46
|
-
return ident;
|
|
47
45
|
}
|
|
46
|
+
return ident;
|
|
48
47
|
};
|
|
49
48
|
|
|
50
49
|
// Fix for Chrome below v61 formatting CSS floats with comma in some locales
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const markSwupElements: (element: Element, containers: string[]) => void;
|