如何检测自动滚动到锚点?

6
我有一个单页网站,example.com。它有两个部分:页面顶部的intro和页面底部的contact。如果我想让某人在不必浏览intro的情况下访问contact部分,我会给他们这个链接:example.com/#contact。下面我将讨论这些访问。
浏览器会自动滚动到contact部分,但会忽略固定在页面顶部的导航,因此contact部分会被滚动到导航之后,使得contact部分的顶部变得不可见。我想通过JavaScript来纠正这个问题,方法是从滚动位置中减去固定导航的高度。让我们把这个函数称为scrollCorrector。问题是我不知道何时会发生这样的自动滚动,所以scrollCorrector并不总是被调用。
什么时候应该调用scrollCorrector呢?当由于哈希部分而发生自动滚动时。为什么不使用onscroll呢?因为这样我无法区分自动滚动和用户滚动。为什么不在每个<a href="example.com/#contact"> 上使用onclick呢?我会使用它,但如果用户通过浏览器的后退按钮进行导航呢?好吧,我还将使用onpopstate。但如果用户通过手动重写URL从example.com/#intro跳转到example.com/#contact呢?那么我将使用onhashchange。但是,如果用户已经在example.com/#contact上了,并单击地址栏,然后按回车键而没有进行任何修改呢?那么以上所有方法均无法帮助。
那么我应该监听哪个事件呢?如果这样的事件不存在,scrollCorrector如何知道刚刚发生了自动滚动?

1
什么是问题?什么是跳跃?(我可能很愚蠢???) - Neilos
我的意思是当浏览器滚动到文档中具有与URL哈希部分匹配的ID的特定元素时。 - Tamás Bolvári
那么问题可能更清楚一些:“如何检测相同哈希值的哈希更改?” - Tamás Bolvári
但是,如果哈希值没有改变,那就不是哈希更改。 - jfriend00
当然没问题。但这会导致“跳跃”(重新滚动到相同的元素),不是吗?好的,那么让我们深入了解一下。我在页面顶部有一个固定的导航栏。如果有人带着 #contact 访问网站,浏览器会跳转到联系部分。但是导航栏会重叠在该部分上,因此某些内容将无法显示。这里需要使用JS:它会使页面滚动更多,以使内容变得可见。何时应进行此滚动修正?每次用户通过访问 #contact 页面跳转到联系部分时。 - Tamás Bolvári
显示剩余5条评论
2个回答

4
滚动事件将会触发,所以你可以:
  • 检查你的实际location.hash,如果为空,我们不关心
  • 防抖动事件,确保它不是由鼠标滚轮触发的
  • 获取实际的document.querySelector(location.hash).getBoundingClientRect().top,如果它等于===0,那么调用你的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>

注意事项:
- 它在滚动事件和校正之间引入了100毫秒的超时,这是可见的。
- 它并非百分之百可靠,用户可能仅触发一个事件(通过鼠标滚轮或键盘),并恰好落在正确位置,从而产生误报。但是发生这种情况的几率如此之小,以至于对于这种行为来说可能是可以接受的。


没问题,但是防抖器应该调用什么类型的函数来判断是否为自动滚动呢?一旦我知道了这一点,实现你在第2和第3点中提到的scrollCorrector逻辑就很容易了。但只要我无法区分用户滚动和自动滚动,就无法以这种方式完成。所以我还在寻找其他方法,因为这似乎是不可能的。https://dev59.com/Zmw05IYBdhLWcg3wxkqr#7210072 - Tamás Bolvári
1
我指向David Walsh的文章,因为他使用了一个聪明的超时来避免多个调用发生在短时间内(几毫秒的顺序)。您可以调整此函数以检测是否有其他滚动事件触发到间隔。如果是这样,它来自用户操作,您应该返回该函数。否则,请执行其他检查。它可能不是100%的防弹,但很难通过鼠标滚轮仅产生一个滚动事件并恰好落在哈希链接元素的scrollHeight上。也许这种误差可以接受? - Kaiido
Ps:当我有时间时,我可能会给你写整个函数。 - Kaiido
现在很清楚了。只要浏览器不平滑滚动,而是直接“跳转”到锚定元素,这是一个好主意。我已经检查了从Android浏览器到Edge到Chrome的所有内容,幸运的是它们都没有使用平滑滚动,所以这个想法是明显的赢家,谢谢。这是简化的实现:https://jsfiddle.net/36yw1x5z/1/ - Tamás Bolvári

1

我看了一下,确实可以看到使用window.onhashchange存在的限制。

我理解你的需求,但我认为这样的东西并不存在。

这是我想出来的最好方案(完全放弃使用hashchange):

<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>

谢谢,但这只在重新加载和链接点击时发出警报。尝试在Mozilla Firefox或Microsoft Edge中打开它,以便您看到example.com/index.html#b。滚动到#a而不单击或重新加载。现在单击地址栏,不要更改任何内容,只需按Enter键。您将再次看到浏览器滚动到#b,而没有任何警报。这就是问题所在。我已经尝试过onloadonhashchangeonpopstate,但这些都没有帮助。我会尝试以某种方式使用onscroll... - Tamás Bolvári
你是对的。我在Chrome上测试了一下,在Chrome上它实现了你想要的功能。 - Neilos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接