diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2024-08-30 17:21:18 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2024-08-30 17:21:18 +0800 |
| commit | efbc2ffe6a1ee5ec38a4396305561d6515d0943e (patch) | |
| tree | 2b4c94f18f2c792dfda813050755d5c0273fae22 /node_modules/swup/src/modules/Hooks.ts | |
| parent | 43acceb75957c880a5ba5a31aae221394a36d5f6 (diff) | |
node fixes, updates
Diffstat (limited to 'node_modules/swup/src/modules/Hooks.ts')
| -rw-r--r-- | node_modules/swup/src/modules/Hooks.ts | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/node_modules/swup/src/modules/Hooks.ts b/node_modules/swup/src/modules/Hooks.ts new file mode 100644 index 00000000..36af2bf7 --- /dev/null +++ b/node_modules/swup/src/modules/Hooks.ts @@ -0,0 +1,573 @@ +import type { DelegateEvent } from 'delegate-it'; + +import type Swup from '../Swup.js'; +import { isPromise, runAsPromise } from '../utils.js'; +import { Visit } from './Visit.js'; +import type { FetchOptions, PageData } from './fetchPage.js'; + +export interface HookDefinitions { + 'animation:out:start': undefined; + 'animation:out:await': { skip: boolean }; + 'animation:out:end': undefined; + 'animation:in:start': undefined; + 'animation:in:await': { skip: boolean }; + 'animation:in:end': undefined; + 'animation:skip': undefined; + 'cache:clear': undefined; + 'cache:set': { page: PageData }; + 'content:replace': { page: PageData }; + 'content:scroll': undefined; + 'enable': undefined; + 'disable': undefined; + 'fetch:request': { url: string; options: FetchOptions }; + 'fetch:error': { url: string; status: number; response: Response }; + 'fetch:timeout': { url: string }; + 'history:popstate': { event: PopStateEvent }; + 'link:click': { el: HTMLAnchorElement; event: DelegateEvent<MouseEvent> }; + 'link:self': undefined; + 'link:anchor': { hash: string }; + 'link:newtab': { href: string }; + 'page:load': { page?: PageData; cache?: boolean; options: FetchOptions }; + 'page:view': { url: string; title: string }; + 'scroll:top': { options: ScrollIntoViewOptions }; + 'scroll:anchor': { hash: string; options: ScrollIntoViewOptions }; + 'visit:start': undefined; + 'visit:transition': undefined; + 'visit:abort': undefined; + 'visit:end': undefined; +} + +export interface HookReturnValues { + 'content:scroll': Promise<boolean> | boolean; + 'fetch:request': Promise<Response>; + 'page:load': Promise<PageData>; + 'scroll:top': boolean; + 'scroll:anchor': boolean; +} + +export type HookArguments<T extends HookName> = HookDefinitions[T]; + +export type HookName = keyof HookDefinitions; + +/** A generic hook handler. */ +export type HookHandler<T extends HookName> = ( + /** Context about the current visit. */ + visit: Visit, + /** Local arguments passed into the handler. */ + args: HookArguments<T> +) => Promise<unknown> | unknown; + +/** A default hook handler with an expected return type. */ +export type HookDefaultHandler<T extends HookName> = ( + /** Context about the current visit. */ + visit: Visit, + /** Local arguments passed into the handler. */ + args: HookArguments<T>, + /** Default handler to be executed. Available if replacing an internal hook handler. */ + defaultHandler?: HookDefaultHandler<T> +) => T extends keyof HookReturnValues ? HookReturnValues[T] : Promise<unknown> | unknown; + +export type Handlers = { + [K in HookName]: HookHandler<K>[]; +}; + +/** Unregister a previously registered hook handler. */ +export type HookUnregister = () => void; + +/** Define when and how a hook handler is executed. */ +export type HookOptions = { + /** Execute the hook once, then remove the handler */ + once?: boolean; + /** Execute the hook before the internal default handler */ + before?: boolean; + /** Set a priority for when to execute this hook. Lower numbers execute first. Default: `0` */ + priority?: number; + /** Replace the internal default handler with this hook handler */ + replace?: boolean; +}; + +export type HookRegistration< + T extends HookName, + H extends HookHandler<T> | HookDefaultHandler<T> = HookHandler<T> +> = { + id: number; + hook: T; + handler: H; + defaultHandler?: HookDefaultHandler<T>; +} & HookOptions; + +type HookEventDetail = { + hook: HookName; + args: unknown; + visit: Visit; +}; + +export type HookEvent = CustomEvent<HookEventDetail>; + +type HookLedger<T extends HookName> = Map<HookHandler<T>, HookRegistration<T>>; + +interface HookRegistry extends Map<HookName, HookLedger<HookName>> { + get<K extends HookName>(key: K): HookLedger<K> | undefined; + set<K extends HookName>(key: K, value: HookLedger<K>): this; +} + +/** + * Hook registry. + * + * Create, trigger and handle hooks. + * + */ +export class Hooks { + /** Swup instance this registry belongs to */ + protected swup: Swup; + + /** Map of all registered hook handlers. */ + protected registry: HookRegistry = new Map(); + + // Can we deduplicate this somehow? Or make it error when not in sync with HookDefinitions? + // https://stackoverflow.com/questions/53387838/how-to-ensure-an-arrays-values-the-keys-of-a-typescript-interface/53395649 + protected readonly hooks: HookName[] = [ + 'animation:out:start', + 'animation:out:await', + 'animation:out:end', + 'animation:in:start', + 'animation:in:await', + 'animation:in:end', + 'animation:skip', + 'cache:clear', + 'cache:set', + 'content:replace', + 'content:scroll', + 'enable', + 'disable', + 'fetch:request', + 'fetch:error', + 'fetch:timeout', + 'history:popstate', + 'link:click', + 'link:self', + 'link:anchor', + 'link:newtab', + 'page:load', + 'page:view', + 'scroll:top', + 'scroll:anchor', + 'visit:start', + 'visit:transition', + 'visit:abort', + 'visit:end' + ]; + + constructor(swup: Swup) { + this.swup = swup; + this.init(); + } + + /** + * Create ledgers for all core hooks. + */ + protected init() { + this.hooks.forEach((hook) => this.create(hook)); + } + + /** + * Create a new hook type. + */ + create(hook: string) { + if (!this.registry.has(hook as HookName)) { + this.registry.set(hook as HookName, new Map()); + } + } + + /** + * Check if a hook type exists. + */ + exists(hook: HookName): boolean { + return this.registry.has(hook); + } + + /** + * Get the ledger with all registrations for a hook. + */ + protected get<T extends HookName>(hook: T): HookLedger<T> | undefined { + const ledger = this.registry.get(hook); + if (ledger) { + return ledger; + } + console.error(`Unknown hook '${hook}'`); + } + + /** + * Remove all handlers of all hooks. + */ + clear() { + this.registry.forEach((ledger) => ledger.clear()); + } + + /** + * Register a new hook handler. + * @param hook Name of the hook to listen for + * @param handler The handler function to execute + * @param options Object to specify how and when the handler is executed + * Available options: + * - `once`: Only execute the handler once + * - `before`: Execute the handler before the default handler + * - `priority`: Specify the order in which the handlers are executed + * - `replace`: Replace the default handler with this handler + * @returns A function to unregister the handler + */ + + // Overload: replacing default handler + on<T extends HookName, O extends HookOptions>(hook: T, handler: HookDefaultHandler<T>, options: O & { replace: true }): HookUnregister; // prettier-ignore + // Overload: passed in handler options + on<T extends HookName, O extends HookOptions>(hook: T, handler: HookHandler<T>, options: O): HookUnregister; // prettier-ignore + // Overload: no handler options + on<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister; // prettier-ignore + // Implementation + on<T extends HookName, O extends HookOptions>( + hook: T, + handler: O['replace'] extends true ? HookDefaultHandler<T> : HookHandler<T>, + options: Partial<O> = {} + ): HookUnregister { + const ledger = this.get(hook); + if (!ledger) { + console.warn(`Hook '${hook}' not found.`); + return () => {}; + } + + const id = ledger.size + 1; + const registration: HookRegistration<T> = { ...options, id, hook, handler }; + ledger.set(handler, registration); + + return () => this.off(hook, handler); + } + + /** + * Register a new hook handler to run before the default handler. + * Shortcut for `hooks.on(hook, handler, { before: true })`. + * @param hook Name of the hook to listen for + * @param handler The handler function to execute + * @param options Any other event options (see `hooks.on()` for details) + * @returns A function to unregister the handler + * @see on + */ + // Overload: passed in handler options + before<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore + // Overload: no handler options + before<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister; + // Implementation + before<T extends HookName>( + hook: T, + handler: HookHandler<T>, + options: HookOptions = {} + ): HookUnregister { + return this.on(hook, handler, { ...options, before: true }); + } + + /** + * Register a new hook handler to replace the default handler. + * Shortcut for `hooks.on(hook, handler, { replace: true })`. + * @param hook Name of the hook to listen for + * @param handler The handler function to execute instead of the default handler + * @param options Any other event options (see `hooks.on()` for details) + * @returns A function to unregister the handler + * @see on + */ + // Overload: passed in handler options + replace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore + // Overload: no handler options + replace<T extends HookName>(hook: T, handler: HookDefaultHandler<T>): HookUnregister; // prettier-ignore + // Implementation + replace<T extends HookName>( + hook: T, + handler: HookDefaultHandler<T>, + options: HookOptions = {} + ): HookUnregister { + return this.on(hook, handler, { ...options, replace: true }); + } + + /** + * Register a new hook handler to run once. + * Shortcut for `hooks.on(hook, handler, { once: true })`. + * @param hook Name of the hook to listen for + * @param handler The handler function to execute + * @param options Any other event options (see `hooks.on()` for details) + * @see on + */ + // Overload: passed in handler options + once<T extends HookName>(hook: T, handler: HookHandler<T>, options: HookOptions): HookUnregister; // prettier-ignore + // Overload: no handler options + once<T extends HookName>(hook: T, handler: HookHandler<T>): HookUnregister; + // Implementation + once<T extends HookName>( + hook: T, + handler: HookHandler<T>, + options: HookOptions = {} + ): HookUnregister { + return this.on(hook, handler, { ...options, once: true }); + } + + /** + * Unregister a hook handler. + * @param hook Name of the hook the handler is registered for + * @param handler The handler function that was registered. + * If omitted, all handlers for the hook will be removed. + */ + // Overload: unregister a specific handler + off<T extends HookName>(hook: T, handler: HookHandler<T> | HookDefaultHandler<T>): void; + // Overload: unregister all handlers + off<T extends HookName>(hook: T): void; + // Implementation + off<T extends HookName>(hook: T, handler?: HookHandler<T> | HookDefaultHandler<T>): void { + const ledger = this.get(hook); + if (ledger && handler) { + const deleted = ledger.delete(handler); + if (!deleted) { + console.warn(`Handler for hook '${hook}' not found.`); + } + } else if (ledger) { + ledger.clear(); + } + } + + /** + * Trigger a hook asynchronously, executing its default handler and all registered handlers. + * Will execute all handlers in order and `await` any `Promise`s they return. + * @param hook Name of the hook to trigger + * @param visit The visit object this hook belongs to + * @param args Arguments to pass to the handler + * @param defaultHandler A default implementation of this hook to execute + * @returns The resolved return value of the executed default handler + */ + // Overload: default order of arguments + async call<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore + // Overload: legacy order of arguments, with visit missing + async call<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>>; // prettier-ignore + // Implementation + async call<T extends HookName>( + hook: T, + arg1: Visit | HookArguments<T>, + arg2: HookArguments<T> | HookDefaultHandler<T>, + arg3?: HookDefaultHandler<T> + ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>> { + const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3); + + const { before, handler, after } = this.getHandlers(hook, defaultHandler); + await this.run(before, visit, args); + const [result] = await this.run(handler, visit, args, true); + await this.run(after, visit, args); + this.dispatchDomEvent(hook, visit, args); + return result; + } + + /** + * Trigger a hook synchronously, executing its default handler and all registered handlers. + * Will execute all handlers in order, but will **not** `await` any `Promise`s they return. + * @param hook Name of the hook to trigger + * @param visit The visit object this hook belongs to + * @param args Arguments to pass to the handler + * @param defaultHandler A default implementation of this hook to execute + * @returns The (possibly unresolved) return value of the executed default handler + */ + // Overload: default order of arguments + callSync<T extends HookName>(hook: T, visit: Visit | undefined, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore + // Overload: legacy order of arguments, with visit missing + callSync<T extends HookName>(hook: T, args: HookArguments<T>, defaultHandler?: HookDefaultHandler<T>): ReturnType<HookDefaultHandler<T>>; // prettier-ignore + // Implementation + callSync<T extends HookName>( + hook: T, + arg1: Visit | HookArguments<T>, + arg2: HookArguments<T> | HookDefaultHandler<T>, + arg3?: HookDefaultHandler<T> + ): ReturnType<HookDefaultHandler<T>> { + const [visit, args, defaultHandler] = this.parseCallArgs(hook, arg1, arg2, arg3); + const { before, handler, after } = this.getHandlers(hook, defaultHandler); + this.runSync(before, visit, args); + const [result] = this.runSync(handler, visit, args, true); + this.runSync(after, visit, args); + this.dispatchDomEvent(hook, visit, args); + return result; + } + + /** + * Parse the call arguments for call() and callSync() to allow legacy argument order. + */ + protected parseCallArgs<T extends HookName>( + hook: T, + arg1: Visit | HookArguments<T> | undefined, + arg2: HookArguments<T> | HookDefaultHandler<T>, + arg3?: HookDefaultHandler<T> + ): [Visit | undefined, HookArguments<T>, HookDefaultHandler<T> | undefined] { + const isLegacyOrder = + !(arg1 instanceof Visit) && (typeof arg1 === 'object' || typeof arg2 === 'function'); + if (isLegacyOrder) { + // Legacy positioning: arguments in second or handler passed in third place + return [undefined, arg1 as HookArguments<T>, arg2 as HookDefaultHandler<T>]; + } else { + // Default positioning: visit passed in as first argument + return [arg1, arg2 as HookArguments<T>, arg3]; + } + } + + /** + * Execute the handlers for a hook, in order, as `Promise`s that will be `await`ed. + * @param registrations The registrations (handler + options) to execute + * @param args Arguments to pass to the handler + */ + + // Overload: running HookDefaultHandler: expect HookDefaultHandler return type + protected async run<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): Promise<Awaited<ReturnType<HookDefaultHandler<T>>>[]>; // prettier-ignore + // Overload: running user handler: expect no specific type + protected async run<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): Promise<unknown[]>; // prettier-ignore + // Implementation + protected async run<T extends HookName, R extends HookRegistration<T>[]>( + registrations: R, + visit: Visit | undefined = this.swup.visit, + args: HookArguments<T>, + rethrow: boolean = false + ): Promise<Awaited<ReturnType<HookDefaultHandler<T>>> | unknown[]> { + const results = []; + for (const { hook, handler, defaultHandler, once } of registrations) { + if (visit?.done) continue; + if (once) this.off(hook, handler); + try { + const result = await runAsPromise(handler, [visit, args, defaultHandler]); + results.push(result); + } catch (error) { + if (rethrow) { + throw error; + } else { + console.error(`Error in hook '${hook}':`, error); + } + } + } + return results; + } + + /** + * Execute the handlers for a hook, in order, without `await`ing any returned `Promise`s. + * @param registrations The registrations (handler + options) to execute + * @param args Arguments to pass to the handler + */ + + // Overload: running HookDefaultHandler: expect HookDefaultHandler return type + protected runSync<T extends HookName>(registrations: HookRegistration<T, HookDefaultHandler<T>>[], visit: Visit | undefined, args: HookArguments<T>, rethrow: true): ReturnType<HookDefaultHandler<T>>[]; // prettier-ignore + // Overload: running user handler: expect no specific type + protected runSync<T extends HookName>(registrations: HookRegistration<T>[], visit: Visit | undefined, args: HookArguments<T>): unknown[]; // prettier-ignore + // Implementation + protected runSync<T extends HookName, R extends HookRegistration<T>[]>( + registrations: R, + visit: Visit | undefined = this.swup.visit, + args: HookArguments<T>, + rethrow: boolean = false + ): (ReturnType<HookDefaultHandler<T>> | unknown)[] { + const results = []; + for (const { hook, handler, defaultHandler, once } of registrations) { + if (visit?.done) continue; + if (once) this.off(hook, handler); + try { + const result = (handler as HookDefaultHandler<T>)(visit, args, defaultHandler); + results.push(result); + if (isPromise(result)) { + console.warn( + `Swup will not await Promises in handler for synchronous hook '${hook}'.` + ); + } + } catch (error) { + if (rethrow) { + throw error; + } else { + console.error(`Error in hook '${hook}':`, error); + } + } + } + return results; + } + + /** + * Get all registered handlers for a hook, sorted by priority and registration order. + * @param hook Name of the hook + * @param defaultHandler The optional default handler of this hook + * @returns An object with the handlers sorted into `before` and `after` arrays, + * as well as a flag indicating if the original handler was replaced + */ + protected getHandlers<T extends HookName>(hook: T, defaultHandler?: HookDefaultHandler<T>) { + const ledger = this.get(hook); + if (!ledger) { + return { found: false, before: [], handler: [], after: [], replaced: false }; + } + + const registrations = Array.from(ledger.values()); + + // Let TypeScript know that replaced handlers are default handlers by filtering to true + const def = (T: HookRegistration<T>): T is HookRegistration<T, HookDefaultHandler<T>> => true; // prettier-ignore + const sort = this.sortRegistrations; + + // Filter into before, after, and replace handlers + const before = registrations.filter(({ before, replace }) => before && !replace).sort(sort); + const replace = registrations.filter(({ replace }) => replace).filter(def).sort(sort); // prettier-ignore + const after = registrations.filter(({ before, replace }) => !before && !replace).sort(sort); + const replaced = replace.length > 0; + + // Define main handler registration + // Created as HookRegistration[] array to allow passing it into hooks.run() directly + let handler: HookRegistration<T, HookDefaultHandler<T>>[] = []; + if (defaultHandler) { + handler = [{ id: 0, hook, handler: defaultHandler }]; + if (replaced) { + const index = replace.length - 1; + const replacingHandler = replace[index].handler; + const createDefaultHandler = (index: number): HookDefaultHandler<T> | undefined => { + const next = replace[index - 1]; + if (next) { + return (visit, args) => + next.handler(visit, args, createDefaultHandler(index - 1)); + } else { + return defaultHandler; + } + }; + const nestedDefaultHandler = createDefaultHandler(index); + handler = [ + { id: 0, hook, handler: replacingHandler, defaultHandler: nestedDefaultHandler } + ]; + } + } + + return { found: true, before, handler, after, replaced }; + } + + /** + * Sort two hook registrations by priority and registration order. + * @param a The registration object to compare + * @param b The other registration object to compare with + * @returns The sort direction + */ + protected sortRegistrations<T extends HookName>( + a: HookRegistration<T>, + b: HookRegistration<T> + ): number { + const priority = (a.priority ?? 0) - (b.priority ?? 0); + const id = a.id - b.id; + return priority || id || 0; + } + + /** + * Dispatch a custom event on the `document` for a hook. Prefixed with `swup:` + * @param hook Name of the hook. + */ + protected dispatchDomEvent<T extends HookName>( + hook: T, + visit: Visit | undefined, + args?: HookArguments<T> + ): void { + if (visit?.done) return; + + const detail: HookEventDetail = { hook, args, visit: visit || this.swup.visit }; + document.dispatchEvent( + new CustomEvent<HookEventDetail>(`swup:any`, { detail, bubbles: true }) + ); + document.dispatchEvent( + new CustomEvent<HookEventDetail>(`swup:${hook}`, { detail, bubbles: true }) + ); + } +} |
