;
/** Create a match function from a path pattern that checks if a URLs matches it. */
export const matchPath = (
path: Params[0],
options?: Params[1]
): MatchFunction
=> {
if (Array.isArray(path) && !path.length) {
path = '';
}
try {
return match
(path, options);
} catch (error) {
throw new Error(`[swup] Error parsing path "${String(path)}":\n${String(error)}`);
}
};
swup-swup-23b2631/src/index.ts 0000664 0000000 0000000 00000002277 15147416072 0016235 0 ustar 00root root 0000000 0000000 import type { Path } from 'path-to-regexp';
import type { DelegateEventUnsubscribe } from './helpers/delegateEvent.js';
import type { DelegateEvent, DelegateEventHandler } from 'delegate-it';
import Swup from './Swup.js';
import type { Options } from './Swup.js';
import type { CacheData } from './modules/Cache.js';
import type { PageData } from './modules/fetchPage.js';
import type {
Visit,
VisitFrom,
VisitTo,
VisitAnimation,
VisitScroll,
VisitHistory
} from './modules/Visit.js';
import type {
HookName,
HookDefinitions,
HookArguments,
HookReturnValues,
HookHandler,
HookDefaultHandler,
HookOptions,
HookUnregister,
HookEvent
} from './modules/Hooks.js';
import type { Plugin } from './modules/plugins.js';
export default Swup;
export * from './helpers.js';
export * from './utils.js';
export type {
Swup,
Options,
Plugin,
CacheData,
PageData,
Path,
Visit,
VisitFrom,
VisitTo,
VisitAnimation,
VisitScroll,
VisitHistory,
HookName,
HookDefinitions,
HookArguments,
HookReturnValues,
HookHandler,
HookHandler as Handler, // backwards compatibility
HookDefaultHandler,
HookOptions,
HookUnregister,
HookEvent,
DelegateEvent,
DelegateEventHandler,
DelegateEventUnsubscribe
};
swup-swup-23b2631/src/modules/ 0000775 0000000 0000000 00000000000 15147416072 0016216 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/src/modules/Cache.ts 0000775 0000000 0000000 00000004257 15147416072 0017604 0 ustar 00root root 0000000 0000000 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 = 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);
}
}
swup-swup-23b2631/src/modules/Classes.ts 0000664 0000000 0000000 00000002375 15147416072 0020172 0 ustar 00root root 0000000 0000000 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));
}
}
swup-swup-23b2631/src/modules/Hooks.ts 0000664 0000000 0000000 00000051670 15147416072 0017662 0 ustar 00root root 0000000 0000000 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 };
'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;
'fetch:request': Promise;
'page:load': Promise;
'scroll:top': boolean;
'scroll:anchor': boolean;
}
export type HookArguments = HookDefinitions[T];
export type HookName = keyof HookDefinitions;
export type HookNameWithModifier = `${HookName}.${HookModifier}`;
type HookModifier = 'once' | 'before' | 'replace';
/** A generic hook handler. */
export type HookHandler = (
/** Context about the current visit. */
visit: Visit,
/** Local arguments passed into the handler. */
args: HookArguments
) => Promise | unknown;
/** A default hook handler with an expected return type. */
export type HookDefaultHandler = (
/** Context about the current visit. */
visit: Visit,
/** Local arguments passed into the handler. */
args: HookArguments,
/** Default handler to be executed. Available if replacing an internal hook handler. */
defaultHandler?: HookDefaultHandler
) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise | unknown;
export type Handlers = {
[K in HookName]: HookHandler[];
};
export type HookInitOptions = {
[K in HookName as K | `${K}.${HookModifier}`]: HookHandler;
} & {
[K in HookName as K | `${K}.${HookModifier}.${HookModifier}`]: HookHandler;
};
/** 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 | HookDefaultHandler = HookHandler
> = {
id: number;
hook: T;
handler: H;
defaultHandler?: HookDefaultHandler;
} & HookOptions;
type HookEventDetail = {
hook: HookName;
args: unknown;
visit: Visit;
};
export type HookEvent = CustomEvent;
type HookLedger = Map, HookRegistration>;
interface HookRegistry extends Map> {
get(key: K): HookLedger | undefined;
set(key: K, value: HookLedger): 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(hook: T): HookLedger | 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(hook: T, handler: HookDefaultHandler, options: O & { replace: true }): HookUnregister; // prettier-ignore
// Overload: passed in handler options
on(hook: T, handler: HookHandler, options: O): HookUnregister; // prettier-ignore
// Overload: no handler options
on(hook: T, handler: HookHandler): HookUnregister; // prettier-ignore
// Implementation
on(
hook: T,
handler: O['replace'] extends true ? HookDefaultHandler : HookHandler,
options: Partial = {}
): HookUnregister {
const ledger = this.get(hook);
if (!ledger) {
console.warn(`Hook '${hook}' not found.`);
return () => {};
}
const id = ledger.size + 1;
const registration: HookRegistration = { ...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(hook: T, handler: HookHandler, options: HookOptions): HookUnregister; // prettier-ignore
// Overload: no handler options
before(hook: T, handler: HookHandler): HookUnregister;
// Implementation
before(
hook: T,
handler: HookHandler,
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(hook: T, handler: HookDefaultHandler, options: HookOptions): HookUnregister; // prettier-ignore
// Overload: no handler options
replace(hook: T, handler: HookDefaultHandler): HookUnregister; // prettier-ignore
// Implementation
replace(
hook: T,
handler: HookDefaultHandler,
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(hook: T, handler: HookHandler, options: HookOptions): HookUnregister; // prettier-ignore
// Overload: no handler options
once(hook: T, handler: HookHandler): HookUnregister;
// Implementation
once(
hook: T,
handler: HookHandler,
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(hook: T, handler: HookHandler | HookDefaultHandler): void;
// Overload: unregister all handlers
off(hook: T): void;
// Implementation
off(hook: T, handler?: HookHandler | HookDefaultHandler): 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(hook: T, visit: Visit | undefined, args: HookArguments, defaultHandler?: HookDefaultHandler): Promise>>>; // prettier-ignore
// Overload: legacy order of arguments, with visit missing
async call(hook: T, args: HookArguments, defaultHandler?: HookDefaultHandler): Promise>>>; // prettier-ignore
// Implementation
async call(
hook: T,
arg1: Visit | HookArguments,
arg2: HookArguments | HookDefaultHandler,
arg3?: HookDefaultHandler
): Promise>>> {
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(hook: T, visit: Visit | undefined, args: HookArguments, defaultHandler?: HookDefaultHandler): ReturnType>; // prettier-ignore
// Overload: legacy order of arguments, with visit missing
callSync(hook: T, args: HookArguments, defaultHandler?: HookDefaultHandler): ReturnType>; // prettier-ignore
// Implementation
callSync(
hook: T,
arg1: Visit | HookArguments,
arg2: HookArguments | HookDefaultHandler,
arg3?: HookDefaultHandler
): ReturnType> {
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(
hook: T,
arg1: Visit | HookArguments | undefined,
arg2: HookArguments | HookDefaultHandler,
arg3?: HookDefaultHandler
): [Visit | undefined, HookArguments, HookDefaultHandler | 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, arg2 as HookDefaultHandler];
} else {
// Default positioning: visit passed in as first argument
return [arg1, arg2 as HookArguments, 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(registrations: HookRegistration>[], visit: Visit | undefined, args: HookArguments, rethrow: true): Promise>>[]>; // prettier-ignore
// Overload: running user handler: expect no specific type
protected async run(registrations: HookRegistration[], visit: Visit | undefined, args: HookArguments): Promise; // prettier-ignore
// Implementation
protected async run[]>(
registrations: R,
visit: Visit | undefined = this.swup.visit,
args: HookArguments,
rethrow: boolean = false
): Promise>> | 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(registrations: HookRegistration>[], visit: Visit | undefined, args: HookArguments, rethrow: true): ReturnType>[]; // prettier-ignore
// Overload: running user handler: expect no specific type
protected runSync(registrations: HookRegistration[], visit: Visit | undefined, args: HookArguments): unknown[]; // prettier-ignore
// Implementation
protected runSync[]>(
registrations: R,
visit: Visit | undefined = this.swup.visit,
args: HookArguments,
rethrow: boolean = false
): (ReturnType> | 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)(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(hook: T, defaultHandler?: HookDefaultHandler) {
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 is HookRegistration> => 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>[] = [];
if (defaultHandler) {
handler = [{ id: 0, hook, handler: defaultHandler }];
if (replaced) {
const index = replace.length - 1;
const { handler: replacingHandler, once } = replace[index];
const createDefaultHandler = (index: number): HookDefaultHandler | 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, once, handler: replacingHandler, defaultHandler: nestedDefaultHandler }]; // prettier-ignore
}
}
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(
a: HookRegistration,
b: HookRegistration
): 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(
hook: T,
visit: Visit | undefined,
args?: HookArguments
): void {
if (visit?.done) return;
const detail: HookEventDetail = { hook, args, visit: visit || this.swup.visit };
document.dispatchEvent(
new CustomEvent(`swup:any`, { detail, bubbles: true })
);
document.dispatchEvent(
new CustomEvent(`swup:${hook}`, { detail, bubbles: true })
);
}
/**
* Parse a hook name into the name and any modifiers.
* @param hook Name of the hook.
*/
parseName(hook: HookName | HookNameWithModifier): [HookName, Partial] {
const [name, ...modifiers] = hook.split('.');
const options = modifiers.reduce((acc, mod) => ({ ...acc, [mod]: true }), {});
return [name as HookName, options];
}
}
swup-swup-23b2631/src/modules/Visit.ts 0000664 0000000 0000000 00000011137 15147416072 0017667 0 ustar 00root root 0000000 0000000 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;
/** User-defined metadata */
meta: Record;
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
};
this.meta = {};
}
/** @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);
}
swup-swup-23b2631/src/modules/animatePageIn.ts 0000775 0000000 0000000 00000001416 15147416072 0021275 0 ustar 00root root 0000000 0000000 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
*/
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);
};
swup-swup-23b2631/src/modules/animatePageOut.ts 0000775 0000000 0000000 00000001201 15147416072 0021466 0 ustar 00root root 0000000 0000000 import type Swup from '../Swup.js';
import type { Visit } from './Visit.js';
/**
* Perform the out/leave animation of the current page.
* @returns Promise
*/
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);
};
swup-swup-23b2631/src/modules/awaitAnimations.ts 0000664 0000000 0000000 00000010233 15147416072 0021715 0 ustar 00root root 0000000 0000000 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[];
}
): Promise {
// 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 | 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;
}
swup-swup-23b2631/src/modules/fetchPage.ts 0000664 0000000 0000000 00000006354 15147416072 0020464 0 ustar 00root root 0000000 0000000 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 {
/** 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 {
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 | 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;
}
swup-swup-23b2631/src/modules/getAnchorElement.ts 0000775 0000000 0000000 00000001346 15147416072 0022021 0 ustar 00root root 0000000 0000000 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;
};
swup-swup-23b2631/src/modules/navigate.ts 0000775 0000000 0000000 00000016361 15147416072 0020376 0 ustar 00root root 0000000 0000000 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;
/** Custom metadata associated with this visit. */
meta?: Record;
};
/**
* Navigate to a new URL.
* @param url The URL to navigate to.
* @param options Options for how to perform this visit.
* @returns Promise
*/
export function navigate(
this: Swup,
url: string,
options: NavigationOptions & FetchOptions = {},
init: Omit = {}
) {
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
*/
export async function performNavigation(
this: Swup,
visit: Visit,
options: NavigationOptions & FetchOptions = {}
): Promise {
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;
}
// Get custom metadata from option
visit.meta = options.meta || {};
// 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 misinterpret 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;
}
}
swup-swup-23b2631/src/modules/plugins.ts 0000664 0000000 0000000 00000003566 15147416072 0020261 0 ustar 00root root 0000000 0000000 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 compatibility. */
version?: string;
/** The swup instance that mounted this plugin */
swup?: Swup;
/** Version requirements of this plugin. Example: `{ swup: '>=4' }` */
requires?: Record;
/** 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)}`
);
}
swup-swup-23b2631/src/modules/renderPage.ts 0000775 0000000 0000000 00000003340 15147416072 0020645 0 ustar 00root root 0000000 0000000 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 {
// 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 });
};
swup-swup-23b2631/src/modules/replaceContent.ts 0000775 0000000 0000000 00000003117 15147416072 0021541 0 ustar 00root root 0000000 0000000 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;
};
swup-swup-23b2631/src/modules/resolveUrl.ts 0000775 0000000 0000000 00000002017 15147416072 0020733 0 ustar 00root root 0000000 0000000 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);
}
swup-swup-23b2631/src/modules/scrollToContent.ts 0000775 0000000 0000000 00000001651 15147416072 0021730 0 ustar 00root root 0000000 0000000 import type Swup from '../Swup.js';
import type { Visit } from './Visit.js';
/**
* Update the scroll position after page render.
* @returns Promise
*/
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;
};
swup-swup-23b2631/src/utils.ts 0000664 0000000 0000000 00000000215 15147416072 0016254 0 ustar 00root root 0000000 0000000 // Re-export all utils to allow custom package export path
// e.g. import { queryAll } from 'swup/utils';
export * from './utils/index.js';
swup-swup-23b2631/src/utils/ 0000775 0000000 0000000 00000000000 15147416072 0015706 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/src/utils/index.ts 0000664 0000000 0000000 00000004013 15147416072 0017363 0 ustar 00root root 0000000 0000000 /** Find an element by selector. */
export const query = (selector: string, context: Document | Element = document) => {
return context.querySelector(selector);
};
/** Find a set of elements by selector. */
export const queryAll = (
selector: string,
context: Document | Element = document
): HTMLElement[] => {
return Array.from(context.querySelectorAll(selector));
};
/** Return a Promise that resolves after the next event loop. */
export const nextTick = (): Promise => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
resolve();
});
});
});
};
/** Check if an object is a Promise or a Thenable */
export function isPromise(obj: unknown): obj is PromiseLike {
return (
!!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof (obj as Record).then === 'function'
);
}
/** Call a function as a Promise. Resolves with the returned Promsise or immediately. */
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any
export function runAsPromise(func: Function, args: unknown[] = []): Promise {
return new Promise((resolve, reject) => {
const result: unknown = func(...args);
if (isPromise(result)) {
result.then(resolve, reject);
} else {
resolve(result);
}
});
}
/**
* Force a layout reflow, e.g. after adding classnames
* @see https://stackoverflow.com/a/21665117/3759615
*/
export function forceReflow(element?: HTMLElement): void {
element = element || document.body;
element?.getBoundingClientRect();
}
/**
* Read data attribute from closest element with that attribute.
*
* Returns `undefined` if no element is found or attribute is missing.
* Returns `true` if attribute is present without a value.
*/
export function getContextualAttr(
el: Element | undefined,
attr: string
): string | boolean | undefined {
const target = el?.closest(`[${attr}]`);
return target?.hasAttribute(attr) ? target?.getAttribute(attr) || true : undefined;
}
swup-swup-23b2631/tests/ 0000775 0000000 0000000 00000000000 15147416072 0015121 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/config/ 0000775 0000000 0000000 00000000000 15147416072 0016366 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/config/playwright.config.ts 0000664 0000000 0000000 00000004463 15147416072 0022403 0 ustar 00root root 0000000 0000000 import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
const baseURL = 'http://localhost:8274';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
/* Directory containing the test files */
testDir: '../functional',
/* Folder for test artifacts: screenshots, videos, ... */
outputDir: '../results',
/* Timeout individual tests after 5 seconds */
timeout: 10_000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 1 : 0,
/* Limit parallel workers on CI, use default locally. */
workers: process.env.CI ? 1 : undefined,
// Limit the number of failures on CI to save resources
maxFailures: process.env.CI ? 10 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? [
['dot'],
['github'],
['blob', { outputDir: '../reports/blobs' }],
] : [
['list'],
['html', { outputFolder: '../reports/html', open: 'on-failure' }],
['json', { outputFile: '../reports/json/report.json' }],
],
expect: {
/* Timeout async expect matchers after 2 seconds */
timeout: 3_000,
},
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Capture screenshot after each test failure. */
screenshot: 'only-on-failure',
/* Capture video if failed tests. */
video: 'retain-on-failure',
},
/* Configure projects for major browsers */
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
/* Run your local dev server before starting the tests */
webServer: {
url: baseURL,
command: 'npm run test:e2e:start',
reuseExistingServer: !process.env.CI,
},
});
swup-swup-23b2631/tests/config/serve.json 0000775 0000000 0000000 00000000225 15147416072 0020407 0 ustar 00root root 0000000 0000000 {
"public": "tests/fixtures",
"cleanUrls": false,
"redirects": [
{ "source": "/redirect-2.html", "destination": "/redirect-3.html" }
]
}
swup-swup-23b2631/tests/config/vitest.config.ts 0000664 0000000 0000000 00000000663 15147416072 0021525 0 ustar 00root root 0000000 0000000 /**
* Vitest config file
* @see https://vitest.dev/config/
*/
import path from "path";
import { defineConfig } from 'vitest/config';
const __dirname = path.dirname(__filename);
export default defineConfig({
test: {
environment: 'jsdom',
include: [
'src/**/__tests__/**/*.?(c|m)[jt]s?(x)',
'tests/unit/**/?(*.){test,spec}.?(c|m)[jt]s?(x)'
],
setupFiles: [
path.resolve(__dirname, './vitest.setup.ts')
]
}
});
swup-swup-23b2631/tests/config/vitest.setup.ts 0000664 0000000 0000000 00000000213 15147416072 0021407 0 ustar 00root root 0000000 0000000 import { vi } from 'vitest';
// Stub browser functions for vitest
console.log = vi.fn();
console.warn = vi.fn();
console.error = vi.fn();
swup-swup-23b2631/tests/fixtures/ 0000775 0000000 0000000 00000000000 15147416072 0016772 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/alpinejs/ 0000775 0000000 0000000 00000000000 15147416072 0020577 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/alpinejs/page-1.html 0000664 0000000 0000000 00000006525 15147416072 0022547 0 ustar 00root root 0000000 0000000
Page 1
Page 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Custom animation
swup-swup-23b2631/tests/fixtures/alpinejs/page-2.html 0000664 0000000 0000000 00000006274 15147416072 0022551 0 ustar 00root root 0000000 0000000
Page 2
Page 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/animation-complex.html 0000664 0000000 0000000 00000002346 15147416072 0023311 0 ustar 00root root 0000000 0000000
Animation duration: complex
Animation duration: complex
swup-swup-23b2631/tests/fixtures/animation-duration.html 0000664 0000000 0000000 00000002215 15147416072 0023462 0 ustar 00root root 0000000 0000000
Animation duration
Animation duration
swup-swup-23b2631/tests/fixtures/animation-keyframes.html 0000664 0000000 0000000 00000002670 15147416072 0023630 0 ustar 00root root 0000000 0000000
Animation duration: keyframes
Animation duration: keyframes
swup-swup-23b2631/tests/fixtures/animation-native.html 0000664 0000000 0000000 00000003205 15147416072 0023123 0 ustar 00root root 0000000 0000000
Animation duration: native
Animation duration: native
swup-swup-23b2631/tests/fixtures/animation-none.html 0000664 0000000 0000000 00000002023 15147416072 0022571 0 ustar 00root root 0000000 0000000
Animation duration: none
Animation duration: none
swup-swup-23b2631/tests/fixtures/animation-partial.html 0000664 0000000 0000000 00000002313 15147416072 0023270 0 ustar 00root root 0000000 0000000
Animation duration: partial
Animation duration: partial
swup-swup-23b2631/tests/fixtures/assets/ 0000775 0000000 0000000 00000000000 15147416072 0020274 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/assets/main.css 0000664 0000000 0000000 00000011067 15147416072 0021737 0 ustar 00root root 0000000 0000000 * {
box-sizing: border-box;
}
html {
font-size: 62.5%;
}
html,
body {
background: #fff;
margin: 0 auto;
width: 100%;
height: 100%;
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
color: #181818;
}
body {
font-size: 16px;
}
h1 {
font-size: 30px;
margin: 0 auto 20px;
padding: 3px;
}
@media (min-width: 768px) {
h1 {
font-size: 60px;
margin: 0 auto 40px;
}
}
p,
main li {
font-size: 16px;
color: #181818;
text-align: left;
}
p,
main ul {
margin: 0 auto 30px;
}
@media (min-width: 768px) {
p,
main ul {
margin: 0 auto 50px;
}
}
main ul {
display: block;
padding: 0;
list-style-type: none;
}
main ul li {
display: block;
padding: 0 0 0 20px;
position: relative;
margin: 0 0 10px;
}
@media (min-width: 768px) {
main ul li {
margin: 0 0 20px;
}
}
main ul li:before {
content: '';
height: 2px;
width: 10px;
background: #00a2fe;
display: block;
position: absolute;
left: 0;
top: 10px;
}
.u-color-blue {
color: #00a2fe !important;
}
.u-color-purple {
color: #733df9 !important;
}
h1 span,
p span,
li span {
display: inline-block;
overflow: hidden;
vertical-align: middle;
}
h1 span > span,
p span > span,
li span > span {
display: block;
animation: slideUp 0.8s cubic-bezier(0.075, 0.82, 0.165, 1) forwards;
transform: translate3d(0, 100%, 0);
}
html.is-changing .transition-default {
transition: 0.1s;
opacity: 1;
}
html.is-animating .transition-default {
opacity: 0;
}
.transition-default.is-changing {
transition: 0.1s;
opacity: 1;
}
.transition-default.is-animating {
opacity: 0;
}
.wrapper {
width: 100%;
text-align: center;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
margin: 0;
padding: 40px 20px;
}
.wrapper-inner {
max-width: 600px;
margin: 0 auto;
}
.button {
display: inline-block;
text-transform: uppercase;
line-height: 1.4;
padding: 16px 30px;
border-radius: 4px;
text-decoration: none;
cursor: pointer;
text-align: center;
min-width: 160px;
font-weight: 700;
font-size: 15px;
margin: 0 1px 10px;
}
@media (min-width: 768px) {
.button {
margin: 0 10px 10px;
}
}
.button--white {
background: #fff;
border: 1px solid #181818;
color: #181818;
margin: 0;
}
.button--blue {
border: 2px solid #00a2fe;
background: #00a2fe;
color: #fff;
box-shadow: 0 16px 10px -10px rgba(0,162,254,0.6);
}
.button--blue:active {
box-shadow: 0 14px 10px -10px rgba(0,162,254,0.6);
}
.button--blue-border {
border: 2px solid #00a2fe;
background: #fff;
color: #00a2fe;
box-shadow: 0 16px 10px -10px rgba(0,162,254,0.2);
}
.button--blue-border:active {
box-shadow: 0 14px 10px -10px rgba(0,162,254,0.2);
}
.button--purple {
border: 2px solid #733df9;
background: #733df9;
color: #fff;
box-shadow: 0 16px 10px -10px rgba(115,61,249,0.6);
}
.button--purple:active {
box-shadow: 0 14px 10px -10px rgba(115,61,249,0.6);
}
.button--purple-border {
border: 2px solid #733df9;
background: #fff;
color: #733df9;
box-shadow: 0 16px 10px -10px rgba(115,61,249,0.2);
}
.button--purple-border:active {
box-shadow: 0 14px 10px -10px rgba(115,61,249,0.2);
}
.button--fade {
opacity: 1;
transition: opacity 0.3s 1s;
}
html.is-animating .button--fade {
transition: opacity 0.3s 0s;
opacity: 0;
}
.button:active {
transform: scale(0.98);
}
.increment {
display: block;
width: 200px;
margin: 0 auto 20px;
}
@media (min-width: 768px) {
.increment {
margin: 0 auto 50px;
}
}
.increment__display {
display: block;
border: 2px solid #ddd;
background: #fff;
margin: 0 auto 20px;
appearance: none;
border-radius: 4px;
width: 100%;
}
.increment__display {
height: 100px;
display: flex;
justify-content: center;
align-items: center;
font-size: 60px;
}
@media (min-width: 768px) {
.increment__display {
height: 160px;
font-size: 100px;
}
}
.increment__button {
width: 100%;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 10px;
z-index: 100;
}
@media (min-width: 768px) {
.header {
padding: 20px;
}
}
.header ul {
padding: 0;
margin: 0;
display: block;
float: right;
}
.header li {
display: inline-block;
vertical-align: top;
margin: 0 10px;
text-align: center;
}
.header li > .github-button,
.header li > span {
display: inline-block;
margin: 4px 0 0;
}
.header a {
color: #181818;
text-decoration: none;
font-weight: 700;
}
@keyframes slideUp {
0% {
transform: translate3d(0, 100%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
swup-swup-23b2631/tests/fixtures/containers-1.html 0000664 0000000 0000000 00000002515 15147416072 0022166 0 ustar 00root root 0000000 0000000
Containers 1
Containers 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Heading 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/containers-2.html 0000664 0000000 0000000 00000002330 15147416072 0022162 0 ustar 00root root 0000000 0000000
Containers 2
Containers 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Heading 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/containers-missing.html 0000664 0000000 0000000 00000002331 15147416072 0023473 0 ustar 00root root 0000000 0000000
Containers missing
Containers missing
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Heading 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/history.html 0000664 0000000 0000000 00000002045 15147416072 0021362 0 ustar 00root root 0000000 0000000
History
History
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/ignore-visits.html 0000664 0000000 0000000 00000002713 15147416072 0022465 0 ustar 00root root 0000000 0000000
Ignore visits
Ignore visits
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/instance.html 0000664 0000000 0000000 00000000547 15147416072 0021472 0 ustar 00root root 0000000 0000000
Instance
swup-swup-23b2631/tests/fixtures/link-resolution.html 0000664 0000000 0000000 00000006562 15147416072 0023027 0 ustar 00root root 0000000 0000000
Link resolution
Link resolution
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/link-selector.html 0000664 0000000 0000000 00000002666 15147416072 0022445 0 ustar 00root root 0000000 0000000
Link selector
Link selector
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
SVG link to page
swup-swup-23b2631/tests/fixtures/nested/ 0000775 0000000 0000000 00000000000 15147416072 0020254 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/nested/nested-1.html 0000664 0000000 0000000 00000001621 15147416072 0022562 0 ustar 00root root 0000000 0000000
Nested Page 1
Nested Page 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/nested/nested-2.html 0000664 0000000 0000000 00000001621 15147416072 0022563 0 ustar 00root root 0000000 0000000
Nested Page 2
Nested Page 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/page-1.html 0000664 0000000 0000000 00000007035 15147416072 0020737 0 ustar 00root root 0000000 0000000
Page 1
Page 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Custom animation
swup-swup-23b2631/tests/fixtures/page-2.html 0000664 0000000 0000000 00000006072 15147416072 0020740 0 ustar 00root root 0000000 0000000
Page 2
Page 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/page-3.html 0000664 0000000 0000000 00000006072 15147416072 0020741 0 ustar 00root root 0000000 0000000
Page 3
Page 3
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/persist-1.html 0000664 0000000 0000000 00000002201 15147416072 0021502 0 ustar 00root root 0000000 0000000
Persist 1
Persist 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Persist 1
Persist 1
swup-swup-23b2631/tests/fixtures/persist-2.html 0000664 0000000 0000000 00000002201 15147416072 0021503 0 ustar 00root root 0000000 0000000
Persist 2
Persist 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Persist 2
Persist 2
swup-swup-23b2631/tests/fixtures/plugins/ 0000775 0000000 0000000 00000000000 15147416072 0020453 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/plugins/body-class-plugin/ 0000775 0000000 0000000 00000000000 15147416072 0024007 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/plugins/body-class-plugin/page-1.html 0000664 0000000 0000000 00000001536 15147416072 0025754 0 ustar 00root root 0000000 0000000
Body Class Plugin 1
Body Class Plugin 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/plugins/body-class-plugin/page-2.html 0000664 0000000 0000000 00000001536 15147416072 0025755 0 ustar 00root root 0000000 0000000
Body Class Plugin 2
Body Class Plugin 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/plugins/scroll-plugin/ 0000775 0000000 0000000 00000000000 15147416072 0023245 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/plugins/scroll-plugin/page-1.html 0000664 0000000 0000000 00000006436 15147416072 0025216 0 ustar 00root root 0000000 0000000
Scroll Plugin 1
Scroll Plugin 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Anchors
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda consectetur
consequatur enim esse incidunt iusto, magnam maxime molestias nisi perferendis
perspiciatis quibusdam quos repellat, vel veniam vero voluptate voluptatem.
Links
swup-swup-23b2631/tests/fixtures/plugins/scroll-plugin/page-2.html 0000664 0000000 0000000 00000001775 15147416072 0025220 0 ustar 00root root 0000000 0000000
Scroll Plugin 2
Scroll Plugin 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
Anchor
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda consectetur
consequatur enim esse incidunt iusto, magnam maxime molestias nisi perferendis
perspiciatis quibusdam quos repellat, vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/rapid-navigation/ 0000775 0000000 0000000 00000000000 15147416072 0022226 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/fixtures/rapid-navigation/page-1.html 0000664 0000000 0000000 00000003537 15147416072 0024176 0 ustar 00root root 0000000 0000000
Rapid Navigation Page 1
Rapid Navigation Page 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/rapid-navigation/page-2.html 0000664 0000000 0000000 00000002102 15147416072 0024162 0 ustar 00root root 0000000 0000000
Rapid Navigation Page 2
Rapid Navigation Page 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/rapid-navigation/page-3.html 0000664 0000000 0000000 00000002102 15147416072 0024163 0 ustar 00root root 0000000 0000000
Rapid Navigation Page 3
Rapid Navigation Page 3
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias assumenda
consectetur consequatur enim esse incidunt iusto, magnam maxime molestias nisi
perferendis perspiciatis quibusdam quos repellat, vel veniam vero voluptate
voluptatem.
swup-swup-23b2631/tests/fixtures/redirect-1.html 0000664 0000000 0000000 00000001727 15147416072 0021626 0 ustar 00root root 0000000 0000000
Redirect 1
Redirect 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/redirect-2.html 0000664 0000000 0000000 00000001757 15147416072 0021632 0 ustar 00root root 0000000 0000000
Redirect 2
Redirect 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/redirect-3.html 0000664 0000000 0000000 00000001757 15147416072 0021633 0 ustar 00root root 0000000 0000000
Redirect 3
Redirect 3
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/fixtures/scrolling-1.html 0000664 0000000 0000000 00000007023 15147416072 0022014 0 ustar 00root root 0000000 0000000
Scrolling 1
Scrolling 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Anchors
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias
assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Links
swup-swup-23b2631/tests/fixtures/scrolling-2.html 0000664 0000000 0000000 00000004112 15147416072 0022011 0 ustar 00root root 0000000 0000000
Scrolling 2
Scrolling 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
Anchor
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Alias assumenda consectetur consequatur enim esse incidunt iusto, magnam
maxime molestias nisi perferendis perspiciatis quibusdam quos repellat,
vel veniam vero voluptate voluptatem.
swup-swup-23b2631/tests/functional/ 0000775 0000000 0000000 00000000000 15147416072 0017263 5 ustar 00root root 0000000 0000000 swup-swup-23b2631/tests/functional/alpinejs.spec.ts 0000664 0000000 0000000 00000001170 15147416072 0022370 0 ustar 00root root 0000000 0000000 import { test } from '@playwright/test';
import { waitForSwup } from '../support/swup.js';
import { clickOnLink } from '../support/commands.js';
import { prefixed } from '../support/utils.js';
const url = prefixed('/alpinejs/');
test.describe('alpinejs compatibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto(url('/page-1.html'));
await waitForSwup(page);
});
test('should listen to dom events', async ({ page }) => {
await clickOnLink(page, url('/page-2.html'));
await page.waitForSelector('.alpine-component.click-fired');
await page.waitForSelector('.alpine-component.any-fired');
});
});
swup-swup-23b2631/tests/functional/animation-classes.spec.ts 0000664 0000000 0000000 00000003136 15147416072 0024201 0 ustar 00root root 0000000 0000000 import { expect, test } from '@playwright/test';
import { clickOnLink, expectToBeAt } from '../support/commands.js';
test.describe('animation classes', () => {
test("doesn't remove `is-leaving` until right before replacing the content", async ({
page
}) => {
await page.goto('/page-1.html');
await page.evaluate(() => {
window.data = {};
window._swup.hooks.before('content:replace', async (visit) => {
window.data.before =
window.document.documentElement.classList.contains('is-leaving');
});
window._swup.hooks.on('content:replace', async (visit) => {
window.data.after =
window.document.documentElement.classList.contains('is-leaving');
});
});
await clickOnLink(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data)).toEqual({
before: true,
after: false
});
});
test("doesn't add `is-rendering` until right before replacing the content", async ({
page
}) => {
await page.goto('/page-1.html');
await page.evaluate(() => {
window.data = {};
window._swup.hooks.before('content:replace', async (visit) => {
window.data.before =
window.document.documentElement.classList.contains('is-rendering');
});
window._swup.hooks.on('content:replace', async (visit) => {
window.data.after =
window.document.documentElement.classList.contains('is-rendering');
});
});
await clickOnLink(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data)).toEqual({
before: false,
after: true
});
});
});
swup-swup-23b2631/tests/functional/animation-timing.spec.ts 0000664 0000000 0000000 00000001324 15147416072 0024030 0 ustar 00root root 0000000 0000000 import { test } from '@playwright/test';
import { expectSwupAnimationDuration } from '../support/swup.js';
test.describe('animation timing', () => {
test('detects animation timing', async ({ page }) => {
await page.goto('/animation-duration.html');
await expectSwupAnimationDuration(page, { out: 400, in: 400, total: 800 });
});
test('detects complex animation timing', async ({ page }) => {
await page.goto('/animation-complex.html');
await expectSwupAnimationDuration(page, { out: 600, in: 600, total: 1200 });
});
test('detects keyframe timing', async ({ page }) => {
await page.goto('/animation-keyframes.html');
await expectSwupAnimationDuration(page, { out: 700, in: 700, total: 1400 });
});
});
swup-swup-23b2631/tests/functional/api-navigation.spec.ts 0000664 0000000 0000000 00000000635 15147416072 0023476 0 ustar 00root root 0000000 0000000 import { test } from '@playwright/test';
import { expectToBeAt } from '../support/commands.js';
test.describe('api navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/page-1.html');
});
test('navigate to pages using swup api', async ({ page }) => {
await page.evaluate(() => window._swup.navigate('/page-2.html'));
await expectToBeAt(page, '/page-2.html', 'Page 2');
});
});
swup-swup-23b2631/tests/functional/cache.spec.ts 0000664 0000000 0000000 00000012043 15147416072 0021627 0 ustar 00root root 0000000 0000000 import { expect, test } from '@playwright/test';
import { expectToBeAt } from '../support/commands.js';
import {
expectSwupToHaveCacheEntries,
expectSwupToHaveCacheEntry,
navigateWithSwup
} from '../support/swup.js';
test.describe('cache', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/page-1.html');
});
test('caches pages', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await expectSwupToHaveCacheEntry(page, '/page-2.html');
});
test('caches pages from absolute URLs', async ({ page, baseURL }) => {
await navigateWithSwup(page, `${baseURL}/page-2.html`);
await expectToBeAt(page, '/page-2.html', 'Page 2');
await expectSwupToHaveCacheEntry(page, '/page-2.html');
});
test('does not cache pages for POST requests', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html', { method: 'POST' });
await expectToBeAt(page, '/page-2.html', 'Page 2');
await expectSwupToHaveCacheEntries(page, []);
});
test('disables cache from swup options', async ({ page }) => {
await page.evaluate(() => {
window.data = { read: null, write: null };
window._swup.options.cache = false;
window._swup.hooks.on('page:load', (visit, { cache }) => {
window.data.read = cache;
window.data.write = window._swup.cache.has(visit.to.url);
});
});
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await navigateWithSwup(page, '/page-1.html');
await expectToBeAt(page, '/page-1.html', 'Page 1');
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await expectSwupToHaveCacheEntries(page, []);
expect(await page.evaluate(() => window.data.read)).toEqual(false);
expect(await page.evaluate(() => window.data.write)).toEqual(false);
});
test('disables cache from navigation options', async ({ page }) => {
await page.evaluate(() => {
window.data = { read: {}, write: {} };
window._swup.hooks.on('page:load', (visit, { cache }) => {
window.data.read[visit.to.url] = cache;
window.data.write[visit.to.url] = window._swup.cache.has(visit.to.url);
});
});
// Check disabling completely
await navigateWithSwup(page, '/page-2.html', { cache: { read: false, write: false } });
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data.read['/page-2.html'])).toEqual(false);
expect(await page.evaluate(() => window.data.write['/page-2.html'])).toEqual(false);
// Check disabling writes
await navigateWithSwup(page, '/page-1.html', { cache: { write: false } });
await expectToBeAt(page, '/page-1.html', 'Page 1');
expect(await page.evaluate(() => window.data.write['/page-1.html'])).toEqual(false);
// Clear cache
await page.evaluate(() => window._swup.cache.clear());
// Check disabling reads
await navigateWithSwup(page, '/page-2.html', { cache: { read: false } });
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data.read['/page-2.html'])).toEqual(false);
expect(await page.evaluate(() => window.data.write['/page-2.html'])).toEqual(true);
});
test('disables cache from visit object', async ({ page }) => {
await page.evaluate(() => {
window.data = { read: {}, write: {} };
window._swup.hooks.on('page:load', (visit, { cache }) => {
window.data.read[visit.to.url] = cache;
window.data.write[visit.to.url] = window._swup.cache.has(visit.to.url);
});
});
// Check disabling writes
await page.evaluate(() => {
window._swup.hooks.on('visit:start', (visit) => (visit.cache.write = false));
});
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data.write['/page-2.html'])).toEqual(false);
// Go back and forth
await navigateWithSwup(page, '/page-1.html');
await expectToBeAt(page, '/page-1.html', 'Page 1');
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
// Check disabling reads
await page.evaluate(() => {
window._swup.hooks.on('visit:start', (visit) => (visit.cache.read = false));
});
await navigateWithSwup(page, '/page-1.html');
await expectToBeAt(page, '/page-1.html', 'Page 1');
expect(await page.evaluate(() => window.data.read['/page-1.html'])).toEqual(false);
});
test('marks cached pages in page:load', async ({ page }) => {
await page.evaluate(() => {
window._swup.hooks.on('page:load', (visit, { cache }) => (window.data = cache));
});
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data)).toEqual(false);
await navigateWithSwup(page, '/page-1.html');
await expectToBeAt(page, '/page-1.html', 'Page 1');
expect(await page.evaluate(() => window.data)).toEqual(false);
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data)).toEqual(true);
});
});
swup-swup-23b2631/tests/functional/containers.spec.ts 0000664 0000000 0000000 00000001532 15147416072 0022732 0 ustar 00root root 0000000 0000000 import { test } from '@playwright/test';
import { expectH1, expectH2, expectPageReload, expectToBeAt } from '../support/commands.js';
import { navigateWithSwup } from '../support/swup.js';
test.describe('containers', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/containers-1.html');
});
test('can be customized from visit object', async ({ page }) => {
await page.evaluate(() => {
window._swup.hooks.on('visit:start', (visit) => visit.containers = ['#aside']);
});
await navigateWithSwup(page, '/containers-2.html');
await expectH1(page, 'Containers 1');
await expectH2(page, 'Heading 2');
});
test('forces reload on container mismatch', async ({ page }) => {
await expectPageReload(page, () => navigateWithSwup(page, '/containers-missing.html'));
await expectToBeAt(page, '/containers-missing.html');
});
});
swup-swup-23b2631/tests/functional/events.spec.ts 0000664 0000000 0000000 00000003120 15147416072 0022064 0 ustar 00root root 0000000 0000000 import { test, expect } from '@playwright/test';
import { clickOnLink } from '../support/commands.js';
test.describe('events', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/page-1.html');
});
test('triggers custom dom events on document', async ({ page }) => {
await page.evaluate(() => {
document.addEventListener(
'swup:link:click',
(event: any) => (window.data = event.detail.hook)
);
});
await clickOnLink(page, '/page-2.html');
expect(await page.evaluate(() => window.data)).toStrictEqual('link:click');
});
test('custom dom events bubble to window', async ({ page }) => {
await page.evaluate(() => {
window.addEventListener(
'swup:link:click',
(event: any) => (window.data = event.detail.hook)
);
});
await clickOnLink(page, '/page-2.html');
expect(await page.evaluate(() => window.data)).toStrictEqual('link:click');
});
test('triggers dom events for "swup:any"', async ({ page }) => {
await page.evaluate(() => {
document.addEventListener('swup:any', (event: any) => {
if (event.detail.hook === 'link:click') window.data = event.detail.hook;
});
});
await clickOnLink(page, '/page-2.html');
expect(await page.evaluate(() => window.data)).toStrictEqual('link:click');
});
test('prevents the default click event', async ({ page }) => {
await page.evaluate(() => {
document.documentElement.addEventListener(
'click',
(event) => (window.data = event.defaultPrevented)
);
});
await clickOnLink(page, '/page-2.html');
expect(await page.evaluate(() => window.data)).toStrictEqual(true);
});
});
swup-swup-23b2631/tests/functional/history.spec.ts 0000664 0000000 0000000 00000012107 15147416072 0022266 0 ustar 00root root 0000000 0000000 import { test, expect } from '@playwright/test';
import { expectPageReload, expectToBeAt, sleep } from '../support/commands.js';
import { navigateWithSwup, pushSwupHistoryState } from '../support/swup.js';
test.describe('history', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/history.html');
});
test('creates a new history state on visit', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.history.state.url)).toEqual('/page-2.html');
});
test('replaces history state via data attribute', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
await expectToBeAt(page, '/history.html', 'History');
const state = await page.evaluate(() => window.history.state);
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.getByTestId('update-link').click();
await expectToBeAt(page, '/page-3.html', 'Page 3');
await page.goBack();
expect(await page.evaluate(() => window.history.state)).toEqual(state);
});
test('replaces history state via API', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
await expectToBeAt(page, '/history.html', 'History');
const state = await page.evaluate(() => window.history.state);
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await navigateWithSwup(page, '/page-3.html', { history: 'replace' });
await expectToBeAt(page, '/page-3.html', 'Page 3');
await page.goBack();
expect(await page.evaluate(() => window.history.state)).toEqual(state);
});
test('navigates to previous page on popstate', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
await expectToBeAt(page, '/history.html', 'History');
});
test('navigates to next page on popstate', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
await expectToBeAt(page, '/history.html', 'History');
await sleep(50);
await page.goForward();
await expectToBeAt(page, '/page-2.html', 'Page 2');
});
test('saves state into the history', async ({ page }) => {
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
const state = await page.evaluate(() => window.history.state);
expect(state).toMatchObject({ source: 'swup', url: '/history.html' });
});
test('calculates travel direction of history visits', async ({ page }) => {
await page.evaluate(() => {
window.data = null;
window._swup.hooks.on('history:popstate', (visit) => (window.data = visit.history.direction));
});
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
await expectToBeAt(page, '/history.html', 'History');
expect(await page.evaluate(() => window.data)).toEqual('backwards');
await page.goForward();
await expectToBeAt(page, '/page-2.html', 'Page 2');
expect(await page.evaluate(() => window.data)).toEqual('forwards');
await navigateWithSwup(page, '/page-3.html');
await expectToBeAt(page, '/page-3.html', 'Page 3');
await page.evaluate(() => window.history.go(-2));
await expectToBeAt(page, '/history.html', 'History');
expect(await page.evaluate(() => window.data)).toEqual('backwards');
});
test('triggers a custom popstate event', async ({ page }) => {
await page.evaluate(() => {
window.data = null;
window._swup.hooks.on('history:popstate', () => window.data = true);
});
await navigateWithSwup(page, '/page-2.html');
await expectToBeAt(page, '/page-2.html', 'Page 2');
await page.goBack();
expect(await page.evaluate(() => window.data)).toEqual(true);
});
test('ignores foreign popstate entries', async ({ page }) => {
await pushSwupHistoryState(page, '/page-2.html', { source: 'not-swup' });
await pushSwupHistoryState(page, '/page-3.html', { source: 'not-swup' });
await page.goBack();
await expectToBeAt(page, '/page-2.html', 'History');
});
test('replaces current history entry on error', async ({ page }) => {
await page.route('/error-500.html', route => route.fulfill({
status: 500,
headers: { 'Content-Type': 'text/html' },
body: 'Error Error