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

これまで使っていた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>
を目次にするため、上のように書きました。
合致した全てのエレメントに対して関数を実行する。
これは、合致するエレメントが見つかる度に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>
で行われる処理です。
- 変数
currentlevel
は1
が入っています。 - 変数
level
は2
(<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>
のところで行われる処理です。
- 変数
currentlevel
は2
が入っています。 - 変数
level
は1
(<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>
と書きます。

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>

「目次に戻る」も自動で表示させる
文章が長いときにあると便利な「目次に戻る」ナビゲーションを、<h2>
の前に自動で表示させることにします。
<h2>
の前に、<div class="back-toc">…</div>
を追加します。
$('h2').before(' <div class="back-toc"><a href="#toc">もくじに戻る↑</a></div>');
ピンクの文字と矢印を入れてみました。ボタン風にするともっと見やすいですね。

こちらは、見出しの直後に小さなボタンを置くイメージ(ピンクの矢印)。
$('h2').append(' <div class="back-toc"><a href="#toc">↑</a></div>');

参考:連載: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"
に指定している背景色や枠線がちょっと出ちゃうので、気になります。

なので、無理やりですが…
見出し(<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」なら簡単に目次を自動で表示してくれますし、見出しの数から表示・非表示も設定できますし(今回これが無理やりなのが心残り)、やっぱりプラグインは便利ですね。