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

Posted

技术标签:

【中文标题】向下滚动到部分时突出显示菜单项【英文标题】:Highlight Menu Item when Scrolling Down to Section 【发布时间】:2015-11-30 11:45:47 【问题描述】:

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

我制作了一小段 jquery 代码,当您向下滚动到与哈希链接中​​具有相同 ID 的部分时,该代码会突出显示哈希链接。

$(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');
        
    );
);

现在的问题是它突出显示了所有哈希链接,而不仅仅是与该部分相关的那个。谁能指出错误,还是我忘记了?

【问题讨论】:

【参考方案1】:

在这一行:

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

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

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

并且只选择与id匹配的href。有意义吗?

【讨论】:

谢谢,我试试看! :-)【参考方案2】:

编辑:

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

如果你只是在这里找代码,底部有注释的sn-p。


原答案

不要将.active class 添加到所有链接,您应该确定属性 href 与节的 id 相同的那个em>。

然后您可以将.active class 添加到该链接并将其从其余链接中删除。

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

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


提高性能

即使这段代码能完成它的工作,也远不是最优的。无论如何,请记住:

我们应该忘记小的效率,比如大约 97% 的时间: 过早的优化是万恶之源。然而我们不应该通过 在关键的 3% 中增加我们的机会。 (唐纳德·高德纳

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

基本上,提高性能的三个步骤:

尽可能多地完成以前的工作:

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

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

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

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

请注意锚点选择器中的两个反斜杠:哈希“#”在 CSS 中具有特殊含义,因此 it must be escaped(感谢 @Johnnie)。

此外,您可以缓存每个部分的位置(Bootstrap 的 Scrollspy 会这样做)。但是,如果您这样做,您需要记住在每次更改时更新它们(用户调整窗口大小、通过 ajax 添加新内容、展开子部分等)。

优化事件处理程序:

想象用户在滚动一个部分:活动的导航链接不需要改变。但是,如果您查看上面的代码,您会发现实际上它更改了几次。在正确的链接被突出显示之前,之前的所有链接也会这样做(因为它们的相应部分也验证条件position >= target)。

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

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

$() 是必要的,因为 get() 返回 DOM 元素,而不是 jQuery 对象。

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

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

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

尽可能少地触发事件:

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

此时,您肯定想了解油门、去抖动和 requestAnimationFrame。 This article 是一个很好的讲座,并为您提供了关于其中三个的很好的概述。对于我们的情况,节流最适合我们的需求。

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

我在 sn-p 中实现了一个节流功能。从那里你可以变得更复杂,甚至更好,使用像 underscore.js 或 lodash 这样的库(如果你不需要整个库,你可以随时从那里提取节流函数)。

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

特殊情况:

我不会将这些案例包含在 sn-p 中,以免进一步复杂化。

在下面的 sn-p 中,当该部分到达页面的最顶部时,链接将突出显示。如果您希望它们之前突出显示,您可以通过这种方式添加一个小的偏移量:

if (position + offset >= target) 

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

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

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

考虑了一些浏览器支持问题。你可以阅读更多关于它的信息here 和here。

片段和测试

最后,这里有一个注释的 sn-p。请注意,我更改了一些变量的名称以使其更具描述性。

// 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 会测试我们讨论过的不同改进。

编码愉快!

【讨论】:

为什么 addClass 对我有效而 removeClass 无效? @FrankLucas 很难断章取义...我建议您打开一个新问题以提供更多信息。 @大卫。我试过剪断了,但最后一部分没有突出显示。 @WosleyAlarico:那只是因为我把这些部分做得很小,最后一个部分从未到达页面顶部 :) 我修改了 CSS 以使最后一个部分更高。 @David.这对于为该部分指定高度是有意义的。但是现在,如果您查看当前正在最后润色的这个网站:www.scentology.burnnotice.co.za。该部分相当大,但最后一个菜单项仍然没有突出显示。只是为了强调,我没有给每个部分设置一个特定的高度。提前感谢支持【参考方案3】:

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

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

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

【讨论】:

我不得不通过这个答案将我的更改为$('#navigation &gt; ul &gt; li &gt; a[href$=' + id + ']');:***.com/a/303961/1241287 花了一天的大部分时间试图让它工作并找到这个解决方案,谢谢!【参考方案4】:

我采用了 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));

【讨论】:

还请解释您的代码如何帮助解决 OP 的问题。 :) 变量名称上存在$ 前缀,例如$navigationLinks,通常表示它专门是一个 jQuery 变量,***.com/a/553734/292408,所以如果这个答案想要真正无 jQuery,则应该删除 $ @user1020495,为什么要反向迭代部分?这样做比正常迭代有什么重要意义【参考方案5】:
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();
);

【讨论】:

虽然此代码可能会回答问题,但提供有关 why 和/或 如何 此代码回答问题的附加上下文可提高其长期价值. 谢谢。最后尝试了一堆代码后,这个完美运行。

以上是关于向下滚动到部分时突出显示菜单项的主要内容,如果未能解决你的问题,请参考以下文章

滚动时突出显示菜单(如果到达 div)

切换 div 并突出显示当前菜单项

滚动时突出显示多个菜单锚链接

突出显示在 Android 设置应用程序的搜索结果中找到的菜单项

我们如何在单击菜单条项时突出显示活动菜单项?

子菜单链接处于活动状态时突出显示 Wordpress 父菜单项