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.
Files changed (79) hide show
  1. package/README.md +94 -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 +53 -45
  11. package/dist/types/helpers/Location.d.ts +10 -7
  12. package/dist/types/helpers/delegateEvent.d.ts +2 -2
  13. package/dist/types/helpers/matchPath.d.ts +3 -0
  14. package/dist/types/helpers.d.ts +1 -4
  15. package/dist/types/index.d.ts +7 -4
  16. package/dist/types/modules/Cache.d.ts +14 -14
  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__/cache.test.d.ts +1 -0
  21. package/dist/types/modules/__test__/hooks.test.d.ts +1 -0
  22. package/dist/types/modules/__test__/replaceContent.test.d.ts +1 -0
  23. package/dist/types/modules/awaitAnimations.d.ts +21 -0
  24. package/dist/types/modules/enterPage.d.ts +5 -2
  25. package/dist/types/modules/fetchPage.d.ts +23 -3
  26. package/dist/types/modules/getAnchorElement.d.ts +2 -1
  27. package/dist/types/modules/leavePage.d.ts +5 -2
  28. package/dist/types/modules/plugins.d.ts +7 -0
  29. package/dist/types/modules/renderPage.d.ts +6 -6
  30. package/dist/types/modules/replaceContent.d.ts +8 -11
  31. package/dist/types/modules/visit.d.ts +33 -0
  32. package/dist/types/utils/index.d.ts +3 -1
  33. package/package.json +13 -9
  34. package/src/Swup.ts +172 -182
  35. package/src/__test__/index.test.ts +8 -3
  36. package/src/helpers/Location.ts +12 -9
  37. package/src/helpers/__test__/matchPath.test.ts +54 -0
  38. package/src/helpers/delegateEvent.ts +3 -2
  39. package/src/helpers/matchPath.ts +22 -0
  40. package/src/helpers.ts +2 -5
  41. package/src/index.ts +36 -4
  42. package/src/modules/Cache.ts +43 -33
  43. package/src/modules/Classes.ts +48 -0
  44. package/src/modules/Context.ts +121 -0
  45. package/src/modules/Hooks.ts +413 -0
  46. package/src/modules/__test__/cache.test.ts +142 -0
  47. package/src/modules/__test__/hooks.test.ts +263 -0
  48. package/src/modules/__test__/replaceContent.test.ts +92 -0
  49. package/src/modules/awaitAnimations.ts +169 -0
  50. package/src/modules/enterPage.ts +23 -17
  51. package/src/modules/fetchPage.ts +74 -29
  52. package/src/modules/getAnchorElement.ts +2 -1
  53. package/src/modules/leavePage.ts +26 -20
  54. package/src/modules/plugins.ts +7 -2
  55. package/src/modules/renderPage.ts +52 -33
  56. package/src/modules/replaceContent.ts +33 -16
  57. package/src/modules/visit.ts +143 -0
  58. package/src/utils/index.ts +25 -5
  59. package/dist/types/helpers/cleanupAnimationClasses.d.ts +0 -2
  60. package/dist/types/helpers/fetch.d.ts +0 -5
  61. package/dist/types/helpers/getDataFromHtml.d.ts +0 -7
  62. package/dist/types/helpers/markSwupElements.d.ts +0 -1
  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/loadPage.d.ts +0 -15
  67. package/dist/types/modules/transitions.d.ts +0 -6
  68. package/readme.md +0 -60
  69. package/src/helpers/cleanupAnimationClasses.ts +0 -8
  70. package/src/helpers/fetch.ts +0 -33
  71. package/src/helpers/getDataFromHtml.ts +0 -39
  72. package/src/helpers/markSwupElements.ts +0 -16
  73. package/src/modules/__test__/events.test.ts +0 -72
  74. package/src/modules/events.ts +0 -92
  75. package/src/modules/getAnimationPromises.ts +0 -183
  76. package/src/modules/getPageData.ts +0 -24
  77. package/src/modules/loadPage.ts +0 -81
  78. package/src/modules/transitions.ts +0 -10
  79. /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
- cleanupAnimationClasses,
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 { enterPage } from './modules/enterPage.js';
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 { getAnimationPromises } from './modules/getAnimationPromises.js';
19
- import { getPageData } from './modules/getPageData.js';
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 { on, off, triggerEvent, Handlers } from './modules/events.js';
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 { updateTransition, shouldSkipTransition } from './modules/transitions.js';
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
- linkSelector: string;
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
- requestHeaders: Record<string, string>;
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
- version = version;
56
-
57
- _handlers: Handlers = {
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
- // running plugin instances
53
+ /** Registered plugin instances */
85
54
  plugins: Plugin[] = [];
86
- // variable for current transition info object
87
- transition: Transition = {};
88
- // cache instance
55
+ /** Global context of the current visit */
56
+ context: Context;
57
+ /** Cache instance */
89
58
  cache: Cache;
90
- // allows us to compare the current and new path inside popStateHandler
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
- // variable for keeping event listeners from "delegate"
93
- delegatedListeners: DelegatedListeners = {};
94
- // so we are able to remove the listener
95
- boundPopStateHandler: (event: PopStateEvent) => void;
65
+ /** Index of the current history entry */
66
+ currentHistoryIndex = 1;
67
+ /** Delegated event subscription handle */
68
+ private clickDelegate?: DelegateEventUnsubscribe;
96
69
 
97
- loadPage = loadPage;
98
- performPageLoad = performPageLoad;
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
- cleanupAnimationClasses = cleanupAnimationClasses;
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.boundPopStateHandler = this.popStateHandler.bind(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
- enable() {
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
- // Add event listeners
155
- this.delegatedListeners.click = delegateEvent(
156
- this.options.linkSelector,
157
- 'click',
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.boundPopStateHandler);
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 enabled event
179
- this.triggerEvent('enabled');
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
- // Add swup-enabled class to html tag
182
- document.documentElement.classList.add('swup-enabled');
157
+ await nextTick();
183
158
 
184
- // Trigger page view event
185
- this.triggerEvent('pageView');
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 listeners
190
- this.delegatedListeners.click!.destroy();
163
+ async destroy() {
164
+ // remove delegated listener
165
+ this.clickDelegate!.destroy();
191
166
 
192
167
  // remove popstate listener
193
- window.removeEventListener('popstate', this.boundPopStateHandler);
168
+ window.removeEventListener('popstate', this.popStateHandler);
194
169
 
195
170
  // empty cache
196
- this.cache.empty();
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
- // remove swup data atributes from blocks
204
- queryAll('[data-swup]').forEach((element) => {
205
- element.removeAttribute('data-swup');
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.off();
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 linkEl = event.delegateTarget;
242
- const { href, url, hash } = Location.fromElement(linkEl as HTMLAnchorElement);
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: linkEl, event })) {
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.triggerEvent('openPageInNewTab', event);
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.triggerEvent('clickLink', event);
261
- event.preventDefault();
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
- // Get the custom transition name, if present
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
- updateHistoryRecord(url + hash);
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
- if (hash) {
337
- this.scrollToElement = hash;
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
- if (!this.options.animateHistoryBrowsing) {
345
- document.documentElement.classList.remove('is-animating');
346
- cleanupAnimationClasses();
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.performPageLoad({ url, event });
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 loadPage method', () => {
87
+ it('should be called from visit method', () => {
83
88
  const ignoreVisit = vi.fn(() => true);
84
89
  const swup = new Swup({ ignoreVisit });
85
- swup.loadPage({ url: '/path/' });
90
+ swup.visit('/path/');
86
91
 
87
92
  expect(ignoreVisit.mock.calls).toHaveLength(1);
88
93
  });
@@ -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
- get url() {
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 {Element} el
19
- * @return new Location instance
21
+ * @param el
22
+ * @returns new Location instance
20
23
  */
21
- static fromElement(el: HTMLAnchorElement): Location {
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 {URL|string} url
29
- * @return new Location instance
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 Unsubscribe = {
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
- ): Unsubscribe => {
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
+ };