var SortableTree = Class.create({  initialize: function(element, options) {    this.element = $(element);    this.root = new SortableTree.Node(this, null, element, options);    this.isSortable = false;  },     toggleSortable: function() {    this.isSortable ? this.setUnsortable() : this.setSortable();  },    setSortable: function() {    Element.addClassName(this.root.element, 'sortable');    this.root.setSortable();    this.isSortable = true;  },   setUnsortable: function() {    Element.removeClassName(this.root.element, 'sortable');    this.root.setUnsortable();    this.isSortable = false;  },    find: function(element) {    return this.root.find($(element));  },   unmark_all: function() {    this.root.unmark();  }}); SortableTree.Node = Class.create({  initialize: function(tree, parent, element, options) {    this.tree = tree;    this.parent = parent;    this.element = $(element);     this.options = Object.extend({      tagName: 'LI',      containerTagName: 'UL',      droppable: {},      draggable: {}    }, options || {});     this.droppable_options = Object.extend({      onHover:      function(drag, drop, overlap){ this.onHover(drag, drop, overlap); }.bind(this),      onDrop:       function(drag, drop, event){ this.onDrop(drag, drop, event); }.bind(this),       overlap:      'vertical',      hoverclass:   'drop_hover'    }, options.droppable);     this.draggable_options = Object.extend({      ghosting: true,      revert: true,      constraint:  'vertical',      reverteffect: function(element, top_offset, left_offset) {        element.setStyle({left: '0px', top:  '0px'});        // would be so cool to be able to use this. but it leaves a backgroundColor        // style property on the element which overwrites the class' value        // (i.e. the drop marker) and apperently can't be removed anymore (?)        new Effect.Highlight(element, { startcolor: '#FFFF99' })      }    }, options.draggable);     this.initChildren();  },    id: function() {    if (!this._id) {      var match = this.element.id.match(/^[\w]+_([\d]*)$/);      this._id = encodeURIComponent(match ? match[1] : null);    }    return this._id;  },    previousSibling: function() {    var pos = this.parent.children.indexOf(this);    return pos > 0 ? this.parent.children[pos - 1] : null;  },    initChildren: function() {    this.children = [];      var container = this.findContainer(this.element);    if(container){      $A(container.childNodes).each(function(child) {        if(this.acceptTagName(child)) {          this.children.push(new SortableTree.Node(this.tree, this, child, this.options));        }      }.bind(this));    }  },   acceptTagName: function(element) {    return element.tagName && element.tagName.toUpperCase() == this.options.tagName;  },   setSortable: function() {    Droppables.add(this.element, this.droppable_options);    this.draggable = new Draggable(this.element, this.draggable_options);    this.children.each(function(child) { child.setSortable(); });  },   setUnsortable: function() {    Droppables.remove(this.element);    this.draggable.destroy();    this.children.each(function(child) { child.setUnsortable(); });  },    find: function(element) {    if(element == this.element) return this;    for(var i = 0; i < this.children.length; i++) {      var node = this.children[i].find(element);      if(node) return node;     }  },   findContainer: function(element) {    if(element.tagName != this.options.containerTagName) {      element = $A(element.childNodes).detect(function(node) {         return node.tagName == this.options.containerTagName;      }.bind(this));    }    return element;  },   findOrCreateContainer: function(element) {    var container = this.findContainer(element);    if(!container) {      container = document.createElement(this.options.containerTagName);      element.appendChild(container);    }    return container;  },   onHover: function(drag, drop, overlap) {        if(this.canContainChildren(drop)) {      this.dropPosition = overlap < 0.33 ? 'bottom' : overlap > 0.77 ? 'top' : 'insert';    } else {      this.dropPosition = overlap < 0.5 ? 'bottom' : 'top';    }    this.mark(drop);    if(this.options.onHover) this.options.onHover(drag, drop, overlap);  },     canContainChildren: function(element) {    if(this.options.droppable.container) {      return element.match(this.options.droppable.container);    }    return true;  },   onDrop: function(drag, drop, event) {    drag = this.tree.find(drag);    drop = this.tree.find(drop);     // i.e. don't do anything if it's a toplevel node and has been dropped on "itself"    // another way around this could be to change scriptaculous to affect() a node    // when it has been dropped on itself    if(drop.parent || this.dropPosition == 'insert') {       switch(this.dropPosition) {        case 'top':    drop.parent.insertBefore(drag, drop); break;        case 'bottom': drop.parent.insertBefore(drag, drop.nextSibling()); break;        case 'insert': this.insertBefore(drag, this.firstChild()); break;      }    }     if(this.options.onDrop) this.options.onDrop(drag, drop, event);  },   mark: function(element, position) {    this.tree.unmark_all();    Element.addClassName(element, 'drop_' + this.dropPosition);  },   unmark: function() {    ['drop_top', 'drop_bottom', 'drop_insert'].each(function(classname){      Element.removeClassName(this.element, classname);    }.bind(this));    this.children.each(function(child) { child.unmark(); });  },    to_params: function(name) {    name = name || this.tree.element.id;    var leftNode = this.previousSibling();    return name + '[' + this.id() + '][parent_id]=' + this.parent.id() + '&' +            name + '[' + this.id() + '][left_id]=' + (leftNode ? leftNode.id() : ''); // null  },    firstChild: function() {    return this.children.length > 0 ? this.children[0] : null;  },    previousSibling: function() {    var pos = this.parent.children.indexOf(this);    return pos > 0 ? this.parent.children[pos - 1] : null;  },    nextSibling: function() {    var pos = this.parent.children.indexOf(this);    return pos + 1 < this.parent.children.length ? this.parent.children[pos + 1] : null;      },    removeChild: function(node) {    this.children.splice(this.children.indexOf(node), 1);    node.element.parentNode.removeChild(node.element);  },    insertBefore: function(node, sibling) {    if(node == sibling) return;        node.parent.removeChild(node);    node.parent = this;    var pos = sibling ? this.children.indexOf(sibling) : this.children.length;    this.children.splice(pos, 0, node);     this.findOrCreateContainer(this.element).insertBefore(node.element, sibling ? sibling.element : null);  }});
