JavaScriptでドラッグ&ドロップを実現するためのライブラリ、
昨今、
ドラッグ&ドロップという操作は、
0001:// script.aculo.us dragdrop.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008
0002:
0003:// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
0004:// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, [email protected])
0005://
0006:// script.aculo.us is freely distributable under the terms of an MIT-style license.
0007:// For details, see the script.aculo.us web site: http://script.aculo.us/
0008:
1~8行目は著作権表示です。
0009:if(Object.isUndefined(Effect))
0010: throw("dragdrop.js requires including script.aculo.us' effects.js library");
0011:
9~11行目で、
Droppables
ドラッグ&ドロップの、
このクラスにおいて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 - ドロップがあったときに呼ぶフックです。このフックは引数に、
ドロップ先の要素、 ドロップした要素、 マウスイベントのイベントオブジェクト、 をとります。
それではコードを見ていきましょう。
0012:var Droppables = {
0013: drops: [],
0014:
12~14行目のdropsは、
0015: remove: function(element) {
0016: this.drops = this.drops.reject(function(d) { return d.element==$(element) });
0017: },
0018:
15~18行目のremoveは、
0019: add: function(element) {
0020: element = $(element);
0021: var options = Object.extend({
0022: greedy: true,
0023: hoverclass: null,
0024: tree: false
0025: }, arguments[1] || { });
0026:
0027: // cache containers
0028: if(options.containment) {
0029: options._containers = [];
0030: var containment = options.containment;
0031: if(Object.isArray(containment)) {
0032: containment.each( function(c) { options._containers.push($(c)) });
0033: } else {
0034: options._containers.push($(containment));
0035: }
0036: }
0037:
0038: if(options.accept) options.accept = [options.accept].flatten();
0039:
0040: Element.makePositioned(element); // fix IE
0041: options.element = element;
0042:
0043: this.drops.push(options);
0044: },
0045:
19~45行目のaddは、
31行目で、
33行目で、
38行目で、
40行目で、
43行目で、
0046: findDeepestChild: function(drops) {
0047: deepest = drops[0];
0048:
0049: for (i = 1; i < drops.length; ++i)
0050: if (Element.isParent(drops[i].element, deepest.element))
0051: deepest = drops[i];
0052:
0053: return deepest;
0054: },
0055:
46~55行目のfindDeepestChildは、
Element.
0947:// Returns true if child is contained within element
0948:Element.isParent = function(child, element) {
0949: if (!child.parentNode || child == element) return false;
0950: if (child.parentNode == element) return true;
0951: return Element.isParent(child.parentNode, element);
0952:}
0953:
0056: isContained: function(element, drop) {
0057: var containmentNode;
0058: if(drop.tree) {
0059: containmentNode = element.treeNode;
0060: } else {
0061: containmentNode = element.parentNode;
0062: }
0063: return drop._containers.detect(function(c) { return containmentNode == c });
0064: },
0065:
56~65行目のisContainedは、
58行目で、
61行目で、
0066: isAffected: function(point, element, drop) {
0067: return (
0068: (drop.element!=element) &&
0069: ((!drop._containers) ||
0070: this.isContained(element, drop)) &&
0071: ((!drop.accept) ||
0072: (Element.classNames(element).detect(
0073: function(v) { return drop.accept.include(v) } ) )) &&
0074: Position.within(drop.element, point[0], point[1]) );
0075: },
0076:
66~76行目のisAffectedは、
70行目で、
72行目で、
74行目で、
0077: deactivate: function(drop) {
0078: if(drop.hoverclass)
0079: Element.removeClassName(drop.element, drop.hoverclass);
0080: this.last_active = null;
0081: },
0082:
77~82行目のdeactivateは、
0083: activate: function(drop) {
0084: if(drop.hoverclass)
0085: Element.addClassName(drop.element, drop.hoverclass);
0086: this.last_active = drop;
0087: },
0088:
83~88行目のactivateは、
0089: show: function(point, element) {
0090: if(!this.drops.length) return;
0091: var drop, affected = [];
0092:
0093: this.drops.each( function(drop) {
0094: if(Droppables.isAffected(point, element, drop))
0095: affected.push(drop);
0096: });
0097:
0098: if(affected.length>0)
0099: drop = Droppables.findDeepestChild(affected);
0100:
0101: if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
0102: if (drop) {
0103: Position.within(drop.element, point[0], point[1]);
0104: if(drop.onHover)
0105: drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
0106:
0107: if (drop != this.last_active) Droppables.activate(drop);
0108: }
0109: },
0110:
89~110行目のshowは、
93行目で、
98行目で、
101行目で、
104行目で、
0111: fire: function(event, element) {
0112: if(!this.last_active) return;
0113: Position.prepare();
0114:
0115: if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
0116: if (this.last_active.onDrop) {
0117: this.last_active.onDrop(element, this.last_active.element, event);
0118: return true;
0119: }
0120: },
0121:
111~121行目のfireは、
113行目で、
115行目で、
116行目で、
0122: reset: function() {
0123: if(this.last_active)
0124: this.deactivate(this.last_active);
0125: }
0126:}
0127:
122~127行目のresetは、
Draggables
ドラッグ可能要素の全体を管理するためのクラスです。以下で、
- drags
- Draggableクラスのインスタンスをすべて保持する配列です。
- observers
- SortableObserverクラスのインスタンスが入る配列です。
- onStartCount、
onEndCount、 onDragCount - observers配列にあるフックの数を記憶するためのプロパティです。フックの追加、
削除のたびに_cacheObserverCallbacksで更新します。 - activeDraggable
- 現在ドラッグ中の要素を示します。マウスイベントで常に更新します。
- _lastPointer
- 前回のマウスポインタの位置です。前回のマウスイベントと比べてポインタの位置に変化があったときだけ処理を行うために必要です。
- _lastScrollPointer
- 前回のスクロール位置です。
- _timeout
- activate関数で処理を指定時間遅延するためのタイマです。
それではコードを見ていきましょう。
0128:var Draggables = {
0129: drags: [],
0130: observers: [],
0131:
129、
0132: register: function(draggable) {
0133: if(this.drags.length == 0) {
0134: this.eventMouseUp = this.endDrag.bindAsEventListener(this);
0135: this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
0136: this.eventKeypress = this.keyPress.bindAsEventListener(this);
0137:
0138: Event.observe(document, "mouseup", this.eventMouseUp);
0139: Event.observe(document, "mousemove", this.eventMouseMove);
0140: Event.observe(document, "keypress", this.eventKeypress);
0141: }
0142: this.drags.push(draggable);
0143: },
0144:
132~144行目のregisterは、
133~141行目で、
142行目で、
0145: unregister: function(draggable) {
0146: this.drags = this.drags.reject(function(d) { return d==draggable });
0147: if(this.drags.length == 0) {
0148: Event.stopObserving(document, "mouseup", this.eventMouseUp);
0149: Event.stopObserving(document, "mousemove", this.eventMouseMove);
0150: Event.stopObserving(document, "keypress", this.eventKeypress);
0151: }
0152: },
0153:
145~153行目のunregisterは、
147~151行目で、
0154: activate: function(draggable) {
0155: if(draggable.options.delay) {
0156: this._timeout = setTimeout(function() {
0157: Draggables._timeout = null;
0158: window.focus();
0159: Draggables.activeDraggable = draggable;
0160: }.bind(this), draggable.options.delay);
0161: } else {
0162: window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
0163: this.activeDraggable = draggable;
0164: }
0165: },
0166:
154~166行目のactivateは、
155~160行目で、
158、
159、
0167: deactivate: function() {
0168: this.activeDraggable = null;
0169: },
0170:
167~170行目のdeactivateは、
0171: updateDrag: function(event) {
0172: if(!this.activeDraggable) return;
0173: var pointer = [Event.pointerX(event), Event.pointerY(event)];
0174: // Mozilla-based browsers fire successive mousemove events with
0175: // the same coordinates, prevent needless redrawing (moz bug?)
0176: if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
0177: this._lastPointer = pointer;
0178:
0179: this.activeDraggable.updateDrag(event, pointer);
0180: },
0181:
171~181行目のupdateDragは、
176行目で、
179行目で、
0182: endDrag: function(event) {
0183: if(this._timeout) {
0184: clearTimeout(this._timeout);
0185: this._timeout = null;
0186: }
0187: if(!this.activeDraggable) return;
0188: this._lastPointer = null;
0189: this.activeDraggable.endDrag(event);
0190: this.activeDraggable = null;
0191: },
0192:
182~192行目のendDragは、
189行目で、
0193: keyPress: function(event) {
0194: if(this.activeDraggable)
0195: this.activeDraggable.keyPress(event);
0196: },
0197:
193~197行目のkeyPressは、
0198: addObserver: function(observer) {
0199: this.observers.push(observer);
0200: this._cacheObserverCallbacks();
0201: },
0202:
198~202行目のaddObserverは、
200行目で、
0203: removeObserver: function(element) { // element instead of observer fixes mem leaks
0204: this.observers = this.observers.reject( function(o) { return o.element==element });
0205: this._cacheObserverCallbacks();
0206: },
0207:
203~207行目のremoveObserverは、
203行目のコメントで、
205行目で、
0208: notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
0209: if(this[eventName+'Count'] > 0)
0210: this.observers.each( function(o) {
0211: if(o[eventName]) o[eventName](eventName, draggable, event);
0212: });
0213: if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
0214: },
0215:
208~215行目のnotifyは、
209行目で、
210行目で、
213行目で、
0216: _cacheObserverCallbacks: function() {
0217: ['onStart','onEnd','onDrag'].each( function(eventName) {
0218: Draggables[eventName+'Count'] = Draggables.observers.select(
0219: function(o) { return o[eventName]; }
0220: ).length;
0221: });
0222: }
0223:}
0224:
0225:/*--------------------------------------------------------------------------*/
0226:
216~226行目の_cacheObserverCallbacksは、
217行目で、