最終回
この連載も今回が最終回です。
単に季節物のPrototypeライブラリを解説する、という視点ではなく、今後ライブラリのバージョンがさらにあがっても応用できるような基礎知識や、深追いするためのヒントなどを織り交ぜてきたつもりです。
「あとで読む」つもりの方も多いかと思いますが、読むのを忘れないでもらえれば幸いです。:-))
では、最後まで一気に行きましょう。
Abstract.TimedObserver オブジェクト
2846行目からはAbstract.TimedObserverです。
Form.Element.Observer、Form.Observerのprototypeとして使われます。ベースクラスとして使われるだけなので、このオブジェクト自体はClass.create()されずに単なるオブジェクト({})として初期化されています。
よって、2848行目からのinitialize()メソッドはForm.Element.Observer, Form.ObserverオブジェクトがClass.create()で初期化されることで、共通して利用されるコンストラクタとなります。
コンストラクタの中身は、まずインスタンス変数に引数を格納し、各派生クラスで定義されている getValue() を呼んで現状の値を保存しています。渡されたパラメータに基づいて registerCallback()メソッドを呼び出して、その中で定期的なタイマーをsetInterval()で設定しています。
そのregisterCallback()です。前述したように、単にsetInterval()を呼んでいるだけです。特に特徴的な所も無く、this.メソッド名.bind(this)の慣用句を使っているくらいです。
2861行目からはタイマーイベントごとに呼ばれるonTimerEvent()メソッドです。
まずgetValue()で最新の値を取得します。Form.Element.Observerの場合はgetValue()がnullを返す可能性があるので、それも考慮してlastValueから変化があったかどうかをchangedに入れます。
あとは変化した場合にコンストラクタから渡されたコールバック関数を呼び、次のためにlastValueを上書きして終了です。
Form.Element.Observer クラス
2872行目からはForm.Element.Observerクラスです。
フォームの入力要素を定期的に監視し、変化があった場合にコールバック関数を呼び出します。
実装はClass.create()で雛形を生成し、prototypeに先ほどのAbstract.TimedObserver.prototypeとこのクラス固有のgetValue()メソッドをマージしたものを代入します。
getValue()メソッドでは、Form.Element.getValue()を使って入力要素の値を取得しています。
Form.Observer クラス
2879行目からはForm.Observerクラスです。こちらはフォーム全体を監視します。
Form.Element.Observerと同様にAbstract.TimedObserverから継承し、getValue()には Form.serialize()を使ってフォーム全体の値を文字列として取得しています。
Abstract.EventObserver オブジェクト
2888行目からはAbstract.EventObserverです。構成はAbstract.TimedObserverとよく似ていて、initialize()でコンストラクタは定義しつつ、このクラス自体はClass.create()は使っていません。後述するForm.Element.EventObserverとForm.EventObserverから使われます。
コンストラクタでは、同様にパラメータをインスタンス変数に格納し、現在の値をgetValue()で取得して保存しています。その後、<form>タグの場合はregisterFormCallbacks()を呼び、それ以外ならregisterCallback()を呼んで各入力要素にイベントハンドラを設定しています。
個人的にはここはif文で分けるのではなく、ひとつメソッドを定義して派生クラス側でやることを分ける方がすっきりするのではないか、と思います。
2901行目からはonElementEvent()です。
これは各要素の'click'/'change'イベントハンドラとして実際に登録される関数で、現在のフォーム/要素の値をgetValue()で取得し、前回と比較して変わっていればコールバック関数を呼び出す、という形になっています。
Abstract.TimedObserver.onTimerEventでも同じような処理をしていますが、こちらの方がシンプルな条件判断になっています。getValue()がnullを返しうるのは変わらないので、両方揃えたほうが良さそうですが、今のところはこのようなコードになっています。
2909行目では、Form.EventObserverの時に使われるregisterFormCallbacks()メソッドです。
Form.getElementsでフォーム内の全入力要素を取得し、各々についてregisterCallback()を呼び出してイベントハンドラを設定しています。
2913行目からはregisterCallback()です。要素に対して必要なイベントハンドラを設定します。
type='checkbox', type='radio'の時だけonClickイベントを対象にし、それ以外はonChangeイベントを見張っています。お決まりのthis.メソッド名.bind(this) を使って関数を渡しています。
Form.Element.EventObserver クラス
2928行目からはForm.Element.EventObserverクラスです。Insertion.Beforeなどでも使っている形で、Abstract.* オブジェクトをnewして新たにメモリ上に確保したオブジェクトに対してObject.extend()を適用しています。
Form.Element.TimedObserverのEventObserver版で、特に変わったところはありません。TimedObserver, EventObserverで同じようなクラス定義をしているので、もうちょっと冗長性を無くした書き方ができるかもしれません。
Form.EventObserver クラス
Form.EventObserverです。こちらもTimedObserverの時と同様の拡張を行っています。
Event オブジェクトへの拡張
2941行目からはEventオブジェクトです。IEではグローバルオブジェクトとしてEventが存在しますが、それ以外の場合は空のオブジェクトとして作成しておきます。
2945行目からはObject.extend()を使ってEventオブジェクトを拡張しています。prototypeではなくEvent直下に拡張しているので、Event.element()などのように呼び出す形となります。
まずはキーコードを名前で参照するために数値との対応を定義しています。
2960行目からはEvent.element()関数です。
イベントが発生した要素を返します。IE系はsrcElementに、他の多くはtargetプロパティに入っているので、どちらかを返すようにしています。
2964行目からはisLeftClick()です。
Mozilla系ではwhichプロパティに1が入っていれば左クリック、IE系ではbuttonプロパティが1なら左クリックだけのクリックになります(1、2、4の論理和)。どちらかが真なら左クリックとしています。
したがって、IE系では右クリックしながら左クリックなどは真になりません。また、IEではそもそもonMouseDown, onMouseUp, onMouseMoveイベントでしかbuttonプロパティに値が入りません。
pointerX(), pointerY()関数です。イベントが発生した場所の、ページ上の位置を返します。スクロールしていてもいなくても、ある要素の位置は同じ値になります。
Mozilla系ではevent.pageX, event.pageYプロパティで該当する値を取得できます。
IE系では、event.clientXにクライアント領域内のオフセットが入ります。後方互換モードではdocument.body.scrollLeftに、標準準拠モードではdocument.documentElement.scrollLeftにクライアント領域のスクロールオフセットが入るので、ページ内の座標に変換するためにそれらのどちらかを足します。
イベントキャンセル用のEvent.stop()です。
Mozilla, Safari、OperaはDOM準拠のpreventDefault()メソッドが使えるので、こちらを呼び出しイベントのデフォルトの挙動(ボタンのonClickでの動作など)をキャンセルし、stopPropagation()でイベントの伝播を停止します。
一方IEでは、event.returnValueにfalseを入れることでデフォルトの挙動をキャンセルし、event.cancelBubbleにtrueを入れることでイベントの伝播を停止します。
2991行目からはEvent.findElement()です。
イベントターゲットの要素ではなく、その親のうち、指定されたタグ名を持つ直近の要素を返します。
while文でparentNodeがnullの場合は、自身がdocumentノードなのでdocumentノードを返し、tagNameプロパティが指定されたものと同じであればその要素を返します。そうでなければparentNodeをたどります。
observersは初期値はfalseですが、使われているときは配列となります。これは後述するIEのメモリリーク対策として、イベントを登録するときに一旦保存しておき、ページのonUnload時にdetachEvent()する、という処理を行うためです。
Event.observe()から呼ばれる内部関数 _observeAndCache()です。名前にCacheとありますが特にキャッシュ的な機能を持つわけではなく、イベントを解除するために保存しておく機能があるだけです。
IEとそれ以外ではイベントの登録方法が違うので、addEventListener()が存在すればDOM準拠の方法で、attachEvent()が存在すればIEでの方法でイベントを登録します。
また、次のunloadCache()で使うためにthis.observersに渡された情報を保存しておきます。
Event.unloadCache()関数です。3052行目でwindowにonUnloadイベントのハンドラとして、この関数が登録されています。
IEで動的にイベントを追加すると、ページ遷移してもその分のメモリが開放されないようなので、それを回避するために onUnload でこの関数が呼ばれ、Event.observers に蓄積された登録済みイベントを、すべて Event.stopObserving() を使ってイベント登録を解除しています。
3016行目では、登録されたイベントハンドラEvent.observers[i]のプロパティ'0'に対してnullを代入しています。これが何を意図しているかはわかりませんでした。
3021行目からはEvent.observe()です。
引数useCaptureが省略された場合はundefinedになるので、その場合はfalseにしておきます。
あとは受け取った引数をEvent._observeAndCache()に渡してイベント登録して終了です。
このobserve()と次のstopObserving()でのkeypress, keydownイベントの扱いは、Operaで問題となります。
下記リンクに記載されている修正でいいかと思いますが、Prototype 1.6.0では該当部分がごっそり書き換わってしまったので、適用できるのは1.5.1.1までとなります。
3032行目からはstopObserving()です。
observe()と同様に引数のuseCapture, nameを整理して、DOM準拠ブラウザ、IEで分岐してそれぞれremoveEventListener()かdetachEvent()を使ってイベント登録を解除しています。
detachEvent()がtry {}で括られているのは、IEでの例外対策とのことです。
ブラウザがIEの時のみ、windowのonUnloadイベント時にEvent.unloadCache()関数が呼ばれるようにしてメモリリークを防いでいます。
Position オブジェクト
最後のオブジェクトPositionです。特にクラス化はしておらず、Position.prepare()などの形で呼び出されることになります。
また、Prototype 1.6.0以降では、Elementに機能が統合されたため、Positionオブジェクト自体が非推奨となっています。
3057行目のincludeScrollOffsetsはbooleanで、今のところはfalseで固定されているようです。Position.within()の対象がスクロールするものの時にだけtrueにしろ、とコメントでは言及されています。
3061行目からはPosition.prepare()です。
Position.withinIncludingScrolloffsets()がここで設定しているdeltaX, deltaYを参照しているので、関数を呼び出す前にprepare()を呼んで設定しておく必要があります。
求めたいのはページ自体のスクロール量なのですが、Mozilla, SafariなどではpageXOffsetが使えます。IEでは標準準拠モードでdocument.documentElement.scrollLeftが、後方互換モードでdocument.body.scrollLeftが使えます。これらのうちいずれかがdeltaXとなります。
deltaYの方も同様です。
3072行目からはrealOffset()です。積み重なったスクロール可能なオブジェクトの階層を遡って、すべてのスクロール量を積算して [X, Y] の配列で返します。
対象となる要素から、頂点のdocumentまで遡り、各々のscrollTop, scrollLeftを合計しています。
3082行目からはcumulativeOffset()です。ドキュメント基点からのオフセットを[X, Y]の配列で返します。
実装はrealOffset()と似ていますが、offsetTop, offsetLeftを合計しているのと、遡るのがparentNodeではなくoffsetParentなところが違います。
Safariでは<body>の子供がposition:absoluteの時に正しい値を返さないため、3258行目で関数全体を上書き定義しなおしています。
3092行目からはpositionedOffset()です。
cumulativeOffset()と似ていますが、ドキュメントからではなく、一番近い親のうちCSS positionがabsoluteかrelativeなものからのオフセットを求めるところが異なります。
offsetParent プロパティをクロスブラウザ化したものです。CSSでのcontainer blockを求めるのに使います。
もし渡された要素がoffsetParentを持っていればそのまま返し、document.bodyが渡されていたらそれもそのまま返します。
あとはparentNodeを辿っていき、CSS positionがstatic以外の要素が出てきたらそれを返します。
それ以外の場合はdocument.bodyを返しています。
3118行目からはwithin()です。
Position.includeScrollOffsetsの所でも書いたように、その値が真なら代わりにwithinIncludingScrolloffsets()が呼ばれます。
そうでなければ、cumulativeOffset()を呼び出して要素のドキュメント基点からの位置を算出し、渡された座標が要素の枠内に含まれているかどうかをbooleanで返しています。
this.xcomp, this.ycomp, this.offsetは、別途Position.overlap()で使うためにキャッシュされます。といいつつ、これくらいのことをキャッシュするのにわざわざ「overlap()を呼ぶ前には必ずwithin()を呼ばなければならない」という制約を付けている理由は不明です。階層が深い要素に対してoffsetの計算が時間がかかる場合には、一旦計算しておいてその後に何度もoverlap()を呼び出して計算する、という使い方をするのならキャッシュの意味はありそうです。
3132行目からはwithinIncludingScrolloffsets()です。対象要素の上位にスクロールする要素がいる場合にwithin()の代わりに使います。
within()と異なるのは、渡されたx, yに関して、ドキュメント自体のスクロールオフセットdeltaX, deltaYの分を引いていることと、realOffset()を使って本来の位置から動いた分を増やしています。
結果として、this.xcomp, this.ycompには「スクロールしていなかったらそこにあるべき位置」が入ります。
これらの値と、指定された要素の座標(cumulativeOffset())を比較して、その要素の中に含まれているかどうかをbooleanで返します。
3146行目からはoverlap()関数です。
within()で設定されたthis.offset, this.xcomp, this.ycompを使っています。
modeがverticalかhorizontalかによって分岐していますが、どちらも渡された要素と計算された座標を元に、どれくらいの割合で重なっているかを算出します。
たとえばmodeが'vertical'の場合、(this.offset[1] + element.offsetHeight) で要素の下辺のY座標となり、((this.offset[1] + element.offsetHeight) - this.ycomp)は渡されたXY座標のY位置から下辺までの長さとなります。これをelement.offsetHeightで割っているので、最終的に返されるのは要素の高さを1とした時、渡されたY座標がどの割合の位置にあるか、ということを示す0から1の間の数値となります。
具体的には、渡されたY座標が要素の上辺と同じ場合には1, 下辺と同じ場合には0, 真ん中の場合は0.5となります。
modeが'horizontal'の場合はX軸が対象となります。
3156行目からはpage()です。指定された要素の、viewportから見たオフセットを返します。
まず、要素から遡ってoffsetTop, offsetLeftを加算しておきます。これでドキュメント基点からのオフセットが得られます。
途中、cumulativeOffset()と同様にSafari対策が入っています。
次に、再度要素から遡って、scrollTop, scrollLeftを減算していきます。そうするとスクロールした分だけ左上にずれていくので、最終的にviewportでのオフセット値になります。
3181行目からはclone()関数です。source要素の位置にtarget要素を移動します。
まずoptionsに、デフォルト値を用意した上でマージします。関数の仮引数には明示されていませんが、三番目の引数としてオプション指定のためのオブジェクトが渡せますので、それをarguments[2]として参照しています。
target要素がposition: absoluteの場合、親からの相対位置では正しい位置にならないので、offsetParent()で相対基準の要素を取得し、そのviewport上の位置をdeltaとして保存しておきます。
この場合、たとえばtargetのleft, topに0, 0を指定しても、parentと同じ位置に移動してしまいページの左上には行きません。そこで、left = (p[0] - delta[0]) という形で代入することで、その分を引いて調整するようにしています。
あとは、optionsの指定に従ってtop, left, width, heightを設定します。これらは単位付きで指定しないといけないので、+ 'px' として設定しています。
3129行目からはabsolutize()です。ページレイアウトを変更せずに、指定された要素をposition: absoluteに変更します。
absoluteにすると直近のstatic 以外の親が基準になるので、Position.positionedOffset()を使ってそこからのオフセットを出します。
あとはaboslute 用に実際に設定する値を用意し、要素の_originalLeftなどのプロパティにrelativeに戻す時のための値を入れておきます。このとき、_originalLeft, _originalTopには、absoluteの基準となる要素の座標からの相対的な位置、として記録しています。これは現在の絶対位置を保存しておくと、absoluteの基準となる要素の位置が変わってしまった場合に追従できなくなってしまうからのようです。
3242行目からはrelativize()です。absolutize()でposition: absoluteに変更したものをrelativeに戻します。
absolutize()でも書いたように、元の位置ではなく現在の場所からの相対的な位置への移動、という形で位置を回復しています。
3261行目からは、Safari用にPosition.cumulativeOffset()を上書き定義しています。
Safariはbodyの子要素がpositon:absoluteの時に、offsetTop, offsetLeftでマージンの値を返してしまうようです。
最後に、prototype.jsのロードが完了するタイミングで、Element.addMethods()を実行しています。
Element, Element.Methodsなどと分かれているものを、Object.extend()を使ってElementに集めたり、の初期化処理を行っています。