【jQuery】スクロールに追尾してくれる便利な目次を作る【WordPress】

最終更新日:

前回、プラグインを使わずに投稿の目次を自動挿入する機能の作り方を紹介しました。

【プラグイン不要】めっちゃ見やすい目次を自動挿入する方法【コピペで実装】

今回はこの記事の続き的な感じで、スクロールについてきてくれる便利な目次をささって実装していきたいと思います。例によって、jQueryをフル活用して実装します!

機能の整理

コードを書き出す前に、今回実装しようと考えている機能を整理してみましょう。 プログラミング全般に言えることかもしれませんが、コードを書く前にある程度紙などに機能の設計図のようなものを書いて、頭の中で整理してからコーディングを開始しましょう。

行き当たりばったりにコーディングを行うと、途中で何を実装したかったのか忘れてしまったり、当初予定していた機能を組み込み忘れたりしてしまいます。

スクロールと連動して見出しを上の方に表示する

まず第一に考えている機能はスクロールと連動し、現在表示されている段落の見出しを上の方に自動で表示してくれる機能です。 この機能を実装することで、見出しがビューポート(ブラウザの表示領域)から外れてしまってなんの見出しなのかわからなくなってしまっている状況でも、上を見るだけですぐに把握することができるようになります。

幅はともかく、ビューポートの高さについてはスマホもパソコンもそこまで高くはないので、こういった機能があると便利でしょう。

目次展開機能

あとは、上の「目次」というボタンを押すことでその場で目次フィールドを展開できるようにしましょう。言葉だけで説明するのは難しいですが、とりあえず私の頭の中では構想ができました。

スクロール連動機能を実装

それでは早速、一つ目の機能を実装します。とその前に、以下の記事を参照して自動目次生成機能を作っておいてくださいね~

【プラグイン不要】めっちゃ見やすい目次を自動挿入する方法【コピペで実装】

目次が見えなくなったタイミングで目次を画面に固定する

ページを読み込んだ時、目次は確実にビューポートの中にあるので上に表示する必要はありません。そして記事を読み進めるために下にスクロールしていくと、しだいに目次は見えなくなっていきます。完全に見えなくなったタイミングで目次を画面に固定するようにしてみましょう。

jQuery(function(){
    const main_content = jQuery("div.entry-content"); //記事本文を覆っている要素を取得
    const h2_h3_list = main_content.find("h2,h3"); //h2とh3要素を取得する
    var index_DOM = ""; //目次のHTML

    h2_h3_list.each(function(){
        const tag_name = jQuery(this).prop("tagName"); //H2やH3など、タグの種類を取得する
        const text = jQuery(this).text(); //見出しのテキストを取得

        index_DOM += `<li class="${tag_name}"><a href="#">${text}</a></li>`; //この見出しのli要素を作成
    });

    jQuery("div#post_index ul").html(index_DOM); //HTML出力

    jQuery("div#post_index ul li a").on('click',function(){
        const index = jQuery("div#post_index ul li a").index(this); //クリックされたのが目次の何番目の要素か
        const that_y = h2_h3_list.eq(index).offset().top; //クリックされた見出しの記事内でのy座標

        jQuery("body,html").animate({scrollTop:that_y - 50},1000); //見出しまで滑らかに移動

        return false;
    })

//ここから追加したコード

    const index_field = jQuery("div#post_index");
    const fix_start_y = index_field.offset().top + index_field.height(); //目次の固定を始めるy座標(目次の上の座標+目次の高さ)

    jQuery(window).scroll(function () {//スクロールした時
        const current_y = jQuery(this).scrollTop();//現在のスクロール座標
        if(current_y > fix_start_y){//目次が見えなくなっていたら
            index_field.css({position:'fixed',top:'-30px',width:String(main_content.width() - 60) + "px"}); //目次を固定
        }else{
            index_field.css({position:'relative',top:'0',width:'auto'}); //固定を解除
        }       

    });
});

長くてすみません。これは前回の記事で紹介した目次を自動で挿入する機能まで含まれています。追加したコードは「ここから追加したコード」以降になります。 これを記述してページを表示してみると、スクロールをしたときにしっかりと所定の位置で固定されるようになります。

※目次のdiv要素のz-indexを10とかに設定しないと、他の要素が重なってしまって目次が見えなくなることがあります。

目次をコンパクトにする

このままだと目次が大きすぎるので、目次をコンパクトにしていきましょう。

jQuery(function(){
    const main_content = jQuery("div.entry-content"); //記事本文を覆っている要素を取得
    const h2_h3_list = main_content.find("h2,h3"); //h2とh3要素を取得する
    var index_DOM = ""; //目次のHTML

    h2_h3_list.each(function(){
        const tag_name = jQuery(this).prop("tagName"); //H2やH3など、タグの種類を取得する
        const text = jQuery(this).text(); //見出しのテキストを取得

        index_DOM += `<li class="${tag_name}"><a href="#">${text}</a></li>`; //この見出しのli要素を作成
    });

    jQuery("div#post_index ul").html(index_DOM); //HTML出力

    jQuery("div#post_index ul li a").on('click',function(){
        const index = jQuery("div#post_index ul li a").index(this); //クリックされたのが目次の何番目の要素か
        const that_y = h2_h3_list.eq(index).offset().top; //クリックされた見出しの記事内でのy座標

        jQuery("body,html").animate({scrollTop:that_y - 50},1000); //見出しまで滑らかに移動

        return false;
    })

    const index_field = jQuery("div#post_index");
    const fix_start_y = index_field.offset().top + index_field.height(); //目次の固定を始めるy座標(目次の上の座標+目次の高さ)

    jQuery(window).scroll(function () {//スクロールした時
        const current_y = jQuery(this).scrollTop();//現在のスクロール座標
        if(current_y > fix_start_y){//目次が見えなくなっていたら
            index_field.css({
                position:'fixed',
                top:'-30px',
                width:String(main_content.width() - 60) + "px",
                height:'40px'
            }) //目次を固定
            .find("h2").css({
                width:'50px',
                display:'inline-block',
                verticalAlign:'top'
            })
            .parent().find("ul").css({
                display:'inline-block',
                width:'calc(100% - 60px)',
                height:'40px'
            })

        }else{
            index_field.css({
                position:'relative',
                top:'0',
                width:'auto',
                height:'270px'
            }) //固定を解除
            .find("h2").css({
                width:'auto',
                display:'block'
            })
            .parent().find("ul").css({
                display:'block',
                width:'auto',
                height:'220px'
            })            

        }       

    });
});

全体としてはめちゃくちゃ長いコードになってしまいましたが、何とか実装できました。

それではここから、現在表示している段落の見出しが上に出るようにスクリプトを書きます。

jQuery(function(){
    const main_content = jQuery("div.entry-content"); //記事本文を覆っている要素を取得
    const h2_h3_list = main_content.find("h2,h3"); //h2とh3要素を取得する
    var index_DOM = ""; //目次のHTML

    h2_h3_list.each(function(){
        const tag_name = jQuery(this).prop("tagName"); //H2やH3など、タグの種類を取得する
        const text = jQuery(this).text(); //見出しのテキストを取得

        index_DOM += `<li class="${tag_name}"><a href="#">${text}</a></li>`; //この見出しのli要素を作成
    });

    jQuery("div#post_index ul").html(index_DOM); //HTML出力

    jQuery("div#post_index ul li a").on('click',function(){
        const index = jQuery("div#post_index ul li a").index(this); //クリックされたのが目次の何番目の要素か
        const that_y = h2_h3_list.eq(index).offset().top; //クリックされた見出しの記事内でのy座標

        jQuery("body,html").animate({scrollTop:that_y - 50},1000); //見出しまで滑らかに移動

        return false;
    })

    const index_field = jQuery("div#post_index");
    const fix_start_y = index_field.offset().top + index_field.height(); //目次の固定を始めるy座標(目次の上の座標+目次の高さ)

    jQuery(window).scroll(function () {//スクロールした時
        const current_y = jQuery(this).scrollTop();//現在のスクロール座標
        const vh = jQuery(this).height();//ビューポートの高さ

        if(current_y > fix_start_y){//目次が見えなくなっていたら
            index_field.css({
                position:'fixed',
                top:'-30px',
                width:String(main_content.width() - 60) + "px",
                height:'40px'
            }) //目次を固定
            .find("h2").css({
                width:'50px',
                display:'inline-block',
                verticalAlign:'top'
            })
            .parent().find("ul").css({
                display:'inline-block',
                width:'calc(100% - 60px)',
                height:'40px'
            });

            //現在の段落の見出しを表示する
            const standard_y = current_y + vh / 2;//画面の真ん中の位置を基準にする
            var current_index = 0;

            h2_h3_list.each(function(index){//記事内のh2,h3のリストそれぞれについて
                const this_y = jQuery(this).offset().top; //この見出しのy座標
                if(standard_y > this_y) current_index = index; //この見出しの位置を上回っていたら
            });

            const current_index_y = index_field.find("ul li").eq(current_index).position().top - index_field.find("ul li").eq(0).position().top; //その見出しの、目次リスト内での位置を取得
            index_field.find("ul li a").css({color:'black'}); //一度すべて黒くする
            index_field.find("ul li a").eq(current_index).css({color:'#f40000'}); //見出しの色を赤くする
            index_field.find("ul").scrollTop(current_index_y); //目次をスクロールしてその見出しを見せる

        }else{
            index_field.css({
                position:'relative',
                top:'0',
                width:'auto',
                height:'270px'
            }) //固定を解除
            .find("h2").css({
                width:'auto',
                display:'block'
            })
            .parent().find("ul").css({
                display:'block',
                width:'auto',
                height:'220px'
            });            

        }       

    });
});

こんな感じですね。スクロールするごとにすべての見出しの位置と現在のスクロール位置を比較して、適切な見出しを上に表示させています。

スクロールするごとに処理してたら重たくなるんじゃないのって思うかもしれませんが、案外これくらいの処理だったら負荷はかからないものです。

目次展開機能を実装

さてラストスパートです。ここまできたらあとは目次展開機能を実装してみましょう。

jQuery(function(){
    const main_content = jQuery("div.entry-content"); //記事本文を覆っている要素を取得
    const h2_h3_list = main_content.find("h2,h3"); //h2とh3要素を取得する
    var index_DOM = ""; //目次のHTML

    h2_h3_list.each(function(){
        const tag_name = jQuery(this).prop("tagName"); //H2やH3など、タグの種類を取得する
        const text = jQuery(this).text(); //見出しのテキストを取得

        index_DOM += `<li class="${tag_name}"><a href="#">${text}</a></li>`; //この見出しのli要素を作成
    });

    jQuery("div#post_index ul").html(index_DOM); //HTML出力

    jQuery("div#post_index ul li a").on('click',function(){
        const index = jQuery("div#post_index ul li a").index(this); //クリックされたのが目次の何番目の要素か
        const that_y = h2_h3_list.eq(index).offset().top; //クリックされた見出しの記事内でのy座標

        jQuery("body,html").animate({scrollTop:that_y - 50},1000); //見出しまで滑らかに移動

        return false;
    })

    const index_field = jQuery("div#post_index");
    const fix_start_y = index_field.offset().top + index_field.height(); //目次の固定を始めるy座標(目次の上の座標+目次の高さ)

    jQuery(window).scroll(function () {//スクロールした時
        const current_y = jQuery(this).scrollTop();//現在のスクロール座標
        const vh = jQuery(this).height();//ビューポートの高さ

        if(current_y > fix_start_y){//目次が見えなくなっていたら
            index_field.css({
                position:'fixed',
                top:'-30px',
                width:String(main_content.width() - 60) + "px",
                height:'40px'
            }) //目次を固定
            .find("h2").css({
                width:'50px',
                display:'inline-block',
                verticalAlign:'top'
            })
            .parent().find("ul").css({
                display:'inline-block',
                width:'calc(100% - 60px)',
                height:'40px'
            });

            //現在の段落の見出しを表示する
            const standard_y = current_y + vh / 2;//画面の真ん中の位置を基準にする
            var current_index = 0;

            h2_h3_list.each(function(index){//記事内のh2,h3のリストそれぞれについて
                const this_y = jQuery(this).offset().top; //この見出しのy座標
                if(standard_y > this_y) current_index = index; //この見出しの位置を上回っていたら
            });

            const current_index_y = index_field.find("ul li").eq(current_index).position().top - index_field.find("ul li").eq(0).position().top; //その見出しの、目次リスト内での位置を取得
            index_field.find("ul li a").css({color:'black'}); //一度すべて黒くする
            index_field.find("ul li a").eq(current_index).css({color:'#f40000'}); //見出しの色を赤くする
            index_field.find("ul").scrollTop(current_index_y); //目次をスクロールしてその見出しを見せる

        }else{
            index_field.css({
                position:'relative',
                top:'0',
                width:'auto',
                height:'270px'
            }) //固定を解除
            .find("h2").css({
                width:'auto',
                display:'block'
            })
            .parent().find("ul").css({
                display:'block',
                width:'auto',
                height:'220px'
            });      
            index_field.find("ul li a").css({color:'black'}); //すべて黒くする      

        }       

    });

    index_field.find("h2").on('click',function(){
        index_field.css({
            height:'270px'
        })
        .find("h2").css({
            width:'auto',
            display:'block'
        })
        .parent().find("ul").css({
            display:'block',
            width:'100%',
            height:'220px'
        });  
    });
});

といっても、先ほどのスクロール関数の下にこのような記述をするだけです。目次の「目次」という文字列をクリックされたときにその場で目次を展開するような関数を定義している感じですね。

あとは、目次のdivやul要素のCSSに「transition:.5s;」とかを追記したりすれば、滑らかに目次の開閉ができるようになります。

また、このままだとスクロールして見出しが上に固定されて小さくなるタイミングで記事のHTMLがガクっと動いてしまうので、目次のdivをさらにdivで囲って、そちらに「height:330px;」を適用すれば、ずれなくなります。

<div style="height:330px;"><div id="post_index"><h2>目次</h2><ul></ul></div></div>

このような感じですね。それでは今回はこの辺で終わりたいと思います。

次はサイドバー関連の便利機能を紹介したいと思います。


関連記事

    人気記事

    じゅんき
    BableTech再整備中です10月頃にまた本格的に始動する見込みです。ちなみに管理人20歳の情報系大学生です。忙しいです(泣)

    記事内用語

    詳細ページ