summaryrefslogtreecommitdiff
path: root/node_modules/swup/src/modules
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2024-08-30 17:21:18 +0800
committerlittleblack111 <littleblack11111@gmail.com>2024-08-30 17:21:18 +0800
commitefbc2ffe6a1ee5ec38a4396305561d6515d0943e (patch)
tree2b4c94f18f2c792dfda813050755d5c0273fae22 /node_modules/swup/src/modules
parent43acceb75957c880a5ba5a31aae221394a36d5f6 (diff)
node fixes, updates
Diffstat (limited to 'node_modules/swup/src/modules')
-rwxr-xr-xnode_modules/swup/src/modules/Cache.ts87
-rw-r--r--node_modules/swup/src/modules/Classes.ts54
-rw-r--r--node_modules/swup/src/modules/Hooks.ts573
-rw-r--r--node_modules/swup/src/modules/Visit.ts170
-rwxr-xr-xnode_modules/swup/src/modules/animatePageIn.ts32
-rwxr-xr-xnode_modules/swup/src/modules/animatePageOut.ts19
-rw-r--r--node_modules/swup/src/modules/awaitAnimations.ts148
-rw-r--r--node_modules/swup/src/modules/fetchPage.ts114
-rwxr-xr-xnode_modules/swup/src/modules/getAnchorElement.ts32
-rwxr-xr-xnode_modules/swup/src/modules/navigate.ts234
-rw-r--r--node_modules/swup/src/modules/plugins.ts77
-rwxr-xr-xnode_modules/swup/src/modules/renderPage.ts51
-rwxr-xr-xnode_modules/swup/src/modules/replaceContent.ts51
-rwxr-xr-xnode_modules/swup/src/modules/resolveUrl.ts33
-rwxr-xr-xnode_modules/swup/src/modules/scrollToContent.ts38
15 files changed, 1713 insertions, 0 deletions
diff --git a/node_modules/swup/src/modules/Cache.ts b/node_modules/swup/src/modules/Cache.ts
new file mode 100755
index 00000000..913e10a9
--- /dev/null
+++ b/node_modules/swup/src/modules/Cache.ts
@@ -0,0 +1,87 @@
+import type Swup from '../Swup.js';
+import { Location } from '../helpers.js';
+import { type PageData } from './fetchPage.js';
+
+export interface CacheData extends PageData {}
+
+/**
+ * In-memory page cache.
+ */
+export class Cache {
+ /** Swup instance this cache belongs to */
+ protected swup: Swup;
+
+ /** Cached pages, indexed by URL */
+ protected pages: Map<string, CacheData> = new Map();
+
+ constructor(swup: Swup) {
+ this.swup = swup;
+ }
+
+ /** Number of cached pages in memory. */
+ get size(): number {
+ return this.pages.size;
+ }
+
+ /** All cached pages. */
+ get all() {
+ const copy = new Map();
+ this.pages.forEach((page, key) => {
+ copy.set(key, { ...page });
+ });
+ return copy;
+ }
+
+ /** Check if the given URL has been cached. */
+ has(url: string): boolean {
+ return this.pages.has(this.resolve(url));
+ }
+
+ /** Return a shallow copy of the cached page object if available. */
+ get(url: string): CacheData | undefined {
+ const result = this.pages.get(this.resolve(url));
+ if (!result) return result;
+ return { ...result };
+ }
+
+ /** Create a cache record for the specified URL. */
+ set(url: string, page: CacheData) {
+ url = this.resolve(url);
+ page = { ...page, url };
+ this.pages.set(url, page);
+ this.swup.hooks.callSync('cache:set', undefined, { page });
+ }
+
+ /** Update a cache record, overwriting or adding custom data. */
+ update(url: string, payload: object) {
+ url = this.resolve(url);
+ const page = { ...this.get(url), ...payload, url } as CacheData;
+ this.pages.set(url, page);
+ }
+
+ /** Delete a cache record. */
+ delete(url: string): void {
+ this.pages.delete(this.resolve(url));
+ }
+
+ /** Empty the cache. */
+ clear(): void {
+ this.pages.clear();
+ this.swup.hooks.callSync('cache:clear', undefined, undefined);
+ }
+
+ /** Remove all cache entries that return true for a given predicate function. */
+ prune(predicate: (url: string, page: CacheData) => boolean): void {
+ this.pages.forEach((page, url) => {
+ if (predicate(url, page)) {
+ this.delete(url);
+ }
+ });
+ }
+
+ /** Resolve URLs by making them local and letting swup resolve them. */
+ protected resolve(urlToResolve: string): string {
+ const { url } = Location.fromUrl(urlToResolve);
+ return this.swup.resolveUrl(url);
+ }
+}
diff --git a/node_modules/swup/src/modules/Classes.ts b/node_modules/swup/src/modules/Classes.ts
new file mode 100644
index 00000000..7e034511
--- /dev/null
+++ b/node_modules/swup/src/modules/Classes.ts
@@ -0,0 +1,54 @@
+import type Swup from '../Swup.js';
+import { queryAll } from '../utils.js';
+
+export class Classes {
+ protected swup: Swup;
+ protected swupClasses = [
+ 'to-',
+ 'is-changing',
+ 'is-rendering',
+ 'is-popstate',
+ 'is-animating',
+ 'is-leaving'
+ ];
+
+ constructor(swup: Swup) {
+ this.swup = swup;
+ }
+
+ protected get selectors(): string[] {
+ const { scope } = this.swup.visit.animation;
+ if (scope === 'containers') return this.swup.visit.containers;
+ if (scope === 'html') return ['html'];
+ if (Array.isArray(scope)) return scope;
+ return [];
+ }
+
+ protected get selector(): string {
+ return this.selectors.join(',');
+ }
+
+ protected get targets(): HTMLElement[] {
+ if (!this.selector.trim()) return [];
+ return queryAll(this.selector);
+ }
+
+ add(...classes: string[]): void {
+ this.targets.forEach((target) => target.classList.add(...classes));
+ }
+
+ remove(...classes: string[]): void {
+ this.targets.forEach((target) => target.classList.remove(...classes));
+ }
+
+ clear(): void {
+ this.targets.forEach((target) => {
+ const remove = target.className.split(' ').filter((c) => this.isSwupClass(c));
+ target.classList.remove(...remove);
+ });
+ }
+
+ protected isSwupClass(className: string): boolean {
+ return this.swupClasses.some((c) => className.startsWith(c));
+ }
+}
diff --git a/node_modules/swup/src/modules/Hooks.ts b/node_modules/swup/src/modules/Hooks.ts
new file mode 100644
index 00000000..36af2bf7
--- /dev/null
+++ b/node_modules/swup/src/modules/Hooks.ts
@@ -0,0 +1,573 @@
+import type { DelegateEvent } from 'delegate-it';
+
+import type Swup from '../Swup.js';
+import { isPromise, runAsPromise } from '../utils.js';
+import { Visit } from './Visit.js';
+import type { FetchOptions, PageData } from './fetchPage.js';
+
+export interface HookDefinitions {
+ 'animation:out:start': undefined;
+ 'animation:out:await': { skip: boolean };
+ 'animation:out:end': undefined;
+ 'animation:in:start': undefined;
+ 'animation:in:await': { skip: boolean };
+ 'animation:in:end': undefined;
+ 'animation:skip': undefined;
+ 'cache:clear': undefined;
+ 'cache:set': { page: PageData };
+ 'content:replace': { page: PageData };
+ 'content:scroll': undefined;
+ 'enable': undefined;
+ 'disable': undefined;
+ 'fetch:request': { url: string; options: FetchOptions };
+ 'fetch:error': { url: string; status: number; response: Response };
+ 'fetch:timeout': { url: string };
+ 'history:popstate': { event: PopStateEvent };
+ 'link:click': { el: HTMLAnchorElement; event: DelegateEvent<MouseEvent> };
+ 'link:self': undefined;
+ 'link:anchor': { hash: string };
+ 'link:newtab': { href: string };
+ 'page:load': { page?: PageData; cache?: boolean; options: FetchOptions };
+ 'page:view': { url: string; title: string };
+ 'scroll:top': { options: ScrollIntoViewOptions };
+ 'scroll:anchor': { hash: string; options: ScrollIntoViewOptions };
+ 'visit:start': undefined;
+ 'visit:transition': undefined;
+ 'visit:abort': undefined;
+ 'visit:end': undefined;
+}
+
+export interface HookReturnValues {
+ 'content:scroll': Promise<boolean> | boolean;
+ 'fetch:request': Promise<Response>;
+ 'page:load': Promise<PageData>;
+ 'scroll:top': boolean;
+ 'scroll:anchor': boolean;
+}
+
+export type HookArguments<T extends HookName> = HookDefinitions[T];
+
+export type HookName = keyof HookDefinitions;
+
+/** A generic hook handler. */
+export type HookHandler<T extends HookName> = (
+ /** Context about the current visit. */
+ visit: Visit,
+ /** Local arguments passed into the handler. */
+ args: HookArguments<T>
+) => Promise<unknown> | unknown;
+
+/** A default hook handler with an expected return type. */
+export type HookDefaultHandler<T extends HookName> = (
+ /** Context about the current visit. */
+ visit: Visit,
+ /** Local arguments passed into the handler. */
+ args: HookArguments<T>,
+ /** Default handler to be executed. Available if replacing an internal hook handler. */
+ defaultHandler?: HookDefaultHandler<T>
+) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise<unknown> | unknown;
+
+export type Handlers = {
+ [K in HookName]: HookHandler<K>[];
+};
+
+/** Unregister a previously registered hook handler. */
+export type HookUnregister = () => void;
+
+/** Define when and how a hook handler is executed. */
+export type HookOptions = {
+ /** Execute the hook once, then remove the handler */
+ once?: boolean;
+ /** Execute the hook before the internal default handler */
+ before?: boolean;
+ /** Set a priority for when to execute this hook. Lower numbers execute first. Default: `0` */
+ priority?: number;
+ /** Replace the internal default handler with this hook handler */
+ replace?: boolean;
+};
+
+export type HookRegistration<
+ T extends HookName,
+ H extends HookHandler<T> | HookDefaultHandler<T> = HookHandler<T>
+> = {
+ id: number;
+ hook: T;
+ handler: H;
+ defaultHandler?: HookDefaultHandler<T>;
+} & HookOptions;
+
+type HookEventDetail = {
+ hook: HookName;
+ args: unknown;
+ visit: Visit;
+};
+
+export type HookEvent = CustomEvent<HookEventDetail>;
+
+type HookLedger<T extends HookName> = Map<HookHandler<T>, HookRegistration<T>>;
+
+interface HookRegistry extends Map<HookName, HookLedger<HookName>> {
+ get<K extends HookName>(key: K): HookLedger<K> | undefined;
+ set<K extends HookName>(key: K, value: HookLedger<K>): this;
+}
+
+/**
+ * Hook registry.
+ *
+ * Create, trigger and handle hooks.
+ *
+ */
+export class Hooks {
+ /** Swup instance this registry belongs to */
+ protected swup: Swup;
+
+ /** Map of all registered hook handlers. */
+ protected registry: HookRegistry = new Map();
+
+ // Can we deduplicate this somehow? Or make it error when not in sync with HookDefinitions?
+ // https://stackoverflow.com/questions/53387838/how-to-ensure-an-arrays-values-the-keys-of-a-typescript-interface/53395649
+ protected readonly hooks: HookName[] = [
+ 'animation:out:start',
+ 'animation:out:await',
+ 'animation:out:end',
+ 'animation:in:start',
+ 'animation:in:await',
+ 'animation:in:end',
+ 'animation:skip',
+ 'cache:clear',
+ 'cache:set',
+ 'content:replace',
+ 'content:scroll',
+ 'enable',
+ 'disable',
+ 'fetch:request',
+ 'fetch:error',
+ 'fetch:timeout',
+ 'history:popstate',
+ 'link:click',
+ 'link:self',
+ 'link:anchor',
+ 'link:newtab',
+ 'page:load',
+ 'page:view',
+ 'scroll:top',
+ 'scroll:anchor',
+ 'visit:start',
+ 'visit:transition',
+ 'visit:abort',
+ 'visit:end'
+ ];
+
+ constructor(swup: Swup) {
+ this.swup = swup;
+ this.init();
+ }
+
+ /**
+ * Create ledgers for all core hooks.
+ */
+ protected init() {
+ this.hooks.forEach((hook) => this.create(hook));
+ }
+
+ /**
+ * Create a new hook type.
+ */
+ create(hook: string) {
+ if (!this.registry.has(hook as HookName)) {
+ this.registry.set(hook as HookName, new Map());
+ }
+ }
+
+ /**
+ * Check if a hook type exists.
+ */
+ exists(hook: HookName): boolean {
+ return this.registry.has(hook);
+ }
+
+ /**
+ * Get the ledger with all registrations for a hook.
+ */
+ protected get<T extends HookName>(hook: T): HookLedger<T> | undefined {
+ const ledger = this.registry.get(hook);
+ if (ledger) {
+ return ledger;
+ }
+ console.error(`Unknown hook '${hook}'`);
+ }
+
+ /**
+ * Remove all handlers of all hooks.
+ */
+ clear() {
+ this.registry.forEach((ledger) => ledger.clear());
+ }
+
+ /**
+ * Register a new hook handler.
+ * @param hook Name of the hook to listen for
+ * @param handler The handler function to execute
+ * @param options Object to specify how and when the handler is executed
+ * Available options:
+ * - `once`: Only execute the handler once
+ * - `before`: Execute the handler before the default handler
+ * - `priority`: Specify the order in which the handlers are executed
+ * - `replace`: Replace the default handler with this handler
+ * @returns A function to unregister the handler
+ */
+
+ // Overload: replacing default handler
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: HookDefaultHandler<T>, options: O & { replace: true }): HookUnregister; // prettier-ignore
+ // Overload: passed in handler options
+ on<T extends HookName, O extends HookOptions>(hook: T, handler: HookHandler<T>, options: O): HookUnregister; // prettier-ignore
+ // Overload: no handler options
+ on<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister; // prettier-ignore
+ // Implementation
+ on<T extends HookName, O extends HookOptions>(
+ hook: T,
+ handler: O['replace'] extends true ? HookDefaultHandler<T> : HookHandler<T>,
+ options: Partial<O> = {}
+ ): HookUnregister {
+ const ledger = this.get(hook);
+ if (!ledger) {
+ console.warn(`Hook '${hook}' not found.`);
+ return () => {};
+ }
+
+ const id = ledger.size + 1;
+ const registration: HookRegistration<T> = { ...options, id, hook, handler };
+ ledger.set(handler, registration);
+
+ return () => this.off(hook, handler);
+ }
+
+ /**
+ * Register a new hook handler to run before the default handler.
+ * Shortcut for `hooks.on(hook, handler, { before: true })`.
+ * @param hook Name of the hook to listen for
+ * @param handler The handler function to execute
+ * @param options Any other event options (see `hooks.on()` for details)
+ * @returns A function to unregister the handler
+ * @see on
+ */
+ // Overload: passed in handler options
+ before<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore
+ // Overload: no handler options
+ before<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister;
+ // Implementation
+ before<T extends HookName>(
+ hook: T,
+ handler: HookHandler<T>,
+ options: HookOptions = {}
+ ): HookUnregister {
+ return this.on(hook, handler, { ...options, before: true });
+ }
+
+ /**
+ * Register a new hook handler to replace the default handler.
+ * Shortcut for `hooks.on(hook, handler, { replace: true })`.
+ * @param hook Name of the hook to listen for
+ * @param handler The handler function to execute instead of the default handler
+ * @param options Any other event options (see `hooks.on()` for details)
+ * @returns A function to unregister the handler
+ * @see on
+ */
+ // Overload: passed in handler options
+ replace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore
+ // Overload: no handler options
+ replace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>): HookUnregister; // prettier-ignore
+ // Implementation
+ replace<T extends HookName>(
+ hook: T,
+ handler: HookDefaultHandler<T>,
+ options: HookOptions = {}
+ ): HookUnregister {
+ return this.on(hook, handler, { ...options, replace: true });
+ }
+
+ /**
+ * Register a new hook handler to run once.
+ * Shortcut for `hooks.on(hook, handler, { once: true })`.
+ * @param hook Name of the hook to listen for
+ * @param handler The handler function to execute
+ * @param options Any other event options (see `hooks.on()` for details)
+ * @see on
+ */
+ // Overload: passed in handler options
+ once<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore
+ // Overload: no handler options
+ once<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister;
+ // Implementation
+ once<T extends HookName>(
+ hook: T,
+ handler: HookHandler<T>,
+ options: HookOptions = {}
+ ): HookUnregister {
+ return this.on(hook, handler, { ...options, once: true });
+ }
+
+ /**
+ * Unregister a hook handler.
+ * @param hook Name of the hook the handler is registered for
+ * @param handler The handler function that was registered.
+ * If omitted, all handlers for the hook will be removed.
+ */
+ // Overload: unregister a specific handler
+ off<T extends HookName>(hook: T, handler: HookHandler<T> | HookDefaultHandler<T>): void;
+ // Overload: unregister all handlers
+ off<T extends HookName>(hook: T): void;
+ // Implementation
+ off<T extends HookName>(hook: T, handler?: HookHandler<T> | HookDefaultHandler<T>): void {
+ const ledger = this.get(hook);
+ if (ledger && handler) {
+ const deleted = ledger.delete(handler);
+ if (!deleted) {
+ console.warn(`Handler for hook '${hook}' not found.`);
+ }
+ } else if (ledger) {
+ ledger.clear();
+ }
+ }
+
+ /**
+ * Trigger a hook asynchronously, executing its default handler and all registered handlers.
+ * Will execute all handlers in order and `await` any `Promise`s they return.
+ * @param hook Name of the hook to trigger
+ * @param visit The visit object this hook belongs to
+ * @param args Arguments to pass to the handler
+ * @param defaultHandler A default implementation of this hook to execute
+ * @returns The resolved return value of the executed default handler
+ */
+ // Overload: default order of arguments
+ async call<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore
+ // Overload: legacy order of arguments, with visit missing
+ async call<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore
+ // Implementation
+ async call<T extends HookName>(
+ hook: T,
+ arg1: Visit | HookArguments<T>,
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
+ arg3?: HookDefaultHandler<T>
+ ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>> {
+ const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);
+
+ const { before, handler, after } = this.getHandlers(hook, defaultHandler);
+ await this.run(before, visit, args);
+ const [result] = await this.run(handler, visit, args, true);
+ await this.run(after, visit, args);
+ this.dispatchDomEvent(hook, visit, args);
+ return result;
+ }
+
+ /**
+ * Trigger a hook synchronously, executing its default handler and all registered handlers.
+ * Will execute all handlers in order, but will **not** `await` any `Promise`s they return.
+ * @param hook Name of the hook to trigger
+ * @param visit The visit object this hook belongs to
+ * @param args Arguments to pass to the handler
+ * @param defaultHandler A default implementation of this hook to execute
+ * @returns The (possibly unresolved) return value of the executed default handler
+ */
+ // Overload: default order of arguments
+ callSync<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore
+ // Overload: legacy order of arguments, with visit missing
+ callSync<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore
+ // Implementation
+ callSync<T extends HookName>(
+ hook: T,
+ arg1: Visit | HookArguments<T>,
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
+ arg3?: HookDefaultHandler<T>
+ ): ReturnType<HookDefaultHandler<T>> {
+ const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);
+ const { before, handler, after } = this.getHandlers(hook, defaultHandler);
+ this.runSync(before, visit, args);
+ const [result] = this.runSync(handler, visit, args, true);
+ this.runSync(after, visit, args);
+ this.dispatchDomEvent(hook, visit, args);
+ return result;
+ }
+
+ /**
+ * Parse the call arguments for call() and callSync() to allow legacy argument order.
+ */
+ protected parseCallArgs<T extends HookName>(
+ hook: T,
+ arg1: Visit | HookArguments<T> | undefined,
+ arg2: HookArguments<T> | HookDefaultHandler<T>,
+ arg3?: HookDefaultHandler<T>
+ ): [Visit | undefined, HookArguments<T>, HookDefaultHandler<T> | undefined] {
+ const isLegacyOrder =
+ !(arg1 instanceof Visit) && (typeof arg1 === 'object' || typeof arg2 === 'function');
+ if (isLegacyOrder) {
+ // Legacy positioning: arguments in second or handler passed in third place
+ return [undefined, arg1 as HookArguments<T>, arg2 as HookDefaultHandler<T>];
+ } else {
+ // Default positioning: visit passed in as first argument
+ return [arg1, arg2 as HookArguments<T>, arg3];
+ }
+ }
+
+ /**
+ * Execute the handlers for a hook, in order, as `Promise`s that will be `await`ed.
+ * @param registrations The registrations (handler + options) to execute
+ * @param args Arguments to pass to the handler
+ */
+
+ // Overload: running HookDefaultHandler: expect HookDefaultHandler return type
+ protected async run<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>[]>; // prettier-ignore
+ // Overload: running user handler: expect no specific type
+ protected async run<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore
+ // Implementation
+ protected async run<T extends HookName, R extends HookRegistration<T>[]>(
+ registrations: R,
+ visit: Visit | undefined = this.swup.visit,
+ args: HookArguments<T>,
+ rethrow: boolean = false
+ ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>> | unknown[]> {
+ const results = [];
+ for (const { hook, handler, defaultHandler, once } of registrations) {
+ if (visit?.done) continue;
+ if (once) this.off(hook, handler);
+ try {
+ const result = await runAsPromise(handler, [visit, args, defaultHandler]);
+ results.push(result);
+ } catch (error) {
+ if (rethrow) {
+ throw error;
+ } else {
+ console.error(`Error in hook '${hook}':`, error);
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Execute the handlers for a hook, in order, without `await`ing any returned `Promise`s.
+ * @param registrations The registrations (handler + options) to execute
+ * @param args Arguments to pass to the handler
+ */
+
+ // Overload: running HookDefaultHandler: expect HookDefaultHandler return type
+ protected runSync<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): ReturnType<HookDefaultHandler<T>>[]; // prettier-ignore
+ // Overload: running user handler: expect no specific type
+ protected runSync<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): unknown[]; // prettier-ignore
+ // Implementation
+ protected runSync<T extends HookName, R extends HookRegistration<T>[]>(
+ registrations: R,
+ visit: Visit | undefined = this.swup.visit,
+ args: HookArguments<T>,
+ rethrow: boolean = false
+ ): (ReturnType<HookDefaultHandler<T>> | unknown)[] {
+ const results = [];
+ for (const { hook, handler, defaultHandler, once } of registrations) {
+ if (visit?.done) continue;
+ if (once) this.off(hook, handler);
+ try {
+ const result = (handler as HookDefaultHandler<T>)(visit, args, defaultHandler);
+ results.push(result);
+ if (isPromise(result)) {
+ console.warn(
+ `Swup will not await Promises in handler for synchronous hook '${hook}'.`
+ );
+ }
+ } catch (error) {
+ if (rethrow) {
+ throw error;
+ } else {
+ console.error(`Error in hook '${hook}':`, error);
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Get all registered handlers for a hook, sorted by priority and registration order.
+ * @param hook Name of the hook
+ * @param defaultHandler The optional default handler of this hook
+ * @returns An object with the handlers sorted into `before` and `after` arrays,
+ * as well as a flag indicating if the original handler was replaced
+ */
+ protected getHandlers<T extends HookName>(hook: T, defaultHandler?: HookDefaultHandler<T>) {
+ const ledger = this.get(hook);
+ if (!ledger) {
+ return { found: false, before: [], handler: [], after: [], replaced: false };
+ }
+
+ const registrations = Array.from(ledger.values());
+
+ // Let TypeScript know that replaced handlers are default handlers by filtering to true
+ const def = (T: HookRegistration<T>): T is HookRegistration<T, HookDefaultHandler<T>> => true; // prettier-ignore
+ const sort = this.sortRegistrations;
+
+ // Filter into before, after, and replace handlers
+ const before = registrations.filter(({ before, replace }) => before && !replace).sort(sort);
+ const replace = registrations.filter(({ replace }) => replace).filter(def).sort(sort); // prettier-ignore
+ const after = registrations.filter(({ before, replace }) => !before && !replace).sort(sort);
+ const replaced = replace.length > 0;
+
+ // Define main handler registration
+ // Created as HookRegistration[] array to allow passing it into hooks.run() directly
+ let handler: HookRegistration<T, HookDefaultHandler<T>>[] = [];
+ if (defaultHandler) {
+ handler = [{ id: 0, hook, handler: defaultHandler }];
+ if (replaced) {
+ const index = replace.length - 1;
+ const replacingHandler = replace[index].handler;
+ const createDefaultHandler = (index: number): HookDefaultHandler<T> | undefined => {
+ const next = replace[index - 1];
+ if (next) {
+ return (visit, args) =>
+ next.handler(visit, args, createDefaultHandler(index - 1));
+ } else {
+ return defaultHandler;
+ }
+ };
+ const nestedDefaultHandler = createDefaultHandler(index);
+ handler = [
+ { id: 0, hook, handler: replacingHandler, defaultHandler: nestedDefaultHandler }
+ ];
+ }
+ }
+
+ return { found: true, before, handler, after, replaced };
+ }
+
+ /**
+ * Sort two hook registrations by priority and registration order.
+ * @param a The registration object to compare
+ * @param b The other registration object to compare with
+ * @returns The sort direction
+ */
+ protected sortRegistrations<T extends HookName>(
+ a: HookRegistration<T>,
+ b: HookRegistration<T>
+ ): number {
+ const priority = (a.priority ?? 0) - (b.priority ?? 0);
+ const id = a.id - b.id;
+ return priority || id || 0;
+ }
+
+ /**
+ * Dispatch a custom event on the `document` for a hook. Prefixed with `swup:`
+ * @param hook Name of the hook.
+ */
+ protected dispatchDomEvent<T extends HookName>(
+ hook: T,
+ visit: Visit | undefined,
+ args?: HookArguments<T>
+ ): void {
+ if (visit?.done) return;
+
+ const detail: HookEventDetail = { hook, args, visit: visit || this.swup.visit };
+ document.dispatchEvent(
+ new CustomEvent<HookEventDetail>(`swup:any`, { detail, bubbles: true })
+ );
+ document.dispatchEvent(
+ new CustomEvent<HookEventDetail>(`swup:${hook}`, { detail, bubbles: true })
+ );
+ }
+}
diff --git a/node_modules/swup/src/modules/Visit.ts b/node_modules/swup/src/modules/Visit.ts
new file mode 100644
index 00000000..b0d5d271
--- /dev/null
+++ b/node_modules/swup/src/modules/Visit.ts
@@ -0,0 +1,170 @@
+import type Swup from '../Swup.js';
+import type { Options } from '../Swup.js';
+import type { HistoryAction, HistoryDirection } from './navigate.js';
+
+/** See below for the class Visit {} definition */
+// export interface Visit {}
+
+export interface VisitFrom {
+ /** The URL of the previous page */
+ url: string;
+ /** The hash of the previous page */
+ hash?: string;
+}
+
+export interface VisitTo {
+ /** The URL of the next page */
+ url: string;
+ /** The hash of the next page */
+ hash?: string;
+ /** The HTML content of the next page */
+ html?: string;
+ /** The parsed document of the next page, available during visit */
+ document?: Document;
+}
+
+export interface VisitAnimation {
+ /** Whether this visit is animated. Default: `true` */
+ animate: boolean;
+ /** Whether to wait for the next page to load before starting the animation. Default: `false` */
+ wait: boolean;
+ /** Name of a custom animation to run. */
+ name?: string;
+ /** Whether this animation uses the native browser ViewTransition API. Default: `false` */
+ native: boolean;
+ /** Elements on which to add animation classes. Default: `html` element */
+ scope: 'html' | 'containers' | string[];
+ /** Selector for detecting animation timing. Default: `[class*="transition-"]` */
+ selector: Options['animationSelector'];
+}
+
+export interface VisitScroll {
+ /** Whether to reset the scroll position after the visit. Default: `true` */
+ reset: boolean;
+ /** Anchor element to scroll to on the next page. */
+ target?: string | false;
+}
+
+export interface VisitTrigger {
+ /** DOM element that triggered this visit. */
+ el?: Element;
+ /** DOM event that triggered this visit. */
+ event?: Event;
+}
+
+export interface VisitCache {
+ /** Whether this visit will try to load the requested page from cache. */
+ read: boolean;
+ /** Whether this visit will save the loaded page in cache. */
+ write: boolean;
+}
+
+export interface VisitHistory {
+ /** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
+ action: HistoryAction;
+ /** Whether this visit was triggered by a browser history navigation. */
+ popstate: boolean;
+ /** The direction of travel in case of a browser history navigation: backward or forward. */
+ direction: HistoryDirection | undefined;
+}
+
+export interface VisitInitOptions {
+ to: string;
+ from?: string;
+ hash?: string;
+ el?: Element;
+ event?: Event;
+}
+
+/** @internal */
+export const VisitState = {
+ CREATED: 1,
+ QUEUED: 2,
+ STARTED: 3,
+ LEAVING: 4,
+ LOADED: 5,
+ ENTERING: 6,
+ COMPLETED: 7,
+ ABORTED: 8,
+ FAILED: 9
+} as const;
+
+/** @internal */
+export type VisitState = (typeof VisitState)[keyof typeof VisitState];
+
+/** An object holding details about the current visit. */
+export class Visit {
+ /** A unique ID to identify this visit */
+ id: number;
+ /** The current state of this visit @internal */
+ state: VisitState;
+ /** The previous page, about to leave */
+ from: VisitFrom;
+ /** The next page, about to enter */
+ to: VisitTo;
+ /** The content containers, about to be replaced */
+ containers: Options['containers'];
+ /** Information about animated page transitions */
+ animation: VisitAnimation;
+ /** What triggered this visit */
+ trigger: VisitTrigger;
+ /** Cache behavior for this visit */
+ cache: VisitCache;
+ /** Browser history behavior on this visit */
+ history: VisitHistory;
+ /** Scroll behavior on this visit */
+ scroll: VisitScroll;
+
+ constructor(swup: Swup, options: VisitInitOptions) {
+ const { to, from, hash, el, event } = options;
+
+ this.id = Math.random();
+ this.state = VisitState.CREATED;
+ this.from = { url: from ?? swup.location.url, hash: swup.location.hash };
+ this.to = { url: to, hash };
+ this.containers = swup.options.containers;
+ this.animation = {
+ animate: true,
+ wait: false,
+ name: undefined,
+ native: swup.options.native,
+ scope: swup.options.animationScope,
+ selector: swup.options.animationSelector
+ };
+ this.trigger = { el, event };
+ this.cache = {
+ read: swup.options.cache,
+ write: swup.options.cache
+ };
+ this.history = {
+ action: 'push',
+ popstate: false,
+ direction: undefined
+ };
+ this.scroll = {
+ reset: true,
+ target: undefined
+ };
+ }
+ /** @internal */
+ advance(state: VisitState) {
+ if (this.state < state) {
+ this.state = state;
+ }
+ }
+
+ /** @internal */
+ abort() {
+ this.state = VisitState.ABORTED;
+ }
+
+ /** Is this visit done, i.e. completed, failed, or aborted? */
+ get done(): boolean {
+ return this.state >= VisitState.COMPLETED;
+ }
+}
+
+/** Create a new visit object. */
+export function createVisit(this: Swup, options: VisitInitOptions): Visit {
+ return new Visit(this, options);
+}
diff --git a/node_modules/swup/src/modules/animatePageIn.ts b/node_modules/swup/src/modules/animatePageIn.ts
new file mode 100755
index 00000000..fc384b73
--- /dev/null
+++ b/node_modules/swup/src/modules/animatePageIn.ts
@@ -0,0 +1,32 @@
+import type Swup from '../Swup.js';
+import { nextTick } from '../utils.js';
+import type { Visit } from './Visit.js';
+
+/**
+ * Perform the in/enter animation of the next page.
+ * @returns Promise<void>
+ */
+export const animatePageIn = async function (this: Swup, visit: Visit) {
+ // Check if failed/aborted in the meantime
+ if (visit.done) return;
+
+ const animation = this.hooks.call(
+ 'animation:in:await',
+ visit,
+ { skip: false },
+ (visit, { skip }) => {
+ if (skip) return;
+ return this.awaitAnimations({ selector: visit.animation.selector });
+ }
+ );
+
+ await nextTick();
+
+ await this.hooks.call('animation:in:start', visit, undefined, () => {
+ this.classes.remove('is-animating');
+ });
+
+ await animation;
+
+ await this.hooks.call('animation:in:end', visit, undefined);
+};
diff --git a/node_modules/swup/src/modules/animatePageOut.ts b/node_modules/swup/src/modules/animatePageOut.ts
new file mode 100755
index 00000000..d42bf57f
--- /dev/null
+++ b/node_modules/swup/src/modules/animatePageOut.ts
@@ -0,0 +1,19 @@
+import type Swup from '../Swup.js';
+import type { Visit } from './Visit.js';
+
+/**
+ * Perform the out/leave animation of the current page.
+ * @returns Promise<void>
+ */
+export const animatePageOut = async function (this: Swup, visit: Visit) {
+ await this.hooks.call('animation:out:start', visit, undefined, () => {
+ this.classes.add('is-changing', 'is-animating', 'is-leaving');
+ });
+
+ await this.hooks.call('animation:out:await', visit, { skip: false }, (visit, { skip }) => {
+ if (skip) return;
+ return this.awaitAnimations({ selector: visit.animation.selector });
+ });
+
+ await this.hooks.call('animation:out:end', visit, undefined);
+};
diff --git a/node_modules/swup/src/modules/awaitAnimations.ts b/node_modules/swup/src/modules/awaitAnimations.ts
new file mode 100644
index 00000000..e4b77e74
--- /dev/null
+++ b/node_modules/swup/src/modules/awaitAnimations.ts
@@ -0,0 +1,148 @@
+import { queryAll } from '../utils.js';
+import type Swup from '../Swup.js';
+import type { Options } from '../Swup.js';
+
+const TRANSITION = 'transition';
+const ANIMATION = 'animation';
+
+type AnimationType = typeof TRANSITION | typeof ANIMATION;
+type AnimationEndEvent = `${AnimationType}end`;
+type AnimationProperty = 'Delay' | 'Duration';
+type AnimationStyleKey = `${AnimationType}${AnimationProperty}` | 'transitionProperty';
+
+export type AnimationDirection = 'in' | 'out';
+
+/**
+ * Return a Promise that resolves when all CSS animations and transitions
+ * are done on the page. Filters by selector or takes elements directly.
+ */
+export async function awaitAnimations(
+ this: Swup,
+ {
+ selector,
+ elements
+ }: {
+ selector: Options['animationSelector'];
+ elements?: NodeListOf<HTMLElement> | HTMLElement[];
+ }
+): Promise<void> {
+ // Allow usage of swup without animations: { animationSelector: false }
+ if (selector === false && !elements) {
+ return;
+ }
+
+ // Allow passing in elements
+ let animatedElements: HTMLElement[] = [];
+ if (elements) {
+ animatedElements = Array.from(elements);
+ } else if (selector) {
+ animatedElements = queryAll(selector, document.body);
+ // Warn if no elements match the selector, but keep things going
+ if (!animatedElements.length) {
+ console.warn(`[swup] No elements found matching animationSelector \`${selector}\``);
+ return;
+ }
+ }
+
+ const awaitedAnimations = animatedElements.map((el) => awaitAnimationsOnElement(el));
+ const hasAnimations = awaitedAnimations.filter(Boolean).length > 0;
+ if (!hasAnimations) {
+ if (selector) {
+ console.warn(
+ `[swup] No CSS animation duration defined on elements matching \`${selector}\``
+ );
+ }
+ return;
+ }
+
+ await Promise.all(awaitedAnimations);
+}
+
+function awaitAnimationsOnElement(element: HTMLElement): Promise<void> | false {
+ const { type, timeout, propCount } = getTransitionInfo(element);
+
+ // Resolve immediately if no transition defined
+ if (!type || !timeout) {
+ return false;
+ }
+
+ return new Promise((resolve) => {
+ const endEvent: AnimationEndEvent = `${type}end`;
+ const startTime = performance.now();
+ let propsTransitioned = 0;
+
+ const end = () => {
+ element.removeEventListener(endEvent, onEnd);
+ resolve();
+ };
+
+ const onEnd = (event: TransitionEvent | AnimationEvent) => {
+ // Skip transitions on child elements
+ if (event.target !== element) {
+ return;
+ }
+
+ // Skip transitions that happened before we started listening
+ const elapsedTime = (performance.now() - startTime) / 1000;
+ if (elapsedTime < event.elapsedTime) {
+ return;
+ }
+
+ // End if all properties have transitioned
+ if (++propsTransitioned >= propCount) {
+ end();
+ }
+ };
+
+ setTimeout(() => {
+ if (propsTransitioned < propCount) {
+ end();
+ }
+ }, timeout + 1);
+
+ element.addEventListener(endEvent, onEnd);
+ });
+}
+
+function getTransitionInfo(element: Element) {
+ const styles = window.getComputedStyle(element);
+
+ const transitionDelays = getStyleProperties(styles, `${TRANSITION}Delay`);
+ const transitionDurations = getStyleProperties(styles, `${TRANSITION}Duration`);
+ const transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);
+
+ const animationDelays = getStyleProperties(styles, `${ANIMATION}Delay`);
+ const animationDurations = getStyleProperties(styles, `${ANIMATION}Duration`);
+ const animationTimeout = calculateTimeout(animationDelays, animationDurations);
+
+ const timeout = Math.max(transitionTimeout, animationTimeout);
+ const type: AnimationType | null =
+ timeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null;
+ const propCount = type
+ ? type === TRANSITION
+ ? transitionDurations.length
+ : animationDurations.length
+ : 0;
+
+ return {
+ type,
+ timeout,
+ propCount
+ };
+}
+
+export function getStyleProperties(styles: CSSStyleDeclaration, key: AnimationStyleKey): string[] {
+ return (styles[key] || '').split(', ');
+}
+
+export function calculateTimeout(delays: string[], durations: string[]): number {
+ while (delays.length < durations.length) {
+ delays = delays.concat(delays);
+ }
+
+ return Math.max(...durations.map((duration, i) => toMs(duration) + toMs(delays[i])));
+}
+
+export function toMs(time: string): number {
+ return parseFloat(time) * 1000;
+}
diff --git a/node_modules/swup/src/modules/fetchPage.ts b/node_modules/swup/src/modules/fetchPage.ts
new file mode 100644
index 00000000..96275448
--- /dev/null
+++ b/node_modules/swup/src/modules/fetchPage.ts
@@ -0,0 +1,114 @@
+import type Swup from '../Swup.js';
+import { Location } from '../helpers.js';
+import type { Visit } from './Visit.js';
+
+/** A page object as used by swup and its cache. */
+export interface PageData {
+ /** The URL of the page */
+ url: string;
+ /** The complete HTML response received from the server */
+ html: string;
+}
+
+/** Define how a page is fetched. */
+export interface FetchOptions extends Omit<RequestInit, 'cache'> {
+ /** The request method. */
+ method?: 'GET' | 'POST';
+ /** The body of the request: raw string, form data object or URL params. */
+ body?: string | FormData | URLSearchParams;
+ /** The request timeout in milliseconds. */
+ timeout?: number;
+ /** Optional visit object with additional context. @internal */
+ visit?: Visit;
+}
+
+export class FetchError extends Error {
+ url: string;
+ status?: number;
+ aborted: boolean;
+ timedOut: boolean;
+ constructor(
+ message: string,
+ details: { url: string; status?: number; aborted?: boolean; timedOut?: boolean }
+ ) {
+ super(message);
+ this.name = 'FetchError';
+ this.url = details.url;
+ this.status = details.status;
+ this.aborted = details.aborted || false;
+ this.timedOut = details.timedOut || false;
+ }
+}
+
+/**
+ * Fetch a page from the server, return it and cache it.
+ */
+export async function fetchPage(
+ this: Swup,
+ url: URL | string,
+ options: FetchOptions = {}
+): Promise<PageData> {
+ url = Location.fromUrl(url).url;
+
+ const { visit = this.visit } = options;
+ const headers = { ...this.options.requestHeaders, ...options.headers };
+ const timeout = options.timeout ?? this.options.timeout;
+ const controller = new AbortController();
+ const { signal } = controller;
+ options = { ...options, headers, signal };
+
+ let timedOut = false;
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
+ if (timeout && timeout > 0) {
+ timeoutId = setTimeout(() => {
+ timedOut = true;
+ controller.abort('timeout');
+ }, timeout);
+ }
+
+ // Allow hooking before this and returning a custom response-like object (e.g. custom fetch implementation)
+ let response: Response;
+ try {
+ response = await this.hooks.call(
+ 'fetch:request',
+ visit,
+ { url, options },
+ (visit, { url, options }) => fetch(url, options)
+ );
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ } catch (error) {
+ if (timedOut) {
+ this.hooks.call('fetch:timeout', visit, { url });
+ throw new FetchError(`Request timed out: ${url}`, { url, timedOut });
+ }
+ if ((error as Error)?.name === 'AbortError' || signal.aborted) {
+ throw new FetchError(`Request aborted: ${url}`, { url, aborted: true });
+ }
+ throw error;
+ }
+
+ const { status, url: responseUrl } = response;
+ const html = await response.text();
+
+ if (status === 500) {
+ this.hooks.call('fetch:error', visit, { status, response, url: responseUrl });
+ throw new FetchError(`Server error: ${responseUrl}`, { status, url: responseUrl });
+ }
+
+ if (!html) {
+ throw new FetchError(`Empty response: ${responseUrl}`, { status, url: responseUrl });
+ }
+
+ // Resolve real url after potential redirect
+ const { url: finalUrl } = Location.fromUrl(responseUrl);
+ const page = { url: finalUrl, html };
+
+ // Write to cache for safe methods and non-redirects
+ if (visit.cache.write && (!options.method || options.method === 'GET') && url === finalUrl) {
+ this.cache.set(page.url, page);
+ }
+
+ return page;
+}
diff --git a/node_modules/swup/src/modules/getAnchorElement.ts b/node_modules/swup/src/modules/getAnchorElement.ts
new file mode 100755
index 00000000..b25de2bc
--- /dev/null
+++ b/node_modules/swup/src/modules/getAnchorElement.ts
@@ -0,0 +1,32 @@
+import { query } from '../utils.js';
+
+/**
+ * Find the anchor element for a given hash.
+ *
+ * @param hash Hash with or without leading '#'
+ * @returns The element, if found, or null.
+ *
+ * @see https://html.spec.whatwg.org/#find-a-potential-indicated-element
+ */
+export const getAnchorElement = (hash?: string): Element | null => {
+ if (hash && hash.charAt(0) === '#') {
+ hash = hash.substring(1);
+ }
+
+ if (!hash) {
+ return null;
+ }
+
+ const decoded = decodeURIComponent(hash);
+ let element =
+ document.getElementById(hash) ||
+ document.getElementById(decoded) ||
+ query(`a[name='${CSS.escape(hash)}']`) ||
+ query(`a[name='${CSS.escape(decoded)}']`);
+
+ if (!element && hash === 'top') {
+ element = document.body;
+ }
+
+ return element;
+};
diff --git a/node_modules/swup/src/modules/navigate.ts b/node_modules/swup/src/modules/navigate.ts
new file mode 100755
index 00000000..7793f891
--- /dev/null
+++ b/node_modules/swup/src/modules/navigate.ts
@@ -0,0 +1,234 @@
+import type Swup from '../Swup.js';
+import { FetchError, type FetchOptions, type PageData } from './fetchPage.js';
+import { type VisitInitOptions, type Visit, VisitState } from './Visit.js';
+import { createHistoryRecord, updateHistoryRecord, Location, classify } from '../helpers.js';
+import { getContextualAttr } from '../utils.js';
+
+export type HistoryAction = 'push' | 'replace';
+export type HistoryDirection = 'forwards' | 'backwards';
+export type NavigationToSelfAction = 'scroll' | 'navigate';
+export type CacheControl = Partial<{ read: boolean; write: boolean }>;
+
+/** Define how to navigate to a page. */
+type NavigationOptions = {
+ /** Whether this visit is animated. Default: `true` */
+ animate?: boolean;
+ /** Name of a custom animation to run. */
+ animation?: string;
+ /** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */
+ history?: HistoryAction;
+ /** Whether this visit should read from or write to the cache. */
+ cache?: CacheControl;
+};
+
+/**
+ * Navigate to a new URL.
+ * @param url The URL to navigate to.
+ * @param options Options for how to perform this visit.
+ * @returns Promise<void>
+ */
+export function navigate(
+ this: Swup,
+ url: string,
+ options: NavigationOptions & FetchOptions = {},
+ init: Omit<VisitInitOptions, 'to'> = {}
+) {
+ if (typeof url !== 'string') {
+ throw new Error(`swup.navigate() requires a URL parameter`);
+ }
+
+ // Check if the visit should be ignored
+ if (this.shouldIgnoreVisit(url, { el: init.el, event: init.event })) {
+ window.location.assign(url);
+ return;
+ }
+
+ const { url: to, hash } = Location.fromUrl(url);
+
+ const visit = this.createVisit({ ...init, to, hash });
+ this.performNavigation(visit, options);
+}
+
+/**
+ * Start a visit to a new URL.
+ *
+ * Internal method that assumes the visit context has already been created.
+ *
+ * As a user, you should call `swup.navigate(url)` instead.
+ *
+ * @param url The URL to navigate to.
+ * @param options Options for how to perform this visit.
+ * @returns Promise<void>
+ */
+export async function performNavigation(
+ this: Swup,
+ visit: Visit,
+ options: NavigationOptions & FetchOptions = {}
+): Promise<void> {
+ if (this.navigating) {
+ if (this.visit.state >= VisitState.ENTERING) {
+ // Currently navigating and content already loaded? Finish and queue
+ visit.state = VisitState.QUEUED;
+ this.onVisitEnd = () => this.performNavigation(visit, options);
+ return;
+ } else {
+ // Currently navigating and content not loaded? Abort running visit
+ await this.hooks.call('visit:abort', this.visit, undefined);
+ delete this.visit.to.document;
+ this.visit.state = VisitState.ABORTED;
+ }
+ }
+
+ this.navigating = true;
+ this.visit = visit;
+
+ const { el } = visit.trigger;
+ options.referrer = options.referrer || this.location.url;
+
+ if (options.animate === false) {
+ visit.animation.animate = false;
+ }
+
+ // Clean up old animation classes
+ if (!visit.animation.animate) {
+ this.classes.clear();
+ }
+
+ // Get history action from option or attribute on trigger element
+ const history = options.history || getContextualAttr(el, 'data-swup-history');
+ if (typeof history === 'string' && ['push', 'replace'].includes(history)) {
+ visit.history.action = history as HistoryAction;
+ }
+
+ // Get custom animation name from option or attribute on trigger element
+ const animation = options.animation || getContextualAttr(el, 'data-swup-animation');
+ if (typeof animation === 'string') {
+ visit.animation.name = animation;
+ }
+
+ // Sanitize cache option
+ if (typeof options.cache === 'object') {
+ visit.cache.read = options.cache.read ?? visit.cache.read;
+ visit.cache.write = options.cache.write ?? visit.cache.write;
+ } else if (options.cache !== undefined) {
+ visit.cache = { read: !!options.cache, write: !!options.cache };
+ }
+ // Delete this so that window.fetch doesn't mis-interpret it
+ delete options.cache;
+
+ try {
+ await this.hooks.call('visit:start', visit, undefined);
+
+ visit.state = VisitState.STARTED;
+
+ // Begin loading page
+ const page = this.hooks.call('page:load', visit, { options }, async (visit, args) => {
+ // Read from cache
+ let cachedPage: PageData | undefined;
+ if (visit.cache.read) {
+ cachedPage = this.cache.get(visit.to.url);
+ }
+
+ args.page = cachedPage || (await this.fetchPage(visit.to.url, args.options));
+ args.cache = !!cachedPage;
+
+ return args.page;
+ });
+
+ /**
+ * When the page is loaded: mark the visit as loaded and save
+ * the raw html and a parsed document of the received page in the visit object
+ */
+ page.then(({ html }) => {
+ visit.advance(VisitState.LOADED);
+ visit.to.html = html;
+ visit.to.document = new DOMParser().parseFromString(html, 'text/html');
+ });
+
+ // Create/update history record if this is not a popstate call or leads to the same URL
+ const newUrl = visit.to.url + visit.to.hash;
+ if (!visit.history.popstate) {
+ if (visit.history.action === 'replace' || visit.to.url === this.location.url) {
+ updateHistoryRecord(newUrl);
+ } else {
+ this.currentHistoryIndex++;
+ createHistoryRecord(newUrl, { index: this.currentHistoryIndex });
+ }
+ }
+ this.location = Location.fromUrl(newUrl);
+
+ // Mark visit type with classes
+ if (visit.history.popstate) {
+ this.classes.add('is-popstate');
+ }
+ if (visit.animation.name) {
+ this.classes.add(`to-${classify(visit.animation.name)}`);
+ }
+
+ // Wait for page before starting to animate out?
+ if (visit.animation.wait) {
+ await page;
+ }
+
+ // Check if failed/aborted in the meantime
+ if (visit.done) return;
+
+ // Perform the actual transition: animate and replace content
+ await this.hooks.call('visit:transition', visit, undefined, async () => {
+ // No animation? Just await page and render
+ if (!visit.animation.animate) {
+ await this.hooks.call('animation:skip', undefined);
+ await this.renderPage(visit, await page);
+ return;
+ }
+
+ // Animate page out, render page, animate page in
+ visit.advance(VisitState.LEAVING);
+ await this.animatePageOut(visit);
+ if (visit.animation.native && document.startViewTransition) {
+ await document.startViewTransition(
+ async () => await this.renderPage(visit, await page)
+ ).finished;
+ } else {
+ await this.renderPage(visit, await page);
+ }
+ await this.animatePageIn(visit);
+ });
+
+ // Check if failed/aborted in the meantime
+ if (visit.done) return;
+
+ // Finalize visit
+ await this.hooks.call('visit:end', visit, undefined, () => this.classes.clear());
+ visit.state = VisitState.COMPLETED;
+ this.navigating = false;
+
+ /** Run eventually queued function */
+ if (this.onVisitEnd) {
+ this.onVisitEnd();
+ this.onVisitEnd = undefined;
+ }
+ } catch (error) {
+ // Return early if error is undefined or signals an aborted request
+ if (!error || (error as FetchError)?.aborted) {
+ visit.state = VisitState.ABORTED;
+ return;
+ }
+
+ visit.state = VisitState.FAILED;
+
+ // Log to console
+ console.error(error);
+
+ // Remove current history entry, then load requested url in browser
+ this.options.skipPopStateHandling = () => {
+ window.location.assign(visit.to.url + visit.to.hash);
+ return true;
+ };
+
+ // Go back to the actual page we're still at
+ window.history.back();
+ } finally {
+ delete visit.to.document;
+ }
+}
diff --git a/node_modules/swup/src/modules/plugins.ts b/node_modules/swup/src/modules/plugins.ts
new file mode 100644
index 00000000..299d7cb3
--- /dev/null
+++ b/node_modules/swup/src/modules/plugins.ts
@@ -0,0 +1,77 @@
+import type Swup from '../Swup.js';
+
+export type Plugin = {
+ /** Identify as a swup plugin */
+ isSwupPlugin: true;
+ /** Name of this plugin */
+ name: string;
+ /** Version of this plugin. Currently not in use, defined here for backward compatiblity. */
+ version?: string;
+ /** The swup instance that mounted this plugin */
+ swup?: Swup;
+ /** Version requirements of this plugin. Example: `{ swup: '>=4' }` */
+ requires?: Record<string, string | string[]>;
+ /** Run on mount */
+ mount: () => void;
+ /** Run on unmount */
+ unmount: () => void;
+ _beforeMount?: () => void;
+ _afterUnmount?: () => void;
+ _checkRequirements?: () => boolean;
+};
+
+const isSwupPlugin = (maybeInvalidPlugin: unknown): maybeInvalidPlugin is Plugin => {
+ // @ts-ignore: this might be anything, object or no
+ return Boolean(maybeInvalidPlugin?.isSwupPlugin);
+};
+
+/** Install a plugin. */
+export const use = function (this: Swup, plugin: unknown) {
+ if (!isSwupPlugin(plugin)) {
+ console.error('Not a swup plugin instance', plugin);
+ return;
+ }
+
+ plugin.swup = this;
+ if (plugin._checkRequirements) {
+ if (!plugin._checkRequirements()) {
+ return;
+ }
+ }
+ if (plugin._beforeMount) {
+ plugin._beforeMount();
+ }
+ plugin.mount();
+
+ this.plugins.push(plugin);
+
+ return this.plugins;
+};
+
+/** Uninstall a plugin. */
+export function unuse(this: Swup, pluginOrName: Plugin | string) {
+ const plugin = this.findPlugin(pluginOrName);
+ if (!plugin) {
+ console.error('No such plugin', plugin);
+ return;
+ }
+
+ plugin.unmount();
+ if (plugin._afterUnmount) {
+ plugin._afterUnmount();
+ }
+
+ this.plugins = this.plugins.filter((p) => p !== plugin);
+
+ return this.plugins;
+}
+
+/** Find a plugin by name or reference. */
+export function findPlugin(this: Swup, pluginOrName: Plugin | string) {
+ return this.plugins.find(
+ (plugin) =>
+ plugin === pluginOrName ||
+ plugin.name === pluginOrName ||
+ plugin.name === `Swup${String(pluginOrName)}`
+ );
+}
diff --git a/node_modules/swup/src/modules/renderPage.ts b/node_modules/swup/src/modules/renderPage.ts
new file mode 100755
index 00000000..9dc1f9f5
--- /dev/null
+++ b/node_modules/swup/src/modules/renderPage.ts
@@ -0,0 +1,51 @@
+import { updateHistoryRecord, getCurrentUrl, classify, Location } from '../helpers.js';
+import type Swup from '../Swup.js';
+import type { PageData } from './fetchPage.js';
+import { VisitState, type Visit } from './Visit.js';
+
+/**
+ * Render the next page: replace the content and update scroll position.
+ */
+export const renderPage = async function (this: Swup, visit: Visit, page: PageData): Promise<void> {
+ // Check if failed/aborted in the meantime
+ if (visit.done) return;
+
+ visit.advance(VisitState.ENTERING);
+
+ const { url } = page;
+
+ // update state if the url was redirected
+ if (!this.isSameResolvedUrl(getCurrentUrl(), url)) {
+ updateHistoryRecord(url);
+ this.location = Location.fromUrl(url);
+ visit.to.url = this.location.url;
+ visit.to.hash = this.location.hash;
+ }
+
+ // replace content: allow handlers and plugins to overwrite paga data and containers
+ await this.hooks.call('content:replace', visit, { page }, (visit, { page }) => {
+ this.classes.remove('is-leaving');
+ // only add for animated page loads
+ if (visit.animation.animate) {
+ this.classes.add('is-rendering');
+ }
+ const success = this.replaceContent(visit);
+ if (!success) {
+ throw new Error('[swup] Container mismatch, aborting');
+ }
+ if (visit.animation.animate) {
+ // Make sure to add these classes to new containers as well
+ this.classes.add('is-changing', 'is-animating', 'is-rendering');
+ if (visit.animation.name) {
+ this.classes.add(`to-${classify(visit.animation.name)}`);
+ }
+ }
+ });
+
+ // scroll into view: either anchor or top of page
+ await this.hooks.call('content:scroll', visit, undefined, () => {
+ return this.scrollToContent(visit);
+ });
+
+ await this.hooks.call('page:view', visit, { url: this.location.url, title: document.title });
+};
diff --git a/node_modules/swup/src/modules/replaceContent.ts b/node_modules/swup/src/modules/replaceContent.ts
new file mode 100755
index 00000000..62859acd
--- /dev/null
+++ b/node_modules/swup/src/modules/replaceContent.ts
@@ -0,0 +1,51 @@
+import type Swup from '../Swup.js';
+import { query, queryAll } from '../utils.js';
+import type { Visit } from './Visit.js';
+
+/**
+ * Perform the replacement of content after loading a page.
+ *
+ * @returns Whether all containers were replaced.
+ */
+export const replaceContent = function (this: Swup, visit: Visit): boolean {
+ const incomingDocument = visit.to.document;
+ if (!incomingDocument) return false;
+
+ // Update browser title
+ const title = incomingDocument.querySelector('title')?.innerText || '';
+ document.title = title;
+
+ // Save persisted elements
+ const persistedElements = queryAll('[data-swup-persist]:not([data-swup-persist=""])');
+
+ // Update content containers
+ const replaced = visit.containers
+ .map((selector) => {
+ const currentEl = document.querySelector(selector);
+ const incomingEl = incomingDocument.querySelector(selector);
+ if (currentEl && incomingEl) {
+ currentEl.replaceWith(incomingEl.cloneNode(true));
+ return true;
+ }
+ if (!currentEl) {
+ console.warn(`[swup] Container missing in current document: ${selector}`);
+ }
+ if (!incomingEl) {
+ console.warn(`[swup] Container missing in incoming document: ${selector}`);
+ }
+ return false;
+ })
+ .filter(Boolean);
+
+ // Restore persisted elements
+ persistedElements.forEach((existing) => {
+ const key = existing.getAttribute('data-swup-persist');
+ const replacement = query(`[data-swup-persist="${key}"]`);
+ if (replacement && replacement !== existing) {
+ replacement.replaceWith(existing);
+ }
+ });
+
+ // Return true if all containers were replaced
+ return replaced.length === visit.containers.length;
+};
diff --git a/node_modules/swup/src/modules/resolveUrl.ts b/node_modules/swup/src/modules/resolveUrl.ts
new file mode 100755
index 00000000..a687d6fe
--- /dev/null
+++ b/node_modules/swup/src/modules/resolveUrl.ts
@@ -0,0 +1,33 @@
+import type Swup from '../Swup.js';
+
+/**
+ * Utility function to validate and run the global option 'resolveUrl'
+ * @param {string} url
+ * @returns {string} the resolved url
+ */
+export function resolveUrl(this: Swup, url: string): string {
+ if (typeof this.options.resolveUrl !== 'function') {
+ console.warn(`[swup] options.resolveUrl expects a callback function.`);
+ return url;
+ }
+ const result = this.options.resolveUrl(url);
+ if (!result || typeof result !== 'string') {
+ console.warn(`[swup] options.resolveUrl needs to return a url`);
+ return url;
+ }
+ if (result.startsWith('//') || result.startsWith('http')) {
+ console.warn(`[swup] options.resolveUrl needs to return a relative url`);
+ return url;
+ }
+ return result;
+}
+
+/**
+ * Compares the resolved version of two paths and returns true if they are the same
+ * @param {string} url1
+ * @param {string} url2
+ * @returns {boolean}
+ */
+export function isSameResolvedUrl(this: Swup, url1: string, url2: string): boolean {
+ return this.resolveUrl(url1) === this.resolveUrl(url2);
+}
diff --git a/node_modules/swup/src/modules/scrollToContent.ts b/node_modules/swup/src/modules/scrollToContent.ts
new file mode 100755
index 00000000..0c081a28
--- /dev/null
+++ b/node_modules/swup/src/modules/scrollToContent.ts
@@ -0,0 +1,38 @@
+import type Swup from '../Swup.js';
+import type { Visit } from './Visit.js';
+
+/**
+ * Update the scroll position after page render.
+ * @returns Promise<boolean>
+ */
+export const scrollToContent = function (this: Swup, visit: Visit): boolean {
+ const options: ScrollIntoViewOptions = { behavior: 'auto' };
+ const { target, reset } = visit.scroll;
+ const scrollTarget = target ?? visit.to.hash;
+
+ let scrolled = false;
+
+ if (scrollTarget) {
+ scrolled = this.hooks.callSync(
+ 'scroll:anchor',
+ visit,
+ { hash: scrollTarget, options },
+ (visit, { hash, options }) => {
+ const anchor = this.getAnchorElement(hash);
+ if (anchor) {
+ anchor.scrollIntoView(options);
+ }
+ return !!anchor;
+ }
+ );
+ }
+
+ if (reset && !scrolled) {
+ scrolled = this.hooks.callSync('scroll:top', visit, { options }, (visit, { options }) => {
+ window.scrollTo({ top: 0, left: 0, ...options });
+ return true;
+ });
+ }
+
+ return scrolled;
+};