向下滚动到截面时突出显示菜单项

Jen*_*ist 12 javascript hash jquery hyperlink sections

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

我做了一小段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');
        }
    });
});
Run Code Online (Sandbox Code Playgroud)

现在的问题是它突出显示所有哈希链接,而不仅仅是该部分与之关系的哈希链接.任何人都可以指出错误,还是我忘记了什么?

Dav*_*vid 35

编辑:

我已经修改了我的答案,谈了一些关于性能和一些特殊情况的话题.

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


原始答案

您应该识别属性href与节的id相同的那个,而不是将.active 添加到所有链接.

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

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

通过上述修改,您的代码将正确突出显示相应的链接.希望能帮助到你!


提高绩效

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

我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源.然而,我们不应该放弃那个关键的3%的机会.(唐纳德克努特)

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

基本上,有三个步骤可以改善性能:

尽可能多地进行以前的工作:

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

var $navigationLinks = $('#navigation > ul > li > a');
var $sections = $(".section"); 
Run Code Online (Sandbox Code Playgroud)

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

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

请注意锚选择器中的两个反斜杠:散列' # '在CSS中具有特殊含义,因此必须对其进行转义(感谢@Johnnie).

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

优化事件处理程序:

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

一种解决方案是迭代底部到顶部的部分,第一个.offset().top等于或小于$(window).scrollTop正确的部分.是的,您可以依赖jQuery以DOM的顺序返回对象(从版本1.3.2开始).要从下到上迭代,只需按相反顺序选择它们:

var $sections = $( $(".section").get().reverse() );
$sections.each( ... );
Run Code Online (Sandbox Code Playgroud)

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

找到正确的部分后,您应该return false退出循环并避免检查其他部分.

最后,如果已突出显示正确的导航链接,则不应执行任何操作,因此请将其检出:

if ( !$navigationLink.hasClass( 'active' ) ) {
    $navigationLinks.removeClass('active');
    $navigationLink.addClass('active');
}
Run Code Online (Sandbox Code Playgroud)

尽可能少地触发事件:

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

此时,确定要阅读有关油门,去抖动和requestAnimationFrame的信息.这篇文章是一个很好的讲座,给你一个非常好的概述,其中有三个.对于我们的情况,节流最符合我们的需求.

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

我在代码片段中实现了一个节流功能.从那里你可以得到更复杂,甚至更好,使用像underscore.jslodash这样的库(如果你不需要整个库,你总是可以从那里提取油门功能).

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

特殊情况:

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

在下面的代码段中,当该部分到达页面的最顶部时,链接将突出显示.如果您希望之前突出显示它们,则可以通过以下方式添加小偏移量:

if (position + offset >= target) {
Run Code Online (Sandbox Code Playgroud)

如果您有一个顶部导航栏,这是特别有用的.

如果您的上一部分太小而无法到达页面顶部,则当滚动条位于其最底部位置时,您可以高亮显示其相应的链接:

if ( $(window).scrollTop() >= $(document).height() - $(window).height() ) {
    // highlight the last link
Run Code Online (Sandbox Code Playgroud)

有一些浏览器支持问题的想法.你可以在这里这里阅读更多相关信息.

片段和测试

最后,这里有一个注释片段.请注意,我已更改某些变量的名称,以使其更具描述性.

// 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 );
Run Code Online (Sandbox Code Playgroud)
#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;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

如果您感兴趣,这个小提琴测试我们谈到的不同改进.

快乐的编码!


小智 7

我采用了 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));
Run Code Online (Sandbox Code Playgroud)


小智 5

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

$('#navigation > ul > li > a[href=\\#' + id + ']');
Run Code Online (Sandbox Code Playgroud)

现在我的浏览器不会在那个片段上抛出错误。