DOM拡張

[新着] TAG indexオフライン版 3.0 を準備中です



0   名前: yon : 2007/05/03(木) 01:35  ID:TJa6xiTw sub-Ax
【何をしたいのか】
HTMLのDOMを拡張して、例えば、
document.getElementsByTagName("div")[0].spanNum()
で、渡されたdivエレメントの中のspanの個数を返すようにしたいです。

【現在の状況】
IE7で開発しています。
Object.extend(HTMLDivElement.prototype, { ←エラー
spanNum:function(){
return this.getElementsByTagName("span").length;
}
});
上のでは、「HTMLDivElementなんてないよ」と言われます。
http://krook.org/jsdom/を参考にHTMLDivElementを入れたのですが、特定の環境のみのAPIだったようです。

【備考】
ArrayやStringであれば、上の方法で拡張できるのですが。。
IE7で開発していますが、FireFox2.0やOpera8でも動けばなお良いです。
よろしくお願いします。

1   名前: 匿名 : 2007/05/03(木) 01:35  ID:hz/7nqoj sub-Cz
ネイティブではないホストオブジェクトの扱いは実装依存。

・Firefox、Opera、Safari は、HTMLDivElement などの DOM インタフェースを JavaScript コンストラクタとして扱うから、prototype を拡張できる。
・IE は DOM オブジェクトを JScript オブジェクトとは見なさないから、拡張もできない。

と言うか、そもそも拡張を隠蔽する仕組みのない JavaScript で、ネイティブ・ホストオブジェクトの拡張は避けるべき。場合によっては、グローバル変数をばらまくコードよりタチが悪い。



(a). DOM オブジェクトの拡張に関しては、DOM Level 3 で Node#setUserData、Node#getUserData が追加された。これによりノードにあらゆるデータ(関数含む)を格納できる。しかし、これを実装しているブラウザはまだない。

(b). Function#call、Function#apply によるメソッドの転用。
var DOMExtensions = {
    spanNum : function () {
        return this.getElementsByTagName ('span').length;
    }
};

var node = document.getElementsByTagName ('div')[0];
DOMExtensions.spanNum.call (node);

(c). 新クラスの作成。
function EnumerableHTMLDivElement (element) {
    this.element = element;
}

EnumerableHTMLDivElement.prototype.spanNum = function () {
    return this.element.getElementsByTagName ('span').length;
};

var node = document.getElementsByTagName ('div')[0];
var obj = new EnumerableHTMLDivElement (node);
obj.spanNum ();

大抵は (b) か (c) で事足りる。



余談。Mozilla/Firefox 限定。
HTMLDivElement.prototype.__defineGetter__ = function ('spanNum', function () {
    return this.getElementsByTagName ('span').length;
} );

var node = document.getElementsByTagName ('div')[0];
node.spanNum;  // length のような生きたプロパティ

prototype を拡張するのなら、それが不要になった時点で必ず削除してくれ。

2   名前: 匿名 : 2007/05/03(木) 01:35  ID:hz/7nqoj sub-Cz
>>1
訂正。Safari は HTMLDivElement は持つが prototype は持たない。HTMLDivElement.prototype を拡張できるのは、現状 Gecko/Firefox と Opera のみ。まあ流れ的に、そのうち Safari も対応すると思う。

結局、拡張に大きな制限があるのは IE のみ。ひょっとして .NET か何かでできるのかもしれないが、私は知らない。



ついで。相互運用性を気にせず、技術的な関心として言えば、prototype 拡張は確かに面白い。本当は、JavaScript のように __proto__ を弄れるとプロトタイプベース言語としてサイコウなんだが、ECMA 規格外だし JScript も対応していない。Web で大っぴらに使えないというのがつまらない。

JavaScript 1.6 で追加された Array 拡張を、IE その他で使う:
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:indexOf
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:lastIndexOf
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:filter
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:forEach
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:every
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:map
http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Array:some

Mozilla/Gecko で document.all、innerText、outerHTML を使う:
http://webfx.eae.net/dhtml/ieemu/htmlmodel.html
http://webfx.eae.net/dhtml/ieemu/allmodel.html

各ブラウザの挙動を可能な限り ECMA 規格に合わせる:
http://trac.dojotoolkit.org/browser/tools/testHarness/burst/fix_ecma.js?rev=2

3   名前: yon : 2007/05/03(木) 01:35  ID:TJa6xiTw sub-Ax
>>匿名さん
細かな解説ありがとうございます。
自分、趣味で開発をしていて、近くに聞ける人がいないので知識が偏りがちです。
恥ずかしながら、Function#call、Function#applyは今回初めて知りました。
また勉強し直してきます。

DOMを自由に拡張して〜、
var node = document.getElementsByTagName("div")[0]."div要素内のspan要素の中から指定されたspan要素だけを含んだdiv要素を返す関数"."div要素の内部のspan要素を指定された規則に従い並べ替える関数"."div要素の内部のspan要素を逆順に並べ替える関数";
というように、div要素を持ちまわして処理を記述できたらステキだな〜、と思ったわけです。

すぐに検証してお返事差し上げたいところなのですが、
私の技術不足のため、検証にしばらく時間をください。
(c)の新クラスの作成で、なんとかなりそうな気がしています。
近日中にまた現れます。

--------------------
そういえばOperaの最新は9でしたね。(←自分が使ってるのに忘れてる)

4   名前: yon : 2007/05/03(木) 01:35  ID:TJa6xiTw sub-Ax
検証できましたので報告します。
以下の要素において、
<DIV id="test">
<DIV name="0"><SPAN>ken</SPAN><SPAN>19</SPAN></DIV>
<DIV name="1"><SPAN>john</SPAN><SPAN>18</SPAN></DIV>
<DIV name="2"><SPAN>mary</SPAN><SPAN>14</SPAN></DIV>
<DIV name="3"><SPAN>yasu</SPAN><SPAN>17</SPAN></DIV>
</DIV>
以下の新規クラスを作成し、
function EnumerableHTMLDivElement(element){
    this.element=element;
}
EnumerableHTMLDivElement.prototype=Object.extend({
    rlength:function(){  //Row length
        return this.element.getElementsByTagName("div").length;
    },
    clength:function(){  //Column length
        return this.element.getElementsByTagName("div")[0].getElementsByTagName("span").length;
    },
    swap:function(numa,numb){
        var tmp=this.element.getElementsByTagName("div")[numa].name;
        this.element.getElementsByTagName("div")[numa].name=this.element.getElementsByTagName("div")[numb].name;
        this.element.getElementsByTagName("div")[numb].name=tmp;
        return this;
    },
    reverse:function(){
        for(var i=0;i<this.rlength()/2;i++){
            this.swap(i,this.rlength()-1-i);
        }
        return this;
    }
});
以下のようにして動作が確認できました。
var obj=new BookDiv($("test"));
obj.swap(0,1).reverse();
/* 以下のようになる
<DIV id="test">
<DIV name="3"><SPAN>ken</SPAN><SPAN>19</SPAN></DIV>
<DIV name="2"><SPAN>john</SPAN><SPAN>18</SPAN></DIV>
<DIV name="0"><SPAN>mary</SPAN><SPAN>14</SPAN></DIV>
<DIV name="1"><SPAN>yasu</SPAN><SPAN>17</SPAN></DIV>
</DIV>
*/
とりあえずname属性だけですが、後はゴリゴリ実装していこうと思います。
ありがとうございました。

----------------

>>ネイティブ・ホストオブジェクトの拡張
アプリケーションの保守・拡張・変更時のために、不用意な拡張は避けるべきだと
分かってはいますが、、、
自分はStringとかちょこちょこ拡張してますねぇ。
Object.extend(String.prototype, {
    pickout:function(start,end){
        return this.substring(0,start)+this.substring(end,this.length);
    }
});

5   名前: 匿名 : 2007/05/03(木) 01:35  ID:uR8UqwGx sub-Cz
いやいやいや、ちょっと待って。div 要素は name 属性など持ちません
<ul id="test">
  <li class="person">
    <span class="name">Ken</span>
    <span class="age">19</span>
  </li>
  <li class="person">
    <span class="name">John</span>
    <span class="age">18</span>
  </li>
  <li class="person">
    <span class="name">Mary</span>
    <span class="age">14</span>
  </li>
  <li class="person">
    <span class="name">Yasu</span>
    <span class="age">17</span>
  </li>
</ul>

あんまり慌てたので無駄に XPath を使用。Mozilla/Firefox、Opera のみ。IE 不可。class で要素判別しているので、上記の要素名自体は変更可(リストよりもテーブル構造かも)。
<script type="application/javascript">

function getElementsByXPath (expression) {
    return document.evaluate (expression, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}

function personLength () {
    return getElementsByXPath.call (this, 'child::*[ @class="person" ]').snapshotLength;
}

function swapPersons (person1, person2) {
    if (person1 && person2) {
        var tmp = document.createElement ('span');
        person1.parentNode.replaceChild (tmp, person1);
        person2.parentNode.replaceChild (person1, person2);
        tmp.parentNode.replaceChild (person2, tmp);
    }
}

function swapPersonsByPosition (pos1, pos2) {
    var result = getElementsByXPath.call (this, 'child::*[ @class="person" ]'
        + '[ position() = ' + pos1 + ' or position() = ' + pos2 + ']');
    swapPersons.call (this, result.snapshotItem (0), result.snapshotItem (1));
}

function swapPersonsByName (name1, name2) {
    var result = getElementsByXPath.call (this, 'child::*[ @class="person" ]'
        + '[ child::*[ @class="name" ] = "' + name1 + '" '
        + 'or child::*[ @class="name" ] = "' + name2 + '"]');
    swapPersons.call (this, result.snapshotItem (0), result.snapshotItem (1));
}

function reverse () {
    var i = 0;
    var I = personLength.call (this);
    for (; i < (I / 2); i++) swapPersonsByPosition.call (this, i + 1, I - i);
}

//______________________________________________________________________

function PersonList (element) {
    this.element = element;
}

PersonList.prototype.reverse = function () {
    reverse.apply (this.element, arguments);
    return this;
};

PersonList.prototype.swapPersonsByPosition = function () {
    swapPersonsByPosition.apply (this.element, arguments);
    return this;
};

PersonList.prototype.swapPersonsByName = function () {
    swapPersonsByName.apply (this.element, arguments);
    return this;
};

//______________________________________________________________________

var pList = new PersonList (document.getElementById ('test'));

pList.swapPersonsByPosition (1, 2)
     .swapPersonsByName ('John', 'Mary')
     .reverse ();  // Yasu, John, Ken, Mary

</script>

> 自分はStringとかちょこちょこ拡張してますねぇ。

他のフレームワーク、ライブラリを使用しないことが分かりきってれば、構いません。

と言うか、prototype.js を組み込んだ時点で、モジュール性の高いコードを書くのは困難(以前は Object 汚染なんて話題もあったくらいだし)。

6   名前: 匿名 : 2007/05/03(木) 01:35  ID:uR8UqwGx sub-Cz
>>5
> モジュール性の高いコードを書くのは困難

ごめんなさい、自分で何言ってるか分からん。あと、>>5 のコードで span を生成するくらいなら Range にすれば良かったと思ったけど略。

div 要素の name 属性に関して補足すると、これに div 要素オブジェクトの name プロパティとしてアクセスできるのは、手抜き実装(あるいは歴史的な経緯?)の IE だけです。他のブラウザでは undefined ですのでご注意。

一応、IE5.5+、Firefox、Opera、Safari 対応版。急いだのでミスがあると思いますが。
// HTMLElement
function hasClassName (className) {
    var expression = new RegExp (' ' + className + ' ', 'i');
    return expression.test (' ' + this.className + ' ');
}

// NodeList
function getElementsByFilter (filter) {
    var result = [ ];
    var i = 0;
    var I = this.length;
    
    for (; i < I; i++) {
        if (this[i].nodeType == 1 /*Node.ELEMENT_NODE*/ && filter (this[i], i)) {
            result.push (this[i]);
        }
    }
    return result;
}

// Element
function getChildElements (filter) {
    return getElementsByFilter.call (this.childNodes, filter || function () { return true; } );
}

// Element
function getDescendantElements (filter) {
    return getElementsByFilter.call (this.getElementsByTagName ('*'), filter || function () { return true; } );
}

//______________________________________________________________________

function swapPersons (person1, person2) {
    if (person1 && person2) {
        var tmp = document.createElement ('span');
        person1.parentNode.replaceChild (tmp, person1);
        person2.parentNode.replaceChild (person1, person2);
        tmp.parentNode.replaceChild (person2, tmp);
    }
}

// Element
function personLength () {
    return getChildElements.call (this).length;
}

// Element
function swapPersonsByPosition (pos1, pos2) {
    var nodes = getChildElements.call (this, function (node, i) { return true; } );
    swapPersons.call (this, nodes[pos1], nodes[pos2]);
}

// Element
function swapPersonsByName (name1, name2) {
    var nodes = getDescendantElements.call (this, function (node) {
        return hasClassName.call (node, 'name') &&
               (node.firstChild.data == name1 || node.firstChild.data == name2);
    } );
    if (nodes[0]) nodes[0] = nodes[0].parentNode;
    if (nodes[1]) nodes[1] = nodes[1].parentNode;
    swapPersons.call (this, nodes[0], nodes[1]);
}

// Element
function reverse () {
    var i = 0;
    var I = personLength.call (this);
    for (; i < (I / 2); i++) swapPersonsByPosition.call (this, i, I - 1 - i);
}

//______________________________________________________________________

function PersonList (element) {
    this.element = element;
}

PersonList.prototype.reverse = function () {
    reverse.apply (this.element, arguments);
    return this;
};

PersonList.prototype.swapPersonsByPosition = function () {
    swapPersonsByPosition.apply (this.element, arguments);
    return this;
};

PersonList.prototype.swapPersonsByName = function () {
    swapPersonsByName.apply (this.element, arguments);
    return this;
};

//______________________________________________________________________

var pList = new PersonList (document.getElementById ('test'));

pList.swapPersonsByPosition (0, 1)
     .swapPersonsByName ('John', 'Mary')
     .reverse ();
             // Yasu, John, Ken, Mary

7   名前: yon : 2007/05/03(木) 01:35  ID:TJa6xiTw sub-Ax
>>div 要素は name 属性など持ちません
うわ恥ずかし・・・
直しておきます。

>>XPath
いいなぁXPathいいなぁ。
そうだよみんなIEなんか使わないでOperaにすればいいんだ!(それも極端)
というか、みんなHTMLやめてXML+XSLでWebページ書けばいいんだよ。。

短い時間に、2種も全く違うコードを上げていただき、ありがとうございます。
// NodeList
function getElementsByFilter (filter) {
    var result = [ ];
    var i = 0;
    var I = this.length;
    
    for (; i < I; i++) {
        if (this[i].nodeType == 1 /*Node.ELEMENT_NODE*/ && filter (this[i], i)) {
            result.push (this[i]);
        }
    }
    return result;
}
これの出力が、thisが階層になっていた場合どうなるのか私は知らないのですが、
おそらく全部動くと思われます。・・・はい。テストします。

ところで、差し支えなければ、
ptototypeの拡張は一度にやるより
EnumerableHTMLDivElement.prototype=Object.extend({
    rlength:function(){  //Row length
        return this.element.getElementsByTagName("div").length;
    },
    clength:function(){  //Column length
        return this.element.getElementsByTagName("div")[0].getElementsByTagName("span").length;
    }
});
分けてやる
PersonList.prototype.swapPersonsByPosition = function () {
    swapPersonsByPosition.apply (this.element, arguments);
    return this;
};

PersonList.prototype.swapPersonsByName = function () {
    swapPersonsByName.apply (this.element, arguments);
    return this;
};
理由をお教え願えないでしょうか?
それとももしかしてこれらは私のカン違いで、似てるだけで全く意味が違うとか?

あともう一つ。forに関してですが、
function reverse () {
    var i = 0;
    var I = personLength.call (this);
    for (; i < (I / 2); i++) swapPersonsByPosition.call (this, i, I - 1 - i);
}
Iを先に求めておくのは、実行速度のためということで納得できるのですが、
iを先に0と定義しておくのは何か意味があるのでしょうか?
私ならば
function reverse () {
    var I = personLength.call (this);
    for (var i = 0; i < (I / 2); i++) swapPersonsByPosition.call (this, i, I - 1 - i);
}
としてしまうのですが。

8   名前: 匿名 : 2007/05/03(木) 01:35  ID:qsklDQde sub-Cz
> そうだよみんなIEなんか使わないでOperaにすればいいんだ!(それも極端)
> というか、みんなHTMLやめてXML+XSLでWebページ書けばいいんだよ。。

同士発見! 兄貴と呼んでも良いですか。それとも、お兄様の方が良いですか。

> thisが階層になっていた場合どうなるのか

ちょっと意味がとれませんでしたが、厳密にやるなら
function getElementsByFilter (filter) {
    if (this instanceOf NodeList) {
        ....
    } else {
        throw TypeError;
    }
}

のように型チェックを行うと良いでしょう……と思いきや、IE は駄目か。まあとにかく、動的な this が入り乱れているので、かなり読みづらいコードだと思います。ご容赦。

> ptototypeの拡張は一度にやるより……分けてやる理由を

私は prototype.js を使用していません。ですから、
PersonList.prototype = {
    swap   : function () { ...; },
    length : function () { ...; }
};

だと、既存の prototype を上書きすることになります。これは一度に初期化したいときに便利です。一方、
// public class PersonList extends List
PersonList.prototype = new List;
PersonList.prototype.constructor = PersonList;

PersonList.prototype.swap   = function () { ...; };
PersonList.prototype.length = function () { ...; };

のように継承させるなら、上書きではなく、上記のように追加しなければなりません。

今回の場合、どちらでも構いません。prototype.js の Object.extend は、for..in で回して後者のように追加しているだけです。

> forに関してですが、

これは単なる私のクセで、最初に while で書いてしまうからです。
function reverse () {
    var i = 0;
    var I = personLength.call (this);
    while (i < I) swapPersonsByPosition.call (this, i, I - 1 - i++);
}

こう書いてみて、見通しが悪いなあと思ったときに for に書き換えているので、ああいう書き方になってしまいます。

敢えて後付けの理由を考えるなら、JavaScript にはブロックスコープがないので、関数内で使用する変数を最初にまとめて宣言しておきたい、というのもあるかもしれません(JavaScript 1.7 では let によるブロックスコープが追加されましたけどね。Firefox2、Opera8 では使用できます)。

一覧へ戻る