向下滚动到部分时突出显示菜单项

Highlight Menu Item when Scrolling Down to Section

我知道这个问题在这个论坛上已经被问了一百万次,但是 none 篇文章帮助我找到了解决方案。

我制作了一小段 jquery 代码,当您向下滚动到与 hash-link 具有相同 ID 的部分时,它会突出显示 hash-link。

$(window).scroll(function() {
    var position = $(this).scrollTop();

    $('.section').each(function() {
        var target = $(this).offset().top;
        var id = $(this).attr('id');

        if (position >= target) {
            $('#navigation > ul > li > a').attr('href', id).addClass('active');
        }
    });
});

现在的问题是它突出显示了所有散列-link,而不仅仅是该部分与之相关的那个。谁能指出错误,还是我忘记了什么?

这一行:

 $('#navigation > ul > li > a').attr('href', id).addClass('active');

您实际上是在设置每个 $('#navigation > ul > li > a') 元素的 href 属性,然后将活动 class 也添加到所有元素。可能您需要做的是:

$('#navigation > ul > li > a[href=#' + id + ']')

而select只有href匹配id的a。有道理吗?

编辑:

我修改了我的答案以谈论一些性能和一些特殊情况。

如果您只是在这里寻找代码,底部有一段注释。


原回答

而不是将 .active class 添加到所有 link 中,您应该确定属性 href 与该部分的 id.

相同

然后您可以将 .active class 添加到 link 并将其从其余部分中删除。

        if (position >= target) {
            $('#navigation > ul > li > a').removeClass('active');
            $('#navigation > ul > li > a[href=#' + id + ']').addClass('active');
        }

通过上述修改,您的代码将正确突出显示相应的 link。希望对您有所帮助!


提高性能

即使这段代码能够完成它的工作,也远非最佳。无论如何,记住:

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. (Donald Knuth)

因此,如果在慢速设备中进行事件测试时,您没有遇到任何性能问题,那么您最好停止阅读并考虑项目的下一个惊人功能!

基本上,可以通过三个步骤来提高性能:

尽量多做以前的工作:

为了避免一次又一次地搜索 DOM(每次触发事件时),您可以预先缓存 jQuery 对象(例如在 document.ready 上):

var $navigationLinks = $('#navigation > ul > li > a');
var $sections = $(".section"); 

然后,您可以将每个部分映射到相应的导航link:

var sectionIdTonavigationLink = {};
$sections.each( function(){
    sectionIdTonavigationLink[ $(this).attr('id') ] = $('#navigation > ul > li > a[href=\#' + $(this).attr('id') + ']');
});

请注意锚点中的两个反斜杠 select 或:散列“#”在 CSS 中具有特殊含义,因此 it must be escaped (thanks ).

此外,您可以缓存每个部分的位置(Bootstrap 的 Scrollspy 就是这样做的)。但是,如果你这样做,你需要记住在每次更改时更新它们(用户调整 window 的大小,通过 [=140= 添加新内容,扩展小节等)。

优化事件处理器:

假设用户正在 一个部分内滚动:活动导航link 不需要更改。但是如果你看上面的代码,你会发现实际上它改变了好几次。在正确的 link 被高亮显示之前,所有之前的 link 也会高亮显示(因为它们对应的部分也验证条件 position >= target)。

一个解决方案是将底部的部分迭代到顶部,第一个 .offset().top 等于或小于 $(window).scrollTop 的部分是正确的。是的,you can rely on jQuery returning the objects in the order of the DOM (since version 1.3.2)。要从下到上迭代,只需按相反的顺序 select 它们:

var $sections = $( $(".section").get().reverse() );
$sections.each( ... );

$() 是必需的,因为 get() returns DOM 元素,而不是 jQuery 对象。

找到正确的部分后,您应该return false退出循环并避免检查更多部分。

最后,如果正确的导航 link 已经突出显示,您不应该做任何事情,所以请检查一下:

if ( !$navigationLink.hasClass( 'active' ) ) {
    $navigationLinks.removeClass('active');
    $navigationLink.addClass('active');
}

尽量少触发事件:

防止高评级事件(滚动、调整大小...)使您的网站变慢或无响应的最明确方法是控制调用事件处理程序的频率:确保您不需要检查哪个link每秒需要高亮100次!如果除了 link 突出显示之外,您还可以添加一些奇特的视差效果,您可以 运行 快速介绍麻烦。

此时,您确定要阅读有关 throttle、debounce 和 requestAnimationFrame 的内容。 This article 是一个很好的讲座,可以很好地概述其中的三个。对于我们的案例,节流最符合我们的需求。

基本上,节流强制执行两个函数执行之间的最小时间间隔。

我在代码片段中实现了节流功能。从那里你可以变得更复杂,甚至更好,使用像 underscore.js or lodash 这样的库(如果你不需要整个库,你总是可以从那里提取 throttle 函数)。

注意:环顾四周,您会发现更简单的油门功能。提防它们,因为它们可能会错过最后一个事件触发器(这是最重要的一个!)。

特殊情况:

我不会在代码片段中包含这些案例,以免进一步复杂化。

在下面的代码片段中,当该部分到达页面的最顶部时,link 将突出显示。如果你想让它们之前突出显示,你可以通过这种方式添加一个小的偏移量:

if (position + offset >= target) {

当您有顶部导航栏时,这特别有用。

如果您的最后一个部分太小而无法到达页面顶部,您可以在滚动条处于最底部时突出显示相应的 link:

if ( $(window).scrollTop() >= $(document).height() - $(window).height() ) {
    // highlight the last link

存在一些浏览器支持问题。您可以阅读更多相关信息 here and here

片段和测试

最后,这里有一段评论。请注意,我更改了一些变量的名称以使其更具描述性。

// cache the navigation links 
var $navigationLinks = $('#navigation > ul > li > a');
// cache (in reversed order) the sections
var $sections = $($(".section").get().reverse());

// map each section id to their corresponding navigation link
var sectionIdTonavigationLink = {};
$sections.each(function() {
    var id = $(this).attr('id');
    sectionIdTonavigationLink[id] = $('#navigation > ul > li > a[href=\#' + id + ']');
});

// throttle function, enforces a minimum time interval
function throttle(fn, interval) {
    var lastCall, timeoutId;
    return function () {
        var now = new Date().getTime();
        if (lastCall && now < (lastCall + interval) ) {
            // if we are inside the interval we wait
            clearTimeout(timeoutId);
            timeoutId = setTimeout(function () {
                lastCall = now;
                fn.call();
            }, interval - (now - lastCall) );
        } else {
            // otherwise, we directly call the function 
            lastCall = now;
            fn.call();
        }
    };
}

function highlightNavigation() {
    // get the current vertical position of the scroll bar
    var scrollPosition = $(window).scrollTop();

    // iterate the sections
    $sections.each(function() {
        var currentSection = $(this);
        // get the position of the section
        var sectionTop = currentSection.offset().top;

        // if the user has scrolled over the top of the section  
        if (scrollPosition >= sectionTop) {
            // get the section id
            var id = currentSection.attr('id');
            // get the corresponding navigation link
            var $navigationLink = sectionIdTonavigationLink[id];
            // if the link is not active
            if (!$navigationLink.hasClass('active')) {
                // remove .active class from all the links
                $navigationLinks.removeClass('active');
                // add .active class to the current link
                $navigationLink.addClass('active');
            }
            // we have found our section, so we return false to exit the each loop
            return false;
        }
    });
}

$(window).scroll( throttle(highlightNavigation,100) );

// if you don't want to throttle the function use this instead:
// $(window).scroll( highlightNavigation );
#navigation {
    position: fixed;
}
#sections {
    position: absolute;
    left: 150px;
}
.section {
    height: 200px;
    margin: 10px;
    padding: 10px;
    border: 1px dashed black;
}
#section5 {
    height: 1000px;
}
.active {
    background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="navigation">
    <ul>
        <li><a href="#section1">Section 1</a></li>
        <li><a href="#section2">Section 2</a></li>
        <li><a href="#section3">Section 3</a></li>
        <li><a href="#section4">Section 4</a></li>
        <li><a href="#section5">Section 5</a></li>
    </ul>
</div>
<div id="sections">
    <div id="section1" class="section">
        I'm section 1
    </div>
    <div id="section2" class="section">
        I'm section 2
    </div>
    <div id="section3" class="section">
        I'm section 3
    </div>
    <div id="section4" class="section">
        I'm section 4
    </div>
    <div id="section5" class="section">
        I'm section 5
    </div>
</div>

如果您有兴趣,this fiddle 测试我们讨论过的不同改进。

编码愉快!

对于最近尝试使用此解决方案的任何人,我在尝试使其工作时遇到了障碍。您可能需要像这样转义 href:

$('#navigation > ul > li > a[href=\#' + id + ']');

现在我的浏览器不会对该片段抛出错误。

我已经采用了 David 的优秀代码并从中删除了所有 jQuery 依赖项,以防有人感兴趣:

// cache the navigation links 
var $navigationLinks = document.querySelectorAll('nav > ul > li > a');
// cache (in reversed order) the sections
var $sections = document.getElementsByTagName('section');

// map each section id to their corresponding navigation link
var sectionIdTonavigationLink = {};
for (var i = $sections.length-1; i >= 0; i--) {
 var id = $sections[i].id;
 sectionIdTonavigationLink[id] = document.querySelectorAll('nav > ul > li > a[href=\#' + id + ']') || null;
}

// throttle function, enforces a minimum time interval
function throttle(fn, interval) {
 var lastCall, timeoutId;
 return function () {
  var now = new Date().getTime();
  if (lastCall && now < (lastCall + interval) ) {
   // if we are inside the interval we wait
   clearTimeout(timeoutId);
   timeoutId = setTimeout(function () {
    lastCall = now;
    fn.call();
   }, interval - (now - lastCall) );
  } else {
   // otherwise, we directly call the function 
   lastCall = now;
   fn.call();
  }
 };
}

function getOffset( el ) {
 var _x = 0;
 var _y = 0;
 while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
  _x += el.offsetLeft - el.scrollLeft;
  _y += el.offsetTop - el.scrollTop;
  el = el.offsetParent;
 }
 return { top: _y, left: _x };
}

function highlightNavigation() {
 // get the current vertical position of the scroll bar
 var scrollPosition = window.pageYOffset || document.documentElement.scrollTop;

 // iterate the sections
 for (var i = $sections.length-1; i >= 0; i--) {
  var currentSection = $sections[i];
  // get the position of the section
  var sectionTop = getOffset(currentSection).top;

    // if the user has scrolled over the top of the section  
  if (scrollPosition >= sectionTop - 250) {
   // get the section id
   var id = currentSection.id;
   // get the corresponding navigation link
   var $navigationLink = sectionIdTonavigationLink[id];
   // if the link is not active
   if (typeof $navigationLink[0] !== 'undefined') {
    if (!$navigationLink[0].classList.contains('active')) {
     // remove .active class from all the links
     for (i = 0; i < $navigationLinks.length; i++) {
      $navigationLinks[i].className = $navigationLinks[i].className.replace(/ active/, '');
     }
     // add .active class to the current link
     $navigationLink[0].className += (' active');
    }
   } else {
     // remove .active class from all the links
     for (i = 0; i < $navigationLinks.length; i++) {
      $navigationLinks[i].className = $navigationLinks[i].className.replace(/ active/, '');
     }
   } 
   // we have found our section, so we return false to exit the each loop
   return false;
  }
 }
}

window.addEventListener('scroll',throttle(highlightNavigation,150));

function navHighlight() {
    var scrollTop = $(document).scrollTop();

    $("section").each(function () {
        var xPos = $(this).position();
        var sectionPos = xPos.top;
        var sectionHeight = $(this).height();
        var overall = scrollTop + sectionHeight;

        if ((scrollTop + 20) >= sectionPos && scrollTop < overall) {
            $(this).addClass("SectionActive");
            $(this).prevAll().removeClass("SectionActive");
        }

        else if (scrollTop <= overall) {
            $(this).removeClass("SectionActive");
        }

        var xIndex = $(".SectionActive").index();
        var accIndex = xIndex + 1;

        $("nav li:nth-child(" + accIndex + ")").addClass("navActivePage").siblings().removeClass("navActivePage");
    });
}


.navActivePage {
    color: #fdc166;
}


$(document).scroll(function () {
    navHighlight();
});