swup 4.3.2 → 4.3.4

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/src/Swup.ts CHANGED
@@ -90,7 +90,7 @@ export default class Swup {
90
90
  /** URL of the currently visible page */
91
91
  currentPageUrl: string = getCurrentUrl();
92
92
  /** Index of the current history entry */
93
- protected currentHistoryIndex = 1;
93
+ protected currentHistoryIndex: number;
94
94
  /** Delegated event subscription handle */
95
95
  protected clickDelegate?: DelegateEventUnsubscribe;
96
96
 
@@ -144,6 +144,8 @@ export default class Swup {
144
144
  this.hooks = new Hooks(this);
145
145
  this.visit = this.createVisit({ to: '' });
146
146
 
147
+ this.currentHistoryIndex = (history.state as HistoryState)?.index ?? 1;
148
+
147
149
  if (!this.checkRequirements()) {
148
150
  return;
149
151
  }
@@ -181,8 +183,10 @@ export default class Swup {
181
183
  // Mount plugins
182
184
  this.options.plugins.forEach((plugin) => this.use(plugin));
183
185
 
184
- // Modify initial history record
185
- updateHistoryRecord(null, { index: 1 });
186
+ // Create initial history record
187
+ if ((history.state as HistoryState)?.source !== 'swup') {
188
+ updateHistoryRecord(null, { index: this.currentHistoryIndex });
189
+ }
186
190
 
187
191
  // Give consumers a chance to hook into enable
188
192
  await nextTick();
@@ -323,10 +327,11 @@ export default class Swup {
323
327
  this.visit.history.popstate = true;
324
328
 
325
329
  // Determine direction of history visit
326
- const index = Number((event.state as HistoryState)?.index);
327
- if (index) {
330
+ const index = (event.state as HistoryState)?.index ?? 0;
331
+ if (index && index !== this.currentHistoryIndex) {
328
332
  const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
329
333
  this.visit.history.direction = direction;
334
+ this.currentHistoryIndex = index;
330
335
  }
331
336
 
332
337
  // Disable animation & scrolling for history visits
@@ -3,6 +3,8 @@ import { HistoryAction, HistoryDirection } from './navigate.js';
3
3
 
4
4
  /** An object holding details about the current visit. */
5
5
  export interface Visit {
6
+ /** A unique ID to identify this visit */
7
+ id: number;
6
8
  /** The previous page, about to leave */
7
9
  from: VisitFrom;
8
10
  /** The next page, about to enter */
@@ -92,6 +94,7 @@ export function createVisit(
92
94
  { to, from = this.currentPageUrl, hash, el, event }: VisitInitOptions
93
95
  ): Visit {
94
96
  return {
97
+ id: Math.random(),
95
98
  from: { url: from },
96
99
  to: { url: to, hash },
97
100
  containers: this.options.containers,
@@ -0,0 +1,92 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import Swup from '../../Swup.js';
3
+ import { Visit, createVisit } from '../Visit.js';
4
+
5
+ class SwupWithPublicVisitMethods extends Swup {
6
+ public createVisit = createVisit;
7
+ }
8
+
9
+ const swup = new SwupWithPublicVisitMethods();
10
+ let visit: Visit;
11
+
12
+ describe('Visit', () => {
13
+ beforeEach(() => {
14
+ visit = swup.createVisit({ to: '' });
15
+ });
16
+
17
+ it('is an object', () => {
18
+ expect(visit).to.be.an('object');
19
+ });
20
+
21
+ it('has an id', () => {
22
+ expect(visit.id).to.be.a('number');
23
+ });
24
+
25
+ it('generates unique ids', () => {
26
+ let id = visit.id;
27
+ visit = swup.createVisit({ to: '' });
28
+ expect(visit.id).to.not.equal(id);
29
+ });
30
+
31
+ it('has a from object with the current URL', () => {
32
+ expect(visit.from).to.be.an('object');
33
+ expect(visit.from.url).to.be.a('string');
34
+ visit = swup.createVisit({ to: '', from: '/from' });
35
+ expect(visit.from).toMatchObject({ url: '/from' });
36
+ });
37
+
38
+ it('has a to object with the next URL', () => {
39
+ expect(visit.to).to.be.an('object');
40
+ expect(visit.to.url).to.be.a('string');
41
+ visit = swup.createVisit({ to: '/to' });
42
+ expect(visit.to).toMatchObject({ url: '/to' });
43
+ });
44
+
45
+ it('has an animation object', () => {
46
+ expect(visit.animation).to.be.an('object');
47
+ expect(visit.animation).toMatchObject({
48
+ animate: true,
49
+ name: undefined,
50
+ scope: swup.options.animationScope,
51
+ selector: swup.options.animationSelector
52
+ });
53
+ });
54
+
55
+ it('has a container array', () => {
56
+ expect(visit.containers).to.be.an('array');
57
+ expect(visit.containers).toEqual(swup.options.containers);
58
+ });
59
+
60
+ it('has a trigger object', () => {
61
+ expect(visit.trigger).to.be.an('object');
62
+ expect(visit.trigger).toMatchObject({
63
+ el: undefined,
64
+ event: undefined
65
+ });
66
+ });
67
+
68
+ it('has a cache object', () => {
69
+ expect(visit.cache).to.be.an('object');
70
+ expect(visit.cache).toEqual({
71
+ read: swup.options.cache,
72
+ write: swup.options.cache
73
+ });
74
+ });
75
+
76
+ it('has a history object', () => {
77
+ expect(visit.history).to.be.an('object');
78
+ expect(visit.history).toEqual({
79
+ action: 'push',
80
+ popstate: false,
81
+ direction: undefined
82
+ });
83
+ });
84
+
85
+ it('has a scroll object', () => {
86
+ expect(visit.scroll).to.be.an('object');
87
+ expect(visit.scroll).toEqual({
88
+ reset: true,
89
+ target: undefined
90
+ });
91
+ });
92
+ });
@@ -61,37 +61,41 @@ export function navigate(
61
61
  export async function performNavigation(
62
62
  this: Swup,
63
63
  options: NavigationOptions & FetchOptions = {}
64
- ) {
65
- const { el } = this.visit.trigger;
64
+ ): Promise<void> {
65
+ // Save this localy to a) allow ignoring the visit if a new one was started in the meantime
66
+ // and b) avoid unintended modifications to any newer visits
67
+ const visit = this.visit;
68
+
69
+ const { el } = visit.trigger;
66
70
  options.referrer = options.referrer || this.currentPageUrl;
67
71
 
68
72
  if (options.animate === false) {
69
- this.visit.animation.animate = false;
73
+ visit.animation.animate = false;
70
74
  }
71
75
 
72
76
  // Clean up old animation classes
73
- if (!this.visit.animation.animate) {
77
+ if (!visit.animation.animate) {
74
78
  this.classes.clear();
75
79
  }
76
80
 
77
81
  // Get history action from option or attribute on trigger element
78
82
  const history = options.history || el?.getAttribute('data-swup-history') || undefined;
79
83
  if (history && ['push', 'replace'].includes(history)) {
80
- this.visit.history.action = history as HistoryAction;
84
+ visit.history.action = history as HistoryAction;
81
85
  }
82
86
 
83
87
  // Get custom animation name from option or attribute on trigger element
84
88
  const animation = options.animation || el?.getAttribute('data-swup-animation') || undefined;
85
89
  if (animation) {
86
- this.visit.animation.name = animation;
90
+ visit.animation.name = animation;
87
91
  }
88
92
 
89
93
  // Sanitize cache option
90
94
  if (typeof options.cache === 'object') {
91
- this.visit.cache.read = options.cache.read ?? this.visit.cache.read;
92
- this.visit.cache.write = options.cache.write ?? this.visit.cache.write;
95
+ visit.cache.read = options.cache.read ?? visit.cache.read;
96
+ visit.cache.write = options.cache.write ?? visit.cache.write;
93
97
  } else if (options.cache !== undefined) {
94
- this.visit.cache = { read: !!options.cache, write: !!options.cache };
98
+ visit.cache = { read: !!options.cache, write: !!options.cache };
95
99
  }
96
100
  // Delete this so that window.fetch doesn't mis-interpret it
97
101
  delete options.cache;
@@ -103,7 +107,7 @@ export async function performNavigation(
103
107
  const pagePromise = this.hooks.call('page:load', { options }, async (visit, args) => {
104
108
  // Read from cache
105
109
  let cachedPage: PageData | undefined;
106
- if (this.visit.cache.read) {
110
+ if (visit.cache.read) {
107
111
  cachedPage = this.cache.get(visit.to.url);
108
112
  }
109
113
 
@@ -114,34 +118,36 @@ export async function performNavigation(
114
118
  });
115
119
 
116
120
  // Create/update history record if this is not a popstate call or leads to the same URL
117
- if (!this.visit.history.popstate) {
121
+ if (!visit.history.popstate) {
118
122
  // Add the hash directly from the trigger element
119
- const newUrl = this.visit.to.url + this.visit.to.hash;
120
- if (
121
- this.visit.history.action === 'replace' ||
122
- this.visit.to.url === this.currentPageUrl
123
- ) {
123
+ const newUrl = visit.to.url + visit.to.hash;
124
+ if (visit.history.action === 'replace' || visit.to.url === this.currentPageUrl) {
124
125
  updateHistoryRecord(newUrl);
125
126
  } else {
126
- const index = this.currentHistoryIndex + 1;
127
- createHistoryRecord(newUrl, { index });
127
+ this.currentHistoryIndex++;
128
+ createHistoryRecord(newUrl, { index: this.currentHistoryIndex });
128
129
  }
129
130
  }
130
131
 
131
132
  this.currentPageUrl = getCurrentUrl();
132
133
 
133
134
  // Wait for page before starting to animate out?
134
- if (this.visit.animation.wait) {
135
+ if (visit.animation.wait) {
135
136
  const { html } = await pagePromise;
136
- this.visit.to.html = html;
137
+ visit.to.html = html;
137
138
  }
138
139
 
139
140
  // Wait for page to load and leave animation to finish
140
141
  const animationPromise = this.animatePageOut();
141
142
  const [page] = await Promise.all([pagePromise, animationPromise]);
142
143
 
144
+ // Abort if another visit was started in the meantime
145
+ if (visit.id !== this.visit.id) {
146
+ return;
147
+ }
148
+
143
149
  // Render page: replace content and scroll to top/fragment
144
- await this.renderPage(this.visit.to.url, page);
150
+ await this.renderPage(page);
145
151
 
146
152
  // Wait for enter animation
147
153
  await this.animatePageIn();
@@ -150,8 +156,8 @@ export async function performNavigation(
150
156
  await this.hooks.call('visit:end', undefined, () => this.classes.clear());
151
157
 
152
158
  // Reset visit info after finish?
153
- // if (this.visit.to && this.isSameResolvedUrl(this.visit.to.url, requestedUrl)) {
154
- // this.createVisit({ to: undefined });
159
+ // if (visit.to && this.isSameResolvedUrl(visit.to.url, requestedUrl)) {
160
+ // this.visit = this.createVisit({ to: undefined });
155
161
  // }
156
162
  } catch (error: unknown) {
157
163
  // Return early if error is undefined (probably aborted preload request)
@@ -164,7 +170,7 @@ export async function performNavigation(
164
170
 
165
171
  // Rewrite `skipPopStateHandling` to redirect manually when `history.go` is processed
166
172
  this.options.skipPopStateHandling = () => {
167
- window.location.href = this.visit.to.url + this.visit.to.hash;
173
+ window.location.href = visit.to.url + visit.to.hash;
168
174
  return true;
169
175
  };
170
176
 
@@ -4,18 +4,12 @@ import { PageData } from './fetchPage.js';
4
4
 
5
5
  /**
6
6
  * Render the next page: replace the content and update scroll position.
7
- * @returns Promise<void>
8
7
  */
9
- export const renderPage = async function (this: Swup, requestedUrl: string, page: PageData) {
8
+ export const renderPage = async function (this: Swup, page: PageData): Promise<void> {
10
9
  const { url, html } = page;
11
10
 
12
11
  this.classes.remove('is-leaving');
13
12
 
14
- // do nothing if another page was requested in the meantime
15
- if (!this.isSameResolvedUrl(getCurrentUrl(), requestedUrl)) {
16
- return;
17
- }
18
-
19
13
  // update state if the url was redirected
20
14
  if (!this.isSameResolvedUrl(getCurrentUrl(), url)) {
21
15
  updateHistoryRecord(url);