]> projects.mako.cc - selectricity/blob - public/javascripts/dragdrop.js
63a68243b6c2d38d0b5a6e289ccd22a8cdfbeb71
[selectricity] / public / javascripts / dragdrop.js
1 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // 
3 // Element.Class part Copyright (c) 2005 by Rick Olson
4 // 
5 // See scriptaculous.js for full license.
6
7 /*--------------------------------------------------------------------------*/
8
9 var Droppables = {
10   drops: [],
11
12   remove: function(element) {
13     this.drops = this.drops.reject(function(d) { return d.element==element });
14   },
15
16   add: function(element) {
17     element = $(element);
18     var options = Object.extend({
19       greedy:     true,
20       hoverclass: null  
21     }, arguments[1] || {});
22
23     // cache containers
24     if(options.containment) {
25       options._containers = [];
26       var containment = options.containment;
27       if((typeof containment == 'object') && 
28         (containment.constructor == Array)) {
29         containment.each( function(c) { options._containers.push($(c)) });
30       } else {
31         options._containers.push($(containment));
32       }
33     }
34
35     Element.makePositioned(element); // fix IE
36     options.element = element;
37
38     this.drops.push(options);
39   },
40
41   isContained: function(element, drop) {
42     var parentNode = element.parentNode;
43     return drop._containers.detect(function(c) { return parentNode == c });
44   },
45
46   isAffected: function(pX, pY, element, drop) {
47     return (
48       (drop.element!=element) &&
49       ((!drop._containers) ||
50         this.isContained(element, drop)) &&
51       ((!drop.accept) ||
52         (Element.Class.has_any(element, drop.accept))) &&
53       Position.within(drop.element, pX, pY) );
54   },
55
56   deactivate: function(drop) {
57     if(drop.hoverclass)
58       Element.Class.remove(drop.element, drop.hoverclass);
59     this.last_active = null;
60   },
61
62   activate: function(drop) {
63     if(this.last_active) this.deactivate(this.last_active);
64     if(drop.hoverclass)
65       Element.Class.add(drop.element, drop.hoverclass);
66     this.last_active = drop;
67   },
68
69   show: function(event, element) {
70     if(!this.drops.length) return;
71     var pX = Event.pointerX(event);
72     var pY = Event.pointerY(event);
73     Position.prepare();
74
75     var i = this.drops.length-1; do {
76       var drop = this.drops[i];
77       if(this.isAffected(pX, pY, element, drop)) {
78         if(drop.onHover)
79            drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
80         if(drop.greedy) { 
81           this.activate(drop);
82           return;
83         }
84       }
85     } while (i--);
86     
87     if(this.last_active) this.deactivate(this.last_active);
88   },
89
90   fire: function(event, element) {
91     if(!this.last_active) return;
92     Position.prepare();
93
94     if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
95       if (this.last_active.onDrop) 
96         this.last_active.onDrop(element, this.last_active.element, event);
97   },
98
99   reset: function() {
100     if(this.last_active)
101       this.deactivate(this.last_active);
102   }
103 }
104
105 var Draggables = {
106   observers: [],
107   addObserver: function(observer) {
108     this.observers.push(observer);    
109   },
110   removeObserver: function(element) {  // element instead of obsever fixes mem leaks
111     this.observers = this.observers.reject( function(o) { return o.element==element });
112   },
113   notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
114     this.observers.invoke(eventName, draggable);
115   }
116 }
117
118 /*--------------------------------------------------------------------------*/
119
120 var Draggable = Class.create();
121 Draggable.prototype = {
122   initialize: function(element) {
123     var options = Object.extend({
124       handle: false,
125       starteffect: function(element) { 
126         new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
127       },
128       reverteffect: function(element, top_offset, left_offset) {
129         var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
130         new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
131       },
132       endeffect: function(element) { 
133          new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
134       },
135       zindex: 1000,
136       revert: false
137     }, arguments[1] || {});
138
139     this.element      = $(element);
140     if(options.handle && (typeof options.handle == 'string'))
141       this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
142       
143     if(!this.handle) this.handle = $(options.handle);
144     if(!this.handle) this.handle = this.element;
145
146     Element.makePositioned(this.element); // fix IE    
147
148     this.offsetX      = 0;
149     this.offsetY      = 0;
150     this.originalLeft = this.currentLeft();
151     this.originalTop  = this.currentTop();
152     this.originalX    = this.element.offsetLeft;
153     this.originalY    = this.element.offsetTop;
154
155     this.options      = options;
156
157     this.active       = false;
158     this.dragging     = false;   
159
160     this.eventMouseDown = this.startDrag.bindAsEventListener(this);
161     this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
162     this.eventMouseMove = this.update.bindAsEventListener(this);
163     this.eventKeypress  = this.keyPress.bindAsEventListener(this);
164     
165     this.registerEvents();
166   },
167   destroy: function() {
168     Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
169     this.unregisterEvents();
170   },
171   registerEvents: function() {
172     Event.observe(document, "mouseup", this.eventMouseUp);
173     Event.observe(document, "mousemove", this.eventMouseMove);
174     Event.observe(document, "keypress", this.eventKeypress);
175     Event.observe(this.handle, "mousedown", this.eventMouseDown);
176   },
177   unregisterEvents: function() {
178     //if(!this.active) return;
179     //Event.stopObserving(document, "mouseup", this.eventMouseUp);
180     //Event.stopObserving(document, "mousemove", this.eventMouseMove);
181     //Event.stopObserving(document, "keypress", this.eventKeypress);
182   },
183   currentLeft: function() {
184     return parseInt(this.element.style.left || '0');
185   },
186   currentTop: function() {
187     return parseInt(this.element.style.top || '0')
188   },
189   startDrag: function(event) {
190     if(Event.isLeftClick(event)) {
191       
192       // abort on form elements, fixes a Firefox issue
193       var src = Event.element(event);
194       if(src.tagName && (
195         src.tagName=='INPUT' ||
196         src.tagName=='SELECT' ||
197         src.tagName=='BUTTON' ||
198         src.tagName=='TEXTAREA')) return;
199       
200       // this.registerEvents();
201       this.active = true;
202       var pointer = [Event.pointerX(event), Event.pointerY(event)];
203       var offsets = Position.cumulativeOffset(this.element);
204       this.offsetX =  (pointer[0] - offsets[0]);
205       this.offsetY =  (pointer[1] - offsets[1]);
206       Event.stop(event);
207     }
208   },
209   finishDrag: function(event, success) {
210     // this.unregisterEvents();
211
212     this.active = false;
213     this.dragging = false;
214
215     if(this.options.ghosting) {
216       Position.relativize(this.element);
217       Element.remove(this._clone);
218       this._clone = null;
219     }
220
221     if(success) Droppables.fire(event, this.element);
222     Draggables.notify('onEnd', this);
223
224     var revert = this.options.revert;
225     if(revert && typeof revert == 'function') revert = revert(this.element);
226
227     if(revert && this.options.reverteffect) {
228       this.options.reverteffect(this.element, 
229       this.currentTop()-this.originalTop,
230       this.currentLeft()-this.originalLeft);
231     } else {
232       this.originalLeft = this.currentLeft();
233       this.originalTop  = this.currentTop();
234     }
235
236     if(this.options.zindex)
237       this.element.style.zIndex = this.originalZ;
238
239     if(this.options.endeffect) 
240       this.options.endeffect(this.element);
241
242
243     Droppables.reset();
244   },
245   keyPress: function(event) {
246     if(this.active) {
247       if(event.keyCode==Event.KEY_ESC) {
248         this.finishDrag(event, false);
249         Event.stop(event);
250       }
251     }
252   },
253   endDrag: function(event) {
254     if(this.active && this.dragging) {
255       this.finishDrag(event, true);
256       Event.stop(event);
257     }
258     this.active = false;
259     this.dragging = false;
260   },
261   draw: function(event) {
262     var pointer = [Event.pointerX(event), Event.pointerY(event)];
263     var offsets = Position.cumulativeOffset(this.element);
264     offsets[0] -= this.currentLeft();
265     offsets[1] -= this.currentTop();
266     var style = this.element.style;
267     if((!this.options.constraint) || (this.options.constraint=='horizontal'))
268       style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
269     if((!this.options.constraint) || (this.options.constraint=='vertical'))
270       style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
271     if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
272   },
273   update: function(event) {
274    if(this.active) {
275       if(!this.dragging) {
276         var style = this.element.style;
277         this.dragging = true;
278         
279         if(Element.getStyle(this.element,'position')=='') 
280           style.position = "relative";
281         
282         if(this.options.zindex) {
283           this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
284           style.zIndex = this.options.zindex;
285         }
286
287         if(this.options.ghosting) {
288           this._clone = this.element.cloneNode(true);
289           Position.absolutize(this.element);
290           this.element.parentNode.insertBefore(this._clone, this.element);
291         }
292
293         Draggables.notify('onStart', this);
294         if(this.options.starteffect) this.options.starteffect(this.element);
295       }
296
297       Droppables.show(event, this.element);
298       this.draw(event);
299       if(this.options.change) this.options.change(this);
300
301       // fix AppleWebKit rendering
302       if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
303
304       Event.stop(event);
305    }
306   }
307 }
308
309 /*--------------------------------------------------------------------------*/
310
311 var SortableObserver = Class.create();
312 SortableObserver.prototype = {
313   initialize: function(element, observer) {
314     this.element   = $(element);
315     this.observer  = observer;
316     this.lastValue = Sortable.serialize(this.element);
317   },
318   onStart: function() {
319     this.lastValue = Sortable.serialize(this.element);
320   },
321   onEnd: function() {
322     Sortable.unmark();
323     if(this.lastValue != Sortable.serialize(this.element))
324       this.observer(this.element)
325   }
326 }
327
328 var Sortable = {
329   sortables: new Array(),
330   options: function(element){
331     element = $(element);
332     return this.sortables.detect(function(s) { return s.element == element });
333   },
334   destroy: function(element){
335     element = $(element);
336     this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
337       Draggables.removeObserver(s.element);
338       s.droppables.each(function(d){ Droppables.remove(d) });
339       s.draggables.invoke('destroy');
340     });
341     this.sortables = this.sortables.reject(function(s) { return s.element == element });
342   },
343   create: function(element) {
344     element = $(element);
345     var options = Object.extend({ 
346       element:     element,
347       tag:         'li',       // assumes li children, override with tag: 'tagname'
348       dropOnEmpty: false,
349       tree:        false,      // fixme: unimplemented
350       overlap:     'vertical', // one of 'vertical', 'horizontal'
351       constraint:  'vertical', // one of 'vertical', 'horizontal', false
352       containment: element,    // also takes array of elements (or id's); or false
353       handle:      false,      // or a CSS class
354       only:        false,
355       hoverclass:  null,
356       ghosting:    false,
357       format:      null,
358       onChange:    Prototype.emptyFunction,
359       onUpdate:    Prototype.emptyFunction
360     }, arguments[1] || {});
361
362     // clear any old sortable with same element
363     this.destroy(element);
364
365     // build options for the draggables
366     var options_for_draggable = {
367       revert:      true,
368       ghosting:    options.ghosting,
369       constraint:  options.constraint,
370       handle:      options.handle };
371
372     if(options.starteffect)
373       options_for_draggable.starteffect = options.starteffect;
374
375     if(options.reverteffect)
376       options_for_draggable.reverteffect = options.reverteffect;
377     else
378       if(options.ghosting) options_for_draggable.reverteffect = function(element) {
379         element.style.top  = 0;
380         element.style.left = 0;
381       };
382
383     if(options.endeffect)
384       options_for_draggable.endeffect = options.endeffect;
385
386     if(options.zindex)
387       options_for_draggable.zindex = options.zindex;
388
389     // build options for the droppables  
390     var options_for_droppable = {
391       overlap:     options.overlap,
392       containment: options.containment,
393       hoverclass:  options.hoverclass,
394       onHover:     Sortable.onHover,
395       greedy:      !options.dropOnEmpty
396     }
397
398     // fix for gecko engine
399     Element.cleanWhitespace(element); 
400
401     options.draggables = [];
402     options.droppables = [];
403
404     // make it so
405
406     // drop on empty handling
407     if(options.dropOnEmpty) {
408       Droppables.add(element,
409         {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
410       options.droppables.push(element);
411     }
412
413     (this.findElements(element, options) || []).each( function(e) {
414       // handles are per-draggable
415       var handle = options.handle ? 
416         Element.Class.childrenWith(e, options.handle)[0] : e;    
417       options.draggables.push(
418         new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
419       Droppables.add(e, options_for_droppable);
420       options.droppables.push(e);      
421     });
422
423     // keep reference
424     this.sortables.push(options);
425
426     // for onupdate
427     Draggables.addObserver(new SortableObserver(element, options.onUpdate));
428
429   },
430
431   // return all suitable-for-sortable elements in a guaranteed order
432   findElements: function(element, options) {
433     if(!element.hasChildNodes()) return null;
434     var elements = [];
435     $A(element.childNodes).each( function(e) {
436       if(e.tagName && e.tagName==options.tag.toUpperCase() &&
437         (!options.only || (Element.Class.has(e, options.only))))
438           elements.push(e);
439       if(options.tree) {
440         var grandchildren = this.findElements(e, options);
441         if(grandchildren) elements.push(grandchildren);
442       }
443     });
444
445     return (elements.length>0 ? elements.flatten() : null);
446   },
447
448   onHover: function(element, dropon, overlap) {
449     if(overlap>0.5) {
450       Sortable.mark(dropon, 'before');
451       if(dropon.previousSibling != element) {
452         var oldParentNode = element.parentNode;
453         element.style.visibility = "hidden"; // fix gecko rendering
454         dropon.parentNode.insertBefore(element, dropon);
455         if(dropon.parentNode!=oldParentNode) 
456           Sortable.options(oldParentNode).onChange(element);
457         Sortable.options(dropon.parentNode).onChange(element);
458       }
459     } else {
460       Sortable.mark(dropon, 'after');
461       var nextElement = dropon.nextSibling || null;
462       if(nextElement != element) {
463         var oldParentNode = element.parentNode;
464         element.style.visibility = "hidden"; // fix gecko rendering
465         dropon.parentNode.insertBefore(element, nextElement);
466         if(dropon.parentNode!=oldParentNode) 
467           Sortable.options(oldParentNode).onChange(element);
468         Sortable.options(dropon.parentNode).onChange(element);
469       }
470     }
471   },
472
473   onEmptyHover: function(element, dropon) {
474     if(element.parentNode!=dropon) {
475       var oldParentNode = element.parentNode;
476       dropon.appendChild(element);
477       Sortable.options(oldParentNode).onChange(element);
478       Sortable.options(dropon).onChange(element);
479     }
480   },
481
482   unmark: function() {
483     if(Sortable._marker) Element.hide(Sortable._marker);
484   },
485
486   mark: function(dropon, position) {
487     // mark on ghosting only
488     var sortable = Sortable.options(dropon.parentNode);
489     if(sortable && !sortable.ghosting) return; 
490
491     if(!Sortable._marker) {
492       Sortable._marker = $('dropmarker') || document.createElement('DIV');
493       Element.hide(Sortable._marker);
494       Element.Class.add(Sortable._marker, 'dropmarker');
495       Sortable._marker.style.position = 'absolute';
496       document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
497     }    
498     var offsets = Position.cumulativeOffset(dropon);
499     Sortable._marker.style.top  = offsets[1] + 'px';
500     if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
501     Sortable._marker.style.left = offsets[0] + 'px';
502     Element.show(Sortable._marker);
503   },
504
505   serialize: function(element) {
506     element = $(element);
507     var sortableOptions = this.options(element);
508     var options = Object.extend({
509       tag:  sortableOptions.tag,
510       only: sortableOptions.only,
511       name: element.id,
512       format: sortableOptions.format || /^[^_]*_(.*)$/
513     }, arguments[1] || {});
514     return $(this.findElements(element, options) || []).collect( function(item) {
515       return (encodeURIComponent(options.name) + "[]=" + 
516               encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
517     }).join("&");
518   }
519 }

Benjamin Mako Hill || Want to submit a patch?