prototype.jsを読み解く

第6回 Prototypeライブラリ(1609~2051行目)

この記事を読むのに必要な時間:およそ 18 分

Abstract.Insertion オブジェクト

1925: Abstract.Insertion = function(adjacency) {
1926:   this.adjacency = adjacency;
1927: }
1928: 

Abstract.Insertionオブジェクトのコンストラクタです。

adjacencyには,"beforeBegin", "afterBegin", "beforeEnd", "afterEnd"が指定され,後ほどinsertAdjacentHTML()に渡されます。

1929: Abstract.Insertion.prototype = {
1930:   initialize: function(element, content) {
1931:     this.element = $(element);
1932:     this.content = content.stripScripts();
1933: 
1934:     if (this.adjacency && this.element.insertAdjacentHTML) {
1935:       try {
1936:         this.element.insertAdjacentHTML(this.adjacency, this.content);
1937:       } catch (e) {
1938:         var tagName = this.element.tagName.toUpperCase();
1939:         if (['TBODY', 'TR'].include(tagName)) {
1940:           this.insertContent(this.contentFromAnonymousTable());
1941:         } else {
1942:           throw e;
1943:         }
1944:       }
1945:     } else {
1946:       this.range = this.element.ownerDocument.createRange();
1947:       if (this.initializeRange) this.initializeRange();
1948:       this.insertContent([this.range.createContextualFragment(this.content)]);
1949:     }
1950: 
1951:     setTimeout(function() {content.evalScripts()}, 10);
1952:   },
1953: 
1954:   contentFromAnonymousTable: function() {
1955:     var div = document.createElement('div');
1956:     div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
1957:     return $A(div.childNodes[0].childNodes[0].childNodes);
1958:   }
1959: }
1960: 

Abstract.Insertionのprototypeです。

具体的なInsertion.Before などの prototype を構築する際に,

Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { ... }

のような形を取ります。new Abstract.Insertion()から返されるメモリ上のインスタンスオブジェクトには,prototypeプロパティとしてAbstract.Insertion.prototypeを持つことになります。これをベースに,Insertion.Beforeならそれ独自のプロパティ・メソッドを{ ... }に加えていくことになります。

よって,1930行目からが各Insertion.*クラスで共有されるコンストラクタとなります。

Insertion.Beforeなどのクラスは,利用時にはnew Insertion.Beforeとnewを使うことで動作します。コンストラクタでは,adjacency引数が指定されていて,elementにinsertAdjacentHTMLプロパティとしてメソッドが存在する(IE, Opera)かどうかを見ています。

もし偽の場合,insertAdjacentHTML()のように細かく位置を指定することができません。まずcreateRange(), createContextualFragment()を使ってコンテキストに合ったDOMツリーフラグメントを作成します。それをinsertContent()に渡して実際に挿入します。insertContent()はBefore, Top, Bottom, After各クラスで上書き定義されており,その中で状況に合った位置に挿入するようになっています。

insertAdjacentHTML()が使える場合はそれをtry {}し,もし失敗したらタグがテーブル関係かどうかを調べます。IE では<tbody>, <tr>などの中途半端なDOMツリーは構築できないので,その場合はcontentFromAnonymousTable()を使って,空のテーブルを作ってそこに渡されたHTMLを流し込んだ状態でDOMツリー化する,という手順を踏みます。

最後に,<script>タグ部分を非同期に実行して終了です。

1954行目からは,先ほどコンストラクタで呼び出されたcontentFromAnonymousTable()です。

IEでは<td>, <th>タグを頂点とするDOMツリー生成ができないので,いったん外側のテーブルタグ文字列で囲った文字列を作り,それをダミーの<div>のinnerHTMLプロパティに流し込んで,その後奥に入っている<td>, <th>部分のDOMツリーを取り出して返す,ということをしています。

1961: var Insertion = new Object();
1962: 

Insertion.*クラス用の入れ物として,Insertionオブジェクトを用意しています。この下にBefore, Top, Bottom, Afterが入ります。

Insertion.Before クラス

1963: Insertion.Before = Class.create();
1964: Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
1965:   initializeRange: function() {
1966:     this.range.setStartBefore(this.element);
1967:   },
1968: 
1969:   insertContent: function(fragments) {
1970:     fragments.each((function(fragment) {
1971:       this.element.parentNode.insertBefore(fragment, this.element);
1972:     }).bind(this));
1973:   }
1974: });
1975: 

1963行目からは Insertion.Beforeクラスです。Class.create()した後で,前述のとおりAbstract.Insertionをnewしたものに,Insertion.Before独自の拡張をObject.extend()して,Insertion.Before.prototypeに入れています。

Insertion.* 各クラスで拡張しているのは,insertAdjacentHTML()が使えないときに,コンストラクタから呼び出されるinitializeRange()とinsertContent()というメソッドになります。

initializeRange()は,this.rangeにcreateRange()したものが入っている前提で,HTML文字列をDOMツリー化する際のコンテキストを設定します。

Insertion.Beforeの場合,渡されたelementの直前に挿入されるのでrange.setStartBefore()を使います。

1969行目からのinsertContent()では,渡された文書フラグメントを,element.parentNode.insertBefore()を使ってelementの直前に流し込んでいます。

Insertion.Top クラス

1976: Insertion.Top = Class.create();
1977: Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
1978:   initializeRange: function() {
1979:     this.range.selectNodeContents(this.element);
1980:     this.range.collapse(true);
1981:   },
1982: 
1983:   insertContent: function(fragments) {
1984:     fragments.reverse(false).each((function(fragment) {
1985:       this.element.insertBefore(fragment, this.element.firstChild);
1986:     }).bind(this));
1987:   }
1988: });
1989: 

1976行目からはInsertion.Topです。

基本的な構造はInsertion.Beforeと同じで,Abstract.Insertionコンストラクタに渡す文字列が'afterBegin'となっています。

initializeRange()では,selectNodeContents()でelement全体を示す範囲を作り,collapse(true) ⁠trueは先頭側に縮める)でelementの直前を指すようにします。

insertContent()の方は,受け取ったfragmentsをreverse(false)でコピーしつつ逆順に並び替え,それぞれに対してinsertBefore(element.firstChild)してelementの直前にひとつずつ入れていきます。ひとつずつinsertBefore()しているので,reverse()しておかないと,HTML文字列から作ったDOMツリーの配列が逆順に現れてしまいます。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。