6

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?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Tamás Bolvári
  • 2,976
  • 6
  • 34
  • 57
  • 1
    What is the question? What is a jump? (I might be being stupid???) – Neilos Oct 02 '15 at 23:20
  • I mean when the browser scrolls to a specific element in the document that has a matching id with the URL's hash portion. – Tamás Bolvári Oct 02 '15 at 23:24
  • Ok I think I understand now, but your question is very unclear, you might want to state your problem more clearly. There is no built in functionality to do what you want, you could do something like this http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling checking if the anchors are visible when you scroll. Then when it has "jumped" you could fire a custom event. Is that the kind of thing you mean? – Neilos Oct 02 '15 at 23:34
  • With regards to the anchor only firing one event you could bind events to the anchor directly. But unfortunately you will struggle to get an event of any kind to fire when the user presses enter in the address bar without making any changes. – Neilos Oct 02 '15 at 23:35
  • I agree, but I don't know how could I describe the problem more clearly. I might rewrite it later, but now it's 2AM here and I'm out of coffee. :) Thank you for the help anyway. I know how to check if the element is in the viewport. The thing I don't know is when should I check it. I mean I know when to check it, but don't know if the event is happened or not. Clicks on links can be detected. Hash changes can be detected. But "hash changes to the same hash" that result in the browser scrolling again to the specified element can not be detected. At least I haven't figured it out yet. – Tamás Bolvári Oct 02 '15 at 23:46
  • So the question might be clearer this way: "How to detect hash changes to the same hash?" – Tamás Bolvári Oct 02 '15 at 23:48
  • But, it's not a hash change if the hash doesn't change. – jfriend00 Oct 02 '15 at 23:50
  • Sure. But it results in a "jump" (scrolling to the same element again), isn't it? Okay let's get into the details then. I have a fixed navigation bar on the top of the page. If someone comes to the site with #contact in the hash portion, the browser jumps to the contact section. But the nav bar overlaps the section, so some of the content is invisible. Here comes the JS: it makes the page scroll a bit more, so that the content becomes visible. When should this scroll correction happen? Everytime the user jumps to the contact section by visiting the #contact page. – Tamás Bolvári Oct 02 '15 at 23:59
  • Basically I'm looking for something like a `submit` event of the address bar. You could tell me about `popstate`, or `hashchange`, but these don't get fired in this case. – Tamás Bolvári Oct 03 '15 at 00:29

2 Answers2

4

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.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • That's ok, but what type of function should the debouncer call to tell if it's an automatic scroll? Once I know this, it's easy to implement the `scrollCorrector` logic that you've mentioned in point 2 and 3. But as long as I can't differentiate between user scrolls and automatic scrolls, it can't be done this way. So I'm still looking for an other approach, because this seems to be impossible. http://stackoverflow.com/a/7210072/1293492 – Tamás Bolvári Oct 03 '15 at 22:07
  • 1
    Well I pointed to David Walsh's article since he does use a clever timeout to avoid multiple calls happening to short (in the order of a few ms). You can tweak this function to detect if there was an other scroll event fired into the interval. If so, it came from a user action and you should return the function. Else, do the other check. It may not be 100% bullet proof, but it is very hard to produce only one scroll event through mousewheel and fall exactly at the scrollHeight of the hash-linked element. Maybe this marge of error can be acceptable ? – Kaiido Oct 04 '15 at 03:05
  • Ps : I might write you the whole func when I'll have time – Kaiido Oct 04 '15 at 04:14
  • It's clear now. It's a great idea as long as browsers don't smooth this type of scrolling, but simply "jump" straight to the anchored element. I've checked everything from Android browser to Edge to Chrome, and fortunatelly none of them use smooth scrolling, so this idea is the clear winner, thank you. Here is the simplified implementation: https://jsfiddle.net/36yw1x5z/1/ – Tamás Bolvári Oct 04 '15 at 21:14
1

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>
Neilos
  • 2,696
  • 4
  • 25
  • 51
  • Thank you, but this alerts only on reloads and link clicks. Try to open this in Mozilla Firefox or Microsoft Edge, so that you see `example.com/index.html#b`. Scroll away to #a without clicking or reloading. Now click on the address bar, don't change anything, just press enter. You'll see the browser scrolling to #b again, without any alerts. That's the problem. I've tried `onload`, `onhashchange`, `onpopstate` but none of these helps. I'll try to use `onscroll` somehow... – Tamás Bolvári Oct 03 '15 at 01:23
  • You're right. I tested that in chrome, and in chrome it does what you want. – Neilos Oct 03 '15 at 19:53