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.
Files changed (83) hide show
  1. package/README.md +100 -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 +62 -54
  11. package/dist/types/helpers/Location.d.ts +10 -7
  12. package/dist/types/helpers/delegateEvent.d.ts +3 -5
  13. package/dist/types/helpers/matchPath.d.ts +3 -0
  14. package/dist/types/helpers.d.ts +7 -10
  15. package/dist/types/index.d.ts +9 -6
  16. package/dist/types/modules/Cache.d.ts +15 -15
  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__/hooks.test.d.ts +1 -0
  21. package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -0
  22. package/dist/types/modules/awaitAnimations.d.ts +21 -0
  23. package/dist/types/modules/enterPage.d.ts +6 -3
  24. package/dist/types/modules/fetchPage.d.ts +24 -4
  25. package/dist/types/modules/getAnchorElement.d.ts +8 -0
  26. package/dist/types/modules/leavePage.d.ts +6 -3
  27. package/dist/types/modules/plugins.d.ts +12 -5
  28. package/dist/types/modules/renderPage.d.ts +7 -7
  29. package/dist/types/modules/replaceContent.d.ts +8 -11
  30. package/dist/types/modules/visit.d.ts +33 -0
  31. package/dist/types/utils/index.d.ts +3 -1
  32. package/dist/types/utils.d.ts +1 -1
  33. package/package.json +7 -6
  34. package/src/Swup.ts +83 -65
  35. package/src/__test__/index.test.ts +3 -3
  36. package/src/helpers/Location.ts +2 -2
  37. package/src/helpers/delegateEvent.ts +2 -2
  38. package/src/helpers.ts +0 -1
  39. package/src/index.ts +34 -4
  40. package/src/modules/Cache.ts +2 -2
  41. package/src/modules/Classes.ts +48 -0
  42. package/src/modules/Context.ts +49 -19
  43. package/src/modules/Hooks.ts +103 -83
  44. package/src/modules/__test__/cache.test.ts +6 -6
  45. package/src/modules/__test__/hooks.test.ts +111 -40
  46. package/src/modules/__test__/replaceContent.test.ts +92 -0
  47. package/src/modules/{getAnimationPromises.ts → awaitAnimations.ts} +13 -18
  48. package/src/modules/enterPage.ts +21 -17
  49. package/src/modules/fetchPage.ts +12 -12
  50. package/src/modules/getAnchorElement.ts +2 -1
  51. package/src/modules/leavePage.ts +16 -12
  52. package/src/modules/plugins.ts +11 -8
  53. package/src/modules/renderPage.ts +28 -18
  54. package/src/modules/replaceContent.ts +24 -16
  55. package/src/modules/visit.ts +143 -0
  56. package/src/utils/index.ts +1 -2
  57. package/dist/types/helpers/cleanupAnimationClasses.d.ts +0 -2
  58. package/dist/types/helpers/fetch.d.ts +0 -5
  59. package/dist/types/helpers/getDataFromHtml.d.ts +0 -7
  60. package/dist/types/helpers/markSwupElements.d.ts +0 -1
  61. package/dist/types/modules/destroy.d.ts +0 -2
  62. package/dist/types/modules/enable.d.ts +0 -2
  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/handleLinkToSamePage.d.ts +0 -2
  67. package/dist/types/modules/isSameResolvedUrl.d.ts +0 -8
  68. package/dist/types/modules/linkClickHandler.d.ts +0 -3
  69. package/dist/types/modules/loadPage.d.ts +0 -12
  70. package/dist/types/modules/off.d.ts +0 -3
  71. package/dist/types/modules/on.d.ts +0 -5
  72. package/dist/types/modules/popStateHandler.d.ts +0 -2
  73. package/dist/types/modules/resolveUrl.d.ts +0 -7
  74. package/dist/types/modules/shouldIgnoreVisit.d.ts +0 -4
  75. package/dist/types/modules/transitions.d.ts +0 -6
  76. package/dist/types/modules/triggerEvent.d.ts +0 -3
  77. package/dist/types/modules/triggerWillOpenNewWindow.d.ts +0 -2
  78. package/dist/types/modules/updateTransition.d.ts +0 -2
  79. package/readme.md +0 -78
  80. package/src/helpers/cleanupAnimationClasses.ts +0 -8
  81. package/src/modules/loadPage.ts +0 -99
  82. /package/dist/types/{modules/__test__/events.test.d.ts → helpers/__test__/matchPath.test.d.ts} +0 -0
  83. /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
- * Get an array of Promises that resolve when all animations are done on the page.
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 getAnimationPromises(
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 resolved;
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 resolved;
44
+ return;
48
45
  }
49
46
  }
50
47
 
51
- const animationPromises = animatedElements
52
- .map((element) => getAnimationPromiseForElement(element))
53
- .filter(Boolean) as Promise<void>[];
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 resolved;
56
+ return;
62
57
  }
63
58
 
64
- return animationPromises;
59
+ await Promise.all(awaitedAnimations);
65
60
  }
66
61
 
67
- function getAnimationPromiseForElement(element: Element): Promise<void> | undefined {
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 undefined;
67
+ return false;
73
68
  }
74
69
 
75
70
  return new Promise((resolve) => {
@@ -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.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');
9
+ if (!this.context.animation.animate) {
10
+ return;
19
11
  }
20
12
 
21
- await this.hooks.trigger('transitionEnd', undefined, () => {
22
- this.cleanupAnimationClasses();
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
- this.context = this.createContext({ to: undefined });
27
+ await animation;
28
+
29
+ await this.hooks.trigger('animation:in:end');
26
30
  };
@@ -31,12 +31,12 @@ export async function fetchPage(
31
31
  url: URL | string,
32
32
  options: FetchOptions & { triggerHooks?: boolean } = {}
33
33
  ): Promise<PageData> {
34
- const { url: requestUrl } = Location.fromUrl(url);
34
+ url = Location.fromUrl(url).url;
35
35
 
36
- if (this.cache.has(requestUrl)) {
37
- const page = this.cache.get(requestUrl) as PageData;
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('pageLoaded', { page, cache: true });
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
- 'fetchPage',
50
- { url: requestUrl, options },
51
- async (context, { url, options, response }) => await (response || fetch(url, options))
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('serverError', { status, response, url: responseUrl });
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 } = new Location(responseUrl);
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 (requestUrl === finalUrl) {
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('pageLoaded', { page, cache: false });
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) === '#') {
@@ -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.transition.animate) {
6
- await this.hooks.trigger('animationSkipped');
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('animationOutStart', undefined, () => {
11
- document.documentElement.classList.add('is-changing', 'is-leaving', 'is-animating');
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
- document.documentElement.classList.add('is-popstate');
17
+ this.classes.add('is-popstate');
14
18
  }
15
- if (this.context.transition.name) {
16
- document.documentElement.classList.add(`to-${classify(this.context.transition.name)}`);
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
- 'awaitAnimation',
22
- { selector: this.options.animationSelector, direction: 'out' },
23
- async (context, { selector, direction }) => {
24
- await Promise.all(this.getAnimationPromises({ selector, direction }));
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('animationOutDone');
32
+ await this.hooks.trigger('animation:out:end');
29
33
  };
@@ -1,17 +1,20 @@
1
1
  import Swup from '../Swup.js';
2
2
 
3
3
  export type Plugin = {
4
- name: string;
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
- document.documentElement.classList.remove('is-leaving');
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!.url = this.currentPageUrl;
23
+ this.context.to.url = this.currentPageUrl;
20
24
  }
21
25
 
22
- // only add for page loads with transitions
23
- if (this.context.transition.animate) {
24
- document.documentElement.classList.add('is-rendering');
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
- 'replaceContent',
30
- { page, containers: this.context.containers },
31
- (context, { page, containers }) => {
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
- 'scrollToContent',
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('pageView', { url: this.currentPageUrl, title: document.title });
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
- ): void {
15
- const doc = new DOMParser().parseFromString(html, 'text/html');
16
+ ): boolean {
17
+ const incomingDocument = new DOMParser().parseFromString(html, 'text/html');
16
18
 
17
19
  // Update browser title
18
- const title = doc.querySelector('title')?.innerText || '';
20
+ const title = incomingDocument.querySelector('title')?.innerText || '';
19
21
  document.title = title;
20
22
 
21
23
  // Update content containers
22
- containers.forEach((selector) => {
23
- const currentEl = document.querySelector(selector);
24
- const incomingEl = doc.querySelector(selector);
25
- if (!currentEl) {
26
- console.warn(`[swup] Container missing in current document: ${selector}`);
27
- return;
28
- }
29
- if (!incomingEl) {
30
- console.warn(`[swup] Container missing in incoming document: ${selector}`);
31
- return;
32
- }
33
- currentEl.replaceWith(incomingEl);
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
+ }
@@ -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,2 +0,0 @@
1
- export declare const isSwupClass: (className: string) => boolean;
2
- export declare const cleanupAnimationClasses: () => void;
@@ -1,5 +0,0 @@
1
- import { TransitionOptions } from '../modules/loadPage';
2
- import { Options } from '../Swup';
3
- export declare const fetch: (options: TransitionOptions & {
4
- headers: Options['requestHeaders'];
5
- }, callback: (request: XMLHttpRequest) => void) => XMLHttpRequest;
@@ -1,7 +0,0 @@
1
- export type PageHtmlData = {
2
- title: string;
3
- originalContent: string;
4
- blocks: string[];
5
- pageClass?: string;
6
- };
7
- export declare const getDataFromHtml: (html: string, containers: string[]) => PageHtmlData;
@@ -1 +0,0 @@
1
- export declare const markSwupElements: (element: Element, containers: string[]) => void;