JavaScriptでページコンテンツに目次を挿入する方法
ワードプレスのコンテンツページにおいて、目次を出力するのに「Table of Contents Plus」などを使っている人が多くいらっしゃると思いますが、目次って実は結構簡単に作成出来ちゃいます。
JavaScriptで生成するメリットとしては、場所を選ばずに差込む形で生成することができるということですので、別にワードプレスでなくても有用な筈です。
例えば、僕が作成するテーマやプラグインでは、ウィジェットで目次を差込む形にしており、コンテンツ内にもウィジェットエリアを生成しているので、サイト運営者の任意の場所に目次を設置することができるようになっています。
ちょうどこの下に生成されていると思いますが、実はパソコンでご覧の場合は右のスライド、モバイルの場合は右下のウィジェットをクリックしてみてください。
それぞれウィジェットとして目次が現れ、項目をクリックするとその見出しに移動できると思います。
ウィジェットではラッパーだけを出力
PHP側での処理はラッパーとなる空のHTML、あるいはデータを持つHTMLを出力するだけです。
子テーマなどを使用して直接テンプレートファイルに書き込んでも問題ありません。
ただ、JavaScriptで探しやすいようにクラスを与えるなどの処理だけはしておきましょう。
<div class="custom-toc"></div>
こんな感じで大丈夫です。
他に条件に応じたオプションを加えたい場合はデータをJSONにしてオプションを与えておくのも手です。
<div class="custom-toc" data-toc="{optionOne:1}"></div>
という感じですが、属性値はエスケープしておきましょう。
PHPだと、
$option = array(
'optionOne' => 1
);
echo '<div class="custom-toc" data-toc="' . json_encode( $option ) . '"></div>';
のようにして出力しておきましょう。
JavaScriptで目次を生成
作成方法として簡単に説明すると、
- 目次に使用するターゲット要素(主に見出しタグH1~H6がターゲットにされます)のインナーを、アンカーリンクに使用するIDを持ったSPANタグで囲う。
- 1と同時に見出し毎にレベル分けして、ターゲット要素に向かうアンカーリンクタグを生成
- 前の項目で出力した目次用のラッパーHTML内に、生成したアンカーリンクリスト(目次)を挿入
といった処理の流れです。
var idcount = 1;
var toc = '';
var tocListItems = '';
var currentlevel = 0;
var id;
// ターゲット要素
var targetSelectors = ".post-content h1, .post-content h2, .post-content h3, .post-content h4, .post-content h5, .post-content h6";
// ターゲット要素を取得して、見出し毎にレベル分けしてアンカーリンクを生成
document.querySelectorAll( targetSelectors ).forEach( function( headline, index ) {
id = "headline_" + idcount;
if ( 'undefined' === typeof headline.children.id ) {
//headline.innerHTML = '<span id="' + id + '">' + headline.innerHTML + '</span>';
headline.id = id;
}
idcount++;
var level = 0;
if( headline.nodeName.toLowerCase() == "h2" ) {
level = 1;
} else if( headline.nodeName.toLowerCase() == "h3" ) {
level = 2;
} else if( headline.nodeName.toLowerCase() == "h4" ) {
level = 3;
} else if( headline.nodeName.toLowerCase() == "h5" ) {
level = 4;
} else if( headline.nodeName.toLowerCase() == "h6" ) {
level = 5;
}
while( currentlevel < level ) {
tocListItems += '<li class="has-children"><ul class="toc-sub-menu">';
currentlevel++;
}
while( currentlevel > level ) {
tocListItems += '</ul></li>';
currentlevel--;
}
tocListItems += '<li><a href="javascript: void(0);" data-location-id="' + id + '">' + document.getElementById( id ).innerText + "<\/a><\/li>\n";
});
// 目次のラッパーに生成したアンカーリンクのリストを挿入
document.querySelectorAll( '.custom-toc' ).forEach( function( el, i ) {
if( '' !== tocListItems ) {
toc = '<div class="toc-inner"><ul class="toc-menu">' + tocListItems + '</ul></div>';
} else {
el.remove();
}
el.innerHTML += toc;
el.querySelectorAll( 'li' ).forEach( function( listItemEl, listItemIndex ) {
var children = listItemEl.querySelector( '.toc-sub-menu' );
if ( null !== children && ! listItemEl.classList.contains( 'has-children' ) ) {
listItemEl.classList.add( 'has-children' );
}
});
});
これでクラス「custom-toc」を持つラッパー内にアンカーリンクのリストが生成された筈です。
見出しへの移動
前の項目で生成したアンカーリンクだけでは、クリックした際に画面がパッと切り替わるだけとなりますので、クリックイベントに一手間加えてあげます。
セレクタで目次のリンクタグを指定し、イベント処理の内容に以下のようなスクロール動作を加えます。
window.scrollTo({
top: offset,
behavior: "smooth"
});
jQueryでも何でもいいです。
offsetは見出しタグなどのターゲットの位置のy座標です。
このサイトでも同様の目次が使用されていますので、デモとしてご覧ください。
最後にデメリット
当然JavaScriptが必要となりますので、有効でない場合は生成されません。
また、SEO的にどう反映されるかが疑問となります。
アンカーリンクが検索結果に反映されていることもあるようですが、JavaScriptで生成される場合、ドキュメントがロードされた時点では、或いは目次生成のJavaScriptが読まれるまでは、見出しにIDが割り振られていないことになります。
つまり、仮にアンカーリンクが検索結果に反映されていたとしても、指定した見出しにうまくジャンプできない可能性が高いと思いますので、コンテンツ内に出力する場合は、PHPで生成した方が良いかもしれませんが、毎回フロントエンドでコンテンツを読ませて目次を生成することを考えると、バックエンドでコンテンツ保存時に一緒に生成しておくのがベターなのかもしれません。