Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ wiki-images files
wiki-wishlist
*.sublime-project
*.sublime-workspace
.editorconfig
.idea
dist
/plugins
49 changes: 20 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
};

```
8 changes: 1 addition & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
105 changes: 76 additions & 29 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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]`;
Expand All @@ -33,7 +29,7 @@ type ScrollPositionsCache = Record<string, ScrollPositionsCacheEntry>;

declare module 'swup' {
export interface Swup {
scrollTo?: (offset: number, animate?: boolean) => void;
scrollTo?: (offset: number, animate?: boolean, scrollingElement?: Element) => void;
}

export interface VisitScroll {
Expand All @@ -57,17 +53,13 @@ export default class SwupScrollPlugin extends Plugin {

requires = { swup: '>=4.2.0' };

scrl: any;

defaults: Options = {
doScrollingRightAway: false,
animateScroll: {
betweenPages: true,
samePageWithHash: true,
samePage: true
},
scrollFriction: 0.3,
scrollAcceleration: 0.04,
getAnchorElement: undefined,
offset: 0,
scrollContainers: `[data-swup-scroll-container]`,
Expand Down Expand Up @@ -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'
});
};

/**
Expand Down Expand Up @@ -169,7 +181,6 @@ export default class SwupScrollPlugin extends Plugin {

this.cachedScrollPositions = {};
delete this.swup.scrollTo;
delete this.scrl;
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
}