jQueryでドロップダウンメニューをつくるまでの4ステップ

WordPressのメニューなどを、jQueryを使ってちょっとかっこいいドロップダウンメニューにするためのメモ。

あまり凝ったデザインや演出はありませんが、実はほとんどがCSSで対処できますのでノンプログラマーの人におすすめです。

ステップ1: メニューのHTMLを準備する。

メニューのHTMLは以下のようなネストしたリストを前提としています。

<ul id="nav">
    <li><a href="a.html">製品案内</a></li>
    <li><a href="b.html">会社概要</a>
        <ul>
            <li><a href="c.html">交通アクセス</a></li>
        </ul>
    </li>
<ul>

ステップ2: CSSを準備する。

CSSは以下のような感じで。背景などの装飾は一切加えていないテンプレートのようなものです。

#nav, #nav ul
{
    margin: 0;
    padding: 0;
    list-style-type: none;
}
#nav > li
{
    float: left;
    position: relative;
    height: 40px;
    line-height: 40px;
}
#nav a
{
    display: block;
    white-space: nowrap;
}
#nav ul
{
    display: none;
    position: absolute;
    top: 40px;
    left: 0px;
    z-index: 10;
}

ステップ3: JavaScriptを用意する。

意外なことにJavaScriptは、とてもシンプル。
以下のソースを</body>の直前にコピペしてください。

jQueryはあらかじめ読んでおいてくださいね。

$('#nav > li').each(function(){
    if ($('ul', this).length) {
        $(this).addClass('has_child');
    }
    $(this).bind('mouseenter', function(){
        $('ul', this).slideDown(200);
    });
    $(this).bind('mouseleave', function(){
        $('ul', this).hide();
    });
});

ステップ4: かっこよくする。

ここから先はデザインに応じてCSSを試行錯誤することになります。

  • #navに背景を設定する。
  • #nav ulに背景を設定する。
  • a要素に背景を設定する。
  • a:hoverで背景を変える。
  • 行の高さや文字サイズ、余白などの微調整。(特に孫となるリスト要素などに注意。)
  • このJavaScriptではサブメニューがあるli要素にhas_childという値を持つクラス属性をつけていますので、たとえば三角のアイコンを設置するなどのCSSを設定する。

はい、実はステップ4が最も大変なのでした。^^

以下のようなCSSと組み合わせると幸せになれると思います。

CSS3 Fancy Menu / LavaLamp Menu

JavaScriptは苦手だけどCSSはいけるぜ!という人はぜひ挑戦を。^^

JavaScriptでHTMLエスケープ

昨日、公開した WordPress プラグイン “mce_preformatted” は、整形済みテキストを本文に挿入するために、 textarea 内に入力されたテキストの HTML エスケープを行っている。

通常、HTML のエスケープには正規表現を使用するが、このプラグインでは違うアプローチで HTML をエスケープしている。

通常の方法

以下は正規表現を使用した方法。

function htmlEscape(s){
	s=s.replace(/&/g,'&amp;');
	s=s.replace(/>/g,'&gt;');
	s=s.replace(/</g,'&lt;');
	return s;
}

今回使用した方法

obj.innerText (Firefox では obj.textContent)を使用した方法。

function htmlEscape(s){
    var obj = document.createElement('pre');
    if (typeof obj.textContent != 'undefined') {
        obj.textContent = s;
    } else {
        obj.innerText = s;
    }
    return obj.innerHTML;
}

結果は全く同じ。
たいした根拠はないのだが、正規表現よりもエコな気がするんだけど、どうなんだろう?

String 型のメソッドとして登録する

以下のようにすると、さらにエレガントかもしれない。

String.prototype.htmlEscape = function(){
    var obj = document.createElement('pre');
    if (typeof obj.textContent != 'undefined') {
        obj.textContent = this;
    } else {
        obj.innerText = this;
    }
    return obj.innerHTML;
}

こうしておくと、以下のように書けるので、ちょっとだけかっこいい。

var str = '<strong>Cool!</strong>';
alert(str.htmlEscape());

PHP5のDOM拡張モジュールでHTMLをパースする

PHP5で追加されたDOM拡張モジュールではHTMLのパースも可能である。
さらに、このDOM拡張モジュールは標準でインストールされている。

今回の記事では、このDOMDocumentクラスについてのおぼえがき。

ちなみに、DOMが苦手な人は”要素”とか”属性”を理解できずに、まとめてタグとしてしか理解出来ていないひとが多い気がする。

HTMLを読み込む

まずはじめに、DOMDocument オブジェクトを生成する。

$doc = new DOMDocument();

そのあとで、HTMLを読み込む。
以下のようにloadHTML()などを使用すると閉じタグのないHTMLでもパース可能になる。

$doc->loadHTML('<html></html>'); // HTMLソースを読み込む
$doc->loadHTMLFile('/path/to/index.html'); // HTMLファイルを読み込む

DOM拡張モジュールによるHTMLの読み込みには上記のように2種類の方法が用意されており、HTMLファイルを読み込む方法は、http:// からはじまるURLでも指定可能である。

2011/03/04 追記

HTMLを読み込む際に、上記の処理では文字化けするサイトがあることがわかった。
以下のような処理を行えば解消した。

$html = file_get_contents('/path/to/index.html');
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'auto');
$dom->loadHTML($html);

参考: 2008-05-30 – hamacoの日記

HTMLをパースするために実際の処理

HTMLパースの流れはJavaScript等のDOM操作と同じなので、わかるひとにはすぐにわかると思う。

HTML内の全てのA要素を取得する。

$elements = $doc->getElementsByTagName('a');

ちなみに、パラメータで指定する要素名(上記の例ではa)は、大文字小文字を区別するようなので注意。

取得した要素がいくつあるかを取得する。

以下のような感じ。

$elements->length

取り出した全ての $elements のhref 属性を出力する。

foreach ($element as $e) {
    print $e->getAttribute('href');
}

これも大文字小文字を区別するのかな?

取り出した全ての $elements のノード値を取得する。

<a>~</a> で囲まれた部分を取得するには。

foreach ($element as $e) {
    print $e->nodeValue;
}

取り出した $elements の n 番目の属性値を取得する。

取得した最初のa要素の href 属性を取得するには?

$elements->item(0)->getAttribute('href');

参考

innerHTMLとremoveChild()

IEとfirefoxでinnerHTMLとremoveChildを使用した際の挙動が違っていたのでメモ。

以下のソースは、DIV要素を作成してoBodyオブジェクトに挿入した後で、oBodyの中身を空にする処理を行っている。(実際にはこんなことありませんが。。。)

var oDiv = document.createElement('DIV');
oDiv.innerHTML = 'おはよう';
oBody.insertBefore(oDiv, null);
oBody.innerHTML = '';
alert(oDiv.innerHTML); // IEでは何も出ない

この処理の後、firefoxではoDivはまだDIV要素として存在しているが、IEでは空のオブジェクトになってしまう。

一方で、oBody.innerHTMLの行を以下のように変更してやると、IEでもoDivオブジェクトの値が残る。

var oDiv = document.createElement('DIV');
oDiv.innerHTML = 'おはよう';
oBody.insertBefore(oDiv, null);
oBody.removeChild(oBody.firstChild);
alert(oDiv.innerHTML); // おはよう

意味分かりにくいですよね?

とにかく、ある要素の中身を空にしたい場合は、inerHTML = ”みたいな安易な方法を使うよりも、まじめにremoveChild()を使用した方がいいということです。

XMLに対するSafariのgetElementsByTagName()

JavaScriptでRSSをパースするスクリプトを作成してテストしていたら、Safariでだけうまく動作しなかった。

いろいろテストしてみたら、以下のような部分に問題があったことがわかった。

var siteLink = chnl.getElementsByTagName('link')[0].childNodes[0].nodeValue;

RSSのほうでは以下のような感じ。

-- 中略 --
<channel>
 <title>デジタルカタログ制作のデジパン</title>
 <atom:link href="http://www.digipan.jp/feed" rel="self" type="application/rss+xml" />
 <link>http://www.digipan.jp</link>
-- 中略 --

どうやら、Safariは名前空間atomのlink(atom:linkという要素)もgetElementsByTagName(‘link’)で拾ってしまうらしい。

IEやFirefoxではこのような問題はなかったが、どちらの解釈が正しいのかは不明。

そういえば、昔はFirefoxもこういう解釈だった気がするけど気のせい?

XML文字列をDOMエレメントに変換する

先日公開したJavaScriptでクロスドメインでのXMLアクセスを可能にするライブラリ「CrossOver」は、コールバック関数への引数がXML形式のテキストデータです。

DOMオブジェクトで戻せば余計な処理をゴリゴリ書く必要がなくて、親切だったのかもしれませんが、XML.ObjTreeなどの他の優秀なライブラリとの連携を前提にすることで、最小限の機能だけを提供することが可能になり、信頼性の確保が可能になると考えました。

それに実は私、アクションスクリプトは詳しくありません。

ただ、そうはいっても、やっぱりDOM操作をゴリゴリやりたいという声も多いようなので、XML形式のテキストデータをDOMオブジェクトに変換する関数を書いておきます。

// xmlStringをDOMオブジェクトに変換
function parseFromString( xml ){
  var root;
  if ( window.DOMParser ) {
    var xmldom = new DOMParser();
    xmldom.async = false;
    var dom = xmldom.parseFromString( xml, "application/xml" );
    if ( ! dom ) return;
    root = dom.documentElement;
  } else if ( window.ActiveXObject ) {
    xmldom = new ActiveXObject('Microsoft.XMLDOM');
    xmldom.async = false;
    xmldom.loadXML( xml );
    root = xmldom.documentElement;
  }
  return root;
}

CrossOverで定義したコールバック関数に渡された値を上記関数に渡してやると、戻り値がDOMオブジェクトに変身します。