この文書の所在
Junkieta.netLog → MSIE-DOM, 2006-01-23

MSIEDOM Level2 HTML

  1. はじめに
  2. DOM Level2 HTMLのプロトタイプ
  3. MSIE向けにElementコンストラクタ翻訳
  4. 要素別のプロトタイプも用意
  5. 名前空間(NS)メソッドの実装
  6. 既知の問題

はじめに

XML/(X)HTML文書の動的な制御には、DOMがよく使われるけれども、MSIEでは云々…という話がしょっちゅうある。ここでの目的は、ぽかぽかWeb研究室DOM Level 2 標準技術をMSIEで使うシリーズ(イベント、基本操作XML読み込みとXPath)と同様、MSIEも視野に入れつつなんとか標準技術の便利さを享受しよう、というアプローチ。別名、悪あがき。

今回はECMAScript(JavaScript,JScript)を用いてDOM2HTMLのプロトタイプをあれこれいじれるようにしてみたい。

DOM Level2 HTMLのプロトタイプ

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向けにElementコンストラクタ翻訳

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
}

これで、要素別のプロトタイプいじりも可能になる。たとえばulol要素にli要素を生成・削除するメソッドを持たせるなど、便利に使えるやも。要素を追加するほどメモリも食うことになるが。

名前空間(NS)メソッドの実装

名前空間で分けられている語彙を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でしか動かなくなるという本末転倒なことになりかねないので。

既知の問題

  1. プロトタイプへの参照ではなく生成した要素にプロパティを直接代入しているため、プロトタイプの変化が生成後の要素に反映されない
  2. for in ループでの代入のため、Nativeのメソッドと同名のプロパティは列挙されず、上書き不可能(たとえばtoString)。
webmaster@junkieta.net
published:2006-01-23,updated:2006-02-17