// script.aculo.us dragdrop.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// 
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(typeof Effect == 'undefined')
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],							// array of { doc:document, droppables:[] }
  allListeners: [],			// array of { doc:document, listeners:[] }
  scrollables: [],				// array of { doc:document, scrollables:[] }
  scrollSensitivity: 20,
  scrollSpeed: 15,
	useSingleList: true,
	
  remove: function(dropElement) {
  	var iDrop = this._indexOfDropByElement(dropElement);	// don't create drop
  	if (iDrop >= 0)
  	{
  		var drop = this.drops[iDrop];
    	drop.droppables = drop.droppables.reject(function(d) { return d.element==$(dropElement) });
    	if (!drop.droppables.length)
    	{
    		this._removeDrop(iDrop, true);	// compact also
    	}
    }
  },

  add: function(dropElement) {
    dropElement = $(dropElement);
//debug("Droppables.add: added "+dropElement.id);
    var droppable = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false,

      dropstateclasses: null
    }, arguments[1] || {});
    droppable.posOptions = Position.posOptions(droppable);
    droppable.cumulative = Position.cumulativeOffset(dropElement, droppable.posOptions);

    // cache containers
    if(droppable.containment) {
      droppable._containers = [];
      var containment = droppable.containment;
      if((typeof containment == 'object') && 
        (containment.constructor == Array)) {
        containment.each( function(c) { droppable._containers.push($(c)) });
      } else {
        droppable._containers.push($(containment));
      }
    }
    
    if(droppable.accept)
    {
			droppable.accept = [droppable.accept].flatten();
		}

    Element.makePositioned(dropElement); // fix IE
    droppable.element = dropElement;

		var iDrop = this._indexOfDropByElement(dropElement, true);	// create drop if needed
		this.drops[iDrop].droppables.push(droppable);
  },
  
  addScrollable: function(scrollable)
  {
  	this.scrollables.push(scrollable);
  },
  
  removeScrollable: function(scrollable)
  {
  	this.scrollables = this.allScrollables.without(scrollable);
  },
  
  findDeepestChild: function(drops) {
    deepest = drops[0];
      
    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];
    
    return deepest;
  },

  isContained: function(dragElement, droppable) {
    var containmentNode;
    if(droppable.tree) {
      containmentNode = dragElement.treeNode; 
    } else {
      containmentNode = dragElement.parentNode;
    }
    var contained = droppable._containers.detect(function(c) { return containmentNode == c });
//debug("isContained: contained="+contained);
    return contained;
  },
  
  isAffected: function(point, dragElement, droppable) {
  	var posOptions = droppable.posOptions;
//debug("isAffected: (droppable.element!=dragElement)="+(droppable.element!=dragElement));
//debug("isAffected: (!droppable._containers)="+(!droppable._containers));
//if (droppable._containers) debug("isAffected: this.isContained(dragElement, droppable)="+this.isContained(dragElement, droppable));
//debug("isAffected: (!droppable.accept)="+(!droppable.accept));
//if (droppable.accept) debug("isAffected: (Element.classNames(dragElement).detect(function(v) { return droppable.accept.include(v) } ) )="+(Element.classNames(dragElement).detect(function(v) { return droppable.accept.include(v) } ) ));
		var affected = ((droppable.element!=dragElement) &&
      ((!droppable._containers) ||
        this.isContained(dragElement, droppable)) &&
      (this.isAcceptable(dragElement, droppable, posOptions)) &&
      (this.within(droppable.element, point[0], point[1], posOptions) ) );
//if (affected) debug("isAffected: "+true);
    return affected;
  },

	isAcceptable: function(dragElement, droppable) {
  	var posOptions = droppable.posOptions;
  	if (!droppable.accept) return true;
  	var names = Element.classNames(dragElement);
if (droppable.accept[0] == 'drag2')
{
	var x = 1;
}
  	var acceptable = names.detect(function(v) { return droppable.accept.include(v) } ) || false;
if (acceptable)
{
	acceptable = acceptable;
}
  	return acceptable;
	},
	
  deactivate: function(droppable) {
    if(droppable.dropstateclasses)
    	Element.removeClassName(droppable.element, droppable.dropstateclasses[Math.abs(this.last_dropstate)]);
    else if(droppable.hoverclass)
      Element.removeClassName(droppable.element, droppable.hoverclass);
    this.last_active = undefined;
    this.last_dropstate = 0;
  },

  activate: function(droppable,dropState) {
    if(droppable.dropstateclasses)
      Element.addClassName(droppable.element, droppable.dropstateclasses[Math.abs(dropState)]);
    else if(droppable.hoverclass)
      Element.addClassName(droppable.element, droppable.hoverclass);
    this.last_active = droppable;
    this.last_dropstate = dropState;
  },

  show: function(event, point, dragElement) {
    if(!this.drops.length) return;
    var eventElement = ((event) ? Event.element(event) : null);
//debug("event="+((eventElement)?eventElement.id+"/"+eventElement.ownerDocument.title:"null"));
    var doc = ((eventElement) ? eventElement.ownerDocument : dragElement.ownerDocument);
    var iDrop = this._indexOfDropByDocument(doc);
    if (iDrop < 0) return;
    var affected = [];
  	var posOptions = Position.posOptions(arguments[2]);

    this.drops[iDrop].droppables.each( function(droppable) {
      if(Droppables.isAffected(point, dragElement, droppable))
        affected.push(droppable);
    });
        
    if(affected.length>0) {
      droppable = Droppables.findDeepestChild(affected);
      this.within(droppable.element, point[0], point[1], posOptions);
      if(droppable.onHover)
        droppable.onHover(dragElement, droppable.element, Position.overlap(droppable.overlap, droppable.element, posOptions));
      
	    if((this.last_active) && (this.last_active != droppable)) this.deactivate(this.last_active);
      if (droppable.getDropState)
      {
      	var state = droppable.getDropState(dragElement, droppable);
		    Droppables.activate(droppable, state);
      }
      else
      {
 	     Droppables.activate(droppable);
      }
     } else if(this.last_active) {
     		Droppables.deactivate(droppable);
     }
    
  },

  fire: function(event, dragElement) {
//debug("this.last_active="+this.last_active+", this.last_dropstate="+this.last_dropstate);
//debug("this.last_active="+this.last_active+" "+this.last_active.element.id+" "+this.last_active.last_within);
    if(!this.last_active) return;
		if((this.last_active.dropstateclasses) && (this.last_dropstate < 0)) return;
  	//var posOptions = this.last_active.posOptions;
//debug("preparing");
    //Position.prepare(posOptions);

		//var pos = Event.pointer(event, posOptions);
//debug("pos="+Object.dump(pos)+", last_active="+this.last_active);
		//var affected = this.isAffected(pos, dragElement, this.last_active);
//debug("affected="+affected);
    if (true) //(affected)
      if (this.last_active.onDrop) {
        this.last_active.onDrop(dragElement, this.last_active.element, event); 
        return true; 
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  },
  
  within: function(dropElement, x, y) {
    var posOptions = Position.posOptions(arguments[3]);
    //return Position.withinIncludingScrolloffsets(dropElement, x, y, posOptions);

    var offsetcache = Position.realOffset(dropElement, posOptions);
    
    var doc = dropElement.ownerDocument;
    var win = doc.defaultView || doc.parentWindow;
   	var winscroll = Position.windowScroll(((posOptions.outerWindow) ? posOptions.outerWindow : window));		// only want the top-most one (no chaining)

    this.xcomp = x + offsetcache[0] - winscroll[0];
    this.ycomp = y + offsetcache[1] - winscroll[1];
    dropElement.offset = dropElement.offset || Position.cumulativeOffset(dropElement, posOptions);
    var offset = dropElement.offset;
    var isWithin = (this.ycomp >= offset[1] &&
            this.ycomp <  offset[1] + dropElement.offsetHeight &&
            this.xcomp >= offset[0] &&
            this.xcomp <  offset[0] + dropElement.offsetWidth);
//debug("["+dropElement.id+"] within="+isWithin+", mouse=("+x+","+y+"), offset=("+offsetcache[0]+","+offsetcache[1]+"), delta=("+Position.deltaX+","+Position.deltaY+"), box=("+this.offset[0]+","+this.offset[1]+","+(this.offset[0]+dropElement.offsetWidth)+","+(this.offset[1]+dropElement.offsetHeight)+")");
if (isWithin)
{
	isWithin = isWithin;
}
    return isWithin;
  },
  
  cleanup: function()
  {
  	for (var i=0; i<this.drops.length; i++)
  	{
  		if (this.drops[i])
  		{
		    var win = doc.defaultView || doc.parentWindow || null;
		    if (! win)
		    {
		    	this._removeDrop(i);
		    }
  		}
  	}
  	this.drops = this.drops.compact();
  },
  
  _indexOfDropByDroppable: function(droppable)
  {
  	return this._indexOfDropByDocument(droppable.element);
  },
  
  _indexOfDropByElement: function(dropElement, makeNew)
  {
  	makeNew = makeNew || false;
  	var doc = dropElement.ownerDocument;
  	return this._indexOfDropByDocument(doc, makeNew);
  },
  
  _indexOfDropByDocument: function(doc, makeNew)
  {
  	makeNew = makeNew || false;

		if (this.useSingleList)
		{
			if ((this.drops.length) && (this.drops[0] != null))
			{
				this._addDropListeners(this.drops[0], doc);
				return 0;
			}
			if (makeNew)
			{
	  		this._addDrop(0, doc);
				return 0;
			}
			return -1;
		}
		
  	var drop = null;
  	var firstNull = -1;
  	for (var i=0; i<this.drops.length; i++)
  	{
  		if (!this.drops[i])
  		{
  			firstNull = ((firstNull == -1) ? i : firstNull); 
  		}
  		else if (this.drops[i].doc == doc)
  		{
  			return i;
  		}
  	}
  	
  	if (makeNew)
  	{
  		firstNull = ((firstNull == -1) ? this.drops.length : firstNull);
  		this._addDrop(firstNull, doc);
  		return firstNull;
  	}
  	
  	return -1;
  },
  
  _addDrop: function(iDrop, doc)
  {
  	var drop = { doc:doc, dropListeners:[], droppables:[] };
  	this.drops[iDrop] = drop;
  },
  
  _addDropListeners: function(drop, doc)
  {
  	// loop to install listeners all the way to the top
  	var addAll = (!drop.dropListeners.length);
  	while (doc)
  	{
  		var iListener = this._indexOfListenersByDocument(doc, true);
  		var add = true;
  		if (!addAll)
  		{
  			for (var i=0; i<drop.dropListeners.length; i++)
  			{
  				if (drop.dropListeners[i].doc == doc)
  				{
  					add = false;
  					break;
  				}
  			}
  		}
  		if (add)
  		{
  			drop.dropListeners.push(this.allListeners[iListener]);
  		}
    	var win = doc.defaultView || doc.parentWindow;
    	doc = (((win) && (win != top)) ? win.parent.document : null);
    	if (doc != null)
    	{
    		doc = doc;
    	}
  	}
  },
  
  _removeDrop: function(iDrop, compact)
  {
  	compact = compact || false;
  	
  	var drop = this.drops[i];
  	drop.doc = null;
  	drop.dropListeners = null;	// we'll catch this on cleanup
  	drop.droppables = null;
  	this.drops[i] = null;
  	if (compact)
  	{
  		this.drops.compact();
  		this._cleanListeners();
  	}
  },
  
  _indexOfListenersByDocument: function(doc, makeNew)
  {
  	var len = this.allListeners.length;
  	for (var i=0; i<len; i++)
  	{
  		if (this.allListeners[i].doc == doc)
  		{
  			return i;
  		}
  	}
  	if (makeNew)
  	{
  		this._installListeners(doc);
  		return len;
  	}
  	return -1;
  },
  
  _installListeners: function(doc)
  {
  	var listeners = {
  		doc: doc,
			eventMouseMove: this._eventMouseMove.bindAsEventListener(this, doc)
		};
  	this.allListeners.push(listeners);
	  Event.observe(doc, "mousemove", listeners.eventMouseMove);
	  return listeners;
  },
  
  _removeListeners: function(iListener)
  {
		Event.stopObserving(doc, "mousemove", this.allListeners[iListener].listeners.eventMouseMove);
		this.allListeners[iListener] = null;
  },
  
  _cleanListeners: function()
  {
  	var keep = [];		// array of this.listeners to keep (others will be undefined)
  	for (var iDrop=0; iDrop<this.drops.length; iDrop++)
  	{
  		var drop = this.drops[iDrop];
  		for (var i=0; i<drop.listeners; i++)
  		{
  			var listener = drop.listeners[i];
  			var ix = this._indexOfListenersByDocument(listener.doc);
  			if (ix >= 0)
  			{
  				keep[ix] = listener;
  			}
  		}
  	}
  	for (var i=0; i<this.listeners.length; i++)
  	{
  		if (! this.listeners[i])
  		{
  			this._removeListeners(i);
  			this.listeners[i] = null;
  		} 
  	}
  	this.listeners.compact();
  },
  
  _eventMouseMove: function(event, doc)
  {
  	if (!Draggables.activeDraggable) return;
  	Draggables.updateDrag(event);
  }
}

var Draggables = {
  drags: [],
  observers: [],
  docs: [],
  listeners: [],
  activeDraggable: null,
  
  register: function(draggable) {
    var doc = draggable.element.ownerDocument;
 //debug("register: draggable.element.ownerDocument="+draggable.element.ownerDocument+" "+draggable.element.ownerDocument.title);
    draggable.document = doc;
    if (! doc._registeredDragables)
    {
    	doc._registeredDragables = true;
    	this.docs.push(doc);
      
      var listener = {
		      eventMouseUp: this.endDrag.bindAsEventListener(this),
		      eventMouseMove: this.updateDrag.bindAsEventListener(this),
		      eventKeypress: this.keyPress.bindAsEventListener(this)
				};
      this.listeners.push(listener);
      Event.observe(doc, "mouseup", listener.eventMouseUp);
      Event.observe(doc, "mousemove", listener.eventMouseMove);
      Event.observe(doc, "keypress", listener.eventKeypress);
    }
    this.drags.push(draggable);
  },
  
  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
    	for (var i=0; i<this.docs.length; i++)
    	{
    		var doc = this.docs[i];
    		doc._registeredDragables = unused;
    		
    		var listener = this.listeners[i];
				Event.stopObserving(doc, "mouseup", listener.eventMouseUp);
				Event.stopObserving(doc, "mousemove", listener.eventMouseMove);
				Event.stopObserving(doc, "keypress", listener.eventKeypress);
	    }
	    docs = [];
    }
  },
  
  activate: function(draggable) {
    if(draggable.options.delay) { 
      this._timeout = setTimeout(function() { 
        Draggables._timeout = null; 
        window.focus(); 
        Draggables.activeDraggable = draggable; 
      }.bind(this), draggable.options.delay); 
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },
  
  deactivate: function() {
    this.activeDraggable = null;
  },
  
  updateDrag: function(event) {
//debug("updateDrag: this.activeDraggable="+this.activeDraggable);
    if(!this.activeDraggable) return;
 //debug("updateDrag: doc="+doc+" "+doc.title);
    var active = this.activeDraggable;
 		var posOptions = active.options.posOptions;
var e = Event.element(event);
//debug("Draggables.updateDrag: e="+e.tagName+"/"+e.id);
    var pointer = Event.pointer(event, posOptions);
//debug("Draggables.updateDrag: pointer=("+pointer[0]+","+pointer[1]+"), doc="+active.options.outerWindow.document.title);
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;
    
    this.activeDraggable.updateDrag(event, pointer);
  },
  
  endDrag: function(event) {
    if(this._timeout) { 
      clearTimeout(this._timeout); 
      this._timeout = null; 
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },
  
  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },
  
  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },
  
  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },
  
  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },
  
  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
}

/*--------------------------------------------------------------------------*/

var Draggable = Class.create();
Draggable._dragging    = {};

Draggable.prototype = {
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){ 
            Draggable._dragging[element] = false 
          }
        }); 
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };
    
    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
        }
      });
    
    this.options = Object.extend(defaults, arguments[1] || {});
    this.options.posOptions = Position.posOptions(this.options);

    this.element = $(element);
    
    if(this.options.handle && (typeof this.options.handle == 'string'))
      this.handle = this.element.down('.'+this.options.handle, 0);
    
    if(!this.handle) this.handle = $(this.options.handle);
    if(!this.handle) this.handle = this.element;
    
    if(this.options.scroll && !this.options.scroll.scrollTo && !this.options.scroll.outerHTML) {
      this.options.scroll = $(this.options.scroll);
      this._isScrollChild = Element.childOf(this.element, this.options.scroll);
    }

		if (this.options.bounds)
		{
			var bounds = this.options.bounds;
			if (typeof bounds == 'number')
			{
				this.options.bounds = [ 0, 0, bounds, bounds ];
			}
			else if (Object.isArray(bounds))
			{
				switch (bounds.length)
				{
					case 0:
					{
						this.options.bounds = undefined;
						break;
					}
					case 1:
					{
						this.options.bounds = [ 0, 0, bounds, bounds ];
						break;
					}
					case 2:
					case 3:
					{
						this.options.bounds = [ 0, 0, bounds[0], bounds[1] ];
						break;
					}
				}
			}
		}
		
    Element.makePositioned(this.element); // fix IE    

    this.delta    = this.currentDelta();
    this.cumulative = Position.cumulativeOffset(this.element, this.options.posOptions);
    this.dragging = false;   

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
    
    Draggables.register(this);
  },
  
  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },
  
  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },
  
  initDrag: function(event) {
var doc = Event.element(event).ownerDocument;
//debug("initDrag: doc="+doc.title+", pageXY=("+(event.pageX)+","+(event.pageY)+"), clientXY=("+(event.clientX)+","+(event.clientY)+"), docScrollLT=("+(doc.documentElement.scrollLeft)+","+(doc.documentElement.scrollTop)+"), bodyScrollLT=("+(doc.body.scrollLeft)+","+(doc.body.scrollTop)+")");
//debug("initDrag: event="+Object.dump(event));
    if(typeof Draggable._dragging[this.element] != 'undefined' &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;
        
			var posOptions = this.options.posOptions;
//posOptions.debug = true;
      var pointer = Event.pointer(event, posOptions);	
      var cumulative = this.cumulative;
      var real = Position.realOffset(this.element, posOptions);
   		var winscroll = Position.windowScroll(Position.outerWindow(posOptions));		// only want the top-most one (no chaining)
      var scroll = [ real[0]-winscroll[0], real[1]-winscroll[1] ];
      this.offset = [0,1].map( function(i) { return (pointer[i] - cumulative[i] + scroll[i]) });
      
      Draggables.activate(this);
      Event.stop(event);
//top.doFloaterUpdate(true,0,0);
    }
  },
  
  startDrag: function(event) {
    this.dragging = true;
    
		var posOptions = this.options.posOptions;

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }
    
		this._predragDocument = this.element.ownerDocument;
		this._predragParent = this.element.parentNode;
		this._predragNextSibling = this.element.nextSibling;
		this._predragPosition = this.element.style.position;
		this._predragLeft = parseFloat(this.element.style.left || 0);
		this._predragTop = parseFloat(this.element.style.top || 0);
//top.edump("start-1",this.element);
    
    if((this.options.ghosting) || (this.options.outerWindow != null))
    {
//debug("this.options.outerWindow="+this.options.outerWindow);
			var doc = ((this.options.outerWindow != null) ? this.options.outerWindow.document : document);
//debug("doc="+((doc)?doc:"null"));
			if (this.element.ownerDocument == doc)
			{
//top.console.log("same document");
				// everybody seems able to do this
		    if(this.options.ghosting) {
		      this._clone = this.element.cloneNode(true);
		      this.element.parentNode.insertBefore(this._clone, this.element);
		    }
				Element.remove(this.element);
			}
			else if (doc.adoptElement)
			{
//top.console.log("has adoptElement");
				// DOM-3, allows cross-frame move
		    if(this.options.ghosting) {
		      this._clone = this.element.cloneNode(true);
		      this.element.parentNode.insertBefore(this._clone, this.element);
		    }
				this.element = Object.extend($(doc.adoptElement(this.element)), this.element);
			}
			else if (Prototype.Browser.IE)
			{
//top.console.log("is IE, doc="+doc.title);
				// IE doesn't allow cross-frame moves, so do it by hand, this.elements becomes the _clone & is copied to target frame
				var e = Draggable._importNode(doc, this.element, true);
//debug("extended="+e);
		    this._clone = this.element;
		    if(! this.options.ghosting) {
		    	// just hide it (it'll make it easier later)
		    	this.element.hide();
//debug("hidden");
		    }
		    this.element = e;
			}
			else
			{
//debug("default");
				// non-ie seems to allow this
		    if(this.options.ghosting) {
		      this._clone = this.element.cloneNode(true);
		      this.element.parentNode.insertBefore(this._clone, this.element);
		    }
				Element.remove(this.element);
			}
			doc.body.appendChild(this.element);

			this.element.style.position = "absolute";
			this.element.style.left = this.cumulative[0] + "px";
			this.element.style.top = this.cumulative[1] + "px";
    }
//top.edump("start-2",this.element);
    
    this.scrollables = undefined;
    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }
    else if (Droppables.scrollables.length)
    {
    	this.scrollables = [];
    	for (var i=0; i<Droppables.scrollables.length; i++)
    	{
    		var e = Droppables.scrollables[i];
    		if (e.frames)	// window
    		{
	       	var where = this._getWindowScroll(e);
    			this.scrollables.push({ isWindow:true, scrollable:e, originalScrollLeft:where.left, originalScrollTop:where.top });
    		}
    		else
    		{
    			this.scrollables.push({ isWindow:false, scrollable:e, originalScrollLeft:e.scrollLeft, originalScrollTop:e.scrollTop });
    		}
    	}
    }
    
    Draggables.notify('onStart', this, event);
        
    if(this.options.starteffect) this.options.starteffect(this.element);
  },
  
  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);
    
			var posOptions = this.options.posOptions;

    if(!this.options.quiet){
      Position.prepare(this.element.ownerDocument, posOptions);
      Droppables.show(event, pointer, this.element, posOptions);
    }
    
    Draggables.notify('onDrag', this, event);
    
    this.draw(pointer);
    if(this.options.change) this.options.change(this);
    
    var hasScrollables = (this.scrollables);
    var isScrolling = ((this.options.scroll) || (hasScrollables));
    if(isScrolling) {
      this.stopScrolling();
      
      var p;
      if (isScrolling)
      {
      	if (this.options.scroll)
      	{
		      if (this.options.scroll == window) {
		        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
		      } else {
		        p = Position.page(this.options.scroll);
		        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
		        p[1] += this.options.scroll.scrollTop + Position.deltaY;
		        p.push(p[0]+this.options.scroll.offsetWidth);
		        p.push(p[1]+this.options.scroll.offsetHeight);
		      }
		     }
		     else
		     {
	      	 	var scrollable;
	      	 	var scrollables = this._toScrollables();
	      	 	for (var i=0; i<scrollables.length; i++)
	      	 	{
	      	 		scrollable = scrollables[i];
	      	 		if (scrollable.isWindow)
	      	 		{
			       		with(this._getWindowScroll(scrollable.scrollable)) { p = [ left, top, left+width, top+height ]; }
	      	 		}
	      	 		else
	      	 		{
				        p = Position.page(scrollable, posOptions);
								var winscroll = Position.windowScroll(Position.outerWindow(posOptions));		// only want the top-most one (no chaining)
				        p[0] += scrollable.scrollLeft - winscroll[0];
				        p[1] += scrollable.scrollTop - winscroll[1];
				        p.push(p[0]+scrollable.offsetWidth);
				        p.push(p[1]+scrollable.offsetHeight);
	      	 		}
	      	 	}
	      	}
      	 var sensitivity = ((hasScrollables) ? Draggables.scrollSensitivity : this.options.scrollSensitivity); 
	      var speed = [0,0];
	      if(pointer[0] < (p[0]+sensitivity)) speed[0] = pointer[0]-(p[0]+sensitivity);
	      if(pointer[1] < (p[1]+sensitivity)) speed[1] = pointer[1]-(p[1]+sensitivity);
	      if(pointer[0] > (p[2]-sensitivity)) speed[0] = pointer[0]-(p[2]-sensitivity);
	      if(pointer[1] > (p[3]-sensitivity)) speed[1] = pointer[1]-(p[3]-sensitivity);
	      this.startScrolling(speed);
      	}
      }
    
    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);
    
     Event.stop(event);
  },
  
  finishDrag: function(event, success) {
    this.dragging = false;

		var posOptions = this.options.posOptions;
    
    if(this.options.quiet){
      Position.prepare(this._predragDocument, posOptions);
      var pointer = Event.pointer(event, posOptions);
      Droppables.show(event, pointer, this.element, posOptions);
    }

//top.edump("stop-1",this.element);
    if((this.options.ghosting) || (this.options.outerWindow != null))
    {
    	var alreadyInserted = false;
			var doc = doc = this._predragParent.ownerDocument;
			if (this.element.ownerDocument == doc)
			{
				// just remove it (we'll insert & take care of the _clone later)
				Element.remove(this.element);
			}
			else if (doc.adoptElement)
			{
				// adopt it back (we'll insert & take care of the _clone later)
				this.element = Object.extend($(doc.adoptElement(this.element)), this.element);
			}
			else if (Prototype.Browser.IE)
			{
				// IE doesn't allow cross-frame moves (we always have a clone, so swap them)
				var e = this.element;
	      this.element = this._clone;
	      this._clone = e;
		    Element.remove(this._clone);
				this._clone = null;
	      if (! this.options.ghosting) {
	      	this.element.show();
	      }
	      alreadyInserted = true;
			}
			else
			{
				// just remove it (we'll insert & take care of the _clone later)
				Element.remove(this.element);
			}
			
			// take care of inserting it back in
			if (! alreadyInserted)
			{
	      if (this.options.outerWindow == null)		// so _must_ be using _clone
	      {
	      	this._clone.parentNode.insertBefore(this.element, this._clone);
	      }
	      else if (this._predragNextSibling != null)
				{
					this._predragParent.insertBefore(this.element, this._predragNextSibling);
				}
				else
				{
					this._predragParent.insertBefore(this.element);
				}
			}

//debug("_predragLeft="+this._predragLeft+", _predragTop="+this._predragTop+", _predragPosition="+this._predragPosition);
			this.element.style.left = this._predragLeft + "px";
			this.element.style.top = this._predragTop + "px";
			this.element.style.position = this._predragPosition;
			
			if (this._clone)
			{
		    Element.remove(this._clone);
				this._clone = null;
			}
		}
//top.edump("stop-2",this.element);

    var dropped = false; 
    if(success) { 
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false; 
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && typeof revert == 'function') revert = revert(this.element);
    
    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect) 
      this.options.endeffect(this.element);
      
    Draggables.deactivate(this);
    Droppables.reset();
  },
  
  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },
  
  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },
  
  draw: function(pointer) {
		var posOptions = this.options.posOptions;
if (false) {
    var pos = Position.cumulativeOffset(this.element, posOptions);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element, posOptions);
			var winscroll = Position.windowScroll(Position.outerWindow(posOptions));		// only want the top-most one (no chaining)
      pos[0] += r[0] - winscroll[0];
      pos[1] += r[1] - winscroll[1];
    }
    
    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];
    
    var hasScrollables = (this.scrollables);
    var isScrolling = ((this.options.scroll) || (hasScrollables));
    if (isScrolling)
    {
    	if (hasScrollables)
    	{
    	}
    	else if (this.options.scroll != window && this._isScrollChild)
    	{
  	    pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
	      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    	}
    }

    var p = [0,1].map(function(i){ 
      return (pointer[i]-pos[i]-this.offset[i]) 
    }.bind(this));
} else {
			var doc = this.element.ownerDocument.title;
    	var p = [0,1].map(function(i){ 
      return (pointer[i]-this.offset[i]) 
    }.bind(this));
//debug("draw: pointer=("+pointer[0]+","+pointer[1]+"), offset=("+this.offset[0]+","+this.offset[1]+"), p=("+p[0]+","+p[1]+"), doc="+doc);
}
    
    if(this.options.snap) {
      if(typeof this.options.snap == 'function') {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(this.options.snap instanceof Array) {
        p = p.map( function(v, i) {
          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
      } else {
        p = p.map( function(v) {
          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
      }
    }}
    
    if (this.options.bounds)
    {
    	var bounds = ((typeof this.options.bounds == 'function')
    			? this.options.bounds(this)
    			: this.options.bounds);
    	if (bounds)
    	{
	    	p[0] = Math.max(Math.min(p[0], bounds[0]+bounds[2]-this.element.offsetWidth), bounds[0]);
	    	p[1] = Math.max(Math.min(p[1], bounds[1]+bounds[3]-this.element.offsetHeight), bounds[1]);
	    }
    }
    
    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";
    
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },
  
  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },
  
  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    var hasScrollables = (this.scrollables);
    var scaler = ((hasScrollables) ? Droppables.scrollSpeed : this.options.scrollSpeed);
    this.scrollSpeed = [speed[0]*scaler,speed[1]*scaler];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },
  
  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    var scrollables = this._toScrollables();
    for (var i=0; i<scrollables.length; i++)
    {
    	var scrollable = scrollables[i];
    	if (scrollable.isWindow)
    	{
	      with (this._getWindowScroll(scrollable.scrollable)) {
	        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
	          var d = delta / 1000;
	          scrollable.scrollable.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
	        }
	      }
    	}
    	else
    	{
	     	scrollable.scrollable.scrollLeft += this.scrollSpeed[0] * delta / 1000;
	     	scrollable.scrollable.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    	}
    }
    
		var posOptions = this.options.posOptions;
    Position.prepare(this._predragDocument, posOptions);
    Droppables.show(null, Draggables._lastPointer, this.element, posOptions);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }
    
    if(this.options.change) this.options.change(this);
  },
  
  _toScrollables: function()
  {
	 		var scrollable = this.options.scroll;
			return ((this.scrollables)
					? this.scrollables
					: [{ isWindow:(scrollable == window), scrollable:scrollable, originalScrollLeft:this.originalScrollLeft, originalScrollTop:this.originalScrollTop }]);
  },
  
  changed: function()
  {
  	this.cumulative = Position.cumulativeOffset(this.element, this.options.posOptions);
  },
  
  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
}
  
Draggable._importNode = function(doc, externalNode, deep)
  {
  	externalNode = $(externalNode);
  	
//top.console.log("_importNode(): doc="+doc+", externalNode="+top.estring(externalNode,true)+", deep="+deep);
  	if (doc.importNode)
  	{
//top.console.log("has importNode()");
  		return $(doc.importNode(externalNode, deep));
  	}
  	
		var oNew;
		
		if(externalNode.nodeType == 1)
		{
			oNew = $(doc.createElement(externalNode.nodeName));
			
			var hasWidth = false;
			var hasHeight = false;
			for(var i=0; i<externalNode.attributes.length; i++)
			{
				var attrName = externalNode.attributes[i].name;
				hasWidth = ((hasWidth) || (attrName == "width"));
				hasHeight = ((hasHeight) || (attrName == "height"));
				var attrValue = externalNode.attributes[i].nodeValue;
				if((attrValue != null) && (attrValue != ''))
				{
					if(attrName == "class")
						oNew.setAttribute("className", attrValue);
					else 
						oNew.setAttribute(attrName, attrValue);
				}
			}

			if((externalNode.style != null) && (externalNode.style.cssText != null))
			{
				oNew.style.cssText = externalNode.style.cssText;
			}
			
			// reassign width/height in case they got stomped (setting src attribute in <IMG> stomps in IE)
			if (hasWidth || hasHeight)
			{
				if (oNew.width != externalNode.width) oNew.setAttribute("width", externalNode.width);
				if (oNew.height != externalNode.height) oNew.setAttribute("height", externalNode.height);
			}
		} 
		else if(externalNode.nodeType == 3)
		{
			oNew = $(doc.createTextNode(externalNode.nodeValue));
		}
		
//debug("oNew="+oNew);
		if((deep) && externalNode.hasChildNodes())
		{
			for(var oChild = externalNode.firstChild; oChild; oChild = oChild.nextSibling)
			{
				var oNewChild = Draggable._importNode(doc, oChild, true);
//debug("oNewChild="+oNewChild);
				oNew.appendChild(oNewChild);
			}
		}
		
//debug("oNew="+oNew);
		return oNew;
	};

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create();
SortableObserver.prototype = {
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },
  
  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },
  
  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
}

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
  
  sortables: {},
  
  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {  
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },
  
  destroy: function(element){
    var s = Sortable.options(element);
    
    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');
      
      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({ 
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false, 
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,
      
      // these take arrays of elements or ids and can be 
      // used for better initialization performance
      elements:    false,
      handles:     false,
      
      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || {});
		options.posOptions = Position.posOptions(options);

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables  
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    }
    
    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    }

    // fix for gecko engine
    Element.cleanWhitespace(element); 

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).getElementsByClassName(options.handle)[0] : e); 
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });
    
    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },
  
  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode) 
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode) 
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },
  
  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);
        
    if(!Element.isParent(dropon, element)) {
      var index;
      
      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;
            
      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
        
        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }
      
      dropon.insertBefore(element, child);
      
      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return; 

    if(!Sortable._marker) {
      Sortable._marker = 
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }    
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
    
    if(position=='after')
      if(sortable.overlap == 'horizontal') 
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
    
    Sortable._marker.show();
  },
  
  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];
  
    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;
      
      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      }
      
      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child)
      
      parent.children.push (child);
    }

    return parent; 
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || {});
    
    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    }
    
    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || {});
    
    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || {});
    
    var nodeMap = {};
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });
   
    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },
  
  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || {});
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
    
    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" + 
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
}

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
}

Element.findChildren = function(element, only, recursive, tagName) {   
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
}

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}
