Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c9b3733
Store scroll position in history state
daun Jul 8, 2024
d712426
Restore scroll position on history visits
daun Jul 8, 2024
9ee6bb0
Bind scroll store method
daun Jul 8, 2024
f1d585d
Merge branch 'master' into feat/scroll-restoration
daun Jul 16, 2024
29561ab
Merge branch 'master' into feat/scroll-restoration
daun Jul 22, 2024
918a661
Update debounce types
daun Jul 22, 2024
946c536
Merge branch 'master' into feat/scroll-restoration
daun Jul 24, 2024
2526077
Merge branch 'master' into feat/scroll-restoration
daun Jul 25, 2024
04d7adc
Fix scroll debounce interval
daun Jul 25, 2024
b0d72fd
Create temporary visit
daun Jul 25, 2024
af63e86
Add tests for scrolling and history
daun Jul 25, 2024
4c6b00c
Create history interfaces
daun Jul 26, 2024
68669ea
Refactor to scroll module
daun Jul 26, 2024
6114b99
Update hook arguments
daun Jul 26, 2024
fb4420a
Store history state in visit
daun Jul 26, 2024
78fd45a
Trigger scroll restoration in identical resolved urls
daun Jul 26, 2024
174c6cb
Store scroll position before non-popstate visits
daun Jul 26, 2024
29fffc5
Refactor popstate url handling
daun Jul 26, 2024
3c88189
Fix parameter order
daun Aug 12, 2024
b3896f6
Simplify scroll position tests
daun Aug 13, 2024
228e96a
Fix scroll position test
daun Aug 13, 2024
50c6bee
Fix rapid navigation tests
daun Aug 13, 2024
b715a85
Type local history scroll positions object
daun Aug 13, 2024
e536977
Remove log statement
daun Aug 13, 2024
c35d132
Simplify loop
daun Aug 13, 2024
6e34fcd
Simplify position storage
daun Aug 13, 2024
21bffc7
Merge branch 'master' into feat/scroll-restoration
daun Sep 24, 2024
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
47 changes: 28 additions & 19 deletions src/Swup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import { navigate, performNavigation, type NavigationToSelfAction } from './modu
import { fetchPage } from './modules/fetchPage.js';
import { animatePageOut } from './modules/animatePageOut.js';
import { replaceContent } from './modules/replaceContent.js';
import { scrollToContent } from './modules/scrollToContent.js';
import { storeScrollPosition, restoreScrollPosition, scrollToContent } from './modules/scroll.js';
import { animatePageIn } from './modules/animatePageIn.js';
import { renderPage } from './modules/renderPage.js';
import { use, unuse, findPlugin, type Plugin } from './modules/plugins.js';
import { isSameResolvedUrl, resolveUrl } from './modules/resolveUrl.js';
import { nextTick } from './utils.js';
import { nextTick, debounce } from './utils.js';
import { type HistoryState } from './helpers/history.js';

/** Options for customizing swup's behavior. */
Expand Down Expand Up @@ -139,6 +139,8 @@ export default class Swup {
protected animatePageIn = animatePageIn;
protected animatePageOut = animatePageOut;
protected scrollToContent = scrollToContent;
protected storeScrollPosition = storeScrollPosition;
protected restoreScrollPosition = restoreScrollPosition;
/** Find the anchor element for a given hash */
getAnchorElement = getAnchorElement;

Expand All @@ -155,6 +157,7 @@ export default class Swup {

this.handleLinkClick = this.handleLinkClick.bind(this);
this.handlePopState = this.handlePopState.bind(this);
this.storeScrollPosition = debounce(this.storeScrollPosition.bind(this), 100);

this.cache = new Cache(this);
this.classes = new Classes(this);
Expand All @@ -174,10 +177,10 @@ export default class Swup {

window.addEventListener('popstate', this.handlePopState);

// Set scroll restoration to manual if animating history visits
if (this.options.animateHistoryBrowsing) {
window.history.scrollRestoration = 'manual';
}
// Manage scroll position restoration
window.history.scrollRestoration = 'manual';
window.addEventListener('scroll', this.storeScrollPosition, { passive: true });
this.storeScrollPosition();

// Initial save to cache
if (this.options.cache) {
Expand Down Expand Up @@ -217,12 +220,15 @@ export default class Swup {

/** Disable this instance, removing listeners and classnames. */
async destroy() {
// remove delegated listener
// remove delegated click listener
this.clickDelegate!.destroy();

// remove popstate listener
window.removeEventListener('popstate', this.handlePopState);

// remove scroll listener
window.removeEventListener('scroll', this.storeScrollPosition);

// empty cache
this.cache.clear();

Expand Down Expand Up @@ -318,8 +324,9 @@ export default class Swup {
return;
}

// Exit early if the resolved path hasn't changed
// Exit if the resolved path hasn't changed
if (this.isSameResolvedUrl(url, from)) {
this.storeScrollPosition();
return;
}

Expand All @@ -329,27 +336,23 @@ export default class Swup {
}

protected handlePopState(event: PopStateEvent) {
const href: string = (event.state as HistoryState)?.url ?? window.location.href;

// Exit early if this event should be ignored
if (this.options.skipPopStateHandling(event)) {
return;
}

// Exit early if the resolved path hasn't changed
if (this.isSameResolvedUrl(getCurrentUrl(), this.location.url)) {
return;
}

const { url, hash } = Location.fromUrl(href);
const state: HistoryState = event.state as HistoryState;
const location = Location.fromUrl(state?.url ?? window.location.href);
const { url, hash } = location;

const visit = this.createVisit({ to: url, hash, event });

// Mark as history visit
visit.history.popstate = true;
visit.history.state = state;

// Determine direction of history visit
const index = (event.state as HistoryState)?.index ?? 0;
const index = state?.index ?? 0;
if (index && index !== this.currentHistoryIndex) {
const direction = index - this.currentHistoryIndex > 0 ? 'forwards' : 'backwards';
visit.history.direction = direction;
Expand All @@ -361,12 +364,18 @@ export default class Swup {
visit.scroll.reset = false;
visit.scroll.target = false;

// Animated history visit: re-enable animation & scroll reset
// Animated history visit: re-enable animation
if (this.options.animateHistoryBrowsing) {
visit.animation.animate = true;
visit.scroll.reset = true;
}

// Resolved path hasn't changed? Update visit+location and restore scroll position
if (this.isSameResolvedUrl(url, this.location.url)) {
this.restoreScrollPosition(visit);
return;
}

// Otherwise, perform full navigation
this.hooks.callSync('history:popstate', visit, { event }, () => {
this.performNavigation(visit);
});
Expand Down
20 changes: 20 additions & 0 deletions src/helpers/history.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import { getCurrentUrl } from './getCurrentUrl.js';

export interface HistoryScrollPosition {
x: number;
y: number;
}

export interface HistoryScrollPositions {
[key: string]: HistoryScrollPosition;
}

export interface HistoryScrollRestoration {
el: Window | Element;
x: number;
y: number;
}

export interface HistoryScrollRestorations {
[key: string]: HistoryScrollRestoration;
}

export interface HistoryState {
url: string;
source: 'swup';
random: number;
index?: number;
scroll?: HistoryScrollPositions;
[key: string]: unknown;
}

Expand Down
5 changes: 5 additions & 0 deletions src/modules/Hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type Swup from '../Swup.js';
import { isPromise, runAsPromise } from '../utils.js';
import { Visit } from './Visit.js';
import type { FetchOptions, PageData } from './fetchPage.js';
import type { HistoryScrollPositions, HistoryScrollRestorations } from '../helpers/history.js';

export interface HookDefinitions {
'animation:out:start': undefined;
Expand Down Expand Up @@ -31,6 +32,8 @@ export interface HookDefinitions {
'page:view': { url: string; title: string };
'scroll:top': { options: ScrollIntoViewOptions };
'scroll:anchor': { hash: string; options: ScrollIntoViewOptions };
'scroll:store': { scroll: HistoryScrollPositions };
'scroll:restore': { restore: HistoryScrollRestorations; options: ScrollIntoViewOptions };
'visit:start': undefined;
'visit:transition': undefined;
'visit:abort': undefined;
Expand Down Expand Up @@ -160,6 +163,8 @@ export class Hooks {
'page:view',
'scroll:top',
'scroll:anchor',
'scroll:store',
'scroll:restore',
'visit:start',
'visit:transition',
'visit:abort',
Expand Down
6 changes: 5 additions & 1 deletion src/modules/Visit.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { HistoryState } from '../helpers/history.js';
import type Swup from '../Swup.js';
import type { Options } from '../Swup.js';
import type { HistoryAction, HistoryDirection } from './navigate.js';
Expand Down Expand Up @@ -66,6 +67,8 @@ export interface VisitHistory {
popstate: boolean;
/** The direction of travel in case of a browser history navigation: backward or forward. */
direction: HistoryDirection | undefined;
/** The state associated with this history entry. */
state: HistoryState | undefined;
}

export interface VisitInitOptions {
Expand Down Expand Up @@ -141,7 +144,8 @@ export class Visit {
this.history = {
action: 'push',
popstate: false,
direction: undefined
direction: undefined,
state: undefined
};
this.scroll = {
reset: true,
Expand Down
6 changes: 6 additions & 0 deletions src/modules/navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ export async function performNavigation(
// Delete this so that window.fetch doesn't mis-interpret it
delete options.cache;

// Store current scroll position into history state
// (unless this is a popstate call where we no longer have access to the previous history state)
if (!visit.history.popstate) {
this.storeScrollPosition();
}

try {
await this.hooks.call('visit:start', visit, undefined);

Expand Down
85 changes: 85 additions & 0 deletions src/modules/scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type Swup from '../Swup.js';
import {
updateHistoryRecord,
type HistoryScrollPositions,
type HistoryScrollRestorations,
type HistoryState
} from '../helpers/history.js';
import type { Visit } from './Visit.js';

/**
* Update the scroll position after page render.
*/
export function scrollToContent(this: Swup, visit: Visit): boolean {
const options: ScrollIntoViewOptions = { behavior: 'auto' };
const hash = visit.scroll.target ?? visit.to.hash;

let scrolled = false;

if (visit.history.popstate) {
this.restoreScrollPosition(visit);
scrolled = true;
}

if (hash && !scrolled) {
// prettier-ignore
scrolled = this.hooks.callSync('scroll:anchor', visit, { hash, options }, (visit, { hash, options }) =>
scrollToElement(this.getAnchorElement(hash), options)
);
}

if (visit.scroll.reset && !scrolled) {
scrolled = this.hooks.callSync('scroll:top', visit, { options }, (visit, { options }) =>
scrollToPosition(window, 0, 0, options)
);
}

return scrolled;
}

export function storeScrollPosition(this: Swup) {
// Create temporary visit to avoid re-using the previous one
const visit = this.createVisit({ to: '' });
const scroll: HistoryScrollPositions = { window: { x: window.scrollX, y: window.scrollY } };

this.hooks.callSync('scroll:store', visit, { scroll }, (visit, { scroll }) => {
updateHistoryRecord(null, { scroll });
});
}

export function restoreScrollPosition(this: Swup, visit: Visit) {
const options: ScrollIntoViewOptions = { behavior: 'instant' };

const state = visit.history.state ?? (window.history.state as HistoryState);
const position = state?.scroll?.window ?? { x: 0, y: 0 };
const restore: HistoryScrollRestorations = { window: { ...position, el: window } };

this.hooks.callSync(
'scroll:restore',
visit,
{ restore, options },
(visit, { restore, options }) => {
for (const { el, x, y } of Object.values(restore)) {
scrollToPosition(el, x, y, options);
}
}
);
}

export function scrollToElement(el: Element | null, options?: ScrollIntoViewOptions): boolean {
if (el) {
el.scrollIntoView(options);
return true;
}
return false;
}

export function scrollToPosition(
el: Window | Element,
left: number,
top: number,
options?: ScrollIntoViewOptions
): boolean {
el.scrollTo({ left, top, ...options });
return true;
}
38 changes: 0 additions & 38 deletions src/modules/scrollToContent.ts

This file was deleted.

15 changes: 15 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ export function forceReflow(element?: HTMLElement): void {
element?.getBoundingClientRect();
}

/**
* Minimal debounce function.
* @see https://www.joshwcomeau.com/snippets/javascript/debounce/
*/
export function debounce<F extends (...args: unknown[]) => unknown>(
callback: F,
wait = 0
): (...args: Parameters<F>) => void {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>) => {
clearTimeout(timeout);
timeout = setTimeout(() => callback(...args), wait);
};
}

/**
* Read data attribute from closest element with that attribute.
*
Expand Down
Loading
Loading