UNPKG

swup

Version:

Versatile and extensible page transition library for server-rendered websites

1 lines 112 kB
{"version":3,"file":"Swup.cjs","sources":["../src/helpers/classify.ts","../src/helpers/getCurrentUrl.ts","../src/helpers/history.ts","../src/helpers/delegateEvent.ts","../src/helpers/Location.ts","../src/modules/fetchPage.ts","../src/modules/Cache.ts","../src/utils/index.ts","../src/modules/Classes.ts","../src/modules/Visit.ts","../src/modules/Hooks.ts","../src/modules/getAnchorElement.ts","../src/modules/awaitAnimations.ts","../src/modules/navigate.ts","../src/modules/animatePageOut.ts","../src/modules/replaceContent.ts","../src/modules/scrollToContent.ts","../src/modules/animatePageIn.ts","../src/modules/renderPage.ts","../src/modules/plugins.ts","../src/modules/resolveUrl.ts","../src/Swup.ts","../src/helpers/matchPath.ts"],"sourcesContent":["/** Turn a string into a slug by lowercasing and replacing whitespace. */\nexport const classify = (text: string, fallback?: string): string => {\n\tconst output = String(text)\n\t\t.toLowerCase()\n\t\t// .normalize('NFD') // split an accented letter in the base letter and the acent\n\t\t// .replace(/[\\u0300-\\u036f]/g, '') // remove all previously split accents\n\t\t.replace(/[\\s/_.]+/g, '-') // replace spaces and _./ with '-'\n\t\t.replace(/[^\\w-]+/g, '') // remove all non-word chars\n\t\t.replace(/--+/g, '-') // replace repeating '-' with single '-'\n\t\t.replace(/^-+|-+$/g, ''); // trim '-' from edges\n\treturn output || fallback || '';\n};\n","/** Get the current page URL: path name + query params. Optionally including hash. */\nexport const getCurrentUrl = ({ hash }: { hash?: boolean } = {}): string => {\n\treturn window.location.pathname + window.location.search + (hash ? window.location.hash : '');\n};\n","import { getCurrentUrl } from './getCurrentUrl.js';\n\nexport interface HistoryState {\n\turl: string;\n\tsource: 'swup';\n\trandom: number;\n\tindex?: number;\n\t[key: string]: unknown;\n}\n\ntype HistoryData = Record<string, unknown>;\n\n/** Create a new history record with a custom swup identifier. */\nexport const createHistoryRecord = (url: string, data: HistoryData = {}): void => {\n\turl = url || getCurrentUrl({ hash: true });\n\tconst state: HistoryState = {\n\t\turl,\n\t\trandom: Math.random(),\n\t\tsource: 'swup',\n\t\t...data\n\t};\n\twindow.history.pushState(state, '', url);\n};\n\n/** Update the current history record with a custom swup identifier. */\nexport const updateHistoryRecord = (url: string | null = null, data: HistoryData = {}): void => {\n\turl = url || getCurrentUrl({ hash: true });\n\tconst currentState = (window.history.state as HistoryState) || {};\n\tconst state: HistoryState = {\n\t\t...currentState,\n\t\turl,\n\t\trandom: Math.random(),\n\t\tsource: 'swup',\n\t\t...data\n\t};\n\twindow.history.replaceState(state, '', url);\n};\n","import delegate, {\n\ttype DelegateEventHandler,\n\ttype DelegateOptions,\n\ttype EventType\n} from 'delegate-it';\nimport type { ParseSelector } from 'typed-query-selector/parser.js';\n\nexport type DelegateEventUnsubscribe = {\n\tdestroy: () => void;\n};\n\n/** Register a delegated event listener. */\nexport const delegateEvent = <\n\tSelector extends string,\n\tTElement extends Element = ParseSelector<Selector, HTMLElement>,\n\tTEvent extends EventType = EventType\n>(\n\tselector: Selector,\n\ttype: TEvent,\n\tcallback: DelegateEventHandler<GlobalEventHandlersEventMap[TEvent], TElement>,\n\toptions?: DelegateOptions\n): DelegateEventUnsubscribe => {\n\tconst controller = new AbortController();\n\toptions = { ...options, signal: controller.signal };\n\tdelegate<Selector, TElement, TEvent>(selector, type, callback, options);\n\treturn { destroy: () => controller.abort() };\n};\n","/**\n * A helper for creating a Location from either an element\n * or a URL object/string\n *\n */\nexport class Location extends URL {\n\tconstructor(url: URL | string, base: string = document.baseURI) {\n\t\tsuper(url.toString(), base);\n\t\t// Fix Safari bug with extending native classes\n\t\tObject.setPrototypeOf(this, Location.prototype);\n\t}\n\n\t/**\n\t * The full local path including query params.\n\t */\n\tget url(): string {\n\t\treturn this.pathname + this.search;\n\t}\n\n\t/**\n\t * Instantiate a Location from an element's href attribute\n\t * @param el\n\t * @returns new Location instance\n\t */\n\tstatic fromElement(el: Element): Location {\n\t\tconst href = el.getAttribute('href') || el.getAttribute('xlink:href') || '';\n\t\treturn new Location(href);\n\t}\n\n\t/**\n\t * Instantiate a Location from a URL object or string\n\t * @param url\n\t * @returns new Location instance\n\t */\n\tstatic fromUrl(url: URL | string): Location {\n\t\treturn new Location(url);\n\t}\n}\n","import type Swup from '../Swup.js';\nimport { Location } from '../helpers.js';\nimport type { Visit } from './Visit.js';\n\n/** A page object as used by swup and its cache. */\nexport interface PageData {\n\t/** The URL of the page */\n\turl: string;\n\t/** The complete HTML response received from the server */\n\thtml: string;\n}\n\n/** Define how a page is fetched. */\nexport interface FetchOptions extends Omit<RequestInit, 'cache'> {\n\t/** The request method. */\n\tmethod?: 'GET' | 'POST';\n\t/** The body of the request: raw string, form data object or URL params. */\n\tbody?: string | FormData | URLSearchParams;\n\t/** The request timeout in milliseconds. */\n\ttimeout?: number;\n\t/** Optional visit object with additional context. @internal */\n\tvisit?: Visit;\n}\n\nexport class FetchError extends Error {\n\turl: string;\n\tstatus?: number;\n\taborted: boolean;\n\ttimedOut: boolean;\n\tconstructor(\n\t\tmessage: string,\n\t\tdetails: { url: string; status?: number; aborted?: boolean; timedOut?: boolean }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'FetchError';\n\t\tthis.url = details.url;\n\t\tthis.status = details.status;\n\t\tthis.aborted = details.aborted || false;\n\t\tthis.timedOut = details.timedOut || false;\n\t}\n}\n\n/**\n * Fetch a page from the server, return it and cache it.\n */\nexport async function fetchPage(\n\tthis: Swup,\n\turl: URL | string,\n\toptions: FetchOptions = {}\n): Promise<PageData> {\n\turl = Location.fromUrl(url).url;\n\n\tconst { visit = this.visit } = options;\n\tconst headers = { ...this.options.requestHeaders, ...options.headers };\n\tconst timeout = options.timeout ?? this.options.timeout;\n\tconst controller = new AbortController();\n\tconst { signal } = controller;\n\toptions = { ...options, headers, signal };\n\n\tlet timedOut = false;\n\tlet timeoutId: ReturnType<typeof setTimeout> | null = null;\n\tif (timeout && timeout > 0) {\n\t\ttimeoutId = setTimeout(() => {\n\t\t\ttimedOut = true;\n\t\t\tcontroller.abort('timeout');\n\t\t}, timeout);\n\t}\n\n\t// Allow hooking before this and returning a custom response-like object (e.g. custom fetch implementation)\n\tlet response: Response;\n\ttry {\n\t\tresponse = await this.hooks.call(\n\t\t\t'fetch:request',\n\t\t\tvisit,\n\t\t\t{ url, options },\n\t\t\t(visit, { url, options }) => fetch(url, options)\n\t\t);\n\t\tif (timeoutId) {\n\t\t\tclearTimeout(timeoutId);\n\t\t}\n\t} catch (error) {\n\t\tif (timedOut) {\n\t\t\tthis.hooks.call('fetch:timeout', visit, { url });\n\t\t\tthrow new FetchError(`Request timed out: ${url}`, { url, timedOut });\n\t\t}\n\t\tif ((error as Error)?.name === 'AbortError' || signal.aborted) {\n\t\t\tthrow new FetchError(`Request aborted: ${url}`, { url, aborted: true });\n\t\t}\n\t\tthrow error;\n\t}\n\n\tconst { status, url: responseUrl } = response;\n\tconst html = await response.text();\n\n\tif (status === 500) {\n\t\tthis.hooks.call('fetch:error', visit, { status, response, url: responseUrl });\n\t\tthrow new FetchError(`Server error: ${responseUrl}`, { status, url: responseUrl });\n\t}\n\n\tif (!html) {\n\t\tthrow new FetchError(`Empty response: ${responseUrl}`, { status, url: responseUrl });\n\t}\n\n\t// Resolve real url after potential redirect\n\tconst { url: finalUrl } = Location.fromUrl(responseUrl);\n\tconst page = { url: finalUrl, html };\n\n\t// Write to cache for safe methods and non-redirects\n\tif (visit.cache.write && (!options.method || options.method === 'GET') && url === finalUrl) {\n\t\tthis.cache.set(page.url, page);\n\t}\n\n\treturn page;\n}\n","import type Swup from '../Swup.js';\nimport { Location } from '../helpers.js';\nimport { type PageData } from './fetchPage.js';\n\nexport interface CacheData extends PageData {}\n\n/**\n * In-memory page cache.\n */\nexport class Cache {\n\t/** Swup instance this cache belongs to */\n\tprotected swup: Swup;\n\n\t/** Cached pages, indexed by URL */\n\tprotected pages: Map<string, CacheData> = new Map();\n\n\tconstructor(swup: Swup) {\n\t\tthis.swup = swup;\n\t}\n\n\t/** Number of cached pages in memory. */\n\tget size(): number {\n\t\treturn this.pages.size;\n\t}\n\n\t/** All cached pages. */\n\tget all() {\n\t\tconst copy = new Map();\n\t\tthis.pages.forEach((page, key) => {\n\t\t\tcopy.set(key, { ...page });\n\t\t});\n\t\treturn copy;\n\t}\n\n\t/** Check if the given URL has been cached. */\n\thas(url: string): boolean {\n\t\treturn this.pages.has(this.resolve(url));\n\t}\n\n\t/** Return a shallow copy of the cached page object if available. */\n\tget(url: string): CacheData | undefined {\n\t\tconst result = this.pages.get(this.resolve(url));\n\t\tif (!result) return result;\n\t\treturn { ...result };\n\t}\n\n\t/** Create a cache record for the specified URL. */\n\tset(url: string, page: CacheData) {\n\t\turl = this.resolve(url);\n\t\tpage = { ...page, url };\n\t\tthis.pages.set(url, page);\n\t\tthis.swup.hooks.callSync('cache:set', undefined, { page });\n\t}\n\n\t/** Update a cache record, overwriting or adding custom data. */\n\tupdate(url: string, payload: object) {\n\t\turl = this.resolve(url);\n\t\tconst page = { ...this.get(url), ...payload, url } as CacheData;\n\t\tthis.pages.set(url, page);\n\t}\n\n\t/** Delete a cache record. */\n\tdelete(url: string): void {\n\t\tthis.pages.delete(this.resolve(url));\n\t}\n\n\t/** Empty the cache. */\n\tclear(): void {\n\t\tthis.pages.clear();\n\t\tthis.swup.hooks.callSync('cache:clear', undefined, undefined);\n\t}\n\n\t/** Remove all cache entries that return true for a given predicate function. */\n\tprune(predicate: (url: string, page: CacheData) => boolean): void {\n\t\tthis.pages.forEach((page, url) => {\n\t\t\tif (predicate(url, page)) {\n\t\t\t\tthis.delete(url);\n\t\t\t}\n\t\t});\n\t}\n\n\t/** Resolve URLs by making them local and letting swup resolve them. */\n\tprotected resolve(urlToResolve: string): string {\n\t\tconst { url } = Location.fromUrl(urlToResolve);\n\t\treturn this.swup.resolveUrl(url);\n\t}\n}\n","/** Find an element by selector. */\nexport const query = (selector: string, context: Document | Element = document) => {\n\treturn context.querySelector<HTMLElement>(selector);\n};\n\n/** Find a set of elements by selector. */\nexport const queryAll = (\n\tselector: string,\n\tcontext: Document | Element = document\n): HTMLElement[] => {\n\treturn Array.from(context.querySelectorAll(selector));\n};\n\n/** Return a Promise that resolves after the next event loop. */\nexport const nextTick = (): Promise<void> => {\n\treturn new Promise((resolve) => {\n\t\trequestAnimationFrame(() => {\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t});\n};\n\n/** Check if an object is a Promise or a Thenable */\nexport function isPromise<T>(obj: unknown): obj is PromiseLike<T> {\n\treturn (\n\t\t!!obj &&\n\t\t(typeof obj === 'object' || typeof obj === 'function') &&\n\t\ttypeof (obj as Record<string, unknown>).then === 'function'\n\t);\n}\n\n/** Call a function as a Promise. Resolves with the returned Promsise or immediately. */\n// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any\nexport function runAsPromise(func: Function, args: unknown[] = []): Promise<unknown> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst result: unknown = func(...args);\n\t\tif (isPromise(result)) {\n\t\t\tresult.then(resolve, reject);\n\t\t} else {\n\t\t\tresolve(result);\n\t\t}\n\t});\n}\n\n/**\n * Force a layout reflow, e.g. after adding classnames\n * @see https://stackoverflow.com/a/21665117/3759615\n */\nexport function forceReflow(element?: HTMLElement): void {\n\telement = element || document.body;\n\telement?.getBoundingClientRect();\n}\n\n/**\n * Read data attribute from closest element with that attribute.\n *\n * Returns `undefined` if no element is found or attribute is missing.\n * Returns `true` if attribute is present without a value.\n */\nexport function getContextualAttr(\n\tel: Element | undefined,\n\tattr: string\n): string | boolean | undefined {\n\tconst target = el?.closest(`[${attr}]`);\n\treturn target?.hasAttribute(attr) ? target?.getAttribute(attr) || true : undefined;\n}\n","import type Swup from '../Swup.js';\nimport { queryAll } from '../utils.js';\n\nexport class Classes {\n\tprotected swup: Swup;\n\tprotected swupClasses = [\n\t\t'to-',\n\t\t'is-changing',\n\t\t'is-rendering',\n\t\t'is-popstate',\n\t\t'is-animating',\n\t\t'is-leaving'\n\t];\n\n\tconstructor(swup: Swup) {\n\t\tthis.swup = swup;\n\t}\n\n\tprotected get selectors(): string[] {\n\t\tconst { scope } = this.swup.visit.animation;\n\t\tif (scope === 'containers') return this.swup.visit.containers;\n\t\tif (scope === 'html') return ['html'];\n\t\tif (Array.isArray(scope)) return scope;\n\t\treturn [];\n\t}\n\n\tprotected get selector(): string {\n\t\treturn this.selectors.join(',');\n\t}\n\n\tprotected get targets(): HTMLElement[] {\n\t\tif (!this.selector.trim()) return [];\n\t\treturn queryAll(this.selector);\n\t}\n\n\tadd(...classes: string[]): void {\n\t\tthis.targets.forEach((target) => target.classList.add(...classes));\n\t}\n\n\tremove(...classes: string[]): void {\n\t\tthis.targets.forEach((target) => target.classList.remove(...classes));\n\t}\n\n\tclear(): void {\n\t\tthis.targets.forEach((target) => {\n\t\t\tconst remove = target.className.split(' ').filter((c) => this.isSwupClass(c));\n\t\t\ttarget.classList.remove(...remove);\n\t\t});\n\t}\n\n\tprotected isSwupClass(className: string): boolean {\n\t\treturn this.swupClasses.some((c) => className.startsWith(c));\n\t}\n}\n","import type Swup from '../Swup.js';\nimport type { Options } from '../Swup.js';\nimport type { HistoryAction, HistoryDirection } from './navigate.js';\n\n/** See below for the class Visit {} definition */\n// export interface Visit {}\n\nexport interface VisitFrom {\n\t/** The URL of the previous page */\n\turl: string;\n\t/** The hash of the previous page */\n\thash?: string;\n}\n\nexport interface VisitTo {\n\t/** The URL of the next page */\n\turl: string;\n\t/** The hash of the next page */\n\thash?: string;\n\t/** The HTML content of the next page */\n\thtml?: string;\n\t/** The parsed document of the next page, available during visit */\n\tdocument?: Document;\n}\n\nexport interface VisitAnimation {\n\t/** Whether this visit is animated. Default: `true` */\n\tanimate: boolean;\n\t/** Whether to wait for the next page to load before starting the animation. Default: `false` */\n\twait: boolean;\n\t/** Name of a custom animation to run. */\n\tname?: string;\n\t/** Whether this animation uses the native browser ViewTransition API. Default: `false` */\n\tnative: boolean;\n\t/** Elements on which to add animation classes. Default: `html` element */\n\tscope: 'html' | 'containers' | string[];\n\t/** Selector for detecting animation timing. Default: `[class*=\"transition-\"]` */\n\tselector: Options['animationSelector'];\n}\n\nexport interface VisitScroll {\n\t/** Whether to reset the scroll position after the visit. Default: `true` */\n\treset: boolean;\n\t/** Anchor element to scroll to on the next page. */\n\ttarget?: string | false;\n}\n\nexport interface VisitTrigger {\n\t/** DOM element that triggered this visit. */\n\tel?: Element;\n\t/** DOM event that triggered this visit. */\n\tevent?: Event;\n}\n\nexport interface VisitCache {\n\t/** Whether this visit will try to load the requested page from cache. */\n\tread: boolean;\n\t/** Whether this visit will save the loaded page in cache. */\n\twrite: boolean;\n}\n\nexport interface VisitHistory {\n\t/** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */\n\taction: HistoryAction;\n\t/** Whether this visit was triggered by a browser history navigation. */\n\tpopstate: boolean;\n\t/** The direction of travel in case of a browser history navigation: backward or forward. */\n\tdirection: HistoryDirection | undefined;\n}\n\nexport interface VisitInitOptions {\n\tto: string;\n\tfrom?: string;\n\thash?: string;\n\tel?: Element;\n\tevent?: Event;\n}\n\n/** @internal */\nexport const VisitState = {\n\tCREATED: 1,\n\tQUEUED: 2,\n\tSTARTED: 3,\n\tLEAVING: 4,\n\tLOADED: 5,\n\tENTERING: 6,\n\tCOMPLETED: 7,\n\tABORTED: 8,\n\tFAILED: 9\n} as const;\n\n/** @internal */\nexport type VisitState = (typeof VisitState)[keyof typeof VisitState];\n\n/** An object holding details about the current visit. */\nexport class Visit {\n\t/** A unique ID to identify this visit */\n\tid: number;\n\t/** The current state of this visit @internal */\n\tstate: VisitState;\n\t/** The previous page, about to leave */\n\tfrom: VisitFrom;\n\t/** The next page, about to enter */\n\tto: VisitTo;\n\t/** The content containers, about to be replaced */\n\tcontainers: Options['containers'];\n\t/** Information about animated page transitions */\n\tanimation: VisitAnimation;\n\t/** What triggered this visit */\n\ttrigger: VisitTrigger;\n\t/** Cache behavior for this visit */\n\tcache: VisitCache;\n\t/** Browser history behavior on this visit */\n\thistory: VisitHistory;\n\t/** Scroll behavior on this visit */\n\tscroll: VisitScroll;\n\t/** User-defined metadata */\n\tmeta: Record<string, unknown>;\n\n\tconstructor(swup: Swup, options: VisitInitOptions) {\n\t\tconst { to, from, hash, el, event } = options;\n\n\t\tthis.id = Math.random();\n\t\tthis.state = VisitState.CREATED;\n\t\tthis.from = { url: from ?? swup.location.url, hash: swup.location.hash };\n\t\tthis.to = { url: to, hash };\n\t\tthis.containers = swup.options.containers;\n\t\tthis.animation = {\n\t\t\tanimate: true,\n\t\t\twait: false,\n\t\t\tname: undefined,\n\t\t\tnative: swup.options.native,\n\t\t\tscope: swup.options.animationScope,\n\t\t\tselector: swup.options.animationSelector\n\t\t};\n\t\tthis.trigger = { el, event };\n\t\tthis.cache = {\n\t\t\tread: swup.options.cache,\n\t\t\twrite: swup.options.cache\n\t\t};\n\t\tthis.history = {\n\t\t\taction: 'push',\n\t\t\tpopstate: false,\n\t\t\tdirection: undefined\n\t\t};\n\t\tthis.scroll = {\n\t\t\treset: true,\n\t\t\ttarget: undefined\n\t\t};\n\t\tthis.meta = {};\n\t}\n\n\t/** @internal */\n\tadvance(state: VisitState) {\n\t\tif (this.state < state) {\n\t\t\tthis.state = state;\n\t\t}\n\t}\n\n\t/** @internal */\n\tabort() {\n\t\tthis.state = VisitState.ABORTED;\n\t}\n\n\t/** Is this visit done, i.e. completed, failed, or aborted? */\n\tget done(): boolean {\n\t\treturn this.state >= VisitState.COMPLETED;\n\t}\n}\n\n/** Create a new visit object. */\nexport function createVisit(this: Swup, options: VisitInitOptions): Visit {\n\treturn new Visit(this, options);\n}\n","import type { DelegateEvent } from 'delegate-it';\n\nimport type Swup from '../Swup.js';\nimport { isPromise, runAsPromise } from '../utils.js';\nimport { Visit } from './Visit.js';\nimport type { FetchOptions, PageData } from './fetchPage.js';\n\nexport interface HookDefinitions {\n\t'animation:out:start': undefined;\n\t'animation:out:await': { skip: boolean };\n\t'animation:out:end': undefined;\n\t'animation:in:start': undefined;\n\t'animation:in:await': { skip: boolean };\n\t'animation:in:end': undefined;\n\t'animation:skip': undefined;\n\t'cache:clear': undefined;\n\t'cache:set': { page: PageData };\n\t'content:replace': { page: PageData };\n\t'content:scroll': undefined;\n\t'enable': undefined;\n\t'disable': undefined;\n\t'fetch:request': { url: string; options: FetchOptions };\n\t'fetch:error': { url: string; status: number; response: Response };\n\t'fetch:timeout': { url: string };\n\t'history:popstate': { event: PopStateEvent };\n\t'link:click': { el: HTMLAnchorElement; event: DelegateEvent<MouseEvent> };\n\t'link:self': undefined;\n\t'link:anchor': { hash: string };\n\t'link:newtab': { href: string };\n\t'page:load': { page?: PageData; cache?: boolean; options: FetchOptions };\n\t'page:view': { url: string; title: string };\n\t'scroll:top': { options: ScrollIntoViewOptions };\n\t'scroll:anchor': { hash: string; options: ScrollIntoViewOptions };\n\t'visit:start': undefined;\n\t'visit:transition': undefined;\n\t'visit:abort': undefined;\n\t'visit:end': undefined;\n}\n\nexport interface HookReturnValues {\n\t'content:scroll': Promise<boolean> | boolean;\n\t'fetch:request': Promise<Response>;\n\t'page:load': Promise<PageData>;\n\t'scroll:top': boolean;\n\t'scroll:anchor': boolean;\n}\n\nexport type HookArguments<T extends HookName> = HookDefinitions[T];\n\nexport type HookName = keyof HookDefinitions;\n\nexport type HookNameWithModifier = `${HookName}.${HookModifier}`;\n\ntype HookModifier = 'once' | 'before' | 'replace';\n\n/** A generic hook handler. */\nexport type HookHandler<T extends HookName> = (\n\t/** Context about the current visit. */\n\tvisit: Visit,\n\t/** Local arguments passed into the handler. */\n\targs: HookArguments<T>\n) => Promise<unknown> | unknown;\n\n/** A default hook handler with an expected return type. */\nexport type HookDefaultHandler<T extends HookName> = (\n\t/** Context about the current visit. */\n\tvisit: Visit,\n\t/** Local arguments passed into the handler. */\n\targs: HookArguments<T>,\n\t/** Default handler to be executed. Available if replacing an internal hook handler. */\n\tdefaultHandler?: HookDefaultHandler<T>\n) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise<unknown> | unknown;\n\nexport type Handlers = {\n\t[K in HookName]: HookHandler<K>[];\n};\n\nexport type HookInitOptions = {\n\t[K in HookName as K | `${K}.${HookModifier}`]: HookHandler<K>;\n} & {\n\t[K in HookName as K | `${K}.${HookModifier}.${HookModifier}`]: HookHandler<K>;\n};\n\n/** Unregister a previously registered hook handler. */\nexport type HookUnregister = () => void;\n\n/** Define when and how a hook handler is executed. */\nexport type HookOptions = {\n\t/** Execute the hook once, then remove the handler */\n\tonce?: boolean;\n\t/** Execute the hook before the internal default handler */\n\tbefore?: boolean;\n\t/** Set a priority for when to execute this hook. Lower numbers execute first. Default: `0` */\n\tpriority?: number;\n\t/** Replace the internal default handler with this hook handler */\n\treplace?: boolean;\n};\n\nexport type HookRegistration<\n\tT extends HookName,\n\tH extends HookHandler<T> | HookDefaultHandler<T> = HookHandler<T>\n> = {\n\tid: number;\n\thook: T;\n\thandler: H;\n\tdefaultHandler?: HookDefaultHandler<T>;\n} & HookOptions;\n\ntype HookEventDetail = {\n\thook: HookName;\n\targs: unknown;\n\tvisit: Visit;\n};\n\nexport type HookEvent = CustomEvent<HookEventDetail>;\n\ntype HookLedger<T extends HookName> = Map<HookHandler<T>, HookRegistration<T>>;\n\ninterface HookRegistry extends Map<HookName, HookLedger<HookName>> {\n\tget<K extends HookName>(key: K): HookLedger<K> | undefined;\n\tset<K extends HookName>(key: K, value: HookLedger<K>): this;\n}\n\n/**\n * Hook registry.\n *\n * Create, trigger and handle hooks.\n *\n */\nexport class Hooks {\n\t/** Swup instance this registry belongs to */\n\tprotected swup: Swup;\n\n\t/** Map of all registered hook handlers. */\n\tprotected registry: HookRegistry = new Map();\n\n\t// Can we deduplicate this somehow? Or make it error when not in sync with HookDefinitions?\n\t// https://stackoverflow.com/questions/53387838/how-to-ensure-an-arrays-values-the-keys-of-a-typescript-interface/53395649\n\tprotected readonly hooks: HookName[] = [\n\t\t'animation:out:start',\n\t\t'animation:out:await',\n\t\t'animation:out:end',\n\t\t'animation:in:start',\n\t\t'animation:in:await',\n\t\t'animation:in:end',\n\t\t'animation:skip',\n\t\t'cache:clear',\n\t\t'cache:set',\n\t\t'content:replace',\n\t\t'content:scroll',\n\t\t'enable',\n\t\t'disable',\n\t\t'fetch:request',\n\t\t'fetch:error',\n\t\t'fetch:timeout',\n\t\t'history:popstate',\n\t\t'link:click',\n\t\t'link:self',\n\t\t'link:anchor',\n\t\t'link:newtab',\n\t\t'page:load',\n\t\t'page:view',\n\t\t'scroll:top',\n\t\t'scroll:anchor',\n\t\t'visit:start',\n\t\t'visit:transition',\n\t\t'visit:abort',\n\t\t'visit:end'\n\t];\n\n\tconstructor(swup: Swup) {\n\t\tthis.swup = swup;\n\t\tthis.init();\n\t}\n\n\t/**\n\t * Create ledgers for all core hooks.\n\t */\n\tprotected init() {\n\t\tthis.hooks.forEach((hook) => this.create(hook));\n\t}\n\n\t/**\n\t * Create a new hook type.\n\t */\n\tcreate(hook: string) {\n\t\tif (!this.registry.has(hook as HookName)) {\n\t\t\tthis.registry.set(hook as HookName, new Map());\n\t\t}\n\t}\n\n\t/**\n\t * Check if a hook type exists.\n\t */\n\texists(hook: HookName): boolean {\n\t\treturn this.registry.has(hook);\n\t}\n\n\t/**\n\t * Get the ledger with all registrations for a hook.\n\t */\n\tprotected get<T extends HookName>(hook: T): HookLedger<T> | undefined {\n\t\tconst ledger = this.registry.get(hook);\n\t\tif (ledger) {\n\t\t\treturn ledger;\n\t\t}\n\t\tconsole.error(`Unknown hook '${hook}'`);\n\t}\n\n\t/**\n\t * Remove all handlers of all hooks.\n\t */\n\tclear() {\n\t\tthis.registry.forEach((ledger) => ledger.clear());\n\t}\n\n\t/**\n\t * Register a new hook handler.\n\t * @param hook Name of the hook to listen for\n\t * @param handler The handler function to execute\n\t * @param options Object to specify how and when the handler is executed\n\t * Available options:\n\t * - `once`: Only execute the handler once\n\t * - `before`: Execute the handler before the default handler\n\t * - `priority`: Specify the order in which the handlers are executed\n\t * - `replace`: Replace the default handler with this handler\n\t * @returns A function to unregister the handler\n\t */\n\n\t// Overload: replacing default handler\n\ton<T extends HookName, O extends HookOptions>(hook: T, handler: HookDefaultHandler<T>, options: O & { replace: true }): HookUnregister; // prettier-ignore\n\t// Overload: passed in handler options\n\ton<T extends HookName, O extends HookOptions>(hook: T, handler: HookHandler<T>, options: O): HookUnregister; // prettier-ignore\n\t// Overload: no handler options\n\ton<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister; // prettier-ignore\n\t// Implementation\n\ton<T extends HookName, O extends HookOptions>(\n\t\thook: T,\n\t\thandler: O['replace'] extends true ? HookDefaultHandler<T> : HookHandler<T>,\n\t\toptions: Partial<O> = {}\n\t): HookUnregister {\n\t\tconst ledger = this.get(hook);\n\t\tif (!ledger) {\n\t\t\tconsole.warn(`Hook '${hook}' not found.`);\n\t\t\treturn () => {};\n\t\t}\n\n\t\tconst id = ledger.size + 1;\n\t\tconst registration: HookRegistration<T> = { ...options, id, hook, handler };\n\t\tledger.set(handler, registration);\n\n\t\treturn () => this.off(hook, handler);\n\t}\n\n\t/**\n\t * Register a new hook handler to run before the default handler.\n\t * Shortcut for `hooks.on(hook, handler, { before: true })`.\n\t * @param hook Name of the hook to listen for\n\t * @param handler The handler function to execute\n\t * @param options Any other event options (see `hooks.on()` for details)\n\t * @returns A function to unregister the handler\n\t * @see on\n\t */\n\t// Overload: passed in handler options\n\tbefore<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore\n\t// Overload: no handler options\n\tbefore<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister;\n\t// Implementation\n\tbefore<T extends HookName>(\n\t\thook: T,\n\t\thandler: HookHandler<T>,\n\t\toptions: HookOptions = {}\n\t): HookUnregister {\n\t\treturn this.on(hook, handler, { ...options, before: true });\n\t}\n\n\t/**\n\t * Register a new hook handler to replace the default handler.\n\t * Shortcut for `hooks.on(hook, handler, { replace: true })`.\n\t * @param hook Name of the hook to listen for\n\t * @param handler The handler function to execute instead of the default handler\n\t * @param options Any other event options (see `hooks.on()` for details)\n\t * @returns A function to unregister the handler\n\t * @see on\n\t */\n\t// Overload: passed in handler options\n\treplace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore\n\t// Overload: no handler options\n\treplace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>): HookUnregister; // prettier-ignore\n\t// Implementation\n\treplace<T extends HookName>(\n\t\thook: T,\n\t\thandler: HookDefaultHandler<T>,\n\t\toptions: HookOptions = {}\n\t): HookUnregister {\n\t\treturn this.on(hook, handler, { ...options, replace: true });\n\t}\n\n\t/**\n\t * Register a new hook handler to run once.\n\t * Shortcut for `hooks.on(hook, handler, { once: true })`.\n\t * @param hook Name of the hook to listen for\n\t * @param handler The handler function to execute\n\t * @param options Any other event options (see `hooks.on()` for details)\n\t * @see on\n\t */\n\t// Overload: passed in handler options\n\tonce<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore\n\t// Overload: no handler options\n\tonce<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister;\n\t// Implementation\n\tonce<T extends HookName>(\n\t\thook: T,\n\t\thandler: HookHandler<T>,\n\t\toptions: HookOptions = {}\n\t): HookUnregister {\n\t\treturn this.on(hook, handler, { ...options, once: true });\n\t}\n\n\t/**\n\t * Unregister a hook handler.\n\t * @param hook Name of the hook the handler is registered for\n\t * @param handler The handler function that was registered.\n\t * If omitted, all handlers for the hook will be removed.\n\t */\n\t// Overload: unregister a specific handler\n\toff<T extends HookName>(hook: T, handler: HookHandler<T> | HookDefaultHandler<T>): void;\n\t// Overload: unregister all handlers\n\toff<T extends HookName>(hook: T): void;\n\t// Implementation\n\toff<T extends HookName>(hook: T, handler?: HookHandler<T> | HookDefaultHandler<T>): void {\n\t\tconst ledger = this.get(hook);\n\t\tif (ledger && handler) {\n\t\t\tconst deleted = ledger.delete(handler);\n\t\t\tif (!deleted) {\n\t\t\t\tconsole.warn(`Handler for hook '${hook}' not found.`);\n\t\t\t}\n\t\t} else if (ledger) {\n\t\t\tledger.clear();\n\t\t}\n\t}\n\n\t/**\n\t * Trigger a hook asynchronously, executing its default handler and all registered handlers.\n\t * Will execute all handlers in order and `await` any `Promise`s they return.\n\t * @param hook Name of the hook to trigger\n\t * @param visit The visit object this hook belongs to\n\t * @param args Arguments to pass to the handler\n\t * @param defaultHandler A default implementation of this hook to execute\n\t * @returns The resolved return value of the executed default handler\n\t */\n\t// Overload: default order of arguments\n\tasync call<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore\n\t// Overload: legacy order of arguments, with visit missing\n\tasync call<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore\n\t// Implementation\n\tasync call<T extends HookName>(\n\t\thook: T,\n\t\targ1: Visit | HookArguments<T>,\n\t\targ2: HookArguments<T> | HookDefaultHandler<T>,\n\t\targ3?: HookDefaultHandler<T>\n\t): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>> {\n\t\tconst [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);\n\n\t\tconst { before, handler, after } = this.getHandlers(hook, defaultHandler);\n\t\tawait this.run(before, visit, args);\n\t\tconst [result] = await this.run(handler, visit, args, true);\n\t\tawait this.run(after, visit, args);\n\t\tthis.dispatchDomEvent(hook, visit, args);\n\t\treturn result;\n\t}\n\n\t/**\n\t * Trigger a hook synchronously, executing its default handler and all registered handlers.\n\t * Will execute all handlers in order, but will **not** `await` any `Promise`s they return.\n\t * @param hook Name of the hook to trigger\n\t * @param visit The visit object this hook belongs to\n\t * @param args Arguments to pass to the handler\n\t * @param defaultHandler A default implementation of this hook to execute\n\t * @returns The (possibly unresolved) return value of the executed default handler\n\t */\n\t// Overload: default order of arguments\n\tcallSync<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore\n\t// Overload: legacy order of arguments, with visit missing\n\tcallSync<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore\n\t// Implementation\n\tcallSync<T extends HookName>(\n\t\thook: T,\n\t\targ1: Visit | HookArguments<T>,\n\t\targ2: HookArguments<T> | HookDefaultHandler<T>,\n\t\targ3?: HookDefaultHandler<T>\n\t): ReturnType<HookDefaultHandler<T>> {\n\t\tconst [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3);\n\t\tconst { before, handler, after } = this.getHandlers(hook, defaultHandler);\n\t\tthis.runSync(before, visit, args);\n\t\tconst [result] = this.runSync(handler, visit, args, true);\n\t\tthis.runSync(after, visit, args);\n\t\tthis.dispatchDomEvent(hook, visit, args);\n\t\treturn result;\n\t}\n\n\t/**\n\t * Parse the call arguments for call() and callSync() to allow legacy argument order.\n\t */\n\tprotected parseCallArgs<T extends HookName>(\n\t\thook: T,\n\t\targ1: Visit | HookArguments<T> | undefined,\n\t\targ2: HookArguments<T> | HookDefaultHandler<T>,\n\t\targ3?: HookDefaultHandler<T>\n\t): [Visit | undefined, HookArguments<T>, HookDefaultHandler<T> | undefined] {\n\t\tconst isLegacyOrder =\n\t\t\t!(arg1 instanceof Visit) && (typeof arg1 === 'object' || typeof arg2 === 'function');\n\t\tif (isLegacyOrder) {\n\t\t\t// Legacy positioning: arguments in second or handler passed in third place\n\t\t\treturn [undefined, arg1 as HookArguments<T>, arg2 as HookDefaultHandler<T>];\n\t\t} else {\n\t\t\t// Default positioning: visit passed in as first argument\n\t\t\treturn [arg1, arg2 as HookArguments<T>, arg3];\n\t\t}\n\t}\n\n\t/**\n\t * Execute the handlers for a hook, in order, as `Promise`s that will be `await`ed.\n\t * @param registrations The registrations (handler + options) to execute\n\t * @param args Arguments to pass to the handler\n\t */\n\n\t// Overload: running HookDefaultHandler: expect HookDefaultHandler return type\n\tprotected 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\n\t// Overload: running user handler: expect no specific type\n\tprotected async run<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore\n\t// Implementation\n\tprotected async run<T extends HookName, R extends HookRegistration<T>[]>(\n\t\tregistrations: R,\n\t\tvisit: Visit | undefined = this.swup.visit,\n\t\targs: HookArguments<T>,\n\t\trethrow: boolean = false\n\t): Promise<Awaited<ReturnType<HookDefaultHandler<T>>> | unknown[]> {\n\t\tconst results = [];\n\t\tfor (const { hook, handler, defaultHandler, once } of registrations) {\n\t\t\tif (visit?.done) continue;\n\t\t\tif (once) this.off(hook, handler);\n\t\t\ttry {\n\t\t\t\tconst result = await runAsPromise(handler, [visit, args, defaultHandler]);\n\t\t\t\tresults.push(result);\n\t\t\t} catch (error) {\n\t\t\t\tif (rethrow) {\n\t\t\t\t\tthrow error;\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`Error in hook '${hook}':`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * Execute the handlers for a hook, in order, without `await`ing any returned `Promise`s.\n\t * @param registrations The registrations (handler + options) to execute\n\t * @param args Arguments to pass to the handler\n\t */\n\n\t// Overload: running HookDefaultHandler: expect HookDefaultHandler return type\n\tprotected runSync<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): ReturnType<HookDefaultHandler<T>>[]; // prettier-ignore\n\t// Overload: running user handler: expect no specific type\n\tprotected runSync<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): unknown[]; // prettier-ignore\n\t// Implementation\n\tprotected runSync<T extends HookName, R extends HookRegistration<T>[]>(\n\t\tregistrations: R,\n\t\tvisit: Visit | undefined = this.swup.visit,\n\t\targs: HookArguments<T>,\n\t\trethrow: boolean = false\n\t): (ReturnType<HookDefaultHandler<T>> | unknown)[] {\n\t\tconst results = [];\n\t\tfor (const { hook, handler, defaultHandler, once } of registrations) {\n\t\t\tif (visit?.done) continue;\n\t\t\tif (once) this.off(hook, handler);\n\t\t\ttry {\n\t\t\t\tconst result = (handler as HookDefaultHandler<T>)(visit, args, defaultHandler);\n\t\t\t\tresults.push(result);\n\t\t\t\tif (isPromise(result)) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`Swup will not await Promises in handler for synchronous hook '${hook}'.`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (rethrow) {\n\t\t\t\t\tthrow error;\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`Error in hook '${hook}':`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn results;\n\t}\n\n\t/**\n\t * Get all registered handlers for a hook, sorted by priority and registration order.\n\t * @param hook Name of the hook\n\t * @param defaultHandler The optional default handler of this hook\n\t * @returns An object with the handlers sorted into `before` and `after` arrays,\n\t * as well as a flag indicating if the original handler was replaced\n\t */\n\tprotected getHandlers<T extends HookName>(hook: T, defaultHandler?: HookDefaultHandler<T>) {\n\t\tconst ledger = this.get(hook);\n\t\tif (!ledger) {\n\t\t\treturn { found: false, before: [], handler: [], after: [], replaced: false };\n\t\t}\n\n\t\tconst registrations = Array.from(ledger.values());\n\n\t\t// Let TypeScript know that replaced handlers are default handlers by filtering to true\n\t\tconst def = (T: HookRegistration<T>): T is HookRegistration<T, HookDefaultHandler<T>> => true; // prettier-ignore\n\t\tconst sort = this.sortRegistrations;\n\n\t\t// Filter into before, after, and replace handlers\n\t\tconst before = registrations.filter(({ before, replace }) => before && !replace).sort(sort);\n\t\tconst replace = registrations.filter(({ replace }) => replace).filter(def).sort(sort); // prettier-ignore\n\t\tconst after = registrations.filter(({ before, replace }) => !before && !replace).sort(sort);\n\t\tconst replaced = replace.length > 0;\n\n\t\t// Define main handler registration\n\t\t// Created as HookRegistration[] array to allow passing it into hooks.run() directly\n\t\tlet handler: HookRegistration<T, HookDefaultHandler<T>>[] = [];\n\t\tif (defaultHandler) {\n\t\t\thandler = [{ id: 0, hook, handler: defaultHandler }];\n\t\t\tif (replaced) {\n\t\t\t\tconst index = replace.length - 1;\n\t\t\t\tconst { handler: replacingHandler, once } = replace[index];\n\t\t\t\tconst createDefaultHandler = (index: number): HookDefaultHandler<T> | undefined => {\n\t\t\t\t\tconst next = replace[index - 1];\n\t\t\t\t\tif (next) {\n\t\t\t\t\t\treturn (visit, args) =>\n\t\t\t\t\t\t\tnext.handler(visit, args, createDefaultHandler(index - 1));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn defaultHandler;\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tconst nestedDefaultHandler = createDefaultHandler(index);\n\t\t\t\thandler = [{ id: 0, hook, once, handler: replacingHandler, defaultHandler: nestedDefaultHandler }]; // prettier-ignore\n\t\t\t}\n\t\t}\n\n\t\treturn { found: true, before, handler, after, replaced };\n\t}\n\n\t/**\n\t * Sort two hook registrations by priority and registration order.\n\t * @param a The registration object to compare\n\t * @param b The other registration object to compare with\n\t * @returns The sort direction\n\t */\n\tprotected sortRegistrations<T extends HookName>(\n\t\ta: HookRegistration<T>,\n\t\tb: HookRegistration<T>\n\t): number {\n\t\tconst priority = (a.priority ?? 0) - (b.priority ?? 0);\n\t\tconst id = a.id - b.id;\n\t\treturn priority || id || 0;\n\t}\n\n\t/**\n\t * Dispatch a custom event on the `document` for a hook. Prefixed with `swup:`\n\t * @param hook Name of the hook.\n\t */\n\tprotected dispatchDomEvent<T extends HookName>(\n\t\thook: T,\n\t\tvisit: Visit | undefined,\n\t\targs?: HookArguments<T>\n\t): void {\n\t\tif (visit?.done) return;\n\n\t\tconst detail: HookEventDetail = { hook, args, visit: visit || this.swup.visit };\n\t\tdocument.dispatchEvent(\n\t\t\tnew CustomEvent<HookEventDetail>(`swup:any`, { detail, bubbles: true })\n\t\t);\n\t\tdocument.dispatchEvent(\n\t\t\tnew CustomEvent<HookEventDetail>(`swup:${hook}`, { detail, bubbles: true })\n\t\t);\n\t}\n\n\t/**\n\t * Parse a hook name into the name and any modifiers.\n\t * @param hook Name of the hook.\n\t */\n\tparseName(hook: HookName | HookNameWithModifier): [HookName, Partial<HookOptions>] {\n\t\tconst [name, ...modifiers] = hook.split('.');\n\t\tconst options = modifiers.reduce((acc, mod) => ({ ...acc, [mod]: true }), {});\n\t\treturn [name as HookName, options];\n\t}\n}\n","import { query } from '../utils.js';\n\n/**\n * Find the anchor element for a given hash.\n *\n * @param hash Hash with or without leading '#'\n * @returns The element, if found, or null.\n *\n * @see https://html.spec.whatwg.org/#find-a-potential-indicated-element\n */\nexport const getAnchorElement = (hash?: string): Element | null => {\n\tif (hash && hash.charAt(0) === '#') {\n\t\thash = hash.substring(1);\n\t}\n\n\tif (!hash) {\n\t\treturn null;\n\t}\n\n\tconst decoded = decodeURIComponent(hash);\n\tlet element =\n\t\tdocument.getElementById(hash) ||\n\t\tdocument.getElementById(decoded) ||\n\t\tquery(`a[name='${CSS.escape(hash)}']`) ||\n\t\tquery(`a[name='${CSS.escape(decoded)}']`);\n\n\tif (!element && hash === 'top') {\n\t\telement = document.body;\n\t}\n\n\treturn element;\n};\n","import { queryAll } from '../utils.js';\nimport type Swup from '../Swup.js';\nimport type { Options } from '../Swup.js';\n\nconst TRANSITION = 'transition';\nconst ANIMATION = 'animation';\n\ntype AnimationType = typeof TRANSITION | typeof ANIMATION;\ntype AnimationEndEvent = `${AnimationType}end`;\ntype AnimationProperty = 'Delay' | 'Duration';\ntype AnimationStyleKey = `${AnimationType}${AnimationProperty}` | 'transitionProperty';\n\nexport type AnimationDirection = 'in' | 'out';\n\n/**\n * Return a Promise that resolves when all CSS animations and transitions\n * are done on the page. Filters by selector or takes elements directly.\n */\nexport async function awaitAnimations(\n\tthis: Swup,\n\t{\n\t\tselector,\n\t\telements\n\t}: {\n\t\tselector: Options['animationSelector'];\n\t\telements?: NodeListOf<HTMLElement> | HTMLElement[];\n\t}\n): Promise<void> {\n\t// Allow usage of swup without animations: { animationSelector: false }\n\tif (selector === false && !elements) {\n\t\treturn;\n\t}\n\n\t// Allow passing in elements\n\tlet animatedElements: HTMLElement[] = [];\n\tif (elements) {\n\t\tanimatedElements = Array.from(elements);\n\t} else if (selector) {\n\t\tanimatedElements = queryAll(selector, document.body);\n\t\t// Warn if no elements match the selector, but keep things going\n\t\tif (!animatedElements.length) {\n\t\t\tconsole.warn(`[swup] No elements found matching animationSelector \\`${selector}\\``);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tconst awaitedAnimations = animatedElements.map((el) => awaitAnimationsOnElement(el));\n\tconst hasAnimations = awaitedAnimations.filter(Boolean).length > 0;\n\tif (!hasAnimations) {\n\t\tif (selector) {\n\t\t\tconsole.warn(\n\t\t\t\t`[swup] No CSS animation duration defined on elements matching \\`${selector}\\``\n\t\t\t);\n\t\t}\n\t\treturn;\n\t}\n\n\tawait Promise.all(awaitedAnimations);\n}\n\nfunction awaitAnimationsOnElement(element: HTMLElement): Promise<void> | false {\n\tconst { type, timeout, propCount } = getTransitionInfo(element);\n\n\t// Resolve immediately if no transition defined\n\tif (!type || !timeout) {\n\t\treturn false;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tconst endEvent: AnimationEndEvent = `${type}end`;\n\t\tconst startTime = performance.now();\n\t\tlet propsTransitioned = 0;\n\n\t\tconst end = () => {\n\t\t\telement.removeEventListener(endEvent, onEnd);\n\t\t\tresolve();\n\t\t};\n\n\t\tconst onEnd = (event: TransitionEvent | AnimationEvent) => {\n\t\t\t// Skip transitions on child elements\n\t\t\tif (event.target !== element) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Skip transitions that happened before we started listening\n\t\t\tconst elapsedTime = (performance.now() - startTime) / 1000;\n\t\t\tif (elapsedTime < event.elapsedTime) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// End if all properties have transitioned\n\t\t\tif (++propsTransitioned >= propCount) {\n\t\t\t\tend();\n\t\t\t}\n\t\t};\n\n\t\tsetTimeout(() => {\n\t\t\tif (propsTransitioned < propCount) {\n\t\t\t\tend();\n\t\t\t}\n\t\t}, timeout + 1);\n\n\t\telement.addEventListener(endEvent, onEnd);\n\t});\n}\n\nfunction getTransitionInfo(element: Element) {\n\tconst styles = window.getComputedStyle(element);\n\n\tconst transitionDelays = getStyleProperties(styles, `${TRANSITION}Delay`);\n\tconst transitionDurations = getStyleProperties(styles, `${TRANSITION}Duration`);\n\tconst transitionTimeout = calculateTimeout(transitionDelays, transitionDurations);\n\n\tconst animationDelays = getStyleProperties(styles, `${ANIMATION}Delay`);\n\tconst animationDurations = getStyleProperties(styles, `${ANIMATION}Duration`);\n\tconst animationTimeout = calculateTimeout(animationDelays, animationDurations);\n\n\tconst timeout = Math.max(transitionTimeout, animationTimeout);\n\tconst type: AnimationType | null =\n\t\ttimeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null;\n\tconst propCount = type\n\t\t? type === TRANSITION\n\t\t\t? transitionDurations.length\n\t\t\t: animationDurations.length\n\t\t: 0;\n\n\treturn {\n\t\ttype,\n\t\ttimeout,\n\t\tpropCount\n\t};\n}\n\nexport function getStyleProperties(styles: CSSStyleDeclaration, key: AnimationStyleKey): string[] {\n\treturn (styles[key] || '').split(', ');\n}\n\nexport function calculateTimeout(delays: string[], durations: string[]): number {\n\twhile (delays.length < durations.length) {\n\t\tdelays = delays.concat(delays);\n\t}\n\n\treturn Math.max(...durations.map((duration, i) => toMs(duration) + toMs(delays[i])));\n}\n\nexport function toMs(time: string): number {\n\treturn parseFloat(time) * 1000;\n}\n","import type Swup from '../Swup.js';\nimport { FetchError, type FetchOptions, type PageData } from './fetchPage.js';\nimport { type VisitInitOptions, type Visit, VisitState } from './Visit.js';\nimport { createHistoryRecord, updateHistoryRecord, Location, classify } from '../helpers.js';\nimport { getContextualAttr } from '../utils.js';\n\nexport type HistoryAction = 'push' | 'replace';\nexport type HistoryDirection = 'forwards' | 'backwards';\nexport type NavigationToSelfAction = 'scroll' | 'navigate';\nexport type CacheControl = Partial<{ read: boolean; write: boolean }>;\n\n/** Define how to navigate to a page. */\ntype NavigationOptions = {\n\t/** Whether this visit is animated. Default: `true` */\n\tanimate?: boolean;\n\t/** Name of a custom animation to run. */\n\tanimation?: string;\n\t/** History action to perform: `push` for creating a new history entry, `replace` for replacing the current entry. Default: `push` */\n\thistory?: HistoryAction;\n\t/** Whether this visit should read from or write to the cache. */\n\tcache?: CacheControl;\n\t/** Custom metadata associated with this visit. */\n\tmeta?: Record<string, unknown>;\n};\n\n/**\n * Navigate to a new URL.\n * @param url The URL to navigate to.\n * @param options Options for how to perform this visit.\n * @returns Promise<void>\n */\nexport function navigate(\n\tthis: Swup,\n\turl: string,\n\toptions: NavigationOptions & FetchOptions = {},\n\tinit: Omit<VisitInitOptions, 'to'> = {}\n) {\n\tif (typeof url !== 'string') {\n\t\tthrow new Error(`swup.navigate() requires a URL parameter`);\n\t}\n\n\t// Check if the visit should be ignored\n\tif (this.shouldIgnoreVisit(url, { el: init.el, event: init.event })) {\n\t\twindow.location.assign(url);\n\t\treturn;\n\t}\n\n\tconst { url: to, hash } = Location.fromUrl(url);\n\n\tconst visit = this.createVisit({ ...init, to, hash });\n\tthis.performNavigation(visit, options);\n}\n\n/**\n * Start a visit to a new URL.\n *\n * Internal method that assumes the visit context has already been created.\n *\n * As a user, you should call `swup.navigate(url)` instead.\n *\n * @param url The URL to navigate to.\n * @param options Options for how to perform this visit.\n * @returns Promise<void>\n */\nexport async function performNavigation(\n\tthis: Swup,\n\tvisit: Visit,\n\toptions: NavigationOptions & FetchOptions = {}\n): Promise<void> {\n\tif (this.navigating) {\n\t\tif (this.visit.state >= VisitState.ENTERING) {\n\t\t\t// Currently navigating and content already loaded? Finish