JavaScript から HTML Element の class 属性を取得する方法

JavaScript から HTML 要素の class 属性を読み取ったり変更したりしたいとき、DOM の Element#getAttribute メソッド および Element#setAttribute メソッド を使用することができます。

// HTML 要素の取得
var elem = document.getElementById("XXXX");
// class 属性に設定されている値を取得
elem.getAttribute("class");

ただし、Internet Explorer 6 や 7 ではこの方法ではだめです、という話 を前に書きました。 前に書いた記事のように Element#getAttribute メソッドの引数を "className" とすれば IE6 や 7 でもちゃんと class 名を取得できますが、せっかくなのでクロスブラウザで使用できる方法を書いておこうと思います。

DOM HTML を使用して HTML 要素の class 名を取得

HTML 文書を簡単に操作するための API として DOM HTML があります。 ここでは DOM HTML を使用して HTML 要素の class 名を取得、変更する方法を書きます。 DOM HTML の Level 1 の勧告は以下のページです。

HTML 要素は DOM HTML では interface HTMLElement です。 HTMLElement にはプロパティ *1 className があり、そのプロパティによって HTML 要素の class 名の取得 *2 や変更ができます。

// HTML 要素 (interface HTMLElement を実装しているオブジェクト) を取得
var elem = document.getElementById("XXXX");
// class 名の取得
var className = elem.className;
// class 名の設定
elem.className = "test";

DOM HTML は現在主に使われているブラウザには実装されていますので、ブラウザの判定などせずにどのブラウザでも同じ方法で class 名の取得、変更ができます。

複数の class 名が設定されている場合

HTML 要素には、space characters を区切りにして複数の class 名を設定する事ができます。

<div class="className1 className2">
  ...
</div>

ここで、space characters は U+0020、U+0009、U+000A、U+000C、U+000D の 5 文字 *3 です。 というわけでこの 5 文字を区切り文字として string#split メソッドを使用すれば複数の class 名を配列で取得できます。

// HTML 要素 (interface HTMLElement を実装しているオブジェクト) を取得
var elem = document.getElementById( "XXXX" );
// class 名を space characters 区切りで取得 (不要なスペースは削除する)
var classNames = elem.className.
                 replace( /(?:^[\x09\x0A\x0C\x0D\x20]+)|(?:[\x09\x0A\x0C\x0D\x20]+$)/g, "").
                 split( /[\x09\x0A\x0C\x0D\x20]+/ );

参考記事

HTML 要素にクラスを追加したり削除したりするのに便利なユーティリティメソッドを書いてみました。 良ければ参考にしてください。

*1:正確には "属性" ですが、属性というと HTML 要素の属性とまぎらわしいのでここではプロパティと呼びます。

*2:class 属性が指定されていない場合でも、空白文字 "" が取得できます。

*3:HTML 5 の草案で確認。 (via: HTML 5 の草案 (class 属性の項目))

HTML Element をドラッグにより移動可能にする JavaScript ライブラリ

HTML Element (要素) をドラッグにより移動可能にする JavaScript ライブラリを作ってみました。

技術的なこと

とまあそれだけじゃ面白くないのでドラッグに関してちょっとだけメモを。

要素をドラッグする、と言っても実際には 「ドラッグしている」 というイベントを検出しているわけではありません。 drag イベントというものもありますが、ここでやりたい 「画像などの要素をドラッグして移動する」 ということには使えないようです。 *1

実際に使用しているのは下記の 3 つのイベントです。

  • mousedown イベント : ドラッグの開始
  • mousemove イベント : ドラッグ中のマウスの移動
  • mouseup イベント : ドラッグの終了

移動させたい要素に mousedown イベントを受け取るイベントリスナを追加しておき、ドラッグの開始を検出します。 また、ドラッグ中のマウスの移動は mousemove イベントの検出により検知します。 ただし、mousemove イベントを検出するイベントリスナは移動させる要素に追加するのではなく、document に追加します。 そうすることで、万一 JavaScript の処理が追いつかず移動させる要素からマウスポインタが外れてしまっても問題が起こらなくなります。 ドラッグの終了は mouseup イベントで検知します。 これを検出するイベントリスナも document に追加しておけばよいでしょう。

このように 3 つのイベントをそれぞれ処理するイベントリスナを組み合わせることで、ドラッグによる要素の移動を実現することができます。

*1:コメントでの指摘を受けて削除。 drag イベントを使用する方法もそのうち試してみたいと思います。

JavaScript における文字列置換 (String#replace メソッド)

JavaScript において文字列の置換 (文字列中のある部分文字列を別の文字列に変換する) を行うためには String#replace メソッドを使用します。

// 第 1 引数が置換対象の文字列, 第 2 引数が置換後の文字列
window.alert( "test".replace("t", "a") );
// => "aest" と表示

ここで、最初の "t" は "a" に置換されていますが、2 番目の "t" は置換されていません。 これは、デフォルトでは String#replace メソッドは最初にマッチしたものしか置換しないためです。 該当する文字全てを置換したい場合は、第 3 引数に "g" を渡す方法 (これは ECMAScript の仕様にはない模様. JavaScriptJScript の独自拡張ぽい) や、第 1 引数を g フラグ付きの RegExp オブジェクトにする方法があります。

// 第 3 引数に "g" を渡す方法
window.alert( "test".replace("t", "a", "g") );
// 第 1 引数を RegExp オブジェクトにする方法
window.alert( "test".replace(/t/g, "a") );
// => どちらの方法でも "aesa" と表示

この例ではどちらの方法でも結果は同じですが、前者は 「第 1 引数の文字列と同値のもの」 を置換し、後者は 「第 1 引数の正規表現にマッチするもの」 を置換するという違いがあります。 当然ながら正規表現を使った方が柔軟な置換を行う事ができます。

後方参照

正規表現でマッチした部分や、括弧でくくりグループ化した部分、マッチした部分よりも前全部、または後全部といったものを第 2 引数の文字列の中で指定する事もできます。

正規表現にマッチした部分は "$&"、グループ化した部分は前から順に "$1"、"$2"、"$3" ...、マッチした部分の前全部は "$`"、後全部は "$'" で指定できます。 また、$ 記号を直接出したい場合は "$$" と入力します。

// マッチした部分 (数字) を <...> で囲む
window.alert( "test100and200hogehoge4fuga".replace(/\d+/g, "<$&>") );

関数を利用したより柔軟な置換

さらに、第 2 引数には関数を指定する事もできます。 マッチした文字列や、グループ化した部分の文字列、また置換対象の文字全体などが関数の引数として渡され、関数の戻り値が置換後の文字として使われます。 関数内で条件分岐などをして置換後の文字を柔軟に変化させる事ができます。 第 1 引数としてマッチした文字列が渡されますので、簡単な置換であれば第 1 引数だけ使えば良いでしょう。

例えば、以下のように XML の組み込みエンティティ参照の文字列を実体に変換する事も簡単にできます。 (この例では各エンティティ参照を 1 種類ずつ処理しても大丈夫ですが、文字参照も絡んでくるとこの例のように一度に処理する必要が出てきます。)

// XML の組み込みエンティティ参照の文字列を実体に変換する関数
function unescapeForXML(a) {
    var str = null;
    if ( a == "&amp;" ) { str = "&"; }
    else if ( a == "&gt;" ) { str = ">"; }
    else if ( a == "&lt;" ) { str = "<"; }
    else if ( a == "&quot;" ) { str = '"'; }
    else if ( a == "&apos;" ) { str = "'"; }
    else { str = ""; } // ここは例外を発生させるべきかも
    return str;
}
var str = "teat: 200 &gt; 100 &amp;&amp; 300 &lt; 400";
new_str = str.replace(/&amp;|&gt;|&lt;|&quot;|&apos;/g, unescapeForXML);
window.alert(new_str);
// => "teat: 200 > 100 && 300 < 400" と表示される

参考

下記ページが参考になります。 第 2 引数を関数にしたときに、関数に渡される引数がどうなるかなどは ECMAScript (JavaScriptJScript の統一仕様) の仕様書をご覧ください。

IE にて a 要素の下に "@" を含む TextNode を append するとおかしくなる事がある件

<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="for_test"></div>
<script type="text/javascript">
var p_elem = document.getElementById("for_test");
var test_elem = p_elem.appendChild( document.createElement("a") );
test_elem.appendChild( document.createTextNode(" @test ") );
test_elem.setAttribute( "href", "http://example.com/" );
</script>
</body>
</html>

という HTML を表示すると、本来は

@test

となるはずであるのに IE 6, 7, 8 で表示すると

http://example.com/

となってしまう。 謎。 どうやら @ 記号がダメみたいです。

回避方法

<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="for_test"></div>
<script type="text/javascript">
var p_elem = document.getElementById("for_test");
var test_elem = p_elem.appendChild( document.createElement("a") );
test_elem.appendChild( document.createElement("span") ).appendChild( document.createTextNode(" @test ") );
test_elem.setAttribute( "href", "http://example.com/" );
</script>
</body>
</html>

という風に、a 要素の下に直接 TextNode をくっつけるのではなく、span 要素を間に入れるとこの問題は回避できる。

回避方法 2 [2009-12-24 追記]

はてブにて id:mgng さんより教えて頂きました。 下記のように Element#setAttribute を先に行い、後から Node#appendChild を行うという方法でも期待通りの実行結果となります。

<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="for_test"></div>
<script type="text/javascript">
var p_elem = document.getElementById("for_test");
var test_elem = p_elem.appendChild( document.createElement("a") );
// Element#setAttribute を先に行う
test_elem.setAttribute( "href", "http://example.com/" );
// その後で Node#appendChild を行う
test_elem.appendChild( document.createTextNode(" @test ") );
</script>
</body>
</html>

Internet Explorer 6 および 7 で class 属性を取得する方法

追記

この記事よりも良い方法を下記の記事に書きました。 下記記事を参照してください。

本文

Internet Explorer 6 (IE6) および 7 (IE7) で、(X)HTML 文書中の要素の class 属性の値を JavaScript を使って取得しようとしてなかなか上手く行かなかったのでメモ。

普通、class 属性の値を取得するためには、例えば変数 elem に Element オブジェクトが入っているとして

classValue = elem.getAttribute("class");
という風に Element.getAttribute メソッド を使用します。 当然 Firefox 3.5 や IE8 などでは上手く動きました。 が、何故か IE6 と IE7 では上手く属性値がとれませんでした。 IE6 や IE7 の場合はどうすればいいかは IEでclassの値を取得する場合の注意 - JavaScript に書いてありました。

<a class="foo" href="#">bar</a> なエレメントにおいて class の値を取得するには、通常 obj.getAttribute('class') でいいのですが、IE は属性値を className にしないととれません

というわけで、IE6, 7 でもちゃんと class 属性の値をとるためには以下のようにします。

classValue = elem.getAttribute("class") || elem.getAttribute("className");

参照先では "className" がダブルクオーテーション (") でくくられていませんが、実際には必要ですのでご注意ください。

イベントリスナの実行順序

addEventListener の実行順序 より。

(function(){
function a() {alert('a')}
function b() {alert('b')}
function c() {alert('c')}
window.addEventListener('click', a, false);
window.addEventListener('click', b, false);
window.addEventListener('click', c, false);
window.addEventListener('click', a, false);
})()

opera -> b, c, a;

firefox -> a, b, c;

ちょうどクロスブラウザな (IE でも Firefox でも Safari でも使える) イベントリスナ追加関数を作っていて、そういや同一の EventTarget に追加したイベントリスナの実行順序の保証ってした方がいいのかなぁ、と思って色々調べてみました。

補足) InternetExplorer の attachEvent では、登録順とイベントリスナの呼び出し順は何の関係もない模様。 Firefox 3.5 と Opera 9.6、Safari 4 では基本的に登録順に呼び出される。 が、上で引用したような差異がある。

とりあえず DOM 2 Events では、「同一 EventTarget に結び付けられた複数の EventListener の実行順に関する仕様は作られていない」 ようです。 DOM 2 Events の勧告 より。 Although all EventListeners on the EventTarget are guaranteed to be triggered by any event which is received by that EventTarget, no specification is made as to the order in which they will receive the event with regards to the other EventListeners on the EventTarget.

そして DOM 3 Events では 「登録順に実行される」 と規定されているみたいですが、DOM 3 Events の勧告 のどこに書いてあるのかイマイチわかんなかったです。 「同じ条件で同じ EventTarget に同じ EventListener を追加しようとした場合、それは何の効果ももたらさない (実行順も元のまま)」 というのは EventTarget のところ に書いてあったけど。

DOM 2 Events では実行順の保証はないみたいだし、とりあえず今作ってる関数では実行順を気にしないことにしよう。

名前つきの関数リテラルに関するよくわからない IE の挙動

昨日書いた 「IE でも event.currentTarget を使えるようにする」 の記事の後半部分のコードなのですが、IE8 でじっくりと動作を追ってみるとどうも期待通りの動作ではないことがわかりました。

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
return function myself(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", myself);
};
})() );
が問題の箇所で、どうも window.detachEvent("onunload", myself); の部分でちゃんと detachEvent できていないようなのでした。 unload イベントに対応させていたため発覚しづらかったのですが、よくよく調べてみると確かにちゃんと detachEvent できていません。 あれ? なんで?

ということで色々調べてみました。

で、結果としては IE における名前つき関数リテラルFirefox などとは違う挙動をする、ということがわかりました。

言葉の定義

とりあえず関数リテラルって何? というところを定義しておきます。 なんかネット上だと色んな情報があるんで。 ここは恐ろしいインターネッツですね。

私が言う関数リテラルとは

var func = function(args) {
....
};
のように、式の一部として文の中で作られる関数のこと。 あくまで関数部分 (function() { ... }) が関数リテラルです! 関数式とも言います。

マイコミジャーナルさんの記事 だと、上の例の変数 func を 「関数リテラル」、function() { ... } を 「無名関数」 などとみなしているようですが、それはちょっと違うんでないかい? と思うわけで・・・。 まあ今回の記事の争点は 「関数リテラルと無名関数の違い」 とかではないんで、うちの記事では上の定義でやりますよー、と軽く流しましょう。

名前つきの関数リテラル

なぜ関数リテラルが 「無名関数」 と呼ばれるのか、というところですが、通常関数リテラルには関数名を付けないからです。 上の例でも、変数 func というのはあくまで関数リテラルを代入しているだけであって、関数リテラル自体には名前がついていません。

ただ、関数リテラルに名前を付けることができないのかというとそうではなく、名前を付けることもできます。

var func = function myname(args) {
....
// 関数内で、自分自身を指すために myname を使うことが出来る
};
// 関数外では myname という関数は見えない
上のように名前を付けることもできます。 ただし、この名前は関数内でしか利用できません。

名前つきの関数リテラルに関する IE の挙動

で、ここからが本題なのですが、名前付き関数リテラルに関する IE の挙動に関して。

var test = function myname(f) {
window.alert(f === myname ? "一緒" : "違う");
};
test(test);

上のコードを実行すると、どういう結果になるでしょう? 私は "一緒" という文字がポップアップされると思っていました。 しかし、IE8 で実際に実行すると、なぜか "違う" という文字がポップアップされてしまうのです。 謎だ・・・。 IE はホントに斜め上過ぎて困りますね!

ちなみに Firefox 3.5 だとちゃんと "一緒" とポップアップしてくれました。

さらに色々調べると、関数外では myname という名前で関数を参照できないはずなのに、なぜか IE ではできてしまいました!

そんなわけで IE で名前付き関数リテラルを使うときは注意しましょう!

おまけ

var test = function myname(f) {
window.alert(f === myname ? "一緒" : "違う");
};
test(test); // => IE8: "違う", Firefox 3.5: "一緒"
// Firefox の場合下はエラーになる
try {
test(myself); // => IE8: "一緒", Firefox 3.5: Error (myself が未定義のため)
} catch(e) { window.alert(e); }
try {
myself(test); // => IE8: "違う", Firefox 3.5: Error (myself が未定義のため)
} catch(e) { window.alert(e); }

昨日のコードのだめなところ

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
return function myself(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", myself);
};
})() );

それで昨日のコードがどうだめなのかというと・・・。 登録するイベントリスナを関数リテラルで宣言し、それに myself という名前を付け、関数実行時に window.detachEvent("onunload", myself) で detach する、というわけですが、関数リテラルの評価結果 (?) と myselfIE では同一のものだとみなされないため、ちゃんと detach できなかった、というわけです。

修正するならこんな感じですね。

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
var cleanupListener = function(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", cleanupListener);
};
return cleanupListener;
})() );

arguments.callee 【2009.07.13 追記】

同じような内容の記事を発見したので参照。

そこに書いてあった方法なのですが、arguments.callee を使えばわざわざ関数リテラルに名前を付ける必要もないんですね! なるほどなるほど。
var f = function() {
// 自分自身
var self = arguments.callee;
// ....
};