Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to detect automatic scrolling to anchors?

I have a one page website, example.com. There are two sections: intro at the top of the page, and contact at the bottom of the page. If I want someone to visit the contact section without having to scroll through the intro, I give them this link: example.com/#contact. I'm talking about these visits below.

The browser automatically scrolls down to the contact section, but it ignores the fixed navigation at the top of the page, so the contact section is scrolled behind the navigation so the top of the contact section becomes invisible. This is what I want to correct using JavaScript, by subtracting the height of the fixed navigation from the scroll position. Let's call this function the scrollCorrector. The problem is that I don't know exactly when such an automatic scrolling happens, so the scrollCorrector isn't called everytime it should be.

When should the scrollCorrector be called? When the automatic scrolling happens because of the hash portion. Why not to use onscroll? Because this way I can't differenciate an auto scroll from a user scroll. Why not to use onclick on every <a href="example.com/#contact">? I'll use it, but what if a user navigates by the browser's back button? Okay, I'll use onpopstate as well. But what if the user comes from example.com/#intro by manually rewriting the URL to example.com/#contact? Okay, I'll use onhashchange as well. But what if the user is already on example.com/#contact, clicks to the address bar, and presses enter without any modification? None of the above helps then.

What event should I listen to then? If such an event doesn't exist, how could the scrollCorrector know that an automatic scroll has just happened?

like image 773
Tamás Bolvári Avatar asked Sep 27 '22 14:09

Tamás Bolvári


2 Answers

The scroll event will fire, so you could,

  • check your actual location.hash, if empty we don't care
  • debounce the event to be sure it's not a mousewheel that triggered it
  • get the actual document.querySelector(location.hash).getBoundingClientRect().top, if it is ===0 then call your scrollCorrector.

var AnchorScroller = function() {
  // a variable to keep track of last scroll event
  var last = -100, // we set it to -100 for the first call (on page load) be understood as an anchor call
    // a variable to keep our debounce timeout so that we can cancel it further
    timeout;

  this.debounce = function(e) {
    // first check if we've got a hash set, then if the last call to scroll was made more than 100ms ago
    if (location.hash !== '' && performance.now() - last > 100)
    // if so, set a timeout to be sure there is no other scroll coming
      timeout = setTimeout(shouldFire, 100);
    // that's not an anchor scroll, stop it right now !	
    else clearTimeout(timeout);
    // set the new timestamp
    last = performance.now();
  }

  function shouldFire() {
    // a pointer to our anchored element
    var el = document.querySelector(window.location.hash);
    // if non-null (an other usage of the location.hash) and that it is at top of our viewport
    if (el && el.getBoundingClientRect().top === 0) {
      // it is an anchor scroll
      window.scrollTo(0, window.pageYOffset - 64);
    }
  }
};
window.onscroll = new AnchorScroller().debounce;
body {
  margin: 64px 0 0 0;
}
nav {
  background: blue;
  opacity: 0.7;
  position: fixed;
  top: 0;
  width: 100%;
}
a {
  color: #fff;
  float: left;
  font-size: 30px;
  height: 64px;
  line-height: 64px;
  text-align: center;
  text-decoration: none;
  width: 50%;
}
div {
  background: grey;
  border-top: 5px #0f0 dashed;
  font-size: 30px;
  padding: 25vh 0;
  text-align: center;
}
#intro,#contact {  background: red;}
<nav>
  <a href="#intro">Intro</a>
  <a href="#contact">Contact</a>
</nav>
<div id="intro">  Intro </div>
<div> Lorem </div>
<div id="contact">  Contact </div>
<div> Ipsum </div>

Caveats :
- it introduces a 100ms timeout between the scroll event and the correction, which is visible.
- it's not 100% bullet-proof, an user could trigger only one event (by mousewheel or keyboard) and fall exactly at the right position so it produces a false-positive. But chances for that to happen are so small that it might be acceptable for such a behaviour.

like image 62
Kaiido Avatar answered Oct 04 '22 14:10

Kaiido


I have looked and yes I can see the limitations you mention with using window.onhashchange.

I understand what you want but I don't think such a thing exists.

This is the best I came up with (abandoning hashchange altogether):

<html>
<head>
<script>
"use strict";
(function () {
    window.myFunc = function(href) {
        window.alert("Link clicked, hash is: " + href);
    };
    window.alert("Page just reloaded, hash is: " + window.location.hash);
})();
</script>
</head>
<body>
<a href="#a" onclick="myFunc(this.hash)">a</a><br />
<a href="#b" onclick="myFunc(this.hash)">b</a>
<h1 id="a">a</a>
<h1 id="b">b</a>
</body>
</html>
like image 35
Neilos Avatar answered Oct 04 '22 15:10

Neilos