web/privatedoc/js/dragdrop.js
changeset 137 d1c25e3dfa36
parent 136 bde1974c263b
child 138 feb3a296e2bf
equal deleted inserted replaced
136:bde1974c263b 137:d1c25e3dfa36
     1 // script.aculo.us dragdrop.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
       
     2 
       
     3 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
       
     4 //           (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
       
     5 // 
       
     6 // See scriptaculous.js for full license.
       
     7 
       
     8 /*--------------------------------------------------------------------------*/
       
     9 
       
    10 if(typeof Effect == 'undefined')
       
    11   throw("dragdrop.js requires including script.aculo.us' effects.js library");
       
    12 
       
    13 var Droppables = {
       
    14   drops: [],
       
    15 
       
    16   remove: function(element) {
       
    17     this.drops = this.drops.reject(function(d) { return d.element==$(element) });
       
    18   },
       
    19 
       
    20   add: function(element) {
       
    21     element = $(element);
       
    22     var options = Object.extend({
       
    23       greedy:     true,
       
    24       hoverclass: null,
       
    25       tree:       false
       
    26     }, arguments[1] || {});
       
    27 
       
    28     // cache containers
       
    29     if(options.containment) {
       
    30       options._containers = [];
       
    31       var containment = options.containment;
       
    32       if((typeof containment == 'object') && 
       
    33         (containment.constructor == Array)) {
       
    34         containment.each( function(c) { options._containers.push($(c)) });
       
    35       } else {
       
    36         options._containers.push($(containment));
       
    37       }
       
    38     }
       
    39     
       
    40     if(options.accept) options.accept = [options.accept].flatten();
       
    41 
       
    42     Element.makePositioned(element); // fix IE
       
    43     options.element = element;
       
    44 
       
    45     this.drops.push(options);
       
    46   },
       
    47   
       
    48   findDeepestChild: function(drops) {
       
    49     deepest = drops[0];
       
    50       
       
    51     for (i = 1; i < drops.length; ++i)
       
    52       if (Element.isParent(drops[i].element, deepest.element))
       
    53         deepest = drops[i];
       
    54     
       
    55     return deepest;
       
    56   },
       
    57 
       
    58   isContained: function(element, drop) {
       
    59     var containmentNode;
       
    60     if(drop.tree) {
       
    61       containmentNode = element.treeNode; 
       
    62     } else {
       
    63       containmentNode = element.parentNode;
       
    64     }
       
    65     return drop._containers.detect(function(c) { return containmentNode == c });
       
    66   },
       
    67   
       
    68   isAffected: function(point, element, drop) {
       
    69     return (
       
    70       (drop.element!=element) &&
       
    71       ((!drop._containers) ||
       
    72         this.isContained(element, drop)) &&
       
    73       ((!drop.accept) ||
       
    74         (Element.classNames(element).detect( 
       
    75           function(v) { return drop.accept.include(v) } ) )) &&
       
    76       Position.within(drop.element, point[0], point[1]) );
       
    77   },
       
    78 
       
    79   deactivate: function(drop) {
       
    80     if(drop.hoverclass)
       
    81       Element.removeClassName(drop.element, drop.hoverclass);
       
    82     this.last_active = null;
       
    83   },
       
    84 
       
    85   activate: function(drop) {
       
    86     if(drop.hoverclass)
       
    87       Element.addClassName(drop.element, drop.hoverclass);
       
    88     this.last_active = drop;
       
    89   },
       
    90 
       
    91   show: function(point, element) {
       
    92     if(!this.drops.length) return;
       
    93     var affected = [];
       
    94     
       
    95     if(this.last_active) this.deactivate(this.last_active);
       
    96     this.drops.each( function(drop) {
       
    97       if(Droppables.isAffected(point, element, drop))
       
    98         affected.push(drop);
       
    99     });
       
   100         
       
   101     if(affected.length>0) {
       
   102       drop = Droppables.findDeepestChild(affected);
       
   103       Position.within(drop.element, point[0], point[1]);
       
   104       if(drop.onHover)
       
   105         drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
       
   106       
       
   107       Droppables.activate(drop);
       
   108     }
       
   109   },
       
   110 
       
   111   fire: function(event, element) {
       
   112     if(!this.last_active) return;
       
   113     Position.prepare();
       
   114 
       
   115     if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
       
   116       if (this.last_active.onDrop) 
       
   117         this.last_active.onDrop(element, this.last_active.element, event);
       
   118   },
       
   119 
       
   120   reset: function() {
       
   121     if(this.last_active)
       
   122       this.deactivate(this.last_active);
       
   123   }
       
   124 }
       
   125 
       
   126 var Draggables = {
       
   127   drags: [],
       
   128   observers: [],
       
   129   
       
   130   register: function(draggable) {
       
   131     if(this.drags.length == 0) {
       
   132       this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
       
   133       this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
       
   134       this.eventKeypress  = this.keyPress.bindAsEventListener(this);
       
   135       
       
   136       Event.observe(document, "mouseup", this.eventMouseUp);
       
   137       Event.observe(document, "mousemove", this.eventMouseMove);
       
   138       Event.observe(document, "keypress", this.eventKeypress);
       
   139     }
       
   140     this.drags.push(draggable);
       
   141   },
       
   142   
       
   143   unregister: function(draggable) {
       
   144     this.drags = this.drags.reject(function(d) { return d==draggable });
       
   145     if(this.drags.length == 0) {
       
   146       Event.stopObserving(document, "mouseup", this.eventMouseUp);
       
   147       Event.stopObserving(document, "mousemove", this.eventMouseMove);
       
   148       Event.stopObserving(document, "keypress", this.eventKeypress);
       
   149     }
       
   150   },
       
   151   
       
   152   activate: function(draggable) {
       
   153     if(draggable.options.delay) { 
       
   154       this._timeout = setTimeout(function() { 
       
   155         Draggables._timeout = null; 
       
   156         window.focus(); 
       
   157         Draggables.activeDraggable = draggable; 
       
   158       }.bind(this), draggable.options.delay); 
       
   159     } else {
       
   160       window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
       
   161       this.activeDraggable = draggable;
       
   162     }
       
   163   },
       
   164   
       
   165   deactivate: function() {
       
   166     this.activeDraggable = null;
       
   167   },
       
   168   
       
   169   updateDrag: function(event) {
       
   170     if(!this.activeDraggable) return;
       
   171     var pointer = [Event.pointerX(event), Event.pointerY(event)];
       
   172     // Mozilla-based browsers fire successive mousemove events with
       
   173     // the same coordinates, prevent needless redrawing (moz bug?)
       
   174     if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
       
   175     this._lastPointer = pointer;
       
   176     
       
   177     this.activeDraggable.updateDrag(event, pointer);
       
   178   },
       
   179   
       
   180   endDrag: function(event) {
       
   181     if(this._timeout) { 
       
   182       clearTimeout(this._timeout); 
       
   183       this._timeout = null; 
       
   184     }
       
   185     if(!this.activeDraggable) return;
       
   186     this._lastPointer = null;
       
   187     this.activeDraggable.endDrag(event);
       
   188     this.activeDraggable = null;
       
   189   },
       
   190   
       
   191   keyPress: function(event) {
       
   192     if(this.activeDraggable)
       
   193       this.activeDraggable.keyPress(event);
       
   194   },
       
   195   
       
   196   addObserver: function(observer) {
       
   197     this.observers.push(observer);
       
   198     this._cacheObserverCallbacks();
       
   199   },
       
   200   
       
   201   removeObserver: function(element) {  // element instead of observer fixes mem leaks
       
   202     this.observers = this.observers.reject( function(o) { return o.element==element });
       
   203     this._cacheObserverCallbacks();
       
   204   },
       
   205   
       
   206   notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
       
   207     if(this[eventName+'Count'] > 0)
       
   208       this.observers.each( function(o) {
       
   209         if(o[eventName]) o[eventName](eventName, draggable, event);
       
   210       });
       
   211     if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
       
   212   },
       
   213   
       
   214   _cacheObserverCallbacks: function() {
       
   215     ['onStart','onEnd','onDrag'].each( function(eventName) {
       
   216       Draggables[eventName+'Count'] = Draggables.observers.select(
       
   217         function(o) { return o[eventName]; }
       
   218       ).length;
       
   219     });
       
   220   }
       
   221 }
       
   222 
       
   223 /*--------------------------------------------------------------------------*/
       
   224 
       
   225 var Draggable = Class.create();
       
   226 Draggable._dragging    = {};
       
   227 
       
   228 Draggable.prototype = {
       
   229   initialize: function(element) {
       
   230     var defaults = {
       
   231       handle: false,
       
   232       reverteffect: function(element, top_offset, left_offset) {
       
   233         var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
       
   234         new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
       
   235           queue: {scope:'_draggable', position:'end'}
       
   236         });
       
   237       },
       
   238       endeffect: function(element) {
       
   239         var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
       
   240         new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
       
   241           queue: {scope:'_draggable', position:'end'},
       
   242           afterFinish: function(){ 
       
   243             Draggable._dragging[element] = false 
       
   244           }
       
   245         }); 
       
   246       },
       
   247       zindex: 1000,
       
   248       revert: false,
       
   249       scroll: false,
       
   250       scrollSensitivity: 20,
       
   251       scrollSpeed: 15,
       
   252       snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
       
   253       delay: 0
       
   254     };
       
   255     
       
   256     if(arguments[1] && typeof arguments[1].endeffect == 'undefined')
       
   257       Object.extend(defaults, {
       
   258         starteffect: function(element) {
       
   259           element._opacity = Element.getOpacity(element);
       
   260           Draggable._dragging[element] = true;
       
   261           new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
       
   262         }
       
   263       });
       
   264     
       
   265     var options = Object.extend(defaults, arguments[1] || {});
       
   266 
       
   267     this.element = $(element);
       
   268     
       
   269     if(options.handle && (typeof options.handle == 'string')) {
       
   270       var h = Element.childrenWithClassName(this.element, options.handle, true);
       
   271       if(h.length>0) this.handle = h[0];
       
   272     }
       
   273     if(!this.handle) this.handle = $(options.handle);
       
   274     if(!this.handle) this.handle = this.element;
       
   275     
       
   276     if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
       
   277       options.scroll = $(options.scroll);
       
   278       this._isScrollChild = Element.childOf(this.element, options.scroll);
       
   279     }
       
   280 
       
   281     Element.makePositioned(this.element); // fix IE    
       
   282 
       
   283     this.delta    = this.currentDelta();
       
   284     this.options  = options;
       
   285     this.dragging = false;   
       
   286 
       
   287     this.eventMouseDown = this.initDrag.bindAsEventListener(this);
       
   288     Event.observe(this.handle, "mousedown", this.eventMouseDown);
       
   289     
       
   290     Draggables.register(this);
       
   291   },
       
   292   
       
   293   destroy: function() {
       
   294     Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
       
   295     Draggables.unregister(this);
       
   296   },
       
   297   
       
   298   currentDelta: function() {
       
   299     return([
       
   300       parseInt(Element.getStyle(this.element,'left') || '0'),
       
   301       parseInt(Element.getStyle(this.element,'top') || '0')]);
       
   302   },
       
   303   
       
   304   initDrag: function(event) {
       
   305     if(typeof Draggable._dragging[this.element] != 'undefined' &&
       
   306       Draggable._dragging[this.element]) return;
       
   307     if(Event.isLeftClick(event)) {    
       
   308       // abort on form elements, fixes a Firefox issue
       
   309       var src = Event.element(event);
       
   310       if(src.tagName && (
       
   311         src.tagName=='INPUT' ||
       
   312         src.tagName=='SELECT' ||
       
   313         src.tagName=='OPTION' ||
       
   314         src.tagName=='BUTTON' ||
       
   315         src.tagName=='TEXTAREA')) return;
       
   316         
       
   317       var pointer = [Event.pointerX(event), Event.pointerY(event)];
       
   318       var pos     = Position.cumulativeOffset(this.element);
       
   319       this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
       
   320       
       
   321       Draggables.activate(this);
       
   322       Event.stop(event);
       
   323     }
       
   324   },
       
   325   
       
   326   startDrag: function(event) {
       
   327     this.dragging = true;
       
   328     
       
   329     if(this.options.zindex) {
       
   330       this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
       
   331       this.element.style.zIndex = this.options.zindex;
       
   332     }
       
   333     
       
   334     if(this.options.ghosting) {
       
   335       this._clone = this.element.cloneNode(true);
       
   336       Position.absolutize(this.element);
       
   337       this.element.parentNode.insertBefore(this._clone, this.element);
       
   338     }
       
   339     
       
   340     if(this.options.scroll) {
       
   341       if (this.options.scroll == window) {
       
   342         var where = this._getWindowScroll(this.options.scroll);
       
   343         this.originalScrollLeft = where.left;
       
   344         this.originalScrollTop = where.top;
       
   345       } else {
       
   346         this.originalScrollLeft = this.options.scroll.scrollLeft;
       
   347         this.originalScrollTop = this.options.scroll.scrollTop;
       
   348       }
       
   349     }
       
   350     
       
   351     Draggables.notify('onStart', this, event);
       
   352         
       
   353     if(this.options.starteffect) this.options.starteffect(this.element);
       
   354   },
       
   355   
       
   356   updateDrag: function(event, pointer) {
       
   357     if(!this.dragging) this.startDrag(event);
       
   358     Position.prepare();
       
   359     Droppables.show(pointer, this.element);
       
   360     Draggables.notify('onDrag', this, event);
       
   361     
       
   362     this.draw(pointer);
       
   363     if(this.options.change) this.options.change(this);
       
   364     
       
   365     if(this.options.scroll) {
       
   366       this.stopScrolling();
       
   367       
       
   368       var p;
       
   369       if (this.options.scroll == window) {
       
   370         with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
       
   371       } else {
       
   372         p = Position.page(this.options.scroll);
       
   373         p[0] += this.options.scroll.scrollLeft;
       
   374         p[1] += this.options.scroll.scrollTop;
       
   375         
       
   376         p[0] += (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0);
       
   377         p[1] += (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
       
   378         
       
   379         p.push(p[0]+this.options.scroll.offsetWidth);
       
   380         p.push(p[1]+this.options.scroll.offsetHeight);
       
   381       }
       
   382       var speed = [0,0];
       
   383       if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
       
   384       if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
       
   385       if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
       
   386       if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
       
   387       this.startScrolling(speed);
       
   388     }
       
   389     
       
   390     // fix AppleWebKit rendering
       
   391     if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
       
   392     
       
   393     Event.stop(event);
       
   394   },
       
   395   
       
   396   finishDrag: function(event, success) {
       
   397     this.dragging = false;
       
   398 
       
   399     if(this.options.ghosting) {
       
   400       Position.relativize(this.element);
       
   401       Element.remove(this._clone);
       
   402       this._clone = null;
       
   403     }
       
   404 
       
   405     if(success) Droppables.fire(event, this.element);
       
   406     Draggables.notify('onEnd', this, event);
       
   407 
       
   408     var revert = this.options.revert;
       
   409     if(revert && typeof revert == 'function') revert = revert(this.element);
       
   410     
       
   411     var d = this.currentDelta();
       
   412     if(revert && this.options.reverteffect) {
       
   413       this.options.reverteffect(this.element, 
       
   414         d[1]-this.delta[1], d[0]-this.delta[0]);
       
   415     } else {
       
   416       this.delta = d;
       
   417     }
       
   418 
       
   419     if(this.options.zindex)
       
   420       this.element.style.zIndex = this.originalZ;
       
   421 
       
   422     if(this.options.endeffect) 
       
   423       this.options.endeffect(this.element);
       
   424       
       
   425     Draggables.deactivate(this);
       
   426     Droppables.reset();
       
   427   },
       
   428   
       
   429   keyPress: function(event) {
       
   430     if(event.keyCode!=Event.KEY_ESC) return;
       
   431     this.finishDrag(event, false);
       
   432     Event.stop(event);
       
   433   },
       
   434   
       
   435   endDrag: function(event) {
       
   436     if(!this.dragging) return;
       
   437     this.stopScrolling();
       
   438     this.finishDrag(event, true);
       
   439     Event.stop(event);
       
   440   },
       
   441   
       
   442   draw: function(point) {
       
   443     var pos = Position.cumulativeOffset(this.element);
       
   444     if(this.options.ghosting) {
       
   445       var r   = Position.realOffset(this.element);
       
   446       window.status = r.inspect();
       
   447       pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
       
   448     }
       
   449     
       
   450     var d = this.currentDelta();
       
   451     pos[0] -= d[0]; pos[1] -= d[1];
       
   452     
       
   453     if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
       
   454       pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
       
   455       pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
       
   456     }
       
   457     
       
   458     var p = [0,1].map(function(i){ 
       
   459       return (point[i]-pos[i]-this.offset[i]) 
       
   460     }.bind(this));
       
   461     
       
   462     if(this.options.snap) {
       
   463       if(typeof this.options.snap == 'function') {
       
   464         p = this.options.snap(p[0],p[1],this);
       
   465       } else {
       
   466       if(this.options.snap instanceof Array) {
       
   467         p = p.map( function(v, i) {
       
   468           return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
       
   469       } else {
       
   470         p = p.map( function(v) {
       
   471           return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
       
   472       }
       
   473     }}
       
   474     
       
   475     var style = this.element.style;
       
   476     if((!this.options.constraint) || (this.options.constraint=='horizontal'))
       
   477       style.left = p[0] + "px";
       
   478     if((!this.options.constraint) || (this.options.constraint=='vertical'))
       
   479       style.top  = p[1] + "px";
       
   480     
       
   481     if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
       
   482   },
       
   483   
       
   484   stopScrolling: function() {
       
   485     if(this.scrollInterval) {
       
   486       clearInterval(this.scrollInterval);
       
   487       this.scrollInterval = null;
       
   488       Draggables._lastScrollPointer = null;
       
   489     }
       
   490   },
       
   491   
       
   492   startScrolling: function(speed) {
       
   493     if(!(speed[0] || speed[1])) return;
       
   494     this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
       
   495     this.lastScrolled = new Date();
       
   496     this.scrollInterval = setInterval(this.scroll.bind(this), 10);
       
   497   },
       
   498   
       
   499   scroll: function() {
       
   500     var current = new Date();
       
   501     var delta = current - this.lastScrolled;
       
   502     this.lastScrolled = current;
       
   503     if(this.options.scroll == window) {
       
   504       with (this._getWindowScroll(this.options.scroll)) {
       
   505         if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
       
   506           var d = delta / 1000;
       
   507           this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
       
   508         }
       
   509       }
       
   510     } else {
       
   511       this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
       
   512       this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
       
   513     }
       
   514     
       
   515     Position.prepare();
       
   516     Droppables.show(Draggables._lastPointer, this.element);
       
   517     Draggables.notify('onDrag', this);
       
   518     if (this._isScrollChild) {
       
   519       Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
       
   520       Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
       
   521       Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
       
   522       if (Draggables._lastScrollPointer[0] < 0)
       
   523         Draggables._lastScrollPointer[0] = 0;
       
   524       if (Draggables._lastScrollPointer[1] < 0)
       
   525         Draggables._lastScrollPointer[1] = 0;
       
   526       this.draw(Draggables._lastScrollPointer);
       
   527     }
       
   528     
       
   529     if(this.options.change) this.options.change(this);
       
   530   },
       
   531   
       
   532   _getWindowScroll: function(w) {
       
   533     var T, L, W, H;
       
   534     with (w.document) {
       
   535       if (w.document.documentElement && documentElement.scrollTop) {
       
   536         T = documentElement.scrollTop;
       
   537         L = documentElement.scrollLeft;
       
   538       } else if (w.document.body) {
       
   539         T = body.scrollTop;
       
   540         L = body.scrollLeft;
       
   541       }
       
   542       if (w.innerWidth) {
       
   543         W = w.innerWidth;
       
   544         H = w.innerHeight;
       
   545       } else if (w.document.documentElement && documentElement.clientWidth) {
       
   546         W = documentElement.clientWidth;
       
   547         H = documentElement.clientHeight;
       
   548       } else {
       
   549         W = body.offsetWidth;
       
   550         H = body.offsetHeight
       
   551       }
       
   552     }
       
   553     return { top: T, left: L, width: W, height: H };
       
   554   }
       
   555 }
       
   556 
       
   557 /*--------------------------------------------------------------------------*/
       
   558 
       
   559 var SortableObserver = Class.create();
       
   560 SortableObserver.prototype = {
       
   561   initialize: function(element, observer) {
       
   562     this.element   = $(element);
       
   563     this.observer  = observer;
       
   564     this.lastValue = Sortable.serialize(this.element);
       
   565   },
       
   566   
       
   567   onStart: function() {
       
   568     this.lastValue = Sortable.serialize(this.element);
       
   569   },
       
   570   
       
   571   onEnd: function() {
       
   572     Sortable.unmark();
       
   573     if(this.lastValue != Sortable.serialize(this.element))
       
   574       this.observer(this.element)
       
   575   }
       
   576 }
       
   577 
       
   578 var Sortable = {
       
   579   SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
       
   580   
       
   581   sortables: {},
       
   582   
       
   583   _findRootElement: function(element) {
       
   584     while (element.tagName != "BODY") {  
       
   585       if(element.id && Sortable.sortables[element.id]) return element;
       
   586       element = element.parentNode;
       
   587     }
       
   588   },
       
   589 
       
   590   options: function(element) {
       
   591     element = Sortable._findRootElement($(element));
       
   592     if(!element) return;
       
   593     return Sortable.sortables[element.id];
       
   594   },
       
   595   
       
   596   destroy: function(element){
       
   597     var s = Sortable.options(element);
       
   598     
       
   599     if(s) {
       
   600       Draggables.removeObserver(s.element);
       
   601       s.droppables.each(function(d){ Droppables.remove(d) });
       
   602       s.draggables.invoke('destroy');
       
   603       
       
   604       delete Sortable.sortables[s.element.id];
       
   605     }
       
   606   },
       
   607 
       
   608   create: function(element) {
       
   609     element = $(element);
       
   610     var options = Object.extend({ 
       
   611       element:     element,
       
   612       tag:         'li',       // assumes li children, override with tag: 'tagname'
       
   613       dropOnEmpty: false,
       
   614       tree:        false,
       
   615       treeTag:     'ul',
       
   616       overlap:     'vertical', // one of 'vertical', 'horizontal'
       
   617       constraint:  'vertical', // one of 'vertical', 'horizontal', false
       
   618       containment: element,    // also takes array of elements (or id's); or false
       
   619       handle:      false,      // or a CSS class
       
   620       only:        false,
       
   621       delay:       0,
       
   622       hoverclass:  null,
       
   623       ghosting:    false,
       
   624       scroll:      false,
       
   625       scrollSensitivity: 20,
       
   626       scrollSpeed: 15,
       
   627       format:      this.SERIALIZE_RULE,
       
   628       onChange:    Prototype.emptyFunction,
       
   629       onUpdate:    Prototype.emptyFunction
       
   630     }, arguments[1] || {});
       
   631 
       
   632     // clear any old sortable with same element
       
   633     this.destroy(element);
       
   634 
       
   635     // build options for the draggables
       
   636     var options_for_draggable = {
       
   637       revert:      true,
       
   638       scroll:      options.scroll,
       
   639       scrollSpeed: options.scrollSpeed,
       
   640       scrollSensitivity: options.scrollSensitivity,
       
   641       delay:       options.delay,
       
   642       ghosting:    options.ghosting,
       
   643       constraint:  options.constraint,
       
   644       handle:      options.handle };
       
   645 
       
   646     if(options.starteffect)
       
   647       options_for_draggable.starteffect = options.starteffect;
       
   648 
       
   649     if(options.reverteffect)
       
   650       options_for_draggable.reverteffect = options.reverteffect;
       
   651     else
       
   652       if(options.ghosting) options_for_draggable.reverteffect = function(element) {
       
   653         element.style.top  = 0;
       
   654         element.style.left = 0;
       
   655       };
       
   656 
       
   657     if(options.endeffect)
       
   658       options_for_draggable.endeffect = options.endeffect;
       
   659 
       
   660     if(options.zindex)
       
   661       options_for_draggable.zindex = options.zindex;
       
   662 
       
   663     // build options for the droppables  
       
   664     var options_for_droppable = {
       
   665       overlap:     options.overlap,
       
   666       containment: options.containment,
       
   667       tree:        options.tree,
       
   668       hoverclass:  options.hoverclass,
       
   669       onHover:     Sortable.onHover
       
   670       //greedy:      !options.dropOnEmpty
       
   671     }
       
   672     
       
   673     var options_for_tree = {
       
   674       onHover:      Sortable.onEmptyHover,
       
   675       overlap:      options.overlap,
       
   676       containment:  options.containment,
       
   677       hoverclass:   options.hoverclass
       
   678     }
       
   679 
       
   680     // fix for gecko engine
       
   681     Element.cleanWhitespace(element); 
       
   682 
       
   683     options.draggables = [];
       
   684     options.droppables = [];
       
   685 
       
   686     // drop on empty handling
       
   687     if(options.dropOnEmpty || options.tree) {
       
   688       Droppables.add(element, options_for_tree);
       
   689       options.droppables.push(element);
       
   690     }
       
   691 
       
   692     (this.findElements(element, options) || []).each( function(e) {
       
   693       // handles are per-draggable
       
   694       var handle = options.handle ? 
       
   695         Element.childrenWithClassName(e, options.handle)[0] : e;    
       
   696       options.draggables.push(
       
   697         new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
       
   698       Droppables.add(e, options_for_droppable);
       
   699       if(options.tree) e.treeNode = element;
       
   700       options.droppables.push(e);      
       
   701     });
       
   702     
       
   703     if(options.tree) {
       
   704       (Sortable.findTreeElements(element, options) || []).each( function(e) {
       
   705         Droppables.add(e, options_for_tree);
       
   706         e.treeNode = element;
       
   707         options.droppables.push(e);
       
   708       });
       
   709     }
       
   710 
       
   711     // keep reference
       
   712     this.sortables[element.id] = options;
       
   713 
       
   714     // for onupdate
       
   715     Draggables.addObserver(new SortableObserver(element, options.onUpdate));
       
   716 
       
   717   },
       
   718 
       
   719   // return all suitable-for-sortable elements in a guaranteed order
       
   720   findElements: function(element, options) {
       
   721     return Element.findChildren(
       
   722       element, options.only, options.tree ? true : false, options.tag);
       
   723   },
       
   724   
       
   725   findTreeElements: function(element, options) {
       
   726     return Element.findChildren(
       
   727       element, options.only, options.tree ? true : false, options.treeTag);
       
   728   },
       
   729 
       
   730   onHover: function(element, dropon, overlap) {
       
   731     if(Element.isParent(dropon, element)) return;
       
   732 
       
   733     if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
       
   734       return;
       
   735     } else if(overlap>0.5) {
       
   736       Sortable.mark(dropon, 'before');
       
   737       if(dropon.previousSibling != element) {
       
   738         var oldParentNode = element.parentNode;
       
   739         element.style.visibility = "hidden"; // fix gecko rendering
       
   740         dropon.parentNode.insertBefore(element, dropon);
       
   741         if(dropon.parentNode!=oldParentNode) 
       
   742           Sortable.options(oldParentNode).onChange(element);
       
   743         Sortable.options(dropon.parentNode).onChange(element);
       
   744       }
       
   745     } else {
       
   746       Sortable.mark(dropon, 'after');
       
   747       var nextElement = dropon.nextSibling || null;
       
   748       if(nextElement != element) {
       
   749         var oldParentNode = element.parentNode;
       
   750         element.style.visibility = "hidden"; // fix gecko rendering
       
   751         dropon.parentNode.insertBefore(element, nextElement);
       
   752         if(dropon.parentNode!=oldParentNode) 
       
   753           Sortable.options(oldParentNode).onChange(element);
       
   754         Sortable.options(dropon.parentNode).onChange(element);
       
   755       }
       
   756     }
       
   757   },
       
   758   
       
   759   onEmptyHover: function(element, dropon, overlap) {
       
   760     var oldParentNode = element.parentNode;
       
   761     var droponOptions = Sortable.options(dropon);
       
   762         
       
   763     if(!Element.isParent(dropon, element)) {
       
   764       var index;
       
   765       
       
   766       var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
       
   767       var child = null;
       
   768             
       
   769       if(children) {
       
   770         var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
       
   771         
       
   772         for (index = 0; index < children.length; index += 1) {
       
   773           if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
       
   774             offset -= Element.offsetSize (children[index], droponOptions.overlap);
       
   775           } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
       
   776             child = index + 1 < children.length ? children[index + 1] : null;
       
   777             break;
       
   778           } else {
       
   779             child = children[index];
       
   780             break;
       
   781           }
       
   782         }
       
   783       }
       
   784       
       
   785       dropon.insertBefore(element, child);
       
   786       
       
   787       Sortable.options(oldParentNode).onChange(element);
       
   788       droponOptions.onChange(element);
       
   789     }
       
   790   },
       
   791 
       
   792   unmark: function() {
       
   793     if(Sortable._marker) Element.hide(Sortable._marker);
       
   794   },
       
   795 
       
   796   mark: function(dropon, position) {
       
   797     // mark on ghosting only
       
   798     var sortable = Sortable.options(dropon.parentNode);
       
   799     if(sortable && !sortable.ghosting) return; 
       
   800 
       
   801     if(!Sortable._marker) {
       
   802       Sortable._marker = $('dropmarker') || document.createElement('DIV');
       
   803       Element.hide(Sortable._marker);
       
   804       Element.addClassName(Sortable._marker, 'dropmarker');
       
   805       Sortable._marker.style.position = 'absolute';
       
   806       document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
       
   807     }    
       
   808     var offsets = Position.cumulativeOffset(dropon);
       
   809     Sortable._marker.style.left = offsets[0] + 'px';
       
   810     Sortable._marker.style.top = offsets[1] + 'px';
       
   811     
       
   812     if(position=='after')
       
   813       if(sortable.overlap == 'horizontal') 
       
   814         Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
       
   815       else
       
   816         Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
       
   817     
       
   818     Element.show(Sortable._marker);
       
   819   },
       
   820   
       
   821   _tree: function(element, options, parent) {
       
   822     var children = Sortable.findElements(element, options) || [];
       
   823   
       
   824     for (var i = 0; i < children.length; ++i) {
       
   825       var match = children[i].id.match(options.format);
       
   826 
       
   827       if (!match) continue;
       
   828       
       
   829       var child = {
       
   830         id: encodeURIComponent(match ? match[1] : null),
       
   831         element: element,
       
   832         parent: parent,
       
   833         children: new Array,
       
   834         position: parent.children.length,
       
   835         container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
       
   836       }
       
   837       
       
   838       /* Get the element containing the children and recurse over it */
       
   839       if (child.container)
       
   840         this._tree(child.container, options, child)
       
   841       
       
   842       parent.children.push (child);
       
   843     }
       
   844 
       
   845     return parent; 
       
   846   },
       
   847 
       
   848   /* Finds the first element of the given tag type within a parent element.
       
   849     Used for finding the first LI[ST] within a L[IST]I[TEM].*/
       
   850   _findChildrenElement: function (element, containerTag) {
       
   851     if (element && element.hasChildNodes)
       
   852       for (var i = 0; i < element.childNodes.length; ++i)
       
   853         if (element.childNodes[i].tagName == containerTag)
       
   854           return element.childNodes[i];
       
   855   
       
   856     return null;
       
   857   },
       
   858 
       
   859   tree: function(element) {
       
   860     element = $(element);
       
   861     var sortableOptions = this.options(element);
       
   862     var options = Object.extend({
       
   863       tag: sortableOptions.tag,
       
   864       treeTag: sortableOptions.treeTag,
       
   865       only: sortableOptions.only,
       
   866       name: element.id,
       
   867       format: sortableOptions.format
       
   868     }, arguments[1] || {});
       
   869     
       
   870     var root = {
       
   871       id: null,
       
   872       parent: null,
       
   873       children: new Array,
       
   874       container: element,
       
   875       position: 0
       
   876     }
       
   877     
       
   878     return Sortable._tree (element, options, root);
       
   879   },
       
   880 
       
   881   /* Construct a [i] index for a particular node */
       
   882   _constructIndex: function(node) {
       
   883     var index = '';
       
   884     do {
       
   885       if (node.id) index = '[' + node.position + ']' + index;
       
   886     } while ((node = node.parent) != null);
       
   887     return index;
       
   888   },
       
   889 
       
   890   sequence: function(element) {
       
   891     element = $(element);
       
   892     var options = Object.extend(this.options(element), arguments[1] || {});
       
   893     
       
   894     return $(this.findElements(element, options) || []).map( function(item) {
       
   895       return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
       
   896     });
       
   897   },
       
   898 
       
   899   setSequence: function(element, new_sequence) {
       
   900     element = $(element);
       
   901     var options = Object.extend(this.options(element), arguments[2] || {});
       
   902     
       
   903     var nodeMap = {};
       
   904     this.findElements(element, options).each( function(n) {
       
   905         if (n.id.match(options.format))
       
   906             nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
       
   907         n.parentNode.removeChild(n);
       
   908     });
       
   909    
       
   910     new_sequence.each(function(ident) {
       
   911       var n = nodeMap[ident];
       
   912       if (n) {
       
   913         n[1].appendChild(n[0]);
       
   914         delete nodeMap[ident];
       
   915       }
       
   916     });
       
   917   },
       
   918   
       
   919   serialize: function(element) {
       
   920     element = $(element);
       
   921     var options = Object.extend(Sortable.options(element), arguments[1] || {});
       
   922     var name = encodeURIComponent(
       
   923       (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
       
   924     
       
   925     if (options.tree) {
       
   926       return Sortable.tree(element, arguments[1]).children.map( function (item) {
       
   927         return [name + Sortable._constructIndex(item) + "[id]=" + 
       
   928                 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
       
   929       }).flatten().join('&');
       
   930     } else {
       
   931       return Sortable.sequence(element, arguments[1]).map( function(item) {
       
   932         return name + "[]=" + encodeURIComponent(item);
       
   933       }).join('&');
       
   934     }
       
   935   }
       
   936 }
       
   937 
       
   938 /* Returns true if child is contained within element */
       
   939 Element.isParent = function(child, element) {
       
   940   if (!child.parentNode || child == element) return false;
       
   941 
       
   942   if (child.parentNode == element) return true;
       
   943 
       
   944   return Element.isParent(child.parentNode, element);
       
   945 }
       
   946 
       
   947 Element.findChildren = function(element, only, recursive, tagName) {    
       
   948   if(!element.hasChildNodes()) return null;
       
   949   tagName = tagName.toUpperCase();
       
   950   if(only) only = [only].flatten();
       
   951   var elements = [];
       
   952   $A(element.childNodes).each( function(e) {
       
   953     if(e.tagName && e.tagName.toUpperCase()==tagName &&
       
   954       (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
       
   955         elements.push(e);
       
   956     if(recursive) {
       
   957       var grandchildren = Element.findChildren(e, only, recursive, tagName);
       
   958       if(grandchildren) elements.push(grandchildren);
       
   959     }
       
   960   });
       
   961 
       
   962   return (elements.length>0 ? elements.flatten() : []);
       
   963 }
       
   964 
       
   965 Element.offsetSize = function (element, type) {
       
   966   if (type == 'vertical' || type == 'height')
       
   967     return element.offsetHeight;
       
   968   else
       
   969     return element.offsetWidth;
       
   970 }