XML/(X)HTML文書の動的な制御には、DOMがよく使われるけれども、MSIEでは云々…という話がしょっちゅうある。ここでの目的は、ぽかぽかWeb研究室のDOM Level 2 標準技術をMSIEで使う
シリーズ(イベント、基本操作、XML読み込みとXPath)と同様、MSIEも視野に入れつつなんとか標準技術の便利さを享受しよう、というアプローチ。別名、悪あがき。
今回はECMAScript(JavaScript,JScript)を用いてDOM2HTMLのプロトタイプをあれこれいじれるようにしてみたい。
ECMAScriptには、プロトタイプ(雛形)にプロパティを追加してやると、同じ雛形を用いてコンストラクタ(工場)から生成されたオブジェクトはみんな同様のプロパティを持つ/扱えるようになる、という仕組みが採用されている。で、ECMAScriptからDOM Level2 HTMLを利用できるWebブラウザなんかは、DOMで扱うHTML要素にそれぞれコンストラクタとプロトタイプを用意している。例としてgetElementsFromChildrenという「子の中から指定した名称の要素を取り出すメソッド(関数)」を、全てのHTML要素の雛形となるHTMLElement
のプロトタイプに追加してみる。
HTMLElement.prototype.getElementsFromChildren = function( name /* string */ ){
var result = new Array; // 結果を格納するリストの生成
name = name.toUpperCase(); // 名称の大文字化
for( var i = 0, children = this.childNodes; i < children.length; i++ )
if ( children[i].nodeType == 1 ) // i番目の子はHTML要素か
if ( name == '*' || children[i].tagName === name ) // 名称が一致するか
result[result.length] = children[i]; // OKなら結果に代入
return result;
};
そしてgetElementsFromChildren
を、HTML文書を元に使用してみる。
<!-- HTML文書のソース --> <div
id
="first"> <h2
>はじめに</h2
> <p
><abbr
lang
="en"xml:lang
="en"title
="eXtensible Markup Language">XML</abbr
>/(X)HTML文書の(略)</p
> <p
>今回はECMAScript(JavaScript,JScript)を用いて(略)</p
> </div
>// DOM使用例 var result = document.getElementById('first').getElementsFromChildren('p'); alert(result[1].firstChild.data); // 「今回はECMAScript(JavaScript,JScript)を用いて(略)」と出力
HTMLElement.prototype
からの継承が効いていれば、resultに格納されたp
要素も、他のHTML要素もみな同じくgetElementsFromChildren
が利用できる、ということになるわけだ。現時点でFirefoxとかOperaはこれがきっちり動く。しかしMSIEではそもそも「HTMLElement
」というコンストラクタは存在しないものとなっている。なので、このままでは実行できない。
MSIEはDOM Level2を実装していないので、HTMLElement
やその大本であるElement
もまた認識されていない。というわけで、これらをMSIE向けにでっち上げることにする。
if( !window.HTMLElement ){ // 実装済みかチェック
var DOMConstructor = function(){ // 「HTML要素にプロパティを委譲するコンストラクタ」を生成するための関数
return new Function(
'element',
'if(element===undefined) return this;' +
'for( var prop in this ) element[prop] = this[prop]; return element;'
);
}
var Element = new DOMConstructor,
HTMLElement = new DOMConstructor;
HTMLElement.prototype = new Element; // Elementと結びつけておく
document._createElement = document.createElement; // もともとのcreateElementを待避させる
document.createElement = function( name ){
return new HTMLElement( this._createElement( name ) ); // 生成した要素にHTMLElementを継承させて返す
}
}
こうしておけば、HTMLElement.prototype
へのプロパティ追加がcreateElement
で生成される要素に反映されるようになる。Element.prototype
への追加でも、HTMLElement.prototype
がプロトタイプチェーン上にあるのでオッケー。ただし、文書上に元から存在している要素には個別に対応してやらないと無理なので、window.onload
後に操作が必要。
以下に続く節はこの小細工を前提に、もう少し便利にしてみるテスト。
DOM Level2 HTMLには、前節で書いたHTMLElement
を基底として、さらにHTMLの各要素に対して(ほぼ)個別のインターフェイスが用意されている。これらも利用できるようにしてみる。
HTMLElement.elements = (function(){
var elements = { // 「要素名:DOM2での名称」という組のObjectを生成
'ol' : 'HTMLOListElement',
'ul' : 'HTMLUListElement' // 利用したい個別要素分だけ書き足す...ちとメンドイ
};
for( var name in elements ){
window[elements[name]] =
elements[name] = new DOMConstructor; // コンストラクタ生成、さらにグローバルオブジェクトにする
elements[name].prototype = new HTMLElement; // HTMLElementを継承
}
return elements; // HTMLElementから参照させる
})();
document.createElement = function( name ){ // createElementの再定義
var element = this._createElement( name );
if( HTMLElement.elements[name] !== undefined )
return new HTMLElement.elements[name]( element ); // 各要素のコンストラクタに振り分ける
else
return new HTMLElement( element ); // コンストラクタが定義されていなければHTMLElementに
}
これで、要素別のプロトタイプいじりも可能になる。たとえばul
、ol
要素にli
要素を生成・削除するメソッドを持たせるなど、便利に使えるやも。要素を追加するほどメモリも食うことになるが。
名前空間で分けられている語彙をDOMプログラミング上でも分けて使えるようにするという、XML向けの「なんとかかんとかNS」というメソッド群。この中のgetElementsByTagNameNS
もなんとか使えるようにしてみる。
HTMLElement.prototype.ns = 'http://www.w3.org/1999/xhtml'; // HTMLの全要素にnsプロパティを継承させるようにする
document.createElementNS = function( ns, name ){
if( ns === HTMLElement.prototype.ns ) {
return document.createElement( name ); // nsがXHTMLだったら変更済みのcreateElementで
}
else {
var element = new Element( document._createElement( name ) ); // 元々のcreateElementとElementを利用
element.ns = ns; // nsを個別にセット
return element;
}
}
Element.prototype.getElementsByTagNameNS = // 全てのElementに継承させる
document.getElementsByTagNameNS = function( ns, name ){
var result = new Array;
for( var i=0, elements = this.getElementsByTagName( name ); i < elements.length; i++ )
if ( elements[i].ns == ns ) result[result.length] = elements[i];
return result;
}
ここまでやれば動作する。HTML文書の場合は名前空間別の処理など必要にはならないけれども、一応可能なことは可能。
ちなみに注意点として、これらのコードはDOM Level2を実装済みの処理系に読ませると多分問題あるので、きっちり振り分けをしといた方がいいだろう。特にcreateElement
の書き換えなどは許可していない処理系もあり、振り分けがうまくいってないとMSIEでしか動かなくなるという本末転倒なことになりかねないので。
for in ループ
での代入のため、Nativeのメソッドと同名のプロパティは列挙されず、上書き不可能(たとえばtoString
)。