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 diff --git a/README.md b/README.md index 0f48332..52b0507 100755 --- a/README.md +++ b/README.md @@ -237,36 +237,27 @@ const swup = new Swup({ }); /** - * 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 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..c54e6e5 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,13 @@ 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); + const maxTop = scrollingElement.scrollHeight - scrollingElement.clientHeight; + + this.swup.scrollTo?.(Math.min(top, maxTop), animate, scrollingElement); return true; } @@ -414,4 +429,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): HTMLElement { + 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 HTMLElement + ? document.scrollingElement + : document.documentElement; + } }