source: products/qPloneDropDownMenu/branches/0.2/skins/qPloneDropDownMenu/javascripts/dragdrop.js @ 1

Last change on this file since 1 was 1, checked in by myroslav, 18 years ago

Building directory structure

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