web-dev-qa-db-fra.com

Mettez en surbrillance l'élément de menu lorsque vous faites défiler la section

Je sais que cette question a été posée un million de fois sur ce forum, mais aucun des articles ne m'a aidé à trouver une solution.

J'ai créé un petit morceau de code jquery qui met en évidence le lien de hachage lorsque vous faites défiler la section avec le même identifiant que dans le lien de hachage.

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

Le problème, c’est qu’il met en évidence tous les liens de hachage au lieu de celui auquel la section est associée. Quelqu'un peut-il signaler l'erreur, ou est-ce quelque chose que j'ai oublié?

11
Jens Kvist

MODIFIER:

J'ai modifié ma réponse pour parler un peu de performance et de cas particuliers.

Si vous êtes juste ici à la recherche de code, un extrait commenté apparaît en bas.


Réponse originale

Au lieu d'ajouter le .activeclasse à tous les liens, vous devez identifier celui dont l'attribut href est identique à celui de la section id.

Ensuite, vous pouvez ajouter le .activeclasse à ce lien et le supprimer du reste.

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

Avec la modification ci-dessus, votre code mettra correctement en évidence le lien correspondant. J'espère que ça aide!


Améliorer les performances

Même lorsque ce code fera son travail, c'est loin d'être optimal. Quoi qu'il en soit, rappelez-vous: 

Nous devrions oublier les petites économies, disons environ 97% du temps: L'optimisation prématurée est la racine de tout Mal. Pourtant, nous ne devrions pas passer nos opportunités dans ce 3% critique. (Donald Knuth)

Donc, si, lors de tests d’événements sur un périphérique lent, vous ne rencontrez aucun problème de performances, le mieux que vous puissiez faire est de cesser de lire et de réfléchir à la prochaine fonctionnalité fantastique de votre projet!

Il y a, en gros, trois étapes pour améliorer les performances:

Faire le plus de travail possible:

Afin d'éviter de rechercher dans le DOM une fois de plus (à chaque fois que l'événement est déclenché), vous pouvez mettre en cache vos objets jQuery au préalable (par exemple, sur document.ready):

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

Ensuite, vous pouvez mapper chaque section sur le lien de navigation correspondant:

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

Notez les deux barres obliques inverses dans le sélecteur d'ancrage: le hachage '#' a une signification particulière dans CSS, donc il doit être échappé (merci @Johnnie ).

Vous pouvez aussi mettre en cache la position de chaque section ( Scrollspy fait-le de Bootstrap). Mais si vous le faites, vous devez vous rappeler de les mettre à jour chaque fois qu'ils changent (l'utilisateur redimensionne la fenêtre, un nouveau contenu est ajouté via ajax, une sous-section est développée, etc.).

Optimiser le gestionnaire d'événement:

Imaginez que l'utilisateur fait défiler à l'intérieur une section: le lien de navigation actif n'a pas besoin d'être modifié. Mais si vous regardez le code ci-dessus, vous verrez qu’il change plusieurs fois. Avant que le lien correct ne soit mis en surbrillance, tous les liens précédents le feront également (car leurs sections correspondantes valident également la condition position >= target).

Une solution consiste à itérer les sections du bas vers le haut, le premier dont .offset().top est égal ou inférieur à $(window).scrollTop est le bon. Et oui, vous pouvez compter sur jQuery pour retourner les objets dans l’ordre du DOM (depuis version 1.3.2 ). Pour parcourir de bas en haut, il suffit de les sélectionner dans l'ordre inverse:

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

La double $() est nécessaire car get() renvoie des éléments DOM, pas des objets jQuery.

Une fois que vous avez trouvé la bonne section, vous devez return false pour quitter la boucle et éviter de vérifier les autres sections.

Enfin, vous ne devriez rien faire si le lien de navigation correct est déjà mis en surbrillance, alors vérifiez-le:

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

Déclenche l'événement le moins possible:

Le moyen le plus sûr d'empêcher les événements de haut niveau (défilement, redimensionnement, etc.) de ralentir ou de bloquer votre site est de contrôler la fréquence à laquelle le gestionnaire d'événements est appelé: vous n'avez pas besoin de vérifier quel lien doit être mis en évidence 100 fois par seconde! Si, en plus du lien surligné, vous ajoutez un effet de parallaxe sophistiqué, vous pouvez avoir des problèmes d'introduction rapides. 

À ce stade, assurez-vous que vous souhaitez en savoir plus sur throttle, debounce et requestAnimationFrame. Cet article est une belle conférence et vous donne un très bon aperçu de trois d'entre eux. Dans notre cas, la limitation correspond mieux à nos besoins.

Fondamentalement, la régulation impose un intervalle de temps minimum entre deux exécutions de fonctions.

J'ai implémenté une fonction d'étranglement dans l'extrait de code. A partir de là, vous pouvez devenir plus sophistiqué, ou même mieux, utiliser une bibliothèque comme underscore.js ou lodash (si vous n’avez pas besoin de toute la bibliothèque, vous pouvez toujours en extraire la fonction papillon).

Remarque: si vous regardez autour de vous, vous trouverez des fonctions d'accélération plus simples. Méfiez-vous d'eux car ils peuvent rater le dernier déclencheur d'événement (et c'est le plus important!).

Cas particuliers:

Je ne vais pas inclure ces cas dans l'extrait pour ne pas le compliquer davantage.

Dans l'extrait ci-dessous, les liens seront mis en évidence lorsque la section atteindra le sommet de la page. Si vous souhaitez les mettre en évidence auparavant, vous pouvez ajouter un petit décalage de la manière suivante:

if (position + offset >= target) {

Ceci est particulièrement utile lorsque vous avez une barre de navigation supérieure.

Et si votre dernière section est trop petite pour atteindre le haut de la page, vous pouvez mettre en surbrillance le lien correspondant lorsque la barre de défilement se trouve dans sa position la plus basse:

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

Certains problèmes de support du navigateur ont été pensés. Vous pouvez en lire plus à ce sujet ici et ici .

Extrait et test

Enfin, vous avez ici un extrait commenté. Veuillez noter que j'ai changé le nom de certaines variables pour les rendre plus descriptives.

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

Et si cela vous intéresse, ce violon teste les différentes améliorations dont nous avons parlé. 

Bonne codage!

30
David

Pour ceux qui essaient d'utiliser cette solution plus récemment, je me suis heurté à un problème en essayant de la faire fonctionner. Vous devrez peut-être échapper au href comme ceci:

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

Et maintenant, mon navigateur ne jette pas d'erreur sur cette pièce.

4
Johnnie

Dans cette ligne:

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

Vous définissez en fait l'attribut href de chaque élément $ ('# navigation> ul> li> a'), puis vous ajoutez également la classe active à chacun d'entre eux. Peut-être que ce que vous devez faire est quelque chose comme:

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

Et sélectionnez uniquement celui qui correspond à l'identifiant. Avoir un sens?

0
duatis