前回の「描く」において既に、次のメソッド群が登場しました。
- document.createElement(name)
- document.createTextNode(data)
- Element#appendChild(node) (ElementクラスのappendChildメソッドの意味)
- Element#setAttribute(name, value)
これだけあれば、要素ツリー構造を作るには十分です。が、ブラウザでは色々とやっかいなことがあります。今回は、ブラウザ特有の事情を調べます。
「作る」の内容は1回分にはおおかったので、これは前半となります。今回は、最初の注意と「オマケ」が一番役にたつかもね。
内容:
- 混乱しがちな概念と用語:ルート、文書ノード、文書要素
- 文書ノードはどうやって作る
- 作る例:こんなにメンドー
- XML DOMとHTML DOM
- オマケ:clearでハマった
- 今回のまとめ
●混乱しがちな概念と用語:ルート、文書ノード、文書要素
文書ツリーの「ルート」って言葉は要注意ですよ。ほんとのルートは文書(Document)ノードです。が、文書要素(document.documentElementでアクセスできる)を「ルート」と呼ぶ場合もありますからね。
ところで、ほんとのルートである文書ノードはなぜあるのでしょう? 文書要素の兄弟の位置にコメント、PI、空白テキスト、DOCTYPE宣言なんかが来ることがあるので、これらの親ノードが必要です -- それが理由のひとつ。もうひとつ、ツリー構造とは直接の関係がないのですが、文書ノードが新しいノードの生成(オブジェクト・ファクトリー)機構を与えているのです。
DOMでは、new Element("div")
のようにして新しいノードを作れません。代わりに、document.createElement("div") とします。この方法だと、すべてのノードに生成元の文書ノードが存在するので、身元保証ができます。また、生成元文書ノードごとに、村というか部族というか、まーコロニーを作るんです。
いま述べた文書ノードの役割をまとめれば:
- 文書ツリーのほんとのルート
- 新しいノードの生成装置
- ノード部族の長<おさ>、統括者
●文書ノードはどうやって作る
ブラウザには、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について、もう少し言うべきことが残っているので、それは次回にします。
- 文書ノードと文書要素を区別しよう。
- 文書ノードは、ノード生成機構とノード群の統括者の役割も持つ。
- 新しい文書ノードを作るには、document.implementation.createDocumentメソッド、またはActiveXObjectコンストラクタを使う。
- DOM APIで要素ツリー構造を作るのはけっこう面倒。
- ブラウザのdocumentは、XML DOMの文書ノードではない。
- 新しく作った文書ノードはXML DOMと思ってよいが、documentに比べて機能が貧弱である。
- オマケ:ハンドラのスコープには注意しよう。ハンドラからは大量のプロパティが見えてしまうから。
- オマケ:子ノード群を順方向インデックスで削除しようとするヤツはアホである。
- ハブエントリ(全体目次)
- 次回 -