Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store and restore scroll positions #26

Merged
merged 11 commits into from
Aug 17, 2022
Merged

Store and restore scroll positions #26

merged 11 commits into from
Aug 17, 2022

Conversation

hirasso
Copy link
Member

@hirasso hirasso commented Aug 8, 2022

As described in #24 :

  • Store the current scroll positions of the window and all elements matching the selector [data-swup-scroll-container] for the previousUrl on willReplaceContent
  • Restore the stored scroll positions on contentReplaced. Also takes care of restoring the window's scroll position, if swup's option animateHistoryBrowsing is set to true.
  • on clickLink, reset the scroll positions for the URL the link points towards.
    • allow to opt-out from resetting scroll positions for a link using the option skipDeletingScrollPositions

@hirasso hirasso requested review from gmrchk and daun August 8, 2022 09:06
@hirasso hirasso marked this pull request as draft August 8, 2022 09:17
@hirasso hirasso removed request for gmrchk and daun August 8, 2022 09:17
…ink`

If a user only navigates using the browser's back and forward buttons and scrolls the window and/or some divs in between, the scroll positions will now also be updated.
@hirasso hirasso requested review from daun and gmrchk August 8, 2022 09:30
src/index.js Outdated
return;
}
// cycle through all elements on the current page and restore their scroll positions, if appropriate
const $elements = document.querySelectorAll('[data-swup-restore-scroll]');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use the queryAll helper here, it converts the NodeList to an array which I think Safari still has problems with.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops really? Do you have a source describing the Safari issue? If so, I have been building a lot of broken websites in the last few years 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmh. Officially it's supported from Safari 10, so that should never have been an issue. Maybe I'm misremembering and it was Edge? It definitely was an issue as recently as 1/2 years ago, even with Babel.

Copy link
Member Author

@hirasso hirasso Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, we should be fine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know how I could actually access swups queryAll helper function from the plugin? I don't really care, would just as well use that instead of querySelectorAll, if I would have access to it ;)

src/index.js Outdated
}

onClickLink = (e) => {
this.deleteStoredScrollPositions(e.delegateTarget.href);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to delete the scroll position on link click?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatically Restoring the scroll position should only be done while navigating the history. Otherwise, it should be 0, just like the browser does it. Maybe we could introduce another attribute here, something like [data-swup-history-link] or the like. Then we could preserve the scroll positions for links matching this attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link would then basically behave for scroll positions like a link with href="javascript:history.back()".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an attr for changing the behavior, though, it should be fine the way it is.

Copy link
Member Author

@hirasso hirasso Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just remembered a few cases I had in the past, where clients wanted to go back to a list of posts from a detail page, and the browser back button wasn't enough for them. So we do need a solution for that IMO. This is what I came up with: d10f95d

The new option behaves just like swup's own option skipPopstateHandling and could be used like this, for example:

{
  skipDeletingScrollPositions: linkElement => {
    return linkElement.classList.contains('backlink') // don't delete the scroll positions for link elements with the class "backlink"
  }
}

src/index.js Outdated
elements: []
}
// fill up the object with scroll positions for each matching element
const $elements = document.querySelectorAll('[data-swup-restore-scroll]');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the selector configurable? We could also exit early here if a normal query returns null and skip the forEach.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll add the option for the selector. Right now, this really is just a POC to see what you think. Could you take it for a spin in some little test project?

We could also exit early here if a normal query returns null and skip the forEach.

querySelectorAll always returns a list, so no need to detect null and bail early here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added scrollContainersSelector to the options.

@daun
Copy link
Member

daun commented Aug 8, 2022

I'll take it for a spin later or tomorrow!

Allows to skip the deletion of scroll positions on `clickLink`, based on a callback.
@hirasso hirasso marked this pull request as ready for review August 8, 2022 11:26
@hirasso
Copy link
Member Author

hirasso commented Aug 8, 2022

Just a heads-up, this are the new options in this PR:

{
  scrollContainersSelector: `[data-swup-scroll-container]`,
  skipDeletingScrollPositions: linkElement => {
    return linkElement.classList.contains('backlink') // don't delete the scroll positions for link elements with the class "backlink"
  }
}

@hirasso
Copy link
Member Author

hirasso commented Aug 9, 2022

I have uploaded a test site for this feature to my server:

https://test.rassohilber.com/swup-scroll-plugin/test/

How to test:

  1. Scroll the bordered div down a bit
  2. Navigate to "Projects" or "About"
  3. Navigate back using the browser's back-button or the link underneath the box

The bordered div's scroll position should be restored.

If you change the option animateHistoryBrowsing through the console and repeat above steps, the window's scroll position also gets restored automatically:

swup.options.animateHistoryBrowsing = true

@hirasso hirasso requested review from daun and removed request for gmrchk August 11, 2022 19:36
@daun
Copy link
Member

daun commented Aug 11, 2022

Looking really good! I've had a few questions/suggestions I just haven't come around to write up properly, I'll try and get to it tomorrow.

@daun
Copy link
Member

daun commented Aug 11, 2022

Working really well, I can see this being super useful in future projects!

A few thoughts:

  • The selector option name could be simpler, how about simply scrollContainers, in keeping with swup’s selectors option?

  • I’m not a fan of having the window and the scroll containers be separate concepts. If we have a single selector like html, [data-scroll-container], we can treat them all the same and it’ll be easier to add new features across all scroll containers, e.g. allow anchor-scrolling in other containers than the window as well

    • That way, one could theoretically disable all scrolling on the window by passing a custom selector without html
    • We’d need to check in the loop of storeScrollPositions if the element is html or another one, and then change where we get/set the position from (window.scrollY or el.scrollTop). Just like you’re doing now, but at a different point in time.
  • The name of the option skipDeletingScrollPositions is confusing, users don’t know about the internals and the deleting part. How about the other way around: restoreScrollPosition(link) => true? In that case, we can extend the function in the future to include e.g. the container itself and allow customizing by link and container.

Let me know if you need help with the docs, happy to help with that.

@hirasso
Copy link
Member Author

hirasso commented Aug 12, 2022

Thanks for your great feedback!

The selector option name could be simpler, how about simply scrollContainers, in keeping with swup’s selectors option?

You mean in keeping with swup's containers option, right? If so, makes sense.

I’m not a fan of having the window and the scroll containers be separate concepts. If we have a single selector like html, [data-scroll-container], we can treat them all the same and it’ll be easier to add new features across all scroll containers, e.g. allow anchor-scrolling in other containers than the window as well

Great idea! One thing to keep in mind: If we do that and handle window and custom elements the same, the automatic scroll restoration should never be animated (technical limitations in gmrchk/scrl, it only supports the window and only scrollTop). It shouldn't be animated, anyways, IMO. We will still have to decide, what we would want to do in the case of animateHistoryBrowsing = true, but we can probably take care of this later.

The name of the option skipDeletingScrollPositions is confusing, users don’t know about the internals and the deleting part. How about the other way around: restoreScrollPosition(link) => true?

😄 I was actually going back and forth with the naming of this option, couldn't decide. Will use yours, instead. Also, the singular (restoreScrollPosition, not restoreScrollPositions) feels more natural, since the consumers probably won't care about how many elements have to be treated to restore the previous scroll position.

In that case, we can extend the function in the future to include e.g. the container itself and allow customizing by link and container.

Sounds interesting, looking forward to hear what you are imagining.

- Remove duplicate code
- Avoid unreachable else-statements
- Introduce guard clauses
- Rename new options
@hirasso
Copy link
Member Author

hirasso commented Aug 17, 2022

@daun after our chat I did some more work here. The main two features:

  • renamed the plugin options
  • handeled a possibly stored window scroll position on forward navigation (clickLink)

To bring some sanity to this update, I did some major cleanup and refactoring in the code. That's why it looks like I have changed a lot, but I haven't, really :)

Happy to walk you through in another call!

The above test site has been updated with the newest version.

@daun
Copy link
Member

daun commented Aug 17, 2022

Looks great and works great, too. The code now is super logical. Is the functionality feature/finalized on your part, i.e ready to merge?

I have two minor nitpicks I'll add as comments, but this is great work! Looking forward to have this in the plugin 🥯

src/index.js Outdated
containers: []
};
// fill up the object with scroll positions for each matching element
const $containers = document.querySelectorAll(this.options.scrollContainers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the codebase uses the $ prefix for dom elements, I would keep that consistent.

@hirasso
Copy link
Member Author

hirasso commented Aug 17, 2022

Great to hear! I'll apply your comments and finalize the inline docs, then it's ready to be merged from my side ⛱️

src/index.js Outdated
};
// fill up the object with scroll positions for each matching element
const $containers = document.querySelectorAll(this.options.scrollContainers);
$containers.forEach((el) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we turn this into a .map and create the containers object directly, we'd save a few lines. Also maybe reorder to create the containers first, then construct the final entry?

(Untested)

const window = { top: window.scrollY, left: window.scrollX };
const containers = queryAll(this.options.scrollContainers)
  .map((el) => ({ top: el.scrollTop, left: el.scrollLeft }));

this.scrollPositionsStore[url] = { window, containers };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I'm old-school, keep forgetting that .map exists nowadays ;) ...window can't be used as a variable I think, but still – I will adopt your suggestion partly just to get used to thinking with .map more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure window can be assigned to a const; it's scoped locally to that method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, wrong explanation – you just can't use it if you are immediately referencing window... Anyways, too complicated to explain, wait for the push :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even then, no? You're grabbing the scroll values from the global window object, only then are you creating a new local const. Maybe I have it backwards, but I'm doing this all over the place and it seems to be working 🤡

Copy link
Member Author

@hirasso hirasso Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I type this in the console:

const window = { top: window.scrollY, left: window.scrollX };

I get the error:

Uncaught SyntaxError: Identifier 'window' has already been declared".

If I put it in the program, I get this error:

ReferenceError: Cannot access 'window2' before initialization

Copy link
Member Author

@hirasso hirasso Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyways, let's not make this more complicated than it necessarily has to be, shall we? ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, you're right, the scope doesn't work out here. I'll stop spreading nonsense now 🧃

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏😁

src/index.js Outdated
* @returns {object}
*/
getStoredScrollPositions(url) {
return this.scrollPositionsStore[url] || {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest not returning the fallback object {} here. It makes it easier to check for a missing entry elsewhere and it's more obvious what's happening. The check on line 309/310 (scrollPositions.containers == null) would need to change then to check for null instead of assuming an empty object. But as a new contributor it'd be easier to grok I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we did this, we would have to do scrollPositions && scrollPositions.containers, also the same for scrollPositions.window in on line 237. That's why I would prefer the function to always return an object.

Copy link
Member

@daun daun Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could move the || {} fallback to after the method call. That'd make it just as simple. Returning an empty object is a bit of a code smell as you need to reach into the empty object to even know if you got a return.

const position = this.getStoredScrollPositions(url) || {};

src/index.js Outdated

// cycle through all containers on the current page and restore their scroll positions, if appropriate
const $containers = document.querySelectorAll(this.options.scrollContainers);
$containers.forEach((el, index) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency and simplicity, queryAll(this.options.scrollContainers).forEach()

@daun
Copy link
Member

daun commented Aug 17, 2022

I added a few comments, let me know what you think or if you agree.

@hirasso hirasso requested a review from daun August 17, 2022 18:40
@hirasso
Copy link
Member Author

hirasso commented Aug 17, 2022

Ready to merge from my side. I'll create another PR for the docs when this has landed in master.

@daun
Copy link
Member

daun commented Aug 17, 2022

Looks good! Ready to merge? Do we need to build dist files again?

@hirasso
Copy link
Member Author

hirasso commented Aug 17, 2022

Ah of course. The dist files 🤖

@hirasso hirasso merged commit 87e1312 into swup:master Aug 17, 2022
@hirasso hirasso deleted the feature/restore-scroll branch August 17, 2022 18:59
@daun
Copy link
Member

daun commented Aug 17, 2022

👏

@hirasso hirasso mentioned this pull request Nov 7, 2022
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants