From 8b472a1ad14123a38c68c5e461c0a1b65fe35222 Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Sat, 31 May 2025 18:24:30 +0200 Subject: [PATCH 1/6] Replace scrl with a native scroll implementation - support scrolling the closest scrolling element from an anchor element - keep the hooks - keep the `autoKill` feature (cancel scroll upon user interaction) --- README.md | 6 --- package-lock.json | 8 +--- package.json | 3 +- src/index.ts | 103 +++++++++++++++++++++++++++++++++------------- 4 files changed, 76 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0f48332..3a90c7a 100755 --- a/README.md +++ b/README.md @@ -98,10 +98,6 @@ For finer control, you can pass an object: } ``` -### scrollFriction and scrollAcceleration - -The animation behavior of the scroll animation can be adjusted by setting `scrollFriction` and `scrollAcceleration`. - ### getAnchorElement Customize how the scroll target is found on the page. Defaults to standard browser behavior (`#id` first, `a[name]` second). @@ -190,8 +186,6 @@ new SwupScrollPlugin({ samePageWithHash: true, samePage: true }, - scrollFriction: 0.3, - scrollAcceleration: 0.04, getAnchorElement: null, markScrollTarget: false, offset: 0, diff --git a/package-lock.json b/package-lock.json index fc0457f..e95ada9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "3.3.2", "license": "MIT", "dependencies": { - "@swup/plugin": "^4.0.0", - "scrl": "^2.0.0" + "@swup/plugin": "^4.0.0" }, "devDependencies": { "@swup/cli": "^5.0.1" @@ -7747,11 +7746,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/scrl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/scrl/-/scrl-2.0.0.tgz", - "integrity": "sha512-BbbVXxrOn58Ge4wjOORIRVZamssQu08ISLL/AC2z9aATIsKqZLESwZVW5YR0Yz0C7qqDRHb4yNXJlQ8yW0SGHw==" - }, "node_modules/seenreq": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/seenreq/-/seenreq-3.0.0.tgz", diff --git a/package.json b/package.json index 475462e..317d58d 100755 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ "url": "https://github.com/swup/scroll-plugin.git" }, "dependencies": { - "@swup/plugin": "^4.0.0", - "scrl": "^2.0.0" + "@swup/plugin": "^4.0.0" }, "devDependencies": { "@swup/cli": "^5.0.1" diff --git a/src/index.ts b/src/index.ts index bd646d7..e3c590e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ import Plugin from '@swup/plugin'; import { Handler, Visit, queryAll } from 'swup'; -// @ts-expect-error -import Scrl from 'scrl'; export type Options = { doScrollingRightAway: boolean; @@ -10,8 +8,6 @@ export type Options = { samePageWithHash: boolean; samePage: boolean; }; - scrollFriction: number; - scrollAcceleration: number; getAnchorElement?: (hash: string) => Element | null; offset: number | ((el: Element) => number); scrollContainers: `[data-swup-scroll-container]`; @@ -33,7 +29,7 @@ type ScrollPositionsCache = Record; declare module 'swup' { export interface Swup { - scrollTo?: (offset: number, animate?: boolean) => void; + scrollTo?: (offset: number, animate?: boolean, scrollingElement?: Element) => void; } export interface VisitScroll { @@ -57,8 +53,6 @@ export default class SwupScrollPlugin extends Plugin { requires = { swup: '>=4.2.0' }; - scrl: any; - defaults: Options = { doScrollingRightAway: false, animateScroll: { @@ -66,8 +60,6 @@ export default class SwupScrollPlugin extends Plugin { samePageWithHash: true, samePage: true }, - scrollFriction: 0.3, - scrollAcceleration: 0.04, getAnchorElement: undefined, offset: 0, scrollContainers: `[data-swup-scroll-container]`, @@ -95,24 +87,44 @@ export default class SwupScrollPlugin extends Plugin { // @ts-expect-error: createVisit is currently private, need to make this semi-public somehow const visit = this.swup.createVisit({ to: this.swup.currentPageUrl }); - // Initialize Scrl lib for smooth animations - this.scrl = new Scrl({ - onStart: () => swup.hooks.callSync('scroll:start', visit, undefined), - onEnd: () => swup.hooks.callSync('scroll:end', visit, undefined), - onCancel: () => swup.hooks.callSync('scroll:end', visit, undefined), - friction: this.options.scrollFriction, - acceleration: this.options.scrollAcceleration - }); - // Add scrollTo method to swup and animate based on current animateScroll option - swup.scrollTo = (offset, animate = true) => { - if (animate) { - this.scrl.scrollTo(offset); - } else { - swup.hooks.callSync('scroll:start', visit, undefined); - window.scrollTo(0, offset); - swup.hooks.callSync('scroll:end', visit, undefined); - } + swup.scrollTo = (offset: number, animate = true, element?: Element) => { + element ??= this.getRootScrollingElement(); + + const eventTarget = element instanceof HTMLHtmlElement ? window : element; + + /** + * Dispatch the scroll:end hook upon completion + */ + eventTarget.addEventListener( + 'scrollend', + () => swup.hooks.callSync('scroll:end', visit, undefined), + { once: true } + ); + + /** + * Make the scroll cancelable upon user interaction + */ + eventTarget.addEventListener( + 'wheel', + () => { + element.scrollTo({ + top: element.scrollTop, + behavior: 'instant' + }); + }, + { once: true } + ); + + /** + * Dispatch the scroll:start hook + */ + swup.hooks.callSync('scroll:start', visit, undefined); + + element.scrollTo({ + top: offset, + behavior: animate ? 'smooth' : 'instant' + }); }; /** @@ -169,7 +181,6 @@ export default class SwupScrollPlugin extends Plugin { this.cachedScrollPositions = {}; delete this.swup.scrollTo; - delete this.scrl; } /** @@ -254,9 +265,11 @@ export default class SwupScrollPlugin extends Plugin { return false; } + const scrollingElement = this.getClosestScrollingElement(element); + const { top: elementTop } = element.getBoundingClientRect(); - const top = elementTop + window.scrollY - this.getOffset(element); - this.swup.scrollTo?.(top, animate); + const top = elementTop + scrollingElement.scrollTop - this.getOffset(element); + this.swup.scrollTo?.(top, animate, scrollingElement); return true; } @@ -414,4 +427,36 @@ export default class SwupScrollPlugin extends Plugin { currentTarget?.removeAttribute('data-swup-scroll-target'); newTarget?.setAttribute('data-swup-scroll-target', ''); } + + /** + * Get the closest parent of an element that can be scrolled. + * Fall back to the Window if not found. + */ + getClosestScrollingElement(element: Element): Element { + let parent: HTMLElement | null = element.parentElement; + + while (parent) { + const { overflowY } = getComputedStyle(parent); + const isScrollable = + ['auto', 'scroll'].includes(overflowY) && parent.scrollHeight > parent.clientHeight; + + if (isScrollable) { + return parent; + } + + parent = parent.parentElement; + } + + // Fallback: return the root scrolling element + return this.getRootScrollingElement(); + } + + /** + * Get the root scrolling element + */ + getRootScrollingElement() { + return document.scrollingElement instanceof Element + ? document.scrollingElement + : document.documentElement; + } } From 4ef72128e4a19d2a8af26fcd42b64a03e9734faa Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Sat, 31 May 2025 18:40:54 +0200 Subject: [PATCH 2/6] Update Documentation --- README.md | 79 ++++++++++++++++++++++++------------------------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 3a90c7a..344668a 100755 --- a/README.md +++ b/README.md @@ -86,15 +86,17 @@ For finer control, you can pass an object: ```javascript // Using a simple boolean... { - animateScroll: !window.matchMedia('(prefers-reduced-motion: reduce)').matches + animateScroll: !window.matchMedia('(prefers-reduced-motion: reduce)').matches; } // ...or this little monster, with full control over everything: { - animateScroll: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? false : { - betweenPages: true, - samePageWithHash: true, - samePage: true - } + animateScroll: window.matchMedia('(prefers-reduced-motion: reduce)').matches + ? false + : { + betweenPages: true, + samePageWithHash: true, + samePage: true + }; } ``` @@ -106,9 +108,9 @@ Customize how the scroll target is found on the page. Defaults to standard brows { // Use a custom data attribute instead of id getAnchorElement: (hash) => { - hash = hash.replace('#', '') - return document.querySelector(`[data-scroll-target="${hash}"]`) - } + hash = hash.replace('#', ''); + return document.querySelector(`[data-scroll-target="${hash}"]`); + }; } ``` @@ -159,7 +161,7 @@ overflowing containers. ```js { // Always restore the scroll position of overflowing tables and sidebars - scrollContainers: '.overflowing-table, .overflowing-sidebar' + scrollContainers: '.overflowing-table, .overflowing-sidebar'; } ``` @@ -172,7 +174,7 @@ recorded for that page. See [Reset vs. restore](#reset-vs-restore) for an explan ```js { // Don't scroll back up for custom back-links, mimicking the browser back button - shouldResetScrollPosition: (link) => !link.matches('.backlink') + shouldResetScrollPosition: (link) => !link.matches('.backlink'); } ``` @@ -218,7 +220,6 @@ swup.hooks.on('scroll:end', () => console.log('Swup finished scrolling')); You can overwrite the scroll function with your own implementation. This way, you can gain full control over how you animate your scroll positions. Here's an example using [GSAP's](https://greensock.com/docs/v3/) [ScrollToPlugin](https://greensock.com/docs/v3/Plugins/ScrollToPlugin): ```js - import Swup from 'swup'; import SwupScrollPlugin from '@swup/scroll-plugin'; @@ -227,40 +228,30 @@ import ScrollToPlugin from 'gsap/ScrollToPlugin'; gsap.registerPlugin(ScrollToPlugin); const swup = new Swup({ - plugins: [new SwupScrollPlugin()] + plugins: [new SwupScrollPlugin()] }); /** - * Overwrite swup's scrollTo function + * Use GSAP ScrollToPlugin for animated scrolling + * @see https://greensock.com/docs/v3/Plugins/ScrollToPlugin */ -swup.scrollTo = (offsetY, animate = true) => { - if (!animate) { - swup.hooks.callSync('scroll:start', undefined); - window.scrollTo(0, offsetY); - swup.hooks.callSync('scroll:end', undefined); - return; - } - - /** - * Use GSAP ScrollToPlugin for animated scrolling - * @see https://greensock.com/docs/v3/Plugins/ScrollToPlugin - */ - gsap.to(window, { - duration: 0.8, - scrollTo: offsetY, - ease: 'power4.inOut', - autoKill: true, - onStart: () => { - swup.hooks.callSync('scroll:start', undefined); - }, - onComplete: () => { - swup.hooks.callSync('scroll:end', undefined); - }, - onAutoKill: () => { - swup.hooks.callSync('scroll:end', undefined); - }, - }); - +swup.scrollTo = (offset, animate, scrollingElement) => { + gsap.to(scrollingElement ?? window, { + duration: animate ? 0.6 : 0, + ease: 'power4.out', + scrollTo: { + y: offset, + autoKill: !isTouch(), + onAutoKill: () => { + swup.hooks.callSync('scroll:end', undefined); + } + }, + onStart: () => { + swup.hooks.callSync('scroll:start', undefined); + }, + onComplete: () => { + swup.hooks.callSync('scroll:end', undefined); + } + }); }; - -``` \ No newline at end of file +``` From b37a79336d771063105240b8192ab667b4dda292 Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Tue, 3 Jun 2025 17:35:51 +0200 Subject: [PATCH 3/6] Constrain scrolling to the maximum available scroll height --- src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index e3c590e..49446b8 100755 --- a/src/index.ts +++ b/src/index.ts @@ -269,7 +269,9 @@ export default class SwupScrollPlugin extends Plugin { const { top: elementTop } = element.getBoundingClientRect(); const top = elementTop + scrollingElement.scrollTop - this.getOffset(element); - this.swup.scrollTo?.(top, animate, scrollingElement); + const maxTop = scrollingElement.scrollHeight - scrollingElement.offsetHeight; + + this.swup.scrollTo?.(Math.min(top, maxTop), animate, scrollingElement); return true; } @@ -432,7 +434,7 @@ export default class SwupScrollPlugin extends Plugin { * Get the closest parent of an element that can be scrolled. * Fall back to the Window if not found. */ - getClosestScrollingElement(element: Element): Element { + getClosestScrollingElement(element: Element): HTMLElement { let parent: HTMLElement | null = element.parentElement; while (parent) { @@ -455,7 +457,7 @@ export default class SwupScrollPlugin extends Plugin { * Get the root scrolling element */ getRootScrollingElement() { - return document.scrollingElement instanceof Element + return document.scrollingElement instanceof HTMLElement ? document.scrollingElement : document.documentElement; } From 7254062f5f46017db121c7201da031c6f4be1346 Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Thu, 5 Jun 2025 12:34:57 +0200 Subject: [PATCH 4/6] Use `clientHeight` instead of `offsetHeight` while determining the scrollingElement's height --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 49446b8..c54e6e5 100755 --- a/src/index.ts +++ b/src/index.ts @@ -269,7 +269,7 @@ export default class SwupScrollPlugin extends Plugin { const { top: elementTop } = element.getBoundingClientRect(); const top = elementTop + scrollingElement.scrollTop - this.getOffset(element); - const maxTop = scrollingElement.scrollHeight - scrollingElement.offsetHeight; + const maxTop = scrollingElement.scrollHeight - scrollingElement.clientHeight; this.swup.scrollTo?.(Math.min(top, maxTop), animate, scrollingElement); From 8cfe02ef86bcdd9d78be5b964f750f60bf5ef000 Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Fri, 6 Jun 2025 22:15:21 +0200 Subject: [PATCH 5/6] Revert linting on README.md --- README.md | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 344668a..52b0507 100755 --- a/README.md +++ b/README.md @@ -86,20 +86,22 @@ For finer control, you can pass an object: ```javascript // Using a simple boolean... { - animateScroll: !window.matchMedia('(prefers-reduced-motion: reduce)').matches; + animateScroll: !window.matchMedia('(prefers-reduced-motion: reduce)').matches } // ...or this little monster, with full control over everything: { - animateScroll: window.matchMedia('(prefers-reduced-motion: reduce)').matches - ? false - : { - betweenPages: true, - samePageWithHash: true, - samePage: true - }; + animateScroll: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? false : { + betweenPages: true, + samePageWithHash: true, + samePage: true + } } ``` +### scrollFriction and scrollAcceleration + +The animation behavior of the scroll animation can be adjusted by setting `scrollFriction` and `scrollAcceleration`. + ### getAnchorElement Customize how the scroll target is found on the page. Defaults to standard browser behavior (`#id` first, `a[name]` second). @@ -108,9 +110,9 @@ Customize how the scroll target is found on the page. Defaults to standard brows { // Use a custom data attribute instead of id getAnchorElement: (hash) => { - hash = hash.replace('#', ''); - return document.querySelector(`[data-scroll-target="${hash}"]`); - }; + hash = hash.replace('#', '') + return document.querySelector(`[data-scroll-target="${hash}"]`) + } } ``` @@ -161,7 +163,7 @@ overflowing containers. ```js { // Always restore the scroll position of overflowing tables and sidebars - scrollContainers: '.overflowing-table, .overflowing-sidebar'; + scrollContainers: '.overflowing-table, .overflowing-sidebar' } ``` @@ -174,7 +176,7 @@ recorded for that page. See [Reset vs. restore](#reset-vs-restore) for an explan ```js { // Don't scroll back up for custom back-links, mimicking the browser back button - shouldResetScrollPosition: (link) => !link.matches('.backlink'); + shouldResetScrollPosition: (link) => !link.matches('.backlink') } ``` @@ -188,6 +190,8 @@ new SwupScrollPlugin({ samePageWithHash: true, samePage: true }, + scrollFriction: 0.3, + scrollAcceleration: 0.04, getAnchorElement: null, markScrollTarget: false, offset: 0, @@ -220,6 +224,7 @@ swup.hooks.on('scroll:end', () => console.log('Swup finished scrolling')); You can overwrite the scroll function with your own implementation. This way, you can gain full control over how you animate your scroll positions. Here's an example using [GSAP's](https://greensock.com/docs/v3/) [ScrollToPlugin](https://greensock.com/docs/v3/Plugins/ScrollToPlugin): ```js + import Swup from 'swup'; import SwupScrollPlugin from '@swup/scroll-plugin'; @@ -228,7 +233,7 @@ import ScrollToPlugin from 'gsap/ScrollToPlugin'; gsap.registerPlugin(ScrollToPlugin); const swup = new Swup({ - plugins: [new SwupScrollPlugin()] + plugins: [new SwupScrollPlugin()] }); /** @@ -254,4 +259,5 @@ swup.scrollTo = (offset, animate, scrollingElement) => { } }); }; -``` + +``` \ No newline at end of file From 83f674cf6ed21b7655df2b4cad00408516b76ccb Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Fri, 6 Jun 2025 22:16:59 +0200 Subject: [PATCH 6/6] Add swup's `.editorconfig` --- .editorconfig | 19 +++++++++++++++++++ .gitignore | 1 - 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..0c0cac3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 100 + +[*.{js,mjs,ts}] +indent_style = tab +indent_size = 4 + +[*.{json,md,yaml,yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 5575814..71872f6 100755 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ wiki-images files wiki-wishlist *.sublime-project *.sublime-workspace -.editorconfig .idea dist /plugins