遠いリポジトリに便利なsvk
第五回目です。
Subversionのlogやdiff、annotateサブコマンドを駆使するといろいろな情報が取れることは前回紹介しました。
ただ、Railsのような海外にある、また人気があって負荷が高いことも多いリポジトリに対して、頻繁にそれらのサブコマンドを使っても、結果が出てくるまで遅いことが多々あり快適に解析が進められません。
そんな時にはsvk を使う手があります。
svkのmirrorサブコマンドを使うと、ローカルのディスク上に、Subversionリポジトリの特定部分以下だけをまるごとミラーすることができます。今回のような状況では、http://svn.rubyonrails.org/rails/spinoffs/prototype/だけでもローカルにミラーしておくと、作業が格段に快適になります。
という手順で利用できます。
では、今回はElementへの拡張からです。Elementまわりは膨大なので、次回も含めて2分割でお送りします。
Element への拡張
まず、必要に応じて空のElementオブジェクトをプレースホルダとして用意します。
まずはElement.extend()関数です。これは既存の要素に、Prototypeライブラリ独自のメソッド群を付け加えるものです。
昔はElement以下の追加メソッドは、Element.visible(targetElement)のように呼び出すようになっていましたが、現在では各要素自体のメソッドとしてtargetElement.visible()のように呼び出すことができるようになっています。
できるだけ効率的に、かつ副作用を抑えつつ便利にするために、このElementに対するメソッド追加はかなりややこしいことになっています。
1294行目からは、メソッドを付け加える必要があるかどうかを判断しています。以下の条件のどれかに該当するとreturn elementで何もせずに返ります。
条件文 |
付け加える必要がない理由 |
!element |
そもそも引数elementが無効、null、undefinedなど |
!element.tagName |
tagNameプロパティが無いので要素ではない |
element.nodeType == 3 |
テキストノード(3)は対象外 |
element._extended |
すでにこの関数により拡張済み |
F.SpecificElementExtensions |
コード最後部のElement.addMethods()の呼び出しにより、HTMLElement.prototype以下にメソッドを追加してあるので、個々の要素インスタンスに対して追加する必要は無し |
element == window |
windowは対象外。それ以前にtagNameのチェックで除外されているはず |
1298行目では、ここで使う変数や、ショートカット関数などを定義しています。
1301行目からは、このブラウザで拡張するべき関数オブジェクトをmethodsオブジェクトに詰め込んでいます。ここでmethodsのプロパティ名が要素に拡張されるメソッド名で、プロパティ値が関数オブジェクトになります。
グローバルなHTMLElementが定義されている場合、F.ElementExtensionsが真となります。この値はIEでは偽ですがFirefox、Safari、Operaでは真になります。真の場合はHTMLElementに定義されているメソッドが使えるのでここで拡張する必要がなくなります。
Element.Methods.ByTagには、タグ毎に拡張するべき関数が入っているので、今回渡された要素のタグ名に対して該当するものがあればそれもmethodsオブジェクトに加えます。
あとは、methodsに入っている値を列挙して、プロパティ値がちゃんと関数オブジェクトで、かつまだelementには該当する名前のプロパティがセットされていない場合に、Element.extend.cache.findOrStore()関数を使って拡張しています。
最後に、element._extendedに「拡張済み」の印を入れて、elementを返します。
1320行目からはElement.extend.cacheです。findOrStore()関数だけがプロパティとして定義されています。
findOrStore()関数は、先ほどのElement.extend()で、関数オブジェクトを渡して呼び出されています。1313行目において、cache.findOrStore()の形で呼び出されているので、この関数におけるthis はcacheすなわちElement.extend.cacheとなります。
return this[value] = this[value] || function(){...} という形は、Element.extend.cache オブジェクトのプロパティ名(といっても文字列ではなく関数オブジェクトがキーになっていますが)にあればそれを返し再度代入、無ければ関数を定義して代入、そしてその代入された値を返す、というものです。これにより、同じ無名関数がいくつも定義されないようにしています。
無名関数の中では、valueとして渡された関数オブジェクトに対してapply()を呼び出しています。例えば1329行目からのElement.Methods.visibleがvalueとして渡されているとすると、apply()の第一引数がnullなので、visible()の中でのthisはグローバルオブジェクトとなります。第二引数に[this].concat($A(arguments))を渡していますが、ここのthisは無名関数のコンテキストでのthisなので、通常はElementインスタンスになります。例えばIDが't1'という要素があれば、$('t1').visible()として呼び出すのでvisible の左側すなわち $('t1') が this となります。 arguments もこの無名関数のコンテキストなので、$('t1').visible(1,2,3) と書けば arguments に [1,2,3] が入ります。これらを concat() で繋いで apply() に渡しているので、visible() 関数の定義では、最初の引数として Element インスタンスを受け取る形になります。
- ECMA-262 第三版 - 15.3.4.3 Function.prototype.apply (thisArg, argArray)
Element.Methodsオブジェクト
1328行目から実際にElementに追加されるメソッド群です。
1329行目のvisible()は要素のstyle.displayプロパティが'none'で無ければ真を返すことで可視状態かどうかを返します。
1333行目のtoggle()は、現在がvisible()かどうかを見たうえでhide()かshow()メソッドのどちらを呼ぶか決めています。
1339行目のhide()では、要素のstyle.displayプロパティに'none'をセットすることで display:none にしています。
1344行目のshow()は逆に、style.displayに''(空文字)を代入することで、その要素本来のdisplayスタイルに戻しています。多くのブラウザにおいてこの「displayに空文字を代入すると本来のスタイルに戻る」という実装になっているようですが、仕様として明確になっているものは見つけられませんでした。
1349行目からのremove()は、DOMのparentNode、removeChild()を使って自分自身をDOMツリーから削除します。
1355行目からのupdate()では、まず引数htmlを文字列に正規化しています。その後、<script>タグ以外の部分をinnerHTMLプロパティに代入し、その後<script>部分を取り出して、それをsetTimeout()経由で実行します。setTimeout()経由なのは、このupdate()関数が実行しようとするスクリプトでブロックしてしまうのを避け、呼び出し元に戻れるようにしています。スクリプト自体は10ms後に別途非同期で実行されるようになっています。
1362行目からはreplace()メソッドです。
まず引数elementをElement化し、htmlを文字列に正規化します。
もしelement.outerHTMLが使える場合(IEなど)、<script>タグ以外をそこに代入して置き換えます。
outerHTMLが使えない場合は、DOM2のcreateRange()を使います。まずインスタンスを作ってから、selectNodeContents()を使ってelementが示す部分全体を選択します。
createContextualFragment()は、Mozilla特有のメソッドで、MDCのドキュメントにも詳しくは書いてありませんが、関数名からして、rangeで定義されたDOMツリーのコンテキストにおいて、渡されたHTML文字列をdocument fragmentに変換するものと思われます。これにより返されるdocument fragmentを、parentNode.replaceChild()で置き換えることでelementが置換されることになります。
最後に、update()メソッドと同様に、<script>タグの部分を非同期に実行されるようにsetTimeout()して終了です。
1377行目からはElement用のinspect()です。
タグの名前と、id、classプロパティをタグ形式で出力します。
まずelementをElement化し、タグ名をtagNameプロパティから取り出して小文字にしてresultに用意します。
$H()を使って、id、classのプロパティ名、タグの属性名の対応表を用意し、その組でeach()ループを実行します。このループ内では'id':'id'という組がpairオブジェクトに格納されてイテレータ関数に渡されます。
イテレータ関数内では、$H()に渡されていた組を取り出し、要素の属性('id', 'className')をelementから取り出して文字列化してvalueに入れます。
valueに値があれば、resultに文字列をフォーマットして付け加えます。
ループ後に、result文字列のタグを閉じて返します。
1388行目からはrecursiveCollect()です。主にライブラリ内部から利用されます。
elementをElement化し、返り値用に elements 配列を用意します。
後は、whileループを使ってpropertyプロパティが存在する限り再帰的にたどっていきます。その際、nodeTypeが1(ELEMENT_NODE)の時はElement化した上でelementsにpush()しておきます。
最後に溜まったelementsを返します。
1397行目からはancestors()です。自らの祖先をたどります。
実装は先ほどのrecursivelyCollect()を'parentNode'を引数として呼び出しているだけです。
1401行目からはdescendants()です。こちらは自分の子孫をすべて列挙します。
elementに対してgetElementsByTagName('*')で、自分の内側に含まれるすべてのタグを配列で取得し、それを$A()を使って拡張Arrayとし、最後にeach()で各々の要素をElement化して返します。
1405行目からはfirstDescendant()です。
DOMのfirstChildはすべてのノードを対象としますが、こちらは要素だけを対象とし、最初の要素を返します。
まず、elementに最初の子供ノードを取得します。これはコメントやテキストノードの可能性があります。
その後、whileでelementが真でnodeTypeがELEMENT_NODE (1)以外の間、nextSiblingをたどっていきます。
nextSiblingを使っているので、element直下の子供しか検索対象としませんが、孫以下の要素が存在するとしたらそれを含むのは子要素なので、firstDescendant()としては孫要素以下に行き着く必要はありません。
最終的にwhileを抜けるのは、elementが偽になった場合か、nodeTypeがELEMENT_NODE のものが見つかった場合です。それを$()でElement化して返します。
1411行目からはimmediateDescendants()です。自分の直下の要素をすべて返します。これもコメントやテキストノードは無視して、要素だけを返します。
まず、そもそもelement.firstChild が偽の場合は空配列を返して終了します。
firstDescendat()の時と同様に、ELEMENT_NODEが見つかるまでnextSiblingをたどります。
もし要素が見つかったら、それを配列に入れ、後述するnextSiblings()の返り値とconcat()でくっつけて返します。要素が見つからなければ空配列を返します。
1418行目からはpreviousSiblings()です。
実装はrecursivelyCollect('previousSibling')を呼び出して返しているだけです。
1422行目からのnextSiblings()も、recursivelyCollect('nextSibling')を呼び出して返しているだけです。
1426行目のsiblings()は、自分以外の兄弟要素を列挙します。
element.previousSiblings()が遡る順で返すのでそれをreverse()で逆順にし、nextSiblings()の返り値とconcat()でくっつけて、それを返しています。
1431行目はmatch()です。指定されたCSSセレクタにマッチする要素を返します。実際には後述するSelectorクラスのmatch()メソッドとなっています。
1437行目からはup()メソッドです。
引数がelementしか指定されていない場合、arguments.length == 1となるので、parentNodeを返して終了です。
次に、ancestors()で自分の祖先を配列ancestorsとして取得します。expressionにCSSルールが指定されていれば、Selector.findElement()を使ってancestorsからマッチする最初の要素を返し、expressionが偽ならancestorsからindexで指定された要素を返します。
1445行目からのdown()メソッドは、up()のちょうど逆になります。argumentsがひとつだけなら、先ほど定義したfirstDescendant()を使って子供のうち最初の要素を返します。
後は、up()ではancestors()で取得した配列を、逆にdescendants()で取得します。
1453行目からのprevios()は、兄弟要素のうち(デフォルトでは)ひとつ前の要素を返します。
expression、indexが指定されていない(arguments.length == 1)の場合は、Selector.handlers.previousElementSibling()を使って一番近い前の兄弟を返しています(この関数は特にCSSセレクタに関連したものではありません)。
後は、previousSiblings()で自分より前の兄弟の要素の配列を取得し、expressionが指定されていればSelector.findElement()を使ってマッチするものを返し、指定されていなければpreviousSiblings配列からindexで指定されたものを返します。
1461行目からのnext()はprevious()の逆で、兄弟のうち後ろの要素を返します。
実装としてはprevious()を単純に逆にしていて、コード中の"previous"が"next"に変わっただけになっています。
1469行目はgetElementsBySelector()です。指定された要素以下で、CSSセレクタにマッチするものを返します。
実際にはSelector.findChildElements()を呼び出しているだけです。
1474行目はgetElementsByClassName()です。
実際には、Prototypeライブラリで定義されたdocument.getElementsByClassName()関数を呼び出しているだけです。
1478行目からはreadAttribute()です。基本的にはgetAttribute()と同様ですが、なぜかIE, Safariは関数オブジェクトではないことと、IEの特異な挙動を吸収するための、ブラウザ互換性のためのメソッドです。
まず、IEでなければ普通にgetAttribute()を使って値を返します。
1481行目ではelement.attributesをチェックして偽ならnullを返すようにしています。これはr6371で"Fix readAttribute for IE7"として付け加えられていますが、手元のWin IE7環境で簡単に試した限りでは、IE7ではいつもattributesが偽になる、というほど単純な話ではないようで、どういう条件でここでreturn nullとなるのかがわかりませんでした。リポジトリのtrunkの方ではその後r7222でこの行が削除されており、その際には特にこの行を削除した理由は明確には触れられていません。また、1.5.1.1以降もこのreadAttribute()は大きな修正がいくつも入っているので、このバージョンでのreadAttribute()はあまり過信しすぎない方がよさそうです。
IEの場合、別途定義した_attributeTranslationsという変換テーブルを用意します。この中でvaluesとして定義されている"style"、"title"への参照は、個別に指定された関数を呼び出すことで実際の値を取得して返します。"href"、"src"、"type"はgetAttribute()を使って、また"disabled"、"checked"、"readonly"、"multiple"は、フラグとして適切な値を返すようにしています。
namesではちょっとした属性名の違いを吸収するためのテーブルとなっており、該当するものがあればここで正規化します。
あとは、element.attributesオブジェクトに該当する属性があればそのnodeValueプロパティを返し、そうでなければnullを返します。
1491行目からはgetHeight()、getWidth() です。
これらは後述するgetDimensions()を呼び出し、返り値からそれぞれheight, widthプロパティの値を返しています。
1499行目はclassNames()です。単にElementClassNames()クラスのインスタンスを作成して返しています。
1503行目からはhasClassName()です。要素が指定されたクラス名を持っているかどうかを調べます。
まず、elementをElement化し、偽が返ってくるようならそのままundefinedを返します(指定されたIDを持つ要素が存在しなかった場合など)。
次に、要素のclassNameプロパティを調べ、クラス名が付いていなければfalseを返します。
後は、できるだけ正規表現の実行を避けるために、まず要素のクラス名文字列が指定されたクラス名と一致しているかを調べ、一致していなければgetElementsByClassName()でも使った正規表現で複数になりうるクラス名の中から指定されたクラス名があるかどうかを探します。見つかったらtrueを返し、そうでなければfalseを返して終了です。
1513行目からはaddClassName()です。
まずは渡されたelementが存在しなければundefinedを返して終了します。
次に、Element.classNames()を使ってElement.ClassNamesオブジェクトを取得し、add()メソッドを呼び出しています。
1519行目のremoveClassName()もaddClassName()とほぼ同様です。最後にadd()の代わりにremove()メソッドを呼んでいます。
1525行目からはtoggleClassName()です。クラス名をトグルする、というのがわかりにくいのですが、指定されたクラス名が要素に入っていれば削除し、入っていなければ追加する、という関数です。
まず[]の内側でhasClassName()を使って指定されたクラスが要素に指定されているかどうかを確認します。真なら'remove'、偽なら'add'を返します。
これがclassNames()の返り値に対するメソッド呼び出しとなり、add()もしくはremove()が実行されます。
最後に変更された要素オブジェクトを返します。
1531行目からはobserve()関数です。
Event.observe()に対するショートカット関数なのですが、通常Event.observeはEvent.observe(element, 'click', func) などとして呼び出しますが、Element.observe の場合は$('t1').observe('click', func)として呼び出すことができます。
この関数が呼び出されるタイミングで、argumentsには[ element, eventName, callback ]という順で値が入っています。1532行目でEvent.observeに対してapply()を呼んで実行していますが、apply()の第一引数がEventなので、Event.observe()の呼び出し時と同様に呼び出される側ではEventがthisになります。
argumentsには上記のような形で引数が入っているので、Event.observe()の時と同じ呼び出し形式で呼び出されることになります。
最後にelementを返して終了です。
Array.first()メソッドはthis[0]を返しているだけなので、素直にarguments[0]を返す方がシンプルでいいと思うのですが、なぜこうしているのでしょうか。この行自体はr4882で導入されています。
1536行目からのstopObserving()もobserve()と同様、Event.stopObserving()へのショートカット関数となります。
1541行目からのcleanWhitespace()では、直接の子供の中から、空白しか含んでいないテキストノードを除去します。
実装は、まずfirstChildで子供の先頭を取得し、whileループでnullでなくなるまでnextSiblingをたどります。
途中でnodeTypeが3 (TEXT_NODE)で/\S/という正規表現(空白文字以外)にマッチしないもの(すなわち空白しか含んでいない)が見つかると、それをremoveChild()で除去します。
element.removeChild()してしまうと、その後ではnode.nextSiblingを参照できないかと思いきや、手元のIE6、Firefox 2で試した限りでは参照できるようです。ただ、removeChild()した後にnextSiblingが参照できない方が自然な気もしますので、安全のために次の兄弟を先に取得しておいてremoveChild()した後でそれを使う、というのは納得できる書き方です。
1554行目からはempty()です。
innerHTMLプロパティを読んで、それが空か空白しか含んでいなければ真を返します。
1558行目からはdescendantOf()です。
ancestorに自分の祖先かどうかを確認したい要素を渡します。
whileループでparentNodeがある限りたどっていき、もしancestorと一致する要素があればtrueを返します。見つからなければfalseを返します。
1565行目からはscrollTo()です。
Position.cumulativeOffset()を使って、該当要素のドキュメントの左上からの相対座標を取得します。
あとはwindow.scrollTo()関数を使ってスクロールして終了です。
1572行目からはgetStyle()です。このメソッドはOpera、IEの場合は別途後で上書き定義されていますので、このコードが利用されるのはそれ以外のブラウザの場合です。
まずelementをElement化して、style変数はキャメルケースに変更します(なのでハイフン区切り、キャメルケースのどちらも渡すことができます)。そして、elementのstyleプロパティから該当するスタイル名の値を取得します。
うまく取れなかった場合、document.defaultView.getComputedStyle()を使う方法を試みます。
取得しようとしたスタイルが'opacity'だった場合、文字列を浮動小数点数に変換します。もしうまく取得できていない場合は1.0を返しておきます。
最後に、取得した値が'auto'だったときはnullを、そうでなければその値を返します。
1584行目からはgetOpacity()です。
先ほどのgetStyle()を使って取得した値を返しているだけです。
1588行目からはsetStyle()です。stylesにはキャメルケースにしたプロパティ名を持つハッシュ形式のオブジェクトになります。
まずelementをElement化するのと、何度も参照するのでelement.styleも変数に代入しておきます。
次にfor (var property in styles)で渡されたオブジェクト内をループします。もしプロパティが'opacity'なら後述するsetOpacity()メソッドを使い、そうでなければstyleプロパティに値をセットします。
その際、'float'か'cssFloat'の場合はブラウザによって使うプロパティ名が異なるので場合わけします(IEは'styleFloat'、Firefox、Safariは'cssFloat'、Operaはどちらも大丈夫です)。
また、公式APIドキュメントにはまだ記述されていませんが、最後の引数としてcamelizedを渡すと、自動的なString.camelized()呼び出しをしないようになります。
1602行目からはsetOpacity()です。
指定された値が1が空文字なら空文字を、0.00001より小さいなど極端に小さければ0を、それ以外はその値をstyle.opacityに代入します。