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
package/src/Swup.ts
CHANGED
|
@@ -2,124 +2,93 @@ import { DelegateEvent } from 'delegate-it';
|
|
|
2
2
|
|
|
3
3
|
import version from './config/version.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
delegateEvent,
|
|
8
|
-
getCurrentUrl,
|
|
9
|
-
Location,
|
|
10
|
-
markSwupElements,
|
|
11
|
-
updateHistoryRecord
|
|
12
|
-
} from './helpers.js';
|
|
13
|
-
import { Unsubscribe } from './helpers/delegateEvent.js';
|
|
5
|
+
import { delegateEvent, getCurrentUrl, Location, updateHistoryRecord } from './helpers.js';
|
|
6
|
+
import { DelegateEventUnsubscribe } from './helpers/delegateEvent.js';
|
|
14
7
|
|
|
15
8
|
import { Cache } from './modules/Cache.js';
|
|
16
|
-
import {
|
|
9
|
+
import { Classes } from './modules/Classes.js';
|
|
10
|
+
import { Context, createContext } from './modules/Context.js';
|
|
11
|
+
import { Hooks } from './modules/Hooks.js';
|
|
17
12
|
import { getAnchorElement } from './modules/getAnchorElement.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
13
|
+
import { awaitAnimations } from './modules/awaitAnimations.js';
|
|
14
|
+
import { visit, performVisit, HistoryAction } from './modules/visit.js';
|
|
20
15
|
import { fetchPage } from './modules/fetchPage.js';
|
|
21
16
|
import { leavePage } from './modules/leavePage.js';
|
|
22
|
-
import { HistoryAction, loadPage, performPageLoad } from './modules/loadPage.js';
|
|
23
17
|
import { replaceContent } from './modules/replaceContent.js';
|
|
24
|
-
import {
|
|
25
|
-
import { use, unuse, findPlugin, Plugin } from './modules/plugins.js';
|
|
18
|
+
import { enterPage } from './modules/enterPage.js';
|
|
26
19
|
import { renderPage } from './modules/renderPage.js';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
import { queryAll } from './utils.js';
|
|
30
|
-
|
|
31
|
-
export type Transition = {
|
|
32
|
-
from?: string;
|
|
33
|
-
to?: string;
|
|
34
|
-
custom?: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type DelegatedListeners = {
|
|
38
|
-
click?: Unsubscribe;
|
|
39
|
-
};
|
|
20
|
+
import { use, unuse, findPlugin, Plugin } from './modules/plugins.js';
|
|
21
|
+
import { nextTick } from './utils.js';
|
|
40
22
|
|
|
41
23
|
export type Options = {
|
|
24
|
+
/** Whether history visits are animated. Default: `false` */
|
|
42
25
|
animateHistoryBrowsing: boolean;
|
|
26
|
+
/** Selector for detecting animation timing. Default: `[class*="transition-"]` */
|
|
43
27
|
animationSelector: string | false;
|
|
44
|
-
|
|
28
|
+
/** Elements on which to add animation classes. Default: `html` element */
|
|
29
|
+
animationScope: 'html' | 'containers';
|
|
30
|
+
/** Enable in-memory page cache. Default: `true` */
|
|
45
31
|
cache: boolean;
|
|
32
|
+
/** Content containers to be replaced on page visits. Default: `['#swup']` */
|
|
46
33
|
containers: string[];
|
|
47
|
-
|
|
48
|
-
plugins: Plugin[];
|
|
49
|
-
skipPopStateHandling: (event: any) => boolean;
|
|
34
|
+
/** Callback for ignoring visits. Receives the element and event that triggered the visit. */
|
|
50
35
|
ignoreVisit: (url: string, { el, event }: { el?: Element; event?: Event }) => boolean;
|
|
36
|
+
/** Selector for links that trigger visits. Default: `'a[href]'` */
|
|
37
|
+
linkSelector: string;
|
|
38
|
+
/** Plugins to register on startup. */
|
|
39
|
+
plugins: Plugin[];
|
|
40
|
+
/** Custom headers sent along with fetch requests. */
|
|
41
|
+
requestHeaders: Record<string, string>;
|
|
42
|
+
/** Rewrite URLs before loading them. */
|
|
51
43
|
resolveUrl: (url: string) => string;
|
|
44
|
+
/** Callback for telling swup to ignore certain popstate events. */
|
|
45
|
+
skipPopStateHandling: (event: any) => boolean;
|
|
52
46
|
};
|
|
53
47
|
|
|
54
48
|
export default class Swup {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
animationInDone: [],
|
|
59
|
-
animationInStart: [],
|
|
60
|
-
animationOutDone: [],
|
|
61
|
-
animationOutStart: [],
|
|
62
|
-
animationSkipped: [],
|
|
63
|
-
clickLink: [],
|
|
64
|
-
contentReplaced: [],
|
|
65
|
-
disabled: [],
|
|
66
|
-
enabled: [],
|
|
67
|
-
openPageInNewTab: [],
|
|
68
|
-
pageLoaded: [],
|
|
69
|
-
pageRetrievedFromCache: [],
|
|
70
|
-
pageView: [],
|
|
71
|
-
popState: [],
|
|
72
|
-
samePage: [],
|
|
73
|
-
samePageWithHash: [],
|
|
74
|
-
serverError: [],
|
|
75
|
-
transitionStart: [],
|
|
76
|
-
transitionEnd: [],
|
|
77
|
-
willReplaceContent: []
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// variable for anchor to scroll to after render
|
|
81
|
-
scrollToElement: string | null = null;
|
|
82
|
-
// variable for save options
|
|
49
|
+
/** Library version */
|
|
50
|
+
version: string = version;
|
|
51
|
+
/** Options passed into the instance */
|
|
83
52
|
options: Options;
|
|
84
|
-
|
|
53
|
+
/** Registered plugin instances */
|
|
85
54
|
plugins: Plugin[] = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
55
|
+
/** Global context of the current visit */
|
|
56
|
+
context: Context;
|
|
57
|
+
/** Cache instance */
|
|
89
58
|
cache: Cache;
|
|
90
|
-
|
|
59
|
+
/** Hook registry */
|
|
60
|
+
hooks: Hooks;
|
|
61
|
+
/** Animation class manager */
|
|
62
|
+
classes: Classes;
|
|
63
|
+
/** URL of the currently visible page */
|
|
91
64
|
currentPageUrl = getCurrentUrl();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
65
|
+
/** Index of the current history entry */
|
|
66
|
+
currentHistoryIndex = 1;
|
|
67
|
+
/** Delegated event subscription handle */
|
|
68
|
+
private clickDelegate?: DelegateEventUnsubscribe;
|
|
96
69
|
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
visit = visit;
|
|
71
|
+
performVisit = performVisit;
|
|
99
72
|
leavePage = leavePage;
|
|
100
73
|
renderPage = renderPage;
|
|
101
74
|
replaceContent = replaceContent;
|
|
102
75
|
enterPage = enterPage;
|
|
103
|
-
triggerEvent = triggerEvent;
|
|
104
76
|
delegateEvent = delegateEvent;
|
|
105
|
-
on = on;
|
|
106
|
-
off = off;
|
|
107
|
-
updateTransition = updateTransition;
|
|
108
|
-
shouldSkipTransition = shouldSkipTransition;
|
|
109
|
-
getAnimationPromises = getAnimationPromises;
|
|
110
|
-
getPageData = getPageData;
|
|
111
77
|
fetchPage = fetchPage;
|
|
78
|
+
awaitAnimations = awaitAnimations;
|
|
112
79
|
getAnchorElement = getAnchorElement;
|
|
113
|
-
log: (message: string, context?: any) => void = () => {}; // here so it can be used by plugins
|
|
114
80
|
use = use;
|
|
115
81
|
unuse = unuse;
|
|
116
82
|
findPlugin = findPlugin;
|
|
117
83
|
getCurrentUrl = getCurrentUrl;
|
|
118
|
-
|
|
84
|
+
createContext = createContext;
|
|
85
|
+
log: (message: string, context?: any) => void = () => {}; // here so it can be used by plugins
|
|
119
86
|
|
|
87
|
+
/** Default options before merging user options */
|
|
120
88
|
defaults: Options = {
|
|
121
89
|
animateHistoryBrowsing: false,
|
|
122
90
|
animationSelector: '[class*="transition-"]',
|
|
91
|
+
animationScope: 'html',
|
|
123
92
|
cache: true,
|
|
124
93
|
containers: ['#swup'],
|
|
125
94
|
ignoreVisit: (url, { el, event } = {}) => !!el?.closest('[data-no-swup]'),
|
|
@@ -128,7 +97,7 @@ export default class Swup {
|
|
|
128
97
|
resolveUrl: (url) => url,
|
|
129
98
|
requestHeaders: {
|
|
130
99
|
'X-Requested-With': 'swup',
|
|
131
|
-
Accept: 'text/html, application/xhtml+xml'
|
|
100
|
+
'Accept': 'text/html, application/xhtml+xml'
|
|
132
101
|
},
|
|
133
102
|
skipPopStateHandling: (event) => event.state?.source !== 'swup'
|
|
134
103
|
};
|
|
@@ -137,28 +106,35 @@ export default class Swup {
|
|
|
137
106
|
// Merge defaults and options
|
|
138
107
|
this.options = { ...this.defaults, ...options };
|
|
139
108
|
|
|
140
|
-
this.
|
|
109
|
+
this.linkClickHandler = this.linkClickHandler.bind(this);
|
|
110
|
+
this.popStateHandler = this.popStateHandler.bind(this);
|
|
141
111
|
|
|
142
112
|
this.cache = new Cache(this);
|
|
113
|
+
this.classes = new Classes(this);
|
|
114
|
+
this.hooks = new Hooks(this);
|
|
115
|
+
this.context = this.createContext({ to: undefined });
|
|
116
|
+
|
|
117
|
+
if (!this.checkRequirements()) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
143
120
|
|
|
144
121
|
this.enable();
|
|
145
122
|
}
|
|
146
123
|
|
|
147
|
-
|
|
148
|
-
// Check for Promise support
|
|
124
|
+
checkRequirements() {
|
|
149
125
|
if (typeof Promise === 'undefined') {
|
|
150
126
|
console.warn('Promise is not supported');
|
|
151
|
-
return;
|
|
127
|
+
return false;
|
|
152
128
|
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.linkClickHandler.bind(this)
|
|
159
|
-
);
|
|
132
|
+
async enable() {
|
|
133
|
+
// Add event listener
|
|
134
|
+
const { linkSelector } = this.options;
|
|
135
|
+
this.clickDelegate = this.delegateEvent(linkSelector, 'click', this.linkClickHandler);
|
|
160
136
|
|
|
161
|
-
window.addEventListener('popstate', this.
|
|
137
|
+
window.addEventListener('popstate', this.popStateHandler);
|
|
162
138
|
|
|
163
139
|
// Initial save to cache
|
|
164
140
|
if (this.options.cache) {
|
|
@@ -166,53 +142,45 @@ export default class Swup {
|
|
|
166
142
|
// https://github.com/swup/swup/issues/475
|
|
167
143
|
}
|
|
168
144
|
|
|
169
|
-
// Mark swup blocks in html
|
|
170
|
-
markSwupElements(document.documentElement, this.options.containers);
|
|
171
|
-
|
|
172
145
|
// Mount plugins
|
|
173
146
|
this.options.plugins.forEach((plugin) => this.use(plugin));
|
|
174
147
|
|
|
175
148
|
// Modify initial history record
|
|
176
|
-
updateHistoryRecord();
|
|
149
|
+
updateHistoryRecord(null, { index: 1 });
|
|
177
150
|
|
|
178
|
-
// Trigger
|
|
179
|
-
this.
|
|
151
|
+
// Trigger enable hook
|
|
152
|
+
await this.hooks.trigger('enable', undefined, () => {
|
|
153
|
+
// Add swup-enabled class to html tag
|
|
154
|
+
document.documentElement.classList.add('swup-enabled');
|
|
155
|
+
});
|
|
180
156
|
|
|
181
|
-
|
|
182
|
-
document.documentElement.classList.add('swup-enabled');
|
|
157
|
+
await nextTick();
|
|
183
158
|
|
|
184
|
-
// Trigger page view
|
|
185
|
-
this.
|
|
159
|
+
// Trigger page view hook
|
|
160
|
+
await this.hooks.trigger('page:view', { url: this.currentPageUrl, title: document.title });
|
|
186
161
|
}
|
|
187
162
|
|
|
188
|
-
destroy() {
|
|
189
|
-
// remove delegated
|
|
190
|
-
this.
|
|
163
|
+
async destroy() {
|
|
164
|
+
// remove delegated listener
|
|
165
|
+
this.clickDelegate!.destroy();
|
|
191
166
|
|
|
192
167
|
// remove popstate listener
|
|
193
|
-
window.removeEventListener('popstate', this.
|
|
168
|
+
window.removeEventListener('popstate', this.popStateHandler);
|
|
194
169
|
|
|
195
170
|
// empty cache
|
|
196
|
-
this.cache.
|
|
171
|
+
this.cache.clear();
|
|
197
172
|
|
|
198
173
|
// unmount plugins
|
|
199
|
-
this.options.plugins.forEach((plugin) =>
|
|
200
|
-
this.unuse(plugin);
|
|
201
|
-
});
|
|
174
|
+
this.options.plugins.forEach((plugin) => this.unuse(plugin));
|
|
202
175
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
176
|
+
// trigger disable hook
|
|
177
|
+
await this.hooks.trigger('disable', undefined, () => {
|
|
178
|
+
// remove swup-enabled class from html tag
|
|
179
|
+
document.documentElement.classList.remove('swup-enabled');
|
|
206
180
|
});
|
|
207
181
|
|
|
208
182
|
// remove handlers
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
// trigger disable event
|
|
212
|
-
this.triggerEvent('disabled');
|
|
213
|
-
|
|
214
|
-
// remove swup-enabled class from html tag
|
|
215
|
-
document.documentElement.classList.remove('swup-enabled');
|
|
183
|
+
this.hooks.clear();
|
|
216
184
|
}
|
|
217
185
|
|
|
218
186
|
shouldIgnoreVisit(href: string, { el, event }: { el?: Element; event?: Event } = {}) {
|
|
@@ -238,17 +206,36 @@ export default class Swup {
|
|
|
238
206
|
}
|
|
239
207
|
|
|
240
208
|
linkClickHandler(event: DelegateEvent<MouseEvent>) {
|
|
241
|
-
const
|
|
242
|
-
const { href, url, hash } = Location.fromElement(
|
|
209
|
+
const el = event.delegateTarget as HTMLAnchorElement;
|
|
210
|
+
const { href, url, hash } = Location.fromElement(el);
|
|
211
|
+
|
|
212
|
+
// Get the animation name, if specified
|
|
213
|
+
const animation = el.getAttribute('data-swup-animation') || undefined;
|
|
214
|
+
|
|
215
|
+
// Get the history action, if specified
|
|
216
|
+
let historyAction: HistoryAction | undefined;
|
|
217
|
+
const historyAttr = el.getAttribute('data-swup-history');
|
|
218
|
+
if (historyAttr && ['push', 'replace'].includes(historyAttr)) {
|
|
219
|
+
historyAction = historyAttr as HistoryAction;
|
|
220
|
+
}
|
|
243
221
|
|
|
244
222
|
// Exit early if the link should be ignored
|
|
245
|
-
if (this.shouldIgnoreVisit(href, { el
|
|
223
|
+
if (this.shouldIgnoreVisit(href, { el, event })) {
|
|
246
224
|
return;
|
|
247
225
|
}
|
|
248
226
|
|
|
227
|
+
this.context = this.createContext({
|
|
228
|
+
to: url,
|
|
229
|
+
hash,
|
|
230
|
+
animation,
|
|
231
|
+
el,
|
|
232
|
+
event,
|
|
233
|
+
action: historyAction
|
|
234
|
+
});
|
|
235
|
+
|
|
249
236
|
// Exit early if control key pressed
|
|
250
237
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
251
|
-
this.
|
|
238
|
+
this.hooks.trigger('link:newtab', { href });
|
|
252
239
|
return;
|
|
253
240
|
}
|
|
254
241
|
|
|
@@ -257,53 +244,42 @@ export default class Swup {
|
|
|
257
244
|
return;
|
|
258
245
|
}
|
|
259
246
|
|
|
260
|
-
this.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Handle links to the same page and exit early, where applicable
|
|
264
|
-
if (!url || url === getCurrentUrl()) {
|
|
265
|
-
this.handleLinkToSamePage(url, hash, event);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Exit early if the resolved path hasn't changed
|
|
270
|
-
if (this.isSameResolvedUrl(url, getCurrentUrl())) return;
|
|
271
|
-
|
|
272
|
-
// Store the element that should be scrolled to after loading the next page
|
|
273
|
-
this.scrollToElement = hash || null;
|
|
247
|
+
this.hooks.triggerSync('link:click', { el, event }, () => {
|
|
248
|
+
const from = this.context.from.url ?? '';
|
|
274
249
|
|
|
275
|
-
|
|
276
|
-
const customTransition = linkEl.getAttribute('data-swup-transition') || undefined;
|
|
277
|
-
|
|
278
|
-
// Get the history action, if set
|
|
279
|
-
let history: HistoryAction | undefined;
|
|
280
|
-
const historyAttr = linkEl.getAttribute('data-swup-history');
|
|
281
|
-
if (historyAttr && ['push', 'replace'].includes(historyAttr)) {
|
|
282
|
-
history = historyAttr as HistoryAction;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Finally, proceed with loading the page
|
|
286
|
-
this.performPageLoad({ url, customTransition, history });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
handleLinkToSamePage(url: string, hash: string, event: DelegateEvent<MouseEvent>) {
|
|
290
|
-
// Emit event and exit early if the url points to the same page without hash
|
|
291
|
-
if (!hash) {
|
|
292
|
-
this.triggerEvent('samePage', event);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// link to the same URL with hash
|
|
297
|
-
this.triggerEvent('samePageWithHash', event);
|
|
298
|
-
|
|
299
|
-
const element = getAnchorElement(hash);
|
|
300
|
-
|
|
301
|
-
// Warn and exit early if no matching element was found for the hash
|
|
302
|
-
if (!element) {
|
|
303
|
-
return console.warn(`Element for offset not found (#${hash})`);
|
|
304
|
-
}
|
|
250
|
+
event.preventDefault();
|
|
305
251
|
|
|
306
|
-
|
|
252
|
+
// Handle links to the same page: with or without hash
|
|
253
|
+
if (!url || url === from) {
|
|
254
|
+
if (hash) {
|
|
255
|
+
updateHistoryRecord(url + hash);
|
|
256
|
+
this.hooks.triggerSync(
|
|
257
|
+
'link:anchor',
|
|
258
|
+
{ hash, options: { behavior: 'auto' } },
|
|
259
|
+
(context, { hash, options }) => {
|
|
260
|
+
const target = this.getAnchorElement(hash);
|
|
261
|
+
if (target) {
|
|
262
|
+
target.scrollIntoView(options);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
} else {
|
|
267
|
+
this.hooks.triggerSync('link:self', undefined, (context) => {
|
|
268
|
+
if (!context.scroll.reset) return;
|
|
269
|
+
window.scroll({ top: 0, left: 0, behavior: 'auto' });
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Exit early if the resolved path hasn't changed
|
|
276
|
+
if (this.isSameResolvedUrl(url, from)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Finally, proceed with loading the page
|
|
281
|
+
this.performVisit(url);
|
|
282
|
+
});
|
|
307
283
|
}
|
|
308
284
|
|
|
309
285
|
triggerWillOpenNewWindow(triggerEl: Element) {
|
|
@@ -314,6 +290,8 @@ export default class Swup {
|
|
|
314
290
|
}
|
|
315
291
|
|
|
316
292
|
popStateHandler(event: PopStateEvent) {
|
|
293
|
+
const href = event.state?.url ?? location.href;
|
|
294
|
+
|
|
317
295
|
// Exit early if this event should be ignored
|
|
318
296
|
if (this.options.skipPopStateHandling(event)) {
|
|
319
297
|
return;
|
|
@@ -324,29 +302,41 @@ export default class Swup {
|
|
|
324
302
|
return;
|
|
325
303
|
}
|
|
326
304
|
|
|
327
|
-
const href = event.state?.url ?? location.href;
|
|
328
|
-
|
|
329
305
|
// Exit early if the link should be ignored
|
|
330
306
|
if (this.shouldIgnoreVisit(href, { event })) {
|
|
331
307
|
return;
|
|
332
308
|
}
|
|
333
309
|
|
|
334
310
|
const { url, hash } = Location.fromUrl(href);
|
|
311
|
+
const animate = this.options.animateHistoryBrowsing;
|
|
312
|
+
const resetScroll = this.options.animateHistoryBrowsing;
|
|
313
|
+
|
|
314
|
+
this.context = this.createContext({
|
|
315
|
+
to: url,
|
|
316
|
+
hash,
|
|
317
|
+
event,
|
|
318
|
+
animate,
|
|
319
|
+
resetScroll
|
|
320
|
+
});
|
|
335
321
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
} else {
|
|
339
|
-
event.preventDefault();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
this.triggerEvent('popState', event);
|
|
322
|
+
// Mark as popstate visit
|
|
323
|
+
this.context.history.popstate = true;
|
|
343
324
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
325
|
+
// Determine direction of history visit
|
|
326
|
+
const index = Number(event.state?.index);
|
|
327
|
+
if (index) {
|
|
328
|
+
const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
|
|
329
|
+
this.context.history.direction = direction;
|
|
347
330
|
}
|
|
348
331
|
|
|
349
|
-
this
|
|
332
|
+
// Does this even do anything?
|
|
333
|
+
// if (!hash) {
|
|
334
|
+
// event.preventDefault();
|
|
335
|
+
// }
|
|
336
|
+
|
|
337
|
+
this.hooks.triggerSync('history:popstate', { event }, () => {
|
|
338
|
+
this.performVisit(url);
|
|
339
|
+
});
|
|
350
340
|
}
|
|
351
341
|
|
|
352
342
|
/**
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
import pckg from '../../package.json';
|
|
5
5
|
import Swup, { Options, Plugin } from '../index.js';
|
|
6
|
+
import * as SwupTS from '../Swup.js';
|
|
6
7
|
|
|
7
8
|
const baseUrl = window.location.origin;
|
|
8
9
|
|
|
@@ -37,7 +38,7 @@ describe('Exports', () => {
|
|
|
37
38
|
resolveUrl: (url) => url,
|
|
38
39
|
requestHeaders: {
|
|
39
40
|
'X-Requested-With': 'swup',
|
|
40
|
-
Accept: 'text/html, application/xhtml+xml'
|
|
41
|
+
'Accept': 'text/html, application/xhtml+xml'
|
|
41
42
|
},
|
|
42
43
|
skipPopStateHandling: (event) => event.state?.source !== 'swup'
|
|
43
44
|
};
|
|
@@ -51,6 +52,10 @@ describe('Exports', () => {
|
|
|
51
52
|
expect(swup.version).not.toBeUndefined();
|
|
52
53
|
expect(swup.version).toEqual(pckg.version);
|
|
53
54
|
});
|
|
55
|
+
|
|
56
|
+
it('UMD compatibility: Swup.ts should only have a default export', () => {
|
|
57
|
+
expect(Object.keys(SwupTS)).toEqual(['default']);
|
|
58
|
+
});
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
describe('ignoreVisit', () => {
|
|
@@ -79,10 +84,10 @@ describe('ignoreVisit', () => {
|
|
|
79
84
|
);
|
|
80
85
|
});
|
|
81
86
|
|
|
82
|
-
it('should be called from
|
|
87
|
+
it('should be called from visit method', () => {
|
|
83
88
|
const ignoreVisit = vi.fn(() => true);
|
|
84
89
|
const swup = new Swup({ ignoreVisit });
|
|
85
|
-
swup.
|
|
90
|
+
swup.visit('/path/');
|
|
86
91
|
|
|
87
92
|
expect(ignoreVisit.mock.calls).toHaveLength(1);
|
|
88
93
|
});
|
package/src/helpers/Location.ts
CHANGED
|
@@ -5,30 +5,33 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export class Location extends URL {
|
|
8
|
-
constructor(url: string, base: string = document.baseURI) {
|
|
8
|
+
constructor(url: URL | string, base: string = document.baseURI) {
|
|
9
9
|
super(url.toString(), base);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The full local path including query params.
|
|
14
|
+
*/
|
|
15
|
+
get url(): string {
|
|
13
16
|
return this.pathname + this.search;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Instantiate a Location from an element's href attribute
|
|
18
|
-
* @param
|
|
19
|
-
* @
|
|
21
|
+
* @param el
|
|
22
|
+
* @returns new Location instance
|
|
20
23
|
*/
|
|
21
|
-
static fromElement(el:
|
|
22
|
-
const href = el.getAttribute('href') || el.getAttribute('xlink:href');
|
|
24
|
+
static fromElement(el: Element): Location {
|
|
25
|
+
const href = el.getAttribute('href') || el.getAttribute('xlink:href') || '';
|
|
23
26
|
return new Location(href!);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Instantiate a Location from a URL object or string
|
|
28
|
-
* @param
|
|
29
|
-
* @
|
|
31
|
+
* @param url
|
|
32
|
+
* @returns new Location instance
|
|
30
33
|
*/
|
|
31
|
-
static fromUrl(url: string): Location {
|
|
34
|
+
static fromUrl(url: URL | string): Location {
|
|
32
35
|
return new Location(url);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { matchPath } from '../../index.js';
|
|
3
|
+
import { pathToRegexp } from 'path-to-regexp';
|
|
4
|
+
|
|
5
|
+
describe('matchPath', () => {
|
|
6
|
+
it('should return false if not matching', () => {
|
|
7
|
+
const urlMatch = matchPath('/users/:user');
|
|
8
|
+
const match = urlMatch('/posts/');
|
|
9
|
+
expect(match).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return an object if matching', () => {
|
|
13
|
+
const urlMatch = matchPath('/users/:user');
|
|
14
|
+
const match = urlMatch('/users/bob');
|
|
15
|
+
expect(match).toEqual({
|
|
16
|
+
path: '/users/bob',
|
|
17
|
+
index: 0,
|
|
18
|
+
params: { user: 'bob' }
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should work with primitive strings', () => {
|
|
23
|
+
const urlMatch = matchPath<{ user: string }>('/users/:user');
|
|
24
|
+
const match = urlMatch('/users/bob');
|
|
25
|
+
const params = !match ? false : match.params;
|
|
26
|
+
expect(params).toEqual({ user: 'bob' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should work with an array of paths', () => {
|
|
30
|
+
const urlMatch = matchPath<{ user: string }>(['/users/', '/users/:user']);
|
|
31
|
+
|
|
32
|
+
const { params: withParams } = urlMatch('/users/bob') || {};
|
|
33
|
+
expect(withParams).toEqual({ user: 'bob' });
|
|
34
|
+
|
|
35
|
+
const { params: withoutParams } = urlMatch('/users/') || {};
|
|
36
|
+
expect(withoutParams).toEqual({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* When passing a regex to `match`, the params in the response are sorted by appearance.
|
|
41
|
+
* Only helpful for falsy/truthy detection
|
|
42
|
+
*/
|
|
43
|
+
it('should work with regex', () => {
|
|
44
|
+
const re = pathToRegexp('/users/:user');
|
|
45
|
+
const urlMatch = matchPath(re);
|
|
46
|
+
const { params } = urlMatch('/users/bob') || {};
|
|
47
|
+
expect(params).toEqual({ '0': 'bob' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should throw with malformed paths', () => {
|
|
51
|
+
// prettier-ignore
|
|
52
|
+
expect(() => matchPath('/\?user=:user')).toThrowError('[swup] Error parsing path');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import delegate, { DelegateEventHandler, DelegateOptions, EventType } from 'delegate-it';
|
|
2
2
|
import { ParseSelector } from 'typed-query-selector/parser.js';
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type DelegateEventUnsubscribe = {
|
|
5
5
|
destroy: () => void;
|
|
6
6
|
};
|
|
7
7
|
|
|
@@ -10,8 +10,9 @@ export const delegateEvent = <Selector extends string, TEvent extends EventType>
|
|
|
10
10
|
type: TEvent,
|
|
11
11
|
callback: DelegateEventHandler<GlobalEventHandlersEventMap[TEvent]>,
|
|
12
12
|
options?: DelegateOptions
|
|
13
|
-
):
|
|
13
|
+
): DelegateEventUnsubscribe => {
|
|
14
14
|
const controller = new AbortController();
|
|
15
|
+
options = { ...options, signal: controller.signal };
|
|
15
16
|
delegate<string, ParseSelector<Selector, HTMLElement>, TEvent>(
|
|
16
17
|
selector,
|
|
17
18
|
type,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { match } from 'path-to-regexp';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
Path,
|
|
5
|
+
ParseOptions,
|
|
6
|
+
TokensToRegexpOptions,
|
|
7
|
+
RegexpToFunctionOptions,
|
|
8
|
+
MatchFunction
|
|
9
|
+
} from 'path-to-regexp';
|
|
10
|
+
|
|
11
|
+
export { Path };
|
|
12
|
+
|
|
13
|
+
export const matchPath = <P extends object = object>(
|
|
14
|
+
path: Path,
|
|
15
|
+
options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions
|
|
16
|
+
): MatchFunction<P> => {
|
|
17
|
+
try {
|
|
18
|
+
return match<P>(path, options);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new Error(`[swup] Error parsing path "${path}":\n${error}`);
|
|
21
|
+
}
|
|
22
|
+
};
|