/* JavaScript utility functions by Bryan Hoyt of Brush Technology
 * http://brush.co.nz
 *
 * *** NEED TO FIX: I've unthinkingly been careless with namespaces.
 * In different browsers, some combination of these are all in the
 * same namespace:
 * - element attributes: <elem attr=''>
 * - object (& therefore element) properties: elem.prop = ''
 * - object (& therefore element) indices: elem['idx']
 * - object (& therefore element) methods:
 *      elem.meth()
 *      elem.meth = function
 *      elem.addMethod('meth', function)
 * Thus, we need to get a browser-comprehensive list of all possible
 * attrs, and all standard elem properties & methods, and make sure that
 * attrs, props, idxes, and meths don't have any duplicate names.
 * - We've had this problem twice already with selection/selected/getSelection().
 * - We've also had it with enable/toenable, appear/toappear, update/toupdate
 */

Events = {
    'form': function() {
      this.addEvent('submit', this.clearEmptyInputs);
      if (this.hasClass('disabled')) {
        var inputs = this.$$('input, select, textarea');
        for (j=0; j<inputs.length; j++) {
          inputs[j].disabled = true;
        }
      }
    },
    'textarea': function() {
      if (this.getAttribute('maxlength')) {
        this.addEvent('keypress', charsLeft);
        this.addEvent('keyup', charsLeft);
        this.addEvent('change', charsLeft);
        this.addEvent('mousedown', charsLeft);
      }
    },
    'a': function() {
      if (this.hasClass('new-window')) {
        this.target = '_blank';
        if (! this.getAttribute('title')) {
          this.title = 'Opens in new window';
        }
      }
      if (this.href.indexOf('mailto:')>=0 && this.href.indexOf('...')>=0) {
        this.addEvent('mouseover', unmungeEmail);
      }
      if (this.hasClass('submit')) {
        this.href = '#_submit-link';
        this.addEvent('click', submitLink);
      }
    },
    '.flash': function() {
      flashDiv(this.id, 0);
    },
    '.toggle': function() {
      this.href = '#_toggle-link';
      this.addEvent('click', this.toggle);
    },
    '.alternate': function() {
      this.href = '#_alternate-link';
      this.addEvent('click', this.alternate);
    },
    '[toappear], [toenable], [toupdate]': function() {
      this.oncondition();
      this.addEvent('keyup', this.oncondition);
      this.addEvent('change', this.oncondition);
      this.addEvent('click', this.oncondition);
    },
    '[tooltip]': {'mouseover': popupTooltip},
    '[eg]': function() {
      this.addEvent('focus', this.removeEg);
      this.addEvent('mousedown', this.removeEg);
      this.addEvent('blur', this.addEg);
      this.addEg();
    },
    '.focus': function() {
      this.focus();
      if (this.getAttribute('eg')) {
        this.addEg(); // Because obj.focus would've removed the Eg.
        this.select();
      }
    },
    '[rollover]': {
      'mouseover': rollover,
      'mouseout': rollout
    },
    '[preload]': function() {
      if (this.matches('a')) preload(this.href);
      else preload(this.preload);
    }
};

function applyClassCode(singlecomponent, obj) {
  /* Loop through elements on the page and turn them into useful members of our UI */
  if (!obj) obj = document;
  var elems = obj.getElementsByTagName('*');

  for (var i=0; i<elems.length; i++) {
    for (var e in Events) {
      var ev = Events[e];
      // matches only single-component names for speed
      if (Element.matches.call(elems[i], e, singlecomponent)) {     // *** we should factor out the .matches() method to precalculate match functions. Complicated, but quick for looping over Events multiple times.
        if (typeof ev == "function") ev.call($(elems[i]));
        else {
            for (var evtype in ev) $(elems[i]).addEvent(evtype, ev[evtype]);
        }
      }
    }
  }
}


/* === HTML element manipulation methods ===
 *
 * We need a new Element class to make it possible to extend IE's elements.
 * (we use the same method in Fx, but if it weren't for IE, we'd use
 *  Element.prototype)
 * Note: Element methods can assume that 'this' has already been
 * extended -- no need to use $(this). That's because the fact that
 * an Element method has been called on 'this' implies that 'this'
 * must've been extended to included that method.
 *
 * Of course, that assumption means that any function which explicitly
 * call()s an Element method with a supplied object should extend it first.
 * Probably instead of doing Element.myfunc.call($(obj)), you can generally
 * just use $(obj).myfunc() to get the same results.
 */
 
function Element() {
}
Element.methods = {};
Element.methods['addMethod'] = function(name, func) {
    this.methods[name] = func;
    this[name] = func;
}
Element.addMethod = Element.methods['addMethod'];
Element.addMethod('extend', function(elem) {
    if (elem._extended) return;
    elem._extended = true;
    for (var name in this.methods) {
        elem[name] = this.methods[name];
    }
    return elem;
});

Element.addMethod('updatetext', function(text) {
    if (this.nodeType == 3) this.nodeValue = text;
    else if (!this.lastChild || this.lastChild.nodeType!=3) {
        elem = document.createTextNode(text);
        this.appendChild(elem);
    }
    else this.firstChild.nodeValue = text;
    return this;
});
Element.addMethod('remove', function(selector) {
    /* if selector is given, remove the child elements matching
     * that selector.
     * If not, remove this element itself.
     */
    if (selector == null) {
        this.parentNode.removeChild(this);
        return;
    }
    var elems = this.$$(selector);
    for (var i in elems) {
        this.removeChild(elems[i]);
    }
});
Element.addMethod('show', function() {
    if (this._display) this.style.display = this._display;
    else this.style.display = 'block';
});
Element.addMethod('hide', function() {
    if (this.style.display != 'none') this._display = this.computedStyle()['display'];
    this.style.display = 'none';
});
Element.addMethod('enable', function() {
    this.removeClass('disabled');
    this.disabled = false;
});
Element.addMethod('disable', function() {
    this.addClass('disabled');
    this.disabled = true;
});

Element.addMethod('toggle', function() {
    if (this.style.display == 'none') this.show();
    else this.hide();
});
Element.addMethod('alternate', function(event) {
  $(this.getAttribute('show')).show();
  $(this.getAttribute('hide')).hide();

  var focusid = this.getAttribute('focusid');
  if (focusid) $(focusid).focus();
});
Element.addMethod('topleft', function() {
    return [this.offsetLeft, this.offsetTop];
});
Element.addMethod('bottomright', function() {
    return [this.offsetLeft+this.offsetWidth, this.offsetTop+this.offsetHeight];
});
Element.addMethod('center', function() {
    return [this.offsetLeft+this.offsetWidth/2, this.offsetTop+this.offsetHeight/2];
});
Element.addMethod('overlaps', function(other) {
    /* Returns true if this overlaps other.
     * Returns false otherwise, including the case when this & other are identical
     */
    $(other);
    if (this == other) return false;
    return overlaps(this.topleft(), this.bottomright(), other.topleft(), other.bottomright());
});
Element.addMethod('overlapping', function(elems) {
    /* Takes an array of elements to test against. For example:
     * obj.overlapping($$('.droppable')) to find all "droppable" elements
     * that are currently being overlapped by obj.
     *
     * Returns elements from the list which this element overlaps.
     *
     * Note: this can be a relatively slow function, since it loops over all
     * the elements you give it each time. If you're calling it in a mousemove
     * event, that can make for some very slow dragging. Speed it up by passing
     * in only the elements that you care about. Also speed it up by
     * selecting the test list in advance, rather than selecting it every
     * mousemove.
     */
    var over = [];
    for (var i=0; i<elems.length; i++) {
        if (this.overlaps(elems[i])) {
            over.push(elems[i]);
        }
    }
    return over;
});
Element.addMethod('matches', function(simple, singlecomponent, final) {
    /* Returns true if this element matches the given SIMPLE selector.
     * Eg elem.matches("a.class.otherclass")
     *
     * Chained selectors are simply not supported
     * (the function would be too complex for too little gain)
     * However, multiple selectors (comma-separated) are supported.
     *
     * You may specify 'singlecomponent' if you only want to match a
     * single-component selector. A bit quicker if you're doing a
     * lengthy loop.
     *
     * 'final' is an internal implementation detail: don't use it.
     */
    if (! final) {
        var selectors = simple.split(',');
        for (var i=0; i<selectors.length; i++) {
            if (Element.matches.call(this, selectors[i], singlecomponent, true)) return true;
        }
        return false;
    }

    var comps;
    if (! singlecomponent) comps = components(simple);
    else comps = [strip(simple)];
    
    var ismatch = true;
    for (var i=0; i<comps.length; i++) {
        var component = comps[i];

        // Note: we avoid using a regex here to get the component's label, coz it's slow
        switch (component.charAt(0)) {
            case '[':
                if (this.getAttribute(component.substring(1, component.length-1))) ismatch = true;
                else ismatch = false;
                break;
            case '#':
                ismatch = this.id == component.substring(1);
                break;
            case '.':
                ismatch = Element.hasClass.call(this, component.substring(1));
                break;
            default:  // component is a tag name.
                ismatch = this.nodeName.toLowerCase() == component.toLowerCase();
                break;
        }
        if (component == '*') ismatch = true;
        if (! ismatch) break;     // Once it doesn't match, no point in further testing
    }
    return ismatch;
});
Element.addMethod('computedStyle', function () {
  if (window.getComputedStyle) {
    return window.getComputedStyle(this, null);
  }
  else return this.currentStyle;
});

Element.addMethod('contains', function (descendant) {
  var ret;
  try {
    if (!descendant) ret = false;
    else if (this.contains) ret = this.contains(descendant);
    else if (descendant == document.body) ret = false;
    else if (descendant.parentNode == this) ret = true;
    else ret = this.contains($(descendant).parentNode);
  }
  catch (err) {
    ret = false;
  }
  return ret;
});

Element.addMethod('_ancestor', function(selectors, stopat) {
  /* like ancestor(), but returns the matching selector also
   */
    if (! stopat) stopat = document.body;
    if (! selectors) selectors = '*';
    if (selectors.substring) selectors = [selectors];
    for (var i in selectors) {
        if ($(this.parentNode).matches(selectors[i])) return [selectors[i], this.parentNode];
    }
    if (this.parentNode == stopat || this.parentNode == document.body) return null;
    if (this.parentNode == null) return null;
    return $(this.parentNode)._ancestor(selectors, stopat); // recurse
});

Element.addMethod('ancestor', function(selectors, stopat) {
  /* find ancestor that matches one of SIMPLE selectors.
   * 'selectors' can be a string or a list of strings.   */
    var ancest = this._ancestor(selectors, stopat);
    if (ancest) return ancest[1];
    return null;
});


/* === 2-dimensional geometry === */

function pointin(point, tl, br) {
    /* Takes 3 points as arrays:
     * - point: [x, y] coordinates of point we're testing
     * - tl: [x, y] top-left of rectangle
     * - br: [x, y] bottom-right of rectangle
     *
     * Returns true if both coordinates are within the boundaries, inclusive.
     */
    if (point[0]<tl[0] || point[0]>br[0]) return false; // x-coord is outside
    if (point[1]<tl[1] || point[1]>br[1]) return false; // y-coord is outside
    return true;
}

function overlaps(tl1, br1, tl2, br2) {
    /* Returns true if the rectangle defined by tl1/br1 overlaps
     * the rectangle defined by tl2/br2.
     */
    tr1 = [br1[0], tl1[1]];
    bl1 = [tl1[0], br1[1]];

    if (pointin(tl1, tl2, br2)) return true;
    if (pointin(br1, tl2, br2)) return true;
    if (pointin(tr1, tl2, br2)) return true;
    if (pointin(bl1, tl2, br2)) return true;

    if (pointin(tl2, tl1, br1)) return true;  // else, tl2/br2 is entirely inside tl1/br1
    return false                              // unless it's nowhere close
}

/* === General document info/manipulation === */

function gettarget() {
  /* Gets the element corresponding to the #fragment portion of the url.
   * Basically a getElementById on the #fragment.
   */
    var loc = document.location.href;
    return $(loc.substr(loc.lastIndexOf('#')));
}

var _preloadedImages = {};
function preload(filename) {
    _preloadedImages[filename] = new Image();
    _preloadedImages[filename].src = filename;
}

Element.addMethod('textSelection', function() {
    /* Only usable on document or iframe.document
     */
    if (this.getSelection) return this.getSelection();      // Fx 2.0 (for editor)
    if (window.getSelection) return this.theSelection();    // Fx 3.x, Opera, Safari
    else return this.theSelection().createRange().text;     // IE
});

Element.addMethod('theSelection', function() {
    /* Returns the selection in a cross-browser fashion
     *
     * eg $(document).textSelection(), NOT $(elem).textSelection()
     * *** the purpose of this might be clearer if we made it a global
     * function with an optional frame/iframe argument
     */
    if (window.getSelection) return window.getSelection();  // Fx, Opera, Safari
    else return this.selection;                             // IE
});

Element.addMethod('inserthtml', function(html) {
    /* Insert given html in place of current selection.
     * Only usable on document or iframe.document
     *
     * eg $(document).inserthtml(), NOT $(elem).inserthtml()
     */
    
    if (window.getSelection) this.execCommand('inserthtml', null, html);
    else this.selection.createRange().pasteHTML(html);
});


function clearSelection() {
    selection = window.getSelection;
    if (selection) {
        if (selection.collapse) selection.collapse();                     // Safari
        else if (selection.removeAllRanges) selection.removeAllRanges();  // Fx
    }
    else if (document.selection) {                                        // IE
        document.selection.clear();
    }
}


/* === String & selector manipulation ===
 *
 * CSS selector examples:
 * component: "#myid", ".myclass", "[myattr]"
 * simple: "#myid.myclass", "[myattr].myclass", "#myid"
 * combinator: "<", ">", "+"
 * single selector: "#myid.myclass > .otherclass"
 * multiple selector: "#myid.myclass > .otherclass, #myid[myattr] > .otherclass"
 */

function strip(string, character) {
    if (! character) character = ' ';
    var end;
    var start;
    for (start=0; start<string.length; start++) {
      if (string.charAt(start) != character) break;
    }

    for (end=start; end<string.length; end++) {
      if (string.charAt(end) == character) break;
    }
    return string.substring(start, end);
}

function components(simple) {
    /* return an array of components from the simple selector 'simple'
     * (a component can be a class, an attribute, or an id)
     * Each componenent is returned with the prefix still attached,
     * eg: ['.class1', '.class2', '#id', '[attr]']
     */
    var chars = [];
    var c;
    var start;
    simple = strip(simple);
    while (simple) {
        var i = 0;
        while (true) {
            c = simple.charAt(i);
            if (c!='.' && c!='[' && c!='#') break;
            start = c;
            if (++i >= simple.length) break;
        }
        while (true) {
            c = simple.charAt(i);
            if (start=='[') {
              if (c==']') {
                i++;
                break;
              }
            }
            else if (c=='.' || c=='#' || c=='[') break;
            if (++i >= simple.length) break;
        }
        var match = simple.substring(0, i);
        simple = simple.substring(match.length);
        chars.push(match);
    }
    return chars;
}

function simples(selector) {
    /* return [[combinator, simple], ...]
     */
    var s = [];
    
    while (selector) {
        var combinator = / *[><\+]* */.exec(selector)[0];
        selector = selector.substring(combinator.length);

        var simple;
        if (selector.indexOf(' ') >= 0) {
            simple = selector.substring(0, selector.indexOf(' '));
            selector = selector.substring(selector.indexOf(' '));
        }
        else {
            simple = selector;
            selector = '';
        }
        combinator = strip(combinator);
        if (! combinator) combinator = null;
        s.push([combinator, simple]);
    }
    return s;
}


/* === Selection-by-CSS-selector functions ===
 *
 * There are a whole bunch of utility functions here, but at the end of the
 * day, the one's you'll most likely be using are:
 * - $(css_selector) to select a single element (by ID, or else the first
 *   element in the list of whatever $$ returns)
 * - $$(css_selector) to select all elements matching css_selector.
 * - Element methods of the above to select descendents of the parent
 *   element that match. So obj.$('.myclass') to get the first element
 *   inside 'obj' that has a class 'myclass'.
 */

function componentlabel(component) {
    var label = /[a-zA-Z0-9-_\*="]+/.exec(component)[0];
    return label;
}

function $$component(component, parent, combinator) {
    /* select all items matching the attribute/id/class/type/universal selector
     * component. Only single components are allowed, e.g. "#id", or ".class",
     * but NOT "#id.class"
     */

    var label;
    var elems;

    Element.extend(parent);
    
    if (! component) return null;

    label = componentlabel(component);
    
    if (combinator=='+' || combinator=='++') {
        elems = [$(parent.nextSibling)];
        if (combinator=='++' && elems[0].nodeName.charAt(0)=='#') {
            elems = [$(elems[0].nextSibling)];
        }
    }
    else if (combinator=='<') {
        elems = [$(parent.parentNode)];
    }
    else {
        switch (component.charAt(0)) {
            case '[':
                elems = parent.getElementsByAttribute(label);
                break;
            case '#':
                var elem = $(document.getElementById(label));
                if (elem === null) elems = [];
                else elems = [elem];
                break;
            case '.':
                elems = parent.getElementsByClassName(label);
                break;
            default:
                elems = [];
                elems_ = parent.getElementsByTagName(label);
                // do NOT change this to "for (var i in elems_)"
                for (var i=0; i<elems_.length; i++) elems.push($(elems_[i]));    // because you can't use .splice on HTMLCollection
                break;
        }
    }
    return elems;
}

function $$simple(simple, parent, combinator) {
    /* Select all items matching the simple (multicomponent, but unchained)
     * selector 'simple'.
     */
    var elems = null;
    Element.extend(parent);
    var component = components(simple)[0];    // assume we've got at least 1 component.
    elems = $$component(component, parent, combinator); // get the pool to filter from

    elems_ = [];
    for (var i=0; i<elems.length; i++) {      // then, narrow our pool down
        var drop = !elems[i].matches(component);
        if (combinator=='>' && parent!=elems[i].parentNode) drop = true;
        if (! drop) elems_.push(elems[i]);
    }
    elems = elems_;

    return elems;
}

function $$single(selector, parent) {
    /* Select all items matching the single selector 'selector'. Instead of
     * this function, you should use the higher-level $() function, which
     * also allows you to bunch multiple selectors together
     */
    var label;

    Element.extend(parent);
    if (! selector) return [];
    
    var pool = [parent];
    var pool_ = [];
    var simps = simples(selector);
    for (var i in simps) {
        combinator = simps[i][0];
        simple = simps[i][1];

        pool_ = [];
        for (var j in pool) {
            pool_ = pool_.concat($$simple(simple, pool[j], combinator));
        }
        pool = pool_;
        if (! pool) break;                  // if there's none in this pool, then nothing will come of narrowing it down!
    }
    for (var i in pool) Element.extend(pool[i]);
    return pool;
}
function $$(selector) {
    /* 'selector' is a string to match a list of elements in the document.
     * It can also be a comma-separated list of selectors.
     *
     * Selector syntax is much the same as CSS, with the following important
     * differences:
     * - combinators (>, <, +, ++) can appear at the beginning of a
     *   chained selector; not just between simple selectors.
     *   The selection is done relative to the element, if this is called
     *   as an element's class method, or else relative to the document.
     * - #id selects the item from the document, whether or not it is
     *   beneath an item matching the preceding part of the selector.
     *   This is useful so that functions which normally operate relative to
     *   a particular element can be made a lot more flexible.
     * - There's a '<' combinator, which selects the *parent* of the preceding item.
     *   It works very similarly to the '>' selector.
     * - There's a '++' combinator, which works just like the '+' combinator
     *   except that it ignores #text, #comment, and #other elements, and just
     *   selects the next <tag-enclosed> element.
     * - We implement [attribute], but don't currently implement [attribute="..."] syntax
     */
    var parent;
    var elems = [];

    if (this.nodeType) parent = this;        // We're being called as a class method
    else parent = document;
    Element.extend(parent);

    if (selector === null) elems = [];
    else if (selector.nodeType) {
        Element.extend(selector);
        elems = [selector];
    }
    else if (selector.substring) {
        var selectors = selector.split(',');
        for (var i in selectors) {
            elems = elems.concat($$single(selectors[i], parent));
        }
    }
    else {
        throw("Unknown type of object passed to $$ function");
    }
    return elems;
}

function $(selector) {
    var parent;
    if (this.nodeType) parent = this;       // We're being called as a class method
    else parent = document;
    Element.extend(parent);
    var elems = parent.$$(selector);
    if (elems.length >= 1) return elems[0];
    else return null;
}

function $new(type, text) {
    /* 'type' is a simple CSS selector. In this case, instead of
     * using it to select elements, we use it to create an element
     * with those properties.
     * 'text' is optional
     * This function is kindof a fancy document.createElement()
     */
    var elem;
    var classes = [];
    var id = null;
    var attrs = [];
    var tag = 'div';
    if (type.substring) {
        var comps = components(type);
        for (var i=0; i<comps.length; i++) {
            var label = componentlabel(comps[i]);
            switch (comps[i].charAt(0)) {
                case '.':
                    classes.push(label);
                    break;
                case '#':
                    id = label;
                    break;
                case '[':
                    attrs.push(label); // *** not using this atm
                    break;
                default:
                    tag = label;
                    break;
            }
        }
        elem = $(document.createElement(tag));
        for (var i=0; i<classes.length; i++) elem.addClass(classes[i]);
        if (id) elem.id = id;
    } else elem = $(type);
    if (text != null) elem.updatetext(text);
    return elem;
}

Element.addMethod('$$', $$);
Element.addMethod('$', $);
Element.addMethod('append', function(type, text) {
    /* appends a new element of 'type' as the last child of 'this'
     * Returns the newly created element.
     * 'text' is optional
     */
    var elem = $new(type, text);
    this.appendChild(elem);
    return elem;
});
Element.addMethod('prepend', function(type, text) {
    /* prepends a new element of 'type' as the first child of 'this'
     * Returns the newly created element.
     * 'text' is optional
     */
    var elem = $new(type, text)
    this.insertBefore(elem, this.firstChild);
    return elem;
});
Element.addMethod('precreate', function(type, text) {
    /* inserts a new element of 'type' just before 'this'
     * 'text' is optional
     * Returns the newly created element.
     */
    var elem = $new(type, text)
    this.parentNode.insertBefore(elem, this);
    return elem;
});
Element.addMethod('postcreate', function(type, text) {
    /* inserts a new element of 'type' just after 'this'
     * 'text' is optional
     * Returns the newly created element.
     */
    var elem = $new(type, text)
    if (this.nextSibling) {
        this.parentNode.insertBefore(elem, this.nextSibling);
    } else {
        this.parentNode.appendChild(elem);
    }
    return elem;
});

Element.addMethod('insertAfter', function(elem, after) {
    /* Like insertBefore
     */
    if (after.nextSibling) {
        this.insertBefore(elem, after.nextSibling);
    } else {
        this.appendChild(elem);
    }
});


/* === Event-handling functions ===
 *
 * *** I think there may be a cleaner way to do this using closures,
 * but it works fine as-is, and I haven't given it further thought.
 * *** These don't yet support adding multiple functions to a single
 * event. That's a must-have feature, so it'll definitely come.
 */

function addGlobalEvent(obj, name, func) {
  // This has to be first, because Opera has both but only attachEvent works how we want.
  if (obj.attachEvent) {
    obj.attachEvent("on" + name, func);
  } else if (obj.addEventListener) {
    obj.addEventListener(name, func, false);    // false means 'bubbling' -- i.e. sub-element events happen before getting passed on to their parents (as opposed to parents firing first).
  }
}

function removeGlobalEvent(obj, name, func) {
  if (document.attachEvent) { // See above
    obj.detachEvent("on" + name, func);
  } else if (document.addEventListener) {
    obj.removeEventListener(name, func, false);
  }
}

function eventTarget(e) {
  var obj;
  if (document.attachEvent) { // See above
    obj = e.srcElement;
  } else if (document.addEventListener) {
    obj = e.target;
  }
  return $(obj);
}

Element.addMethod('addEvent', function(name, func, toplevel) {
  /* Add an event to this element, but not to any objects deeper
   * in the hierarchy. If the element has other elements inside it,
   * then a click/event on the inside elements isn't guaranteed to
   * work properly, if at all.
   */
  addGlobalEvent(this, name, eventHandler);
  if (!this.events) {
    this.events = {};
  }
  if (! toplevel) toplevel = this;
  this.events[name] = [func, toplevel];
});

Element.addMethod('addDeepEvent', function(name, func) {
  /* Add an event to this element and all elements inside it.
   * This takes a wee bit longer (especially if you have a deep hierarchy)
   * so a) don't use it on complex element hierarchies and b) don't
   * use it very often. But don't be afraid to use it when you need to!
   */
    var descendants = this.$$('*');
    this.addEvent(name, func);
    for (var i=0; i<descendants.length; i++) {
        descendants[i].addEvent(name, func, this);
    }
});

Element.addMethod('removeEvent', function(name, func) {
  removeGlobalEvent(this, name, eventHandler);
  this.events[name] = null;
});

function eventHandler(e) {
  e = e || window.event;
  if (e.stopPropagation) e.stopPropagation();
  else e.cancelBubble = true;
  return eventTarget(e).fireEvent(e.type, e);
}

Element.addMethod('fireEvent', function (type, e) {
  if (!this.events) throw "No events defined for this object";
  if (!this.events[type]) throw "Event '" + type + "' not defined for this object";

  if (type=="mouseout" && e.toElement) e.relatedTarget = e.toElement;       // IE
  if (type=="mouseover" && e.fromElement) e.relatedTarget = e.fromElement;  // ditto

  return this.events[type][0].call(this.events[type][1], e);
});


function preventDefault(event) {
  if (event.preventDefault) event.preventDefault();
  else event.returnValue = false;
}

/* === Class and Attribute utility methods === */

Element.addMethod('classes', function() {
  return this.className.split(' ');
});

Element.addMethod('hasClass', function(cName) {
  /* Thanks to Fred Bird from http://fredbird.org */
  return new RegExp('\\b'+cName+'\\b').test(this.className);
});

Element.addMethod('removeClass', function(cName) {
  /* Thanks to Fred Bird from http://fredbird.org */
  if (!this.hasClass(cName)) {
    return false;
  }
  var rep = this.className.match(' '+cName) ? ' '+cName : cName;
  this.className = this.className.replace(rep, '');
  return true;
});

Element.addMethod('addClass', function(cName) {
  /* Thanks to Fred Bird from http://fredbird.org */
  if (!this.hasClass(cName)) {
    this.className += this.className ? ' '+cName : cName;
  }
  return true;
});

Element.addMethod('getElementsByAttribute', function(attribute, tag) {
  /* Thanks to Fred Bird from http://fredbird.org */
  // default tag to *
  tag = tag || '*';
  // listing container descendants
  var all = this.all || this.getElementsByTagName(tag);
  var found = [];
  // searching for targets
  for (var f=0; f<all.length; f++) {
    if ($(all[f]).getAttribute(attribute)) {
      found.push(all[f]);
    }
  }
  return found;
});
Element.addMethod('getElementsByClassName', function(className, tag) {
  /* Thanks to Fred Bird from http://fredbird.org */
  // default tag to *
  tag = tag || '*';
  // listing container descendants
  var all = this.getElementsByTagName(tag);
  var found = [];
  // searching for targets
  for (var f=0; f<all.length; f++) {
    if ($(all[f]).hasClass(className)) {
      found.push(all[f]);
    }
  }
  return found;
});


/* === AJAX === */

var ajaxTimeout = null;
var ajaxseq = 0;

function ajax(url, data, handler) {
  var req = false;
  ajaxseq += 1;
  ajaxseq_ = ajaxseq;

  if (window.XMLHttpRequest) {
    req = new window.XMLHttpRequest();  // Firefox, Safari, Opera
  }
  else if (window.ActiveXObject) {      // Internet Explorer
    try {
      req = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (err1) {
      try {
        req = new ActiveXObject("Microsoft.XMLHTTP");
      }
      catch (err2) {}
    }
  }
  if (!req) {
    throw('Browser does not support Ajax!');
  }
  
  if (handler != null) {
    req.onreadystatechange = function() { processreq(req, handler, ajaxseq_); };
    async = true;
  }
  else async = false;
  
  if (data) {
    req.open('POST', url, async);
    var datas = [];
    var i = 0;
    var idx;
    for (idx in data) {
      datas[i] = escape(idx) + '=' + escape(data[idx]);
      i++;
    }
    data = datas.join('&');
    req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    req.send(data);
  }
  else {
    req.open('GET', url, async);
    req.send(null);
  }
  ajaxTimeout = setTimeout(function() { timeoutHandler(req); }, 7000);
  if (async) return ajaxseq;
  else return processreq(req);
}

function unjsonize(str) {
  var dict;
  eval("dict = " + str);
  return dict;
}

function processreq(req, handler, seq) {
  if (req.readyState != 4) return;
  
  clearTimeout(ajaxTimeout);
  ajaxTimeout = null;
  var data;
  var stat;

  try {
    // Fx throws an exception when you access req.status for certain errors
    stat = req.status;
  }
  catch (err1) {
    stat = 500;
  }

  if (stat == 200 || stat == 403) {
    try {
      data = unjsonize(req.responseText);
    }
    catch (err2) {
      // crazy data, or possibly a formKey error
      stat = 777;
    }
  }
  if (stat != 777) {
    if (handler) handler(data, stat, req, seq);
    else return [data, stat, req];
  }
  else throw('Ajax error: ' + req.responseText);
}

function timeoutHandler(req) {
  req.abort();
}


/* === Dynamic element functions ===
 *
 * These do things like updating elements based on certain conditions,
 * or clearing the 'eg' text in an input field. Most of them are based
 * on custom attributes in the html tag as a way to pass parameters into
 * them. For example an input with example text looks like this:
 * <input type='text' eg='Type your name here...' />
 */

Element.addMethod('toggleother', function() {
    var elems = this.$$(this.getAttribute('totoggle'));
    for (var i=0; i<elems.length; i++) {
        elems[i].toggle();
    }
});

Element.addMethod('clearEmptyInputs', function() {
  /* Empty out all elements in a form that have example text in them */
  var elems = this.getElementsByTagName('*');
  var i;
  for (i=0; i<elems.length; i++) {
    if ($(elems[i]).hasClass('empty') && elems[i].getAttribute('eg')) {
      elems[i].value = '';
      elems[i].removeClass('empty');
    }
  }
});

Element.addMethod('removeEg', function() {
  if (this.hasClass('empty')) {
    if (this.hasClass('focus')) {               // Fix for IE to make sure the eg text doesn't go away the first time.
      this.removeClass('focus');                // Remove the 'focus' class, so we don't ever try to focus it again.
      this.addEvent('keydown', this.removeEg);  // Make sure the text is not marked as 'empty' when they start typing
    } else {
      this.value = '';
    }
    this.select();
    this.removeClass('empty');
  }
});


Element.addMethod('addEg', function() {
  if ((this.value == this.getAttribute('eg')) || (! this.value)) {
    this.addClass('empty');
    this.value = this.getAttribute('eg');
  }
});

Element.addMethod('updatevalue', function(val, changed) {
    var updatecode = this.getAttribute('updatecode');

    var newvalue = val;
    if (updatecode) {
        updatecode = updatecode.replace(/%v/g, 'val');
        updatecode = updatecode.replace(/this\.[a-z0-9]*/g, 'this.getAttribute("$1")');
        updatecode = updatecode.replace(/%c\.[a-z0-9]*/g, 'changed.getAttribute("$1")');
        updatecode = updatecode.replace(/%c/g, 'changed');
        newvalue = eval(updatecode);
    }
    this.firstChild.nodeValue = newvalue;
});

Element.addMethod('oncondition', function() {
    var toupdate = this.getAttribute('toupdate');
    var toappear = this.getAttribute('toappear');
    var toenable = this.getAttribute('toenable');
    var runcode = this.getAttribute('runcode');
    
    var condition = this.getAttribute('condition');
    var match = this.getAttribute('match');
    
    var val = this.getAttribute('val');
    if (! val) {
      if (this.type == 'checkbox' || this.type == 'radio') val = !!this.checked;
      else val = this.value;
    }
    
    if (match) {
        var re = new RegExp(match);
        condition = re.test(val);
    }
    else if (condition) {
        condition = condition.replace(/%v/g, 'val');
        condition = eval(condition);
    }
    else {
        condition = val;
    }
    
    if (toupdate) {
        if (condition) {
            elems = this.$$(toupdate);
            for (var i=0; i<elems.length; i++) {
                elems[i].updatevalue(val, this);
            }
        }
    }
    if (toappear) {
        elems = this.$$(toappear);
        if (condition) {
            for (var i=0; i<elems.length; i++) elems[i].show();
        }
        else {
            for (var i=0; i<elems.length; i++) elems[i].hide();
        }
    }
    if (toenable) {
        elems = this.$$(toenable);
        if (condition) {
            for (var i=0; i<elems.length; i++) elems[i].enable();
        }
        else {
            for (var i=0; i<elems.length; i++) elems[i].disable();
        }
    }
    if (runcode) eval(runcode);
});

Element.addMethod('drag', function(e) {
   /* Note: for some reason, you need to make sure that an element with
    * a drag() event atached has at least one space/character inside it,
    * otherwise (if it's completely empty), we get a selection dragging
    * instead of the element dragging, at least in Fx.
    */
   preventDefault(e);
   var elem = this;                         // make sure the closures work correctly
   var start = [e.clientX, e.clientY];
   var orig = [parseInt(elem.computedStyle()['left']), parseInt(elem.computedStyle()['top'])];
   var offset = [e.clientX-this.offsetLeft, e.clientY-this.offsetTop]
   var testover = $$('.name');              // Calculating this outside the move() function is _much_ quicker
   var move = function(event) {
       preventDefault(event);
       var over = elem.overlapping(testover);
       elem.style.left = (orig[0] + (event.clientX - start[0])) + "px";
       elem.style.top = (orig[1] + (event.clientY - start[1])) + "px";
       return over;
   };
   var up = function() {
       removeGlobalEvent(document, 'mousemove', move);
       removeGlobalEvent(document, 'mouseup', up);
       var tl = elem.topleft();
       elem.fireEvent('drop');
    };
   addGlobalEvent(document, 'mousemove', move);
   addGlobalEvent(document, 'mouseup', up);
});


/* === Money display functions === */

function gstcents(amount) {
  /* return a string formatted as a price, based on amount+GST
   */
  return cents(addgst(amount));
}

function addgst(amount) {
  /* Returns $amount including gst.
   * Expects GST to be defined
   */
  return amount + amount*GST;
}

function cents(amount) {
  /* return a string formatted as a price, with
   * the fractional part rounded to two cent columns
   */
  return amount.toFixed(2);
}

function unmungeEmail() {
  var email = this.href.replace('mailto:', '');
  var text = this.firstChild.nodeValue;
  var titleWords = this.title.split(' ');
  var missing = titleWords[titleWords.length-1];
  email = email.replace(/\.\.\.*/, missing);
  this.href = "mailto:" + email;
  this.title = 'Click to email';
  // I cannot fathom why IE changes nodeValue without this, but it seems to!
  this.firstChild.nodeValue = email;
}

/* === Misc functions ===
 *
 * *** These functions probably don't work, and if they do, they
 * need a major tidyup. Consider them obselete.
 */

var currentTooltip = null;
var currentKeepVisible = null;

function popupTooltip(obj) {
  var tooltip = document.getElementById(obj.getAttribute('tooltip'));
  currentTooltip = tooltip;
  var keepvisible;
  var vId = tooltip.getAttribute('keepvisible');
  if (!vId) {
    keepvisible = obj;
  } else {
    keepvisible = document.getElementById(vId);
  }
  currentKeepVisible = keepvisible;
  addGlobalEvent(keepvisible, 'mouseout', hideTooltip);
  tooltip.style.display = 'block';
}

function hideTooltip(e) {
  var relatedTarget;
  if (e.relatedTarget) {
    relatedTarget = e.relatedTarget;
  } else if (e.toElement) {
    relatedTarget = e.toElement;
  }
  if (contains(currentKeepVisible, relatedTarget)) {
    return false;
  }
  if (relatedTarget == currentKeepVisible) {
    return false;
  }
  currentTooltip.style.display = 'none';
  removeGlobalEvent(currentKeepVisible, 'mouseout', hideTooltip);
  currentTooltip = null;
  currentKeepVisible = null;
  return true;
}

function flashDiv(id, state) {
  var div = document.getElementById(id);
  if (state == 6) {
    return;
  }
  if (state%2 == 0) {
    removeClass(div, 'flash-off');
    removeClass(div, 'flash');
    addClass(div, 'flash');
  } else {
    removeClass(div, 'flash');
    addClass(div, 'flash-off');
  }
  state += 1;
  var code = 'flashDiv("' + id + '", ' + state + ');';
  setTimeout(code, 250);
}

function submitLink(obj) {
  if (obj.nodeName.toLowerCase() != 'a') {
    return;
  }
  var form = ancestor(obj, 'form');
  if (form) {
    form.submit();
  }
}

function rollover(obj, postfix) {
  var img;
  if (obj.nodeName.toLowerCase() == 'img') {
    img = obj;
  } else {
    img = obj.getElementsByTagName('img')[0];
  }

  var altsrc = img.getAttribute('rollover');
  if (!img.origsrc) {
    img.origsrc = img.src;
  }
  if (!altsrc) {
    var parts = img.origsrc.split('.');
    altsrc = parts[0];
    var j;
    for (j = 1; j < (parts.length - 1); j++) {
      altsrc = altsrc + '.' + parts[j];
    }
    altsrc = altsrc + postfix + '.' + parts[parts.length - 1];
  }
  img.src = altsrc;
}

function rollout(obj) {
  var img;
  if (obj.nodeName.toLowerCase() == 'img') {
    img = obj;
  } else {
    img = obj.getElementsByTagName('img')[0];
  }

  var origsrc = img.origsrc;
  img.src = origsrc;
}

function charsLeft(textarea) {
  var maxlength = parseInt(textarea.getAttribute('maxlength'), 10);
  var countdown = document.getElementById(textarea.getAttribute('countdown'));
  textarea.value = textarea.value.substring(0, maxlength);
  countdown.firstChild.nodeValue = maxlength - textarea.value.length;
}


