prototype.jsを読み解く

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

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

Insertion.Bottomクラス

1990: Insertion.Bottom = Class.create();
1991: Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
1992:   initializeRange: function() {
1993:     this.range.selectNodeContents(this.element);
1994:     this.range.collapse(this.element);
1995:   },
1996: 
1997:   insertContent: function(fragments) {
1998:     fragments.each((function(fragment) {
1999:       this.element.appendChild(fragment);
2000:     }).bind(this));
2001:   }
2002: });
2003: 

1990行目からはInsertion.Bottomです。

initializeRange()はInsertion.Topとほぼ同様で,collapse()へのパラメータがthis.elementとなっています。ここはfalseで後ろ側へ縮める,ということになっているので,falseであるべきでしょう(prototype.jsのtrunkではInsertionまわりがごっそり置き換わっているので,最新版ではすでのこの部分のコードは存在しません⁠⁠。

insertContent()では,fragmentsそれぞれをappendChild()してやるだけ,という簡単なもので済んでいます。

Insertion.After クラス

2004: Insertion.After = Class.create();
2005: Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
2006:   initializeRange: function() {
2007:     this.range.setStartAfter(this.element);
2008:   },
2009: 
2010:   insertContent: function(fragments) {
2011:     fragments.each((function(fragment) {
2012:       this.element.parentNode.insertBefore(fragment,
2013:         this.element.nextSibling);
2014:     }).bind(this));
2015:   }
2016: });
2017: 
2018: /*--------------------------------------------------------------------------*/
2019: 

2004行目からはInsertion.Afterです。

initializeRange()では,rangeに対してsetStartAfter()を呼んでいます。

insertContent()では,fragmentsそれぞれに対して,parentNode.insertBefore(element.nextSibling)を呼び出すことで,elementの後ろに挿入されるようになっています。

elementのnextSiblingが存在しない場合,insertBefore()にnullを渡すことになりますが,その場合も子供ノードの最後に挿入されることになっています。

Element.ClassNames

2020: Element.ClassNames = Class.create();

ある要素が持つ,複数になりうるクラス名をEnumerableとして抽象化するためのクラスです。いつもどおりClass.create()した上でprototypeを拡張しています。

2021: Element.ClassNames.prototype = {
2022:   initialize: function(element) {
2023:     this.element = $(element);
2024:   },
2025: 

コンストラクタです。渡されたelementをthis.elementに保存しているだけです。

2026:   _each: function(iterator) {
2027:     this.element.className.split(/\s+/).select(function(name) {
2028:       return name.length > 0;
2029:     })._each(iterator);
2030:   },
2031: 

Enumerable.each()から呼び出される_each()メソッドです。要素のclassNameプロパティを,空白文字でsplit()して,長さがあるものだけをselect()で抽出しています。

これにより,Enumerable.each()を使うと,要素中の各クラス指定ごとにイテレータ関数が実行されることになります。

2032:   set: function(className) {
2033:     this.element.className = className;
2034:   },
2035: 

2032行目からはset()です。これは単に指定された文字列をclassNameプロパティにセットします。事前にクラスが指定されていたとしても,この指定ですべて上書きされます。

2036:   add: function(classNameToAdd) {
2037:     if (this.include(classNameToAdd)) return;
2038:     this.set($A(this).concat(classNameToAdd).join(' '));
2039:   },
2040: 

add()メソッドでは,すでに指定されたクラスが要素に含まれている場合には何もせずに帰ります。

$A(this)すると Enumerable.toArray()が呼ばれ,map()経由でeach()が呼ばれるので,結果としてクラス指定の文字列を空白文字でsplit()した配列になります。これにconcat()した結果を最後にjoin(' ')して,単一のクラス指定文字列に変換します。それをset()でclassNameプロパティに設定して終了です。

2041:   remove: function(classNameToRemove) {
2042:     if (!this.include(classNameToRemove)) return;
2043:     this.set($A(this).without(classNameToRemove).join(' '));
2044:   },
2045: 

2041行目からはremove()メソッドです。

this.include()を使って,指定されたクラス名が自分に含まれていなければそのまま帰ります。

含まれていれば,$A(this)で配列化したものにwithout()で指定されたクラス以外を集めて,join(' ')したものをset()で上書きしています。

2046:   toString: function() {
2047:     return $A(this).join(' ');
2048:   }
2049: };
2050: 

toString()では,配列化したものをjoin(' ')で繋げて返しています。element.classNameをそのまま返してもいいような気がしますが,この方が余計な空白などが取り除かれた形に正規化されるので,そういう理由かもしれません。

2051: Object.extend(Element.ClassNames.prototype, Enumerable);

最後にElement.ClassNamesのprototypeにEnumerableを拡張してElement.ClassNamesは完成です。

著者プロフィール

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

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