jQueryで目次と「目次に戻る」を自動生成(プラグイン無し)

jquery-toc-without-plugins
Web制作メモ >

これまで使っていたWordPressの目次生成プラグイン「Table of Contents Plus」を外しました。

プラグイン無しで記事の見出しから目次を生成したり、「目次に戻る」を自動で表示させたりするjQueryのメモです。

目次のリストを生成する

まずはこちらを参考に、目次のリストをつくります。

ベースになるjQueryのコード

$(function(){
        var idcount = 1;
        var toc = '';
        var currentlevel = 0;
        $("article h2,article h3,article h4",this).each(function(){
            this.id = "chapter-" + idcount;
	        idcount++;
            var level = 0;
            if(this.nodeName.toLowerCase() == "h2") {
                level = 1;
            } else if(this.nodeName.toLowerCase() == "h3") {
                level = 2;
            } else if(this.nodeName.toLowerCase() == "h4") {
                level = 3;
            }
            while(currentlevel < level) {
                toc += '<ol class="chapter">';
                currentlevel++;
            }
            while(currentlevel > level) {
                toc += "</ol>";
                currentlevel--;
            }
            toc += '<li><a href="#' + this.id + '">' + $(this).html() + "</a></li>\n";
        });
        while(currentlevel > 0) {
            toc += "</ol>";
            currentlevel--;
        }
        $("#toc").html(toc);
});

補足

上から順に、かんたんに補足をいれます。

idcount,toc,currentlevelというみっつの変数に、それぞれ以下のように値を代入します。

var idcount = 1;
var toc = '';
var currentlevel = 0;

<article>内に<h2><h3><h4>があったら、それぞれfunction(){以後の処理をします。

$("article h2,article h3,article h4",this).each(function(){

ここで単にh2,h3,h4と書いてしまうと、記事以外の(ページ内全部の)h2が目次の対象になってしまいます。

例えばこのブログでは<article>内の<h2><h4>を目次にするため、上のように書きました。

参考:each(callback) – jQuery 日本語リファレンス

合致した全てのエレメントに対して関数を実行する。
これは、合致するエレメントが見つかる度に1度ずつ、毎回関数が実行されることを意味する。

つづきます。

記事内に指定した見出しが登場したら、その都度以下の処理を行います。

まず、見出しに「chapter- + 変数idcountに入ってる数値」っていうid名をつけます。

this.id = "chapter-" + idcount;

変数idcountには最初1が入っていますから、例えば最初に登場した<h2><h2 id=”chapter-1”>ってなります。

次に、変数idcountの値をひとつ増やします。

idcount++;

変数idcountの値をひとつずつ増やすことで、次に登場する見出しのid名はchapter-2、その次はchapter-3…となります。

新しい変数levelを定義します(いったん0を代入)。

var level = 0;

見出しが<h2>なら、変数levelには1を代入。

if(this.nodeName.toLowerCase() == "h2") {
    level = 1;
}

<h3>なら2<h4>なら3を代入します。

else if(this.nodeName.toLowerCase() == "h3") {
    level = 2;
} 
else if(this.nodeName.toLowerCase() == "h4") {
    level = 3;
}

以下、変数currentlevelと変数levelの値によって処理がかわります。

1. 変数currentlevelの値が変数levelより小さい間は、変数toc<ol class="chapter">っていう文字列を足し、

while(currentlevel < level) {
    toc += '<ol class="chapter">';

さらに変数currentlevelの値をひとつ増やす、という処理を続けます。

currentlevel++;}

2. 逆に、変数currentlevelの値が変数levelより大きい間は、変数toc</ol>っていう文字列を足し、

while(currentlevel > level) {
    toc += "</ol>";

さらに変数currentlevelの値をひとつ減らす、という処理を続けます。

currentlevel--;}

これで分岐はおわりです。

変数tocに「<li><a href="# + chapter- + 変数idcountに入ってる数値 + "> + その見出しのテキスト + </a></li>」って文字列を足していきます。

toc += '<li><a href="#' + this.id + '">' + $(this).html() + "</a></li>\n";

ここまでの流れがわかりづらいので、以下の例にそってもう少し補足します。

たとえば、こういう構成の記事があったとします。

<h2>ひとつめのH2</h2>
<h2>テキストテキスト</h2>
<h3>ひとつめのH3</h3>
<h2>テキストテキスト</h2>
<h2>ふたつめのH2</h2>
<h2>テキストテキスト</h2>

この場合、まず、ひとつめの<h2>ではこんな処理が行われます。

  • 変数currentlevelには最初0が入っています(最初に代入した)。
  • 変数levelには1が代入されます(<h2>だから)。
  • この時点でcurrentlevelのほうがlevelより小さいので、変数toc(最初からっぽ)の値は
    <ol class="chapter">」って文字列になります。
  • currentlevelの値をひとつ増やして、1になります。
  • さらに、変数toc
    <li><a href="# + chapter- + 変数idcountに入ってる数値 + "> + その見出しのテキスト + </a></li>
    という文字列を足します。
  • つまり、「<li><a href="#chapter-1">ひとつめのH2</a></li>」という文字列を足すので、
  • この時点での変数tocの値は、
    <ol class="chapter">
     <li><a href="#chapter-1">ひとつめのH2</a></li>

    です。

つぎに<h3>で行われる処理です。

  • 変数currentlevel1が入っています。
  • 変数level2<h3>だから)。
  • currentlevelのほうがlevelより小さいので、変数toc
    <ol class="chapter">」って文字列を足します。
  • さっきのとあわせて、変数tocの値は
    <ol class="chapter"><li><a href="#chapter-1">ひとつめのH2</a></li><ol class="chapter">」になります。
  • currentlevelの値をひとつ増やして、2になります。
  • 変数tocに、
    <li><a href="#chapter-2">ひとつめのH3</a></li>」を足して、
  • 変数tocの値は
    <ol class="chapter">
     <li><a href="#chapter-1">ひとつめのH2</a></li>
      <ol class="chapter">
       <li><a href="#chapter-2">ひとつめのH3</a></li>

    になります。

さいごの<h2>のところで行われる処理です。

  • 変数currentlevel2が入っています。
  • 変数level1<h2>だから)。
  • こんどは、 currentlevelのほうがlevelより大きいので、変数toc</ol>って文字列を足します。
  • なのでこの時点で変数tocの値は
    <ol class="chapter"><li><a href="#chapter-1">ひとつめのH2</a></li><ol class="chapter"><li><a href="#chapter-2">ひとつめのH3</a></li></ol>
  • 変数currentlevelの値がひとつ減って、1になります。
  • 変数tocに、さらに
    <li><a href="#chapter-3">ふたつめのH2</a></li>を足して、
  • 変数tocの値は
    <ol class="chapter">
     <li><a href="#chapter-1">ひとつめのH2</a></li>
      <ol class="chapter">
        <li><a href="#chapter-2">ひとつめのH3</a></li>
      </ol>
     <li><a href="#chapter-3">ふたつめのH2</a></li>

    になります。

すべての見出しに対して、処理が終わりました。

もう少しつづきます。

変数currentlevelの値が0より大きい間は、変数toc</ol>を足します。

while(currentlevel > 0) {
    toc += "</ol>";

さらに、変数currentlevelの値をひとつ減らします。

currentlevel--}

さっきのさいごの時点での変数currentlevelの値は1でしたので、

  • さっきまでのながーい変数tocに、さらに</ol>を足して、
    <ol class="chapter">
     <li><a href="#chapter-1">ひとつめのH2</a></li>
      <ol class="chapter">
        <li><a href="#chapter-2">ひとつめのH3</a></li>
      </ol>
     <li><a href="#chapter-3">ふたつめのH2</a></li>
    </ol>

    となります。
  • 変数currentlevelの値がひとつ減って、0になります。

リスト末尾後の</ol>が書かれたところで変数currentlevelの値が0になりますので、これでリスト生成は終わりです。

最後に、tocというid名がついた要素の中に、いま生成した目次のリスト(つまり、変数tocのながーい値)を挿入してね、と書いておきます。

$("#toc").html(toc);

参考:.html() | jQuery 1.9 日本語リファレンス | js STUDIO

要素内のHTMLを取得、またはエレメント内に指定したHTMLを挿入します。

これで、好きなところに目次を表示する仕組みが整いました。

目次を表示させる

記事内の、目次を表示させたいところに、 <div id="toc"></div>と書きます。

toc example 21

idがtocであれば、divじゃなくても良いです。spanでもnavでもお好みで。

これで目次が<div id="toc">~</div>内に挿入されます。

<div id="toc">
 <ol class="chapter">
  <li><a href="#chapter-1">ひとつめのH2</a></li>
   <ol class="chapter">
    <li><a href="#chapter-2">ひとつめのH3</a></li>
   </ol>
  <li><a href="#chapter-3">ふたつめのH2</a></li>
 </ol>
</div>
toc example1

「目次に戻る」も自動で表示させる

文章が長いときにあると便利な「目次に戻る」ナビゲーションを、<h2>の前に自動で表示させることにします。

<h2>の前に、<div class="back-toc">…</div>を追加します。

$('h2').before(' <div class="back-toc"><a href="#toc">もくじに戻る↑</a></div>'); 

ピンクの文字と矢印を入れてみました。ボタン風にするともっと見やすいですね。

back to toc1

こちらは、見出しの直後に小さなボタンを置くイメージ(ピンクの矢印)。

$('h2').append(' <div class="back-toc"><a href="#toc">↑</a></div>'); 
toc example 5

参考:連載:jQuery逆引きリファレンス:第4回 要素の操作&ユーティリティ編 (2/19) – @IT

before(c) コンテンツcをカレント要素の前方に追加

最初のh2の前には「目次に戻る」を表示しない

記事冒頭の<h2>の前には「目次に戻る」が無くてもいいかなと思いましたので、

id名chapter-1以外の<h2>の前に、<div class="back-toc">…</div>を追加することにします。

$('h2:not("#chapter-1")').before(' <div class="back-toc"><a href="#toc">もくじに戻る↑</a></div>'); 

おまけ 自動で目次を表示する(テンプレートファイルに直接書く)

このブログでは、テンプレートファイル(single.php)でmoreタグ(<!--more-->)の前と後を出し分けています。

なので、moreタグ直後に目次が自動で表示されるよう、テンプレートファイルに直接書き込んでしまいました。

通常、記事はこんなふうに出力しているかと思います。

single.php

<!--記事タイトルとかアイキャッチ画像とか日付とかがあって、-->

<!--これが記事本文-->
<?php the_content(); ?>

わたしは、ここを以下のように書き換えています。

single.php

<!--記事タイトルとか日付とか…-->

<!--moreタグ前の記事本文-->
<?php if(strpos(get_the_content(),'id="more-')) :
global $more; $more = 0;
the_content(''); 
?>

<!--ここにアドセンス-->
<!--ここに目次-->
<nav id="toc"></nav>

<!--moreタグの後の記事本文-->
<?php $more = 1;
the_content('', true );
else : the_content();
endif; ?>

参考:テンプレートタグ/the content – WordPress Codex 日本語版

If the_content() isn’t working as you desire (displaying the entire story when you only want the content above the Quicktag, for example) you can override the behavior with global $more.

これで、記事の中に<div id="toc"></div>って書かなくても自動で全部の記事に目次が表示されるようになりました。

おまけ 見出しがひとつも無かったら目次を表示しない

自動で目次が入るのは便利ですが、こうすると見出しがひとつもない記事にも<div id="toc"></div>が出力されてしまうようになります。

見出しがないのでもちろん目次のリストは出力されませんが、id="toc"に指定している背景色や枠線がちょっと出ちゃうので、気になります。

toc example 3

なので、無理やりですが…

見出し(<h2>)がひとつでも存在すれば、いままでと同じ処理をします。

if($("article h2")[0]) {
    $("#toc").html(toc);
    $('h2:not("#chapter-1")').before(' <div class="back-toc"><a href="#toc">もくじに戻る↑</a></div>'); 
    } 

見出し(<h2>)がなければ<div id="toc">にクラスを追加して<div id="toc" class="no-toc">と出力させます。

else {
    $('#toc').attr('class', 'no-toc');
}

これを、CSSで非表示にします。

style.css

.no-toc{
    display:none !important;
}

でも、見出しがなければそもそも目次を生成させないようにするなど、もう少しスマートな方法があるような気がします。

まとめ(コードぜんぶ)

いま、footer.php に書いているコードです。

<script>
$(function(){
    var idcount = 1;
    var toc = '';
    var currentlevel = 0;
    $("article h2,article h3,article h4",this).each(function(){
        this.id = "chapter-" + idcount;
        idcount++;
        var level = 0;
        if(this.nodeName.toLowerCase() == "h2") {
            level = 1;
        } else if(this.nodeName.toLowerCase() == "h3") {
            level = 2;
        } else if(this.nodeName.toLowerCase() == "h4") {
            level = 3;
        }
        while(currentlevel < level) {
            toc += '<ol class="chapter">';
            currentlevel++;
        }
        while(currentlevel > level) {
            toc += "</ol>";
            currentlevel--;
        }
        toc += '<li><a href="#' + this.id + '">' + $(this).html() + "</a></li>\n";
    });
    while(currentlevel > 0) {
        toc += "</ol>";
        currentlevel--;
    }
    if($("article h2")[0]) {
    $("#toc").html(toc);
    $('h2:not("#chapter-1")').before(' <div class="back-toc"><a href="#toc">もくじに戻る↑</a></div>'); 
    } 
else{
    $('#toc').attr('class', 'no-toc');
    }
});
</script>

へたっぴなコードですが、何かの参考になればうれしいです。

さいごに…今回はプラグイン無しでがんばってみましたが、これまで使っていた「Table of Contents Plus」なら簡単に目次を自動で表示してくれますし、見出しの数から表示・非表示も設定できますし(今回これが無理やりなのが心残り)、やっぱりプラグインは便利ですね。