MyBlog内で独自に作成し、
利用している階層メニューについて

階層メニュー概要図

HTML文

CSS文

Javascript文

階層メニュー概要図

何はともあれその全体像は以下の図の通り。

CascadeMenuImage.jpg

HTML文

階層メニュー部分のHTML文は以下のようになっている。それはUL-LI-UL-LI-・・・の階層構造であり、入れ子になったリスト構造である。この構造によって入れ子の部分をまとめて管理できるので、親−子−孫−構造こそが階層メニューの真骨頂ではないだろうか。

なお、メニューの各リスト項目をわざわざ<p>タグで囲ったのは、この部分をブロック扱いにするためである。(それによって文字部分ではない余白にマウスが重なっても当該部分をハイライトすることが出来る。)<p>タグを用いずにcssで各メニュー項目をブロック扱いする方法もあるが、この方が筋が通っていると考えた。


【ポイント】

  1. DOMによりHTML要素を直接Objectとしてコントロール出来るので、要素に対するidやclass指定は極めて少なくて済む。(idやclass指定はCSSによるコントロールのためのみに必要になるに過ぎない。Javascriptからは一切不要である。)
  2. 階層の深さには何ら制限がなくいくらでも深くできる。
  3. 階層メニューを隠蔽するイベントは第2階層だけに記述すれば良く、第3階層以下の諸階層では当該階層を表示するイベントだけを記せばよい。入れ子構造になっているからこそ可能となる利点であろう。
  4. よく見かける階層メニューは、次のようなものが多く、各々の欠点を有していると思う。
    1. tableを利用したもの(HTML文の可読性が低下する)
    2. tableタグに限らずリストタグを利用しない場合、一般にHTML文の可読性が低下する。
    3. 各階層が入れ子関係になっていないもの(これも同様に可読性が低下する)
    4. 入れ子関係にない場合、position absolute指定によって表示位置を設定せざるを得なくなる。
  5. ここでは見やすさのために適当に改行を入れたが、改行文字もDOMのnodeとしてカウントされるため(IEでは無視されるが)、実際には削除する必要のある改行もある。
             ■階層メニュー HTML文■
<!--▼階層メニュー開始-->
<ul id="gmenu">
  <li onmouseover="showMenu(this,childwidth)" onmouseout="hideMenu(this)">
    <p>MainList1</p>
    <ul class="submenu1">
      <li><p>SubList1-1</p></li>
      <li><p>SubList1-2</p></li>
      <li><p>・・・・・</p></li>
      <li><p>SubList1-n</p></li>
      <li><p>・・・・・</p></li>
    </ul>
  </li>
  <li onmouseover="showMenu(this,childwidth)" onmouseout="hideMenu(this)">
    <p>MainList2</p>
    <ul class="submenu1">
      <li onmouseover="showMenu2(this,childwidth)"><p>SubList2-1</p>
        <ul class="submenu2">
          <li><p>SubList2-1-1</p></li>
          <li><p>SubList2-1-2</p></li>
          <li><p>・・・・・</p></li>
          <li><p>SubList2-1-n</p></li>
          <li><p>・・・・・</p></li>
        </ul>
      </li>
      <li onmouseover="showMenu2(this,childwidth)">><p>SubList2-2</p>
        <ul class="submenu2">
          <li><p>SubList2-2-1</p></li>
          <li><p>SubList2-2-2</p></li>
          <li><p>・・・・・</p></li>
          <li><p>SubList2-2-n</p></li>
          <li><p>・・・・・</p></li>
        </ul>
      </li>
      <li><p>・・・・・</p></li>
      <li><p>SubList2-n</p></li>
      <li><p>・・・・・</p></li>
    </ul>
  </li>
  <li onmouseover="showMenu(this,childwidth)" onmouseout="hideMenu(this)">
  ・・・(以下略)
</ul>
<!--▲階層メニュー終了-->

■階層メニュー HTMLブロックイメージ■

ブロックイメージ図

CSS文

関連するCSS文の全てを掲載すると膨大になるし、その必要もないと思われるので要点となるCSS文をピックアップする。

id:gmenu
階層メニューの「総元締め」はid:gmenuなるこのulブロックである。
#gmenu {
  font-size:100%; line-height: 1.2;
  margin: 0;
  padding-left:0;
  padding-bottom:0.7em;
  list-style-type:none;
}
width、color、background-color等はid=gmenuの親タグ内で設定してあるが、必要に応じて#gmenu内で設定する。
class:submenu1
.submenu1 {
 position:absolute;  z-index:9;
 display:none; /*初期は隠蔽*/
 margin-top:-1.35em; /*menuタイトルにmenu項目をほぼ並べるため*/
 margin-left:160px; /*menuタイトルの160px右にmenu項目を配置*/
 color: #def; background-color: #333;
 text-align: left; 
 border: 1px solid #def;
 list-style-type:none;
}
ポイントは<ul id="gmenu">からgmenuの幅相当分だけ右にずらすためにmargin-leftを適切に設定することと、<ul id="gmenu">のlist項目行のほぼ右側に来るように、margin-topをマイナス指定することにある。なお、このブロックのwidth指定は、menulistの文字数がそれぞれに異なる場合が一般的なため、Javascript内で関数の引数childwidthにより指定する。
class:submenu2
.submenu2 {
 position:absolute;z-index:10;
 display:none;/*初期は隠蔽*/
 margin-top:-1.35em;/*margin-leftはJavascriptにて定義する*/
 color: #def; background-color: #333;
 text-align: left; 
 border: 1px solid #def;
 list-style-type:disc;
}
このulブロックはその展開元のメニューアイテム(<ul class="submenu1">の<li>項目)のwidthが、一般的に項目毎に異なるために、submenu2ブロックの左端部位置が各々異なる点が味噌である。それ故に各々のsubmenu2の左端位置をJavascriptによって設定し、cssではmargin-leftを設定しない。またsubmenu1同様にwidthについても関数の引数で指定し、Javascriptで設定する。

Javascript文


【ポイントあるいは弁明】

いかにしてちらつきを解消したのか、それが最大のポイントであり最も難しかった。onmouseoverであるブロックを表示することは簡単に出来るしサンプルも沢山ある。しかし、それを隠蔽する手法となると、領域外でクリックさせるものは多々あるものの、onmouseoutで隠蔽するスクリプトについては、少なくとも多段階層メニューのサンプルという点では余り見かけなかった。

そのため半年前からつい先日まで、我流のonmouseout → style.display="none"だけで階層メニューを隠蔽していたのだが、この方法には致命的欠陥があった。それがマウスカーソルをメニュー項目間で移動する度に頻繁に発生するちらつきである。


入れ子構造になっているulブロック内にはliタグもpタグもaタグもある。つまり多くのHTML要素が混在している。言い換えればDOMでいうnodeが沢山ある。問題は、最外側にあるnode内においてonmouseoutを設定した場合において、その内側にある任意のnodeからマウスが離れた時にも、onmouseoutイベントが発生したとbrowserが解釈してしまうことにあったのだ! そしてそのことが半年の間解明できずに時が流れ、あるサイトに遭遇してやっと解明できたのだ。


ちらつきを解消するにはどうすればよいのか? 最外側に設定されたonmouseoutイベントが入れ子になったその内部にあるタグには影響しないようにすれば良いのだろうが、それはどうするのか?

onmouseoutを最内部のタグに設定すれば、その子nodeはないのだからちらつかないかも知れないが、そうなるとメニュー項目の数だけの沢山のタグに全てonmouseoutを設定しなければならず、とても現実的ではない。

いっそのこと、階層メニュー内部の任意の位置、あるいはその最外側よりも外側の任意の箇所でマウスをクリックすれば隠蔽されるようにしてしまおうか? それは実に簡単に可能なことなので、誘惑に屈してしまおうかと思ったことも一度ならずあった。易きに流れることはたやすいことだから・・・。

しかし、しかしである。半年も事実上放置してその解決がクリックを要求する手法では余りに情けない! スマートに消えると共に、ちらつかないようなさっぱりした階層メニューこそが欲しているものであり、また実現できればそれに越したことはないのだから、もう一踏ん張りするしかない───と思っているその時に、あるサイトに遭遇したのだ。


それはある個人Webマスターの喜怒哀楽/entryである。このサイトから重大なヒントを得ることが出来たのだ。

それは「タイマー起動によって隠蔽動作を遅延させ、かつonmouseoverになった瞬間にそのタイマーをキャンセルしてしまう」──これである。この手法がこのサイトに的確に示されていたのである。

かくして完成したスクリプトは以下の通りである。まだまだ不勉強で改善すべき点は多々あると思うが、とりあえずスマートに動作する階層メニューとして一応の完成を自画自賛する次第である。


【課 題】

  1. ある関数のサブルーチン関数をうまく設定できない。つまり関数間での引数のやりとりがうまくコントロールできない。
  2. 任意のnode内の文字列取得がうまく操作できない。
  3. 半年前に徒に設定したidやclassが現時点でもなお、整理されていない。

多段階層メニューJavascript文

関連するJavascript文は長くなるが、以下に全部抜粋しておく。

※ コメント文は当該行以下、次のコメントの前行までのスクリプトの説明。

05/06/08バグ取り修正版

/*++++++++++++++++++++++++++++++++++++++++++      第2階層メニューの表示 ++++++++++++++++++++++++++++++++++++++++++*/

 //Global variable var target="";  //前もってtimer1をセットしておく。 timer1=setTimeout("",500);  /*++++第2階層表示関数++++*/ function showGmenu(which,childwidth) {   //次行でタイマーを解除する。  clearTimeout(timer1);   //ulの全ての子Nodeを捕捉(liもブロック変数も含まれる)  var tempTarget = which.parentNode.childNodes;    //子ノードを順番に辿る。   for (i=0; i < tempTarget.length; i++){    //第二子Nodeがない場合戻る。目的以外のタグを排除   if (tempTarget[i].childNodes[1] == null) continue;    //ul-li-[1stchild]   var menuitem = tempTarget[i].firstChild;    //submenu=ul-li-1stchild=[2ndchild]   var submenu = tempTarget[i].childNodes[1];    //該当Menu表示   if (tempTarget[i] == which) {    with (submenu.style){     display = "block";     width = childwidth + "px" ;    }     //該当MenuItemの色替え    with (menuitem.style) {     backgroundColor = "#dd0";     color = "black";    }   } else {     //非該当menu隠蔽    submenu.style.display = "none";     //非該当MenuItemの色戻し    with (menuitem.style) {     backgroundColor = "#333";     color = "#dd0";    }   }  } }//END function

/*++++++++++++++++++++++++++++++++++++++++++       階層メニューの隠蔽 ++++++++++++++++++++++++++++++++++++++++++*/

 /*++++(1)タイマー起動++++*/ function hideGmenu(which) {  target = which;  timer1 = setTimeout("hide()",500); }  /*++++(2)タイマーにより0.5秒後に隠蔽++++*/ function hide() {   //当該MainMenuItemの色戻し  with (target.firstChild) {   style.backgroundColor = "#333";   style.color = "#dd0";  }   //当該MainMenulistの隠蔽  target.childNodes[1].style.display = "none"; }//END function

/*++++++++++++++++++++++++++++++++++++++++++      第3階層メニューの表示 ++++++++++++++++++++++++++++++++++++++++++*/

function showGmenu2(which,childwidth) {  for (var i=0; which.parentNode.childNodes[i]; i++){    //ul-li-ul-[li]   menulist = which.parentNode.childNodes[i];   if (!menulist.hasChildNodes()) continue;    //ul-li-ul-li-[p]   menuitem = menulist.firstChild;    //ul-li-ul-li-p=[ul]   submenu = menulist.childNodes[1];   (document.all)? listwidth = 180: listwidth = 180 - 20   if (menulist == which) {     //該当menulist    with (menuitem) {     style.backgroundColor = "#dd0";     style.color = "black";    }    with (submenu) {      //該当menuを表示     style.display = "block";      //該当menuの左マージンを設定     if (document.all) style.marginLeft = listwidth -10 + "px";     else style.marginLeft = listwidth +10 + "px";      //展開する第3階層のmenu幅     style.width = childwidth + "px" ;    }   } else {     //非該当menu隠蔽    submenu.style.display = "none";    with (menuitem) {     //他のmenulistの色戻し     style.backgroundColor = "#333";     style.color = "#dd0";    }   }  } }//END function