JavaScriptでドラッグ&ドロップを実現するためのライブラリ、dragdrop.jsを、3回に分けて解説していきます。
昨今、メールクライアントなど、これまでデスクトップにあったものがブラウザの上で動くようになりました。それによって、"外出先でも使える""インストール作業がいらない"などの利点はあるものの、GUIとしては退化してしまった感があります。
ドラッグ&ドロップという操作は、物を運んだり、他の場所に置くといった、人間の行動を直感的に表現していて、優れたGUIです。このライブラリを使えば、Webページをよりインタラクティブなものに変えていくことができるはずです。
1~8行目は著作権表示です。
9~11行目で、dragdrop.jsはeffects.jsに依存しているので、effects.jsがロードされているかをチェックしています。
Droppables
ドラッグ&ドロップの、ドロップ先を記述したクラスです。このクラスの機能には、ドラッグが上空に来たときスタイルの変化でドロップを促したり、ドロップした瞬間にフックを呼んだり、ドロップを受け付けるかどうかを制限するといったものがあります。
このクラスにおいてoptionsは、変数名からはただオプション設定をまとめたものに思えますが、実はドロップ先の情報を一手に引き受けまとめている重要なオブジェクトになっています。実際、drops配列にこのoptionsを格納することからも、それがわかります。
ここでまとめて、このクラスに登場するプロパティの意味を解説します。
- last_active
- 現在、ドラッグが上空にあるドロップ先です。マウスイベントで常に更新されます。
- options.greedy
- デフォルトはtrueです。まだ実装されていませんが、公式wikiの説明では、ドロップ先としてより貪欲に振る舞い、ドラッグが上空にきただけで強制的にドロップさせるオプションのようです。
- options.hoverclass
- デフォルトはnullです。クラス名が入ります。ドロップを受けつけるドラッグが上空にきたときに、ドロップ先の要素に指定したクラス名を追加するオプションです。例えば、このクラス名に要素を明るくするようなCSSを定義しておくことで、ユーザに、ドロップできることを視覚で伝えることができます。
- options.tree
- デフォルトはfalseです。後編で解説するSortableでドロップ先がツリーとして振る舞うときに、このオプションが重要になります。
- options.containment
- DOM idか、DOM idの配列を指定します。ここで指定した要素の子要素のドロップだけを受けつけます。
- options._containers
- 内部的に使います。上述のcontainmentで指定したDOM idに$関数を適用した結果を、ここにキャッシュしておきます。
- options.accept
- DOM idか、DOM idの配列を指定します。ここで指定した要素のドロップだけを受けつけます。
- options.element
- 内部的に使います。ドロップ先の要素です。
- options.onHover
- ドロップを受けつけるドラッグが上空にきたときに呼ぶフックです。このフックは引数に、ドロップ先の要素、ドラッグ中の要素、それらの位置の重なり具合、をとります。
- options.onDrop
- ドロップがあったときに呼ぶフックです。このフックは引数に、ドロップ先の要素、ドロップした要素、マウスイベントのイベントオブジェクト、をとります。
それではコードを見ていきましょう。
12~14行目のdropsは、ドロップ先の情報をまとめたoptionsを、順番に格納するための配列です。
15~18行目のremoveは、引数のドロップ先要素を、drops配列から削除する関数です。
19~45行目のaddは、引数の要素を、ドロップ先としてdrops配列に追加する関数です。
31行目で、options.containmentがDOM idの配列で与えられた場合は、それぞれに$関数を適用して_containersに追加していきます。これはmap関数を使ってもよいでしょう。
33行目で、同様に、DOM idで与えられた場合も、$関数を適用して追加します。
38行目で、options.acceptには、DOM idの配列か、DOM idを与えることになっていますが、ここでDOM idの配列の形に統一します。そのためにいったん配列にしてflatten関数を呼びます。
40行目で、ドロップ先は移動する必要がないのですが、IEの仕様のため、ここでmakePositionedします。
43行目で、drops配列にoptionsを追加します。
46~55行目のfindDeepestChildは、drops配列の要素の中で、もっとも深い子要素になるものを返す関数です。比較にはElement.isParentを使います。
Element.isParentの定義は947~953行目にあります。再帰的に、左辺の要素が右辺の要素に子孫として含まれるかどうかを調べます。
56~65行目のisContainedは、引数に、ドラッグ中の要素と、ドロップ先をとります。ドラッグ中の要素の親要素が、ドロップ先の_containersプロパティに列挙されているかを返す関数です。この判定が、そのドロップを受け付けるかどうかに使われます。
58行目で、Sortableクラスでは、ドロップ先がツリーとして振る舞う場合があります。そこで、drop.treeがtrueのときは、ツリーの根の要素を親要素と考え、element.treeNodeを参照します。
61行目で、通常は、element.parentNodeを参照します。
66~76行目のisAffectedは、引数に、ドラッグ中の座標と、ドラッグ中の要素と、ドロップ先をとります。この座標のドラッグをドロップ先が受けつけるかを返す関数です。
70行目で、上述のisContained関数の判定を調べます。
72行目で、ドロップ先のoptions.acceptに含まれるかを調べます。
74行目で、座標がドロップ先の要素の範囲内にあるか調べるために、Position.withinを使います。これらの条件をすべて満たしている必要があります。
77~82行目のdeactivateは、引数にドロップ先をとります。ドロップ先からhoverclassのクラス名を削除したり、last_activeプロパティをnullにする関数です。
83~88行目のactivateは、引数にドロップ先をとります。ドラッグが上空にあるドロップ先に、hoverclassのクラス名を追加したり、last_activeプロパティをセットする関数です。last_activeプロパティは、現在ドラッグが上空にあるドロップ先を示す値です。
89~110行目のshowは、引数にドラッグ中の座標と、ドラッグ中の要素をとります。ドラッグが上空にあるドロップ先を見つけて処理をする関数です。
93行目で、ドロップ先すべてを、isAffected関数でチェックして、この座標、このドラッグ中の要素について、ドロップを受けつけるものをaffected配列に列挙します。
98行目で、affectedの候補がいくつかあった場合は、findDeepestChild関数で、もっともDOMツリーの階層が深い要素を選びます。
101行目で、前回の結果であるlast_activeと変化があったときは、前回の結果にdeactivate関数を呼び、107行目で、今回の結果にactivate関数を呼びます。
104行目で、onHoverフックがあれば呼びます。Position.overlapを呼び出していますが、この関数は直前に、Position.withinを呼ぶ必要があることに注意してください。それが103行目です。
111~121行目のfireは、引数にイベント情報と、ドラッグした要素をとります。ドラッグ終了直後に呼ばれる関数です。
113行目で、Position.prepareを呼んで、スクロール位置の調整をします。
115行目で、このドロップを受けつけるかをisAffected関数でチェックします。
116行目で、もしonDropフックがあれば呼びます。
122~127行目のresetは、deactivate関数を呼んで、現在ドラッグが上空にあるドロップ先であるlast_activeの状態を、元に戻す関数です。
Draggables
ドラッグ可能要素の全体を管理するためのクラスです。以下で、このクラスに登場するプロパティの意味について解説します。
- drags
- Draggableクラスのインスタンスをすべて保持する配列です。
- observers
- SortableObserverクラスのインスタンスが入る配列です。
- onStartCount、onEndCount、onDragCount
- observers配列にあるフックの数を記憶するためのプロパティです。フックの追加、削除のたびに_cacheObserverCallbacksで更新します。
- activeDraggable
- 現在ドラッグ中の要素を示します。マウスイベントで常に更新します。
- _lastPointer
- 前回のマウスポインタの位置です。前回のマウスイベントと比べてポインタの位置に変化があったときだけ処理を行うために必要です。
- _lastScrollPointer
- 前回のスクロール位置です。
- _timeout
- activate関数で処理を指定時間遅延するためのタイマです。
それではコードを見ていきましょう。
129、130行目のdrags、observersは上述の通りです。
132~144行目のregisterは、引数にドラッグ可能要素をとります。drags配列に、ドラッグ可能要素を登録する関数です。
133~141行目で、もしこれが初めての登録ならば、documentオブジェクトに各種のイベントハンドラを設定します。
142行目で、drags配列に要素を追加します。
145~153行目のunregisterは、引数にドラッグ可能要素をとります。このクラスのdrags配列から、ドラッグ可能要素を削除する関数です。
147~151行目で、もしこれが最終の削除ならば、documentオブジェクトからイベントハンドラを解除します。
154~166行目のactivateは、引数にドラッグ可能要素をとります。現在ドラッグ中の要素を示すactiveDraggableを設定する関数です。指定した時間、処理を遅延する機能があります。
155~160行目で、ドラッグ可能要素のoptions.delayで指定した時間、タイマで処理を遅延します。
158、162行目で、キーボードイベントを受けつけるため、window.focusでフォーカスを持ちます。
159、163行目で、現在ドラッグ中の要素を示すactiveDraggableを更新します。
167~170行目のdeactivateは、現在ドラッグ中の要素を示すactiveDraggableをnullにする関数です。
171~181行目のupdateDragは、documentオブジェクトのmousemoveイベントハンドラです。ドラッグ中の処理をする関数です。
176行目で、前回のマウスポインタの位置と比べて変化があったときに、次の処理に進みます。
179行目で、現在ドラッグ中の要素のupdateDragメソッドを呼びます。
182~192行目のendDragは、documentオブジェクトのmouseupイベントハンドラです。ドラッグ終了時の処理をする関数です。
189行目で、ドラッグしていた要素のendDragメソッドを呼びます。さらに状態をリセットします。
193~197行目のkeyPressは、documentオブジェクトのkeyPressイベントハンドラです。ドラッグ中の要素のkeyPressメソッドを呼びます。デフォルトでは、これはEscキーのときドラッグをキャンセルする処理につながります。
198~202行目のaddObserverは、引数にSortableObserverのインスタンスをとります。ドラッグ可能要素全体についてのフックをobservers配列に追加する関数です。
200行目で、_cacheObserverCallbacksを呼んで、フックの数を常に記憶しておきます。
203~207行目のremoveObserverは、引数にドラッグ可能要素をとります。observers配列からこの要素にからんだフックをすべて削除する関数です。
203行目のコメントで、フックでなく要素を中心に考えることでメモリリークを防げる旨が書かれています。
205行目で、_cacheObserverCallbacksを呼んで、フックの数を常に記憶しておきます。
208~215行目のnotifyは、引数にイベント名('onStart'か'onEnd'か'onDrag')と、ドラッグ可能要素と、イベント情報をとります。イベント名に該当するフックを呼ぶ関数です。
209行目で、observers配列の線形探索を何度もしなくてすむように、_cacheObserverCallbacksで、フックの数を常に記憶してあったのが役に立ちます。
210行目で、observers配列からイベント名に該当するフックを呼びます。
213行目で、さらに、ドラッグ可能要素の該当するフックを呼びます。
216~226行目の_cacheObserverCallbacksは、observers配列のなかのSortableObserverのインスタンスのフックの数を常に記憶するための関数です。
217行目で、フックの数を、それぞれonStartCount、onEndCount、onDragCountプロパティに保存します。