2015/04/17

JavaScriptでWebページの一部内容を折りたたむ機能を付ける

Bloggerへの移行を機会に、長いプログラムなどをクリックで折りたたんだり元に戻したりするような機能を付けたいと思っていたが、調べても自分の求めていたものやそれに近いものが見つけられなかったので、自分で実装することにした。

  1. 求める動作
  2. 考え方と実装方法
    1. ページ読み込み時の初期化
      1. 操作用リンクの自動生成
      2. 対象要素のたどり方と表示状態の初期化
    2. 操作用リンクから呼び出される関数の処理
  3. 実装例
    1. expanderクラスのdiv要素の次の要素を折りたたみ状態にする
    2. openedexpanderクラスのdiv要素の次の要素を表示(展開)状態にする
    3. 上記両方に同時に対応したもの
    4. Bloggerで現在使用しているもののスクリプト圧縮版

求める動作

  • 外部サイトから.jsファイルを参照する必要がなく、短いコードをテンプレートに追加するだけで使える
  • id属性を色々指定したりスタイルシートの記述を追加したりする必要がない
  • JavaScriptが無効の状態でも余計なもの[1]が表示されたり逆に表示されるはずのものが隠れたりせず、内容が正しく表示されるようにする
  • HTML文書側では特定のclass属性の付いた空のdiv要素を置くだけでその部分に操作用リンクが挿入され、次の要素のみが折りたためる
  • ページ読み込み時に自動的に折りたたみ状態か表示(展開)状態かに初期化できる

考え方と実装方法

ページ読み込み時の初期化

以下の処理を関数にし、body要素のonload属性かbody要素の終了タグのすぐ上あたりで呼び出す。

操作用リンクの自動生成

  • expander” というクラス(class属性)の空のdiv要素をHTML文書側に用意し、ページ読み込み時にJavaScriptからこの要素を探索してそれぞれに対して操作用リンク2つを挿入する
    • document.getElementsByClassName()で引数に “expander” を指定して対象の要素群を得る
    • 要素群それぞれに対する処理はプロパティlength[2]を用いたfor文のループで行う
    • ループ内ではじめにプロパティinnerHTMLにリンクのマークアップを記述
  • リンクのマークアップ部分は2つのa要素のみを続けて並べたものとする
    • 順番はどちらでもスクリプト側がこれに合わせればよいが、ここでは “最初に表示(展開)用,次に折りたたみ用” という順番とする
    • onclick属性には別途それぞれに用意した表示(展開)/折りたたみ用の関数を記述し、このリンク自体の要素を “this” をその関数の引数に渡す形にする
    • リンク先(href属性)は “#” とし、かつクリック時にこのリンク先(ページ最上部)に飛ばないようにonclick属性の末尾に “;return false” を付ける

対象要素のたどり方と表示状態の初期化

  • JavaScriptで対象のdiv要素それぞれにリンクを挿入した後は子の2つの要素それぞれとdiv要素の次の要素(折りたたみ対象)に表示/非表示の指定を行う
    • 1つ目のリンクはプロパティfirstElementChild[3]で,2つ目のリンクはプロパティlastElementChild[4]でそれぞれ取得
    • div要素から見て次に来る要素である折りたたみ対象はプロパティnextSibling[5]でたどるが、要素以外の種類が得られる場合があるため、プロパティnodeType[6]が “1[7]になるまで繰り返し取得する
    • 非表示にしたい要素のプロパティstyle[8]のCSSプロパティdisplayには “none” を代入し、表示したい要素の同プロパティには “block” (ブロック要素として表示)や “inline” (インライン要素として表示)を代入するが、今回の使い方では全て前者にする(動作上影響がなく、コード記述量も減らせるため)
  • expander” 以外に “openedexpander” のような別のクラス名の空のdiv要素を用意して同じ要領で処理を行うことで、内容が初期状態で表示(展開)されているものも作れる

操作用リンクから呼び出される関数の処理

  • 表示(展開)用の関数では “表示(展開)用リンクを隠し、折りたたみ用リンクを表示し、折りたたみ対象を表示” の3つの処理を行う
    • 折りたたみ用リンクは “そのリンク自身の次に位置する要素” として(引数で受け取った要素のプロパティ)nextSiblingで取得
    • 折りたたみ対象はリンク自身のプロパティparentNode[9]で親のdiv要素を取得後に先述のnextSiblingの繰り返しで取得
    • 表示状態の変更についての要領は先述したものと同じ
  • 折りたたみ用の関数では “表示(展開)用リンクを表示し、折りたたみ用リンクを隠し、折りたたみ対象を隠す” の3つの処理を行う
    • 表示(展開)用リンクは “そのリンク自身の前に位置する要素” としてプロパティpreviousSibling[10]で取得
    • それ以外の要領は表示(展開)用の関数と同様

実装例

注意点として、初期状態が “折りたたみ” (非表示)の場合、検索エンジンからの訪問者にとっては一致箇所を探しにくいため、ユーザビリティ[11]を損ない、検索エンジンからの評価が悪くなる[12]ことも考えられる。ただ、HTML文書の用途や公開範囲によっては有用なこともある。[13]

expanderクラスのdiv要素の次の要素を折りたたみ状態にする

この形は用途を選ぶので注意。

[任意]ファイル名:expander-demo-default-closed.html エンコーディング:UTF-8
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>expanderクラスのdiv要素の次の要素の内容を初期状態で非表示に</title>
<style>
.codewrapper {border:1px dashed #c99;background:#f0e9ec;padding:.5em;margin-bottom:1em}
pre {padding:2px;border:1px solid #888;background:#f5f5f5}
</style>
</head>
<body>
<div class="codewrapper">
[任意]ファイル名:foo.txt
<div class="expander"></div>
<pre>
ここは
最初は隠れています。
</pre>
</div>
<div class="codewrapper">
[任意]ファイル名:bar.txt
<div class="expander"></div>
<pre>
ここも
はじめは見えないです。
</pre>
</div>
<script>
// "expander" クラスの空のdiv要素の次の内容を
// 全て折りたたんだ状態で初期化する
function initExpandersClosed ()
{
  var elems = document.getElementsByClassName ('expander');
  for (var i = 0; i < elems.length; i++)
  {
    elems[i].innerHTML = "<a href='#' onclick='expand(this);return false'>&larr; expand &rarr;</a><a href='#' onclick='collapse(this);return false'>&rarr; collapse &larr;</a>";
    var e = elems[i].nextSibling;
    while (e.nodeType != 1)
      e = e.nextSibling;
    e.style.display = elems[i].lastElementChild.style.display = "none";
  }
}
function collapse (o)
{
  var link_expand = o.previousSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = content.style.display = "none";
  link_expand.style.display = "block";
}
function expand (o)
{
  var link_collapse = o.nextSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = "none";
  link_collapse.style.display = content.style.display = "block";
}
initExpandersClosed ();
</script>
</body>
</html>

openedexpanderクラスのdiv要素の次の要素を表示(展開)状態にする

個人的にはこの形が好み。

[任意]ファイル名:expander-demo-default-opened.html エンコーディング:UTF-8
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>openedexpanderクラスのdiv要素の次の要素の内容を初期状態で表示に</title>
<style>
.codewrapper {border:1px dashed #c99;background:#f0e9ec;padding:.5em;margin-bottom:1em}
pre {padding:2px;border:1px solid #888;background:#f5f5f5}
</style>
</head>
<body>
<div class="codewrapper">
[任意]ファイル名:hoge.txt
<div class="openedexpander"></div>
<pre>
ここは
最初見えています。
</pre>
</div>
<div class="codewrapper">
[任意]ファイル名:geho.txt
<div class="openedexpander"></div>
<pre>
ここも
表示されています。
</pre>
</div>
<script>
// "openedexpander" クラスの空のdiv要素の次の内容を
// 全て表示した状態で初期化する
function initExpandersOpened ()
{
  var elems = document.getElementsByClassName ('openedexpander');
  for (var i = 0; i < elems.length; i++)
  {
    elems[i].innerHTML = "<a href='#' onclick='expand(this);return false'>&larr; expand &rarr;</a><a href='#' onclick='collapse(this);return false'>&rarr; collapse &larr;</a>";
    elems[i].firstElementChild.style.display = "none";
  }
}
function collapse (o)
{
  var link_expand = o.previousSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = content.style.display = "none";
  link_expand.style.display = "block";
}
function expand (o)
{
  var link_collapse = o.nextSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = "none";
  link_collapse.style.display = content.style.display = "block";
}
initExpandersOpened ();
</script>
</body>
</html>

上記両方に同時に対応したもの

場面に応じて初期状態が設定できるもの。

[任意]ファイル名:expander-demo-default-set-by-classname.html エンコーディング:UTF-8
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>expander/openedexpanderのクラス別に初期状態を決める</title>
<style>
.codewrapper {border:1px dashed #c99;background:#f0e9ec;padding:.5em;margin-bottom:1em}
pre {padding:2px;border:1px solid #888;background:#f5f5f5}
</style>
</head>
<body>
<div class="codewrapper">
[任意]ファイル名:first.txt
<div class="expander"></div>
<pre>
この部分は
最初は見えません。
</pre>
</div>
<div class="codewrapper">
[任意]ファイル名:second.txt
<div class="expander"></div>
<pre>
この場所も
はじめは見えないです。
</pre>
</div>
<div class="codewrapper">
[任意]ファイル名:third.txt
<div class="openedexpander"></div>
<pre>
ここだけは
表示されています。
</pre>
</div>
<script>
// "expander" クラスの空のdiv要素の次の内容を全て折りたたみ
// "openedexpander" クラスの空のdiv要素の次の内容を全て表示
function initExpanders ()
{
  var i;
  var links = "<a href='#' onclick='expand(this);return false'>&larr; expand &rarr;</a><a href='#' onclick='collapse(this);return false'>&rarr; collapse &larr;</a>";
  var elems = document.getElementsByClassName ('expander');
  for (i = 0; i < elems.length; i++)
  {
    elems[i].innerHTML = links;
    var e = elems[i].nextSibling;
    while (e.nodeType != 1)
      e = e.nextSibling;
    e.style.display = elems[i].lastElementChild.style.display = "none";
  }
  elems = document.getElementsByClassName ('openedexpander');
  for (i = 0; i < elems.length; i++)
  {
    elems[i].innerHTML = links;
    elems[i].firstElementChild.style.display = "none";
  }
}
function collapse (o)
{
  var link_expand = o.previousSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = content.style.display = "none";
  link_expand.style.display = "block";
}
function expand (o)
{
  var link_collapse = o.nextSibling;
  var content = o.parentNode.nextSibling;
  while (content.nodeType != 1)
    content = content.nextSibling;
  o.style.display = "none";
  link_collapse.style.display = content.style.display = "block";
}
initExpanders ();
</script>
</body>
</html>

Bloggerで現在使用しているもののスクリプト圧縮版

現在のページでは “見出し:openedexpanderクラスのdiv要素の次の要素を表示(展開)状態にする” をもとに、対象のdiv要素のクラス名を “expander” クラスにしたものを使用している。JavaScriptのコードを圧縮したものをここで貼り付ける。

[挿入]Bloggerのテンプレート内、body要素の終了タグのすぐ上あたり
<script type='text/javascript'>
// <![CDATA[
function initExpanders(){var a=document.getElementsByClassName("expander");for(var b=0;b<a.length;b++){a[b].innerHTML="<a href='#' onclick='expand(this);return false'>&larr; &#34920;&#31034;&#12377;&#12427; (expand) &rarr;</a><a href='#' onclick='collapse(this);return false'>&rarr; &#25240;&#12426;&#12383;&#12383;&#12416; (collapse) &larr;</a>";a[b].firstElementChild.style.display="none"}}function collapse(c){var b=c.previousSibling;var a=c.parentNode.nextSibling;while(a.nodeType!=1){a=a.nextSibling}c.style.display=a.style.display="none";b.style.display="block"}function expand(c){var b=c.nextSibling;var a=c.parentNode.nextSibling;while(a.nodeType!=1){a=a.nextSibling}c.style.display="none";b.style.display=a.style.display="block"};
initExpanders();
// ]]>
</script>

使い方としては

  • Bloggerの “テンプレート > HTML の編集” で “</body>” のすぐ上あたりに上のスクリプトをコピペする(その位置に既にscript要素がある場合はタグで囲まれた部分のみコピーして追加)
  • 記事をHTMLソースで編集し、本文内の折りたたみ対象の要素のすぐ手前に “<div class='expander'></div>” を記述する

とすると初期状態が表示(展開)で折りたためるようになり、スクリプト無効時には余計なものが表示されず折りたたみ対象は正しく表示される。

expander” クラスの要素がdiv要素かどうかのチェックは(コード量を減らす目的で)していないため、空のdiv要素を用いるという点には注意。また、折りたたみ対象はブロック要素を想定しているため、この点にも注意が必要。

なお、動作確認はMozilla FirefoxGoogle Chromeでのみ行っているため、他のWebブラウザで正しく動作するかは不明(Internet Explorerなど、他のWebブラウザでの動作報告は歓迎)。

[1]: クリックしても何も起きないリンクなど
[2]: NodeList.length
[3]: ParentNode.firstElementChild
[4]: ParentNode.lastElementChild
[5]: Node.nextSibling
[6]: Node.nodeType
[7]: Node.ELEMENT_NODEの値
[8]: HTMLElement.style
[9]: Node.parentNode
[10]: Node.previousSibling
[11]: ここでは検索エンジンからの訪問者が検索した内容を効率良く探せるようにすること
[12]: 検索結果に出にくくなるなど
[13]: 一般に公開していないページや、検索結果に出にくくても問題のないページ、ページ内の情報が多くて最初折りたたまれていたほうがたどりやすいページなど