/*
  draglist v1.0
  7 August 2008
  Nicolas Sierro

  Instructions:
    - Download this file
    - Add <script src="draglist.js"></script> to your HTML.
    - Add an id and class="draglist" to any unordered list you might like to reorder.
    - Drag the items around to reorder them.

  This is code was based on:
    - Dan Vanderkam's DragTable, http://danvk.org/dragtable/, http://code.google.com/p/dragtable/
    - Stuart Langridge's SortTable (http://kryogenix.org/code/browser/sorttable)
    - Mike Hall's draggable class (http://www.brainjar.com/dhtml/drag/)

  Licensed under the BSD license.
 */

// Here's the notice from Mike Hall's draggable script:
//*****************************************************************************
// Do not remove this notice.
//
// Copyright 2001 by Mike Hall.
// See http://www.brainjar.com for terms of use.
//*****************************************************************************
draglist = {
  // How far should the mouse move before it's considered a drag, not a click?
  dragRadius2: 100,
  setMinDragDistance: function(x) {
    draglist.dragRadius2 = x * x;
  },

  // Determine browser and version.
  // TODO: eliminate browser sniffing except where it's really necessary.
  Browser: function() {
    var ua, s, i;

    this.isIE    = false;
    this.isNS    = false;
    this.version = null;
    ua = navigator.userAgent;

    s = "MSIE";
    if ((i = ua.indexOf(s)) >= 0) {
      this.isIE = true;
      this.version = parseFloat(ua.substr(i + s.length));
      return;
    }

    s = "Netscape6/";
    if ((i = ua.indexOf(s)) >= 0) {
      this.isNS = true;
      this.version = parseFloat(ua.substr(i + s.length));
      return;
    }

    // Treat any other "Gecko" browser as NS 6.1.
    s = "Gecko";
    if ((i = ua.indexOf(s)) >= 0) {
      this.isNS = true;
      this.version = 6.1;
      return;
    }
  },
  browser: null,

  // Detect all draggable lists and attach handlers to their elements.
  init: function() {
    // Don't initialize twice
    if (arguments.callee.done) return;
    arguments.callee.done = true;
    if (_draglisttimer) clearInterval(_draglisttimer);
    if (!document.createElement || !document.getElementsByTagName) return;

    draglist.dragObj.zIndex = 0;
    draglist.browser = new draglist.Browser();
    forEach(document.getElementsByTagName('ul'), function(ul) {
      if (ul.className.search(/\bdraglist\b/) != -1) {
        draglist.makeDraggable(ul);
      }
    });
  },

  makeDraggable: function(ul) {
    for (var i = ul.childNodes.length-1; i >=0; i--) {
      if (ul.childNodes[i].nodeValue=="\n")
        ul.removeChild(ul.childNodes[i]);
      else
        ul.childNodes[i].onmousedown = draglist.dragStart;
    }
    var cookie=readCookie("draglist_"+ul.id);
    if (cookie) {
      var value=cookie.split(':');
      var elem=[];
      var i=0;
      while (ul.childNodes.length>0) {
        elem[i++]=ul.removeChild(ul.childNodes[0]);
      }
      for (var j = 0; j < value.length; j++) {
        var i=0;
        while ((i<elem.length) && (elem[i].id != value[j])) {
          i++;
        }
        if (i<elem.length)
          ul.appendChild(elem[i]);
      }
    }
  },

  trackChange: function(ul) {
    var value=[];
    var j=0;
    for (var i = 0; i < ul.childNodes.length; i++) {
      if (ul.childNodes[i].nodeName == "LI") {
        value[j++]=ul.childNodes[i].id;
      }
    }
    createCookie("draglist_"+ul.id,value.join(':'),7);
  },

  // Global object to hold drag information.
  dragObj: new Object(),

  // Climb up the DOM until there's a tag that matches.
  findUp: function(elt, tag) {
    do {
      if (elt.nodeName && elt.nodeName.search(tag) != -1)
        return elt;
    } while (elt = elt.parentNode);
    return null;
  },

  // clone an element, copying its style and class.
  fullCopy: function(elt, deep) {
    var new_elt = elt.cloneNode(deep);
    new_elt.className = elt.className;
    forEach(elt.style, function(k) { new_elt.style[k] = elt.style[k]; });
    return new_elt;
  },

  eventPosition: function(event) {
    var x, y;
    if (draglist.browser.isIE) {
      x = window.event.clientX + document.documentElement.scrollLeft
        + document.body.scrollLeft;
      y = window.event.clientY + document.documentElement.scrollTop
        + document.body.scrollTop;
      return {x: x, y: y};
    }
    return {x: event.pageX, y: event.pageY};
  },

  // Determine the position of this element on the page. Many thanks to Magnus
  // Kristiansen for help making this work with "position: fixed" elements.
  absolutePosition: function(elt) {
    var ex = 0, ey = 0;
    do {
      var curStyle = draglist.browser.isIE ? elt.currentStyle
                                            : window.getComputedStyle(elt, '');
      var supportFixed = !(draglist.browser.isIE &&
                           draglist.browser.version < 7);
      if (supportFixed && curStyle.position == 'fixed') {
        // Get the fixed el's offset
        ex += parseInt(curStyle.left, 10);
        ey += parseInt(curStyle.top, 10);
        // Compensate for scrolling
        ex += document.body.scrollLeft;
        ey += document.body.scrollTop;
        // End the loop
        break;
      } else {
        ex += elt.offsetLeft;
        ey += elt.offsetTop;
      }
    } while (elt = elt.offsetParent);
    return {x: ex, y: ey};
  },

  // MouseDown handler -- sets up the appropriate mousemove/mouseup handlers
  // and fills in the global draglist.dragObj object.
  dragStart: function(event, id) {
    var el;
    var x, y;
    var dragObj = draglist.dragObj;

    var browser = draglist.browser;
    if (browser.isIE)
      dragObj.origNode = window.event.srcElement;
    else
      dragObj.origNode = event.target;
    var pos = draglist.eventPosition(event);

    // Drag the entire item, not just the element that was clicked.
    dragObj.origNode = draglist.findUp(dragObj.origNode, "LI");

    // Since a row can't be dragged directly, duplicate its contents and drag that instead.
    var ul = draglist.findUp(dragObj.origNode, "UL");
    dragObj.ul = ul;
    dragObj.startRow = draglist.findRow(ul, pos.y);
    if (dragObj.startRow == -1) return;

    var new_elt = draglist.fullCopy(ul, false);
    new_elt.style.margin = '0';

    new_elt.appendChild(draglist.fullCopy(ul.childNodes[dragObj.startRow], true));

    var obj_pos = draglist.absolutePosition(dragObj.origNode);
    new_elt.style.position = "absolute";
    new_elt.style.left = obj_pos.x + "px";
    new_elt.style.top = obj_pos.y + "px";

    // Hold off adding the element until this is clearly a drag.
    dragObj.addedNode = false;
    dragObj.ulContainer = dragObj.ul.parentNode || document.body;
    dragObj.elNode = new_elt;

    // Save starting positions of cursor and element.
    dragObj.cursorStartX = pos.x;
    dragObj.cursorStartY = pos.y;
    dragObj.elStartLeft  = parseInt(dragObj.elNode.style.left, 10);
    dragObj.elStartTop   = parseInt(dragObj.elNode.style.top,  10);

    if (isNaN(dragObj.elStartLeft)) dragObj.elStartLeft = 0;
    if (isNaN(dragObj.elStartTop))  dragObj.elStartTop  = 0;

    // Update element's z-index.
    dragObj.elNode.style.zIndex = ++dragObj.zIndex;

    // Capture mousemove and mouseup events on the page.
    if (browser.isIE) {
      document.attachEvent("onmousemove", draglist.dragMove);
      document.attachEvent("onmouseup",   draglist.dragEnd);
      window.event.cancelBubble = true;
      window.event.returnValue = false;
    } else {
      document.addEventListener("mousemove", draglist.dragMove, true);
      document.addEventListener("mouseup",   draglist.dragEnd, true);
      event.preventDefault();
    }
  },

  dragMove: function(event) {
    var x, y;
    var dragObj = draglist.dragObj;

    // Get cursor position with respect to the page.
    var pos = draglist.eventPosition(event);

    var dx = dragObj.cursorStartX - pos.x;
    var dy = dragObj.cursorStartY - pos.y;
    if (!dragObj.addedNode && dx * dx + dy * dy > draglist.dragRadius2) {
      dragObj.ulContainer.insertBefore(dragObj.elNode, dragObj.ul);
      dragObj.addedNode = true;
    }

    // Move drag element by the same amount the cursor has moved.
    var style = dragObj.elNode.style;
    style.left = (dragObj.elStartLeft + pos.x - dragObj.cursorStartX) + "px";
    style.top  = (dragObj.elStartTop  + pos.y - dragObj.cursorStartY) + "px";

    if (draglist.browser.isIE) {
      window.event.cancelBubble = true;
      window.event.returnValue = false;
    } else {
      event.preventDefault();
    }
  },

  // Stop capturing mousemove and mouseup events.
  dragEnd: function(event) {
    if (draglist.browser.isIE) {
      document.detachEvent("onmousemove", draglist.dragMove);
      document.detachEvent("onmouseup", draglist.dragEnd);
    } else {
      document.removeEventListener("mousemove", draglist.dragMove, true);
      document.removeEventListener("mouseup", draglist.dragEnd, true);
    }

    // If the floating row wasn't added, the mouse didn't move far enough.
    var dragObj = draglist.dragObj;
    if (!dragObj.addedNode) {
      return;
    }
    dragObj.ulContainer.removeChild(dragObj.elNode);

    // Determine whether the drag ended over the list, and over which row.
    var pos = draglist.eventPosition(event);
    var ul_pos = draglist.absolutePosition(dragObj.ul);
    if (pos.y < ul_pos.y ||
        pos.y > ul_pos.y + dragObj.ul.offsetHeight) {
      return;
    }
    var targetRow = draglist.findRow(dragObj.ul, pos.y);
    if (targetRow != -1 && targetRow != dragObj.startRow) {
      draglist.moveRow(dragObj.ul, dragObj.startRow, targetRow);
    }
  },

  // Which row does the y value fall inside of? y should include scrollTop.
  findRow: function(ul, y) {
    for (var i = 0; i < ul.childNodes.length; i++) {
      if (ul.childNodes[i].nodeName == "LI") {
        var pos = draglist.absolutePosition(ul.childNodes[i]);
        if (pos.y <= y && y <= pos.y + ul.childNodes[i].offsetHeight) {
          return i;
        }
      }
    }
    return -1;
  },

  // Move a row from start index to finish index.
  moveRow: function(ul, sIdx, fIdx) {
    var x = ul.removeChild(ul.childNodes[sIdx]);
    if (fIdx < ul.childNodes.length) {
      ul.insertBefore(x, ul.childNodes[fIdx]);
    } else {
      ul.appendChild(x);
    }
    draglist.trackChange(ul);
  }
}

/* ******************************************************************
   Supporting functions: bundled here to avoid depending on a library
   ****************************************************************** */

// Dean Edwards/Matthias Miller/John Resig
// has a hook for draglist.init already been added? (see below)
var draglistListenOnLoad = false;

/* for Mozilla/Opera9 */
if (document.addEventListener) {
  draglistListenOnLoad = true;
  document.addEventListener("DOMContentLoaded", draglist.init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  draglistListenOnLoad = true;
  document.write("<script id=__draglist_onload defer src=javascript:void(0)><\/script>");
  var script = document.getElementById("__draglist_onload");
  script.onreadystatechange = function() {
    if (this.readyState == "complete") {
      draglist.init(); // call the onload handler
    }
  };
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
  draglistListenOnLoad = true;
  var _draglisttimer = setInterval(function() {
    if (/loaded|complete/.test(document.readyState)) {
      draglist.init(); // call the onload handler
    }
  }, 10);
}

/* for other browsers */
/* Avoid this unless it's absolutely necessary (it breaks sorttable) */
if (!draglistListenOnLoad) {
  window.onload = draglist.init;
}

// Dean's forEach: http://dean.edwards.name/base/forEach.js
/*
  forEach, version 1.0
  Copyright 2006, Dean Edwards
  License: http://www.opensource.org/licenses/mit-license.php
*/

// array-like enumeration
if (!Array.forEach) { // mozilla already supports this
  Array.forEach = function(array, block, context) {
    for (var i = 0; i < array.length; i++) {
      block.call(context, array[i], i, array);
    }
  };
}

// generic enumeration
Function.prototype.forEach = function(object, block, context) {
  for (var key in object) {
    if (typeof this.prototype[key] == "undefined") {
      block.call(context, object[key], key, object);
    }
  }
};

// character enumeration
String.forEach = function(string, block, context) {
  Array.forEach(string.split(""), function(chr, index) {
    block.call(context, chr, index, string);
  });
};

// globally resolve forEach enumeration
var forEach = function(object, block, context) {
  if (object) {
    var resolve = Object; // default
    if (object instanceof Function) {
      // functions have a "length" property
      resolve = Function;
    } else if (object.forEach instanceof Function) {
      // the object implements a custom forEach method so use that
      object.forEach(block, context);
      return;
    } else if (typeof object == "string") {
      // the object is a string
      resolve = String;
    } else if (typeof object.length == "number") {
      // the object is array-like
      resolve = Array;
    }
    resolve.forEach(object, block, context);
  }
};
