このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

ブラウザでミニマムXML (3):作る

前回の「描く」において既に、次のメソッド群が登場しました。

  1. document.createElement(name)
  2. document.createTextNode(data)
  3. Element#appendChild(node) (ElementクラスのappendChildメソッドの意味)
  4. Element#setAttribute(name, value)

これだけあれば、要素ツリー構造を作るには十分です。が、ブラウザでは色々とやっかいなことがあります。今回は、ブラウザ特有の事情を調べます。

「作る」の内容は1回分にはおおかったので、これは前半となります。今回は、最初の注意と「オマケ」が一番役にたつかもね。

内容:

  1. 混乱しがちな概念と用語:ルート、文書ノード、文書要素
  2. 文書ノードはどうやって作る
  3. 作る例:こんなにメンドー
  4. XML DOMとHTML DOM
  5. オマケ:clearでハマった
  6. 今回のまとめ


●混乱しがちな概念と用語:ルート、文書ノード、文書要素

文書ツリーの「ルート」って言葉は要注意ですよ。ほんとのルートは文書(Document)ノードです。が、文書要素(document.documentElementでアクセスできる)を「ルート」と呼ぶ場合もありますからね。

ところで、ほんとのルートである文書ノードはなぜあるのでしょう? 文書要素の兄弟の位置にコメント、PI、空白テキスト、DOCTYPE宣言なんかが来ることがあるので、これらの親ノードが必要です -- それが理由のひとつ。もうひとつ、ツリー構造とは直接の関係がないのですが、文書ノードが新しいノードの生成(オブジェクト・ファクトリー)機構を与えているのです。

DOMでは、new Element("div") のようにして新しいノードを作れません。代わりに、document.createElement("div") とします。この方法だと、すべてのノードに生成元の文書ノードが存在するので、身元保証ができます。また、生成元文書ノードごとに、村というか部族というか、まーコロニーを作るんです。

いま述べた文書ノードの役割をまとめれば:

  1. 文書ツリーのほんとのルート
  2. 新しいノードの生成装置
  3. ノード部族の長<おさ>、統括者

●文書ノードはどうやって作る

ブラウザには、window.document(あるいは単にdocument)としてアクセスできる文書ノードが最初から備わっています。では、documentとは別の新しい文書ノードを作るにはどうしたらいいのでしょうか。

標準仕様(W3C DOM)に従うなら、次のようにします。


// createDocumentに渡す引数は、
// とりあえず気にしないでください。
var newDoc = document.implementation.createDocument("", "", null);

しかし、すべてのブラウザが有効なdocument.implementationを持っているわけではないので、実際には次のような関数を作って対処する必要があります。


function createNewDocument() {
var newDoc;
if (document.implementation &&
document.implementation.createDocument)
{
newDoc = document.implementation.createDocument("", "", null);
} else if (window.ActiveXObject) {
newDoc = new ActiveXObject("Microsoft.XMLDOM")
} else {
newDoc = null;
}
return newDoc;
}

●作る例:こんなにメンドー

第2回「描く」では、次の例を、HTML文書内に直接書き込みました


<div id="target">
<h2>サンプル</h2>
<p>こんにちは。</p>
<p>これは<em>サンプル</em>です。</p>
</div>

今度は、スクリプトによりサンプルを作り出しましょう。以下の関数です。(ノード生成装置としての)文書ノードを関数引数にしています。


function makeSample(doc) {
// doc:Document
// return:void

// 素材の要素を準備
var div = doc.createElement('div');
var h2 = doc.createElement('h2');
var p_1 = doc.createElement('p');
var p_2 = doc.createElement('p');
var em = doc.createElement('em');

// テキストを加えながら組み立てる
// <h2>サンプル</h2>
h2.appendChild(doc.createTextNode("サンプル"));
div.appendChild(h2);
// <p>こんにちは。</p>
p_1.appendChild(doc.createTextNode("こんにちは。"));
div.appendChild(p_1);
// <p>これは<em>サンプル</em>です。</p>
p_2.appendChild(doc.createTextNode("これは"));
em.appendChild(doc.createTextNode("サンプル"));
p_2.appendChild(em);
p_2.appendChild(doc.createTextNode("です。"));
div.appendChild(p_2);
// id属性
div.setAttribute('id', 'target');

// 結果を戻す
return div;
}

ウヘー、DOM APIでツリーを作るのって、メンドーですねー!素材(ノード)を作っては組み立てる作業をシコシコとやるしかありません。が、このメンドーな作業を経験しないとツリー構造に触る実感を得にくいと思いますよ。

素材を生成してから一気に組み立てるか/生成しながら組み立てるか、とか、ツリーの上から組み立てるか/下から組み立てるか、などは趣味の問題です。どうぞご自由に。

XML DOMとHTML DOM

ここから先は実物の動きとそのソースを参考にしながら読んでください(いちいち文中にソースを引用しません)。

makeSample(document)として作ったサンプルと、makeSample(createNewDocument())として作ったサンプルを、前回で準備したdrawElement関数で描いてみます。次のリンクをクリックすれば試せます。必要ならソースも眺めてください。

[HTML内のサンプルを描く]ボタンと[非表示のサンプルを描く]では少し様子が違います。そうです、documentから作った要素では、タグ名が大文字化されます。一方、新しく生成した文書ノードから作った要素では、タグ名の変換は行われません。

XMLでは、名前の大文字小文字は厳密に区別される(例:div, DIV, Divはみんな別物です)のでcreateElementに指定した名前を変えてしまうようなことは許されません。これが守られてないってことは、つまり、documentとして参照されるアノ文書ノードはXML DOM(DOMコア)の文書ノードと考えることはできない、ということです。

実際のところ、ブラウザ備え付けのdocumentは、XMLのDOMではなくて、HTML専用のDOM、それもW3CのHTML DOMでさえなくて、かつてブラウザが勝手に実装したDOMもどき(当時DOM仕様がなかったのだから、「もどき」は可哀想だが)です。アノdocumentは、互換性の呪縛から逃れることができないので、レガシーHTMLをも対象とした“DOMのようなもの”だと割り切ったほうがいいでしょう。XML DOMの挙動を期待してはいけません。

新規の文書ノードはXML DOMの文書ノードと考えても(たいていは)大丈夫です。しかし、これはいいことばかりではありません。HTMLが持っていた豊富な機能性が使えないのです。例えば、getElementByIdメソッドは存在しないか、動かないか、動くかも知れないがお手軽にはいかないかです。

こういう状況が改善されるためには、XHTMLマークアップが一般化して、XML DOM(DOMコア)の拡張としてのHTML DOMが、ブラウザのdocumentとして実装される必要があります。いったい、いつになることやら。はたして、そのようになるかどうかもあやしいですしね。

●オマケ:clearでハマった

サンプルを作っているとき、キャンバスをクリアするためにclear()という関数を作りましたが、これがサッパリ動かないのです。2つのミスを犯していました。その1つは、組み込みclear関数(メソッド)の存在を知らなかったことです。

document.clear()という、文書内容を削除する関数があります。ボタンのクリックハンドラでclear()を呼んでいたのですが、document.clear()が見えていたのですね。JavaScriptの通常のスコーピングでは、大域スコープと関数内(局所)スコープしかないのだけど、ハンドラは例外で、DOMツリーの入れ子がスコープの入れ子になってしまう(これ良くない仕様だと思います)ので、ハンドラからdocumentのプロパティが見えてしまうのです。

ちなみに、clear()を呼んでも自分自身(当該の文書)は削除されないようです。clearは歴史的経緯で残っているけど、非推奨の関数とのこと。それと、window.printってのもあるので、自前でprintなんて関数を作らないほうが無難でしょう。

2番目のミスは、子ノードを削除するのに、次のような素朴なループを回していたことです。


var children = parent.childNodes;
for (var i = 0; i < children.length; i++) {
parent.removeChild(children[i]);
}

インデックスがズレていくから、こりゃダメです。次のように直しました。


while (parent.fistChild) {
parent.removeChild(parent.firstChild);
}

インデックスを使いたいなら、オシリ(末弟)から削除していきます。


var children = parent.childNodes;
for (var i = children.length - 1; i >= 0 ; i--) {
parent.removeChild(children[i]);
}

DOMでなくてもよいなら、一番手っ取り早いのはinnerHTMLを使う方法です(Firefoxでも動きます)。


parent.innerHTML = "";

DOM Rangeを使う方法もあります(IEでは動かない)。


var r = document.createRange();
// 子ノードが存在すると仮定して
r.setStartBefore(parent.firstChild);
r.setEndAfter(parent.lastChild);
r.deleteContents();

●今回のまとめ

XML DOMとHTML DOMについて、もう少し言うべきことが残っているので、それは次回にします。

  1. 文書ノードと文書要素を区別しよう。
  2. 文書ノードは、ノード生成機構とノード群の統括者の役割も持つ。
  3. 新しい文書ノードを作るには、document.implementation.createDocumentメソッド、またはActiveXObjectコンストラクタを使う。
  4. DOM APIで要素ツリー構造を作るのはけっこう面倒。
  5. ブラウザのdocumentは、XML DOMの文書ノードではない。
  6. 新しく作った文書ノードはXML DOMと思ってよいが、documentに比べて機能が貧弱である。
  7. オマケ:ハンドラのスコープには注意しよう。ハンドラからは大量のプロパティが見えてしまうから。
  8. オマケ:子ノード群を順方向インデックスで削除しようとするヤツはアホである。