Выделить пункт меню при прокрутке вниз до раздела

Я знаю, что этот вопрос задавался миллион раз на этом форуме, но ни одна из статей не помогла мне достичь решения.

Я сделал небольшой фрагмент кода jquery, который выделяет хэш-ссылку при прокрутке вниз до раздела с тем же идентификатором, что и в хэш-ссылке.

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

проблема теперь в том, что он выделяет все хэш-ссылки, а не только тот, к которому имеет отношение раздел. Может кто-нибудь указать на ошибку, или это что-то, что я забыл?

3 ответов


EDIT:

я изменил свой ответ, чтобы немного поговорить о производительности и некоторых конкретных случаях.

если вы здесь просто ищете код, внизу есть прокомментированный фрагмент.


оригинальный ответ

вместо добавления .active класс для всех ссылок вы должны определить, какой атрибут href это то же самое, что и в разделе id.

затем вы можете добавить .active класс к этой ссылке и удалите ее из остальных.

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

С вышеуказанной модификацией ваш код правильно выделит соответствующую ссылку. Надеюсь, это поможет!


повышение производительности

даже когда этот код выполнит свою работу, он далек от оптимального. Во всяком случае, помните:

мы должны забыть о небольшой эффективности, скажем, около 97% времени: преждевременная оптимизация-корень всех зол. Но мы не должны пройти. увеличим наши возможности на 3%. (Дональд Кнут)

так что если, тестирование событий в медленном устройстве, вы не испытываете никаких проблем с производительностью, лучшее, что вы можете сделать, это прекратить чтение и подумать о следующей удивительной функции для вашего проекта!

есть, в основном, три шага для повышения производительности:

сделать как можно больше предыдущей работы:

чтобы избежать поиска DOM снова и снова (каждый раз, когда событие запускается), вы можете кэшировать объекты jQuery заранее (например, on 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 so он должен быть экранированы (спасибо @Johnnie).

кроме того, вы можете кэшировать позицию каждого раздела (Bootstrap Scrollspy это делает). Но, если вы это сделаете, вам нужно не забывать обновлять их каждый раз, когда они меняются (пользователь изменяет размер окна, новый контент добавляется через ajax, подраздел расширяется и т. д.).

оптимизация обработчика событий:

представьте, что пользователь прокручивает внутри один раздел: активный навигационная ссылка не нуждается в изменении. Но если вы посмотрите на код выше, вы увидите, что на самом деле он меняется несколько раз. Прежде чем правильная ссылка будет выделена, все предыдущие ссылки также сделают это (потому что их соответствующие разделы также проверяют условие position >= target).

одним из решений является итерация разделов снизу вверх, первый из которых .offset().top равна или меньше $(window).scrollTop является правильным. И да,вы можете положиться на jQuery возвращает объекты в порядке DOM (поскольку версия 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 раз в секунду! Если, помимо подсветки ссылок, вы добавите какой-то причудливый эффект параллакса, вы можете запустить быстрое интро неприятностей.

на данный момент, конечно, вы хотите прочитать о дроссельной заслонке, debounce и requestAnimationFrame. в этой статье хорошая лекция и дать вам очень хорошее представление о трех из них. В нашем случае дросселирование лучше всего соответствует нашим потребностям.

в принципе, дросселирование обеспечивает минимальный интервал времени между двумя исполнениями функций.

я реализовал функцию дроссельной заслонки в фрагменте. Оттуда вы можете получить больше сложные, или даже лучше, использовать библиотеку как подчеркивания.js или лодашь (если вам не нужна вся библиотека вы всегда можете добывать оттуда газ функция).

Примечание: Если вы посмотрите вокруг, вы найдете более простые функции дросселя. Остерегайтесь их, потому что они могут пропустить последний триггер события (и это самое важное!).

частные случаи:

я не буду включать эти дела в фрагмент, чтобы не усложнять это еще больше.

в приведенном ниже фрагменте ссылки будут выделены, когда раздел достигнет самой верхней части страницы. Если вы хотите, чтобы они были выделены раньше, вы можете добавить небольшое смещение таким образом:

if (position + offset >= target) {

это особенно полезно, когда у вас есть верхняя панель навигации.

и если ваш последний раздел слишком мал, чтобы достичь верхней части страницы, вы можете высветить соответствующую ссылку, когда полоса прокрутки находится в самая нижняя позиция:

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

есть некоторые проблемы с поддержкой браузера. Вы можете прочитать больше об этом здесь и здесь.

фрагмент и тест

наконец, здесь у вас есть прокомментированный фрагмент. Обратите внимание, что я изменил название некоторых переменных, чтобы сделать их более понятными.

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

и в случае, если вы заинтересованы, этот скрипка проверяет различные улучшения, о которых мы говорили.

удачи в кодировании!


для тех, кто пытается использовать это решение в последнее время, я нажал на загвоздку, пытаясь заставить его работать. Возможно, вам придется бежать от href так:

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

и теперь мой браузер не выдает ошибку на этой части.


в этой строке:

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

вы фактически устанавливаете атрибут href каждого элемента $('#navigation > ul > li > a'), а затем добавляете активный класс также ко всем из них. Может быть, вам нужно сделать что-то вроде:

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

и выберите только a, который href соответствует идентификатору. Смысл?