diff --git a/README.md b/README.md index 150d0d6..1f818a2 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ export type Rule = { to: Path; containers: string[]; name?: string; - scroll?: boolean | string; + scroll?: ScrollPredicate | boolean | string; focus?: boolean | string; - if?: Predicate; + if?: IfPredicate; }; /** The plugin options */ @@ -222,11 +222,12 @@ Optional, Type: `string` – A name for this rule to allow scoped styling, ideal #### `rule.scroll` -Optional, Type: `boolean | string` – By default, scrolling will be disabled for fragment visits. +Optional, Type: `ScrollPredicate | boolean | string` – By default, scrolling will be disabled for fragment visits. Using this option, you can re-enable it for selected visits: - `true` will scroll to the top - `'#my-element'` will scroll to the first element matching the selector +- A callback function receiving the current [visit](https://swup.js.org/visit/) object, returning a `boolean` or a `string` selector #### `rule.focus` diff --git a/src/inc/ParsedRule.ts b/src/inc/ParsedRule.ts index 28c8cc2..a7e4987 100644 --- a/src/inc/ParsedRule.ts +++ b/src/inc/ParsedRule.ts @@ -1,6 +1,6 @@ import { matchPath, classify, Location } from 'swup'; import type { Swup, Path, Visit } from 'swup'; -import type { Route, Rule, Predicate } from './defs.js'; +import type { Route, Rule, IfPredicate, ScrollPredicate } from './defs.js'; import { dedupe, queryFragmentElement } from './functions.js'; import Logger, { highlight } from './Logger.js'; import { __DEV__ } from './env.js'; @@ -22,10 +22,10 @@ export default class ParsedRule { to: Path; containers: string[]; name?: string; - scroll: boolean | string = false; + scroll: ScrollPredicate | boolean | string = false; focus?: boolean | string; logger?: Logger; - if: Predicate = () => true; + if: IfPredicate = () => true; constructor(options: Options) { this.swup = options.swup; diff --git a/src/inc/defs.ts b/src/inc/defs.ts index 1631107..143ecc8 100644 --- a/src/inc/defs.ts +++ b/src/inc/defs.ts @@ -15,7 +15,8 @@ export interface FragmentElement extends HTMLElement { }; } -export type Predicate = (visit: Visit) => boolean; +export type IfPredicate = (visit: Visit) => boolean; +export type ScrollPredicate = (visit: Visit) => boolean | string; /** A fragment rule */ export type Rule = { @@ -23,9 +24,9 @@ export type Rule = { to: Path; containers: string[]; name?: string; - scroll?: boolean | string; + scroll?: ScrollPredicate | boolean | string; focus?: boolean | string; - if?: Predicate; + if?: IfPredicate; }; /** The plugin options */ @@ -38,6 +39,6 @@ export type Options = { export type FragmentVisit = { name?: string; containers: string[]; - scroll: boolean | string; + scroll: ScrollPredicate | boolean | string; focus?: boolean | string; }; diff --git a/src/inc/functions.ts b/src/inc/functions.ts index d77d103..20310e0 100644 --- a/src/inc/functions.ts +++ b/src/inc/functions.ts @@ -388,13 +388,22 @@ export function dedupe(arr: Array): Array { /** * Adjusts visit.scroll based on given fragment visit */ -export function adjustVisitScroll(fragmentVisit: FragmentVisit, scroll: VisitScroll): VisitScroll { - if (typeof fragmentVisit.scroll === 'boolean') { - return { ...scroll, reset: fragmentVisit.scroll }; +export function adjustVisitScroll(fragmentVisit: FragmentVisit, visit: Visit): VisitScroll { + const scroll = visit.scroll; + let scrollValue = fragmentVisit.scroll; + + if (typeof scrollValue === 'function') { + scrollValue = scrollValue(visit); } - if (typeof fragmentVisit.scroll === 'string' && !scroll.target) { - return { ...scroll, target: fragmentVisit.scroll }; + + if (typeof scrollValue === 'boolean') { + return { ...scroll, reset: scrollValue }; } + + if (typeof scrollValue === 'string' && !scroll.target) { + return { ...scroll, target: scrollValue }; + } + return scroll; } diff --git a/src/inc/handlers.ts b/src/inc/handlers.ts index f7c1c19..97304ae 100644 --- a/src/inc/handlers.ts +++ b/src/inc/handlers.ts @@ -48,7 +48,7 @@ export const onVisitStart: Handler<'visit:start'> = async function (this: Fragme this.logger?.log(`fragment visit: ${highlight(visit.fragmentVisit.containers.join(', '))}`); } - visit.scroll = adjustVisitScroll(fragmentVisit, visit.scroll); + visit.scroll = adjustVisitScroll(fragmentVisit, visit); // Fragment Plugin can't know if Accesssibilty Plugin is installed // @ts-expect-error diff --git a/tests/vitest/adjustVisitScroll.test.ts b/tests/vitest/adjustVisitScroll.test.ts index 8d3cc22..c3cc75c 100644 --- a/tests/vitest/adjustVisitScroll.test.ts +++ b/tests/vitest/adjustVisitScroll.test.ts @@ -1,22 +1,68 @@ import { describe, expect, it } from 'vitest'; import { adjustVisitScroll, stubVisit } from '../../src/inc/functions.js'; -import Swup from 'swup'; describe('adjustVisitScroll()', () => { - it('adjust visit.scroll', () => { - const { scroll } = stubVisit({ to: '' }); + it('adjust visit.scroll with boolean values', () => { + const visit = stubVisit({ to: '' }); - expect(adjustVisitScroll({ containers: [], scroll: true }, scroll)).toEqual({ + expect(adjustVisitScroll({ containers: [], scroll: true }, visit)).toEqual({ reset: true }); - expect(adjustVisitScroll({ containers: [], scroll: false }, scroll)).toEqual({ + expect(adjustVisitScroll({ containers: [], scroll: false }, visit)).toEqual({ reset: false }); + }); + + it('adjust visit.scroll with string selector', () => { + const visit = stubVisit({ to: '' }); - expect(adjustVisitScroll({ containers: [], scroll: '#top' }, scroll)).toEqual({ + expect(adjustVisitScroll({ containers: [], scroll: '#top' }, visit)).toEqual({ reset: true, target: '#top' }); }); + + it('adjust visit.scroll with callback returning boolean', () => { + const visit = stubVisit({ to: '/page' }); + + // Callback returning true + expect(adjustVisitScroll({ containers: [], scroll: () => true }, visit)).toEqual({ + reset: true + }); + + // Callback returning false + expect(adjustVisitScroll({ containers: [], scroll: () => false }, visit)).toEqual({ + reset: false + }); + }); + + it('adjust visit.scroll with callback returning string selector', () => { + const visit = stubVisit({ to: '/page' }); + + expect(adjustVisitScroll({ containers: [], scroll: () => '#my-element' }, visit)).toEqual({ + reset: true, + target: '#my-element' + }); + }); + + it('receives Visit object in callback', () => { + const visit = stubVisit({ from: '/blog', to: '/blog/post-1' }); + let receivedVisit = visit; + + adjustVisitScroll( + { + containers: [], + scroll: (v) => { + receivedVisit = v; + return true; + } + }, + visit + ); + + expect(receivedVisit).toBe(visit); + expect(receivedVisit.to.url).toBe('/blog/post-1'); + expect(receivedVisit.from.url).toBe('/blog'); + }); });