気まぐれお。

気まぐれに何か書きます。

【ブログカスタマイズ】せっかくなら目次も移動させよう!見出し移動ボタンの作り方

f:id:reoasxdtmgt:20210609094153p:plain

先日、下記の記事を投稿しました。

reoasxdtmgt.hatenablog.com

あれから沢山の方にご拡散・ご導入いただき、筆者としても非常に嬉しい限りです。
中には、導入していただいた記事内で前述の記事をご紹介してくださった方もいらっしゃいました。この場を借りてお礼申し上げます。

この記事は導入できなかった方のために随時更新を行っていました。その甲斐もあって満足した記事ができたと自負していますが、1つだけ気になる点がありました。

トップに戻るボタン、要る?

「機能1個じゃ寂しいし、おまけでなんか付けとくか~」と軽い気持ちでつけたボタンでしたが、実際のところこのボタンってそんなに使ってるのかな?という印象です。
PC なら Home キー一発で、スマホだとブラウザによってはタップする場所次第では同様のことができます。スマホならなおさら、誤タップしてしまいどこまで読んだか探し直すなんてことにもなりがちです。

……と筆者は思っていましたが、

ユーザー側としては意外にも欲しいと思う人は多いみたいです。(母数は多くないですが)
消すつもりでしたが下手に消せなくなってしまいました(笑)

(ちなみに、(多分)プロのWebサイト作成者の方でも懐疑的になっている方が多かったです。)

service.plan-b.co.jp

www.gekkoseisaku.com

何はともあれそんな動機があって、代わりの機能は何かないものかと探している最中、他の方の記事を読んでいる時に「さっき読んでたあの内容なんだっけなー」と思ったタイミングがありました。
ここから派生して、見出し単位での移動なら需要があるかもしれないと思い、本記事の作成にあたりました。

ということで今回は「好きな見出しにいつでも移動できるボタン」を紹介します。

利用条件

前回同様、html/css/javascript を使用できるブログサービスが最低条件です。
また、コードへの知識・理解がある程度は必要となります。それらに忌避感のある方は使用をお控えください

該当機能を導入してトラブルが発生した場合、筆者は原則として一切の責任を負いかねます。あくまで何が起きても自己責任ということをご理解いただいた上で導入をご検討ください。

仕様

前提条件

原則は前回の記事と同様で、こちらを導入している前提でお話を進めさせていただきます。
お手数ですが併せてご覧ください。

reoasxdtmgt.hatenablog.com

また、私が使用しているのははてなブログなので、その前提でお伝えしていきます。
他サービスをご利用の方はお手数ですが、この記事の後半にある解説を参考に、適宜読み替えてください。

動作仕様

前回同様、右下にボタンを挿入します。

f:id:reoasxdtmgt:20210604090909p:plain f:id:reoasxdtmgt:20210604090925p:plain

今回追加したボタン (1枚目赤矢印) を押すと、目次と同内容のもの (2枚目) が表示されます。 そして、移動したい見出しをクリックすると、対応する見出しのところまで移動する、という仕組みです。

本記事でも本機能を使用しているので、実際に確認してみてください。

仕様策定の流れ

いざ自分で調査してみようと思い至った時にも役に立つと思うので、今回はどのようにして本機能の実装に至ったかについて少しだけ触れておきます。

経過

HTMLには、そのページにある要素の id 属性を検索し、その位置へと移動する機能があります。
この機能のことをここではページ内リンクと呼びますが、こちらを使えば本機能が実現できる事自体は分かっていました。

www.tagindex.com

つまり、id 属性がついた要素を検索してリンクを作成していけば実装できる、ということです。
そこで、元から id 属性がついている部分を探してみることにしました。

その結果、はてなブログにはこの機能が既に実装されていることが分かりました(笑)
それが目次です。

目次がないと、見出しは「種も仕掛けもないただの見出し」だった*1のですが、目次を入れると自動で id 属性がつくようになり、特定の見出しへ移動できるようになります。

それなら、目次を任意の領域にコピーすればそれで済むじゃん!?ということで、今回の実装に至りました。

デベロッパーツール (開発者ツール)

ここに挙げたような情報を確認するためのツールにデベロッパーツール (開発者ツール)というものが存在します。仰々しい名前に感じるかもしれませんが、各種PC用ブラウザに標準装備されています。
要素の情報を確認するだけでなく、CSSを試しに調整してみたりすることもできるので、興味のある方は一度使ってみてください。

www.sakurasaku-labo.jp

導入方法

前回同様、コピペだけで基本済むようにしています。

事前準備 (1回だけで済む内容)

スクリプトを全ページに挿入する

jQuery という javascript ライブラリと、本機能のスクリプトを挿入します。

<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script>
// 目次ボタン作成
$(function(){
  if ( 0 >= $('.modal-open[data-target="jump"]').length ){
    return;
  }
  var $tableOfContents = $('.table-of-contents');
  if ( 0 >= $tableOfContents.length ){
    return;
  }
  // モーダルダイアログの作成
  var modalDlgStr =
      '<div class="modal" data-id="jump">'
    + '  <div class="modal-overlay modal-close"></div>'
    + '  <div class="modal-content">'
    + '    <p class="modal-close_wrapper">'
    + '      <a href="javascript:void(0);" onclick="return false;" class="modal-close">閉じる <i class="blogicon-close"></i></a>'
    + '    </p>'
    + '  </div>'
    + '</div>';
  $('.entry-content').append( modalDlgStr );
  $('.modal[data-id="jump"] > .modal-content').append( $tableOfContents.clone() );

  // クリックイベントの設定
  $('.modal[data-id="jump"] > .modal-content > .table-of-contents a').addClass('modal-close');
  $('.modal-open').on('click', function(){
    $('.modal[data-id=' + $(this).data('target') + ']').fadeIn();
    return false;
  });
  $('.modal-close').on('click', function(){
    $('.modal').fadeOut();
    return false;
  });

  // ページ内リンクのスムーズスクロール
  $('a[href^="#"]').click(function(){
    var adjust = -30;
    var duration = 500;
    var href= $(this).attr('href');
    var target = $(href == "#" || href == "" ? 'html' : href);
    var position = target.offset().top + adjust;
    $('html, body').animate({scrollTop:position}, duration, 'swing');
    return false;
  });
});
</script>

はてなブログであれば、「デザイン > カスタマイズ > ヘッダ > タイトル下」もしくは「デザイン > カスタマイズ > フッタ」に挿入してください。
使用されているブログサービスにこのような類のものがなければ、本文中に挿入してしまっても構いません。

また、1行目は前回の記事と同じなので、そちらを導入された方は省いてください。

専用の CSS を挿入する

挿入する項目のデザインを決めるものを挿入します。

.modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  display: none;
  z-index: 999;
}

.modal > .modal-overlay {
  position: absolute;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  background: rgba(0,0,0,0.8);
}

.modal > .modal-content {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%,-50%);
  width: 80vw;
  max-width: auto;
  max-height: 60vh;
  box-sizing: border-box;
  margin: 0;
  padding: 24px;
  background: #ffffff;
  overflow-y: scroll;
}
@media screen and (max-width: 980px) {
  .modal > .modal-content {
    max-width: auto;
    width: 80vw;
  }
}

.modal > .modal-content > .modal-close_wrapper {
  padding: 0;
  margin: 0;
  text-align: right;
}
.modal > .modal-content > .modal-close_wrapper > a.modal-close {
  font-size: 1em;
  color: #999999;
  text-decoration: none;
}
.modal > .modal-content > .modal-close_wrapper > a.modal-close:hover {
  color: #bbbbbb;
  text-decoration: none;
}

はてなブログであれば、「デザイン > カスタマイズ > デザインCSS」に挿入してください。

使用されているブログサービスにこのような類のものがなければ、本文中に挿入してしまっても構いません。ただし、その場合は上記の文頭に <style type="text/css">・文末に</style>を追加した上で行ってください。

毎記事行うこと

目次を挿入する

今回の機能は、元々ある目次をそのままコピーして利用するので、目次がないとそもそも機能しません。
そのため、はてなブログの方であれば「 」のアイコンから目次を挿入してください。

本文中に HTML を挿入する

前回のボタンのところに、本機能を実行するボタンを追加します。
ul 要素にある2つ目の li 要素が、今回導入するもののボタンです。残り2つは前回の記事と同様のものです。

<!-- はてな記法・Markdown モードをご利用の場合 -->
<ul class="footer-nav">
  <li><a href="javascript:void(0);" onclick="smoothScroll(); return false;"><i class="blogicon-chevron-up"></i></a></li>
  <li><a href="javascript:void(0);" onclick="return false;" class="modal-open" data-target="jump"><i class="blogicon-list"></i></a></li>
  <!--<li><a href="javascript:void(0);" onclick="showDeckList(); return false;"><i class="blogicon-photo"></i></a></li> (必要に応じて) -->
</ul>

<!-- 見たままモードをご利用の場合「HTML編集」から挿入 -->
<ul class="footer-nav">
  <li><a href="javascript:void(0);" onclick="smoothScroll(); return false;"><em class="blogicon-chevron-up"><span class="dummy">「トップに戻る」ボタン</span></em></a></li>
  <li><a href="javascript:void(0);" onclick="return false;" class="modal-open" data-target="jump"><em class="blogicon-list"><span class="dummy">「見出し移動」ボタン</span></em></a></li>
  <!--<li><a href="javascript:void(0);" onclick="showDeckList(); return false;"><em class="blogicon-photo"><span class="dummy">「画像閲覧」ボタン</span></em></a></li> (必要に応じて) -->
</ul>

注意事項

前回同様のものは省略します。

  • 前回記事で挿入したものは文量削減のため省略しています (「本文中に HTML を挿入する」は位置関係の問題で例外)。必要なスクリプトCSS は、お手数ですが適宜前回の記事から参照してください。
  • 目次のデザインはブログのデザインのものをそのまま使用しています。サイズが合わないなどの不具合がある場合は、目次そのもののデザインもしくはモーダルダイアログ側の目次のデザインの変更をご検討ください。

技術解説

他のサービスを利用されていて、ご自身のサービス向けに調整しないといけない方や、別の使い方に挑戦してみたい方のために、本機能の動作原理をなるべく分かりやすく解説します。

なお、デザインに関しては本記事の趣旨と外れますので、言及しておりません。ご了承ください。

前提知識

前回同様のものは省略します。

即時実行関数

$(function(){
  // TODO: ここに即時実行したい処理を追加
});

詳細な説明は省きますが、この書き方をすると「HTMLの読込が完了した後、即時実行する」という意味になります。

前回の機能において、実際に動作させるタイミングはユーザーが操作した時だけでした。したがって、クリックされた時に呼び出す関数さえ定義してしまえば良かったので、このような処理は必要ありませんでした。
しかし、今回はユーザーが操作し始める前にやりたい処理 (目次のコピーなど) があります。そのため、このような処理が必要になります。

カスタムデータ属性

www.webprofessional.jp

詳細は上記リンクを参照してもらいたいのですが、HTML要素に付与する属性にオリジナルの属性を設定できます。この属性は data-xxxx="hogehoge" のような形で xxxx 部分に好きな属性名を入れられます。

今回は jQuery からの参照でしか利用していませんが、上記リンクの通り様々な方法で使用することができます。

イベントハンドラ

ページに必要な情報がすべて読み込めた、ユーザーが何か入力した……など、Webブラウザでは色々な動作・出来事が発生しています。これらのことをイベントと呼びますが、これらを検出し、それが発生したことをきっかけに特定の処理を行う関数のことをイベントハンドラと呼びます。前回 onclick 属性に書かれた処理もイベントハンドラの1つです。

ちなみに、前回は説明の簡素化のため onclick 属性にイベントハンドラを記述しました。しかし、今回のように特定のクラス (もしくはカスタムデータ属性) に対して (一括で) 設定するやり方が主流と思います。
これは、文書構造である HTML / その見た目を決める CSS / 何かしらの処理を決める javascript 等 のそれぞれで独立して記述すべき、という概念に基づくものです。

見出し移動ボタン機能の動作原理

モーダルダイアログの作成

var $tableOfContents = $('.table-of-contents');
// (中略)
var modalDlgStr =
    '<div class="modal" data-id="jump">'
  + '  <div class="modal-overlay modal-close"></div>'
  + '  <div class="modal-content">'
  + '    <p class="modal-close_wrapper">'
  + '      <a href="javascript:void(0);" onclick="return false;" class="modal-close jump-modal-close">閉じる <i class="blogicon-close"></i></a>'
  + '    </p>'
  + '  </div>'
  + '</div>';
$('.entry-content').append( modalDlgStr );
$('.modal[data-id="jump"] > .modal-content').append( $tableOfContents.clone() );

rilaks.jp

ボタンを押したら出現する、モーダルダイアログを作成します。

HTML を javascript 側で書いて*2変数に代入しています。本文に直接 HTML を挿入しても良いのですが、下記の理由で動的に生成・挿入することにしました。

  • 執筆中の本文は文章が主体となるべきで、余計なものはできるだけ省略したい
  • 挿入する内容が本文によって変化しないため、わざわざ本文に挿入する意味がない
  • SEO的には、クローリングされにくいがメインコンテンツではないので問題なく、むしろ重複するコンテンツがあることの方が望ましくない

最後の2行では、生成したダイアログを $('挿入場所').append('挿入内容')挿入場所の一番下に挿入しています。

最終行では $tableOfContents.clone() を挿入していますが、 .clone() なしに挿入すると、「挿入内容が元々あった場所」からそのコンテンツを移し替えることになるので、結果的に元々あった目次が消えてしまいます
今回はコピーを挿入するようにしていますが、一方で「ボタンですぐ目次が出るなら別に本文中になくても良いや」という方は .clone() の部分を削除してご利用ください。

ここまでで表示自体はできました。
利便性を上げるために以降の処理を入れていきます。

クリックした場合の処理

$('.modal[data-id="jump"] > .modal-content > .table-of-contents a').addClass('modal-close');
$('.modal-open').on('click', function(){
  $('.modal[data-id=' + $(this).data('target') + ']').fadeIn();
  return false;
});
$('.modal-close').on('click', function(){
  $('.modal').fadeOut();
  return false;
});

ここでは、生成したモーダルダイアログの表示・非表示について設定しています。

1行目では「モーダルダイアログに追加した方の目次にある、見出しへのリンク」に対して modal-close クラスを追加しています。こちらは、後述する機能を追加するために利用します。

2行目以降では、modal-open および modal-close クラスが付与されている要素に対して、クリックされた場合の処理を追加しています。モーダルダイアログを、その名前が示す通り、前者は表示・後者は非表示にさせる処理を行います。

modal-open 側の処理 $('.modal[data-id=' + $(this).data('target') + ']') が少々ややこしいですが、HTML と照らし合わせて見ていただくと分かり良いと思います。

<!-- モーダルダイアログ呼び出し元 (属性の順序を変更しています) -->
<li><a class="modal-open" data-target="jump" href="javascript:void(0);"><!-- 略 --></a></li>

<!-- モーダルダイアログ呼び出し先 (動的生成したものです) -->
<div class="modal" data-id="jump"><!-- 略 --></div>

まずは $(this).data('target') に注目してください。

  • $(this) の意味について。ここでは $('.modal-open') が持つ .on() 関数を呼び出しています。つまり、ここでの $(this)$('.modal-open') すなわち 呼び出し元 (今回であれば a 要素) です。
  • .data('target') は、カスタムデータ属性 data-target の値を取得する関数です。

以上から、$(this).data('target') の意味は「呼び出し元である a 要素の data-target 属性の値を取得する」という意味になります。今回の例であれば、結果的に "jump" が取得されます。
ここまで来ればもうお分かりかと思いますが、結果としては $('.modal[data-id="jump"]') となる要素に対して .fadeIn() 関数を実行する、という処理になります。

ちなみに、modal-close 側にモーダルダイアログを指定する処理が存在しないのは、モーダルダイアログは排他的に1つしか存在しないという前提に基づくものです。
現状1つしかないので排他処理は特別実装していませんが、実装次第ではダイアログを指定して閉じる処理を入れてあげた方が良いかもしれません。

スクロールの付与

  // ページ内リンクのスムーズスクロール
  $('a[href^="#"]').click(function(){
    var adjust = -30;
    var duration = 500;
    var href= $(this).attr('href');
    var target = $(href == "#" || href == "" ? 'html' : href);
    var position = target.offset().top + adjust;
    $('html, body').animate({scrollTop:position}, duration, 'swing');
    return false;
  });
});

上記の .on() 関数や .click() 関数でイベントハンドラを設定する際は、設定する際に存在していた要素にしか反映されません。また、.clone() 関数によってコピーされた要素には、コピー元にあったイベントハンドラまではコピーされません。どうして……(現場猫並感)

どうやら、はてなブログではページ内リンクに上記とは別のイベントハンドラを設定していたようですが、コピーできないものは仕方ないので上書きしよう、という魂胆*3です。

なお、この項目は .clone() をしなかった方には関係ありませんので、不要な方は消してしまっても問題ありません。

主たる要素ではないけど大事な部分

if ( 0 >= $('.modal-open[data-id="jump"]').length ){
  return;
}
var $tableOfContents = $('.table-of-contents');
if ( 0 >= $tableOfContents.length ){
  return;
}

もし何かしらの理由で本機能を設置しなかった場合、または目次を設置しなかった場合、余計な処理はしてほしくないので、それ以降の処理を停止させる処理です。

おわりに

ここまでお読みいただきありがとうございました。
今回は前回と比べて難しかった部分もあると思いますので、不明点などあればお気軽にご質問ください。

それでは、またね。

*1:今まで目次をつけないことがなかったので、今回はじめて知りました(笑)

*2:ちなみにバッククォート (`) で囲むと、複数行に亘って文字列とかを記述できるテンプレートリテラルっていう仕組みがあるんですけど、IE11が対応してないんですよね……

*3:元のやつを持ってくることもできたのでは?と執筆中に気付きましたが、諦めたので興味ある方はやってみてください。