web-dev-qa-db-ja.com

セクションまで下にスクロールするときにメニュー項目を強調表示する

この質問がこのフォーラムで100万回も寄せられたことは知っていますが、解決策を見つけるのに役立つ記事はありませんでした。

ハッシュリンクと同じIDのセクションにスクロールダウンすると、ハッシュリンクを強調表示する小さな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');
        }
    });
});

ここでの問題は、セクションが関係しているものだけでなく、すべてのハッシュリンクを強調表示することです。誰かが間違いを指摘できますか、それとも私が忘れていたものですか?

15
Jens Kvist

編集:

パフォーマンスといくつかの特定のケースについて少し話すよう​​に私の回答を変更しました。

ここでコードを探しているだけの場合は、下部にコメント付きのスニペットがあります。


元の答え

_.active_classをすべてのリンクに追加する代わりに、どの属性hrefが同じであるかを特定する必要がありますセクションのidとして。

次に、そのリンクに_.active_classを追加して、残りのリンクから削除できます。

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

上記の変更により、コードは対応するリンクを正しく強調表示します。それが役に立てば幸い!


パフォーマンスの向上

このコードが機能する場合でも、最適とは言えません。とにかく、覚えておいてください:

小さな効率については忘れてください。たとえば、97%の時間です。時期尚早な最適化がすべての悪の根源です。しかし、その重要な3%で機会を逃してはなりません。 (ドナルドクヌース

したがって、遅いデバイスでのイベントテストでパフォーマンスの問題が発生しなかった場合、最善の方法は、読み取りを停止して、プロジェクトの次のすばらしい機能について考えることです!

基本的に、パフォーマンスを改善するには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') + ']');
});
_

アンカーセレクターの2つの円記号に注意してください。ハッシュ ''はCSSで特別な意味を持っているため、 エスケープする必要があります (ありがとう @Johnnie )。

また、各セクションの位置をキャッシュすることもできます(Bootstrapの Scrollspy がそれを行います)。ただし、それを行う場合は、変更するたびに更新することを忘れないでください(ユーザーがウィンドウのサイズを変更したり、新しいコンテンツがajax経由で追加されたり、サブセクションが展開されたりするなど)。

イベントハンドラーを最適化します:

ユーザーが1つのセクション内をスクロールしていると想像してください。アクティブなナビゲーションリンクを変更する必要はありません。しかし、上のコードを見ると、実際には数回変更されていることがわかります。正しいリンクが強調表示される前に、以前のすべてのリンクでも同じことが行われます(対応するセクションも_position >= target_の条件を検証するため)。

1つの解決策は、下から上のセクションを繰り返すことです。最初の.offset().top$(window).scrollTop以下であるものが正しいセクションです。そして、そうです jQueryがDOMの順序でオブジェクトを返すことを信頼できます (- バージョン1.3.2 以降)。下から上に反復するには、逆の順序で選択します。

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

$()はjQueryオブジェクトではなくDOM要素を返すため、二重のget()が必要です。

正しいセクションが見つかったら、_return false_を実行してループを終了し、以降のセクションをチェックしないようにする必要があります。

最後に、正しいナビゲーションリンクが既に強調表示されている場合は何もしないでください。チェックしてください。

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

イベントをできるだけ少なくトリガーします:

評価の高いイベント(スクロール、サイズ変更...)によってサイトが遅くなったり応答しなくなったりするのを防ぐ最も確実な方法は、イベントハンドラーの呼び出し頻度を制御することです。どのリンクを強調表示する必要があるかを確認する必要がないことを確認してください毎秒100回!リンクの強調表示のほかに、派手な視差効果を追加すると、イントロの問題をすばやく実行できます。

この時点で、スロットル、デバウンス、およびrequestAnimationFrameについて必ず読んでください。 この記事 は素晴らしい講義であり、そのうちの3つについて非常に優れた概要を説明します。私たちの場合、スロットルは私たちのニーズに最適です。

基本的に、スロットルは、2つの関数の実行間の最小時間間隔を強制します。

スニペットにスロットル機能を実装しました。そこから、より洗練された、またはさらに優れた nderscore.js または lodash などのライブラリを使用できます(ライブラリ全体が必要ない場合は、いつでもから抽出できます)そこでスロットル機能)。

注:見回すと、より単純なスロットル機能が見つかります。それらは最後のイベントトリガーを見逃す可能性があるので注意してください(そしてそれが最も重要なものです!)。

特定の場合:

これらのケースはスニペットに含めないでください。これ以上複雑にすることはありません。

以下のスニペットでは、セクションがページの最上部に到達すると、リンクが強調表示されます。前にそれらを強調表示したい場合は、次の方法で小さなオフセットを追加できます。

_if (position + offset >= target) {
_

これは、トップナビゲーションバーがある場合に便利です。

最後のセクションが小さすぎてページの上部に到達できない場合は、スクロールバーが一番下にあるときに対応するリンクを強調表示できます。

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

考えられるブラウザサポートの問題がいくつかあります。詳細は here および 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 は私たちが話し合ったさまざまな改善をテストします。

幸せなコーディング!

33
David

最近このソリューションを使用しようとしている人のために、私はそれを機能させるために思わぬ障害にぶつかりました。次のようにhrefをエスケープする必要があるかもしれません:

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

そして今私のブラウザはその部分でエラーをスローしません。

4
Johnnie
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();
});
0
Sean Pelser

私は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));
0
user1020495

この行では:

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

実際には、すべての$( '#navigation> ul> li> a')要素のhref属性を設定してから、それらすべてにアクティブクラスを追加します。あなたがする必要があるのは次のようなものです:

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

そして、hrefがidと一致するaのみを選択します。理にかなっていますか?

0
duatis