連載第7回は、slider.jsを読み解きます。このライブラリはスライダーというGUI部品を提供します。スライダーは、オーディオ機器でボリュームのところによく見かける、目盛の付いた溝にツマミがついたものです。GUI部品としては、やはりボリュームの調整や、あるいはペイントソフトで色を合成するときなどに使われます。連続的な値を、気軽に上げたり下げたりするのに便利です。その反面、細かい調整をするには向きません。
スライダーの各部の名称
コードの解説に入る前に、部品の各部の名称を確かめましょう。以下の画像をご覧ください。ツマミを英語ではハンドルといい、溝をトラックといいます。スライダーの範囲をレンジといいます。複数のハンドルの間にできる区間をスパンといいます。
実際の動作例もご覧ください(script.aculo.usの機能テストに含まれるslider_test.htmlを和訳したものです。
Control.Slider
それではコードを見ていきましょう。
1~7行目は、著作権表示です。
8行目で、将来的に、いろいろなGUI部品を実装する構想があって、それらをControlというクラス以下にまとめようとしているのがわかります。いまのところはスライダーしかありません。
10~15行目で、オプションとフックについてごく簡単なコメントがあります。詳細を以下で説明します。
16~89行目の、initializeは、インスタンスの初期化を行う関数です。引数として、1番めのhandleにハンドルにする要素のDOM id(またはそれらの配列)をとります。2番めのtrackに、トラックにする要素のDOM idをとります。3番めのoptionsに、オプション設定のオブジェクトをとります。
20行目で、1番めの引数のhandleにDOM idの配列が与えられた場合は、配列の中身のそれぞれにPrototype.jsの$関数を適用して、その要素たちをハンドルとして取得します。
23行目で、handleに単にDOM idが与えられた場合は、同様にして、その要素をハンドルとして取得します。
26行目で、2番めの引数のtrackに与えられたDOM idから、同様にして、その要素をトラックとして取得します。
29行目で、axisは、スライダーの軸が垂直か水平かを表します。options.axisで設定できます。デフォルトは'horizontal'(水平)です。
30行目で、incrementは、今のところ、実装されていません。公式Wikiの説明では、これはピクセルとスライダーの値との換算量で、例えば1にすると、1pxのマウスドラッグでスライダーの値が1動くそうです。
31行目で、stepも、今のところ、実装されていません。公式Wikiにも説明がありません。
32行目で、rangeは、スライダーの可動範囲を表します。例えば0~100とするにはoptions.rangeに$R(0,100)と与えます。デフォルトでは$R(0,1)です。範囲は両端の値を含みます。
34行目で、valueは、後方互換性のために用意されている値で、ハンドルの現在値を示していたものです。これはハンドルが1個しかなかったときの名残で、今ではハンドルが複数あるので、代わりにvaluesを使ってください。後方互換性のために、setValue関数が常にこの値をvalues[0]の値に更新します。
35行目で、valuesは、ハンドルたちの現在値を示します。全て0で初期化します。
36行目で、spansは、スパンの要素たちです。options.spansにDOM idの配列を与えると、その要素たちをスパンとして取得します。デフォルトではfalseです。
37行目で、startSpanは、先頭のスパンに指定の要素を使うオプションです。options.startSpanに、先頭のスパンになる要素のDOM idを与えます。
38行目で、endSpanは、最後尾のスパンに指定の要素を使うオプションです。options.endSpanに、最後尾のスパンになる要素のDOM idを与えます。
40行目で、restrictedは、ハンドル同士の交差を制限するかどうかです。options.restrictedで設定します。デフォルトではfalseで、制限しません。
42行目で、maximumは、スライダーの最大値です。options.maximumで与えます。デフォルトではレンジの終点です。後ほどのコードで、allowedValuesがあるときはその最大値が設定されるようになっています。
43行目で、minimumは、スライダーの最小値です。options.minimumで与えます。デフォルトではレンジの始点です。後ほどのコードで、allowedValuesがあるときはその最小値が設定されるようになっています。
46行目で、alignXは、水平なスライダーの、トラックの0点位置を横にずらすためのオプションです。options.alignXで与えます。デフォルトは0です。
47行目で、alignYは、垂直なスライダーの、トラックの0点位置を縦にずらすためのオプションです。options.alignYで与えます。デフォルトは0です。
49行目で、trackLengthは、トラックの表示の長さです。単位はpxです。ここででてくるmaximumOffsetは、トラックのCSSでの終点の位置を返す関数です。minimumOffsetは、トラックのCSSでの始点の位置を返す関数です。これらは後述します。
51行目で、handleLengthは、ハンドルの表示の長さです。単位はpxです。スライダーが垂直ならハンドルの縦幅、水平なら横幅のことです。通常はハンドルのoffsetHeightやoffsetWidthの値を使いますが、スライダーがブラウザに表示されていないときにoffsetHeightなどが取得できないことがあります(ticket4011)。このときはCSSのheightやwidthプロパティの値を、単位である'px'を取り除いて使います。
57行目で、activeは、スライダーがアクティブかどうかのフラグです。ハンドルのドラッグを開始したときにtrueになります。
58行目で、draggingは、ハンドルをドラッグしている間、trueになるフラグです。
59行目で、disabledは、trueにするとハンドルをドラッグできなくなるオプションです。ライブラリの利用者のために用意されています。
61行目で、options.disabledがtrueなら、setDisabledを呼んで、上記のthis.disabledをtrueにします。
64行目で、allowedValuesは、スライダーが取れる数値を列挙した配列です。options.valueに数値の配列を与えることで設定します。ハンドルをドロップしたときなどに、そこに最も近い数値がこの配列から選ばれます。デフォルトではfalseで、この場合は、スライダーは範囲内の全ての数値を取ることができます。
66、67行目で、前述のスライダーの取りうる最小値、最大値のthis.minimumとthis.maximumは、これらの配列の最小値、最大値に設定しなおされます。
70~72行目で、イベントリスナとなる関数を代入しておきます。
75~83行目で、ハンドルを初期化します。次のような処理をします。
76行目で、インデックスを逆順にします。そうする必要は実はないのですが、74行目のコメントで、最終的に1番めのハンドルが選択中になるように念を押しているのだとわかります。
77行目で、そのインデックスのハンドルの値を後述のsetValueを呼んで更新します。options.sliderValueが、配列で与えられていたら、それらを初期値にします。数値で与えられていたら、一律にその値を初期値にします。何も与えられていなければ、レンジの始点の値を使います。
81行目で、ハンドルを、makePositionedでposition: 'relative'にして移動に備えます。さらにマウスクリックのイベントリスナを設定します。これはドラッグの開始を扱うイベントリスナです。
84行目で、トラックをマウスクリックするとドラッグの開始をするようにします。
85行目で、ページ上でマウスボタンを離すとドラッグを終了(ドロップ)するようにします。
86行目で、ページ上でマウスを動かすと、スライダーの値の変更や描画をするようにします。
88行目で、initializedフラグをtrueにします。このフラグが立っているときだけ、onSlideフックやonChangeフックが呼ばれます。
90~98行目の、disposeは、イベントリスナを解除する関数です。内部的には使われていません。ライブラリの利用者が、スライダーをページから消したいときに使います。
99~104行目の、setDisabled, setEnabledは、disabledフラグをtrueやfalseにする関数です。ライブラリの利用者が、ハンドルのドラッグを不可能、または可能に切り替えるときに使います。
105~124行目の、getNearestValueは、スライダーが取れる値の中から、引数の値に最も近いものを計算する関数です。
106行目で、allowedValuesの設定があるときは、以下の動作をします。
107行目で、引数の値がallowedValuesの最大値を越えている場合は、その最大値を返します。
108行目で、引数の値がallowedValuesの最小値を越えている場合は、その最小値を返します。いわゆるclampです。
110~119行目で、allowedValuesの中から引数の値との距離が最も小さくなる値を計算して返します。このコードはinjectメソッドを使って書いてもよいでしょう。
121~123行目で、allowedValuesの設定がないときは、レンジの最大値、最小値でclampします。
125~147行目の、setValueは、ハンドルの値を書き換える関数です。引数にスライダーの値とハンドルのインデックスを取ります。
126行目で、activeは、ハンドルのドラッグ中にtrueになるフラグなので、それがtrueでないときにこの関数が呼ばれたというのは、ドラッグ以外の手段でハンドルの値を書き換えようとしているとき、つまり初期化のときか、ライブラリの利用者が直接この関数を呼んだということです。そこで、127~129行目の処理で、'最近動かしたハンドル'としての情報を更新します。
127行目で、activeHandleIdxをhandleIdxにします。これは現在選択中のハンドルのインデックスを表します。引数の指定がなければ0になります。
128行目で、また、インデックスから現在選択中のハンドルを取得します。このハンドルはクラス名が'selected'になったり、ドラッグ時に位置が動いたりします。
129行目で、updateStylesを呼んで、現在選択中のハンドルのクラス名を'selected'にします。これによって、選択中のハンドルだけCSSで色を変えるといったことができます。
132~137行目で、restrictedが設定されているとき、隣のハンドルの値を越えないように制限します。インデックスの前後を見て、それらのハンドルの値を越えていたらsliderValueを直します。
138行目で、sliderValueを、スライダーの取れる値に直します。getNearestValueで、希望に最も近い値を選ぶようにします。
139行目で、引数で指定されたハンドルの値を更新します。
140行目で、後方互換性のために、valueの値も更新します。これはハンドルが1つしかなかったころの名残です。
142行目で、ハンドルの表示位置を更新します。スライダーが垂直ならCSSのtopプロパティを、水平ならleftプロパティを設定します。後述のtranslateToPxで、ハンドルの値から計算してCSSの値をpx単位で求めます。
145行目で、後述のdrawSpansでスパンを表示します。
146行目で、この関数がライブラリの利用者によって直接呼ばれたときは(これはドラッグ中でないことからわかります)updateFinishedフックを呼びます。
148~151行目の、setValueByは、変化量でハンドルの値を指定する関数です。引数に変化量とハンドルのインデックスを取ります。内部的には使われていません。キーボードからハンドルの値を変えられるようにしたいときなどに便利です。
149行目で、setValueに、ハンドルの現在値と変化量を足した値を渡します。引数でハンドルのインデックスが与えられなかったときはactiveHandleIdxを、あるいは0を、指定するようになっています。例えばドラッグ&ドロップの直後にこの関数を、インデックスを指定せずに呼ぶと、さっきまで動かしていたハンドルが動きます。
152~156行目の、translateToPxは、引数のハンドルの値を、CSSのtopやleftプロパティのために使うpx単位の値に変換する関数です。
154行目の、((トラックの長さ-ハンドルの幅)/レンジの幅)は、ハンドルの値とピクセルの換算率です。
155行目で、それを引数のハンドルの値の始点からの距離にかけて、Math.roundで丸めたものに、単位の'px'を付けて返します。
157~160行目の、translateToValueは、引数で与えるオフセット位置から、ハンドルの値に変換する関数です。ドラッグ&ドロップのときにマウスポインタの位置からスライダーの値を求めるのに使います。
158行目の、(レンジの幅/(トラックの長さ-ハンドルの幅))は、ハンドルの値とピクセルの換算率です。これはtranslateToPxのときの逆数です。
159行目で、それを引数の値にかけて、始点の値を足したものを返します。
161~165行目の、getRangeは、ハンドル同士の間にできる区間から、小さいほうから数えてrange番めの区間を返す関数です。
162行目で、ハンドルの値の配列をsortByメソッドでソートします。Prototype.Kは、Prototype.jsで定義されている恒等関数です。これは単にsort()と呼んだほうがよいと私は思います。
164行目で、ハンドルのrange番めの区間を$R関数で作って返します。
166~168行目の、minimumOffsetは、トラックの0点位置を返す関数です。スライダーの向きが垂直ならalignYを、水平ならalignXを返します。
169~175行目の、maximumOffsetは、トラックの最大位置を返す関数です。
170行目で、スライダーの向きが垂直なら、トラックのoffsetHeightを返します。しかし、スライダーがブラウザに表示されていないときにoffsetHeightが取得できないことがあります(ticket4011)。このときは、172行目にあるように、CSSのheightプロパティの数値部分からトラックの0点位置であるalignYを引いた値を返します。
173行目で、スライダーの向きが水平なら、トラックのoffsetWidthを返すか、あるいは同様に、CSSのwidthプロパティの数値部分からトラックの0点位置であるalignXを引いた値を返します。
176~178行目の、isVerticalは、スライダーの向きが垂直かどうかを返す関数です。
179~189行目の、drawSpansは、スパンたちの位置をそれぞれ計算して描画する関数です。
182行目で、spans配列にあるスパンたちに、順番にgetRangeとsetSpanをかけていきます。getRangeは前述の、r番めの区間の範囲を計算する関数で、setSpanは後述のとおり、実際にCSSを書き換えて描画する関数です。ちなみに、$R(0,this.spans.length-1).eachは、this.spans.eachを使ったほうがきれいだと思います。
183行目で、options.startSpanは、先頭のスパンだけ特別に扱うオプションです。
184行目で、先頭のスパンを扱うために、setSpanに渡す区間の範囲は、始点が0です。これはthis.minimumでなければおかしいので、バグです。そして終点は、ハンドルが1つより多ければ、getRangeで先頭の区間を求めてその始点の値を使い、そうでなければ、ハンドルの値(this.value)を使います。this.valueは、setValueの140行めで常に1番めのハンドルの値になるようになっています。
186行目で、options.endSpanは、最後尾のスパンだけ特別に扱うオプションです。
187行目で、最後尾のスパンを扱うために、setSpanに渡す区間の範囲は、始点は、ハンドルが1つより多ければ、getRangeで最後尾の区間を求めてその終点を使い、そうでなければ、ハンドルの値(this.value)を使います。終点は、スライダーが取りうる最大値を使います。
190~198行目の、setSpanは、実際にスパンのCSSを書き換える関数です。引数に、書き換える要素と、そのスパンの範囲をとります。
191行目で、スライダーが垂直なら、CSSのtopとheightを書き換えます。topにはスパンの始点を、heightにはスパンの幅を、それぞれtranslateToPxでCSSの値に直して代入します。
194行目で、スライダーが水平なら、CSSのleftとwidthを書き換えます。leftにはスパンの始点を、widthにはスパンの幅を、それぞれtranslateToPxでCSSの値に直して代入します。
199~202行目の、updateStylesは、選択中のハンドルのクラス名に'selected'を加える関数です。setValueでハンドルを動かしたときや、startDragでドラッグを始めたときに、この関数が呼ばれます。
200行目で、一度、全てのハンドルのクラス名から'selected'を除きます。
201行目で、改めて、選択中のハンドルのクラス名に'selected'を加えます。
203~238行目の、startDragは、ドラッグを開始する関数です。この関数は、(トラックとハンドルの)mousedownイベントのリスナです。
205行目で、disabledフラグがtrueのときは、ドラッグを開始しません。
206行目で、ドラッグが開始したことを示すactiveフラグをtrueにします。
208行目で、イベントの発生源を取得します。
209行目で、マウスポインタの位置を取得して[x,y]の配列にします。
211行目で、もしイベントの発生源がトラックなら、以下の処理を行います。
212行目で、Prototype.jsのcumulativeOffsetメソッドで、ドキュメント基点からのオフセットを取得します。
214行目で、トラック要素の左上を基点にマウスポインタの位置を計算して、前述のtranslateToValueでスライダーの値を求めます。ここでハンドルの幅の半分を補正します。これでハンドルの中央部分がポインタの真下にきます。このsetValueを呼ぶときに、ハンドルのインデックスを指定していないので、現在選択中の(つまり最近に動かしていた)ハンドルが動きます。
217行目で、選択中のハンドルのオフセットを取得します。
218、219行目で、ポインタ位置と、選択中のハンドルのオフセットのずれ具合を保存しておきます。ドラッグしたときに、このずれを保ちながらハンドルが動くようにするためです。
220行目で、もしイベントの発生源がトラックでなければ、以下でそれに最も近いハンドルを探しだします。
222行目で、whileループで、該当するハンドルが見つかるまで、親要素をたどります。
225行目で、それで実際にハンドルが見つかれば、以下の処理をします。
226、227行目で、それを選択中のハンドルとします。
228行目で、updateStylesを呼んで、選択中のハンドルのクラス名に'selected'を加えます。
230~232行目で、ここでもやはり、ポインタ位置と、選択中のハンドルのオフセットのずれ具合を保存しておきます。
239~246行目の、updateは、ドラッグ開始後にマウスポインタの動きに応じてスライダーを動かす関数です。この関数はdocumentのmousemoveイベントのリスナです。
240行目で、activeフラグがtrueということは、ドラッグ開始後ということです。
241行目で、ドラッグ中であることを示すdraggingフラグをtrueにします。
242行目で、後述のdraw関数を呼びます。
243行目で、サファリブラウザには、window.scrollBy(0,0)で再描画を促します。
247~256行目の、drawは、ドラッグ中のスライダーを描画する関数です。上述のupdate関数(mousemoveイベントリスナ)からeventを引数で渡されます。
248行目で、マウスポインタの位置を取得して配列にします。
249行目で、cumulativeOffsetメソッドで、ドキュメント基点からのオフセットを取得します。
250、251行目で、ドラッグ開始時のハンドルとマウスポインタのずれ具合を加味して、トラックの左上を基点としたポインタの位置を求めます。
253行目で、ポインタの位置をtranslateToValueで、スライダーの値に換算してsetValueします。
254行目で、onSlideフックがあれば呼びます。引数にハンドルの値と、このインスタンスを渡します。
257~264行目の、endDragは、ドラッグを終了(ドロップ)する関数です。
258行目で、activeフラグは、ドラッグ開始後であることを表し、draggingフラグは、ドラッグ中であることを表しています。両方のフラグが立っているとき、後述のfinishDragを呼びます。
262、263行目で、フラグをそれぞれfalseにします。
265~269行目の、finishDragは、ドラッグ終了時にendDragから呼ばれる関数です。updateFinishedを呼びます。
270~276行目の、updateFinishedは、setValueで値が更新されたときと、ドラッグの終了時に呼ばれる関数です。
271行目で、onChangeフックがあれば呼びます。引数にハンドルの値と、このインスタンスを渡します。
273行目で、this.eventをnullにします。