自動生成目次のオプション機能を追加してみました

2021/06/30
3
repair
画像:Business vector created by rawpixel.com - www.freepik.com

自動生成目次を改造し、あったら良いな~と思っていたオプション機能をいくつか追加してみました。改造させて頂いたのはこちらのスケ郎さん作の目次です。

[Blogger] 目次を簡単に自動生成(忙しい人向けのコピペ素材)

プログラミング関連の備忘録。AI関連APIの解説等を行っています。 少しでも誰かの役に立てれれば嬉しいです。

では、早速ご紹介していきます。

特徴

  • 基本的な機能はそのままです
  • 特定の投稿のみ目次を非表示にできます
  • 目次をサイドバーに表示できます
  • スクロールに応じて見出しをハイライトできます

まず、基本的な機能はほとんどいじっていません。オプション等、もとのスケ郎さんの目次プラグインと全く同じ要領で使えますのでご安心ください。

導入方法

導入方法は通常の目次プラグインと全く同じです。

テーマのバックアップ

まず、バックアップを取っておきましょう。

管理画面から「テーマ」→「バックアップ」を選択し、ダウンロードしてファイルを保存します。

backup

HTMLの編集

「HTMLを編集」をクリックし、編集画面を開きます。

</head>の直前に以下のコードを設置してください。

<!-- [START] 目次作成プラグイン-->
<b:if cond='data:blog.pageType == "item"'>
  <script>
    //以下のオプションを好みに合わせて変更して下さい
    //オプションの詳しい説明は、(https://www.sukerou.com/2018/10/blogger-table-of-contents-javascript.html)を参照
    var toc_options = {
      target: ["h2", "h3", "h4"],
      autoNumber: true,
      condTargetCount: 2,
      insertPosition: "firstHeadBefore",
      showToc: true,
      width: "auto",
      marginTop: "20px",
      marginBottom: "20px",
      indent: "20px",
      postBodySelector: ".widget.Blog",
      ignoreURL: [],
      copyToSidebar: true,
      sidebarSelector: "#サイドバー",
      highlight: true
    };
    
    //これ以降のソースは編集しないでください
    (function(i){var j=0;var displayToc=!0;toc_options.ignoreURL.forEach(function(url){if(location.href.match(url)){displayToc=!1}});if(displayToc){document.addEventListener(&quot;DOMContentLoaded&quot;,function(){var p=document.querySelector(toc_options.postBodySelector);if(p==null||typeof p===&quot;undefined&quot;){return} if(toc_options.target.length==0){return} rootContent=h(toc_options,p);if(rootContent.children.length&gt;=toc_options.condTargetCount){var q=c(rootContent);o(q)} if(toc_options.copyToSidebar){var toc=document.querySelector('.b-toc-container');var sidebar=document.querySelector(toc_options.sidebarSelector);if(toc!=null&amp;&amp;sidebar!=null){var cloneToc=toc.cloneNode(!0);cloneToc.classList.add('side-toc');var a=cloneToc.querySelector(&quot;a&quot;);var toggle=function(){var f=e(cloneToc,&quot;hide&quot;);if(f){a.innerText=&quot;非表示&quot;;cloneToc.classList.remove(&quot;hide&quot;)}else{a.innerText=&quot;表示&quot;;cloneToc.classList.add(&quot;hide&quot;)}};a.addEventListener(&quot;click&quot;,toggle);sidebar.appendChild(cloneToc);if(toc_options.highlight){highlight(cloneToc)}}}})} function h(q,p){var u=q.target.length;var t=function(E,D,w){var z=q.target[E];var x=E&lt;u-1?q.target[E+1]:&quot;&quot;;var y=&quot;toc_headline_&quot;+(++j);var F=g(z,m(D),E+1,y);w.children.push(F);D.id=y;var A=f(D);if(x==&quot;&quot;){return} while(!0){if(A==null||typeof A===&quot;undefined&quot;){break} if(b(A)==z){break} if(b(A)==x){t(E+1,A,F)}else{var B=A.getElementsByTagName(x);for(var C=0;C&lt;B.length;C++){t(E+1,B[C],F)}} var A=f(A)}};var r=g(&quot;ROOT&quot;,&quot;&quot;,0);var v=p.getElementsByTagName(q.target[0]);for(var s=0;s&lt;v.length;s++){t(0,v[s],r,&quot;&quot;)} return r} function c(s){var r=document.createElement(&quot;div&quot;);r.classList.add(&quot;b-toc-container&quot;);r.style.marginTop=toc_options.marginTop;r.style.marginBottom=toc_options.marginTop;if(toc_options.width==&quot;100%&quot;){r.style.display=&quot;block&quot;}else{r.style.width=toc_options.width} var q=document.createElement(&quot;p&quot;);var w=document.createElement(&quot;span&quot;);var v=document.createElement(&quot;span&quot;);var u=document.createElement(&quot;span&quot;);v.classList.add(&quot;b-toc-show-wrap&quot;);u.classList.add(&quot;b-toc-show-wrap&quot;);var y=document.createElement(&quot;a&quot;);w.innerText=&quot;目次&quot;;v.innerText=&quot;[&quot;;u.innerText=&quot;]&quot;;y.href=&quot;javascript:void(0);&quot;;q.appendChild(w);q.appendChild(v);q.appendChild(y);q.appendChild(u);var t=function(z){var p=typeof z===&quot;boolean&quot;?z:e(r,&quot;hide&quot;);if(p){y.innerText=&quot;非表示&quot;;r.classList.remove(&quot;hide&quot;)}else{y.innerText=&quot;表示&quot;;r.classList.add(&quot;hide&quot;)}};y.addEventListener(&quot;click&quot;,t);t(toc_options.showToc);var x=document.createElement(&quot;ul&quot;);x.classList.add(&quot;toc-root-list&quot;);s.children.forEach(function(z,p){n(x,z,(p+1)+&quot;&quot;)});r.appendChild(q);r.appendChild(x);return r} function n(s,u,w){var p=document.createElement(&quot;li&quot;);p.classList.add(&quot;toc-list-item&quot;);var q=document.createElement(&quot;a&quot;);p.style.paddingLeft=toc_options.indent;s.style.paddingLeft=0;q.href=&quot;#&quot;+u.id;if(toc_options.autoNumber){var t=document.createElement(&quot;span&quot;);t.classList.add(&quot;toc-number&quot;);t.innerText=w+&quot;.&quot;} var v=document.createElement(&quot;span&quot;);v.classList.add(&quot;toc-text&quot;);v.innerText=u.text;if(toc_options.autoNumber){q.appendChild(t)} q.appendChild(v);p.appendChild(q);s.appendChild(p);if(u.children.length&gt;0){var r=document.createElement(&quot;ul&quot;);r.classList.add(&quot;toc-sub-list&quot;);p.appendChild(r);u.children.forEach(function(y,x){n(r,y,w+&quot;.&quot;+(x+1))})}} function o(q){var r=null;var p=document.querySelector(toc_options.postBodySelector);if(toc_options.insertPosition==&quot;firstHeadBefore&quot;||toc_options.insertPosition==&quot;firstHeadAfter&quot;){r=p.querySelector(toc_options.target[0])}else{if(toc_options.insertPosition==&quot;top&quot;){r=p}} if(r==null){return} if(toc_options.insertPosition==&quot;firstHeadBefore&quot;){k(r,q)}else{if(toc_options.insertPosition==&quot;firstHeadAfter&quot;){a(r,q)}else{if(toc_options.insertPosition==&quot;top&quot;){k(r,q)}}}} function g(q,r,p,s){return{tagName:q,text:r,children:[],nestLevel:p,id:s}} function m(p){return p.innerText} function f(p){return p.nextElementSibling} function d(p){return p.previousElementSibling} function b(p){return p.tagName.toLowerCase()} function e(p,q){return p.classList.contains(q)} function l(p){return p.parentNode} function a(q,s){var r=l(q);var p=f(q);if(r!=null&amp;&amp;p!=null){r.insertBefore(s,p)}} function k(p,r){var q=l(p);if(q!=null){q.insertBefore(r,p)}} function highlight(cloneToc){var htags=document.querySelectorAll('h1, h2, h3, h4, h5, h6');var h_arr=[];htags.forEach(function(htag){if(htag.id.match(/toc_headline_/)){h_arr.push(htag)}});h_arr.sort(function(a,b){return a.id>b.id});var lis=cloneToc.getElementsByTagName('li');var li_arr=Array.from(lis);var activate=function(){for(var i=0;i&lt;h_arr.length;i++){if(i&lt;h_arr.length-1){if(cr_top(h_arr[i])&lt;0&amp;&amp;cr_top(h_arr[i+1])>0){li_arr[i].classList.add('tl-active')}else{li_arr[i].classList.remove('tl-active')}}else{if(cr_top(h_arr[i])&lt;0){li_arr[i].classList.add('tl-active')}else{li_arr[i].classList.remove('tl-active')}}}};window.addEventListener('scroll',function(){throttle(activate(),100)})} function cr_top(elem){return elem.getBoundingClientRect().top} function throttle(fn,wait){var time=Date.now();return function(){if((time+wait)&lt;Date.now()){fn();time=Date.now()}}}})(window)    
  </script>
  <style type="text/css">
     .b-toc-container{background:#f9f9f9;border:1px solid #aaa;padding:10px;margin-bottom:1em;width:auto;display:table;font-size:95%}.b-toc-container p{text-align:center;margin:0;padding:0}.b-toc-container ul{list-style-type:none;list-style:none;margin:0;padding:0}.b-toc-container>ul{margin:15px 0 0}.b-toc-container.hide>ul{display:none}.b-toc-container ul li{margin:0;padding:0 0 0 20px;list-style:none}.b-toc-container ul li:after,.b-toc-container ul li:before{background:0;border-radius:0;content:""}.b-toc-container ul li a{text-decoration:none;color:#008db7!important;font-weight:400;display:flex;align-items:flex-start;flex-wrap:nowrap}.b-toc-container ul li .toc-number{margin:0 .5em 0 0;white-space:nowrap}.b-toc-container ul li .toc-text:hover{text-decoration:underline}.side-toc{position:sticky;top:20px}.tl-active>a{background:#efefef}
  </style>
</b:if>
<!-- [END] 目次作成プラグイン-->

2024/03/05:サイドバー目次の表示/非表示の切り替えができない不具合を修正しました。

テーマを保存後、投稿で目次が表示されていればOKです。

補足

QooQをご使用の場合、デフォルトの状態だとサイドバーの高さが設定されていないため、サイドバー目次へのposition:stickyが効きません。そのため、サイドバー目次を利用される場合はCSSに以下を追加してください。

#サイドバー {
  height: 100%;
}

追加オプション

オプションはtoc_optionsで設定することができます。

//以下のオプションを好みに合わせて変更して下さい
var toc_options = {
  ...
  ignoreURL: [],
  copyToSidebar: true,
  sidebarSelector: "#サイドバー",
  highlight: true
};

ignoreURL

目次を表示させたくないページのURLやキーワードを複数設定できます。

ignoreURL: ["blog-post_1.html", "blog-post_2.html", "hogehoge"]

上のように短いキーワードでも設定できますが、他に同じキーワードを含んでいるページがないか注意してください。例えば「blog-post」と設定した場合、URLに「blog-post」が含まれる全てのページで目次が表示されなくなります。

copyToSidebar

目次をサイドバーにコピーして表示することができます。記事中にある目次はそのままです。

説明
true目次をサイドバーに表示します
false目次をサイドバーに表示しません

sidebarSelector

サイドバーのセレクターを設定します。QooQの場合、id名が「サイドバー」になっているのでそれを指定します。

highlight

スクロールに応じてサイドバー目次の見出しをハイライトすることができます。

説明
true見出しをハイライト表示します
false見出しをハイライト表示しません

スクロールが見出しの位置まで来ると、サイドバー目次の見出しのliタグにtl-activeクラスが付与されます。そのため、ハイライト時の表示を変えたいときは以下のように設定します。

.tl-active > a {
  background: gray; /* 好きな背景色 */
}
scroll

たまに見かけますよね。これをやってみたかったんです(笑)

ソースコード

改造で加えたソースコードは以下の通りです。

var toc_options = {
  ignoreURL: [],
  copyToSidebar: true,
  sidebarSelector: "#サイドバー",
  highlight: true
};

var displayToc = true;
toc_options.ignoreURL.forEach(function(url) {
  if (location.href.match(url)) {
    displayToc = false;
  }
});

if (toc_options.copyToSidebar) {
  var toc = document.querySelector('.b-toc-container');
  var sidebar = document.querySelector(toc_options.sidebarSelector);
  if (toc != null && sidebar != null) {
    var cloneToc = toc.cloneNode(true);
    cloneToc.classList.add('side-toc');
    sidebar.appendChild(cloneToc);
    if (toc_options.highlight) {
      highlight(cloneToc);
    }
  }
}

function highlight(cloneToc) {
  var htags = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
  var h_arr = [];
  htags.forEach(function(htag) {
    if (htag.id.match(/toc_headline_/)) {
      h_arr.push(htag);
    }
  });
  h_arr.sort(function(a, b) {
    return a.id > b.id;
  });
  var lis = cloneToc.getElementsByTagName('li');
  var li_arr = Array.from(lis);
  var activate = function() {
    for (var i = 0; i < h_arr.length; i++) {
      if (i < h_arr.length - 1) {
        if (cr_top(h_arr[i]) < 0 && cr_top(h_arr[i+1]) > 0) {
          li_arr[i].classList.add('tl-active');
        } else {
          li_arr[i].classList.remove('tl-active');
        }
      } else {
        if (cr_top(h_arr[i]) < 0) {
          li_arr[i].classList.add('tl-active');
        } else {
          li_arr[i].classList.remove('tl-active');
        }
      }
    }
  };
  window.addEventListener('scroll', function() {
    throttle(activate(), 100);
  });
}

function cr_top(elem) {
  return elem.getBoundingClientRect().top;
}

function throttle(fn, wait) {
  var time = Date.now();
  return function() {
    if ((time + wait) < Date.now()) {
      fn();
      time = Date.now();
    }
  }
}

※圧縮のため&メンテナンス性の観点からあえて少し古い書き方をしています。

scrollイベントのhighlightに対してはthrottleによる間引き処理を行っており、イベントの連続発火防止によるページパフォーマンスの向上を図っています。

特定ページでの目次の表示/非表示については頭を悩ませた結果、オプションで管理するのが一番確実だと思い今回のような形式にしました。display:noneをページ個別でやるのは面倒だし、SEO的にもあまり良くないみたいなので…。表示させるなら作る、そうでなければそもそも作らない、というのがスッキリしてて良いかなと思います。

3件のコメント
IB-Note/Fumaさん、こんにちは。匿名で失礼いたします。

目次のサイドバー機能を使おうと、試しに導入してみました。スクロールに応じてハイライトするのがとても気に入っています。ところで、
オプションを「非表示」 showToc: false, にすると、
主目次は「表示/非表示」の切替わりができますが、サイドバー目次は「非表示」のままです。
また、showToc: true, の場合は、サイドバー目次は「表示」のままです。

なんとかしようとソースを拝見いたしましたが、自分ではどうにも手が出せないので相談させていただきました。
>匿名さん
コメントありがとうございます。
サイドバー目次の表示/非表示の切り替えができない件について、ソースコードを修正したので再度ご確認いただけますでしょうか。

※現段階では技術的に実現が難しいため、本体の目次とサイドバー目次で連動はできていませんが、切り替え自体はどちらも問題なくできるようになっています。

以上、よろしくお願いいたします。
IB-Note/Fumaさん

修正していただきありがとうございました。サイドバー目次の切換えができるようになりました。