var GLOBALS = GLOBALS || {};
Object.extend(Array.prototype, {
    remove: function (item) {
        var index = this.indexOf(item);
        return (index == -1) ?
            undefined :
            this.splice(index, 1).first();
    }
});
var Cookie = {
    // load the existing cookies
    cookies:  new Hash(),

    // convenience fields for setting common expiration dates
    EXPIRES_NOW:       0,
    EXPIRES_SESSION:   0,
    EXPIRES_HOUR:      3600000,
    EXPIRES_DAY:       86400000,
    EXPIRES_WEEK:      604800000,
    EXPIRES_TWO_WEEKS: 1209600000,
    EXPIRES_MONTH:     2592000000,
    EXPIRES_YEAR:      31536000000,
    EXPIRES_NEVER:     315360000000,

    load: function () {
        var h = new Hash();
        if (document.cookie) {
            document.cookie.split('; ').invoke('split', '=').each(function (cookie) {
                h.set(cookie[0], {value: decodeURIComponent((cookie[1] || "").sub("+", " "))})
            });
        }
        this.cookies = h;
    },

    get: function (name) {
        return Cookie.exists(name) ? 
            this.cookies.get(name).value :
            undefined;
    },

    getAll: function () {
        return this.cookies;
    },

    /**
     * Sets a Cookie with the given name and value.
     *
     * name       Name of the cookie
     * value      Value of the cookie
     * [expires]  Milliseconds from now when the cookie will expire
     *            (default: end of current session)
     * [path]     Path where the cookie is valid
     *            (default: path of calling document)
     * [domain]   Domain where the cookie is valid
     *            (default: domain of calling document)
     * [secure]   Boolean value indicating if the cookie transmission
     *            requires a secure transmission
     */
    set: function (name, value, expires, path, domain, secure) {
        var cookie = name + '=' + value;
        if (expires) cookie += '; expires' + (new Date((new Date()).getTime() + expires)).toGMTString();
        if (path)    cookie += '; path='   + path;
        if (domain)  cookie += '; domain'  + domain;
        if (secure)  cookie += '; secure'  + secure;
        // set document cookie
        document.cookie = cookie;
        // set cookie hash keyed to name
        this.cookies.set(name, {
            value:   value,
            expires: expires,
            path:    path,
            domain:  domain,
            secure:  secure
        });
    },

    unset: function (name) {
        if (this.exists(name)) {
            var opts   = this.cookies.get(name);
            var cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT';
            if (opts.path)   cookie += '; path='  + opts.path;
            if (opts.domain) cookie += '; domain' + opts.domain;
            if (opts.secure) cookie += '; secure' + opts.secure;
            // unset document cookie (by setting a long-past expiration date)
            document.cookie = cookie;
            // unset cookie hash
            this.cookies.unset(name);
        }
    },

    exists: function (name) {
        return Object.isUndefined(this.cookies.get(name)) ?
            false :
            true;
    }
};
Cookie.load();
Object.extend(Date, {
    DAY_LONG_UC:         0,
    DAY_LONG_LC:         1,
    DAY_LONG_UCFIRST:    2,
    DAY_SHORT_UC:        3,
    DAY_SHORT_LC:        4,
    DAY_SHORT_UCFIRST:   5,
    
    MONTH_LONG_UC:       0,
    MONTH_LONG_LC:       1,
    MONTH_LONG_UCFIRST:  2,
    MONTH_SHORT_UC:      3,
    MONTH_SHORT_LC:      4,
    MONTH_SHORT_UCFIRST: 5,

    PRETTY_FORMAT: new Template("#{month} #{day}, #{year} at #{hours}:#{minutes} #{ampm}"),


    /*
     * The parseDateTime method takes a mysql, xml, or json date string
     * (such as "YYYY-MM-DD hh:mm:ss") and returns the number
     * of milliseconds since January 1, 1970, 00:00:00 (local time).
     * This function is useful for setting date values based on string
     * values, for example in conjunction with the setTime method and
     * the Date object.  This is based on the static Date.parse(string) method.
     * 
     */
    parseDateTime: function (datetime) {
        if (datetime) {
            var vals = datetime.match(/^(\d{4})-(\d{2})-(\d{2})[tT ](\d{2}):(\d{2}):(\d{2})$/);
            return Date.UTC(vals[1], vals[2] - 1, vals[3], vals[4], vals[5], vals[6]);
        } else {
            return undefined;
        }
    }
});

Object.extend(Date.prototype, {
    // convert a numeric month (0-11) to the name
    // either short (Jan, Feb, Mar) or long (January, February, March)
    getMonthName: function (style) {
        var name;
        // assign name
        switch (this.getMonth()) {
            case 0:  name = 'January';   break;
            case 1:  name = 'February';  break;
            case 2:  name = 'March';     break;
            case 3:  name = 'April';     break;
            case 4:  name = 'May';       break;
            case 5:  name = 'June';      break;
            case 6:  name = 'July';      break;
            case 7:  name = 'August';    break;
            case 8:  name = 'September'; break;
            case 9:  name = 'October';   break;
            case 10: name = 'November';  break;
            case 11: name = 'December';  break;
            default: break;
        }
        // convert name
        switch (style) {
            case Date.MONTH_LONG_UC:
                name = name.toUpperCase();
                break;
            case Date.MONTH_LONG_LC:
                name = name.toLowerCase();
                break;
            case Date.MONTH_LONG_UCFIRST:
                name = name;
                break;
            case Date.MONTH_SHORT_UC:
                name = name.toUpperCase().slice(0, 3);
                break;
            case Date.MONTH_SHORT_LC:
                name = name.toLowerCase().slice(0, 3);
                break;
            case Date.MONTH_SHORT_UCFIRST:
            default:
                name = name.slice(0, 3);
                break;
        }
        // return name
        return name;
    },

    // convert a numeric day (0-6) to the name
    // either short (Sun, Mon, Tue) or long (Sunday, Monday, Tuesday)
    getDayName: function (style) {
        var name;
        // assign name
        switch (this.getDay()) {
            case 0:  name = 'Sunday';    break;
            case 1:  name = 'Monday';    break;
            case 2:  name = 'Tuesday';   break;
            case 3:  name = 'Wednesday'; break;
            case 4:  name = 'Thursday';  break;
            case 5:  name = 'Friday';    break;
            case 6:  name = 'Saturday';  break;
            default: break;
        }
        // convert name
        switch (style) {
            case Date.DAY_LONG_UC:
                name = name.toUpperCase();
                break;
            case Date.DAY_LONG_LC:
                name = name.toLowerCase();
                break;
            case Date.DAY_LONG_UCFIRST:
                name = name.toLowerCase();
                break;
            case Date.DAY_SHORT_UC:
                name = name.toUpperCase().slice(0, 3);
                break;
            case Date.DAY_SHORT_LC:
                name = name.toLowerCase().slice(0, 3);
                break;
            case Date.DAY_SHORT_UCFIRST:
            default:
                name = name.toLowerCase().slice(0, 3);
                break;
        }
        // return name
        return name;
    },

    toPrettyString: function () {
        var year    = this.getFullYear();
        var month   = this.getMonthName(Date.MONTH_LONG_UCFIRST);
        var day     = this.getDate().toString();
        var hours   = (this.getHours() % 12).toString();
        var minutes = this.getMinutes().toString();
        var seconds = this.getSeconds().toString();
        var ampm    = this.getHours() < 12 ? 'AM' : 'PM';

        return Date.PRETTY_FORMAT.evaluate({
            year:    year,
            month:   month,
            day:     day,
            hours:   (hours.length > 1   ? hours   : "0" + hours),
            minutes: (minutes.length > 1 ? minutes : "0" + minutes),
            seconds: (seconds.length > 1 ? seconds : "0" + seconds),
            ampm:    ampm
        });
    }
});
Element.addMethods({
    setId: function (el, id) {
        el = $(el);
        el.id = id;
        return el;
    },

    // positioning
    moveTo: function (el, left, top) {
        el = $(el);
        el.setStyle({
            left: Object.isString(left) ? left : left + 'px',
            top:  Object.isString(top)  ? top  : top  + 'px'
        });
        return el;
    },

    setLeft: function (el, left) {
        el = $(el)
        el.setStyle({
            left: Object.isString(left) ? left : left + 'px'
        });
        return el;
    },

    setTop: function (el, top) {
        el = $(el)
        el.setStyle({
            top: Object.isString(top)  ? top  : top + 'px'
        });
        return el;
    },

    setDimensions: function (el, width, height) {
        el = $(el);
        el.setStyle({
            width:  Object.isString(width)  ? width  : width  + 'px',
            height: Object.isString(height) ? height : height + 'px'
        });
        return el;
    },

    setWidth: function (el, width) {
        el = $(el)
        el.setStyle({
            width: Object.isString(width) ? width : width + 'px'
        });
        return el;
    },

    setHeight: function (el, height) {
        el = $(el)
        el.setStyle({
            height: Object.isString(height) ? height : height + 'px'
        });
        return el;
    },

    // get positioning
    getPosition: function (el) {
        return $(el).cumulativeOffset();
    },

    getLeft: function (el) {
        return $(el).getPosition().left;
    },

    getRight: function (el) {
        el = $(el);
        return el.getLeft() + el.getWidth();
    },

    getTop: function (el) {
        return $(el).getPosition().top;
    },

    getBottom: function (el) {
        el = $(el);
        return el.getTop() + el.getHeight();
    },

    // checks to see if a point or element is completely contained by this element
    // points are of the form {x: 0, y: 0}
    surrounds: function(el, other) {
        el = $(el);

        // is the other element fully contained within our bounding rectangle?
        if (Object.isElement(other)) {
            other = $(other);
            return (other.getLeft()   >= el.getLeft()  &&
                    other.getRight()  <  el.getRight() &&
                    other.getTop()    >= el.getTop()   &&
                    other.getBottom() <  el.getBottom());

        // is a point within our bounding rectangle?
        } else {
            return (other.x >= el.getLeft()  &&
                    other.x <  el.getRight() &&
                    other.y >= el.getTop()   &&
                    other.y <  el.getBottom());
        }
      },

    // the contrary of surrounds
    inhabits: function (el, other) {
        return Element.surrounds(other, el);
    },

    // checks if "el" has some area within "other"
    overlaps: function (el, other) {
        el     = $(el);
        other  = $(other);
        var tl = {x: el.getLeft(),  y: el.getTop()};
        var br = {x: el.getRight(), y: el.getBottom()};
        return other.surrounds(tl) || other.surrounds(br);
    },

    // the contrary of overlaps
    // checks if "other" has some area within "el"
    underlies: function (el, other) {
        return Element.overlaps(other, el);
    },

    // event handling
    setObserver: function (el, name, event, func, context) {
        el = $(el);
        if (! el._widget_observers) el._widget_observers = new Hash();
        func = func.bindAsEventListener(context || el);
        el._widget_observers.set(name, {
            event:     event,
            func:      func,
            observing: true
        });
        Event.observe(el, event, func);
        return el;
    },

    getObserver: function (el, name) {
        if (! el._widget_observers) el._widget_observers = new Hash();
        return el._widget_observers.get(name);
    },

    stopObserver: function (el, name) {
        el = $(el);
        if (! el._widget_observers) el._widget_observers = new Hash();
        if (el.isObserving(name)) {
            var observer = el._widget_observers.get(name);
            el._widget_observers.set(name, {
                event:     observer.event,
                func:      observer.func,
                observing: false
            });
            Event.stopObserving(el, observer.event, observer.func);
        }
        return el;
    },

    startObserver: function (el, name) {
        el = $(el);
        if (! el._widget_observers) el._widget_observers = new Hash();
        if (! el.isObserving(name)) {
            var observer = el._widget_observers.get(name);
            el._widget_observers.set(name, {
                event:     observer.event,
                func:      observer.func,
                observing: true
            });
            Event.observe(el, observer.event, observer.func);
        }
       return el;
    },

    isObserving: function (el, name) {
        if (! el._widget_observers) el._widget_observers = new Hash();
        var observer = el._widget_observers.get(name);
        return observer ? observer.observing : false;
    }
});

Object.extend(document, {
  setObserver:   Element.Methods.setObserver.methodize(),
  getObserver:   Element.Methods.getObserver.methodize(),
  stopObserver:  Element.Methods.stopObserver.methodize(),
  startObserver: Element.Methods.startObserver.methodize(),
  isObserving:   Element.Methods.isObserving.methodize()
});

var Exception = Class.create({
    initialize: function (message) {
        this.message = message;
    }
});

// Signals that an I/O exception of some sort has occurred.
var IOException = Class.create(Exception, {});

// Thrown to indicate that a method has been passed an illegal or inappropriate argument.
var IllegalArgumentException = Class.create(Exception, {});

// Thrown when an email address is malformed.
var MalformedEmailAddressException = Class.create(Exception, {});

// Thrown when a password does not meet our minimum strength requirements.
var PasswordStrengthException = Class.create(Exception, {});

// An exception that provides information on a database access error or other errors.
var SQLException = Class.create(Exception, {});

// Thrown when a concrete class does not override an abstract field of an abstract superclass
var OverrideException = Class.create(Exception, {});

/*** Not used ***
 *** see Java 1.6. documentation for more details ***
// Unchecked exception thrown when an attempt is made to connect a channel that is already connected.
var AlreadyConnectedException = Class.create(Exception, {});

// Thrown when an exceptional arithmetic condition has occurred.
var ArithmeticException = Class.create(Exception, {});

// Thrown to indicate that an attempt has been made to store the wrong type of object into an array of objects.
var ArrayStoreException = Class.create(Exception, {});

// Unchecked exception thrown when a relative put operation reaches the target buffer's limit.
var BufferOverflowException = Class.create(Exception, {});

// Unchecked exception thrown when a relative get operation reaches the source buffer's limit.
var BufferUnderflowException = Class.create(Exception, {});

// Signals that a method has been invoked at an illegal or inappropriate time.
var IllegalStateException = Class.create(Exception, {});

// Thrown to indicate that an index of some sort (such as to an array, to a string, or to a vector) is out of range.
var IndexOutOfBoundsException = Class.create(Exception, {});

// This runtime exception is thrown to indicate that a method parameter which was expected to be an item name of a composite data or a row index of a tabular data is not valid.
var InvalidKeyException = Class.create(Exception, {});

// Thrown to indicate that a malformed URL has occurred. Either no legal protocol could be found in a specification string or the string could not be parsed.
var MalformedURLException = Class.create(Exception, {});

// Thrown if an application tries to create an array with negative size.
var NegativeArraySizeException = Class.create(Exception, {});

// Thrown by the nextElement method of an Enumeration to indicate that there are no more elements in the enumeration.
var NoSuchElementException = Class.create(Exception, {});

// Signals that the class doesn't have a field of a specified name.
var NoSuchFieldException = Class.create(Exception, {});

// Thrown when a particular method cannot be found.
var NoSuchMethodException = Class.create(Exception, {});

// Unchecked exception thrown when an attempt is made to invoke an I/O operation upon a channel that is not yet connected.
var NotYetConnectedException = Class.create(Exception, {});

// Thrown when an application attempts to use null in a case where an object is required.
var NullPointerException = Class.create(Exception, {});

// Signals that an error has been reached unexpectedly while parsing.
var ParseException = Class.create(Exception, {});

// Signals that the requested operation does not support the requested data type.
var UnsupportedDataTypeException = Class.create(Exception, {});

// Exception thrown when a blocking operation times out.
var TimeoutException = Class.create(Exception, {});

// This exception is thrown when an XML formatted string is being parsed.
var XMLParseException = Class.create(Exception, {});
***/

var Log = {
    proxy: GLOBALS['HTTP_PROXY'] ? GLOBALS['HTTP_PROXY'] + 'Log.php' : '/proxy/Log.php',

    _log: function (type, error) {
        // what shall we send?
        var params = {
            type:     type,
            location: error.stack.trim().split('\n')[2].split('@')[1],
            message:  error.message
        };
        // ship off the message to be logged on the server
        new Ajax.Request(Log.proxy, {
            method:     'post',
            parameters: params
        });
        // local debug string
        if (GLOBALS['DEBUG'] && console)
            console.log(type + '\n' + params.location + '\n' + params.message);
    },

    log: function (msg) {
        Log._log('log', new Error(msg));
    },

    warning: function (msg) {
        Log._log('warning', new Error(msg));
    },

    error: function (msg) {
        Log._log('error', new Error(msg));
    },

    event: function (msg) {
        Log._log('event', new Error(msg));
    }
};

Ajax.Responders.register({
    onException: function (request, error) {
        Log._log('ajax exception', error);
    }
});
var Query = {
    params: new Hash(location.search.toQueryParams()),
    
    get: function (param) {
        return this.params.get(param);
    },

    getAll: function () {
        return this.params;
    },

    set: function (key, value) {
        this.params.set(key, value);
    },

    unset: function (key) {
        this.params.unset(key);
    },

    reset: function () {
        this.params = new Hash(location.search.toQueryParams());
    },

    clear: function () {
        this.params = new Hash();
    },

    toString: function () {
        return this.params.toQueryString();
    },

    inspect: function () {
        return this.params.inspect();
    }
};
Object.extend(String, {
    // pad a string to a given length with a pad character
    PAD_LEFT:  0,
    PAD_RIGHT: 1
});


Object.extend(String.prototype, {
    pad: function (padChar, length, side) {
        var diff = length - this.length;
        padChar  = Object.isUndefined(padChar) ? ' ' : padChar;
        side     = Object.isUndefined(side) ? String.PAD_LEFT : side;
        if (diff <= 0)
            return this.toString();
        else {
            var padding = padChar.times(diff);
            return (side == String.PAD_RIGHT) ?
                this + padding :
                padding + this ;
        }
    }
});
var Valid = {
    email: function (str) {
        var re = /^[\w\.-]+@[\w\.-]+\.[a-z]{2,4}$/;
        return re.test(str);
    },

    url: function (str) {
        var re = /^(https?:\/\/)?[\w\.-]+\.[a-z]{2,4}(\/\S*)?$/;
        return re.test(str);
    }
};
var Widget = Class.create({
    name:  'Widget',

    initialize: function (domElement) {
        // for new elements use initialize(document.createElement('div'))
        this.element = $(domElement);
        this.id      = this.element.identify();
        this.proxy   = GLOBALS['HTTP_PROXY'] ?
            GLOBALS['HTTP_PROXY'] + this.name + '.php' :
            '/proxy/' + this.name + '.php';

        // add css styles
        this.element.addClassName(this.name)
        this.getSuperclasses().each(function (s) {
            this.element.addClassName(s.name);
        }, this);
        
        // set later by this.request()
        this.ajaxResponse = null;
    },

    //*** basic widget info
    getId: function () {
        return this.id;
    },

    setId: function (id) {
        this.id = id;
        this.element.id = id;
    },

    getSuperclass: function () {
        return this.constructor.superclass;
    },

    getSuperclasses: function () {
        var supers = new Array();
        var obj = this.constructor.superclass;
        while (obj) {
            supers.push(obj.prototype);
            obj = obj.superclass;
        }
        return supers;
    },

    //*** event handling
    setObserver: function (name, event, func, context) {
        return this.element.setObserver(name, event, func, context || this);
    },

    getObserver: function (name) {
        return this.element.getObserver(name);
    },

    stopObserver: function (name) {
        return this.element.stopObserver(name);
    },

    startObserver: function (name) {
        return this.element.startObserver(name);
    },

    isObserving: function (name) {
        return this.element.isObserving(name);
    },

    //*** ajax methods
    request: function (params, silent) {
        new Ajax.Request(this.proxy, {
            method:      'post',
            parameters:  params,
            onSuccess:   silent ? null : this.onAjaxSuccess.bind(this),
            onFailure:   silent ? null : this.onAjaxFailure.bind(this),
            onException: silent ? null : this.onAjaxException.bind(this),
            onComplete:  silent ? null : this.onAjaxComplete.bind(this)
        });
    },

    onAjaxSuccess: function (transport) {
        // store response for later use
        if (transport.responseJSON)
            this.ajaxResponse = transport.responseJSON;
        else if (transport.responseXML)
            this.ajaxResponse = transport.responseXML.documentElement;
        else if (transport.responseText)
            this.ajaxResponse = transport.responseText;
        else
            this.ajaxResponse = null;
    },

    onAjaxFailure: function (transport) {
    },

    onAjaxException: function (request, error) {
    },

    onAjaxComplete: function (transport) {
    },

    //*** react to user login/logout if called
    // off by default to avoid registering hundreds of unobserved handlers on document
    // easy to turn on if a specific widget needs it
    observeLogin: function () {
        document.setObserver(this.id + 'Login',  'widget:login',  this.login,  this);
        document.setObserver(this.id + 'Logout', 'widget:logout', this.logout, this);
    },

    // called when a "widget:login" event fires
    // event.memo = {error: 0, userId: 1, username: "name"}
    login: function (event) {
        document.stopObserver(this.id + 'Login');
        document.startObserver(this.id + 'Logout');
    },

    // called when a "widget:logout" event fires
    logout: function (event) {
        document.stopObserver(this.id + 'Logout');
        document.startObserver(this.id + 'Login');
    },

    //*** output
    inspect: function () {
        return this.element.inspect();
    },

    toElement: function () {
        return this.element;
    },

    //*** convenience methods pass through to element
    moveTo: function (left, top) {
        this.element.moveTo(left, top);
    },

    show: function () {
        this.element.show();
    },

    hide: function () {
        this.element.hide();
    }
});

Object.extend(Widget, {
    contain: function (element) {
        var container = $("_widget_container");
        // attempt to create the widget container if it does not exist
        if (! Object.isElement(container)) {
            container    = $(document.createElement('div'));
            container.id = "_widget_container";
            document.body.insert({top: container});
        }
        // insert Widget element
        if (element instanceof Widget) {
            container.insert(Widget.element);
        // insert dom element
        } else if (Object.isElement(element)) {
            container.insert(element);
        }
        return element;
   }
});
var Button = Class.create(Widget, {
    name: 'Button',

    initialize: function ($super, domElement, value) {
        // setup
        $super(domElement);
        this.selected = false;
        this.value    = value;

        // watch for mouse events
        this.setObserver('buttonClick',     'click',     this.click);
        this.setObserver('buttonMouseOver', 'mouseover', this.mouseOver);
        this.setObserver('buttonMouseOut',  'mouseout',  this.mouseOut);
    },

    isSelected: function () {
        return this.selected;
    },

    getValue: function () {
        return this.value;
    },

    setValue: function (value) {
        this.value = value;
    },

    click: function (event) {
        // pass click through to proper method based on button state
        this[this.selected ? 'deselect' : 'select'](event);
    },

    select: function (event) {
        this.selected = true;
        // add styles
        this.element.addClassName('selected');
        this.element.removeClassName('mouseover');
        // stop observing other mouse events
        this.stopObserver('buttonMouseOver');
        this.stopObserver('buttonMouseOut');
    },

    deselect: function (event) {
        this.selected = false;
        // add styles
        this.element.removeClassName('selected');
        // restart observing for other mouse events
        this.startObserver('buttonMouseOver');
        this.startObserver('buttonMouseOut');
    },

    mouseOver: function (event) {
        // add styles
        this.element.addClassName('mouseover');
    },

    mouseOut: function (event) {
        // add styles
        this.element.removeClassName('mouseover');
    }
});
var PushButton = Class.create(Button, {
    name: 'PushButton',

    select: function (event) {
    },

    deselect: function (event) {
    }
});
var Tab = Class.create(Button, {
    name: 'Tab',

    initialize: function ($super, buttonElement, contentElement) {
        // setup
        $super(buttonElement);
        this.stopObserver('buttonClick');
        // create dom elements
        this.button  = this.element;
        if (! Object.isArray(contentElement))
            contentElement = [contentElement];
        this.content = contentElement.map(function (c) { return $(c); });
        // hide the content
        // DO NOT use this.hide, which is overridden in some subclasses
        this.content.invoke('hide');
    },

    getButton: function () {
        return this.button;
    },

    getContent: function () {
        return this.content;
    },

    hide: function () {
        this.deselect();
        this.content.invoke('hide');
        this.startObserver('tabSetClick');
    },

    show: function () {
        this.select();
        this.content.invoke('show');
        this.stopObserver('tabSetClick');
    },

    disappear: function () {
        this.deselect();
        this.content.invoke('hide');
        this.startObserver('tabSetClick');
    },

    appear: function () {
        this.select();
        this.content.invoke('show');
        this.stopObserver('tabSetClick');
    }
});
var TabSet = Class.create(Widget, {
    name: 'TabSet',

    initialize: function ($super, domElement, tabs, selected) {
        // setup
        $super(domElement);
        this.tabs     = new Array();
        this.selected = null;

        // add the initial tabs
        tabs = tabs || new Array();
        if (Object.isArray(tabs))
            tabs.each(function (tab) { this.add(tab); }, this);
        else
            $H(tabs).map(function (pair) { this.add(new Tab(pair.key, pair.value)); }, this);

        // select a tab
        // return if prohibited from selecting anything
        if (selected === false)
            void(0);
        // try selecting a specific tab by index
        else if (Object.isNumber(selected) && this.tabs[selected])
            this.select(null, this.tabs[selected]);
        // try using the tab itself
        else if (selected instanceof Tab)
            this.select(null, selected);
        // select from the cookie if one exists
        else if (Object.isUndefined(selected) && Cookie.exists(this.id + '_selected'))
            Cookie.get(this.id + '_selected').split(',').without('').each(function (i) {
                this.select(null, this.tabs[parseInt(i)], true);
            }, this);
        // finally, select the first tab
        else if (this.tabs[0])
            this.select(null, this.tabs[0]);
    },

    get: function (index) {
        return this.tabs[index];
    },

    getAll: function () {
        return this.tabs;
    },

    add: function (tab) {
        // watch for clicks
        tab.setObserver('tabSetClick', 'click', this.select.bindAsEventListener(this, tab), this);
        // add to array
        this.tabs.push(tab);
    },

    remove: function (tab) {
        var index = this.tabs.indexOf(tab);
        // remove from array
        this.tabs.splice(index, 1);
        // if this tab was selected, make the first tab selected
        if (tab.selected) this.select(null, this.tabs[0]);
        // remove from the dom
        tab.getButton().remove();
        tab.getContent().invoke('remove');
        // return the tab
        return tab;
    },

    select: function (event, tab, shouldAppear) {
        // deselect old tab
        if (this.selected) this.selected.hide();
        // select new tab
        this.selected = tab;
        this.element.scrollTop = 0;
        tab[shouldAppear ? 'appear' : 'show']();
        // save selected tab as a cookie
        Cookie.set(this.id + '_selected', this.tabs.indexOf(tab), Cookie.EXPIRES_SESSION)
        // fire height change
        document.fire('widget:contentChange');
    },

    next: function () {
        var index = this.tabs.indexOf(this.selected);
        var next  = Math.min(this.tabs.length - 1, index + 1);
        this.select(null, this.tabs[next]);
    },

    prev: function () {
        var index = this.tabs.indexOf(this.selected);
        var prev  = Math.max(0, index - 1);
        this.select(null, this.tabs[prev]);
    }
});
var Accordion = Class.create(TabSet, {
    name: 'Accordion',

    initialize: function ($super, domElement, panelElements, selected) {
        var panels = $H(panelElements).map(function (pair) {return new Accordion.Panel(pair.key, pair.value)});
        $super(domElement, panels, selected);
    },

    setSlideUpTime: function (time) {
        this.tabs.invoke('setSlideUpTime', time);
    },

    setSlideDownTime: function (time) {
        this.tabs.invoke('setSlideDownTime', time);
    },

    setSlideTime: function (time) {
        this.setSlideUpTime(time);
        this.setSlideDownTime(time);
    }
});

Accordion.Panel = Class.create(Tab, {
    name:          'AccordionPanel',
    slideUpTime:   1.0,
    slideDownTime: 1.0,

    // Note: the scriptaculous slider insists upon this arbitrary wrapper <div>
    // around the panel content. So 'proper' HTML would be
    // <div id='panel1'><div>blah blah blah</div></div>
    hide: function () {
        this.getContent().each(function (c) {
            Effect.SlideUp(c.id, {
                duration:    this.slideUpTime,
                beforeStart: function () {
                    this.stopObserver('tabSetClick');
                    this.element.addClassName('sliding');
                    this.deselect();
                }.bind(this),
                afterFinish: function () {
                    this.element.removeClassName('sliding')
                    this.startObserver('tabSetClick');
                }.bind(this)
            });
        }, this);
    },

    show: function () {
        this.getContent().each(function (c) {
            Effect.SlideDown(c.id, {
                duration:    this.slideDownTime,
                beforeStart: function () {
                    this.stopObserver('tabSetClick');
                    this.element.addClassName('sliding')
                }.bind(this),
                afterFinish: function () {
                    this.select();
                    this.element.removeClassName('sliding')
                    this.startObserver('tabSetClick');
                }.bind(this)
            });
        }, this);
    },

    disappear: function () {
        this.deselect();
        this.content.invoke('hide');
    },

    appear: function () {
        this.select();
        this.content.invoke('show');
    },

    setSlideUpTime: function (time) {
        this.slideUpTime = time;
    },

    setSlideDownTime: function (time) {
        this.slideDownTime = time;
    }
});
var MultiAccordion = Class.create(Accordion, {
    name: 'MultiAccordion',

    select: function (event, panel, shouldAppear) {
        this.selected = this.selected ? this.selected : [];
        if (panel.selected) {
            panel.hide();
            this.selected.remove(panel);
        } else {
            panel[shouldAppear ? 'appear' : 'show']();
            this.selected.push(panel);
        }
        // save selected tab as a cookie
        var indexes = this.selected.map(function (s) {
            return this.tabs.indexOf(s); 
        }, this);
        Cookie.set(this.id + '_selected', indexes.join(','), Cookie.EXPIRES_SESSION)
        // fire height change
        document.fire('widget:contentChange');
    }
});

var PanelSet = Class.create(Widget, {
    name: 'PanelSet',
    transitionTime: 0.6,

    initialize: function ($super, domElement, buttonsElement, selected) {
        // setup
        $super(domElement);
        this.panels   = new Array();
        this.selected = null;
        this.rotator  = null;

        // add buttons
        this.buttons = new ButtonSet(buttonsElement || document.createElement('div'), 0);

        // add the initial panels
        this.element.setStyle({
            overflow: 'hidden'
        });
        this.element.childElements().each(function (panel) {this.add(panel);}, this);

        // first try selecting a specific panel by index
        if (selected && Object.isNumber(selected) && this.panels[selected])
            this.select(selected);
        // otherwise select the first panel
        else if (this.panels[0])
            this.select(0);
    },

    get: function (index) {
        return this.panels[index];
    },

    getAll: function () {
        return this.panels;
    },

    setTransitionTime: function (time) {
        this.transitionTime = time;
    },

    add: function (panel) {
        // add button
        var button = this.buttons.add();
        button.setObserver('panelSetClick', 'click', this.select.curry(panel), this);
        this.buttons.element.up()[this.buttons.getAll().size() <= 1 ? 'hide' : 'show']();

        // add panel
        this.panels.push($(panel));
        panel.setStyle({
            'display':  'none',
            'position': 'absolute',
            'overflow': 'auto',
            'left':     this.element.getLeft(),
            'top':      this.element.getTop(),
            'width':    this.element.getWidth(),
            'height':   this.element.getHeight()
        });
    },

    remove: function (panel) {
        var index = this.panels.indexOf(panel);
        // remove button
        this.buttons.remove(this.buttons[index]);
        this.buttons.element.up()[this.buttons.getAll().size() <= 1 ? 'hide' : 'show']();
        // remove from array
        this.panels.splice(index, 1);
        // if this tab was selected, make the first tab selected
        if (panel.selected) this.select(this.panels[0]);
        // remove from the dom
        $(panel).remove();
        // return the panel
        return panel;
    },

    select: function (panel) {
        var index = Object.isNumber(panel) ? panel : this.panels.indexOf(panel);
        if (index < 0 || index >= this.panels.length) return;
        if (index == this.selected) return;
        // replace the old panel with the new one
        if (this.selected != null)
            this.hidePanel(this.selected);
        this.showPanel(index);
        // select the new panel
        this.selected = index;
        // select the new button
        var button = this.buttons.get(index);
        button.stopObserver('panelSetClick');
        if (this.buttons.getSelected())
            this.buttons.getSelected().startObserver('panelSetClick');
        this.buttons.select(button);
    },

    hidePanel: function (index) {
        this.panels[index].hide();
    },

    showPanel: function (index) {
        this.panels[index].show();
    },

    next: function (cycle) {
        if (cycle)
            this.select((this.selected + 1) % this.panels.length);
        else
            this.select(Math.min(this.panels.length - 1, this.selected + 1));
    },

    prev: function (cycle) {
        if (cycle)
            this.select((this.panels.length + this.selected - 1) % this.panels.length);
        else
            this.select(Math.max(0, this.selected - 1));
    },

    first: function () {
        this.select(this.panels.first());
    },

    last: function () {
        this.select(this.panels.last())
    },

    autorotate: function (delay) {
        if (this.rotator) this.rotator.stop();
        this.rotator = new PeriodicalExecuter(function (pe) {
            this.next(true);
        }.bind(this), delay);
    }
});

var FadingPanelSet = Class.create(PanelSet, {
    name: 'FadingPanelSet',

    hidePanel: function (index) {
        new Effect.Opacity(this.panels[index], {
            from:        1.0,
            to:          0.0,
            duration:    this.transitionTime,
            transition:  Effect.Transitions.sinoidal,
            delay:       0.0,
            afterFinish: function (effect) { effect.element.hide(); }
        });
    },

    showPanel: function (index) {
        new Effect.Opacity(this.panels[index], {
            from:       0.0,
            to:         1.0,
            duration:   this.transitionTime,
            transition: Effect.Transitions.sinoidal,
            delay:      0.0,
            beforeStart: function (effect) { effect.element.show(); }
        });
    }
});

var ImageOverlay = {
    // state variables
    isInitialized:  false,
    isImageLoaded:  false,
    isFadeComplete: false,

    // display options
    backgroundColor:   '#000000',
    backgroundOpacity: 0.8,
    fadeInTime:        0.6,
    fadeOutTime:       0.3,
    spinnerSrc:        GLOBALS['HTTP_IMAGES'] ? GLOBALS['HTTP_IMAGES'] + 'loading.gif' : '/images/loading.gif',
    verticalPosition:  30,

    initialize: function () {
        this.isInitialized = true;

        // setup wrapper element waaaaaay above other elements
        this.element = $(document.createElement('div'));
        this.element.setStyle({
            position: 'fixed',
            left:     '0px',
            top:      '0px',
            width:    '100%',
            height:   '100%',
            zIndex:   GLOBALS['Z_IMAGE_OVERLAY'] ? GLOBALS['Z_IMAGE_OVERLAY'] : 1000
        });
        this.element.hide();
        this.element.setObserver('click', 'click', this.hide, this);

        // setup opaque background
        this.background    = $(document.createElement('div'));
        this.background.id = 'ImageOverlayBackground';
        this.background.setStyle({
            backgroundColor: this.backgroundColor,
            position:        'fixed',
            left:            '0px',
            top:             '0px',
            width:           '100%',
            height:          '100%',
            opacity:         this.backgroundOpacity
        });

        // setup loading spinner
        this.spinner     = $(document.createElement('img'));
        this.spinner.setId('ImageOverlaySpinner');
        this.spinner.src = this.spinnerSrc;
        this.spinner.setStyle({
            display:  'none',
            position: 'fixed'
        });
        this.spinner.setObserver('load',  'load',  this.onSpinnerLoaded, this);

        // setup image
        this.image    = $(document.createElement('img'));
        this.image.setId('ImageOverlayImage');
        this.image.setStyle({
            display:  'none',
            position: 'fixed'
        });
        this.image.setObserver('load', 'load', this.onImageLoaded, this);

        // add elements to the dom
        document.body.appendChild(this.element);
        this.element.appendChild(this.background);
        this.element.appendChild(this.spinner);
        this.element.appendChild(this.image);
    },

    setBackgroundColor: function (color) {
        this.backgroundColor = color;
        this.background.setStyle({backgroundColor: color});
    },

    setBackgroundOpacity: function (opacity) {
        this.backgroundOpacity = opacity;
    },

    setFadeInTime: function (seconds) {
        this.fadeInTime = seconds;
    },

    setFadeOutTime: function (seconds) {
        this.fadeOutTime = seconds;
    },

    setSpinnerSrc: function (url) {
        this.spinnerSrc  = url;
        this.spinner.src = url;
    },

    setVerticalPosition: function (percent) {
        this.verticalPosition = percent;
    },

    setOptions: function (options) {
        if (options.backgroundColor)
            this.setBackgroundColor(options.backgroundColor);
        if (options.backgroundOpacity)
            this.setBackgroundOpacity(options.backgroundOpacity);
        if (options.fadeInTime)
            this.setFadeInTime(options.fadeInTime);
        if (options.fadeOutTime)
            this.setFadeOutTime(options.fadeOutTime);
        if (options.spinnerSrc)
            this.setSpinnerSrc(options.spinnerSrc);
        if (options.verticalPosition)
            this.setVerticalPosition(verticalPosition);
    },

    show: function (src, alt) {
        // lazy initialize the first time we're called
        if (! this.isInitialized) this.initialize();

        // set this before assigning image src or risk having a cached image appear before fading is complete
        this.isFadeComplete = false;

        // make all src paths into absolute src urls
        // the link.href property will contain the absolute url
        var link  = document.createElement("link");
        link.href = src;
        // load the image if necessary
        if (link.href != this.image.src) {
            this.isImageLoaded = false;
            this.image.src     = link.href;
            this.image.alt     = alt ? alt : "Overlay image";
        }

        // fade in to avoid a jarring surprise
        new Effect.Opacity(this.element, {
            from:        0.0,
            to:          1.0,
            duration:    this.fadeInTime,
            transition:  Effect.Transitions.sinoidal,
            delay:       0.0,
            beforeStart: function () {
                this.element.setStyle({opacity: 0.0});
                this.element.show();
                this.spinner.show();
            }.bind(this),
            afterFinish: this.onFadeComplete.bind(this)
        });
    },

    hide: function () {
        // fade out to avoid a jarring surprise
        new Effect.Opacity(this.element, {
            from:        1.0,
            to:          0.0,
            duration:    this.fadeOutTime,
            transition:  Effect.Transitions.sinoidal,
            delay:       0.0,
            afterFinish: function () {
                this.element.hide();
                this.image.hide();
                this.spinner.hide();
                this.element.setStyle({opacity: 1.0});
            }.bind(this)
        });
    },

    onFadeComplete: function () {
        this.isFadeComplete = true;

        // already loaded! show the image
        if (this.isImageLoaded) {
          this.spinner.hide();
          new Effect.Opacity(this.image, {
            from:        0.0,
            to:          1.0,
            duration:    this.fadeInTime,
            transition:  Effect.Transitions.sinoidal,
            delay:       0.0,
            beforeStart: function () {
                this.image.setStyle({opacity: 0.0});
                this.image.show();
            }.bind(this)
          });
        }
    },

    onSpinnerLoaded: function () {
        // center the new spinner
        var dims = this.image.up().getDimensions();
        this.spinner.setStyle({
            left:   ((dims.width - this.spinner.width) / 2) + 'px',
            top:    ((dims.height - this.spinner.height) * this.verticalPosition / 100) + 'px'
        });
        if (!this.isImageLoaded) this.spinner.show();
    },

    onImageLoaded: function () {
        this.isImageLoaded = true;
        // center the image
        var dims = this.image.up().getDimensions();
        this.image.setStyle({
            left:   ((dims.width - this.image.width) / 2) + 'px',
            top:    ((dims.height - this.image.height) * this.verticalPosition / 100) + 'px'
        });
        // if the fade is done, disable the spinner and show the image
        if (this.isFadeComplete) {
            this.spinner.hide();
            this.image.show();
        }
    }
};
// XXX: why doesn't this work?
// XXX: if it does then I can eliminate the lazy loading in show
//document.observe("dom:loaded", ImageOverlay.initialize());
var Popup = Class.create(Button, {
    name:       'Popup',

    initialize: function ($super, buttonElement, contentElement, alignment, hasOverlay) {
        // setup with bound observers
        $super(buttonElement);

        // setup button
        this.button = this.element;

        // setup opaque background
        this.hasOverlay = Object.isUndefined(hasOverlay) ? false : hasOverlay;
        
        // align content on the page
        this.alignment = Object.isUndefined(alignment) ? Popup.ALIGN_WINDOW_CENTER : alignment;
        this.aligned   = false;

        // watch for changes in the document that could screw up alignment
        document.setObserver(this.id + 'Resize',     'resize',            this.resize, this);
        document.setObserver(this.id + 'FontResize', 'widget:fontresize', this.resize, this);

        contentElement = $(contentElement);
        // setup using existing content ...
        if (Object.isElement(contentElement)) {
            this.content = contentElement;
            this.content.setStyle({
                display: 'none',
                zIndex:  GLOBALS['Z_POPUP_CONTENT_W_BG'] ? GLOBALS[this.hasOverlay ? 'Z_POPUP_CONTENT_W_BG' : 'Z_POPUP_CONTENT_WO_BG'] : 991
            });
            this.content.addClassName('popupContent');
            this.setupContent();

        // ... or create new content div if none exists
        } else {
            this.content = $(document.createElement('div'));
            this.content.setStyle({
                display: 'none',
                zIndex:  GLOBALS['Z_POPUP_CONTENT_W_BG'] ? GLOBALS[this.hasOverlay ? 'Z_POPUP_CONTENT_W_BG' : 'Z_POPUP_CONTENT_WO_BG'] : 991
            });
            this.content.update("<div class='popupContentLoading'>Loading...</div>");
            this.createCloseButton();
            Widget.contain(this.content);
                    
            // load real content with ajax
            var proxy = GLOBALS['HTTP_POPUPS'] ?
                GLOBALS['HTTP_POPUPS'] + this.name + '.php' :
                '/popups/' + this.name + '.php'
            new Ajax.Updater(this.content, proxy, {
                onComplete:  this.setupContent.bind(this),
                onFailure:   this.clearContent.bind(this),
                onException: this.clearContent.bind(this)
            });
        }
        this.content.addClassName(this.name);
        this.content.addClassName('popupContent');

        // stop annoying mousewheel scroll when within the content
        document.setObserver(this.id + 'Mousewheel', 'mousewheel', function (event) { event.stop(); } );
        document.stopObserver(this.id + 'Mousewheel');
        this.content.setObserver('stopScroll',  'mouseover', function (event) { document.startObserver(this.id + 'Mousewheel'); }, this);
        this.content.setObserver('startScroll', 'mouseout',  function (event) { document.stopObserver(this.id + 'Mousewheel'); },  this);
    },

    setupContent: function () {
    	// setup 'close' button
        this.createCloseButton();
        // realign loaded content
        this.align(this.alignment);
    },

    clearContent: function () {
        this.content.update('');
    },

    createCloseButton: function () {
        if (this.hasOverlay) {
            this.closeButton = new Button(document.createElement('a'));
            this.closeButton.stopObserver('buttonClick');
            this.closeButton.setObserver('closeButtonClick', 'click', this.deselect, this);
            this.closeButton.element.innerHTML = '<span></span>';
            this.closeButton.element.addClassName('close_button');
            this.content.insert({top : this.closeButton});
        }
    },

    // return elements    
    getButton: function () {
        return this.button;
    },

    getContent: function () {
        return this.content;
    },

    // move the button and have the content follow
    moveTo: function (left, top) {
        this.button.absolutize();
        this.button.setStyle({
            left: Object.isString(left) ? left : left + 'px',
            top:  Object.isString(top)  ? top  : top  + 'px'
        });
        this.align(this.alignment);
    },

    // handle changes to document size
    resize: function (event) {
        this.align(this.alignment);
    },

    // place the popup content on the page relative to the button
    align: function (alignment) {
        this.alignment = Object.isUndefined(alignment) ?
            this.alignment :
            alignment;

        // various measurements
        // page dimensions
        var pDims     = document.viewport.getDimensions();
        var pWidth    = pDims.width;
        var pHeight   = pDims.height;
        // button position
        var bDims     = this.button.getDimensions();
        var bPos      = this.button.cumulativeOffset();
        var bWidth    = bDims.width;
        var bHeight   = bDims.height;
        var bLeft     = bPos.left;
        var bRight    = bPos.left + bDims.width;
        var bTop      = bPos.top;
        var bBottom   = bPos.top + bDims.height;
        // content position
        var cDims     = this.content.getDimensions();
        var cWidth    = cDims.width;
        var cHeight   = cDims.height;
        var cLeft     = 0;
        var cTop      = 0;
        var cPosition = 'absolute';

        // assign variables by alignment
        switch (alignment) {
            // align relative to the button
            case Popup.ALIGN_BOTTOM_LEFT:
                cLeft     = bLeft;
                cTop      = bBottom;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_BOTTOM_RIGHT:
                cLeft     = bRight - cWidth;
                cTop      = bBottom;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_BOTTOM_CENTER:
                cLeft     = bLeft + (bWidth - cWidth) / 2;
                cTop      = bBottom;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_TOP_LEFT:
                cLeft     = bLeft;
                cTop      = bTop - cHeight;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_TOP_RIGHT:
                cLeft     = bRight - cWidth;
                cTop      = bTop - cHeight;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_TOP_CENTER:
                cLeft     = bLeft + (bWidth - cWidth) / 2;
                cTop      = bTop - cHeight;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_RIGHT_BOTTOM:
                cLeft     = bRight;
                cTop      = bBottom - cHeight;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_RIGHT_TOP:
                cLeft     = bRight;
                cTop      = bTop;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_RIGHT_CENTER:
                cLeft     = bRight;
                cTop      = bTop + (bHeight - cHeight) / 2;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_LEFT_BOTTOM:
                cLeft     = bLeft - cWidth;
                cTop      = bBottom - cHeight;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_LEFT_TOP:
                cLeft     = bLeft - cWidth;
                cTop      = bTop;
                cPosition = 'absolute';
                break;
            case Popup.ALIGN_LEFT_CENTER:
                cLeft     = bLeft - cWidth;
                cTop      = bTop + (bHeight - cHeight) / 2;
                cPosition = 'absolute';
                break;

            // align relative to the window
            case Popup.ALIGN_UPPER_LEFT:
                cLeft     = 0;
                cTop      = 0;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_WINDOW_TOP:
                cLeft     = (pWidth - cWidth) / 2;
                cTop      = 0;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_UPPER_RIGHT:
                cLeft     = pWidth - cWidth;
                cTop      = 0;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_WINDOW_RIGHT:
                cLeft     = pWidth - cWidth;
                cTop      = (pHeight - cHeight) / 2;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_LOWER_RIGHT:
                cLeft     = pWidth - cWidth;
                cTop      = pHeight - cHeight;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_WINDOW_BOTTOM:
                cLeft     = (pWidth - cWidth) / 2;
                cTop      = pHeight - cHeight;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_LOWER_LEFT:
                cLeft     = 0;
                cTop      = pHeight - cHeight;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_WINDOW_LEFT:
                cLeft     = 0;
                cTop      = (pHeight - cHeight) / 2;
                cPosition = 'fixed';
                break;
            case Popup.ALIGN_WINDOW_CENTER:
            default:
                cLeft     = (pWidth - cWidth) / 2;
                cTop      = (pHeight - cHeight) / 2;
                cPosition = 'fixed';
                break;
        }

        // position the content
        this.content.setStyle({
            left:     cLeft + 'px',
            top:      cTop  + 'px',
            position: cPosition
        });
        this.aligned = true;
    },

    // toggle visibility
    hide: function () {
        // hide everything
        this.content.hide();
        if (this.hasOverlay) Popup.hideOverlay();
        document.stopObserver(this.id + 'Mousewheel');
    },

    show: function () {
    	if (this.hasOverlay) Popup.showOverlay();
        // popup button aligned content
        if (this.alignment >= Popup.ALIGN_BOTTOM_LEFT && this.alignment <= Popup.ALIGN_RIGHT_TOP)
            this.content.show();
        // fade in window aligned content
        else
            new Effect.Appear(this.content, {duration: 0.5});
    },

    // handle mouse events
    select: function ($super, event) {
        // lazily align the content to avoid problems on a half-loaded page
        if (! this.aligned) this.align(this.alignment);
        // show my content
        this.show();
        // do other stuff
        $super(event);
    },

    deselect: function ($super, event) {
        // hide my content
        this.hide();
        // reset close button
        if (this.closeButton) this.closeButton.deselect();
        // do other stuff
        $super(event);
    }
});

Object.extend(Popup, {
    // button alignment constants
    ALIGN_BOTTOM_LEFT:   0,
    ALIGN_BOTTOM_CENTER: 1,
    ALIGN_BOTTOM_RIGHT:  2,
    ALIGN_LEFT_TOP:      3,
    ALIGN_LEFT_CENTER:   4,
    ALIGN_LEFT_BOTTOM:   5,
    ALIGN_TOP_RIGHT:     6,
    ALIGN_TOP_CENTER:    7,
    ALIGN_TOP_LEFT:      8,
    ALIGN_RIGHT_BOTTOM:  9,
    ALIGN_RIGHT_CENTER:  10,
    ALIGN_RIGHT_TOP:     11,

    // window alignment constants
    ALIGN_UPPER_LEFT:    12,
    ALIGN_WINDOW_TOP:    13,
    ALIGN_UPPER_RIGHT:   14,
    ALIGN_WINDOW_RIGHT:  15,
    ALIGN_LOWER_RIGHT:   16,
    ALIGN_WINDOW_BOTTOM: 17,
    ALIGN_LOWER_LEFT:    18,
    ALIGN_WINDOW_LEFT:   19,
    ALIGN_WINDOW_CENTER: 20,

    // setup opaque background above everything but Popup content
    createOverlay: function () {
        if (Popup.overlay) return;
        Popup.overlay = $(document.createElement('div'));
        Popup.overlay.setStyle({
            backgroundColor: '#000000',
            opacity:         0.8,
            display:         'none',
            position:        'fixed',
            left:            '0px',
            top:             '0px',
            width:           '100%',
            height:          '100%',
            zIndex:          GLOBALS['Z_POPUP_BACKGROUND'] ? GLOBALS['Z_POPUP_BACKGROUND'] : 990
        });
        document.body.appendChild(Popup.overlay);
    },

    hideOverlay: function () {
        if (! Popup.overlay) Popup.createOverlay();
        Popup.overlay.hide();
    },

    showOverlay: function () {
        if (! Popup.overlay) Popup.createOverlay();
        new Effect.Appear(Popup.overlay, {
            to:       0.8,
            duration: 0.75
        });
    }
});
var FormPopup = Class.create(Popup, {
    name: 'FormPopup',

    setupContent: function ($super) {
        $super();

        // find and observe the form
        this.form    = this.content.select('form').first();
        this.focused = this.form.select('input').first();
        if (this.form) {
            $A(this.form).each(function (input) {
                Element.setObserver(input, 'formPopupChange', 'blur', this.change, this);
            }, this);
            this.form.setObserver('formPopupSubmit', 'submit', this.submit, this);
            this.form.setObserver('formPopupReset',  'reset',  this.reset,  this);
        }

        // find the error message box
        this.errorBox = this.content.select('.error').first();
        if (this.errorBox) this.errorBox.hide();

        // find the success message box
        this.successBox = this.content.select('.success').first();
        if (this.successBox) {
            this.successBox.hide();
            // setup 'close' button
            this.successCloseButton = new Button(document.createElement('a'));
            this.successCloseButton.stopObserver('buttonClick');
            this.successCloseButton.setObserver('closeButtonClick', 'click', this.deselect, this);
            this.successCloseButton.element.innerHTML = '<span>close</span>';
            this.successCloseButton.element.addClassName('close_button_success');
            this.successBox.insert({bottom : this.successCloseButton});
        }

        // find the cookies message box
        this.cookieBox = this.content.select('.nocookies').first();
        if (this.cookieBox) this.cookieBox.hide();
    },

    select: function ($super, event) {
        // reset popup just before open
        this[(!this.cookieBox || navigator.cookieEnabled) ? "showForm" : "showCookie"]();
        // do other stuff
        $super(event);
        // focus on first element
        if (this.form && this.form.findFirstElement()) this.form.focusFirstElement();
    },

    deselect: function ($super, event) {
        // reset close button
        if (this.successCloseButton) this.successCloseButton.deselect();
        // do other stuff
        $super(event);
    },

    // return elements
    getForm: function () {
        return this.form;
    },

    // handle form verification and submission
    verify: function (event) {
        var verified = true;
        return verified;
    },

    focus: function (event) {
    },

    change: function (event) {
        //var target = Event.element(event);
        //var name   = target.name;
        //var value  = target.value;
        //switch (name) { }
    },

    submit: function (event) {
        if (event) Event.stop(event);
        return false;
    },

    reset: function (event) {
        // clear message box
        // and let the event continue to propagate
        this.clearError();
    },

    // send friendly messages to the user
    showError: function (message, append) {
        if (this.errorBox) {
            this.errorBox.innerHTML = append ?
                this.errorBox.innerHTML + "<br />" + message :
                message;
            this.errorBox.show();
        }
        if (this.form)       this.form.show();
        if (this.successBox) this.successBox.hide();
        if (this.cookieBox)  this.cookieBox.hide();
        // re-align if the content has changed
        // (because errors are variable we always have changed content)
        this.aligned = false;
        this.state   = FormPopup.STATE_ERROR;
        this.align(this.alignment);
    },
    
    clearError: function () {
        var state  = this.state;
        this.showError("");
        if (this.errorBox) this.errorBox.hide();
        this.state = state;
        this.align(this.alignment);
    },

    showSuccess: function () {
        this.reset();
        if (this.form)       this.form.hide();
        if (this.cookieBox)  this.cookieBox.hide();
        if (this.successBox) this.successBox.show();
        // re-align if the content has changed
        this.aligned = this.state == FormPopup.STATE_SUCCESS;
        this.state   = FormPopup.STATE_SUCCESS;
        this.align(this.alignment);
    },

    showCookie: function () {
        this.clearError();
        if (this.form)       this.form.reset();
        if (this.form)       this.form.hide();
        if (this.successBox) this.successBox.hide();
        if (this.cookieBox)  this.cookieBox.show();
        // re-align if the content has changed
        this.aligned = this.state == FormPopup.STATE_COOKIE;
        this.state   = FormPopup.STATE_COOKIE;
        this.align(this.alignment);
    },

    showForm: function () {
        this.clearError();
        if (this.form)       this.form.reset();
        if (this.form)       this.form.show();
        if (this.cookieBox)  this.cookieBox.hide();
        if (this.successBox) this.successBox.hide();
        // re-align if the content has changed
        this.aligned = this.state == FormPopup.STATE_FORM;
        this.state   = FormPopup.STATE_FORM;
        this.align(this.alignment);
    },

    onAjaxException: function ($super, request, error) {
        this.showError("Hmmmm. The network did something funny and we didn't get your data. Try again in a second or two.");
    }
});

Object.extend(FormPopup, {
    STATE_FORM:    1,
    STATE_ERROR:   2,
    STATE_SUCCESS: 3,
    STATE_COOKIE:  4
});

var FormWidget = Class.create(Widget, {
    name:  'FormWidget',

    initialize: function ($super, domElement) {
        $super(domElement);
        // the form element
        this.form = this.element.select('form').first();
        $A(this.form).each(function (input) {
            Element.setObserver(input, 'formWidgetChange', 'blur',  this.change, this);
            Element.setObserver(input, 'formWidgetFocus',  'focus', this.focus,  this);
            Element.setObserver(input, 'formWidgetBlur',   'blur',  this.blur,   this);
        }, this);
        this.form.setObserver('formWidgetSubmit', 'submit', this.submit, this);
        this.form.setObserver('formWidgetReset',  'reset',  this.reset, this);
        // error message box
        this.errorBox = this.element.select('.error').first();
        if (this.errorBox) this.errorBox.hide();
    },

    change: function (event) {
        //var target = Event.element(event);
        //var name   = target.name;
        //var value  = target.value;
        //switch (name) { }
    },

    focus: function (event) {
        var target = Event.element(event);
        target.addClassName('focus');
    },

    blur: function (event) {
        var target = Event.element(event);
        target.removeClassName('focus');
    },

    verify: function () {
        var verified = true;
        this.clearError();
        return verified;
    },

    submit: function (event) {
        Event.stop(event);
        return false;
    },

    reset: function (event) {
        this.clearError();
        this.form.reset();
        this.form.focusFirstElement();
    },

    onAjaxException: function ($super, request, error) {
        this.showError("Hmmmm. The network did something funny and we didn't get your data. Try again in a second or two.");
    },

    showError: function (message, append) {
        if (this.errorBox) {
            this.errorBox.innerHTML = append ?
                this.errorBox.innerHTML + "<p>" + message + "</p>" :
                "<p>" + message + "</p>";
            this.errorBox.show();
        }
    },

    clearError: function () {
        this.showError("");
        if (this.errorBox) this.errorBox.hide();
    }
});
var ValueSlider = Class.create(Widget, {
    name:  'ValueSlider',
    _index: 0,
    
    initialize: function ($super, domElement, initVal, maxVal, oneClick, onselect) {
        // setup as an unordered list
        $super(domElement);
        this.ul = $(document.createElement('ul'));
        this.element.insert(this.ul);

        // add the value buttons
        this.buttons  = new Array(maxVal || 5);
        for (var i = 0, length = this.buttons.length; i < length; i++)
            this.buttons[i] = new ValueSlider.Button(this);

        // initial selection
        this.value    = initVal;
        this.maxValue = maxVal;
        this.oneClick = Object.isUndefined(oneClick) ? true : oneClick;
        this.mouseOut(null);
        // return to initial selection on user mouseout
        this.setObserver('valueSliderMouseOut', 'mouseout', this.mouseOut);

        // what specific thing do we do when we're clicked?
        this.onselect = onselect ? onselect.bind(this) : null;
    },

    getValue: function () {
        return this.value;
    },

    setValue: function (value) {
        this.value = Math.max(0, Math.min(value, this.maxValue));
        this.mouseOut(null);
    },

    select: function (event, button) {
        this.value = button.getIndex() + 1;
        // handle final click
        if (this.oneClick) {
            for (var i = 0, length = this.buttons.length; i < length; i++) {
                var b = this.buttons[i];
                b[(i < this.value) ? 'select' : 'deselect'](event);
                // only one click per page load, so squash observers
                b.stopObserver('buttonClick');
                b.stopObserver('buttonMouseOver');
                b.stopObserver('buttonMouseOut');
                this.stopObserver('valueSliderMouseOut');
            }
        }
        // do other stuff on selection
        if (this.onselect) this.onselect(event, button);
    },

    mouseOver: function (event, button) {
        var index = button.getIndex();
        for (var i = 0, length = this.buttons.length; i < length; i++) {
            var b = this.buttons[i];
            // over
            if (i <= index) {
                b.element.removeClassName('half');
                b.element.addClassName('mouseover');
            // out
            } else {
                b.element.removeClassName('half');
                b.element.removeClassName('mouseover');
            }
        }
    },
    
    mouseOut: function (event) {
        for (var i = 0, length = this.buttons.length; i < length; i++) {
            var b    = this.buttons[i];
            var diff = this.value - i;
            // out
            if (diff <= 0.25) {
                b.element.removeClassName('half');
                b.element.removeClassName('mouseover');
            // half over
            } else if (diff <= 0.75) {
                b.element.addClassName('half');
                b.element.removeClassName('mouseover');
            // full over
            } else {
                b.element.removeClassName('half');
                b.element.addClassName('mouseover');
            }
        }
    }
});


ValueSlider.Button = Class.create(Button, {
        name:  'ValueSliderButton',

        initialize: function ($super, slider) {
            // setup
            $super(document.createElement('li'));
            // set owner
            this.slider = slider;
            this.slider.ul.insert(this.element);
            this.index = this.slider._index++;
        },

        getIndex: function () {
            return this.index;
        },

        click: function (event) {
            this.slider.select(event, this);
        },

        mouseOver: function (event) {
            this.slider.mouseOver(event, this);
        }
});
     
/*
* var layout = {
*   size:        100 | 100px | 30% | * | [1-9]*
*   orientation: vertical | horizontal,
*   panels:      [
*       {
*           orientation: vertical
*           size: 100
*           panels: [...]
*       },
*       {
*           orientation: horizontal
*           size: 300
*       }
*   ]
* }
*
* var lay1 = new Layout(null,             layout);
* var lay2 = new Layout('layoutDiv',      layout);
* var lay3 = new Layout(lay2.getPanel(1), layout);
* var lay4 = Layout.create(Layout.MENU_BOTH);
*/

var Layout = Class.create(Widget, {
    name: 'Layout',

    initialize: function ($super, container, layout) {
        // default values
        this.orientation = layout.orientation || Layout.HORIZONTAL;
        this.size        = layout.size        || '100%';
        this.panels      = new Array();

        // setup dom
        // inner layout
        if (container instanceof Layout) {
            this.container = container;
            $super(document.createElement('div'));
            this.container.element.insert(this.element);
        // outer layout in the document body
        } else if (container == null) {
            this.container = null;
            $super(document.body);
        // outer layout in a div
        } else {
            this.container = null;
            $super(container);
        }

        // general style
        this.element.setStyle({
            margin:   '0px',
            overflow: 'auto',
            position: 'absolute'
        });

        // create sublayouts
        layout.panels = layout.panels || [];
        layout.panels.each(function (layout) {
            this.panels.push(new Layout(this, layout));
        }, this);

        // set resize policy on the outer container
        if (this.isOuterContainer()) {
            // fill our space
            this.element.setStyle({
                width:  '100%',
                height: '100%'
            });
            // recursively set child panel sizes
            this.resize();
            // watch for manual resize events
            this.element.observe('resize', this.resize.bindAsEventListener(this));
        }
    },

    isOuterContainer: function () {
        return (this.container == null);
    },

    hasInnerPanels: function () {
        return (this.panels.length > 0);
    },
    
    isHorizontal: function () {
        return (this.orientation == Layout.HORIZONTAL);
    },

    isVertical: function () {
        return (this.orientation == Layout.VERTICAL);
    },

    getContainer: function () {
        return this.container;
    },

    getOrientation: function () {
        return this.orientation;
    },

    getSize: function () {
        return this.size;
    },

    getWidth: function () {
        return this.element.getWidth();
    },

    getHeight: function () {
        return this.element.getHeight();
    },

    getPanel: function (index) {
        return this.panels[index];
    },

    getPanels: function () {
        return this.panels;
    },

    getAllPanels: function () {
        var panels = [];
        for (var i = 0, length = this.panels.length; i < length; i++)
            panels.push(this.panels[i].getAllPanels());
        return panels.flatten();
    },

    setScrolling: function () {
        this.element.setStyle({
            overflow: 'auto'
        });
    },

    setNoScrolling: function () {
        this.element.setStyle({
            overflow: 'visible'
        });
    },

    resize: function (event) {
        // calculate star size for children
        var pixels  = 0;
        var percent = 0;
        var stars   = 0;
        for (var i = 0, length = this.panels.length; i < length; i++) {
            // size: 100 | 100px | 30% | * | [1-9]*
            var vals = this.panels[i].size.match(/^(\d*)(px||%|\*)$/);
            switch (vals[2]) {
                case '%':
                    percent += (vals[1] == '') ? 0 : vals[1];
                    break;
                case '*':
                    stars   += (vals[1] == '') ? 1 : vals[1];
                    break;
                case 'px':
                case '':
                    pixels  += (vals[1] == '') ? 0 : vals[1];
                    break;
                default:
                    break;
            }
        }

        // stars have different weights for horizontal and vertical
        var dim     = this.element.getDimensions();
        var perStar = 0;
        switch (this.orientation) {
            case Layout.VERTICAL:
                perStar = (dim.width - pixels - (dim.width * percent / 100)) / stars;
                break;
            case Layout.HORIZONTAL:
            default:
                perStar = (dim.height - pixels - (dim.height * percent / 100)) / stars;
                break;
        }
        perStar = Math.max(perStar, 0);

        // for each child...
        for (var i = 0, length = this.panels.length; i < length; i++) {
            // get child size
            var panel = this.panels[i];
            var vals  = this.panels[i].size.match(/^(\d*)(px||%|\*)$/);
            var size  = 0;
            switch (vals[2]) {
                case '%':
                    size = vals[1] + '%';
                    break;
                case '*':
                    size = (perStar * ((vals[1] == "") ? 1 : vals[1])) + 'px';
                    break;
                case 'px':
                case '':
                    size = vals[1] + 'px';
                    break;
                default:
                    size = '0px';
                    break;
            }

            // get child position
            var parent     = this.element.up();
            var parentLeft = parent.offsetLeft;
            var parentTop  = parent.offsetTop;
            var prev       = (i > 0) ? this.panels[i-1].element : null;
            var prevRight  = prev    ? prev.offsetLeft + prev.offsetWidth : 0;
            var prevBottom = prev    ? prev.offsetTop + prev.offsetHeight : 0;

            // set child size and positions
            switch (this.orientation) {
                case Layout.VERTICAL:
                    panel.element.setStyle({
                        width:  size,
                        height: dim.height + 'px',
                        left:   prevRight  + 'px',
                        top:    parentTop  + 'px'
                    });
                    break;
                case Layout.HORIZONTAL:
                default:
                    panel.element.setStyle({
                        width:  dim.width  + 'px',
                        height: size,
                        left:   parentLeft + 'px',
                        top:    prevBottom + 'px'
                    });
                    break;
            }
        }

        // recursively resize children
        this.getPanels().each(function (panel) {
            panel.resize();
        }, this);
    }
});

Object.extend(Layout, {
    // orientation constants
    HORIZONTAL:             'horizontal',
    VERTICAL:               'vertical',

    // predefined layouts
    ONE_PANEL:              0,
    MENU_LEFT:              1,
    MENU_TOP:               2,
    MENU_BOTH:              3,
    TWO_PANEL_VERTICAL:     4,
    TWO_PANEL_HORIZONTAL:   5,
    THREE_PANEL_VERTICAL:   6,
    THREE_PANEL_HORIZONTAL: 7,
    FOUR_SQUARE:            8,

    // factory for creating basic predefined layouts
    create: function (type) {
        var layout;
        switch (type) {
            case Layout.MENU_LEFT:
                layout = {
                    orientation: Layout.VERTICAL,
                    size:        '100%',
                    panels:      [
                        {size: '150px'},
                        {size: '*'}
                    ]
                };
                break;

            case Layout.MENU_TOP:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      [
                        {size: '50px'},
                        {size: '*'}
                    ]
                };
                break;

            case Layout.MENU_BOTH:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      [
                        {size: '50px'},
                        {
                            orientation: Layout.VERTICAL,
                            size:        '*',
                            panels:      [
                                {size: '150px'},
                                {size: '*'}
                            ]
                        }
                    ]
                };
                break;

            case Layout.TWO_PANEL_VERTICAL:
                layout = {
                    orientation: Layout.VERTICAL,
                    size:        '100%',
                    panels:      [
                        {size: '50%'},
                        {size: '50%'}
                    ]
                };
                break;

            case Layout.TWO_PANEL_HORIZONTAL:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      [
                        {size: '50%'},
                        {size: '50%'}
                    ]
                };
                break;

            case Layout.THREE_PANEL_VERTICAL:
                layout = {
                    orientation: Layout.VERTICAL,
                    size:        '100%',
                    panels:      [
                        {size: '33%'},
                        {size: '33%'},
                        {size: '34%'}
                    ]
                };
                break;

            case Layout.THREE_PANEL_HORIZONTAL:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      [
                        {size: '33%'},
                        {size: '33%'},
                        {size: '34%'}
                    ]
                };
                break;

            case Layout.FOUR_SQUARE:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      [
                        {
                            orientation: Layout.VERTICAL,
                            size:        '50%',
                            panels:      [
                                {size: '50%'},
                                {size: '50%'}
                            ]
                        },
                        {
                            orientation: Layout.VERTICAL,
                            size:        '50%',
                            panels:      [
                                {size: '50%'},
                                {size: '50%'}
                            ]
                        }
                    ]
                };
                break;

            case Layout.ONE_PANEL:
            default:
                layout = {
                    orientation: Layout.HORIZONTAL,
                    size:        '100%',
                    panels:      []
                };
                break;
        }
        return new Layout(null, layout);
    }
});
var ButtonSet = Class.create(Widget, {
    name: 'ButtonSet',

    initialize: function ($super, domElement, numButtons) {
        // setup
        $super(document.createElement('ul'));
        $(domElement).insert(this.element);
        numButtons = numButtons || 0;

        // create button container
        this.buttons  = new Array();
        this.selected = null;

        // create and add the initial buttons
        for (var i = 0; i < numButtons; i++)
            this.add();
    },

    get: function (index) {
        return this.buttons[index];
    },

    getAll: function () {
        return this.buttons;
    },

    getSelected: function () {
        return this.selected;
    },

    add: function (button) {
        if (Object.isUndefined(button) || !(button instanceof Button)) {
            button = new Button(document.createElement('li'));
            button.setId(this.name + this.buttons.length);
        }
        button.setObserver('buttonSetClick', 'click', this.select.curry(button), this);
        this.buttons.push(button);
        this.element.insert(button.element);
        return button;
    },

    remove: function (button) {
        var index = this.buttons.indexOf(buttons);
        this.buttons.splice(index, 1);
        var el = button.element.remove();
        delete button;
        return el;
    },

    select: function (button) {
        button.select();
        button.stopObserver('buttonClick');
        button.stopObserver('buttonSetClick');
        if (this.selected) {
            this.selected.deselect();
            this.selected.startObserver('buttonClick');
            this.selected.startObserver('buttonSetClick');
        }
        this.selected = button;
    }
});
var FontResizer = Class.create(ButtonSet, {
    name:    'FontResizer',

    // font sizes
    midSize: 100,
    dSize:   20,

    initialize: function ($super, domElement, numButtons, innerHTML) {
        // setup
        $super(domElement, numButtons);
        // add the letter "A" to each item
        this.buttons.each(function (button) {
        	button.element.innerHTML = innerHTML ? innerHTML : '';
        });
        // find the middle button
        var midIndex = Math.floor((this.buttons.length - 1) / 2);
        var mid      = this.get(midIndex);
        // select the middle button
        mid.select();
        this.select(mid);
    },

    select: function ($super, button) {
        $super(button);
        // set font size
        var index    = this.buttons.indexOf(button);
        var midIndex = Math.floor((this.buttons.length - 1) / 2);
        var fontSize = this.midSize + (index - midIndex) * this.dSize;
        $(document.body).setStyle({
            fontSize: fontSize + '%'
        });
        document.fire('widget:fontresize');
    }
});
var BreadcrumbNav = Class.create(Widget, {
    name:         'BreadcrumbNav',

    // crumb formats
    firstFormat:  new Template('<a href="#{url}" class="breadcrumb">#{text}</a>'),
    middleFormat: new Template('#{separator}<a href="#{url}" class="breadcrumb">#{text}</a>'),
    lastFormat:   new Template('#{separator}<span class="breadcrumb">#{text}</span>'),

    /***
     *
     * - crumbs is an array of crumb objects
     *   each crumb is an object containing a url and link text
     *   var crumbs = [
     *     {url: '/Positions/Woman_On_Top/', text: 'woman on top'},
     *     {url: '/Positions/Man_On_Top/index.php', text: 'man on top (aka missionary)'},
     *     {url: '/Positions/Doggy_Style/index.php?x=y', text: 'doggy style is fun'}
     *   ];
     *
     * - separator is the text string to insert between crumbs
     *   it can be any text or html content, or nothing at all
     ***/
    initialize: function ($super, domElement, crumbs, separator) {
        $super(domElement);
        crumbs.each(function (crumb, index) {
            var format;
            switch (index) {
                case (crumbs.length - 1):
                    format = this.lastFormat;
                    break;
                case 0:
                    format = this.firstFormat;
                    break;
                default:
                    format = this.middleFormat;
                    break;
            }
            this.element.insert(
                format.evaluate({
                    separator: separator,
                    url:       crumb.url,
                    text:      crumb.text
                })
            );
        }, this);
    }
});
var Tooltip = Class.create(Widget, {
    name: 'Tooltip',
    dx:   15,
    dy:   15,

    initialize: function ($super, domElement, tip, overDelay, onDelay, follows) {
        $super(document.createElement('div'));
        this.tipjar = $(domElement);
        this.setTip(tip);
        this.element.setStyle({
            position: 'absolute',
            display:  'none'
        });
        Widget.contain(this.element);

        // how long to wait until appearing (in seconds)
        this.overDelay   = overDelay || 0;
        this.overDelayId = null;
        
        // how long to stay on (in seconds)
        this.onDelay     = onDelay   || 10;
        this.onDelayId   = null;

        // watch for events
        this.tipjar.setObserver('tooltipMouseOver', 'mouseover', this.mouseOver, this);
        this.tipjar.setObserver('tooltipMouseOut',  'mouseout',  this.mouseOut,  this);
        this.tipjar.setObserver('tooltipMouseMove', 'mousemove', this.mouseMove, this);
        
        // tooltip follows the mouse pointer?
        this.follows = follows || false;
        this.pointer = {x: 0, y: 0};
    },

    setTip: function (tip) {
        this.element.innerHTML = tip;
    },

    mouseOver: function (event) {
        // ignore bubbled events internal to the tipjar
        if (this.tipjar.underlies(event.relatedTarget)) return;
        // handle the event - after a short delay
        Effect.Appear(this.element, {
            duration: 0.5,
            delay:    this.overDelay,
            queue:    {position: 'end', scope: this.id}
        });
        Effect.Fade(this.element, {
            duration: 0.5,
            delay:    this.onDelay,
            queue:    {position: 'end', scope: this.id}
        });
    },

    mouseOverDelayed: function (event) {
        this.moveTo(this.pointer.x + this.dx, this.pointer.y - this.dy);
    },

    mouseOut: function (event) {
        // ignore mouseout events internal to the tipjar
        if (this.tipjar.underlies(event.relatedTarget)) return;
        // clear other delays and hide the tooltip
        Effect.Queues.get(this.id).invoke('cancel');
        this.hide();
    },

    mouseMove: function (event) {
        this.pointer = event.pointer();
        if (this.follows)
            this.moveTo(this.pointer.x + this.dx, this.pointer.y - this.dy);
    }
});
var ListItem = Class.create(Widget, {
    name:   'ListItem',

    initialize: function ($super, list, obj, insertBeforeItem) {
        $super(document.createElement('li'));
        // store object references
        this.list = list;
        this.obj  = obj;
        // update the DOM
        this.element.innerHTML = this.toInnerHTML();
        // insert after a specific ListItem or at end of list?
        if (insertBeforeItem && insertBeforeItem instanceof ListItem) {
            // insert before another list item
            insertBeforeItem.element.insert({before: this.element});
        } else {
            // insert at the end of the list
            this.list.element.insert(this.element);
        }
        this.element.show();
    },

    getList: function () {
        return this.list;
    },

    getObject: function () {
        return this.obj;
    },

    filter: function (predicate) {
        return true;
    },

    remove: function () {
        return this.list.remove(this);
    },

    // replace the list item innerHTML with different content
    replace: function (obj) {
        this.obj = obj;
        this.element.innerHTML = this.toInnerHTML();
    },

    // fade one ad out, replace it with another, then fade it back in
    fadeReplace: function (obj) {
        // fade out
        // after fade is finished, replace
        new Effect.Opacity(this.element, {
            from:        1.0,
            to:          0.0,
            duration:    1.0,
            transition:  Effect.Transitions.sinoidal,
            queue:       {position: 'front', scope: this.getId()},
            delay:       0.0,
            afterFinish: this.replace.bind(this, obj)
        });
        // fade in
        new Effect.Opacity(this.element, {
            from:       0.0,
            to:         1.0,
            duration:   1.0,
            transition: Effect.Transitions.sinoidal,
            queue:      {position: 'end', scope: this.getId()},
            delay:      0.0
        });
    },

    // override in subclasses
    format: new Template(""),
    
    toInnerHTML: function () {
        // this.format.evaluate({...});
    }
});

