この記事もですが、今、咲-Saki-とは関係のない長大な記事を書いている途中です。
あまりにも長いので、目次だけでPCのウィンドウサイズより大きくなってしまい、目次をコンパクトにするため、小見出しを非表示にしておくことにしました。いや記事を分けろよ!
が、表示/非表示の切り替えがあんまりカッコよくないので、そうだ!スライドさせてみよ!
と思い立ったのであります。

で、いつものように泥縄でわちゃわちゃと JavaScript を書き、なんかもう複雑すぎて収拾がつかなくなりそうだったんですが…
終わってみればとても短くまとめることができました。
このプラグイン(そう…プラグインと言わせてくれ)の機能は
リンクつきのリストを、ふわっと開閉する」です。
この記事の目次です。完成形はこんな感じになります。 ※スマホでは動作しません。
スマホユーザーの方は、PCでのサンプル完成版の動作を撮影したGIF動画を御覧下さい。
サンプル完成版の実物は記事の最後に掲載しています。

HTMLソースのサンプル

まず、サンプル用のHTMLです。
<li>要素の閉じタグ(</li>)は省略しています。
<div class="indexH">

  <h4>見出し</h4>

  <ul>
    <li>
<a href="#item1">大項目1</a> <ul> <li><a href="#item1-1">中項目1-1</a> <li><a href="#item1-2">中項目1-2</a> </ul> <li>
<a href="#item2">大項目2</a> <li>
<a href="#item3">大項目3</a> <ul> <li><a href="#item3-1">中項目3-1</a> <li><a href="#item3-2">中項目3-2</a> <ul> <li><a href="#item3-2-1">小項目3-2-1</a> <li><a href="#item3-2-2">小項目3-2-2</a> <li><a href="#item3-2-3">小項目3-2-3</a> <li><a href="#item3-2-4">小項目3-2-4</a> <li><a href="#item3-2-5">小項目3-2-5</a> <li><a href="#item3-2-6">小項目3-2-6</a> </ul> </ul> </ul> </div>
一番外側にindexHというクラス名を指定した<div>タグを書き、全体を包みます。
その中に「目次」という見出しを入れます(<h1>とか<h2>ではなく<h4>を使っているのは、このブログのテンプレートで文字の大きさがちょうどいいので最初に使ったのを、そのまま踏襲しているだけです)。
その下に<ul>-<li>-<a>という構造のアンカーつきリストを、必要な分だけ入れていきます。
<ul>要素を入れ子にするには、<a>要素の兄弟要素として隣に並べることに留意して下さい。
項目数と階層の深さ、また設置数に制限はありません(常識の範囲内で)。

これだけだと、こんな表示になります。 環境に応じたデフォルトのCSSだけが適用され、非常にシンプルな見た目になっていますね。

CSS

CSSは、色や配置などデザインに関わるものと、JavaScript でスライドを実現するためにプログラム上必要なものとがあります。

テーマ

.indexH{
    font-family: 'times new roman';
    width: 80%;
    background: #edd; /* サンプルは#add8e6 */
    margin: 1em auto;
    padding: 1em;
    box-shadow: 2px 2px 3px rgba(0,0,0,.1);
}
.indexH h4{
    margin:0 1em;
}
.indexH ul{
    list-style-type: none;
}
.indexH li{    /* for smartphone */
    display: block;
}
.indexH a{
    text-decoration: none!important;
    color: navy; /* sample only */
}
背景色やフォント、余白などの設定です。
リストの頭につく「・」を消すには、ullist-style-type: none;を指定すればいい…はずなのですが、スマホだとなぜか効きません。子のlidisplay: block;を指定すると消えます。 ブラウザやブログテンプレートのデフォルトスタイルを完全に上書きできていないのがもやもやしますが、許してほしい。
作りこみさえすればいくらでもカッコよくできる…と思います!
この部分はテーマ用の別クラスに設定してもいいかもしれません。

レイアウト補助 - リストを横並びに

ul.float-indx{
  word-break: keep-all
}
ul.float-indx li{
  display: inline-block;
  margin-right: 1em
}
目次の項目はHTMLの<li>要素なので、デフォルトでは一項目一行で表示されます。
でも短い項目名に一行使うのはもったいない…横に並べたい、という場合に使うCSSです。
他でも使えそうなCSSなので、セレクタに.indexHの限定はつけていません。
横並びさせたい<li>要素の親要素(<ul>要素)に
  <ul class="float-indx">
とクラスを指定してやれば、 指定した部分だけが横並びになります。
デザイン上の理由で、適用は最下層だけにした方がいいです。

開閉機能のためのCSS

.indexH h4{
    display: inline-block;
}
.indexH ul{
    display: none;
}
開閉ボタンを見出しの隣に並べるためと、項目リストを非表示にするためのCSSです。

開閉ボタン

開閉ボタンの構造上の本体はlabel要素、画面に見えるのはsvg要素です。非表示のinput要素が切り替え機能を担当しています。
<label>
<input>
<svg>
.indexH label{
    display: inline-block;
}
.indexH input{
    display: none;
}
.indexH svg{
    background: transparent;
    border-radius: 50% 50%;
    box-shadow: 1px 1px 2px rgba(0,0,0,.2);
    margin-left: 4px;
    width: 18px;
    height: 18px;
    transition: .3s;
    -webkit-transform: rotate(0);    /* for Safari8.4 & Android Browser4.4 */
    transform: rotate(0);
    fill: #333;
}
.indexH input:checked + svg{
    -webkit-transform: rotate(-180deg);    /* for Safari8.4 & Android Browser4.4 */
    transform: rotate(-180deg);
    background: white;
}

見出し

今回作ったプラグインを実行すると、大項目以下が非表示になり、子項目を持つ親項目の右に開閉ボタンが出現します(見出しは最上位の親項目として扱います)。
これはボタンのデザインをお見せするためのサンプルなのでクリックしても子項目は表示されませんが、くるっと回転するのは確認できると思います。
回転方向はネジとか水道栓のイメージで、左回りで開、右回りで閉。
下はボタンの HTMLコード。インラインSVGを利用してアイコンを描画しているのがお分りいただけると思います。
SVGパスのデータは Google先生の Material Design icons からパクりました。
<label>
  <input type=checkbox>
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
    <path d="M12.44 6.44L9 9.88 5.56 6.44 4.5 7.5 9 12l4.5-4.5z"/>
  </svg>
</label>
このコードは HTMLソースに書く必要はなく、<a>要素と<ul>要素との間に、JavaScript で自動的に挿入されます。何か所も同じコードを書くのは面倒なので。
チェックボックス()はCSSで非表示にしてあるのでクリックのし様がなく、本来ならチェックできません。
しかし<label>タグで括っている兄弟要素<svg>のボタン()は表示されているので、こちらをクリックすると連動してチェックされたことになります。
アンケートフォームなどでチェックボックスの横のテキストをクリックしてもチェックマークがつく、あれと同じ仕組みです。
jQuery でボタンのdata属性に開閉の情報を持たせてもよかったんですが、HTML+CSSでここまでできるのが面白いと思って、この部分だけはスクリプト無しにしました。

CSSだけでできる?

では、項目リストの開閉にもCSSが使えるのでは?
と、そんなふうに考えていた時期が私にもありました…
.indexH ul{
  height: 0;
  transition: .3s;
}
.indexH input[type="checkbox"]:checked~ul{
  height: 240px;
}
CSSでは、こんな風に↑プロパティに具体的な数値を指定しないとアニメーションしてくれないんですね。
autoとかじゃダメ。
項目の数によって高さは一定しないし、横並びにした項目は一行に収まったり折り返されたり、文字列の長さやクライアントのフォント設定、ウィンドウサイズによっても変わってくるので、新しい目次を作るたびに入れ子の分の高さまで全部チェックして何通りかのケースのために別々のCSSを書くなんて……
やってられないよ!
パッパッと切り替えるだけならdisplay:nonedisplay:blockで事足りるんですが、にゅるーんとスライドさせたいんだもん!
そんなわけで、今回も JavaScript のお世話になることに。

jQuery ソース

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
Google の CDN から jQuery を読み込んでいます。

構成

枝葉を省略して書くと、こんな構成になっています。
$(function(){
  var $iH, expand;  // 変数宣言
  $iH.find().after(), // 開閉ボタンの設置(1)
  $iH.find().each(),  // 開閉ボタンの設置(2)
  $iH.find().each(),  // タイトルポップアップの設置
  $iH.on();           // 開閉ボタンのクリックイベント処理
});
jQuery()に渡す無名関数の中身は、実質?わずか五行!
以下、この五行を細かく見ていきます。

変数宣言

変数と言いつつ、値は初期値のまま変わらないので実質定数ですが。
var $iH = $('.indexH'),
    expand = '<label><input type=checkbox><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M12.44 6.44L9 9.88 5.56 6.44 4.5 7.5 9 12l4.5-4.5z"/></svg></label>';
  1. $iHは目次全体を包む<div>要素の jQueryオブジェクトです。
    これを目印に目次の各パーツを特定し、操作します。
  2. expandと名づけた変数は、上にも書いた開閉ボタンのHTMLコード(を一行にまとめたもの)です。
    ボタン上に描画するアイコンの SVGコードを含むため、少し長いです。
    このSVGコードを SVGファイルにしておき、画像のように読み込むこともできますが、DOM に直接書き出した方がサーバーへの負担がちょっぴり減ります。

開閉ボタンの設置(1)

$iH.find('h4').after(expand),
最上位の親項目、つまり見出しの<h4>要素の後に、上で定義したexpandを挿入すると、ボタンが設置されます。

開閉ボタンの設置(2)

$iH.find('li').each(function(){
  var $li = $(this);
  if($li.children('ul').length){
    $li.children('a').after(expand);
  }
}),
目次オブジェクト内の<li>要素を検索し、その各々について、子要素として<ul>要素が含まれているか調べます。
この判定にlengthプロパティを使っているのは、$(li.children('ul'))は、判定の瞬間、要素として存在していなくてもオブジェクトとして存在してしまうため、nullundefinedなどを返さないからです。
もしlength0でないなら入れ子が存在するということですから、<a>要素の後に開閉ボタンを設置します。

ポップアップタイトルの設置

$iH.find('a').each(function(){
  if(this.title.length<1){
    this.title = $(this).text();
  }
}),
ポップアップタイトルが設定されていないリンクには、リンクテキストと同じ文字列をそのまま設定します。
これでもうリンクテキストをいちいちtitle=""にコピペしなくて済む…そのために入れました。

開閉ボタンのクリックイベント処理

$iH.on('click', 'input', function(){
目次内に存在する開閉ボタンがクリックされた時の処理を、function(){・・・}の中に書いて登録します。
この関数の部分をイベントハンドラと言います。
  var $ul = $(this).parent().next('ul');
on()のイベントハンドラの中では、on()が働きかけた対象(ここでは$iH)ではなく、引数に指定された要素(ここではinput要素)のオブジェクトがthisになります。
これにparent()メソッドをかけることによって、親要素である<label>要素が取得され、それにnext()メソッドをかけることによって、隣接する<ul>要素、つまり開閉の対象である下位項目のリストが取得されます。
  $ul.animate({
    height: 'toggle',
    opacity: 'toggle'
  }, {
    duration: Math.round($ul.height()/2)
このリストを画面上でにょろ~んと出したり引っ込めたりするには、slideToggle()という、まさにそのために作られたようなメソッドもあるんですが、透明度もアニメーションさせたかったので、普通にanimate()メソッドを使いました。
CSSでは具体的な数値を指定しないとアニメーションしてくれないプロパティも、jQuery だと'toggle'と書くだけで「0」と「デフォルトの値」を往復してくれるんですね。便利!
duratitonはアニメーションにかける時間(単位:ミリ秒)。
適当な定数でもいいんですけど、リストの高さに何となく比例させてみました。
  }, function(){
    if($ul.css('display')=='none'){
      $ul.find('ul').hide(),
      $ul.find('input:checked').each(function(){
        this.checked = false;
      });
    }
分けて載せているので分りにくいですが、ここはまだanimate()の引数部分。
つまりここのfunction()animate()のコールバック関数(引数として渡される関数)です。
上記のイベントハンドラもコールバック関数の一種です。
animate()のコールバック関数は、アニメーションの処理が終了してから呼び出されます。
ここでは、親項目のリストが閉じられたらすべての子孫リストも閉じる処理を行っています。
苦労させられたのは、親項目のリストを非表示にしても子項目が表示されたままだとボタンが表示を記憶していて、親項目を再表示させたときにその子項目も表示されたままになってしまうことでした。
そんなの、<input>タグのchecked属性を外すだけの簡単なお仕事──のはずでした。
が…
HTML属性を操作するメソッドをかけても、なぜかチェックが外れません。
$(this).removeAttr('checked'); //ダメ
$(this).prop('checked', false); //ダメ
どうやってもうまくいかないので調べてみたら、jQueryのバグではないかということでした。
jQuery - Bug Tracker
jQueryでチェックボックスが正しく操作できない
古いバージョンではちゃんと動くらしいのですが、最新版の1.12.4を、この一行のためだけに今更1.6.1(諸説あり)あたりに戻すのもなあ…
というわけで、この部分だけjQueryのメソッドは使わず、生JavaScriptでthis.checked = falseと書いたらできました。
  });  // animate()の閉じカッコ
});  // on()の閉じカッコ
最後にカッコを閉じます。

サンプル完成版

PCでのみ動作します。
開閉ボタンのトグル機能に利用した<input type=checkbox>について補足したかったんですが、長くなり過ぎたのでいったんCMでーす。