/*** Start of file, yahoo.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.4
*/

/**
 * The YAHOO object is the single global object used by YUI Library.  It
 * contains utility function for setting up namespaces, inheritance, and
 * logging.  YAHOO.util, YAHOO.widget, and YAHOO.example are namespaces
 * created automatically for and used by the library.
 * @module YAHOO
 */

/**
 * The YAHOO global namespace object
 * @class YAHOO
 * @static
 */
if (typeof YAHOO == "undefined") {
    YAHOO = {};
}

/**
 * Returns the namespace specified and creates it if it doesn't exist
 *
 * YAHOO.namespace("property.package");
 * YAHOO.namespace("YAHOO.property.package");
 *
 * Either of the above would create YAHOO.property, then
 * YAHOO.property.package
 *
 * Be careful when naming packages. Reserved words may work in some browsers
 * and not others. For instance, the following will fail in Safari:
 *
 * YAHOO.namespace("really.long.nested.namespace");
 *
 * This fails because "long" is a future reserved word in ECMAScript
 * @method namespace
 * @static
 * @param  {String} ns The name of the namespace
 * @return {Object}    A reference to the namespace object
 */
YAHOO.namespace = function(ns) {

    if (!ns || !ns.length) {
        return null;
    }

    var levels = ns.split(".");
    var nsobj = YAHOO;

    // YAHOO is implied, so it is ignored if it is included
    for (var i=(levels[0] == "YAHOO") ? 1 : 0; i<levels.length; ++i) {
        nsobj[levels[i]] = nsobj[levels[i]] || {};
        nsobj = nsobj[levels[i]];
    }

    return nsobj;
};

/**
 * Uses YAHOO.widget.Logger to output a log message, if the widget is available.
 *
 * @method log
 * @static
 * @param  {string}  sMsg       The message to log.
 * @param  {string}  sCategory  The log category for the message.  Default
 *                              categories are "info", "warn", "error", time".
 *                              Custom categories can be used as well. (opt)
 * @param  {string}  sSource    The source of the the message (opt)
 * @return {boolean}            True if the log operation was successful.
 */
YAHOO.log = function(sMsg, sCategory, sSource) {
    var l = YAHOO.widget.Logger;
    if(l && l.log) {
        return l.log(sMsg, sCategory, sSource);
    } else {
        return false;
    }
};

/**
 * Utility to set up the prototype, constructor and superclass properties to
 * support an inheritance strategy that can chain constructors and methods.
 *
 * @method extend
 * @static
 * @param {function} subclass   the object to modify
 * @param {function} superclass the object to inherit
 */
YAHOO.extend = function(subclass, superclass) {
    var f = function() {};
    f.prototype = superclass.prototype;
    subclass.prototype = new f();
    subclass.prototype.constructor = subclass;
    subclass.superclass = superclass.prototype;
    if (superclass.prototype.constructor == Object.prototype.constructor) {
        superclass.prototype.constructor = superclass;
    }
};

YAHOO.namespace("util");
YAHOO.namespace("widget");
YAHOO.namespace("example");


/*** End of file, yahoo.js ***/

/*** Start of file, event.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.4
*/

/**
 * The CustomEvent class lets you define events for your application
 * that can be subscribed to by one or more independent component.
 *
 * @param {String}  type The type of event, which is passed to the callback
 *                  when the event fires
 * @param {Object}  oScope The context the event will fire from.  "this" will
 *                  refer to this object in the callback.  Default value:
 *                  the window object.  The listener can override this.
 * @param {boolean} silent pass true to prevent the event from writing to
 *                  the log system
 * @namespace YAHOO.util
 * @class CustomEvent
 * @constructor
 */
YAHOO.util.CustomEvent = function(type, oScope, silent) {

    /**
     * The type of event, returned to subscribers when the event fires
     * @property type
     * @type string
     */
    this.type = type;

    /**
     * The scope the the event will fire from by default.  Defaults to the window
     * obj
     * @property scope
     * @type object
     */
    this.scope = oScope || window;

    /**
     * By default all custom events are logged in the debug build, set silent
     * to true to disable logging for this event.
     * @property silent
     * @type boolean
     */
    this.silent = silent;

    /**
     * The subscribers to this event
     * @property subscribers
     * @type Subscriber[]
     */
    this.subscribers = [];

    if (!this.silent) {
    }

    // Only add subscribe events for events that are not generated by CustomEvent
    //if (oScope && (oScope.constructor != this.constructor)) {

        /*
         * Custom events provide a custom event that fires whenever there is
         * a new subscriber to the event.  This provides an opportunity to
         * handle the case where there is a non-repeating event that has
         * already fired has a new subscriber.
         *
         * type CustomEvent
         */
        //this.subscribeEvent =
                //new YAHOO.util.CustomEvent("subscribe", this, true);

    //}
};

YAHOO.util.CustomEvent.prototype = {
    /**
     * Subscribes the caller to this event
     * @method subscribe
     * @param {Function} fn       The function to execute
     * @param {Object}   obj      An object to be passed along when the event fires
     * @param {boolean}  bOverride If true, the obj passed in becomes the execution
     *                            scope of the listener
     */
    subscribe: function(fn, obj, bOverride) {
        //if (this.subscribeEvent) {
            //this.subscribeEvent.fire(fn, obj, bOverride);
        //}

        this.subscribers.push( new YAHOO.util.Subscriber(fn, obj, bOverride) );
    },

    /**
     * Unsubscribes the caller from this event
     * @method unsubscribe
     * @param {Function} fn  The function to execute
     * @param {Object}   obj An object to be passed along when the event fires
     * @return {boolean} True if the subscriber was found and detached.
     */
    unsubscribe: function(fn, obj) {
        var found = false;
        for (var i=0, len=this.subscribers.length; i<len; ++i) {
            var s = this.subscribers[i];
            if (s && s.contains(fn, obj)) {
                this._delete(i);
                found = true;
            }
        }

        return found;
    },

    /**
     * Notifies the subscribers.  The callback functions will be executed
     * from the scope specified when the event was created, and with the following
     * parameters:
     *   <pre>
     *   - The type of event
     *   - All of the arguments fire() was executed with as an array
     *   - The custom object (if any) that was passed into the subscribe() method
     *   </pre>
     * @method fire
     * @param {Array} an arbitrary set of parameters to pass to the handler
     */
    fire: function() {
        var len=this.subscribers.length;
        if (!len && this.silent) {
            return;
        }

        var args = [];

        for (var i=0; i<arguments.length; ++i) {
            args.push(arguments[i]);
        }

        if (!this.silent) {
        }

        for (i=0; i<len; ++i) {
            var s = this.subscribers[i];
            if (s) {
                if (!this.silent) {
                }
                var scope = (s.override) ? s.obj : this.scope;
                s.fn.call(scope, this.type, args, s.obj);
            }
        }
    },

    /**
     * Removes all listeners
     * @method unsubscribeAll
     */
    unsubscribeAll: function() {
        for (var i=0, len=this.subscribers.length; i<len; ++i) {
            this._delete(len - 1 - i);
        }
    },

    /**
     * @method _delete
     * @private
     */
    _delete: function(index) {
        var s = this.subscribers[index];
        if (s) {
            delete s.fn;
            delete s.obj;
        }

        // delete this.subscribers[index];
        this.subscribers.splice(index, 1);
    },

    /**
     * @method toString
     */
    toString: function() {
         return "CustomEvent: " + "'" + this.type  + "', " +
             "scope: " + this.scope;

    }
};

/////////////////////////////////////////////////////////////////////

/**
 * Stores the subscriber information to be used when the event fires.
 * @param {Function} fn       The function to execute
 * @param {Object}   obj      An object to be passed along when the event fires
 * @param {boolean}  bOverride If true, the obj passed in becomes the execution
 *                            scope of the listener
 * @class Subscriber
 * @constructor
 */
YAHOO.util.Subscriber = function(fn, obj, bOverride) {

    /**
     * The callback that will be execute when the event fires
     * @property fn
     * @type function
     */
    this.fn = fn;

    /**
     * An optional custom object that will passed to the callback when
     * the event fires
     * @property obj
     * @type object
     */
    this.obj = obj || null;

    /**
     * The default execution scope for the event listener is defined when the
     * event is created (usually the object which contains the event).
     * By setting override to true, the execution scope becomes the custom
     * object passed in by the subscriber
     * @property override
     * @type boolean
     */
    this.override = (bOverride);
};

/**
 * Returns true if the fn and obj match this objects properties.
 * Used by the unsubscribe method to match the right subscriber.
 *
 * @method contains
 * @param {Function} fn the function to execute
 * @param {Object} obj an object to be passed along when the event fires
 * @return {boolean} true if the supplied arguments match this
 *                   subscriber's signature.
 */
YAHOO.util.Subscriber.prototype.contains = function(fn, obj) {
    return (this.fn == fn && this.obj == obj);
};

/**
 * @method toString
 */
YAHOO.util.Subscriber.prototype.toString = function() {
    return "Subscriber { obj: " + (this.obj || "")  +
           ", override: " +  (this.override || "no") + " }";
};

// The first instance of Event will win if it is loaded more than once.
if (!YAHOO.util.Event) {

/**
 * The event utility provides functions to add and remove event listeners,
 * event cleansing.  It also tries to automatically remove listeners it
 * registers during the unload event.
 * @namespace YAHOO.util
 * @class Event
 */
    YAHOO.util.Event = function() {

        /**
         * True after the onload event has fired
         * @property loadComplete
         * @type boolean
         * @private
         */
        var loadComplete =  false;

        /**
         * Cache of wrapped listeners
         * @property listeners
         * @type array
         * @private
         */
        var listeners = [];

        /**
         * Listeners that will be attached during the onload event
         * @property delayedListeners
         * @type array
         * @private
         */
        var delayedListeners = [];

        /**
         * User-defined unload function that will be fired before all events
         * are detached
         * @property unloadListeners
         * @type array
         * @private
         */
        var unloadListeners = [];

        /**
         * Cache of DOM0 event handlers to work around issues with DOM2 events
         * in Safari
         * @property legacyEvents
         * @private
         */
        var legacyEvents = [];

        /**
         * Listener stack for DOM0 events
         * @property legacyHandlers
         * @private
         */
        var legacyHandlers = [];

        /**
         * The number of times to poll after window.onload.  This number is
         * increased if additional late-bound handlers are requested after
         * the page load.
         * @property retryCount
         * @private
         */
        var retryCount = 0;

        /**
         * onAvailable listeners
         * @property onAvailStack
         * @private
         */
        var onAvailStack = [];

        /**
         * Lookup table for legacy events
         * @property legacyMap
         * @private
         */
        var legacyMap = [];

        /**
         * Counter for auto id generation
         * @property counter
         * @private
         */
        var counter = 0;

        return { // PREPROCESS

            /**
             * The number of times we should look for elements that are not
             * in the DOM at the time the event is requested after the document
             * has been loaded.  The default is 200@amp;50 ms, so it will poll
             * for 10 seconds or until all outstanding handlers are bound
             * (whichever comes first).
             * @property POLL_RETRYS
             * @type int
             */
            POLL_RETRYS: 200,

            /**
             * The poll interval in milliseconds
             * @property POLL_INTERVAL
             * @type int
             */
            POLL_INTERVAL: 50,

            /**
             * Element to bind, int constant
             * @property EL
             * @type int
             */
            EL: 0,

            /**
             * Type of event, int constant
             * @property TYPE
             * @type int
             */
            TYPE: 1,

            /**
             * Function to execute, int constant
             * @property FN
             * @type int
             */
            FN: 2,

            /**
             * Function wrapped for scope correction and cleanup, int constant
             * @property WFN
             * @type int
             */
            WFN: 3,

            /**
             * Object passed in by the user that will be returned as a
             * parameter to the callback, int constant
             * @property SCOPE
             * @type int
             */
            SCOPE: 3,

            /**
             * Adjusted scope, either the element we are registering the event
             * on or the custom object passed in by the listener, int constant
             * @property ADJ_SCOPE
             * @type int
             */
            ADJ_SCOPE: 4,

            /**
             * Safari detection is necessary to work around the preventDefault
             * bug that makes it so you can't cancel a href click from the
             * handler.  There is not a capabilities check we can use here.
             * @property isSafari
             * @private
             */
            isSafari: (/Safari|Konqueror|KHTML/gi).test(navigator.userAgent),

            /**
             * IE detection needed to properly calculate pageX and pageY.
             * capabilities checking didn't seem to work because another
             * browser that does not provide the properties have the values
             * calculated in a different manner than IE.
             * @property isIE
             * @private
             */
            isIE: (!this.isSafari && !navigator.userAgent.match(/opera/gi) &&
                    navigator.userAgent.match(/msie/gi)),

            /**
             * @method addDelayedListener
             * @private
             */
            addDelayedListener: function(el, sType, fn, oScope, bOverride) {
                delayedListeners[delayedListeners.length] =
                    [el, sType, fn, oScope, bOverride];

                // If this happens after the inital page load, we need to
                // reset the poll counter so that we continue to search for
                // the element for a fixed period of time.
                if (loadComplete) {
                    retryCount = this.POLL_RETRYS;
                    this.startTimeout(0);
                    // this._tryPreloadAttach();
                }
            },

            /**
             * @method startTimeout
             * @private
             */
            startTimeout: function(interval) {
                var i = (interval || interval === 0) ? interval : this.POLL_INTERVAL;
                var self = this;
                var callback = function() { self._tryPreloadAttach(); };
                this.timeout = setTimeout(callback, i);
            },

            /**
             * Executes the supplied callback when the item with the supplied
             * id is found.  This is meant to be used to execute behavior as
             * soon as possible as the page loads.  If you use this after the
             * initial page load it will poll for a fixed time for the element.
             * The number of times it will poll and the frequency are
             * configurable.  By default it will poll for 10 seconds.
             *
             * @method onAvailable
             *
             * @param {string}   p_id the id of the element to look for.
             * @param {function} p_fn what to execute when the element is found.
             * @param {object}   p_obj an optional object to be passed back as
             *                   a parameter to p_fn.
             * @param {boolean}  p_override If set to true, p_fn will execute
             *                   in the scope of p_obj
             *
             */
            onAvailable: function(p_id, p_fn, p_obj, p_override) {
                onAvailStack.push( { id:       p_id,
                                     fn:       p_fn,
                                     obj:      p_obj,
                                     override: p_override } );

                retryCount = this.POLL_RETRYS;
                this.startTimeout(0);
                // this._tryPreloadAttach();
            },

            /**
             * Appends an event handler
             *
             * @method addListener
             *
             * @param {Object}   el        The html element to assign the
             *                             event to
             * @param {String}   sType     The type of event to append
             * @param {Function} fn        The method the event invokes
             * @param {Object}   oScope    An arbitrary object that will be
             *                             passed as a parameter to the handler
             * @param {boolean}  bOverride If true, the obj passed in becomes
             *                             the execution scope of the listener
             * @return {boolean} True if the action was successful or defered,
             *                        false if one or more of the elements
             *                        could not have the event bound to it.
             */
            addListener: function(el, sType, fn, oScope, bOverride) {

                if (!fn || !fn.call) {
                    return false;
                }

                // The el argument can be an array of elements or element ids.
                if ( this._isValidCollection(el)) {
                    var ok = true;
                    for (var i=0,len=el.length; i<len; ++i) {
                        ok = ( this.on(el[i],
                                       sType,
                                       fn,
                                       oScope,
                                       bOverride) && ok );
                    }
                    return ok;

                } else if (typeof el == "string") {
                    var oEl = this.getEl(el);
                    // If the el argument is a string, we assume it is
                    // actually the id of the element.  If the page is loaded
                    // we convert el to the actual element, otherwise we
                    // defer attaching the event until onload event fires

                    // check to see if we need to delay hooking up the event
                    // until after the page loads.
                    if (loadComplete && oEl) {
                        el = oEl;
                    } else {
                        // defer adding the event until onload fires
                        this.addDelayedListener(el,
                                                sType,
                                                fn,
                                                oScope,
                                                bOverride);

                        return true;
                    }
                }

                // Element should be an html element or an array if we get
                // here.
                if (!el) {
                    return false;
                }

                // we need to make sure we fire registered unload events
                // prior to automatically unhooking them.  So we hang on to
                // these instead of attaching them to the window and fire the
                // handles explicitly during our one unload event.
                if ("unload" == sType && oScope !== this) {
                    unloadListeners[unloadListeners.length] =
                            [el, sType, fn, oScope, bOverride];
                    return true;
                }

                // if the user chooses to override the scope, we use the custom
                // object passed in, otherwise the executing scope will be the
                // HTML element that the event is registered on
                var scope = (bOverride) ? oScope : el;

                // wrap the function so we can return the oScope object when
                // the event fires;
                var wrappedFn = function(e) {
                        return fn.call(scope, YAHOO.util.Event.getEvent(e),
                                oScope);
                    };

                var li = [el, sType, fn, wrappedFn, scope];
                var index = listeners.length;
                // cache the listener so we can try to automatically unload
                listeners[index] = li;

                if (this.useLegacyEvent(el, sType)) {
                    var legacyIndex = this.getLegacyIndex(el, sType);

                    // Add a new dom0 wrapper if one is not detected for this
                    // element
                    if ( legacyIndex == -1 ||
                                el != legacyEvents[legacyIndex][0] ) {

                        legacyIndex = legacyEvents.length;
                        legacyMap[el.id + sType] = legacyIndex;

                        // cache the signature for the DOM0 event, and
                        // include the existing handler for the event, if any
                        legacyEvents[legacyIndex] =
                            [el, sType, el["on" + sType]];
                        legacyHandlers[legacyIndex] = [];

                        el["on" + sType] =
                            function(e) {
                                YAHOO.util.Event.fireLegacyEvent(
                                    YAHOO.util.Event.getEvent(e), legacyIndex);
                            };
                    }

                    // add a reference to the wrapped listener to our custom
                    // stack of events
                    //legacyHandlers[legacyIndex].push(index);
                    legacyHandlers[legacyIndex].push(li);

                // DOM2 Event model
                } else if (el.addEventListener) {
                    el.addEventListener(sType, wrappedFn, false);
                // IE
                } else if (el.attachEvent) {
                    el.attachEvent("on" + sType, wrappedFn);
                }

                return true;

            },

            /**
             * When using legacy events, the handler is routed to this object
             * so we can fire our custom listener stack.
             * @method fireLegacyEvent
             * @private
             */
            fireLegacyEvent: function(e, legacyIndex) {
                var ok = true;

                var le = legacyHandlers[legacyIndex];
                for (var i=0,len=le.length; i<len; ++i) {
                    var li = le[i];
                    if ( li && li[this.WFN] ) {
                        var scope = li[this.ADJ_SCOPE];
                        var ret = li[this.WFN].call(scope, e);
                        ok = (ok && ret);
                    }
                }

                return ok;
            },

            /**
             * Returns the legacy event index that matches the supplied
             * signature
             * @method getLegacyIndex
             * @private
             */
            getLegacyIndex: function(el, sType) {
                var key = this.generateId(el) + sType;
                if (typeof legacyMap[key] == "undefined") {
                    return -1;
                } else {
                    return legacyMap[key];
                }
            },

            /**
             * Logic that determines when we should automatically use legacy
             * events instead of DOM2 events.
             * @method useLegacyEvent
             * @private
             */
            useLegacyEvent: function(el, sType) {
                if (!el.addEventListener && !el.attachEvent) {
                    return true;
                } else if (this.isSafari) {
                    if ("click" == sType || "dblclick" == sType) {
                        return true;
                    }
                }
                return false;
            },

            /**
             * Removes an event handler
             *
             * @method removeListener
             *
             * @param {Object} el the html element or the id of the element to
             * assign the event to.
             * @param {String} sType the type of event to remove
             * @param {Function} fn the method the event invokes
             * @return {boolean} true if the unbind was successful, false
             * otherwise
             */
            removeListener: function(el, sType, fn, index) {

                if (!fn || !fn.call) {
                    return false;
                }

                var i, len;

                // The el argument can be a string
                if (typeof el == "string") {
                    el = this.getEl(el);
                // The el argument can be an array of elements or element ids.
                } else if ( this._isValidCollection(el)) {
                    var ok = true;
                    for (i=0,len=el.length; i<len; ++i) {
                        ok = ( this.removeListener(el[i], sType, fn) && ok );
                    }
                    return ok;
                }

                if ("unload" == sType) {

                    for (i=0, len=unloadListeners.length; i<len; i++) {
                        var li = unloadListeners[i];
                        if (li &&
                            li[0] == el &&
                            li[1] == sType &&
                            li[2] == fn) {
                                unloadListeners.splice(i, 1);
                                return true;
                        }
                    }

                    return false;
                }

                var cacheItem = null;

                //var index = arguments[3];

                if ("undefined" == typeof index) {
                    index = this._getCacheIndex(el, sType, fn);
                }

                if (index >= 0) {
                    cacheItem = listeners[index];
                }

                if (!el || !cacheItem) {
                    return false;
                }

                if (this.useLegacyEvent(el, sType)) {
                    var legacyIndex = this.getLegacyIndex(el, sType);
                    var llist = legacyHandlers[legacyIndex];
                    if (llist) {
                        for (i=0, len=llist.length; i<len; ++i) {
                            li = llist[i];
                            if (li &&
                                li[this.EL] == el &&
                                li[this.TYPE] == sType &&
                                li[this.FN] == fn) {
                                    llist.splice(i, 1);
                            }
                        }
                    }

                } else if (el.removeEventListener) {
                    el.removeEventListener(sType, cacheItem[this.WFN], false);
                } else if (el.detachEvent) {
                    el.detachEvent("on" + sType, cacheItem[this.WFN]);
                }

                // removed the wrapped handler
                delete listeners[index][this.WFN];
                delete listeners[index][this.FN];
                listeners.splice(index, 1);

                return true;

            },

            /**
             * Returns the event's target element
             * @method getTarget
             * @param {Event} ev the event
             * @param {boolean} resolveTextNode when set to true the target's
             *                  parent will be returned if the target is a
             *                  text node.  @deprecated, the text node is
             *                  now resolved automatically
             * @return {HTMLElement} the event's target
             */
            getTarget: function(ev, resolveTextNode) {
                var t = ev.target || ev.srcElement;
                return this.resolveTextNode(t);
            },

            /**
             * In some cases, some browsers will return a text node inside
             * the actual element that was targeted.  This normalizes the
             * return value for getTarget and getRelatedTarget.
             * @method resolveTextNode
             * @param {HTMLElement} node to resolve
             * @return  the normized node
             */
            resolveTextNode: function(node) {
                if (node && node.nodeName &&
                        "#TEXT" == node.nodeName.toUpperCase()) {
                    return node.parentNode;
                } else {
                    return node;
                }
            },

            /**
             * Returns the event's pageX
             * @method getPageX
             * @param {Event} ev the event
             * @return {int} the event's pageX
             */
            getPageX: function(ev) {
                var x = ev.pageX;
                if (!x && 0 !== x) {
                    x = ev.clientX || 0;

                    if ( this.isIE ) {
                        x += this._getScrollLeft();
                    }
                }

                return x;
            },

            /**
             * Returns the event's pageY
             * @method getPageY
             * @param {Event} ev the event
             * @return {int} the event's pageY
             */
            getPageY: function(ev) {
                var y = ev.pageY;
                if (!y && 0 !== y) {
                    y = ev.clientY || 0;

                    if ( this.isIE ) {
                        y += this._getScrollTop();
                    }
                }

                return y;
            },

            /**
             * Returns the pageX and pageY properties as an indexed array.
             * @method getXY
             * @type int[]
             */
            getXY: function(ev) {
                return [this.getPageX(ev), this.getPageY(ev)];
            },

            /**
             * Returns the event's related target
             * @method getRelatedTarget
             * @param {Event} ev the event
             * @return {HTMLElement} the event's relatedTarget
             */
            getRelatedTarget: function(ev) {
                var t = ev.relatedTarget;
                if (!t) {
                    if (ev.type == "mouseout") {
                        t = ev.toElement;
                    } else if (ev.type == "mouseover") {
                        t = ev.fromElement;
                    }
                }

                return this.resolveTextNode(t);
            },

            /**
             * Returns the time of the event.  If the time is not included, the
             * event is modified using the current time.
             * @method getTime
             * @param {Event} ev the event
             * @return {Date} the time of the event
             */
            getTime: function(ev) {
                if (!ev.time) {
                    var t = new Date().getTime();
                    try {
                        ev.time = t;
                    } catch(e) {
                        // can't set the time property
                        return t;
                    }
                }

                return ev.time;
            },

            /**
             * Convenience method for stopPropagation + preventDefault
             * @method stopEvent
             * @param {Event} ev the event
             */
            stopEvent: function(ev) {
                this.stopPropagation(ev);
                this.preventDefault(ev);
            },

            /**
             * Stops event propagation
             * @method stopPropagation
             * @param {Event} ev the event
             */
            stopPropagation: function(ev) {
                if (ev.stopPropagation) {
                    ev.stopPropagation();
                } else {
                    ev.cancelBubble = true;
                }
            },

            /**
             * Prevents the default behavior of the event
             * @method preventDefault
             * @param {Event} ev the event
             */
            preventDefault: function(ev) {
                if (ev.preventDefault) {
                    ev.preventDefault();
                } else {
                    ev.returnValue = false;
                }
            },

            /**
             * Finds the event in the window object, the caller's arguments, or
             * in the arguments of another method in the callstack.  This is
             * executed automatically for events registered through the event
             * manager, so the implementer should not normally need to execute
             * this function at all.
             * @method getEvent
             * @param {Event} the event parameter from the handler
             * @return {Event} the event
             */
            getEvent: function(e) {
                var ev = e || window.event;

                if (!ev) {
                    var c = this.getEvent.caller;
                    while (c) {
                        ev = c.arguments[0];
                        if (ev && Event == ev.constructor) {
                            break;
                        }
                        c = c.caller;
                    }
                }

                return ev;
            },

            /**
             * Returns the charcode for an event
             * @method getCharCode
             * @param {Event} ev the event
             * @return {int} the event's charCode
             */
            getCharCode: function(ev) {
                return ev.charCode || ((ev.type == "keypress") ? ev.keyCode : 0);
            },

            /**
             * Locating the saved event handler data by function ref
             *
             * @method _getCacheIndex
             * @private
             */
            _getCacheIndex: function(el, sType, fn) {
                for (var i=0,len=listeners.length; i<len; ++i) {
                    var li = listeners[i];
                    if ( li                 &&
                         li[this.FN] == fn  &&
                         li[this.EL] == el  &&
                         li[this.TYPE] == sType ) {
                        return i;
                    }
                }

                return -1;
            },

            /**
             * Generates an unique ID for the element if it does not already
             * have one.
             * @method generateId
             * @param el the element
             * @return {string} the id of the element
             */
            generateId: function(el) {
                var id = el.id;

                if (!id) {
                    id = "yuievtautoid-" + counter;
                    ++counter;
                    el.id = id;
                }

                return id;
            },

            /**
             * We want to be able to use getElementsByTagName as a collection
             * to attach a group of events to.  Unfortunately, different
             * browsers return different types of collections.  This function
             * tests to determine if the object is array-like.  It will also
             * fail if the object is an array, but is empty.
             * @method _isValidCollection
             * @param o the object to test
             * @return {boolean} true if the object is array-like and populated
             * @private
             */
            _isValidCollection: function(o) {

                return ( o                    && // o is something
                         o.length             && // o is indexed
                         typeof o != "string" && // o is not a string
                         !o.tagName           && // o is not an HTML element
                         !o.alert             && // o is not a window
                         typeof o[0] != "undefined" );

            },

            /**
             * @private
             * @property elCache
             * DOM element cache
             */
            elCache: {},

            /**
             * We cache elements bound by id because when the unload event
             * fires, we can no longer use document.getElementById
             * @method getEl
             * @private
             */
            getEl: function(id) {
                return document.getElementById(id);
            },

            /**
             * Clears the element cache
             * @deprecated
             * @private
             */
            clearCache: function() { },

            /**
             * hook up any deferred listeners
             * @method _load
             * @private
             */
            _load: function(e) {
                loadComplete = true;
                var EU = YAHOO.util.Event;
                EU._simpleRemove(window, "load", EU._load);
            },

            /**
             * Polling function that runs before the onload event fires,
             * attempting to attach to DOM Nodes as soon as they are
             * available
             * @method _tryPreloadAttach
             * @private
             */
            _tryPreloadAttach: function() {

                if (this.locked) {
                    return false;
                }

                this.locked = true;

                // keep trying until after the page is loaded.  We need to
                // check the page load state prior to trying to bind the
                // elements so that we can be certain all elements have been
                // tested appropriately
                var tryAgain = !loadComplete;
                if (!tryAgain) {
                    tryAgain = (retryCount > 0);
                }

                // Delayed listeners
                var stillDelayed = [];

                for (var i=0,len=delayedListeners.length; i<len; ++i) {
                    var d = delayedListeners[i];
                    // There may be a race condition here, so we need to
                    // verify the array element is usable.
                    if (d) {

                        // el will be null if document.getElementById did not
                        // work
                        var el = this.getEl(d[this.EL]);

                        if (el) {
                            this.on(el, d[this.TYPE], d[this.FN],
                                    d[this.SCOPE], d[this.ADJ_SCOPE]);
                            delete delayedListeners[i];
                        } else {
                            stillDelayed.push(d);
                        }
                    }
                }

                delayedListeners = stillDelayed;

                // onAvailable
                var notAvail = [];
                for (i=0,len=onAvailStack.length; i<len ; ++i) {
                    var item = onAvailStack[i];
                    if (item) {
                        el = this.getEl(item.id);

                        if (el) {
                            var scope = (item.override) ? item.obj : el;
                            item.fn.call(scope, item.obj);
                            delete onAvailStack[i];
                        } else {
                            notAvail.push(item);
                        }
                    }
                }

                retryCount = (stillDelayed.length === 0 &&
                                    notAvail.length === 0) ? 0 : retryCount - 1;

                if (tryAgain) {
                    this.startTimeout();
                }

                this.locked = false;

                return true;

            },

            /**
             * Removes all listeners attached to the given element via addListener.
             * Optionally, the node's children can also be purged.
             * Optionally, you can specify a specific type of event to remove.
             * @method purgeElement
             * @param {HTMLElement} el the element to purge
             * @param {boolean} recurse recursively purge this element's children
             * as well.  Use with caution.
             * @param {string} sType optional type of listener to purge. If
             * left out, all listeners will be removed
             */
            purgeElement: function(el, recurse, sType) {
                var elListeners = this.getListeners(el, sType);
                if (elListeners) {
                    for (var i=0,len=elListeners.length; i<len ; ++i) {
                        var l = elListeners[i];
                        // can't use the index on the changing collection
                        //this.removeListener(el, l.type, l.fn, l.index);
                        this.removeListener(el, l.type, l.fn);
                    }
                }

                if (recurse && el && el.childNodes) {
                    for (i=0,len=el.childNodes.length; i<len ; ++i) {
                        this.purgeElement(el.childNodes[i], recurse, sType);
                    }
                }
            },

            /**
             * Returns all listeners attached to the given element via addListener.
             * Optionally, you can specify a specific type of event to return.
             * @method getListeners
             * @param el {HTMLElement} the element to inspect
             * @param sType {string} optional type of listener to return. If
             * left out, all listeners will be returned
             * @return {Object} the listener. Contains the following fields:
             *    type:   (string)   the type of event
             *    fn:     (function) the callback supplied to addListener
             *    obj:    (object)   the custom object supplied to addListener
             *    adjust: (boolean)  whether or not to adjust the default scope
             *    index:  (int)      its position in the Event util listener cache
             */
            getListeners: function(el, sType) {
                var elListeners = [];
                if (listeners && listeners.length > 0) {
                    for (var i=0,len=listeners.length; i<len ; ++i) {
                        var l = listeners[i];
                        if ( l  && l[this.EL] === el &&
                                (!sType || sType === l[this.TYPE]) ) {
                            elListeners.push({
                                type:   l[this.TYPE],
                                fn:     l[this.FN],
                                obj:    l[this.SCOPE],
                                adjust: l[this.ADJ_SCOPE],
                                index:  i
                            });
                        }
                    }
                }

                return (elListeners.length) ? elListeners : null;
            },

            /**
             * Removes all listeners registered by pe.event.  Called
             * automatically during the unload event.
             * @method _unload
             * @private
             */
            _unload: function(e) {

                var EU = YAHOO.util.Event;

                for (var i=0,len=unloadListeners.length; i<len; ++i) {
                    var l = unloadListeners[i];
                    if (l) {
                        var scope = (l[EU.ADJ_SCOPE]) ? l[EU.SCOPE]: window;
                        l[EU.FN].call(scope, EU.getEvent(e), l[EU.SCOPE] );
                        delete unloadListeners[i];
                        l=null;
                    }
                }

                if (listeners && listeners.length > 0) {
                    //for (i=0,len=listeners.length; i<len ; ++i) {
                    var j = listeners.length;
                    while (j) {
                        var index = j-1;
                        l = listeners[index];
                        if (l) {
                            EU.removeListener(l[EU.EL], l[EU.TYPE],
                                    l[EU.FN], index);
                        }

                        l=null;

                        j = j - 1;
                    }

                    EU.clearCache();
                }

                for (i=0,len=legacyEvents.length; i<len; ++i) {
                    // dereference the element
                    delete legacyEvents[i][0];
                    // delete the array item
                    delete legacyEvents[i];
                }

                EU._simpleRemove(window, "unload", EU._unload);

            },

            /**
             * Returns scrollLeft
             * @method _getScrollLeft
             * @private
             */
            _getScrollLeft: function() {
                return this._getScroll()[1];
            },

            /**
             * Returns scrollTop
             * @method _getScrollTop
             * @private
             */
            _getScrollTop: function() {
                return this._getScroll()[0];
            },

            /**
             * Returns the scrollTop and scrollLeft.  Used to calculate the
             * pageX and pageY in Internet Explorer
             * @method _getScroll
             * @private
             */
            _getScroll: function() {
                var dd = document.documentElement, db = document.body;
                if (dd && (dd.scrollTop || dd.scrollLeft)) {
                    return [dd.scrollTop, dd.scrollLeft];
                } else if (db) {
                    return [db.scrollTop, db.scrollLeft];
                } else {
                    return [0, 0];
                }
            },

            /**
             * Adds a DOM event directly without the caching, cleanup, scope adj, etc
             *
             * @param el the elment to bind the handler to
             * @param {string} sType the type of event handler
             * @param {function} fn the callback to invoke
             * @param {boolen} capture or bubble phase
             * @private
             */
            _simpleAdd: function (el, sType, fn, capture) {
                if (el.addEventListener) {
                    el.addEventListener(sType, fn, (capture));
                } else if (el.attachEvent) {
                    el.attachEvent("on" + sType, fn);
                }
            },

            /**
             * Basic remove listener
             *
             * @param el the elment to bind the handler to
             * @param {string} sType the type of event handler
             * @param {function} fn the callback to invoke
             * @param {boolen} capture or bubble phase
             * @private
             */
            _simpleRemove: function (el, sType, fn, capture) {
                if (el.removeEventListener) {
                    el.removeEventListener(sType, fn, (capture));
                } else if (el.detachEvent) {
                    el.detachEvent("on" + sType, fn);
                }
            }
        };

    } ();

    /**
     * YAHOO.util.Event.on is an alias for addListener
     * @method on
     * @see addListener
     */
    YAHOO.util.Event.on = YAHOO.util.Event.addListener;

    if (document && document.body) {
        YAHOO.util.Event._load();
    } else {
        YAHOO.util.Event._simpleAdd(window, "load", YAHOO.util.Event._load);
    }
    YAHOO.util.Event._simpleAdd(window, "unload", YAHOO.util.Event._unload);
    YAHOO.util.Event._tryPreloadAttach();
}


/*** End of file, event.js ***/

/*** Start of file, dom.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.3
*/

/**
 * @class Provides helper methods for DOM elements.
 */
YAHOO.util.Dom = function() {
   var ua = navigator.userAgent.toLowerCase();
   var isOpera = (ua.indexOf('opera') > -1);
   var isSafari = (ua.indexOf('safari') > -1);
   var isIE = (window.ActiveXObject);

   var id_counter = 0;
   var util = YAHOO.util; // internal shorthand
   var property_cache = {}; // to cache case conversion for set/getStyle

   var toCamel = function(property) {
      var convert = function(prop) {
         var test = /(-[a-z])/i.exec(prop);
         return prop.replace(RegExp.$1, RegExp.$1.substr(1).toUpperCase());
      };

      while(property.indexOf('-') > -1) {
         property = convert(property);
      }

      return property;
      //return property.replace(/-([a-z])/gi, function(m0, m1) {return m1.toUpperCase()}) // cant use function as 2nd arg yet due to safari bug
   };

   var toHyphen = function(property) {
      if (property.indexOf('-') > -1) { // assume hyphen
         return property;
      }

      var converted = '';
      for (var i = 0, len = property.length;i < len; ++i) {
         if (property.charAt(i) == property.charAt(i).toUpperCase()) {
            converted = converted + '-' + property.charAt(i).toLowerCase();
         } else {
            converted = converted + property.charAt(i);
         }
      }

      return converted;
      //return property.replace(/([a-z])([A-Z]+)/g, function(m0, m1, m2) {return (m1 + '-' + m2.toLowerCase())});
   };

   // improve performance by only looking up once
   var cacheConvertedProperties = function(property) {
      property_cache[property] = {
         camel: toCamel(property),
         hyphen: toHyphen(property)
      };
   };

   return {
      /**
       * Returns an HTMLElement reference
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID for getting a DOM reference, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @return {HTMLElement/Array} A DOM reference to an HTML element or an array of HTMLElements.
       */
      get: function(el) {
         if (!el) { return null; } // nothing to work with

         if (typeof el != 'string' && !(el instanceof Array) ) { // assuming HTMLElement or HTMLCollection, so pass back as is
            return el;
         }

         if (typeof el == 'string') { // ID
            return document.getElementById(el);
         }
         else { // array of ID's and/or elements
            var collection = [];
            for (var i = 0, len = el.length; i < len; ++i) {
               collection[collection.length] = util.Dom.get(el[i]);
            }

            return collection;
         }

         return null; // safety, should never happen
      },

      /**
       * Normalizes currentStyle and ComputedStyle.
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @param {String} property The style property whose value is returned.
       * @return {String/Array} The current value of the style property for the element(s).
       */
      getStyle: function(el, property) {
         var f = function(el) {
            var value = null;
            var dv = document.defaultView;

            if (!property_cache[property]) {
               cacheConvertedProperties(property);
            }

            var camel = property_cache[property]['camel'];
            var hyphen = property_cache[property]['hyphen'];

            if (property == 'opacity' && el.filters) {// IE opacity
               value = 1;
               try {
                  value = el.filters.item('DXImageTransform.Microsoft.Alpha').opacity / 100;
               } catch(e) {
                  try {
                     value = el.filters.item('alpha').opacity / 100;
                  } catch(e) {}
               }
            } else if (el.style[camel]) { // camelCase for valid styles
               value = el.style[camel];
            }
            else if (isIE && el.currentStyle && el.currentStyle[camel]) { // camelCase for currentStyle; isIE to workaround broken Opera 9 currentStyle
               value = el.currentStyle[camel];
            }
            else if ( dv && dv.getComputedStyle ) { // hyphen-case for computedStyle
               var computed = dv.getComputedStyle(el, '');

               if (computed && computed.getPropertyValue(hyphen)) {
                  value = computed.getPropertyValue(hyphen);
               }
            }

            return value;
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Wrapper for setting style properties of HTMLElements.  Normalizes "opacity" across modern browsers.
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @param {String} property The style property to be set.
       * @param {String} val The value to apply to the given property.
       */
      setStyle: function(el, property, val) {
         if (!property_cache[property]) {
            cacheConvertedProperties(property);
         }

         var camel = property_cache[property]['camel'];

         var f = function(el) {
            switch(property) {
               case 'opacity' :
                  if (isIE && typeof el.style.filter == 'string') { // in case not appended
                     el.style.filter = 'alpha(opacity=' + val * 100 + ')';

                     if (!el.currentStyle || !el.currentStyle.hasLayout) {
                        el.style.zoom = 1; // when no layout or cant tell
                     }
                  } else {
                     el.style.opacity = val;
                     el.style['-moz-opacity'] = val;
                     el.style['-khtml-opacity'] = val;
                  }

                  break;
               default :
                  el.style[camel] = val;
            }


         };

         util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Gets the current position of an element based on page coordinates.  Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements
       @ return {Array} The XY position of the element(s)
       */
      getXY: function(el) {
         var f = function(el) {

         // has to be part of document to have pageXY
            if (el.offsetParent === null || this.getStyle(el, 'display') == 'none') {
               return false;
            }

            var parentNode = null;
            var pos = [];
            var box;

            if (el.getBoundingClientRect) { // IE
               box = el.getBoundingClientRect();
               var doc = document;
               if ( !this.inDocument(el) && parent.document != document) {// might be in a frame, need to get its scroll
                  doc = parent.document;

                  if ( !this.isAncestor(doc.documentElement, el) ) {
                     return false;
                  }

               }

               var scrollTop = Math.max(doc.documentElement.scrollTop, doc.body.scrollTop);
               var scrollLeft = Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft);

               return [box.left + scrollLeft, box.top + scrollTop];
            }
            else { // safari, opera, & gecko
               pos = [el.offsetLeft, el.offsetTop];
               parentNode = el.offsetParent;
               if (parentNode != el) {
                  while (parentNode) {
                     pos[0] += parentNode.offsetLeft;
                     pos[1] += parentNode.offsetTop;
                     parentNode = parentNode.offsetParent;
                  }
               }
               if (isSafari && this.getStyle(el, 'position') == 'absolute' ) { // safari doubles in some cases
                  pos[0] -= document.body.offsetLeft;
                  pos[1] -= document.body.offsetTop;
               }
            }

            if (el.parentNode) { parentNode = el.parentNode; }
            else { parentNode = null; }

            while (parentNode && parentNode.tagName.toUpperCase() != 'BODY' && parentNode.tagName.toUpperCase() != 'HTML')
            { // account for any scrolled ancestors
               if (util.Dom.getStyle(parentNode, 'display') != 'inline') { // work around opera inline scrollLeft/Top bug
                  pos[0] -= parentNode.scrollLeft;
                  pos[1] -= parentNode.scrollTop;
               }

               if (parentNode.parentNode) { parentNode = parentNode.parentNode; }
               else { parentNode = null; }
            }


            return pos;
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Gets the current X position of an element based on page coordinates.  The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements
       * @return {String/Array} The X position of the element(s)
       */
      getX: function(el) {
         var f = function(el) {
            return util.Dom.getXY(el)[0];
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Gets the current Y position of an element based on page coordinates.  Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements
       * @return {String/Array} The Y position of the element(s)
       */
      getY: function(el) {
         var f = function(el) {
            return util.Dom.getXY(el)[1];
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Set the position of an html element in page coordinates, regardless of how the element is positioned.
       * The element(s) must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements
       * @param {Array} pos Contains X & Y values for new position (coordinates are page-based)
       * @param {Boolean} noRetry By default we try and set the position a second time if the first fails
       */
      setXY: function(el, pos, noRetry) {
         var f = function(el) {
            var style_pos = this.getStyle(el, 'position');
            if (style_pos == 'static') { // default to relative
               this.setStyle(el, 'position', 'relative');
               style_pos = 'relative';
            }

            var pageXY = this.getXY(el);
            if (pageXY === false) { // has to be part of doc to have pageXY
               return false;
            }

            var delta = [ // assuming pixels; if not we will have to retry
               parseInt( this.getStyle(el, 'left'), 10 ),
               parseInt( this.getStyle(el, 'top'), 10 )
            ];

            if ( isNaN(delta[0]) ) {// in case of 'auto'
               delta[0] = (style_pos == 'relative') ? 0 : el.offsetLeft;
            }
            if ( isNaN(delta[1]) ) { // in case of 'auto'
               delta[1] = (style_pos == 'relative') ? 0 : el.offsetTop;
            }

            if (pos[0] !== null) { el.style.left = pos[0] - pageXY[0] + delta[0] + 'px'; }
            if (pos[1] !== null) { el.style.top = pos[1] - pageXY[1] + delta[1] + 'px'; }

            var newXY = this.getXY(el);

            // if retry is true, try one more time if we miss
            if (!noRetry && (newXY[0] != pos[0] || newXY[1] != pos[1]) ) {
               this.setXY(el, pos, true);
            }

         };

         util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Set the X position of an html element in page coordinates, regardless of how the element is positioned.
       * The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @param {Int} x to use as the X coordinate for the element(s).
       */
      setX: function(el, x) {
         util.Dom.setXY(el, [x, null]);
      },

      /**
       * Set the Y position of an html element in page coordinates, regardless of how the element is positioned.
       * The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @param {Int} x to use as the Y coordinate for the element(s).
       */
      setY: function(el, y) {
         util.Dom.setXY(el, [null, y]);
      },

      /**
       * Returns the region position of the given element.
       * The element must be part of the DOM tree to have a region (display:none or elements not appended return false).
       * @param {String/HTMLElement/Array} el Accepts a string to use as an ID, an actual DOM reference, or an Array of IDs and/or HTMLElements.
       * @return {Region/Array} A Region or array of Region instances containing "top, left, bottom, right" member data.
       */
      getRegion: function(el) {
         var f = function(el) {
            var region = new YAHOO.util.Region.getRegion(el);
            return region;
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Returns the width of the client (viewport).
       * Now using getViewportWidth.  This interface left intact for back compat.
       * @return {Int} The width of the viewable area of the page.
       */
      getClientWidth: function() {
         return util.Dom.getViewportWidth();
      },

      /**
       * Returns the height of the client (viewport).
       * Now using getViewportHeight.  This interface left intact for back compat.
       * @return {Int} The height of the viewable area of the page.
       */
      getClientHeight: function() {
         return util.Dom.getViewportHeight();
      },

      /**
       * Returns a array of HTMLElements with the given class
       * For optimized performance, include a tag and/or root node if possible
       * @param {String} className The class name to match against
       * @param {String} tag (optional) The tag name of the elements being collected
       * @param {String/HTMLElement} root (optional) The HTMLElement or an ID to use as the starting point
       * @return {Array} An array of elements that have the given class name
       */
      getElementsByClassName: function(className, tag, root) {
         var method = function(el) { return util.Dom.hasClass(el, className) };
         return util.Dom.getElementsBy(method, tag, root);
      },

      /**
       * Determines whether an HTMLElement has the given className
       * @param {String/HTMLElement/Array} el The element or collection to test
       * @param {String} className the class name to search for
       * @return {Boolean/Array} A boolean value or array of boolean values
       */
      hasClass: function(el, className) {
         var re = new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)');

         var f = function(el) {
            return re.test(el['className']);
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Adds a class name to a given element or collection of elements
       * @param {String/HTMLElement/Array} el The element or collection to add the class to
       * @param {String} className the class name to add to the class attribute
       */
      addClass: function(el, className) {
         var f = function(el) {
            if (this.hasClass(el, className)) { return; } // already present


            el['className'] = [el['className'], className].join(' ');
         };

         util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Removes a class name from a given element or collection of elements
       * @param {String/HTMLElement/Array} el The element or collection to remove the class from
       * @param {String} className the class name to remove from the class attribute
       */
      removeClass: function(el, className) {
         var re = new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g');

         var f = function(el) {
            if (!this.hasClass(el, className)) { return; } // not present


            var c = el['className'];
            el['className'] = c.replace(re, ' ');
            if ( this.hasClass(el, className) ) { // in case of multiple adjacent
               this.removeClass(el, className);
            }

         };

         util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Replace a class with another class for a given element or collection of elements.
       * If no oldClassName is present, the newClassName is simply added.
       * @param {String/HTMLElement/Array} el The element or collection to remove the class from
       * @param {String} oldClassName the class name to be replaced
       * @param {String} newClassName the class name that will be replacing the old class name
       */
      replaceClass: function(el, oldClassName, newClassName) {
         if (oldClassName === newClassName) { // avoid infinite loop
            return false;
         };

         var re = new RegExp('(?:^|\\s+)' + oldClassName + '(?:\\s+|$)', 'g');

         var f = function(el) {

            if ( !this.hasClass(el, oldClassName) ) {
               this.addClass(el, newClassName); // just add it if nothing to replace
               return; // note return
            }

            el['className'] = el['className'].replace(re, ' ' + newClassName + ' ');

            if ( this.hasClass(el, oldClassName) ) { // in case of multiple adjacent
               this.replaceClass(el, oldClassName, newClassName);
            }
         };

         util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Generates a unique ID
       * @param {String/HTMLElement/Array} el (optional) An optional element array of elements to add an ID to (no ID is added if one is already present)
       * @param {String} prefix (optional) an optional prefix to use (defaults to "yui-gen")
       * @return {String/Array} The generated ID, or array of generated IDs (or original ID if already present on an element)
       */
      generateId: function(el, prefix) {
         prefix = prefix || 'yui-gen';
         el = el || {};

         var f = function(el) {
            if (el) {
               el = util.Dom.get(el);
            } else {
               el = {}; // just generating ID in this case
            }

            if (!el.id) {
               el.id = prefix + id_counter++;
            } // dont override existing


            return el.id;
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Determines whether an HTMLElement is an ancestor of another HTML element in the DOM hierarchy
       * @param {String/HTMLElement} haystack The possible ancestor
       * @param {String/HTMLElement} needle The possible descendent
       * @return {Boolean} Whether or not the haystack is an ancestor of needle
       */
      isAncestor: function(haystack, needle) {
         haystack = util.Dom.get(haystack);
         if (!haystack || !needle) { return false; }

         var f = function(needle) {
            if (haystack.contains && !isSafari) { // safari "contains" is broken
               return haystack.contains(needle);
            }
            else if ( haystack.compareDocumentPosition ) {
               return !!(haystack.compareDocumentPosition(needle) & 16);
            }
            else { // loop up and test each parent
               var parent = needle.parentNode;

               while (parent) {
                  if (parent == haystack) {
                     return true;
                  }
                  else if (!parent.tagName || parent.tagName.toUpperCase() == 'HTML') {
                     return false;
                  }

                  parent = parent.parentNode;
               }
               return false;
            }
         };

         return util.Dom.batch(needle, f, util.Dom, true);
      },

      /**
       * Determines whether an HTMLElement is present in the current document
       * @param {String/HTMLElement} el The element to search for
       * @return {Boolean} Whether or not the element is present in the current document
       */
      inDocument: function(el) {
         var f = function(el) {
            return this.isAncestor(document.documentElement, el);
         };

         return util.Dom.batch(el, f, util.Dom, true);
      },

      /**
       * Returns a array of HTMLElements that pass the test applied by supplied boolean method
       * For optimized performance, include a tag and/or root node if possible
       * @param {Function} method A boolean method to test elements with
       * @param {String} tag (optional) The tag name of the elements being collected
       * @param {String/HTMLElement} root (optional) The HTMLElement or an ID to use as the starting point
       */
      getElementsBy: function(method, tag, root) {
         tag = tag || '*';
         root = util.Dom.get(root) || document;

         var nodes = [];
         var elements = root.getElementsByTagName(tag);

         if ( !elements.length && (tag == '*' && root.all) ) {
            elements = root.all; // IE < 6
         }

         for (var i = 0, len = elements.length; i < len; ++i)
         {
            if ( method(elements[i]) ) { nodes[nodes.length] = elements[i]; }
         }


         return nodes;
      },

      /**
       * Returns an array of elements that have had the supplied method applied.
       * The method is called with the element(s) as the first arg, and the optional param as the second ( method(el, o) )
       * @param {String/HTMLElement/Array} el (optional) An element or array of elements to apply the method to
       * @param {Function} method The method to apply to the element(s)
       * @param {Generic} (optional) o An optional arg that is passed to the supplied method
       * @param {Boolean} (optional) override Whether or not to override the scope of "method" with "o"
       * @return {HTMLElement/Array} The element(s) with the method applied
       */
      batch: function(el, method, o, override) {
         var id = el;
         el = util.Dom.get(el);

         var scope = (override) ? o : window;

         if (!el || el.tagName || !el.length) { // is null or not a collection (tagName for SELECT and others that can be both an element and a collection)
            if (!el) {
               return false;
            }
            return method.call(scope, el, o);
         }

         var collection = [];

         for (var i = 0, len = el.length; i < len; ++i) {
            if (!el[i]) {
               id = id[i];
            }
            collection[collection.length] = method.call(scope, el[i], o);
         }

         return collection;
      },

      /**
       * Returns the height of the document.
       * @return {Int} The height of the actual document (which includes the body and its margin).
       */
      getDocumentHeight: function() {
         var scrollHeight=-1,windowHeight=-1,bodyHeight=-1;
         var marginTop = parseInt(util.Dom.getStyle(document.body, 'marginTop'), 10);
         var marginBottom = parseInt(util.Dom.getStyle(document.body, 'marginBottom'), 10);

         var mode = document.compatMode;

         if ( (mode || isIE) && !isOpera ) { // (IE, Gecko)
            switch (mode) {
               case 'CSS1Compat': // Standards mode
                  scrollHeight = ((window.innerHeight && window.scrollMaxY) ?  window.innerHeight+window.scrollMaxY : -1);
                  windowHeight = [document.documentElement.clientHeight,self.innerHeight||-1].sort(function(a, b){return(a-b);})[1];
                  bodyHeight = document.body.offsetHeight + marginTop + marginBottom;
                  break;

               default: // Quirks
                  scrollHeight = document.body.scrollHeight;
                  bodyHeight = document.body.clientHeight;
            }
         } else { // Safari & Opera
            scrollHeight = document.documentElement.scrollHeight;
            windowHeight = self.innerHeight;
            bodyHeight = document.documentElement.clientHeight;
         }

         var h = [scrollHeight,windowHeight,bodyHeight].sort(function(a, b){return(a-b);});
         return h[2];
      },

      /**
       * Returns the width of the document.
       * @return {Int} The width of the actual document (which includes the body and its margin).
       */
      getDocumentWidth: function() {
         var docWidth=-1,bodyWidth=-1,winWidth=-1;
         var marginRight = parseInt(util.Dom.getStyle(document.body, 'marginRight'), 10);
         var marginLeft = parseInt(util.Dom.getStyle(document.body, 'marginLeft'), 10);

         var mode = document.compatMode;

         if (mode || isIE) { // (IE, Gecko, Opera)
            switch (mode) {
               case 'CSS1Compat': // Standards mode
                  docWidth = document.documentElement.clientWidth;
                  bodyWidth = document.body.offsetWidth + marginLeft + marginRight;
                  break;

               default: // Quirks
                  bodyWidth = document.body.clientWidth;
                  docWidth = document.body.scrollWidth;
                  break;
            }
         } else { // Safari
            docWidth = document.documentElement.clientWidth;
            bodyWidth = document.body.offsetWidth + marginLeft + marginRight;
         }

         var w = Math.max(docWidth, bodyWidth);
         return w;
      },

      /**
       * Returns the current height of the viewport.
       * @return {Int} The height of the viewable area of the page (excludes scrollbars).
       */
      getViewportHeight: function() {
         var height = -1;
         var mode = document.compatMode;

         if ( (mode || isIE) && !isOpera ) {
            switch (mode) { // (IE, Gecko)
               case 'CSS1Compat': // Standards mode
                  height = document.documentElement.clientHeight;
                  break;

               default: // Quirks
                  height = document.body.clientHeight;
            }
         } else { // Safari, Opera
            height = self.innerHeight;
         }

         return height;
      },

      /**
       * Returns the current width of the viewport.
       * @return {Int} The width of the viewable area of the page (excludes scrollbars).
       */

      getViewportWidth: function() {
         var width = -1;
         var mode = document.compatMode;

         if (mode || isIE) { // (IE, Gecko, Opera)
            switch (mode) {
            case 'CSS1Compat': // Standards mode
               width = document.documentElement.clientWidth;
               break;

            default: // Quirks
               width = document.body.clientWidth;
            }
         } else { // Safari
            width = self.innerWidth;
         }
         return width;
      }
   };
}();

/**
 * @class A region is a representation of an object on a grid.  It is defined
 * by the top, right, bottom, left extents, so is rectangular by default.  If
 * other shapes are required, this class could be extended to support it.
 *
 * @param {int} t the top extent
 * @param {int} r the right extent
 * @param {int} b the bottom extent
 * @param {int} l the left extent
 * @constructor
 */
YAHOO.util.Region = function(t, r, b, l) {

    /**
     * The region's top extent
     * @type int
     */
    this.top = t;

    /**
     * The region's top extent as index, for symmetry with set/getXY
     * @type int
     */
    this[1] = t;

    /**
     * The region's right extent
     * @type int
     */
    this.right = r;

    /**
     * The region's bottom extent
     * @type int
     */
    this.bottom = b;

    /**
     * The region's left extent
     * @type int
     */
    this.left = l;

    /**
     * The region's left extent as index, for symmetry with set/getXY
     * @type int
     */
    this[0] = l;
};

/**
 * Returns true if this region contains the region passed in
 *
 * @param  {Region}  region The region to evaluate
 * @return {boolean}        True if the region is contained with this region,
 *                          else false
 */
YAHOO.util.Region.prototype.contains = function(region) {
    return ( region.left   >= this.left   &&
             region.right  <= this.right  &&
             region.top    >= this.top    &&
             region.bottom <= this.bottom    );

};

/**
 * Returns the area of the region
 *
 * @return {int} the region's area
 */
YAHOO.util.Region.prototype.getArea = function() {
    return ( (this.bottom - this.top) * (this.right - this.left) );
};

/**
 * Returns the region where the passed in region overlaps with this one
 *
 * @param  {Region} region The region that intersects
 * @return {Region}        The overlap region, or null if there is no overlap
 */
YAHOO.util.Region.prototype.intersect = function(region) {
    var t = Math.max( this.top,    region.top    );
    var r = Math.min( this.right,  region.right  );
    var b = Math.min( this.bottom, region.bottom );
    var l = Math.max( this.left,   region.left   );

    if (b >= t && r >= l) {
        return new YAHOO.util.Region(t, r, b, l);
    } else {
        return null;
    }
};

/**
 * Returns the region representing the smallest region that can contain both
 * the passed in region and this region.
 *
 * @param  {Region} region The region that to create the union with
 * @return {Region}        The union region
 */
YAHOO.util.Region.prototype.union = function(region) {
    var t = Math.min( this.top,    region.top    );
    var r = Math.max( this.right,  region.right  );
    var b = Math.max( this.bottom, region.bottom );
    var l = Math.min( this.left,   region.left   );

    return new YAHOO.util.Region(t, r, b, l);
};

/**
 * toString
 * @return string the region properties
 */
YAHOO.util.Region.prototype.toString = function() {
    return ( "Region {"    +
             "top: "       + this.top    +
             ", right: "   + this.right  +
             ", bottom: "  + this.bottom +
             ", left: "    + this.left   +
             "}" );
};

/**
 * Returns a region that is occupied by the DOM element
 *
 * @param  {HTMLElement} el The element
 * @return {Region}         The region that the element occupies
 * @static
 */
YAHOO.util.Region.getRegion = function(el) {
    var p = YAHOO.util.Dom.getXY(el);

    var t = p[1];
    var r = p[0] + el.offsetWidth;
    var b = p[1] + el.offsetHeight;
    var l = p[0];

    return new YAHOO.util.Region(t, r, b, l);
};

/////////////////////////////////////////////////////////////////////////////

/**
 * @class
 *
 * A point is a region that is special in that it represents a single point on
 * the grid.
 *
 * @param {int} x The X position of the point
 * @param {int} y The Y position of the point
 * @constructor
 * @extends Region
 */
YAHOO.util.Point = function(x, y) {
   if (x instanceof Array) { // accept output from Dom.getXY
      y = x[1];
      x = x[0];
   }

    /**
     * The X position of the point, which is also the right, left and index zero (for Dom.getXY symmetry)
     * @type int
     */

    this.x = this.right = this.left = this[0] = x;

    /**
     * The Y position of the point, which is also the top, bottom and index one (for Dom.getXY symmetry)
     * @type int
     */
    this.y = this.top = this.bottom = this[1] = y;
};

YAHOO.util.Point.prototype = new YAHOO.util.Region();


/*** End of file, dom.js ***/

/*** Start of file, dragdrop.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.4
*/

/**
 * Defines the interface and base operation of items that that can be
 * dragged or can be drop targets.  It was designed to be extended, overriding
 * the event handlers for startDrag, onDrag, onDragOver, onDragOut.
 * Up to three html elements can be associated with a DragDrop instance:
 * <ul>
 * <li>linked element: the element that is passed into the constructor.
 * This is the element which defines the boundaries for interaction with
 * other DragDrop objects.</li>
 * <li>handle element(s): The drag operation only occurs if the element that
 * was clicked matches a handle element.  By default this is the linked
 * element, but there are times that you will want only a portion of the
 * linked element to initiate the drag operation, and the setHandleElId()
 * method provides a way to define this.</li>
 * <li>drag element: this represents an the element that would be moved along
 * with the cursor during a drag operation.  By default, this is the linked
 * element itself as in {@link YAHOO.util.DD}.  setDragElId() lets you define
 * a separate element that would be moved, as in {@link YAHOO.util.DDProxy}
 * </li>
 * </ul>
 * This class should not be instantiated until the onload event to ensure that
 * the associated elements are available.
 * The following would define a DragDrop obj that would interact with any
 * other * DragDrop obj in the "group1" group:
 * <pre>
 *  dd = new YAHOO.util.DragDrop("div1", "group1");
 * </pre>
 * Since none of the event handlers have been implemented, nothing would
 * actually happen if you were to run the code above.  Normally you would
 * override this class or one of the default implementations, but you can
 * also override the methods you want on an instance of the class...
 * <pre>
 *  dd.onDragDrop = function(e, id) {
 *   alert("dd was dropped on " + id);
 *  }
 * </pre>
 * @constructor
 * @param {String} id of the element that is linked to this instance
 * @param {String} sGroup the group of related DragDrop objects
 * @param {object} config an object containing configurable attributes
 *                Valid properties for DragDrop:
 *                    padding, isTarget, maintainOffset, primaryButtonOnly
 */
YAHOO.util.DragDrop = function(id, sGroup, config) {
    if (id) {
        this.init(id, sGroup, config);
    }
};

YAHOO.util.DragDrop.prototype = {

    /**
     * The id of the element associated with this object.  This is what we
     * refer to as the "linked element" because the size and position of
     * this element is used to determine when the drag and drop objects have
     * interacted.
     *
     * @type String
     */
    id: null,

    /**
     * Configuration attributes passed into the constructor
     * @type object
     */
    config: null,

    /**
     * The id of the element that will be dragged.  By default this is same
     * as the linked element , but could be changed to another element. Ex:
     * YAHOO.util.DDProxy
     *
     * @type String
     * @private
     */
    dragElId: null,

    /**
     * the id of the element that initiates the drag operation.  By default
     * this is the linked element, but could be changed to be a child of this
     * element.  This lets us do things like only starting the drag when the
     * header element within the linked html element is clicked.
     *
     * @type String
     * @private
     */
    handleElId: null,

    /**
     * An associative array of HTML tags that will be ignored if clicked.
     * @type {string: string}
     */
    invalidHandleTypes: null,

    /**
     * An associative array of ids for elements that will be ignored if clicked
     * @type {string: string}
     */
    invalidHandleIds: null,

    /**
     * An indexted array of css class names for elements that will be ignored
     * if clicked.
     * @type string[]
     */
    invalidHandleClasses: null,

    /**
     * The linked element's absolute X position at the time the drag was
     * started
     *
     * @type int
     * @private
     */
    startPageX: 0,

    /**
     * The linked element's absolute X position at the time the drag was
     * started
     *
     * @type int
     * @private
     */
    startPageY: 0,

    /**
     * The group defines a logical collection of DragDrop objects that are
     * related.  Instances only get events when interacting with other
     * DragDrop object in the same group.  This lets us define multiple
     * groups using a single DragDrop subclass if we want.
     * @type {string: string}
     */
    groups: null,

    /**
     * Individual drag/drop instances can be locked.  This will prevent
     * onmousedown start drag.
     *
     * @type boolean
     * @private
     */
    locked: false,

    /**
     * Lock this instance
     */
    lock: function() { this.locked = true; },

    /**
     * Unlock this instace
     */
    unlock: function() { this.locked = false; },

    /**
     * By default, all insances can be a drop target.  This can be disabled by
     * setting isTarget to false.
     *
     * @type boolean
     */
    isTarget: true,

    /**
     * The padding configured for this drag and drop object for calculating
     * the drop zone intersection with this object.
     * @type int[]
     */
    padding: null,

    /**
     * @private
     */
    _domRef: null,

    /**
     * Internal typeof flag
     * @private
     */
    __ygDragDrop: true,

    /**
     * Set to true when horizontal contraints are applied
     *
     * @type boolean
     * @private
     */
    constrainX: false,

    /**
     * Set to true when vertical contraints are applied
     *
     * @type boolean
     * @private
     */
    constrainY: false,

    /**
     * The left constraint
     *
     * @type int
     * @private
     */
    minX: 0,

    /**
     * The right constraint
     *
     * @type int
     * @private
     */
    maxX: 0,

    /**
     * The up constraint
     *
     * @type int
     * @private
     */
    minY: 0,

    /**
     * The down constraint
     *
     * @type int
     * @private
     */
    maxY: 0,

    /**
     * Maintain offsets when we resetconstraints.  Used to maintain the
     * slider thumb value, and this needs to be fixed.
     * @type boolean
     */
    maintainOffset: false,

    /**
     * Array of pixel locations the element will snap to if we specified a
     * horizontal graduation/interval.  This array is generated automatically
     * when you define a tick interval.
     * @type int[]
     */
    xTicks: null,

    /**
     * Array of pixel locations the element will snap to if we specified a
     * vertical graduation/interval.  This array is generated automatically
     * when you define a tick interval.
     * @type int[]
     */
    yTicks: null,

    /**
     * By default the drag and drop instance will only respond to the primary
     * button click (left button for a right-handed mouse).  Set to true to
     * allow drag and drop to start with any mouse click that is propogated
     * by the browser
     * @type boolean
     */
    primaryButtonOnly: true,

    /**
     * The availabe property is false until the linked dom element is accessible.
     * @type boolean
     */
    available: false,

    /**
     * Code that executes immediately before the startDrag event
     * @private
     */
    b4StartDrag: function(x, y) { },

    /**
     * Abstract method called after a drag/drop object is clicked
     * and the drag or mousedown time thresholds have beeen met.
     *
     * @param {int} X click location
     * @param {int} Y click location
     */
    startDrag: function(x, y) { /* override this */ },

    /**
     * Code that executes immediately before the onDrag event
     * @private
     */
    b4Drag: function(e) { },

    /**
     * Abstract method called during the onMouseMove event while dragging an
     * object.
     *
     * @param {Event} e
     */
    onDrag: function(e) { /* override this */ },

    /**
     * Code that executes immediately before the onDragEnter event
     * @private
     */
    // b4DragEnter: function(e) { },

    /**
     * Abstract method called when this element fist begins hovering over
     * another DragDrop obj
     *
     * @param {Event} e
     * @param {String || YAHOO.util.DragDrop[]} id In POINT mode, the element
     * id this is hovering over.  In INTERSECT mode, an array of one or more
     * dragdrop items being hovered over.
     */
    onDragEnter: function(e, id) { /* override this */ },

    /**
     * Code that executes immediately before the onDragOver event
     * @private
     */
    b4DragOver: function(e) { },

    /**
     * Abstract method called when this element is hovering over another
     * DragDrop obj
     *
     * @param {Event} e
     * @param {String || YAHOO.util.DragDrop[]} id In POINT mode, the element
     * id this is hovering over.  In INTERSECT mode, an array of dd items
     * being hovered over.
     */
    onDragOver: function(e, id) { /* override this */ },

    /**
     * Code that executes immediately before the onDragOut event
     * @private
     */
    b4DragOut: function(e) { },

    /**
     * Abstract method called when we are no longer hovering over an element
     *
     * @param {Event} e
     * @param {String || YAHOO.util.DragDrop[]} id In POINT mode, the element
     * id this was hovering over.  In INTERSECT mode, an array of dd items
     * that the mouse is no longer over.
     */
    onDragOut: function(e, id) { /* override this */ },

    /**
     * Code that executes immediately before the onDragDrop event
     * @private
     */
    b4DragDrop: function(e) { },

    /**
     * Abstract method called when this item is dropped on another DragDrop
     * obj
     *
     * @param {Event} e
     * @param {String || YAHOO.util.DragDrop[]} id In POINT mode, the element
     * id this was dropped on.  In INTERSECT mode, an array of dd items this
     * was dropped on.
     */
    onDragDrop: function(e, id) { /* override this */ },

    /**
     * Code that executes immediately before the endDrag event
     * @private
     */
    b4EndDrag: function(e) { },

    /**
     * Fired when we are done dragging the object
     *
     * @param {Event} e
     */
    endDrag: function(e) { /* override this */ },

    /**
     * Code executed immediately before the onMouseDown event

     * @param {Event} e
     * @private
     */
    b4MouseDown: function(e) {  },

    /**
     * Event handler that fires when a drag/drop obj gets a mousedown
     * @param {Event} e
     */
    onMouseDown: function(e) { /* override this */ },

    /**
     * Event handler that fires when a drag/drop obj gets a mouseup
     * @param {Event} e
     */
    onMouseUp: function(e) { /* override this */ },

    /**
     * Override the onAvailable method to do what is needed after the initial
     * position was determined.
     */
    onAvailable: function () {
    },

    /**
     * Returns a reference to the linked element
     *
     * @return {HTMLElement} the html element
     */
    getEl: function() {
        if (!this._domRef) {
            this._domRef = YAHOO.util.Dom.get(this.id);
        }

        return this._domRef;
    },

    /**
     * Returns a reference to the actual element to drag.  By default this is
     * the same as the html element, but it can be assigned to another
     * element. An example of this can be found in YAHOO.util.DDProxy
     *
     * @return {HTMLElement} the html element
     */
    getDragEl: function() {
        return YAHOO.util.Dom.get(this.dragElId);
    },

    /**
     * Sets up the DragDrop object.  Must be called in the constructor of any
     * YAHOO.util.DragDrop subclass
     *
     * @param id the id of the linked element
     * @param {String} sGroup the group of related items
     * @param {object} config configuration attributes
     */
    init: function(id, sGroup, config) {
        this.initTarget(id, sGroup, config);
        YAHOO.util.Event.addListener(this.id, "mousedown",
                                          this.handleMouseDown, this, true);
    },

    /**
     * Initializes Targeting functionality only... the object does not
     * get a mousedown handler.
     *
     * @param id the id of the linked element
     * @param {String} sGroup the group of related items
     * @param {object} config configuration attributes
     */
    initTarget: function(id, sGroup, config) {

        // configuration attributes
        this.config = config || {};

        // create a local reference to the drag and drop manager
        this.DDM = YAHOO.util.DDM;
        // initialize the groups array
        this.groups = {};

        // set the id
        this.id = id;

        // add to an interaction group
        this.addToGroup((sGroup) ? sGroup : "default");

        // We don't want to register this as the handle with the manager
        // so we just set the id rather than calling the setter.
        this.handleElId = id;

        YAHOO.util.Event.onAvailable(id, this.handleOnAvailable, this, true);


        // the linked element is the element that gets dragged by default
        this.setDragElId(id);

        // by default, clicked anchors will not start drag operations.
        // @TODO what else should be here?  Probably form fields.
        this.invalidHandleTypes = { A: "A" };
        this.invalidHandleIds = {};
        this.invalidHandleClasses = [];

        this.applyConfig();
    },

    /**
     * Applies the configuration parameters that were passed into the constructor.
     * This is supposed to happen at each level through the inheritance chain.  So
     * a DDProxy implentation will execute apply config on DDProxy, DD, and
     * DragDrop in order to get all of the parameters that are available in
     * each object.
     */
    applyConfig: function() {

        // configurable properties:
        //    padding, isTarget, maintainOffset, primaryButtonOnly
        this.padding           = this.config.padding || [0, 0, 0, 0];
        this.isTarget          = (this.config.isTarget !== false);
        this.maintainOffset    = (this.config.maintainOffset);
        this.primaryButtonOnly = (this.config.primaryButtonOnly !== false);

    },

    /**
     * Executed when the linked element is available
     * @private
     */
    handleOnAvailable: function() {
        this.available = true;
        this.resetConstraints();
        this.onAvailable();
    },

     /**
     * Configures the padding for the target zone in px.  Effectively expands
     * (or reduces) the virtual object size for targeting calculations.
     * Supports css-style shorthand; if only one parameter is passed, all sides
     * will have that padding, and if only two are passed, the top and bottom
     * will have the first param, the left and right the second.
     * @param {int} iTop    Top pad
     * @param {int} iRight  Right pad
     * @param {int} iBot    Bot pad
     * @param {int} iLeft   Left pad
     */
    setPadding: function(iTop, iRight, iBot, iLeft) {
        // this.padding = [iLeft, iRight, iTop, iBot];
        if (!iRight && 0 !== iRight) {
            this.padding = [iTop, iTop, iTop, iTop];
        } else if (!iBot && 0 !== iBot) {
            this.padding = [iTop, iRight, iTop, iRight];
        } else {
            this.padding = [iTop, iRight, iBot, iLeft];
        }
    },

    /**
     * Stores the initial placement of the dd element
     */
    setInitPosition: function(diffX, diffY) {
        var el = this.getEl();

        if (!this.DDM.verifyEl(el)) {
            return;
        }

        var dx = diffX || 0;
        var dy = diffY || 0;

        var p = YAHOO.util.Dom.getXY( el );

        this.initPageX = p[0] - dx;
        this.initPageY = p[1] - dy;

        this.lastPageX = p[0];
        this.lastPageY = p[1];


        this.setStartPosition(p);
    },

    /**
     * Sets the start position of the element.  This is set when the obj
     * is initialized, the reset when a drag is started.
     * @param pos current position (from previous lookup)
     * @private
     */
    setStartPosition: function(pos) {
        var p = pos || YAHOO.util.Dom.getXY( this.getEl() );
        this.deltaSetXY = null;

        this.startPageX = p[0];
        this.startPageY = p[1];
    },

    /**
     * Add this instance to a group of related drag/drop objects.  All
     * instances belong to at least one group, and can belong to as many
     * groups as needed.
     *
     * @param sGroup {string} the name of the group
     */
    addToGroup: function(sGroup) {
        this.groups[sGroup] = true;
        this.DDM.regDragDrop(this, sGroup);
    },

    /**
     * Remove's this instance from the supplied interaction group
     * @param {string}  sGroup  The group to drop
     */
    removeFromGroup: function(sGroup) {
        if (this.groups[sGroup]) {
            delete this.groups[sGroup];
        }

        this.DDM.removeDDFromGroup(this, sGroup);
    },

    /**
     * Allows you to specify that an element other than the linked element
     * will be moved with the cursor during a drag
     *
     * @param id the id of the element that will be used to initiate the drag
     */
    setDragElId: function(id) {
        this.dragElId = id;
    },

    /**
     * Allows you to specify a child of the linked element that should be
     * used to initiate the drag operation.  An example of this would be if
     * you have a content div with text and links.  Clicking anywhere in the
     * content area would normally start the drag operation.  Use this method
     * to specify that an element inside of the content div is the element
     * that starts the drag operation.
     *
     * @param id the id of the element that will be used to initiate the drag
     */
    setHandleElId: function(id) {
        this.handleElId = id;
        this.DDM.regHandle(this.id, id);
    },

    /**
     * Allows you to set an element outside of the linked element as a drag
     * handle
     */
    setOuterHandleElId: function(id) {
        YAHOO.util.Event.addListener(id, "mousedown",
                this.handleMouseDown, this, true);
        this.setHandleElId(id);
    },

    /**
     * Remove all drag and drop hooks for this element
     */
    unreg: function() {
        YAHOO.util.Event.removeListener(this.id, "mousedown",
                this.handleMouseDown);
        this._domRef = null;
        this.DDM._remove(this);
    },

    /**
     * Returns true if this instance is locked, or the drag drop mgr is locked
     * (meaning that all drag/drop is disabled on the page.)
     *
     * @return {boolean} true if this obj or all drag/drop is locked, else
     * false
     */
    isLocked: function() {
        return (this.DDM.isLocked() || this.locked);
    },

    /**
     * Fired when this object is clicked
     *
     * @param {Event} e
     * @param {YAHOO.util.DragDrop} oDD the clicked dd object (this dd obj)
     * @private
     */
    handleMouseDown: function(e, oDD) {


        var EU = YAHOO.util.Event;

        var button = e.which || e.button;

        if (this.primaryButtonOnly && button > 1) {
            return;
        }

        if (this.isLocked()) {
            return;
        }

        this.DDM.refreshCache(this.groups);
        // var self = this;
        // setTimeout( function() { self.DDM.refreshCache(self.groups); }, 0);

        // Only process the event if we really clicked within the linked
        // element.  The reason we make this check is that in the case that
        // another element was moved between the clicked element and the
        // cursor in the time between the mousedown and mouseup events. When
        // this happens, the element gets the next mousedown event
        // regardless of where on the screen it happened.
        var pt = new YAHOO.util.Point(EU.getPageX(e), EU.getPageY(e));
        if ( !this.DDM.isOverTarget(pt, this) )  {
        } else {


            //  check to see if the handle was clicked
            var srcEl = EU.getTarget(e);

            if (this.isValidHandleChild(srcEl) &&
                    (this.id == this.handleElId ||
                     this.DDM.handleWasClicked(srcEl, this.id)) ) {


                // set the initial element position
                this.setStartPosition();


                this.b4MouseDown(e);
                this.onMouseDown(e);
                this.DDM.handleMouseDown(e, this);

                this.DDM.stopEvent(e);
            }
        }
    },

    /**
     * Allows you to specify a tag name that should not start a drag operation
     * when clicked.  This is designed to facilitate embedding links within a
     * drag handle that do something other than start the drag.
     *
     * @param {string} tagName the type of element to exclude
     */
    addInvalidHandleType: function(tagName) {
        var type = tagName.toUpperCase();
        this.invalidHandleTypes[type] = type;
    },

    /**
     * Lets you to specify an element id for a child of a drag handle
     * that should not initiate a drag
     * @param {string} id the element id of the element you wish to ignore
     */
    addInvalidHandleId: function(id) {
        this.invalidHandleIds[id] = id;
    },

    /**
     * Lets you specify a css class of elements that will not initiate a drag
     * @param {string} cssClass the class of the elements you wish to ignore
     */
    addInvalidHandleClass: function(cssClass) {
        this.invalidHandleClasses.push(cssClass);
    },

    /**
     * Unsets an excluded tag name set by addInvalidHandleType
     *
     * @param {string} tagName the type of element to unexclude
     */
    removeInvalidHandleType: function(tagName) {
        var type = tagName.toUpperCase();
        // this.invalidHandleTypes[type] = null;
        delete this.invalidHandleTypes[type];
    },

    /**
     * Unsets an invalid handle id
     * @param {string} the id of the element to re-enable
     */
    removeInvalidHandleId: function(id) {
        delete this.invalidHandleIds[id];
    },

    /**
     * Unsets an invalid css class
     * @param {string} the class of the element(s) you wish to re-enable
     */
    removeInvalidHandleClass: function(cssClass) {
        for (var i=0, len=this.invalidHandleClasses.length; i<len; ++i) {
            if (this.invalidHandleClasses[i] == cssClass) {
                delete this.invalidHandleClasses[i];
            }
        }
    },

    /**
     * Checks the tag exclusion list to see if this click should be ignored
     *
     * @param {ygNode} node
     * @return {boolean} true if this is a valid tag type, false if not
     */
    isValidHandleChild: function(node) {

        var valid = true;
        // var n = (node.nodeName == "#text") ? node.parentNode : node;
        var nodeName;
        try {
            nodeName = node.nodeName.toUpperCase();
        } catch(e) {
            nodeName = node.nodeName;
        }
        valid = valid && !this.invalidHandleTypes[nodeName];
        valid = valid && !this.invalidHandleIds[node.id];

        for (var i=0, len=this.invalidHandleClasses.length; valid && i<len; ++i) {
            valid = !YAHOO.util.Dom.hasClass(node, this.invalidHandleClasses[i]);
        }


        return valid;

    },

    /**
     * Create the array of horizontal tick marks if an interval was specified
     * in setXConstraint().
     *
     * @private
     */
    setXTicks: function(iStartX, iTickSize) {
        this.xTicks = [];
        this.xTickSize = iTickSize;

        var tickMap = {};

        for (var i = this.initPageX; i >= this.minX; i = i - iTickSize) {
            if (!tickMap[i]) {
                this.xTicks[this.xTicks.length] = i;
                tickMap[i] = true;
            }
        }

        for (i = this.initPageX; i <= this.maxX; i = i + iTickSize) {
            if (!tickMap[i]) {
                this.xTicks[this.xTicks.length] = i;
                tickMap[i] = true;
            }
        }

        this.xTicks.sort(this.DDM.numericSort) ;
    },

    /**
     * Create the array of vertical tick marks if an interval was specified in
     * setYConstraint().
     *
     * @private
     */
    setYTicks: function(iStartY, iTickSize) {
        this.yTicks = [];
        this.yTickSize = iTickSize;

        var tickMap = {};

        for (var i = this.initPageY; i >= this.minY; i = i - iTickSize) {
            if (!tickMap[i]) {
                this.yTicks[this.yTicks.length] = i;
                tickMap[i] = true;
            }
        }

        for (i = this.initPageY; i <= this.maxY; i = i + iTickSize) {
            if (!tickMap[i]) {
                this.yTicks[this.yTicks.length] = i;
                tickMap[i] = true;
            }
        }

        this.yTicks.sort(this.DDM.numericSort) ;
    },

    /**
     * By default, the element can be dragged any place on the screen.  Use
     * this method to limit the horizontal travel of the element.  Pass in
     * 0,0 for the parameters if you want to lock the drag to the y axis.
     *
     * @param {int} iLeft the number of pixels the element can move to the left
     * @param {int} iRight the number of pixels the element can move to the
     * right
     * @param {int} iTickSize optional parameter for specifying that the
     * element
     * should move iTickSize pixels at a time.
     */
    setXConstraint: function(iLeft, iRight, iTickSize) {
        this.leftConstraint = iLeft;
        this.rightConstraint = iRight;

        this.minX = this.initPageX - iLeft;
        this.maxX = this.initPageX + iRight;
        if (iTickSize) { this.setXTicks(this.initPageX, iTickSize); }

        this.constrainX = true;
    },

    /**
     * Clears any constraints applied to this instance.  Also clears ticks
     * since they can't exist independent of a constraint at this time.
     */
    clearConstraints: function() {
        this.constrainX = false;
        this.constrainY = false;
        this.clearTicks();
    },

    /**
     * Clears any tick interval defined for this instance
     */
    clearTicks: function() {
        this.xTicks = null;
        this.yTicks = null;
        this.xTickSize = 0;
        this.yTickSize = 0;
    },

    /**
     * By default, the element can be dragged any place on the screen.  Set
     * this to limit the vertical travel of the element.  Pass in 0,0 for the
     * parameters if you want to lock the drag to the x axis.
     *
     * @param {int} iUp the number of pixels the element can move up
     * @param {int} iDown the number of pixels the element can move down
     * @param {int} iTickSize optional parameter for specifying that the
     * element should move iTickSize pixels at a time.
     */
    setYConstraint: function(iUp, iDown, iTickSize) {
        this.topConstraint = iUp;
        this.bottomConstraint = iDown;

        this.minY = this.initPageY - iUp;
        this.maxY = this.initPageY + iDown;
        if (iTickSize) { this.setYTicks(this.initPageY, iTickSize); }

        this.constrainY = true;

    },

    /**
     * resetConstraints must be called if you manually reposition a dd element.
     * @param {boolean} maintainOffset
     */
    resetConstraints: function() {


        // Maintain offsets if necessary
        if (this.initPageX || this.initPageX === 0) {
            // figure out how much this thing has moved
            var dx = (this.maintainOffset) ? this.lastPageX - this.initPageX : 0;
            var dy = (this.maintainOffset) ? this.lastPageY - this.initPageY : 0;

            this.setInitPosition(dx, dy);

        // This is the first time we have detected the element's position
        } else {
            this.setInitPosition();
        }

        if (this.constrainX) {
            this.setXConstraint( this.leftConstraint,
                                 this.rightConstraint,
                                 this.xTickSize        );
        }

        if (this.constrainY) {
            this.setYConstraint( this.topConstraint,
                                 this.bottomConstraint,
                                 this.yTickSize         );
        }
    },

    /**
     * Normally the drag element is moved pixel by pixel, but we can specify
     * that it move a number of pixels at a time.  This method resolves the
     * location when we have it set up like this.
     *
     * @param {int} val where we want to place the object
     * @param {int[]} tickArray sorted array of valid points
     * @return {int} the closest tick
     * @private
     */
    getTick: function(val, tickArray) {

        if (!tickArray) {
            // If tick interval is not defined, it is effectively 1 pixel,
            // so we return the value passed to us.
            return val;
        } else if (tickArray[0] >= val) {
            // The value is lower than the first tick, so we return the first
            // tick.
            return tickArray[0];
        } else {
            for (var i=0, len=tickArray.length; i<len; ++i) {
                var next = i + 1;
                if (tickArray[next] && tickArray[next] >= val) {
                    var diff1 = val - tickArray[i];
                    var diff2 = tickArray[next] - val;
                    return (diff2 > diff1) ? tickArray[i] : tickArray[next];
                }
            }

            // The value is larger than the last tick, so we return the last
            // tick.
            return tickArray[tickArray.length - 1];
        }
    },

    /**
     * toString method
     * @return {string} string representation of the dd obj
     */
    toString: function() {
        return ("DragDrop " + this.id);
    }

};

// Only load the library once.  Rewriting the manager class would orphan
// existing drag and drop instances.
if (!YAHOO.util.DragDropMgr) {

    /**
     * Handles the element interaction for all DragDrop items in the
     * window.  Generally, you will not call this class directly, but it does
     * have helper methods that could be useful in your DragDrop
     * implementations.  This class should not be instantiated; all methods
     * are are static.
     *
     * @constructor
     */
    YAHOO.util.DragDropMgr = new function() {

        /**
         * Two dimensional Array of registered DragDrop objects.  The first
         * dimension is the DragDrop item group, the second the DragDrop
         * object.
         *
         * @type {string: string}
         * @private
         */
        this.ids = {};

        /**
         * Array of element ids defined as drag handles.  Used to determine
         * if the element that generated the mousedown event is actually the
         * handle and not the html element itself.
         *
         * @type {string: string}
         * @private
         */
        this.handleIds = {};

        /**
         * the DragDrop object that is currently being dragged
         *
         * @type DragDrop
         * @private
         **/
        this.dragCurrent = null;

        /**
         * the DragDrop object(s) that are being hovered over
         *
         * @type Array
         * @private
         */
        this.dragOvers = {};

        /**
         * @private
         */

        /**
         * the X distance between the cursor and the object being dragged
         *
         * @type int
         * @private
         */
        this.deltaX = 0;

        /**
         * the Y distance between the cursor and the object being dragged
         *
         * @type int
         * @private
         */
        this.deltaY = 0;

        /**
         * Flag to determine if we should prevent the default behavior of the
         * events we define. By default this is true, but this can be set to
         * false if you need the default behavior (not recommended)
         *
         * @type boolean
         */
        this.preventDefault = true;

        /**
         * Flag to determine if we should stop the propagation of the events
         * we generate. This is true by default but you may want to set it to
         * false if the html element contains other features that require the
         * mouse click.
         *
         * @type boolean
         */
        this.stopPropagation = true;

        /**
         * @private
         */
        this.initalized = false;

        /**
         * All drag and drop can be disabled.
         *
         * @private
         */
        this.locked = false;

        /**
         * Called the first time an element is registered.
         *
         * @private
         */
        this.init = function() {
            this.initialized = true;
        };

        /**
         * In point mode, drag and drop interaction is defined by the
         * location of the cursor during the drag/drop
         * @type int
         */
        this.POINT     = 0;

        /**
         * In intersect mode, drag and drop interactio nis defined by the
         * overlap of two or more drag and drop objects.
         * @type int
         */
        this.INTERSECT = 1;

        /**
         * The current drag and drop mode.  Default it point mode
         * @type int
         */
        this.mode = this.POINT;

        /**
         * Runs method on all drag and drop objects
         * @private
         */
        this._execOnAll = function(sMethod, args) {
            for (var i in this.ids) {
                for (var j in this.ids[i]) {
                    var oDD = this.ids[i][j];
                    if (! this.isTypeOfDD(oDD)) {
                        continue;
                    }
                    oDD[sMethod].apply(oDD, args);
                }
            }
        };

        /**
         * Drag and drop initialization.  Sets up the global event handlers
         * @private
         */
        this._onLoad = function() {

            this.init();


            var EU = YAHOO.util.Event;

            EU.on(document, "mouseup",   this.handleMouseUp, this, true);
            EU.on(document, "mousemove", this.handleMouseMove, this, true);
            EU.on(window,   "unload",    this._onUnload, this, true);
            EU.on(window,   "resize",    this._onResize, this, true);
            // EU.on(window,   "mouseout",    this._test);

        };

        /**
         * Reset constraints on all drag and drop objs
         * @private
         */
        this._onResize = function(e) {
            this._execOnAll("resetConstraints", []);
        };

        /**
         * Lock all drag and drop functionality
         */
        this.lock = function() { this.locked = true; };

        /**
         * Unlock all drag and drop functionality
         */
        this.unlock = function() { this.locked = false; };

        /**
         * Is drag and drop locked?
         *
         * @return {boolean} True if drag and drop is locked, false otherwise.
         */
        this.isLocked = function() { return this.locked; };

        /**
         * Location cache that is set for all drag drop objects when a drag is
         * initiated, cleared when the drag is finished.
         *
         * @private
         */
        this.locationCache = {};

        /**
         * Set useCache to false if you want to force object the lookup of each
         * drag and drop linked element constantly during a drag.
         * @type boolean
         */
        this.useCache = true;

        /**
         * The number of pixels that the mouse needs to move after the
         * mousedown before the drag is initiated.  Default=3;
         * @type int
         */
        this.clickPixelThresh = 3;

        /**
         * The number of milliseconds after the mousedown event to initiate the
         * drag if we don't get a mouseup event. Default=1000
         * @type int
         */
        this.clickTimeThresh = 1000;

        /**
         * Flag that indicates that either the drag pixel threshold or the
         * mousdown time threshold has been met
         * @type boolean
         * @private
         */
        this.dragThreshMet = false;

        /**
         * Timeout used for the click time threshold
         * @type Object
         * @private
         */
        this.clickTimeout = null;

        /**
         * The X position of the mousedown event stored for later use when a
         * drag threshold is met.
         * @type int
         * @private
         */
        this.startX = 0;

        /**
         * The Y position of the mousedown event stored for later use when a
         * drag threshold is met.
         * @type int
         * @private
         */
        this.startY = 0;

        /**
         * Each DragDrop instance must be registered with the DragDropMgr.
         * This is executed in DragDrop.init()
         *
         * @param {DragDrop} oDD the DragDrop object to register
         * @param {String} sGroup the name of the group this element belongs to
         */
        this.regDragDrop = function(oDD, sGroup) {
            if (!this.initialized) { this.init(); }

            if (!this.ids[sGroup]) {
                this.ids[sGroup] = {};
            }
            this.ids[sGroup][oDD.id] = oDD;
        };

        /**
         * Removes the supplied dd instance from the supplied group. Executed
         * by DragDrop.removeFromGroup.
         * @private
         */
        this.removeDDFromGroup = function(oDD, sGroup) {
            if (!this.ids[sGroup]) {
                this.ids[sGroup] = {};
            }

            var obj = this.ids[sGroup];
            if (obj && obj[oDD.id]) {
                delete obj[oDD.id];
            }
        };

        /**
         * Unregisters a drag and drop item.  This is executed in
         * DragDrop.unreg, use that method instead of calling this directly.
         * @private
         */
        this._remove = function(oDD) {
            for (var g in oDD.groups) {
                if (g && this.ids[g][oDD.id]) {
                    delete this.ids[g][oDD.id];
                }
            }
            delete this.handleIds[oDD.id];
        };

        /**
         * Each DragDrop handle element must be registered.  This is done
         * automatically when executing DragDrop.setHandleElId()
         *
         * @param {String} sDDId the DragDrop id this element is a handle for
         * @param {String} sHandleId the id of the element that is the drag
         * handle
         */
        this.regHandle = function(sDDId, sHandleId) {
            if (!this.handleIds[sDDId]) {
                this.handleIds[sDDId] = {};
            }
            this.handleIds[sDDId][sHandleId] = sHandleId;
        };

        /**
         * Utility function to determine if a given element has been
         * registered as a drag drop item.
         *
         * @param {String} id the element id to check
         * @return {boolean} true if this element is a DragDrop item,
         * false otherwise
         */
        this.isDragDrop = function(id) {
            return ( this.getDDById(id) ) ? true : false;
        };

        /**
         * Returns the drag and drop instances that are in all groups the
         * passed in instance belongs to.
         *
         * @param {DragDrop} p_oDD the obj to get related data for
         * @param {boolean} bTargetsOnly if true, only return targetable objs
         * @return {DragDrop[]} the related instances
         */
        this.getRelated = function(p_oDD, bTargetsOnly) {
            var oDDs = [];
            for (var i in p_oDD.groups) {
                for (j in this.ids[i]) {
                    var dd = this.ids[i][j];
                    if (! this.isTypeOfDD(dd)) {
                        continue;
                    }
                    if (!bTargetsOnly || dd.isTarget) {
                        oDDs[oDDs.length] = dd;
                    }
                }
            }

            return oDDs;
        };

        /**
         * Returns true if the specified dd target is a legal target for
         * the specifice drag obj
         *
         * @param {DragDrop} the drag obj
         * @param {DragDrop) the target
         * @return {boolean} true if the target is a legal target for the
         * dd obj
         */
        this.isLegalTarget = function (oDD, oTargetDD) {
            var targets = this.getRelated(oDD, true);
            for (var i=0, len=targets.length;i<len;++i) {
                if (targets[i].id == oTargetDD.id) {
                    return true;
                }
            }

            return false;
        };

        /**
         * My goal is to be able to transparently determine if an object is
         * typeof DragDrop, and the exact subclass of DragDrop.  typeof
         * returns "object", oDD.constructor.toString() always returns
         * "DragDrop" and not the name of the subclass.  So for now it just
         * evaluates a well-known variable in DragDrop.
         *
         * @param {Object} the object to evaluate
         * @return {boolean} true if typeof oDD = DragDrop
         */
        this.isTypeOfDD = function (oDD) {
            return (oDD && oDD.__ygDragDrop);
        };

        /**
         * Utility function to determine if a given element has been
         * registered as a drag drop handle for the given Drag Drop object.
         *
         * @param {String} id the element id to check
         * @return {boolean} true if this element is a DragDrop handle, false
         * otherwise
         */
        this.isHandle = function(sDDId, sHandleId) {
            return ( this.handleIds[sDDId] &&
                            this.handleIds[sDDId][sHandleId] );
        };

        /**
         * Returns the DragDrop instance for a given id
         *
         * @param {String} id the id of the DragDrop object
         * @return {DragDrop} the drag drop object, null if it is not found
         */
        this.getDDById = function(id) {
            for (var i in this.ids) {
                if (this.ids[i][id]) {
                    return this.ids[i][id];
                }
            }
            return null;
        };

        /**
         * Fired after a registered DragDrop object gets the mousedown event.
         * Sets up the events required to track the object being dragged
         *
         * @param {Event} e the event
         * @param oDD the DragDrop object being dragged
         * @private
         */
        this.handleMouseDown = function(e, oDD) {

            this.currentTarget = YAHOO.util.Event.getTarget(e);

            this.dragCurrent = oDD;

            var el = oDD.getEl();

            // track start position
            this.startX = YAHOO.util.Event.getPageX(e);
            this.startY = YAHOO.util.Event.getPageY(e);

            this.deltaX = this.startX - el.offsetLeft;
            this.deltaY = this.startY - el.offsetTop;

            this.dragThreshMet = false;

            this.clickTimeout = setTimeout(
                    function() {
                        var DDM = YAHOO.util.DDM;
                        DDM.startDrag(DDM.startX, DDM.startY);
                    },
                    this.clickTimeThresh );
        };

        /**
         * Fired when either the drag pixel threshol or the mousedown hold
         * time threshold has been met.
         *
         * @param x {int} the X position of the original mousedown
         * @param y {int} the Y position of the original mousedown
         */
        this.startDrag = function(x, y) {
            clearTimeout(this.clickTimeout);
            if (this.dragCurrent) {
                this.dragCurrent.b4StartDrag(x, y);
                this.dragCurrent.startDrag(x, y);
            }
            this.dragThreshMet = true;
        };

        /**
         * Internal function to handle the mouseup event.  Will be invoked
         * from the context of the document.
         *
         * @param {Event} e the event
         * @private
         */
        this.handleMouseUp = function(e) {

            if (! this.dragCurrent) {
                return;
            }

            clearTimeout(this.clickTimeout);

            if (this.dragThreshMet) {
                this.fireEvents(e, true);
            } else {
            }

            this.stopDrag(e);

            this.stopEvent(e);
        };

        /**
         * Utility to stop event propagation and event default, if these
         * features are turned on.
         *
         * @param {Event} e the event as returned by this.getEvent()
         */
        this.stopEvent = function(e) {
            if (this.stopPropagation) {
                YAHOO.util.Event.stopPropagation(e);
            }

            if (this.preventDefault) {
                YAHOO.util.Event.preventDefault(e);
            }
        };

        /**
         * Internal function to clean up event handlers after the drag
         * operation is complete
         *
         * @param {Event} e the event
         * @private
         */
        this.stopDrag = function(e) {

            // Fire the drag end event for the item that was dragged
            if (this.dragCurrent) {
                if (this.dragThreshMet) {
                    this.dragCurrent.b4EndDrag(e);
                    this.dragCurrent.endDrag(e);
                }

                this.dragCurrent.onMouseUp(e);
            }

            this.dragCurrent = null;
            this.dragOvers = {};
        };

        /**
         * Internal function to handle the mousemove event.  Will be invoked
         * from the context of the html element.
         *
         * @TODO figure out what we can do about mouse events lost when the
         * user drags objects beyond the window boundary.  Currently we can
         * detect this in internet explorer by verifying that the mouse is
         * down during the mousemove event.  Firefox doesn't give us the
         * button state on the mousemove event.
         *
         * @param {Event} e the event
         * @private
         */
        this.handleMouseMove = function(e) {
            if (! this.dragCurrent) {
                return true;
            }

            // var button = e.which || e.button;

            // check for IE mouseup outside of page boundary
            if (YAHOO.util.Event.isIE && !e.button) {
                this.stopEvent(e);
                return this.handleMouseUp(e);
            }

            if (!this.dragThreshMet) {
                var diffX = Math.abs(this.startX - YAHOO.util.Event.getPageX(e));
                var diffY = Math.abs(this.startY - YAHOO.util.Event.getPageY(e));
                if (diffX > this.clickPixelThresh ||
                            diffY > this.clickPixelThresh) {
                    this.startDrag(this.startX, this.startY);
                }
            }

            if (this.dragThreshMet) {
                this.dragCurrent.b4Drag(e);
                this.dragCurrent.onDrag(e);
                this.fireEvents(e, false);
            }

            this.stopEvent(e);

            return true;
        };

        /**
         * Iterates over all of the DragDrop elements to find ones we are
         * hovering over or dropping on
         *
         * @param {Event} e the event
         * @param {boolean} isDrop is this a drop op or a mouseover op?
         * @private
         */
        this.fireEvents = function(e, isDrop) {
            var dc = this.dragCurrent;

            // If the user did the mouse up outside of the window, we could
            // get here even though we have ended the drag.
            if (!dc || dc.isLocked()) {
                return;
            }

            var x = YAHOO.util.Event.getPageX(e);
            var y = YAHOO.util.Event.getPageY(e);
            var pt = new YAHOO.util.Point(x,y);

            // cache the previous dragOver array
            var oldOvers = [];

            var outEvts   = [];
            var overEvts  = [];
            var dropEvts  = [];
            var enterEvts = [];

            // Check to see if the object(s) we were hovering over is no longer
            // being hovered over so we can fire the onDragOut event
            for (var i in this.dragOvers) {

                var ddo = this.dragOvers[i];

                if (! this.isTypeOfDD(ddo)) {
                    continue;
                }

                if (! this.isOverTarget(pt, ddo, this.mode)) {
                    outEvts.push( ddo );
                }

                oldOvers[i] = true;
                delete this.dragOvers[i];
            }

            for (var sGroup in dc.groups) {

                if ("string" != typeof sGroup) {
                    continue;
                }

                for (i in this.ids[sGroup]) {
                    var oDD = this.ids[sGroup][i];
                    if (! this.isTypeOfDD(oDD)) {
                        continue;
                    }

                    if (oDD.isTarget && !oDD.isLocked() && oDD != dc) {
                        if (this.isOverTarget(pt, oDD, this.mode)) {
                            // look for drop interactions
                            if (isDrop) {
                                dropEvts.push( oDD );
                            // look for drag enter and drag over interactions
                            } else {

                                // initial drag over: dragEnter fires
                                if (!oldOvers[oDD.id]) {
                                    enterEvts.push( oDD );
                                // subsequent drag overs: dragOver fires
                                } else {
                                    overEvts.push( oDD );
                                }

                                this.dragOvers[oDD.id] = oDD;
                            }
                        }
                    }
                }
            }

            if (this.mode) {
                if (outEvts.length) {
                    dc.b4DragOut(e, outEvts);
                    dc.onDragOut(e, outEvts);
                }

                if (enterEvts.length) {
                    dc.onDragEnter(e, enterEvts);
                }

                if (overEvts.length) {
                    dc.b4DragOver(e, overEvts);
                    dc.onDragOver(e, overEvts);
                }

                if (dropEvts.length) {
                    dc.b4DragDrop(e, dropEvts);
                    dc.onDragDrop(e, dropEvts);
                }

            } else {
                // fire dragout events
                var len = 0;
                for (i=0, len=outEvts.length; i<len; ++i) {
                    dc.b4DragOut(e, outEvts[i].id);
                    dc.onDragOut(e, outEvts[i].id);
                }

                // fire enter events
                for (i=0,len=enterEvts.length; i<len; ++i) {
                    // dc.b4DragEnter(e, oDD.id);
                    dc.onDragEnter(e, enterEvts[i].id);
                }

                // fire over events
                for (i=0,len=overEvts.length; i<len; ++i) {
                    dc.b4DragOver(e, overEvts[i].id);
                    dc.onDragOver(e, overEvts[i].id);
                }

                // fire drop events
                for (i=0, len=dropEvts.length; i<len; ++i) {
                    dc.b4DragDrop(e, dropEvts[i].id);
                    dc.onDragDrop(e, dropEvts[i].id);
                }

            }

        };

        /**
         * Helper function for getting the best match from the list of drag
         * and drop objects returned by the drag and drop events when we are
         * in INTERSECT mode.  It returns either the first object that the
         * cursor is over, or the object that has the greatest overlap with
         * the dragged element.
         *
         * @param  {DragDrop[]} dds The array of drag and drop objects
         * targeted
         * @return {DragDrop}       The best single match
         */
        this.getBestMatch = function(dds) {
            var winner = null;
            // Return null if the input is not what we expect
            //if (!dds || !dds.length || dds.length == 0) {
               // winner = null;
            // If there is only one item, it wins
            //} else if (dds.length == 1) {

            var len = dds.length;

            if (len == 1) {
                winner = dds[0];
            } else {
                // Loop through the targeted items
                for (var i=0; i<len; ++i) {
                    var dd = dds[i];
                    // If the cursor is over the object, it wins.  If the
                    // cursor is over multiple matches, the first one we come
                    // to wins.
                    if (dd.cursorIsOver) {
                        winner = dd;
                        break;
                    // Otherwise the object with the most overlap wins
                    } else {
                        if (!winner ||
                            winner.overlap.getArea() < dd.overlap.getArea()) {
                            winner = dd;
                        }
                    }
                }
            }

            return winner;
        };

        /**
         * Refreshes the cache of the top-left and bottom-right points of the
         * drag and drop objects in the specified group(s).  This is in the
         * format that is stored in the drag and drop instance, so typical
         * usage is:
         *
         * YAHOO.util.DragDropMgr.refreshCache(ddinstance.groups);
         *
         * Alternatively:
         *
         * YAHOO.util.DragDropMgr.refreshCache({group1:true, group2:true});
         *
         * @TODO this really should be an indexed array.  Alternatively this
         * method could accept both.
         *
         * @param {Object} groups an associative array of groups to refresh
         */
        this.refreshCache = function(groups) {
            for (var sGroup in groups) {
                if ("string" != typeof sGroup) {
                    continue;
                }
                for (var i in this.ids[sGroup]) {
                    var oDD = this.ids[sGroup][i];

                    if (this.isTypeOfDD(oDD)) {
                    // if (this.isTypeOfDD(oDD) && oDD.isTarget) {
                        var loc = this.getLocation(oDD);
                        if (loc) {
                            this.locationCache[oDD.id] = loc;
                        } else {
                            delete this.locationCache[oDD.id];
                            // this will unregister the drag and drop object if
                            // the element is not in a usable state
                            // oDD.unreg();
                        }
                    }
                }
            }
        };

        /**
         * This checks to make sure an element exists and is in the DOM.  The
         * main purpose is to handle cases where innerHTML is used to remove
         * drag and drop objects from the DOM.  IE provides an 'unspecified
         * error' when trying to access the offsetParent of such an element
         * @param {HTMLElement} el the element to check
         * @return {boolean} true if the element looks usable
         */
        this.verifyEl = function(el) {
            try {
                if (el) {
                    var parent = el.offsetParent;
                    if (parent) {
                        return true;
                    }
                }
            } catch(e) {
            }

            return false;
        };

        /**
         * Returns a Region object containing the drag and drop element's position
         * and size, including the padding configured for it
         *
         * @param {DragDrop} oDD the drag and drop object to get the
         *                       location for
         * @return {YAHOO.util.Region} a Region object representing the total area
         *                             the element occupies, including any padding
         *                             the instance is configured for.
         */
        this.getLocation = function(oDD) {
            if (! this.isTypeOfDD(oDD)) {
                return null;
            }

            var el = oDD.getEl(), pos, x1, x2, y1, y2, t, r, b, l;

            try {
                pos= YAHOO.util.Dom.getXY(el);
            } catch (e) { }

            if (!pos) {
                return null;
            }

            x1 = pos[0];
            x2 = x1 + el.offsetWidth;
            y1 = pos[1];
            y2 = y1 + el.offsetHeight;

            t = y1 - oDD.padding[0];
            r = x2 + oDD.padding[1];
            b = y2 + oDD.padding[2];
            l = x1 - oDD.padding[3];

            return new YAHOO.util.Region( t, r, b, l );
        };

        /**
         * Checks the cursor location to see if it over the target
         *
         * @param {YAHOO.util.Point} pt The point to evaluate
         * @param {DragDrop} oTarget the DragDrop object we are inspecting
         * @return {boolean} true if the mouse is over the target
         * @private
         */
        this.isOverTarget = function(pt, oTarget, intersect) {
            // use cache if available
            var loc = this.locationCache[oTarget.id];
            if (!loc || !this.useCache) {
                loc = this.getLocation(oTarget);
                this.locationCache[oTarget.id] = loc;

            }

            if (!loc) {
                return false;
            }

            oTarget.cursorIsOver = loc.contains( pt );

            // DragDrop is using this as a sanity check for the initial mousedown
            // in this case we are done.  In POINT mode, if the drag obj has no
            // contraints, we are also done. Otherwise we need to evaluate the
            // location of the target as related to the actual location of the
            // dragged element.
            var dc = this.dragCurrent;
            if (!dc || !dc.getTargetCoord ||
                    (!intersect && !dc.constrainX && !dc.constrainY)) {
                return oTarget.cursorIsOver;
            }

            oTarget.overlap = null;

            // Get the current location of the drag element, this is the
            // location of the mouse event less the delta that represents
            // where the original mousedown happened on the element.  We
            // need to consider constraints and ticks as well.
            var pos = dc.getTargetCoord(pt.x, pt.y);

            var el = dc.getDragEl();
            var curRegion = new YAHOO.util.Region( pos.y,
                                                   pos.x + el.offsetWidth,
                                                   pos.y + el.offsetHeight,
                                                   pos.x );

            var overlap = curRegion.intersect(loc);

            if (overlap) {
                oTarget.overlap = overlap;
                return (intersect) ? true : oTarget.cursorIsOver;
            } else {
                return false;
            }
        };

        /**
         * @private
         */
        this._onUnload = function(e, me) {
            this.unregAll();
        };

        /**
         * Cleans up the drag and drop events and objects.
         * @private
         */
        this.unregAll = function() {

            if (this.dragCurrent) {
                this.stopDrag();
                this.dragCurrent = null;
            }

            this._execOnAll("unreg", []);

            for (i in this.elementCache) {
                delete this.elementCache[i];
            }

            this.elementCache = {};
            this.ids = {};
        };

        /**
         * A cache of DOM elements
         * @private
         */
        this.elementCache = {};

        /**
         * Get the wrapper for the DOM element specified
         *
         * @param {String} id the id of the elment to get
         * @return {YAHOO.util.DDM.ElementWrapper} the wrapped element
         * @private
         * @deprecated
         */
        this.getElWrapper = function(id) {
            var oWrapper = this.elementCache[id];
            if (!oWrapper || !oWrapper.el) {
                oWrapper = this.elementCache[id] =
                    new this.ElementWrapper(YAHOO.util.Dom.get(id));
            }
            return oWrapper;
        };

        /**
         * Returns the actual DOM element
         *
         * @param {String} id the id of the elment to get
         * @return {Object} The element
         * @deprecated
         */
        this.getElement = function(id) {
            return YAHOO.util.Dom.get(id);
        };

        /**
         * Returns the style property for the DOM element (i.e.,
         * document.getElById(id).style)
         *
         * @param {String} id the id of the elment to get
         * @return {Object} The style property of the element
         * @deprecated
         */
        this.getCss = function(id) {
            var el = YAHOO.util.Dom.get(id);
            return (el) ? el.style : null;
        };

        /**
         * Inner class for cached elements
         * @private
         * @deprecated
         */
        this.ElementWrapper = function(el) {
                /**
                 * @private
                 */
                this.el = el || null;
                /**
                 * @private
                 */
                this.id = this.el && el.id;
                /**
                 * @private
                 */
                this.css = this.el && el.style;
            };

        /**
         * Returns the X position of an html element
         * @param el the element for which to get the position
         * @return {int} the X coordinate
         * @deprecated
         */
        this.getPosX = function(el) {
            return YAHOO.util.Dom.getX(el);
        };

        /**
         * Returns the Y position of an html element
         * @param el the element for which to get the position
         * @return {int} the Y coordinate
         * @deprecated
         */
        this.getPosY = function(el) {
            return YAHOO.util.Dom.getY(el);
        };

        /**
         * Swap two nodes.  In IE, we use the native method, for others we
         * emulate the IE behavior
         *
         * @param n1 the first node to swap
         * @param n2 the other node to swap
         */
        this.swapNode = function(n1, n2) {
            if (n1.swapNode) {
                n1.swapNode(n2);
            } else {
                var p = n2.parentNode;
                var s = n2.nextSibling;

                if (s == n1) {
                    p.insertBefore(n1, n2);
                } else if (n2 == n1.nextSibling) {
                    p.insertBefore(n2, n1);
                } else {
                    n1.parentNode.replaceChild(n2, n1);
                    p.insertBefore(n1, s);
                }
            }
        };

        /**
         * @private
         */
        this.getScroll = function () {
            var t, l;
            if (document.documentElement && document.documentElement.scrollTop) {
                t = document.documentElement.scrollTop;
                l = document.documentElement.scrollLeft;
            } else if (document.body) {
                t = document.body.scrollTop;
                l = document.body.scrollLeft;
            }
            return { top: t, left: l };
        };

        /**
         * Returns the specified element style property
         * @param {HTMLElement} el          the element
         * @param {string}      styleProp   the style property
         * @return {string} The value of the style property
         * @deprecated, use YAHOO.util.Dom.getStyle
         */
        this.getStyle = function(el, styleProp) {
            return YAHOO.util.Dom.getStyle(el, styleProp);
        };

        /**
         * Gets the scrollTop
         * @return {int} the document's scrollTop
         */
        this.getScrollTop = function () { return this.getScroll().top; };

        /**
         * Gets the scrollLeft
         * @return {int} the document's scrollTop
         */
        this.getScrollLeft = function () { return this.getScroll().left; };

        /**
         * Sets the x/y position of an element to the location of the
         * target element.
         * @param {HTMLElement} moveEl      The element to move
         * @param {HTMLElement} targetEl    The position reference element
         */
        this.moveToEl = function (moveEl, targetEl) {
            var aCoord = YAHOO.util.Dom.getXY(targetEl);
            YAHOO.util.Dom.setXY(moveEl, aCoord);
        };

        /**
         * Gets the client height
         * @return {int} client height in px
         * @deprecated
         */
        this.getClientHeight = function() {
            return YAHOO.util.Dom.getClientHeight();
        };

        /**
         * Gets the client width
         * @return {int} client width in px
         * @deprecated
         */
        this.getClientWidth = function() {
            return YAHOO.util.Dom.getClientWidth();
        };

        /**
         * numeric array sort function
         */
        this.numericSort = function(a, b) { return (a - b); };

        /**
         * @private
         */
        this._timeoutCount = 0;

        /**
         * Trying to make the load order less important.  Without this we get
         * an error if this file is loaded before the Event Utility.
         * @private
         */
        this._addListeners = function() {
            var DDM = YAHOO.util.DDM;
            if ( YAHOO.util.Event && document ) {
                DDM._onLoad();
            } else {
                if (DDM._timeoutCount > 2000) {
                } else {
                    setTimeout(DDM._addListeners, 10);
                    if (document && document.body) {
                        DDM._timeoutCount += 1;
                    }
                }
            }
        };

        /**
         * Recursively searches the immediate parent and all child nodes for
         * the handle element in order to determine wheter or not it was
         * clicked.
         * @param node the html element to inspect
         */
        this.handleWasClicked = function(node, id) {
            if (this.isHandle(id, node.id)) {
                return true;
            } else {
                // check to see if this is a text node child of the one we want
                var p = node.parentNode;

                while (p) {
                    if (this.isHandle(id, p.id)) {
                        return true;
                    } else {
                        p = p.parentNode;
                    }
                }
            }

            return false;
        };

    } ();

    // shorter alias, save a few bytes
    YAHOO.util.DDM = YAHOO.util.DragDropMgr;
    YAHOO.util.DDM._addListeners();

}

//YAHOO.util.DragDropMgr.enableWindow = function(win) {
    //var EU = YAHOO.util.Event;
    //EU.on(win.document, "mouseup",   this.handleMouseUp,   this, true);
    //EU.on(win.document, "mousemove", this.handleMouseMove, this, true);
//};

/**
 * A DragDrop implementation where the linked element follows the
 * mouse cursor during a drag.
 *
 * @extends YAHOO.util.DragDrop
 * @constructor
 * @param {String} id the id of the linked element
 * @param {String} sGroup the group of related DragDrop items
 * @param {object} config an object containing configurable attributes
 *                Valid properties for DD:
 *                    scroll
 */
YAHOO.util.DD = function(id, sGroup, config) {
    if (id) {
        this.init(id, sGroup, config);
    }
};

// YAHOO.util.DD.prototype = new YAHOO.util.DragDrop();
YAHOO.extend(YAHOO.util.DD, YAHOO.util.DragDrop);

/**
 * When set to true, the utility automatically tries to scroll the browser
 * window wehn a drag and drop element is dragged near the viewport boundary.
 * Defaults to true.
 *
 * @type boolean
 */
YAHOO.util.DD.prototype.scroll = true;

/**
 * Sets the pointer offset to the distance between the linked element's top
 * left corner and the location the element was clicked
 *
 * @param {int} iPageX the X coordinate of the click
 * @param {int} iPageY the Y coordinate of the click
 */
YAHOO.util.DD.prototype.autoOffset = function(iPageX, iPageY) {
    // var el = this.getEl();
    // var aCoord = YAHOO.util.Dom.getXY(el);
    // var x = iPageX - aCoord[0];
    // var y = iPageY - aCoord[1];
    var x = iPageX - this.startPageX;
    var y = iPageY - this.startPageY;
    this.setDelta(x, y);
};

/**
 * Sets the pointer offset.  You can call this directly to force the offset to
 * be in a particular location (e.g., pass in 0,0 to set it to the center of the
 * object, as done in YAHOO.widget.Slider)
 *
 * @param {int} iDeltaX the distance from the left
 * @param {int} iDeltaY the distance from the top
 */
YAHOO.util.DD.prototype.setDelta = function(iDeltaX, iDeltaY) {
    this.deltaX = iDeltaX;
    this.deltaY = iDeltaY;
};

/**
 * Sets the drag element to the location of the mousedown or click event,
 * maintaining the cursor location relative to the location on the element
 * that was clicked.  Override this if you want to place the element in a
 * location other than where the cursor is.
 *
 * @param {int} iPageX the X coordinate of the mousedown or drag event
 * @param {int} iPageY the Y coordinate of the mousedown or drag event
 */

YAHOO.util.DD.prototype.setDragElPos = function(iPageX, iPageY) {
    // the first time we do this, we are going to check to make sure
    // the element has css positioning

    var el = this.getDragEl();

    // if (!this.cssVerified) {
        // var pos = el.style.position;
    // }

    this.alignElWithMouse(el, iPageX, iPageY);
};

/**
 * Sets the element to the location of the mousedown or click event,
 * maintaining the cursor location relative to the location on the element
 * that was clicked.  Override this if you want to place the element in a
 * location other than where the cursor is.
 *
 * @param {HTMLElement} el the element to move
 * @param {int} iPageX the X coordinate of the mousedown or drag event
 * @param {int} iPageY the Y coordinate of the mousedown or drag event
 */
YAHOO.util.DD.prototype.alignElWithMouse = function(el, iPageX, iPageY) {
    var oCoord = this.getTargetCoord(iPageX, iPageY);

    // this.deltaSetXY = null;
    if (!this.deltaSetXY) {
        var aCoord = [oCoord.x, oCoord.y];
        YAHOO.util.Dom.setXY(el, aCoord);
        var newLeft = parseInt( YAHOO.util.Dom.getStyle(el, "left"), 10 );
        var newTop  = parseInt( YAHOO.util.Dom.getStyle(el, "top" ), 10 );

        this.deltaSetXY = [ newLeft - oCoord.x, newTop - oCoord.y ];

    } else {
        YAHOO.util.Dom.setStyle(el, "left", (oCoord.x + this.deltaSetXY[0]) + "px");
        YAHOO.util.Dom.setStyle(el, "top",  (oCoord.y + this.deltaSetXY[1]) + "px");
    }


    this.cachePosition(oCoord.x, oCoord.y);

    this.autoScroll(oCoord.x, oCoord.y, el.offsetHeight, el.offsetWidth);
};

/**
 * Saves the most recent position so that we can reset the constraints and
 * tick marks on-demand.  We need to know this so that we can calculate the
 * number of pixels the element is offset from its original position.
 */
YAHOO.util.DD.prototype.cachePosition = function(iPageX, iPageY) {
    if (iPageX) {
        this.lastPageX = iPageX;
        this.lastPageY = iPageY;
    } else {
        var aCoord = YAHOO.util.Dom.getXY(this.getEl());
        this.lastPageX = aCoord[0];
        this.lastPageY = aCoord[1];
    }
};

/**
 * Auto-scroll the window if the dragged object has been moved beyond the
 * visible window boundary.
 *
 * @param {int} x the drag element's x position
 * @param {int} y the drag element's y position
 * @param {int} h the height of the drag element
 * @param {int} w the width of the drag element
 * @private
 */
YAHOO.util.DD.prototype.autoScroll = function(x, y, h, w) {

    if (this.scroll) {
        // The client height
        var clientH = this.DDM.getClientHeight();

        // The client width
        var clientW = this.DDM.getClientWidth();

        // The amt scrolled down
        var st = this.DDM.getScrollTop();

        // The amt scrolled right
        var sl = this.DDM.getScrollLeft();

        // Location of the bottom of the element
        var bot = h + y;

        // Location of the right of the element
        var right = w + x;

        // The distance from the cursor to the bottom of the visible area,
        // adjusted so that we don't scroll if the cursor is beyond the
        // element drag constraints
        var toBot = (clientH + st - y - this.deltaY);

        // The distance from the cursor to the right of the visible area
        var toRight = (clientW + sl - x - this.deltaX);


        // How close to the edge the cursor must be before we scroll
        // var thresh = (document.all) ? 100 : 40;
        var thresh = 40;

        // How many pixels to scroll per autoscroll op.  This helps to reduce
        // clunky scrolling. IE is more sensitive about this ... it needs this
        // value to be higher.
        var scrAmt = (document.all) ? 80 : 30;

        // Scroll down if we are near the bottom of the visible page and the
        // obj extends below the crease
        if ( bot > clientH && toBot < thresh ) {
            window.scrollTo(sl, st + scrAmt);
        }

        // Scroll up if the window is scrolled down and the top of the object
        // goes above the top border
        if ( y < st && st > 0 && y - st < thresh ) {
            window.scrollTo(sl, st - scrAmt);
        }

        // Scroll right if the obj is beyond the right border and the cursor is
        // near the border.
        if ( right > clientW && toRight < thresh ) {
            window.scrollTo(sl + scrAmt, st);
        }

        // Scroll left if the window has been scrolled to the right and the obj
        // extends past the left border
        if ( x < sl && sl > 0 && x - sl < thresh ) {
            window.scrollTo(sl - scrAmt, st);
        }
    }
};

/**
 * Finds the location the element should be placed if we want to move
 * it to where the mouse location less the click offset would place us.
 *
 * @param {int} iPageX the X coordinate of the click
 * @param {int} iPageY the Y coordinate of the click
 * @return an object that contains the coordinates (Object.x and Object.y)
 * @private
 */
YAHOO.util.DD.prototype.getTargetCoord = function(iPageX, iPageY) {


    var x = iPageX - this.deltaX;
    var y = iPageY - this.deltaY;

    if (this.constrainX) {
        if (x < this.minX) { x = this.minX; }
        if (x > this.maxX) { x = this.maxX; }
    }

    if (this.constrainY) {
        if (y < this.minY) { y = this.minY; }
        if (y > this.maxY) { y = this.maxY; }
    }

    x = this.getTick(x, this.xTicks);
    y = this.getTick(y, this.yTicks);


    return {x:x, y:y};
};

YAHOO.util.DD.prototype.applyConfig = function() {
    YAHOO.util.DD.superclass.applyConfig.call(this);
    this.scroll = (this.config.scroll !== false);
};

/**
 * Event that fires prior to the onMouseDown event.  Overrides
 * YAHOO.util.DragDrop.
 */
YAHOO.util.DD.prototype.b4MouseDown = function(e) {
    // this.resetConstraints();
    this.autoOffset(YAHOO.util.Event.getPageX(e),
                        YAHOO.util.Event.getPageY(e));
};

/**
 * Event that fires prior to the onDrag event.  Overrides
 * YAHOO.util.DragDrop.
 */
YAHOO.util.DD.prototype.b4Drag = function(e) {
    this.setDragElPos(YAHOO.util.Event.getPageX(e),
                        YAHOO.util.Event.getPageY(e));
};

YAHOO.util.DD.prototype.toString = function() {
    return ("DD " + this.id);
};

///////////////////////////////////////////////////////////////////////////////
// Debugging ygDragDrop events that can be overridden
///////////////////////////////////////////////////////////////////////////////
/*
YAHOO.util.DD.prototype.startDrag = function(x, y) {
};

YAHOO.util.DD.prototype.onDrag = function(e) {
};

YAHOO.util.DD.prototype.onDragEnter = function(e, id) {
};

YAHOO.util.DD.prototype.onDragOver = function(e, id) {
};

YAHOO.util.DD.prototype.onDragOut = function(e, id) {
};

YAHOO.util.DD.prototype.onDragDrop = function(e, id) {
};

YAHOO.util.DD.prototype.endDrag = function(e) {
};
*/

/**
 * A DragDrop implementation that inserts an empty, bordered div into
 * the document that follows the cursor during drag operations.  At the time of
 * the click, the frame div is resized to the dimensions of the linked html
 * element, and moved to the exact location of the linked element.
 *
 * References to the "frame" element refer to the single proxy element that
 * was created to be dragged in place of all DDProxy elements on the
 * page.
 *
 * @extends YAHOO.util.DD
 * @constructor
 * @param {String} id the id of the linked html element
 * @param {String} sGroup the group of related DragDrop objects
 * @param {object} config an object containing configurable attributes
 *                Valid properties for DDProxy in addition to those in DragDrop:
 *                   resizeFrame, centerFrame, dragElId
 */
YAHOO.util.DDProxy = function(id, sGroup, config) {
    if (id) {
        this.init(id, sGroup, config);
        this.initFrame();
    }
};

YAHOO.extend(YAHOO.util.DDProxy, YAHOO.util.DD);

/**
 * The default drag frame div id
 * @type String
 */
YAHOO.util.DDProxy.dragElId = "ygddfdiv";

/**
 * By default we resize the drag frame to be the same size as the element
 * we want to drag (this is to get the frame effect).  We can turn it off
 * if we want a different behavior.
 *
 * @type boolean
 */
YAHOO.util.DDProxy.prototype.resizeFrame = true;

/**
 * By default the frame is positioned exactly where the drag element is, so
 * we use the cursor offset provided by YAHOO.util.DD.  Another option that works only if
 * you do not have constraints on the obj is to have the drag frame centered
 * around the cursor.  Set centerFrame to true for this effect.
 *
 * @type boolean
 */
YAHOO.util.DDProxy.prototype.centerFrame = false;

/**
 * Create the drag frame if needed
 */
YAHOO.util.DDProxy.prototype.createFrame = function() {
    var self = this;
    var body = document.body;

    if (!body || !body.firstChild) {
        setTimeout( function() { self.createFrame(); }, 50 );
        return;
    }

    var div = this.getDragEl();

    if (!div) {
        div    = document.createElement("div");
        div.id = this.dragElId;
        var s  = div.style;

        s.position   = "absolute";
        s.visibility = "hidden";
        s.cursor     = "move";
        s.border     = "2px solid #aaa";
        s.zIndex     = 999;

        // appendChild can blow up IE if invoked prior to the window load event
        // while rendering a table.  It is possible there are other scenarios
        // that would cause this to happen as well.
        body.insertBefore(div, body.firstChild);
    }
};

/**
 * Initialization for the drag frame element.  Must be called in the
 * constructor of all subclasses
 */
YAHOO.util.DDProxy.prototype.initFrame = function() {
    // YAHOO.util.DDProxy.createFrame();
    // this.setDragElId(YAHOO.util.DDProxy.dragElId);

    this.createFrame();

};

YAHOO.util.DDProxy.prototype.applyConfig = function() {
    YAHOO.util.DDProxy.superclass.applyConfig.call(this);

    this.resizeFrame = (this.config.resizeFrame !== false);
    this.centerFrame = (this.config.centerFrame);
    this.setDragElId(this.config.dragElId || YAHOO.util.DDProxy.dragElId);

};

/**
 * Resizes the drag frame to the dimensions of the clicked object, positions
 * it over the object, and finally displays it
 *
 * @param {int} iPageX X click position
 * @param {int} iPageY Y click position
 * @private
 */
YAHOO.util.DDProxy.prototype.showFrame = function(iPageX, iPageY) {
    var el = this.getEl();
    var dragEl = this.getDragEl();
    var s = dragEl.style;

    this._resizeProxy();

    if (this.centerFrame) {
        this.setDelta( Math.round(parseInt(s.width,  10)/2),
                       Math.round(parseInt(s.height, 10)/2) );
    }

    this.setDragElPos(iPageX, iPageY);

    YAHOO.util.Dom.setStyle(dragEl, "visibility", "visible");
};

YAHOO.util.DDProxy.prototype._resizeProxy = function() {
    if (this.resizeFrame) {
        var DOM    = YAHOO.util.Dom;
        var el     = this.getEl();
        var dragEl = this.getDragEl();

        var bt = parseInt( DOM.getStyle(dragEl, "borderTopWidth"    ), 10);
        var br = parseInt( DOM.getStyle(dragEl, "borderRightWidth"  ), 10);
        var bb = parseInt( DOM.getStyle(dragEl, "borderBottomWidth" ), 10);
        var bl = parseInt( DOM.getStyle(dragEl, "borderLeftWidth"   ), 10);

        if (isNaN(bt)) { bt = 0; }
        if (isNaN(br)) { br = 0; }
        if (isNaN(bb)) { bb = 0; }
        if (isNaN(bl)) { bl = 0; }


        var newWidth  = Math.max(0, el.offsetWidth  - br - bl);
        var newHeight = Math.max(0, el.offsetHeight - bt - bb);


        DOM.setStyle( dragEl, "width",  newWidth  + "px" );
        DOM.setStyle( dragEl, "height", newHeight + "px" );
    }
};

// overrides YAHOO.util.DragDrop
YAHOO.util.DDProxy.prototype.b4MouseDown = function(e) {
    var x = YAHOO.util.Event.getPageX(e);
    var y = YAHOO.util.Event.getPageY(e);
    this.autoOffset(x, y);
    this.setDragElPos(x, y);
};

// overrides YAHOO.util.DragDrop
YAHOO.util.DDProxy.prototype.b4StartDrag = function(x, y) {
    // show the drag frame
    this.showFrame(x, y);
};

// overrides YAHOO.util.DragDrop
YAHOO.util.DDProxy.prototype.b4EndDrag = function(e) {
    YAHOO.util.Dom.setStyle(this.getDragEl(), "visibility", "hidden");
};

// overrides YAHOO.util.DragDrop
// By default we try to move the element to the last location of the frame.
// This is so that the default behavior mirrors that of YAHOO.util.DD.
YAHOO.util.DDProxy.prototype.endDrag = function(e) {
    var DOM = YAHOO.util.Dom;
    var lel = this.getEl();
    var del = this.getDragEl();

    // Show the drag frame briefly so we can get its position
    // del.style.visibility = "";
    DOM.setStyle(del, "visibility", "");

    // Hide the linked element before the move to get around a Safari
    // rendering bug.
    //lel.style.visibility = "hidden";
    DOM.setStyle(lel, "visibility", "hidden");
    YAHOO.util.DDM.moveToEl(lel, del);
    //del.style.visibility = "hidden";
    DOM.setStyle(del, "visibility", "hidden");
    //lel.style.visibility = "";
    DOM.setStyle(lel, "visibility", "");
};

YAHOO.util.DDProxy.prototype.toString = function() {
    return ("DDProxy " + this.id);
};

/**
 * A DragDrop implementation that does not move, but can be a drop
 * target.  You would get the same result by simply omitting implementation
 * for the event callbacks, but this way we reduce the processing cost of the
 * event listener and the callbacks.
 *
 * @extends YAHOO.util.DragDrop
 * @constructor
 * @param {String} id the id of the element that is a drop target
 * @param {String} sGroup the group of related DragDrop objects
 * @param {object} config an object containing configurable attributes
 *                Valid properties for DDTarget in addition to those in DragDrop:
 *                  none
 */

YAHOO.util.DDTarget = function(id, sGroup, config) {
    if (id) {
        this.initTarget(id, sGroup, config);
    }
};

// YAHOO.util.DDTarget.prototype = new YAHOO.util.DragDrop();
YAHOO.extend(YAHOO.util.DDTarget, YAHOO.util.DragDrop);

YAHOO.util.DDTarget.prototype.toString = function() {
    return ("DDTarget " + this.id);
};


/*** End of file, dragdrop.js ***/

/*** Start of file, connection.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.3
*/

/**
 * The Connection Manager provides a simplified interface to the XMLHttpRequest
 * object.  It handles cross-browser instantiantion of XMLHttpRequest, negotiates the
 * interactive states and server response, returning the results to a pre-defined
 * callback you create.
 * @ class
 */
YAHOO.util.Connect =
{
/**
   * Array of MSFT ActiveX ids for XMLHttpRequest.
   * @private
   * @type array
   */
	_msxml_progid:[
		'MSXML2.XMLHTTP.3.0',
		'MSXML2.XMLHTTP',
		'Microsoft.XMLHTTP'
		],

  /**
   * Object literal of HTTP header(s)
   * @private
   * @type object
   */
	_http_header:{},

  /**
   * Determines if HTTP headers are set.
   * @private
   * @type boolean
   */
	_has_http_headers:false,

 /**
  * Determines if a default header of
  * Content-Type of 'application/x-www-form-urlencoded'
  * will be added to any client HTTP headers sent for POST
  * transactions.
  * @private
  * @type boolean
  */
    _use_default_post_header:true,

 /**
  * Determines if a default header of
  * Content-Type of 'application/x-www-form-urlencoded'
  * will be added to any client HTTP headers sent for POST
  * transactions.
  * @private
  * @type boolean
  */
    _default_post_header:'application/x-www-form-urlencoded',

 /**
  * Property modified by setForm() to determine if the data
  * should be submitted as an HTML form.
  * @private
  * @type boolean
  */
    _isFormSubmit:false,

 /**
  * Property modified by setForm() to determine if a file(s)
  * upload is expected.
  * @private
  * @type boolean
  */
    _isFileUpload:false,

 /**
  * Property modified by setForm() to set a reference to the HTML
  * form node if the desired action is file upload.
  * @private
  * @type object
  */
    _formNode:null,

 /**
  * Property modified by setForm() to set the HTML form data
  * for each transaction.
  * @private
  * @type string
  */
    _sFormData:null,

 /**
  * Collection of polling references to the polling mechanism in handleReadyState.
  * @private
  * @type object
  */
    _poll:{},

 /**
  * Queue of timeout values for each transaction callback with a defined timeout value.
  * @private
  * @type object
  */
    _timeOut:{},

  /**
   * The polling frequency, in milliseconds, for HandleReadyState.
   * when attempting to determine a transaction's XHR readyState.
   * The default is 50 milliseconds.
   * @private
   * @type int
   */
     _polling_interval:50,

  /**
   * A transaction counter that increments the transaction id for each transaction.
   * @private
   * @type int
   */
     _transaction_id:0,

  /**
   * Member to add an ActiveX id to the existing xml_progid array.
   * In the event(unlikely) a new ActiveX id is introduced, it can be added
   * without internal code modifications.
   * @public
   * @param string id The ActiveX id to be added to initialize the XHR object.
   * @return void
   */
	setProgId:function(id)
	{
		this._msxml_progid.unshift(id);
	},

  /**
   * Member to enable or disable the default POST header.
   * @public
   * @param boolean b Set and use default header - true or false .
   * @return void
   */
	setDefaultPostHeader:function(b)
	{
		this._use_default_post_header = b;
	},

  /**
   * Member to modify the default polling interval.
   * @public
   * @param {int} i The polling interval in milliseconds.
   * @return void
   */
	setPollingInterval:function(i)
	{
		if(typeof i == 'number' && isFinite(i)){
			this._polling_interval = i;
		}
	},

  /**
   * Instantiates a XMLHttpRequest object and returns an object with two properties:
   * the XMLHttpRequest instance and the transaction id.
   * @private
   * @param {int} transactionId Property containing the transaction id for this transaction.
   * @return connection object
   */
	createXhrObject:function(transactionId)
	{
		var obj,http;
		try
		{
			// Instantiates XMLHttpRequest in non-IE browsers and assigns to http.
			http = new XMLHttpRequest();
			//  Object literal with http and tId properties
			obj = { conn:http, tId:transactionId };
		}
		catch(e)
		{
			for(var i=0; i<this._msxml_progid.length; ++i){
				try
				{
					// Instantiates XMLHttpRequest for IE and assign to http.
					http = new ActiveXObject(this._msxml_progid[i]);
					//  Object literal with http and tId properties
					obj = { conn:http, tId:transactionId };
					break;
				}
				catch(e){}
			}
		}
		finally
		{
			return obj;
		}
	},

  /**
   * This method is called by asyncRequest to create a
   * valid connection object for the transaction.  It also passes a
   * transaction id and increments the transaction id counter.
   * @private
   * @return object
   */
	getConnectionObject:function()
	{
		var o;
		var tId = this._transaction_id;

		try
		{
			o = this.createXhrObject(tId);
			if(o){
				this._transaction_id++;
			}
		}
		catch(e){}
		finally
		{
			return o;
		}
	},

  /**
   * Method for initiating an asynchronous request via the XHR object.
   * @public
   * @param {string} method HTTP transaction method
   * @param {string} uri Fully qualified path of resource
   * @param callback User-defined callback function or object
   * @param {string} postData POST body
   * @return {object} Returns the connection object
   */
	asyncRequest:function(method, uri, callback, postData)
	{
		var o = this.getConnectionObject();

		if(!o){
			return null;
		}
		else{
			if(this._isFormSubmit){
				if(this._isFileUpload){
					this.uploadFile(o.tId, callback, uri);
					this.releaseObject(o);
					return;
				}

				//If the specified HTTP method is GET, setForm() will return an
				//encoded string that is concatenated to the uri to
				//create a querystring.
				if(method == 'GET'){
					uri += "?" +  this._sFormData;
				}
				else if(method == 'POST'){
					//If POST data exists in addition to the HTML form data,
					//it will be concatenated to the form data.
					postData = (postData?this._sFormData + "&" + postData:this._sFormData);
				}
				this._sFormData = '';
			}

			o.conn.open(method, uri, true);

			if(this._isFormSubmit || (postData && this._use_default_post_header)){
				this.initHeader('Content-Type', this._default_post_header);
				if(this._isFormSubmit){
					this._isFormSubmit = false;
				}
			}

			if(this._has_http_headers){
				this.setHeader(o);
			}

			this.handleReadyState(o, callback);
			o.conn.send(postData?postData:null);

			return o;
		}
	},

  /**
   * This method serves as a timer that polls the XHR object's readyState
   * property during a transaction, instead of binding a callback to the
   * onreadystatechange event.  Upon readyState 4, handleTransactionResponse
   * will process the response, and the timer will be cleared.
   *
   * @private
   * @param {object} o The connection object
   * @param callback User-defined callback object
   * @return void
   */
    handleReadyState:function(o, callback)
    {

		var oConn = this;

		if(callback && callback.timeout){
			this._timeOut[o.tId] = window.setTimeout(function(){ oConn.abort(o, callback, true); }, callback.timeout);
		}

		this._poll[o.tId] = window.setInterval(
			function(){
				if(o.conn && o.conn.readyState == 4){
					window.clearInterval(oConn._poll[o.tId]);
					delete oConn._poll[o.tId];

					if(callback && callback.timeout){
						delete oConn._timeOut[o.tId];
					}

					oConn.handleTransactionResponse(o, callback);
				}
			}
		,this._polling_interval);
    },

  /**
   * This method attempts to interpret the server response and
   * determine whether the transaction was successful, or if an error or
   * exception was encountered.
   *
   * @private
   * @param {object} o The connection object
   * @param {object} callback - User-defined callback object
   * @param {boolean} determines if the transaction was aborted.
   * @return void
   */
    handleTransactionResponse:function(o, callback, isAbort)
    {
		// If no valid callback is provided, then do not process any callback handling.
		if(!callback){
			this.releaseObject(o);
			return;
		}

		var httpStatus, responseObject;

		try
		{
			if(o.conn.status !== undefined && o.conn.status != 0){
				httpStatus = o.conn.status;
			}
			else{
				httpStatus = 13030;
			}
		}
		catch(e){
			// 13030 is the custom code to indicate the condition -- in Mozilla/FF --
			// when the o object's status and statusText properties are
			// unavailable, and a query attempt throws an exception.
			httpStatus = 13030;
		}

		if(httpStatus >= 200 && httpStatus < 300){
			try
			{
				responseObject = this.createResponseObject(o, callback.argument);
				if(callback.success){
					if(!callback.scope){
						callback.success(responseObject);
					}
					else{
						// If a scope property is defined, the callback will be fired from
						// the context of the object.
						callback.success.apply(callback.scope, [responseObject]);
					}
				}
			}
			catch(e){}
		}
		else{
			try
			{
				switch(httpStatus){
					// The following case labels are wininet.dll error codes that may be encountered.
					case 12002: // Server timeout
					case 12029: // 12029 to 12031 correspond to dropped connections.
					case 12030:
					case 12031:
					case 12152: // Connection closed by server.
					case 13030: // See above comments for variable status.
						responseObject = this.createExceptionObject(o.tId, callback.argument, (isAbort?isAbort:false));
						if(callback.failure){
							if(!callback.scope){
								callback.failure(responseObject);
							}
							else{
								callback.failure.apply(callback.scope, [responseObject]);
							}
						}
						break;
					default:
						responseObject = this.createResponseObject(o, callback.argument);
						if(callback.failure){
							if(!callback.scope){
								callback.failure(responseObject);
							}
							else{
								callback.failure.apply(callback.scope, [responseObject]);
							}
						}
				}
			}
			catch(e){}
		}

		this.releaseObject(o);
		responseObject = null;
    },

  /**
   * This method evaluates the server response, creates and returns the results via
   * its properties.  Success and failure cases will differ in the response
   * object's property values.
   * @private
   * @param {object} o The connection object
   * @param {} callbackArg User-defined argument or arguments to be passed to the callback
   * @return object
   */
    createResponseObject:function(o, callbackArg)
    {
		var obj = {};
		var headerObj = {};

		try
		{
			var headerStr = o.conn.getAllResponseHeaders();
			var header = headerStr.split('\n');
			for(var i=0; i < header.length; i++){
				var delimitPos = header[i].indexOf(':');
				if(delimitPos != -1){
					headerObj[header[i].substring(0,delimitPos)] = header[i].substring(delimitPos + 2);
				}
			}
		}
		catch(e){}

		obj.tId = o.tId;
		obj.status = o.conn.status;
		obj.statusText = o.conn.statusText;
		obj.getResponseHeader = headerObj;
		obj.getAllResponseHeaders = headerStr;
		obj.responseText = o.conn.responseText;
		obj.responseXML = o.conn.responseXML;

		if(typeof callbackArg !== undefined){
			obj.argument = callbackArg;
		}

		return obj;
    },

  /**
   * If a transaction cannot be completed due to dropped or closed connections,
   * there may be not be enough information to build a full response object.
   * The failure callback will be fired and this specific condition can be identified
   * by a status property value of 0.
   *
   * If an abort was successful, the status property will report a value of -1.
   *
   * @private
   * @param {int} tId Transaction Id
   * @param callbackArg The user-defined arguments
   * @param isAbort Determines if the exception is an abort.
   * @return object
   */
    createExceptionObject:function(tId, callbackArg, isAbort)
    {
		var COMM_CODE = 0;
		var COMM_ERROR = 'communication failure';
		var ABORT_CODE = -1;
		var ABORT_ERROR = 'transaction aborted';

		var obj = {};

		obj.tId = tId;
		if(isAbort){
			obj.status = ABORT_CODE;
			obj.statusText = ABORT_ERROR;
		}
		else{
			obj.status = COMM_CODE;
			obj.statusText = COMM_ERROR;
		}

		if(callbackArg){
			obj.argument = callbackArg;
		}

		return obj;
    },

  /**
   * Public method that stores the custom HTTP headers for each transaction.
   * @public
   * @param {string} label The HTTP header label
   * @param {string} value The HTTP header value
   * @return void
   */
	initHeader:function(label,value)
	{
		if(this._http_header[label] === undefined){
			this._http_header[label] = value;
		}
		else{
			// Concatenate multiple values, comma-delimited,
			// for the same header label,
			this._http_header[label] =  value + "," + this._http_header[label];
		}

		this._has_http_headers = true;
	},

  /**
   * Accessor that sets the HTTP headers for each transaction.
   * @private
   * @param {object} o The connection object for the transaction.
   * @return void
   */
	setHeader:function(o)
	{
		for(var prop in this._http_header){
			if(this._http_header.hasOwnProperty(prop)){
				o.conn.setRequestHeader(prop, this._http_header[prop]);
			}
		}
		delete this._http_header;

		this._http_header = {};
		this._has_http_headers = false;
	},

  /**
   * This method assembles the form label and value pairs and
   * constructs an encoded string.
   * asyncRequest() will automatically initialize the
   * transaction with a HTTP header Content-Type of
   * application/x-www-form-urlencoded.
   * @public
   * @param {string || object} form id or name attribute, or form object.
   * @param {string} optional boolean to indicate SSL environment.
   * @param {string || boolean} optional qualified path of iframe resource for SSL in IE.
   * @return void
   */
	setForm:function(formId, isUpload, secureUri)
	{
		this._sFormData = '';
		if(typeof formId == 'string'){
			// Determine if the argument is a form id or a form name.
			// Note form name usage is deprecated by supported
			// here for legacy reasons.
			var oForm = (document.getElementById(formId) || document.forms[formId]);
		}
		else if(typeof formId == 'object'){
			// Treat argument as an HTML form object.
			var oForm = formId;
		}
		else{
			return;
		}

		// If the isUpload argument is true, setForm will call createFrame to initialize
		// an iframe as the form target.
		//
		// The argument secureURI is also required by IE in SSL environments
		// where the secureURI string is a fully qualified HTTP path, used to set the source
		// of the iframe, to a stub resource in the same domain.
		if(isUpload){
			this.createFrame(secureUri?secureUri:null);
			this._isFormSubmit = true;
			this._isFileUpload = true;
			this._formNode = oForm;

			return;
		}

		var oElement, oName, oValue, oDisabled;
		var hasSubmit = false;

		// Iterate over the form elements collection to construct the
		// label-value pairs.
		for (var i=0; i<oForm.elements.length; i++){
			oElement = oForm.elements[i];
			oDisabled = oForm.elements[i].disabled;
			oName = oForm.elements[i].name;
			oValue = oForm.elements[i].value;

			// Do not submit fields that are disabled or
			// do not have a name attribute value.
			if(!oDisabled && oName)
			{
				switch (oElement.type)
				{
					case 'select-one':
					case 'select-multiple':
						for(var j=0; j<oElement.options.length; j++){
							if(oElement.options[j].selected){
								if(window.ActiveXObject){
									this._sFormData += encodeURIComponent(oName) + '=' + encodeURIComponent(oElement.options[j].attributes['value'].specified?oElement.options[j].value:oElement.options[j].text) + '&';
								}
								else{
									this._sFormData += encodeURIComponent(oName) + '=' + encodeURIComponent(oElement.options[j].hasAttribute('value')?oElement.options[j].value:oElement.options[j].text) + '&';
								}

							}
						}
						break;
					case 'radio':
					case 'checkbox':
						if(oElement.checked){
							this._sFormData += encodeURIComponent(oName) + '=' + encodeURIComponent(oValue) + '&';
						}
						break;
					case 'file':
						// stub case as XMLHttpRequest will only send the file path as a string.
					case undefined:
						// stub case for fieldset element which returns undefined.
					case 'reset':
						// stub case for input type reset button.
					case 'button':
						// stub case for input type button elements.
						break;
					case 'submit':
						if(hasSubmit == false){
							this._sFormData += encodeURIComponent(oName) + '=' + encodeURIComponent(oValue) + '&';
							hasSubmit = true;
						}
						break;
					default:
						this._sFormData += encodeURIComponent(oName) + '=' + encodeURIComponent(oValue) + '&';
						break;
				}
			}
		}

		this._isFormSubmit = true;
		this._sFormData = this._sFormData.substr(0, this._sFormData.length - 1);
	},

  /**
   * Creates an iframe to be used for form file uploads.  It is remove from the
   * document upon completion of the upload transaction.
   *
   * @private
   * @param {string} optional qualified path of iframe resource for SSL in IE.
   * @return void
   */
	createFrame:function(secureUri){

		// IE does not allow the setting of id and name attributes as object
		// properties via createElement().  A different iframe creation
		// pattern is required for IE.
		var frameId = 'yuiIO' + this._transaction_id;
		if(window.ActiveXObject){
			var io = document.createElement('<IFRAME id="' + frameId + '" name="' + frameId + '">');

			// IE will throw a security exception in an SSL environment if the
			// iframe source isn't set.
			if(typeof secureUri == 'boolean'){
				io.src = 'javascript:false';
			}
			else{
				io.src = secureUri;
			}
		}
		else{
			var io = document.createElement('IFRAME');
			io.id = frameId;
			io.name = frameId;
		}

		io.style.position = 'absolute';
		io.style.top = '-1000px';
		io.style.left = '-1000px';

		document.body.appendChild(io);
	},

  /**
   * Uploads HTML form, including files/attachments,  targeting the
   * iframe created in createFrame.
   *
   * @private
   * @param {int} id The transaction id.
   * @param {object} callback - User-defined callback object.
   * @param {string} uri Fully qualified path of resource.
   * @return void
   */
	uploadFile:function(id, callback, uri){

		var frameId = 'yuiIO' + id;
		var io = document.getElementById(frameId);

		// Initialize the HTML form properties in case they are
		// not defined in the HTML form.
		this._formNode.action = uri;
		this._formNode.enctype = 'multipart/form-data';
		this._formNode.method = 'POST';
		this._formNode.target = frameId;
		this._formNode.submit();

		// Reset form status properties.
		this._formNode = null;
		this._isFileUpload = false;
		this._isFormSubmit = false;

		// Create the upload callback handler that fires when the iframe
		// receives the load event.  Subsequently, the event handler is detached
		// and the iframe removed from the document.

		var uploadCallback = function()
		{
			var obj = {};

			obj.tId = id;
			obj.responseText = io.contentWindow.document.body?io.contentWindow.document.body.innerHTML:null;
			obj.responseXML = io.contentWindow.document.XMLDocument?io.contentWindow.document.XMLDocument:io.contentWindow.document;
			obj.argument = callback.argument;

			if(callback.upload){
				if(!callback.scope){
					callback.upload(obj);
				}
				else{
					callback.upload.apply(callback.scope, [obj]);
				}
			}

			if(YAHOO.util.Event){
				YAHOO.util.Event.removeListener(io, "load", uploadCallback);
			}
			else if(window.ActiveXObject){
				io.detachEvent('onload', uploadCallback);
			}
			else{
				io.removeEventListener('load', uploadCallback, false);
			}
			setTimeout(function(){ document.body.removeChild(io); }, 100);
		};


		// Bind the onload handler to the iframe to detect the file upload response.
		if(YAHOO.util.Event){
			YAHOO.util.Event.addListener(io, "load", uploadCallback);
		}
		else if(window.ActiveXObject){
			io.attachEvent('onload', uploadCallback);
		}
		else{
			io.addEventListener('load', uploadCallback, false);
		}
	},

  /**
   * Public method to terminate a transaction, if it has not reached readyState 4.
   * @public
   * @param {object} o The connection object returned by asyncRequest.
   * @param {object} callback  User-defined callback object.
   * @param {string} isTimeout boolean to indicate if abort was a timeout.
   * @return void
   */
	abort:function(o, callback, isTimeout)
	{
		if(this.isCallInProgress(o)){
			o.conn.abort();
			window.clearInterval(this._poll[o.tId]);
			delete this._poll[o.tId];
			if(isTimeout){
				delete this._timeOut[o.tId];
			}

			this.handleTransactionResponse(o, callback, true);

			return true;
		}
		else{
			return false;
		}
	},

  /**
   * Public method to check if the transaction is still being processed.
   * @public
   * @param {object} o The connection object returned by asyncRequest
   * @return boolean
   */
	isCallInProgress:function(o)
	{
		// if the XHR object assigned to the transaction has not been dereferenced,
		// then check its readyState status.  Otherwise, return false.
		if(o.conn){
			return o.conn.readyState != 4 && o.conn.readyState != 0;
		}
		else{
			//The XHR object has been destroyed.
			return false;
		}
	},

  /**
   * Dereference the XHR instance and the connection object after the transaction is completed.
   * @private
   * @param {object} o The connection object
   * @return void
   */
	releaseObject:function(o)
	{
		//dereference the XHR instance.
		o.conn = null;
		//dereference the connection object.
		o = null;
	}
};
/*** End of file, connection.js ***/

/*** Start of file, autocomplete.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.txt
version: 0.11.3
*/

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Class providing the customizable functionality of a plug-and-play DHTML
 * auto complete widget.  Some key features:
 * <ul>
 * <li>Navigate with up/down arrow keys and/or mouse to pick a selection</li>
 * <li>The drop down container can "roll down" or "fly out" via configurable
 * animation</li>
 * <li>UI look-and-feel customizable through CSS, including container
 * attributes, borders, position, fonts, etc</li>
 * </ul>
 *
 * requires YAHOO.util.Dom Dom utility
 * requires YAHOO.util.Event Event utility
 * requires YAHOO.widget.DataSource Data source class
 * see YAHOO.util.Animation Animation utility
 * see JSON JSON library
 *
 * @constructor
 * @param {element | string} inputEl DOM element reference or string ID of the auto complete input field
 * @param {element | string} containerEl DOM element reference or string ID of the auto complete &lt;div&gt;
 *                              container
 * @param {object} oDataSource Instance of YAHOO.widget.DataSource for query/results
 * @param {object} oConfigs Optional object literal of config params
 */
YAHOO.widget.AutoComplete = function(inputEl,containerEl,oDataSource,oConfigs) {
    if(inputEl && containerEl && oDataSource) {
        // Validate data source
        if (oDataSource && (oDataSource instanceof YAHOO.widget.DataSource)) {
            this.dataSource = oDataSource;
        }
        else {
            return;
        }

        // Validate input element
        if(YAHOO.util.Dom.inDocument(inputEl)) {
            if(typeof inputEl == "string") {
                    this._sName = "instance" + YAHOO.widget.AutoComplete._nIndex + " " + inputEl;
                    this._oTextbox = document.getElementById(inputEl);
            }
            else {
                this._sName = (inputEl.id) ?
                    "instance" + YAHOO.widget.AutoComplete._nIndex + " " + inputEl.id:
                    "instance" + YAHOO.widget.AutoComplete._nIndex;
                this._oTextbox = inputEl;
            }
        }
        else {
            return;
        }

        // Validate container element
        if(YAHOO.util.Dom.inDocument(containerEl)) {
            if(typeof containerEl == "string") {
                    this._oContainer = document.getElementById(containerEl);
            }
            else {
                this._oContainer = containerEl;
            }
            if(this._oContainer.style.display == "none") {
            }
        }
        else {
            return;
        }

        // Set any config params passed in to override defaults
        if (typeof oConfigs == "object") {
            for(var sConfig in oConfigs) {
                if (sConfig) {
                    this[sConfig] = oConfigs[sConfig];
                }
            }
        }

        // Initialization sequence
        this._initContainer();
        this._initProps();
        this._initList();
        this._initContainerHelpers();

        // Set up events
        var oSelf = this;
        var oTextbox = this._oTextbox;
        // Events are actually for the content module within the container
        var oContent = this._oContainer._oContent;

        // Dom events
        YAHOO.util.Event.addListener(oTextbox,"keyup",oSelf._onTextboxKeyUp,oSelf);
        YAHOO.util.Event.addListener(oTextbox,"keydown",oSelf._onTextboxKeyDown,oSelf);
        YAHOO.util.Event.addListener(oTextbox,"keypress",oSelf._onTextboxKeyPress,oSelf);
        YAHOO.util.Event.addListener(oTextbox,"focus",oSelf._onTextboxFocus,oSelf);
        YAHOO.util.Event.addListener(oTextbox,"blur",oSelf._onTextboxBlur,oSelf);
        YAHOO.util.Event.addListener(oContent,"mouseover",oSelf._onContainerMouseover,oSelf);
        YAHOO.util.Event.addListener(oContent,"mouseout",oSelf._onContainerMouseout,oSelf);
        YAHOO.util.Event.addListener(oContent,"scroll",oSelf._onContainerScroll,oSelf);
        YAHOO.util.Event.addListener(oContent,"resize",oSelf._onContainerResize,oSelf);
        if(oTextbox.form) {
            YAHOO.util.Event.addListener(oTextbox.form,"submit",oSelf._onFormSubmit,oSelf);
        }

        // Custom events
        this.textboxFocusEvent = new YAHOO.util.CustomEvent("textboxFocus", this);
        this.textboxKeyEvent = new YAHOO.util.CustomEvent("textboxKey", this);
        this.dataRequestEvent = new YAHOO.util.CustomEvent("dataRequest", this);
        this.dataReturnEvent = new YAHOO.util.CustomEvent("dataReturn", this);
        this.dataErrorEvent = new YAHOO.util.CustomEvent("dataError", this);
        this.containerExpandEvent = new YAHOO.util.CustomEvent("containerExpand", this);
        this.typeAheadEvent = new YAHOO.util.CustomEvent("typeAhead", this);
        this.itemMouseOverEvent = new YAHOO.util.CustomEvent("itemMouseOver", this);
        this.itemMouseOutEvent = new YAHOO.util.CustomEvent("itemMouseOut", this);
        this.itemArrowToEvent = new YAHOO.util.CustomEvent("itemArrowTo", this);
        this.itemArrowFromEvent = new YAHOO.util.CustomEvent("itemArrowFrom", this);
        this.itemSelectEvent = new YAHOO.util.CustomEvent("itemSelect", this);
        this.unmatchedItemSelectEvent = new YAHOO.util.CustomEvent("unmatchedItemSelect", this);
        this.selectionEnforceEvent = new YAHOO.util.CustomEvent("selectionEnforce", this);
        this.containerCollapseEvent = new YAHOO.util.CustomEvent("containerCollapse", this);
        this.textboxBlurEvent = new YAHOO.util.CustomEvent("textboxBlur", this);
        
        // Finish up
        oTextbox.setAttribute("autocomplete","off");
        YAHOO.widget.AutoComplete._nIndex++;
    }
    // Required arguments were not found
    else {
    }
};

/***************************************************************************
 * Public member variables
 ***************************************************************************/
/**
 * The data source object that encapsulates the data used for auto completion.
 * This object should be an inherited object from YAHOO.widget.DataSource.
 *
 * @type object
 */
YAHOO.widget.AutoComplete.prototype.dataSource = null;

/**
 * Number of characters that must be entered before querying for results.
 * Default: 1.
 *
 * @type number
 */
YAHOO.widget.AutoComplete.prototype.minQueryLength = 1;

/**
 * Maximum number of results to display in auto complete container. Default: 10.
 *
 * @type number
 */
YAHOO.widget.AutoComplete.prototype.maxResultsDisplayed = 10;

/**
 * Number of seconds to delay before submitting a query request.  If a query
 * request is received before a previous one has completed its delay, the
 * previous request is cancelled and the new request is set to the delay.
 * Default: 0.5.
 *
 * @type number
 */
YAHOO.widget.AutoComplete.prototype.queryDelay = 0.5;

/**
 * Class name of a highlighted item within the auto complete container.
 * Default: "yui-ac-highlight".
 *
 * @type string
 */
YAHOO.widget.AutoComplete.prototype.highlightClassName = "yui-ac-highlight";

/**
 * Class name of a pre-highlighted item within the auto complete container.
 * Default: null.
 *
 * @type string
 */
YAHOO.widget.AutoComplete.prototype.prehighlightClassName = null;

/**
 * Query delimiter. A single character separator for multiple delimited
 * selections. Multiple delimiter characteres may be defined as an array of
 * strings. A null value or empty string indicates that query results cannot
 * be delimited. This feature is not recommended if you need forceSelection to
 * be true. Default: null.
 *
 * @type string or array
 */
YAHOO.widget.AutoComplete.prototype.delimChar = null;

/**
 * Whether or not the first item in the auto complete container should be
 * automatically highlighted on expand. Default: true.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.autoHighlight = true;

/**
 * Whether or not the auto complete input field should be automatically updated
 * with the first query result as the user types, auto-selecting the substring
 * that the user has not typed. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.typeAhead = false;

/**
 * Whether or not to animate the expansion/collapse of the auto complete
 * container in the horizontal direction. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.animHoriz = false;

/**
 * Whether or not to animate the expansion/collapse of the auto complete
 * container in the vertical direction. Default: true.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.animVert = true;

/**
 * Speed of container expand/collapse animation, in seconds. Default: 0.3.
 *
 * @type number
 */
YAHOO.widget.AutoComplete.prototype.animSpeed = 0.3;

/**
 * Whether or not to force the user's selection to match one of the query
 * results. Enabling this feature essentially transforms the auto complete form
 * input field into a &lt;select&gt; field. This feature is not recommended
 * with delimiter character(s) defined. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.forceSelection = false;

/**
 * Whether or not to allow browsers to cache user-typed input in the input
 * field. Disabling this feature will prevent the widget from setting the
 * autocomplete="off" on the auto complete input field. When autocomplete="off"
 * and users click the back button after form submission, user-typed input can
 * be prefilled by the browser from its cache. This caching of user input may
 * not be desired for sensitive data, such as credit card numbers, in which
 * case, implementers should consider setting allowBrowserAutocomplete to false.
 * Default: true.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.allowBrowserAutocomplete = true;

/**
 * Whether or not the auto complete container should always be displayed.
 * Enabling this feature prevents the toggling of the container to a collapsed
 * state. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.alwaysShowContainer = false;

/**
 * Whether or not to use an iFrame to layer over Windows form elements in
 * IE. Set to true only when the auto complete container will be on top of a
 * &lt;select&gt; field in IE and thus exposed to the IE z-index bug (i.e.,
 * 5.5 < IE < 7). Default:false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.useIFrame = false;

/**
 * Whether or not the auto complete container should have a shadow. Default:false.
 *
 * @type boolean
 */
YAHOO.widget.AutoComplete.prototype.useShadow = false;

/***************************************************************************
 * Public methods
 ***************************************************************************/
 /**
 * Public accessor to the unique name of the auto complete instance.
 *
 * @return {string} Unique name of the auto complete instance
 */
YAHOO.widget.AutoComplete.prototype.toString = function() {
    return "AutoComplete " + this._sName;
};

/**
 * Public accessor to the internal array of DOM &lt;li&gt; elements that
 * display query results within the auto complete container.
 *
 * @return {array} Array of &lt;li&gt; elements within the auto complete
 *                 container
 */
YAHOO.widget.AutoComplete.prototype.getListItems = function() {
    return this._aListItems;
};

/**
 * Public accessor to the data held in an &lt;li&gt; element of the
 * auto complete container.
 *
 * @return {object or array} Object or array of result data or null
 */
YAHOO.widget.AutoComplete.prototype.getListItemData = function(oListItem) {
    if(oListItem._oResultData) {
        return oListItem._oResultData;
    }
    else {
        return false;
    }
};

/**
 * Sets HTML markup for the auto complete container header. This markup will be
 * inserted within a &lt;div&gt; tag with a class of "ac_hd".
 *
 * @param {string} sHeader HTML markup for container header
 */
YAHOO.widget.AutoComplete.prototype.setHeader = function(sHeader) {
    if(sHeader) {
        if(this._oContainer._oContent._oHeader) {
            this._oContainer._oContent._oHeader.innerHTML = sHeader;
            this._oContainer._oContent._oHeader.style.display = "block";
        }
    }
    else {
        this._oContainer._oContent._oHeader.innerHTML = "";
        this._oContainer._oContent._oHeader.style.display = "none";
    }
};

/**
 * Sets HTML markup for the auto complete container footer. This markup will be
 * inserted within a &lt;div&gt; tag with a class of "ac_ft".
 *
 * @param {string} sFooter HTML markup for container footer
 */
YAHOO.widget.AutoComplete.prototype.setFooter = function(sFooter) {
    if(sFooter) {
        if(this._oContainer._oContent._oFooter) {
            this._oContainer._oContent._oFooter.innerHTML = sFooter;
            this._oContainer._oContent._oFooter.style.display = "block";
        }
    }
    else {
        this._oContainer._oContent._oFooter.innerHTML = "";
        this._oContainer._oContent._oFooter.style.display = "none";
    }
};

/**
 * Sets HTML markup for the auto complete container body. This markup will be
 * inserted within a &lt;div&gt; tag with a class of "ac_bd".
 *
 * @param {string} sHeader HTML markup for container body
 */
YAHOO.widget.AutoComplete.prototype.setBody = function(sBody) {
    if(sBody) {
        if(this._oContainer._oContent._oBody) {
            this._oContainer._oContent._oBody.innerHTML = sBody;
            this._oContainer._oContent._oBody.style.display = "block";
            this._oContainer._oContent.style.display = "block";
        }
    }
    else {
        this._oContainer._oContent._oBody.innerHTML = "";
        this._oContainer._oContent.style.display = "none";
    }
    this._maxResultsDisplayed = 0;
};

/**
 * Overridable method that converts a result item object into HTML markup
 * for display. Return data values are accessible via the oResultItem object,
 * and the key return value will always be oResultItem[0]. Markup will be
 * displayed within &lt;li&gt; element tags in the container.
 *
 * @param {object} oResultItem Result item object representing one query result
 * @param {string} sQuery The current query string
 * @return {string} HTML markup of formatted result data
 */
YAHOO.widget.AutoComplete.prototype.formatResult = function(oResultItem, sQuery) {
    var sResult = oResultItem[0];
    if(sResult) {
        return sResult;
    }
    else {
        return "";
    }
};

/**
 * Makes query request to the data source.
 *
 * @param {string} sQuery Query string.
 */
YAHOO.widget.AutoComplete.prototype.sendQuery = function(sQuery) {
    if(sQuery) {
        this._sendQuery(sQuery);
    }
    else {
        return;
    }
};

/***************************************************************************
 * Events
 ***************************************************************************/
/**
 * Fired when the auto complete text input box receives focus. Subscribers
 * receive the following array:<br>
 *     -  args[0] The auto complete object instance
 */
YAHOO.widget.AutoComplete.prototype.textboxFocusEvent = null;

/**
 * Fired when the auto complete text input box receives key input. Subscribers
 * receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The keycode number
 */
YAHOO.widget.AutoComplete.prototype.textboxKeyEvent = null;

/**
 * Fired when the auto complete instance makes a query to the data source.
 * Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The query string
 */
YAHOO.widget.AutoComplete.prototype.dataRequestEvent = null;

/**
 * Fired when the auto complete instance receives query results from the data
 * source. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The query string
 *     - args[2] Results array
 */
YAHOO.widget.AutoComplete.prototype.dataReturnEvent = null;

/**
 * Fired when the auto complete instance does not receive query results from the
 * data source due to an error. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The query string
 */
YAHOO.widget.AutoComplete.prototype.dataErrorEvent = null;

/**
 * Fired when the auto complete container is expanded. If alwaysShowContainer is
 * enabled, then containerExpandEvent will be fired when the container is
 * populated with results. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 */
YAHOO.widget.AutoComplete.prototype.containerExpandEvent = null;

/**
 * Fired when the auto complete textbox has been prefilled by the type-ahead
 * feature. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The query string
 *     - args[2] The prefill string
 */
YAHOO.widget.AutoComplete.prototype.typeAheadEvent = null;

/**
 * Fired when result item has been moused over. Subscribers receive the following
 * array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The &lt;li&gt element item moused to
 */
YAHOO.widget.AutoComplete.prototype.itemMouseOverEvent = null;

/**
 * Fired when result item has been moused out. Subscribers receive the
 * following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The &lt;li&gt; element item moused from
 */
YAHOO.widget.AutoComplete.prototype.itemMouseOutEvent = null;

/**
 * Fired when result item has been arrowed to. Subscribers receive the following
 * array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The &lt;li&gt; element item arrowed to
 */
YAHOO.widget.AutoComplete.prototype.itemArrowToEvent = null;

/**
 * Fired when result item has been arrowed away from. Subscribers receive the
 * following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The &lt;li&gt; element item arrowed from
 */
YAHOO.widget.AutoComplete.prototype.itemArrowFromEvent = null;

/**
 * Fired when an item is selected via mouse click, ENTER key, or TAB key.
 * Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The selected &lt;li&gt; element item
 *     - args[2] The data returned for the item, either as an object, or mapped from the schema into an array
 */
YAHOO.widget.AutoComplete.prototype.itemSelectEvent = null;

/**
 * Fired when an user selection does not match any of the displayed result items.
 * Note that this event may not behave as expected when delimiter characters
 * have been defined. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 *     - args[1] The user selection
 */
YAHOO.widget.AutoComplete.prototype.unmatchedItemSelectEvent = null;

/**
 * Fired if forceSelection is enabled and the user's input has been cleared
 * because it did not match one of the returned query results. Subscribers
 * receive the following array:<br>
 *     - args[0] The auto complete object instance
 */
YAHOO.widget.AutoComplete.prototype.selectionEnforceEvent = null;

/**
 * Fired when the auto complete container is collapsed. If alwaysShowContainer is
 * enabled, then containerCollapseEvent will be fired when the container is
 * cleared of results. Subscribers receive the following array:<br>
 *     - args[0] The auto complete object instance
 */
YAHOO.widget.AutoComplete.prototype.containerCollapseEvent = null;

/**
 * Fired when the auto complete text input box loses focus. Subscribers receive
 * an array of the following array:<br>
 *     - args[0] The auto complete object instance
 */
YAHOO.widget.AutoComplete.prototype.textboxBlurEvent = null;

/***************************************************************************
 * Private member variables
 ***************************************************************************/
/**
 * Internal class variable to index multiple auto complete instances.
 *
 * @type number
 * @private
 */
YAHOO.widget.AutoComplete._nIndex = 0;

/**
 * Name of auto complete instance.
 *
 * @type string
 * @private
 */
YAHOO.widget.AutoComplete.prototype._sName = null;

/**
 * Text input box DOM element.
 *
 * @type object
 * @private
 */
YAHOO.widget.AutoComplete.prototype._oTextbox = null;

/**
 * Whether or not the textbox is currently in focus. If query results come back
 * but the user has already moved on, do not proceed with auto complete behavior.
 *
 * @type boolean
 * @private
 */
YAHOO.widget.AutoComplete.prototype._bFocused = true;

/**
 * Animation instance for container expand/collapse.
 *
 * @type boolean
 * @private
 */
YAHOO.widget.AutoComplete.prototype._oAnim = null;

/**
 * Container DOM element.
 *
 * @type object
 * @private
 */
YAHOO.widget.AutoComplete.prototype._oContainer = null;

/**
 * Whether or not the auto complete container is currently open.
 *
 * @type boolean
 * @private
 */
YAHOO.widget.AutoComplete.prototype._bContainerOpen = false;

/**
 * Whether or not the mouse is currently over the auto complete
 * container. This is necessary in order to prevent clicks on container items
 * from being text input box blur events.
 *
 * @type boolean
 * @private
 */
YAHOO.widget.AutoComplete.prototype._bOverContainer = false;

/**
 * Array of &lt;li&gt; elements references that contain query results within the
 * auto complete container.
 *
 * @type array
 * @private
 */
YAHOO.widget.AutoComplete.prototype._aListItems = null;

/**
 * Number of &lt;li&gt; elements currently displayed in auto complete container.
 *
 * @type number
 * @private
 */
YAHOO.widget.AutoComplete.prototype._nDisplayedItems = 0;

/**
 * Internal count of &lt;li&gt; elements displayed and hidden in auto complete container.
 *
 * @type number
 * @private
 */
YAHOO.widget.AutoComplete.prototype._maxResultsDisplayed = 0;

/**
 * Current query string
 *
 * @type string
 * @private
 */
YAHOO.widget.AutoComplete.prototype._sCurQuery = null;

/**
 * Past queries this session (for saving delimited queries).
 *
 * @type string
 * @private
 */
YAHOO.widget.AutoComplete.prototype._sSavedQuery = null;

/**
 * Pointer to the currently highlighted &lt;li&gt; element in the container.
 *
 * @type object
 * @private
 */
YAHOO.widget.AutoComplete.prototype._oCurItem = null;

/**
 * Whether or not an item has been selected since the container was populated
 * with results. Reset to false by _populateList, and set to true when item is
 * selected.
 *
 * @type boolean
 * @private
 */
YAHOO.widget.AutoComplete.prototype._bItemSelected = false;

/**
 * Key code of the last key pressed in textbox.
 *
 * @type number
 * @private
 */
YAHOO.widget.AutoComplete.prototype._nKeyCode = null;

/**
 * Delay timeout ID.
 *
 * @type number
 * @private
 */
YAHOO.widget.AutoComplete.prototype._nDelayID = -1;

/**
 * Src to iFrame used when useIFrame = true. Supports implementations over SSL
 * as well.
 *
 * @type string
 * @private
 */
YAHOO.widget.AutoComplete.prototype._iFrameSrc = "javascript:false;";

/***************************************************************************
 * Private methods
 ***************************************************************************/
/**
 * Updates and validates latest public config properties.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._initProps = function() {
    // Correct any invalid values
    var minQueryLength = this.minQueryLength;
    if(isNaN(minQueryLength) || (minQueryLength < 1)) {
        minQueryLength = 1;
    }
    var maxResultsDisplayed = this.maxResultsDisplayed;
    if(isNaN(this.maxResultsDisplayed) || (this.maxResultsDisplayed < 1)) {
        this.maxResultsDisplayed = 10;
    }
    var queryDelay = this.queryDelay;
    if(isNaN(this.queryDelay) || (this.queryDelay < 0)) {
        this.queryDelay = 0.5;
    }
    var aDelimChar = (this.delimChar) ? this.delimChar : null;
    if(aDelimChar) {
        if(typeof aDelimChar == "string") {
            this.delimChar = [aDelimChar];
        }
        else if(aDelimChar.constructor != Array) {
            this.delimChar = null;
        }
    }
    var animSpeed = this.animSpeed;
    if((this.animHoriz || this.animVert) && YAHOO.util.Anim) {
        if(isNaN(animSpeed) || (animSpeed < 0)) {
            animSpeed = 0.3;
        }
        if(!this._oAnim ) {
            oAnim = new YAHOO.util.Anim(this._oContainer._oContent, {}, this.animSpeed);
            this._oAnim = oAnim;
        }
        else {
            this._oAnim.duration = animSpeed;
        }
    }
    if(this.forceSelection && this.delimChar) {
    }
    if(this.alwaysShowContainer && (this.useShadow || this.useIFrame)) {
    }

    if(this.alwaysShowContainer) {
        this._bContainerOpen = true;
    }
};

/**
 * Initializes the auto complete container helpers if they are enabled and do
 * not exist
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._initContainerHelpers = function() {
    if(this.useShadow && !this._oContainer._oShadow) {
        var oShadow = document.createElement("div");
        oShadow.className = "yui-ac-shadow";
        this._oContainer._oShadow = this._oContainer.appendChild(oShadow);
    }
    if(this.useIFrame && !this._oContainer._oIFrame) {
        var oIFrame = document.createElement("iframe");
        oIFrame.src = this._iFrameSrc;
        oIFrame.frameBorder = 0;
        oIFrame.scrolling = "no";
        oIFrame.style.position = "absolute";
        oIFrame.style.width = "100%";
        oIFrame.style.height = "100%";
        this._oContainer._oIFrame = this._oContainer.appendChild(oIFrame);
    }
};

/**
 * Initializes the auto complete container once at object creation
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._initContainer = function() {
    if(!this._oContainer._oContent) {
        // The oContent div helps size the iframe and shadow properly
        var oContent = document.createElement("div");
        oContent.className = "yui-ac-content";
        oContent.style.display = "none";
        this._oContainer._oContent = this._oContainer.appendChild(oContent);

        var oHeader = document.createElement("div");
        oHeader.className = "yui-ac-hd";
        oHeader.style.display = "none";
        this._oContainer._oContent._oHeader = this._oContainer._oContent.appendChild(oHeader);

        var oBody = document.createElement("div");
        oBody.className = "yui-ac-bd";
        this._oContainer._oContent._oBody = this._oContainer._oContent.appendChild(oBody);

        var oFooter = document.createElement("div");
        oFooter.className = "yui-ac-ft";
        oFooter.style.display = "none";
        this._oContainer._oContent._oFooter = this._oContainer._oContent.appendChild(oFooter);
    }
    else {
    }
};

/**
 * Clears out contents of container body and creates up to
 * YAHOO.widget.AutoComplete#maxResultsDisplayed &lt;li&gt; elements in an
 * &lt;ul&gt; element.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._initList = function() {
    this._aListItems = [];
    while(this._oContainer._oContent._oBody.hasChildNodes()) {
        var oldListItems = this.getListItems();
        if(oldListItems) {
            for(var oldi = oldListItems.length-1; oldi >= 0; i--) {
                oldListItems[oldi] = null;
            }
        }
        this._oContainer._oContent._oBody.innerHTML = "";
    }

    var oList = document.createElement("ul");
    oList = this._oContainer._oContent._oBody.appendChild(oList);
    for(var i=0; i<this.maxResultsDisplayed; i++) {
        var oItem = document.createElement("li");
        oItem = oList.appendChild(oItem);
        this._aListItems[i] = oItem;
        this._initListItem(oItem, i);
    }
    this._maxResultsDisplayed = this.maxResultsDisplayed;
};

/**
 * Initializes each &lt;li&gt; element in the container list.
 *
 * @param {object} oItem The &lt;li&gt; DOM element
 * @param {number} nItemIndex The index of the element
 * @private
 */
YAHOO.widget.AutoComplete.prototype._initListItem = function(oItem, nItemIndex) {
    var oSelf = this;
    oItem.style.display = "none";
    oItem._nItemIndex = nItemIndex;

    oItem.mouseover = oItem.mouseout = oItem.onclick = null;
    YAHOO.util.Event.addListener(oItem,"mouseover",oSelf._onItemMouseover,oSelf);
    YAHOO.util.Event.addListener(oItem,"mouseout",oSelf._onItemMouseout,oSelf);
    YAHOO.util.Event.addListener(oItem,"click",oSelf._onItemMouseclick,oSelf);
};

/**
 * Handles &lt;li&gt; element mouseover events in the container.
 *
 * @param {event} v The mouseover event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onItemMouseover = function(v,oSelf) {
    if(oSelf.prehighlightClassName) {
        oSelf._togglePrehighlight(this,"mouseover");
    }
    else {
        oSelf._toggleHighlight(this,"to");
    }

    oSelf.itemMouseOverEvent.fire(oSelf, this);
};

/**
 * Handles &lt;li&gt; element mouseout events in the container.
 *
 * @param {event} v The mouseout event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onItemMouseout = function(v,oSelf) {
    if(oSelf.prehighlightClassName) {
        oSelf._togglePrehighlight(this,"mouseout");
    }
    else {
        oSelf._toggleHighlight(this,"from");
    }

    oSelf.itemMouseOutEvent.fire(oSelf, this);
};

/**
 * Handles &lt;li&gt; element click events in the container.
 *
 * @param {event} v The click event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onItemMouseclick = function(v,oSelf) {
    // In case item has not been moused over
    oSelf._toggleHighlight(this,"to");
    oSelf._selectItem(this);
};

/**
 * Handles container mouseover events.
 *
 * @param {event} v The mouseover event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onContainerMouseover = function(v,oSelf) {
    oSelf._bOverContainer = true;
};

/**
 * Handles container mouseout events.
 *
 * @param {event} v The mouseout event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onContainerMouseout = function(v,oSelf) {
    oSelf._bOverContainer = false;
    // If container is still active
    if(oSelf._oCurItem) {
        oSelf._toggleHighlight(oSelf._oCurItem,"to");
    }
};

/**
 * Handles container scroll events.
 *
 * @param {event} v The scroll event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onContainerScroll = function(v,oSelf) {
    oSelf._oTextbox.focus();
};

/**
 * Handles container resize events.
 *
 * @param {event} v The resize event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onContainerResize = function(v,oSelf) {
    oSelf._toggleContainerHelpers(oSelf._bContainerOpen);
};

/**
 * Handles textbox keydown events of functional keys, mainly for UI behavior.
 *
 * @param {event} v The keydown event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onTextboxKeyDown = function(v,oSelf) {
    var nKeyCode = v.keyCode;

    switch (nKeyCode) {
        case 9: // tab
            if(oSelf.delimChar && (oSelf._nKeyCode != nKeyCode)) {
                if(oSelf._bContainerOpen) {
                    YAHOO.util.Event.stopEvent(v);
                }
            }
            // select an item or clear out
            if(oSelf._oCurItem) {
                oSelf._selectItem(oSelf._oCurItem);
            }
            else {
                oSelf._clearList();
            }
            break;
        case 13: // enter
            if(oSelf._nKeyCode != nKeyCode) {
                if(oSelf._bContainerOpen) {
                    YAHOO.util.Event.stopEvent(v);
                }
            }
            if(oSelf._oCurItem) {
                oSelf._selectItem(oSelf._oCurItem);
            }
            else {
                oSelf._clearList();
            }
            break;
        case 27: // esc
            oSelf._clearList();
            return;
        case 39: // right
            oSelf._jumpSelection();
            break;
        case 38: // up
            YAHOO.util.Event.stopEvent(v);
            oSelf._moveSelection(nKeyCode);
            break;
        case 40: // down
            YAHOO.util.Event.stopEvent(v);
            oSelf._moveSelection(nKeyCode);
            break;
        default:
            break;
    }
};

/**
 * Handles textbox keypress events, for stopEvent in Safari and FF 1.5/Mac
 *
 * @param {event} v The keyup event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onTextboxKeyPress = function(v,oSelf) {
    var nKeyCode = v.keyCode;

    switch (nKeyCode) {
    case 9: // tab
    case 13: // enter
        if((oSelf._nKeyCode != nKeyCode)) {
                YAHOO.util.Event.stopEvent(v);
        }
        break;
    case 38: // up
    case 40: // down
        YAHOO.util.Event.stopEvent(v);
        break;
    default:
        break;
    }
};

/**
 * Handles textbox keyup events that trigger queries.
 *
 * @param {event} v The keyup event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onTextboxKeyUp = function(v,oSelf) {
    // Check to see if any of the public properties have been updated
    oSelf._initProps();

    var nKeyCode = v.keyCode;
    oSelf._nKeyCode = nKeyCode;
    var sChar = String.fromCharCode(nKeyCode);
    var sText = this.value; //string in textbox

    // Filter out chars that don't trigger queries
    if (oSelf._isIgnoreKey(nKeyCode) || (sText.toLowerCase() == oSelf._sCurQuery)) {
        return;
    }
    else {
        oSelf.textboxKeyEvent.fire(oSelf, nKeyCode);
    }

    // Set timeout on the request
    if (oSelf.queryDelay > 0) {
        var nDelayID =
            setTimeout(function(){oSelf._sendQuery(sText);},(oSelf.queryDelay * 1000));

        if (oSelf._nDelayID != -1) {
            clearTimeout(oSelf._nDelayID);
        }

        oSelf._nDelayID = nDelayID;
    }
    else {
        // No delay so send request immediately
        oSelf._sendQuery(sText);
    }
};

/**
 * Whether or not key is functional or should be ignored. Note that the right
 * arrow key is NOT an ignored key since it triggers queries for certain intl
 * charsets.
 *
 * @param {number} nKeycode Code of key pressed
 * @return {boolean} Whether or not to be ignore key
 * @private
 */
YAHOO.widget.AutoComplete.prototype._isIgnoreKey = function(nKeyCode) {
    if ((nKeyCode == 9) || (nKeyCode == 13)  || // tab, enter
            (nKeyCode == 16) || (nKeyCode == 17) || // shift, ctl
            (nKeyCode >= 18 && nKeyCode <= 20) || // alt,pause/break,caps lock
            (nKeyCode == 27) || // esc
            (nKeyCode >= 33 && nKeyCode <= 35) || // page up,page down,end
            (nKeyCode >= 36 && nKeyCode <= 38) || // home,left,up
            (nKeyCode == 40) || // down
            (nKeyCode >= 44 && nKeyCode <= 45)) { // print screen,insert
        return true;
    }
    return false;
};

/**
 * Handles text input box receiving focus.
 *
 * @param {event} v The focus event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onTextboxFocus = function (v,oSelf) {
    oSelf._oTextbox.setAttribute("autocomplete","off");
    oSelf._bFocused = true;
    oSelf.textboxFocusEvent.fire(oSelf);
};

/**
 * Handles text input box losing focus.
 *
 * @param {event} v The focus event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onTextboxBlur = function (v,oSelf) {
    // Don't treat as a blur if it was a selection via mouse click
    if(!oSelf._bOverContainer || (oSelf._nKeyCode == 9)) {
        // Current query needs to be validated
        if(!oSelf._bItemSelected) {
            if(!oSelf._bContainerOpen || (oSelf._bContainerOpen && !oSelf._textMatchesOption())) {
                if(oSelf.forceSelection) {
                    oSelf._clearSelection();
                }
                else {
                    oSelf.unmatchedItemSelectEvent.fire(oSelf, oSelf._sCurQuery);
                }
            }
        }

        if(oSelf._bContainerOpen) {
            oSelf._clearList();
        }
        oSelf._bFocused = false;
        oSelf.textboxBlurEvent.fire(oSelf);
    }
};

/**
 * Handles form submission event.
 *
 * @param {event} v The submit event
 * @param {object} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._onFormSubmit = function(v,oSelf) {
    if(oSelf.allowBrowserAutocomplete) {
        oSelf._oTextbox.setAttribute("autocomplete","on");
    }
    else {
        oSelf._oTextbox.setAttribute("autocomplete","off");
    }
};

/**
 * Makes query request to the data source.
 *
 * @param {string} sQuery Query string.
 * @private
 */
YAHOO.widget.AutoComplete.prototype._sendQuery = function(sQuery) {
    // Delimiter has been enabled
    var aDelimChar = (this.delimChar) ? this.delimChar : null;
    if(aDelimChar) {
        // Loop through all possible delimiters and find the latest one
        // A " " may be a false positive if they are defined as delimiters AND
        // are used to separate delimited queries
        var nDelimIndex = -1;
        for(var i = aDelimChar.length-1; i >= 0; i--) {
            var nNewIndex = sQuery.lastIndexOf(aDelimChar[i]);
            if(nNewIndex > nDelimIndex) {
                nDelimIndex = nNewIndex;
            }
        }
        // If we think the last delimiter is a space (" "), make sure it is NOT
        // a false positive by also checking the char directly before it
        if(aDelimChar[i] == " ") {
            for (var j = aDelimChar.length-1; j >= 0; j--) {
                if(sQuery[nDelimIndex - 1] == aDelimChar[j]) {
                    nDelimIndex--;
                    break;
                }
            }
        }
        // A delimiter has been found so extract the latest query
        if (nDelimIndex > -1) {
            var nQueryStart = nDelimIndex + 1;
            // Trim any white space from the beginning...
            while(sQuery.charAt(nQueryStart) == " ") {
                nQueryStart += 1;
            }
            // ...and save the rest of the string for later
            this._sSavedQuery = sQuery.substring(0,nQueryStart);
            // Here is the query itself
            sQuery = sQuery.substr(nQueryStart);
        }
        else if(sQuery.indexOf(this._sSavedQuery) < 0){
            this._sSavedQuery = null;
        }
    }

    // Don't search queries that are too short
    if (sQuery.length < this.minQueryLength) {
        if (this._nDelayID != -1) {
            clearTimeout(this._nDelayID);
        }
        this._clearList();
        return;
    }

    sQuery = encodeURIComponent(sQuery);
    this._nDelayID = -1;    // Reset timeout ID because request has been made
    this.dataRequestEvent.fire(this, sQuery);
    this.dataSource.getResults(this._populateList, sQuery, this);
};

/**
 * Hides all visuals related to the array of &lt;li&gt; elements in the container.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._clearList = function() {
    this._oContainer._oContent.scrollTop = 0;
    var aItems = this._aListItems;

    if(aItems && (aItems.length > 0)) {
        for(var i = aItems.length-1; i >= 0 ; i--) {
            aItems[i].style.display = "none";
        }
    }

    if (this._oCurItem) {
        this._toggleHighlight(this._oCurItem,"from");
    }

    this._oCurItem = null;
    this._nDisplayedItems = 0;
    this._sCurQuery = null;
    this._toggleContainer(false);
};

/**
 * Populates the array of &lt;li&gt; elements in the container with query
 * results. This method is passed to YAHOO.widget.DataSource#getResults as a
 * callback function so results from the datasource are returned to the
 * auto complete instance.
 *
 * @param {string} sQuery The query string
 * @param {object} aResults An array of query result objects from the data source
 * @param {string} oSelf The auto complete instance
 * @private
 */
YAHOO.widget.AutoComplete.prototype._populateList = function(sQuery, aResults, oSelf) {
    if(aResults === null) {
        oSelf.dataErrorEvent.fire(oSelf, sQuery);
    }
    if (!oSelf._bFocused || !aResults) {
        return;
    }

    var isOpera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
    var contentStyle = oSelf._oContainer._oContent.style;
    contentStyle.width = (!isOpera) ? null : "";
    contentStyle.height = (!isOpera) ? null : "";

    var sCurQuery = decodeURIComponent(sQuery);
    oSelf._sCurQuery = sCurQuery;
    oSelf._bItemSelected = false;

    if(oSelf._maxResultsDisplayed != oSelf.maxResultsDisplayed) {
        oSelf._initList();
    }

    var nItems = Math.min(aResults.length,oSelf.maxResultsDisplayed);
    oSelf._nDisplayedItems = nItems;
    if (nItems > 0) {
        oSelf._initContainerHelpers();
        var aItems = oSelf._aListItems;

        // Fill items with data
        for(var i = nItems-1; i >= 0; i--) {
            var oItemi = aItems[i];
            var oResultItemi = aResults[i];
            oItemi.innerHTML = oSelf.formatResult(oResultItemi, sCurQuery);
            oItemi.style.display = "list-item";
            oItemi._sResultKey = oResultItemi[0];
            oItemi._oResultData = oResultItemi;

        }

        // Empty out remaining items if any
        for(var j = aItems.length-1; j >= nItems ; j--) {
            var oItemj = aItems[j];
            oItemj.innerHTML = null;
            oItemj.style.display = "none";
            oItemj._sResultKey = null;
            oItemj._oResultData = null;
        }

        if(oSelf.autoHighlight) {
            // Go to the first item
            var oFirstItem = aItems[0];
            oSelf._toggleHighlight(oFirstItem,"to");
            oSelf.itemArrowToEvent.fire(oSelf, oFirstItem);
            oSelf._typeAhead(oFirstItem,sQuery);
        }
        else {
            oSelf._oCurItem = null;
        }

        // Expand the container
        oSelf._toggleContainer(true);
    }
    else {
        oSelf._clearList();
    }
    oSelf.dataReturnEvent.fire(oSelf, sQuery, aResults);
};

/**
 * When YAHOO.widget.AutoComplete#bForceSelection is true and the user attempts
 * leave the text input box without selecting an item from the query results,
 * the user selection is cleared.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._clearSelection = function() {
    var sValue = this._oTextbox.value;
    var sChar = (this.delimChar) ? this.delimChar[0] : null;
    var nIndex = (sChar) ? sValue.lastIndexOf(sChar, sValue.length-2) : -1;
    if(nIndex > -1) {
        this._oTextbox.value = sValue.substring(0,nIndex);
    }
    else {
         this._oTextbox.value = "";
    }
    this._sSavedQuery = this._oTextbox.value;

    // Fire custom event
    this.selectionEnforceEvent.fire(this);
};

/**
 * Whether or not user-typed value in the text input box matches any of the
 * query results.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._textMatchesOption = function() {
    var foundMatch = false;

    for(var i = this._nDisplayedItems-1; i >= 0 ; i--) {
        var oItem = this._aListItems[i];
        var sMatch = oItem._sResultKey.toLowerCase();
        if (sMatch == this._sCurQuery.toLowerCase()) {
            foundMatch = true;
            break;
        }
    }
    return(foundMatch);
};

/**
 * Updates in the text input box with the first query result as the user types,
 * selecting the substring that the user has not typed.
 *
 * @param {object} oItem The &lt;li&gt; element item whose data populates the input field
 * @param {string} sQuery Query string
 * @private
 */
YAHOO.widget.AutoComplete.prototype._typeAhead = function(oItem, sQuery) {
    // Don't update if turned off
    if (!this.typeAhead) {
        return;
    }

    var oTextbox = this._oTextbox;
    var sValue = this._oTextbox.value; // any saved queries plus what user has typed

    // Don't update with type-ahead if text selection is not supported
    if(!oTextbox.setSelectionRange && !oTextbox.createTextRange) {
        return;
    }

    // Select the portion of text that the user has not typed
    var nStart = sValue.length;
    this._updateValue(oItem);
    var nEnd = oTextbox.value.length;
    this._selectText(oTextbox,nStart,nEnd);
    var sPrefill = oTextbox.value.substr(nStart,nEnd);
    this.typeAheadEvent.fire(this,sQuery,sPrefill);
};

/**
 * Selects text in a text input box.
 *
 * @param {object} oTextbox Text input box element in which to select text
 * @param {number} nStart Starting index of text string to select
 * @param {number} nEnd Ending index of text selection
 * @private
 */
YAHOO.widget.AutoComplete.prototype._selectText = function(oTextbox, nStart, nEnd) {
    if (oTextbox.setSelectionRange) { // For Mozilla
        oTextbox.setSelectionRange(nStart,nEnd);
    }
    else if (oTextbox.createTextRange) { // For IE
        var oTextRange = oTextbox.createTextRange();
        oTextRange.moveStart("character", nStart);
        oTextRange.moveEnd("character", nEnd-oTextbox.value.length);
        oTextRange.select();
    }
    else {
        oTextbox.select();
    }
};

/**
 * Syncs auto complete container with its helpers.
 *
 * @param {boolean} bShow True if container is expanded, false if collapsed
 * @private
 */
YAHOO.widget.AutoComplete.prototype._toggleContainerHelpers = function(bShow) {
    var bFireEvent = false;
    var width = this._oContainer._oContent.offsetWidth + "px";
    var height = this._oContainer._oContent.offsetHeight + "px";

    if(this.useIFrame && this._oContainer._oIFrame) {
        bFireEvent = true;
        if(this.alwaysShowContainer || bShow) {
            this._oContainer._oIFrame.style.width = width;
            this._oContainer._oIFrame.style.height = height;
        }
        else {
            this._oContainer._oIFrame.style.width = 0;
            this._oContainer._oIFrame.style.height = 0;
        }
    }
    if(this.useShadow && this._oContainer._oShadow) {
        bFireEvent = true;
        if(this.alwaysShowContainer || bShow) {
            this._oContainer._oShadow.style.width = width;
            this._oContainer._oShadow.style.height = height;
        }
        else {
           this._oContainer._oShadow.style.width = 0;
            this._oContainer._oShadow.style.height = 0;
        }
    }
};

/**
 * Animates expansion or collapse of the container.
 *
 * @param {boolean} bShow True if container should be expanded, false if
 *                        container should be collapsed
 * @private
 */
YAHOO.widget.AutoComplete.prototype._toggleContainer = function(bShow) {
    // Implementer has container always open so don't mess with it
    if(this.alwaysShowContainer) {
        // Fire these events to give implementers a hook into the container
        // being populated and being emptied
        if(bShow) {
            this.containerExpandEvent.fire(this);
        }
        else {
            this.containerCollapseEvent.fire(this);
        }
        this._bContainerOpen = bShow;
        return;
    }

    var oContainer = this._oContainer;
    // Container is already closed
    if (!bShow && !this._bContainerOpen) {
        oContainer._oContent.style.display = "none";
        return;
    }

    // If animation is enabled...
    var oAnim = this._oAnim;
    if (oAnim && oAnim.getEl() && (this.animHoriz || this.animVert)) {
        // If helpers need to be collapsed, do it right away...
        // but if helpers need to be expanded, wait until after the container expands
        if(!bShow) {
            this._toggleContainerHelpers(bShow);
        }

        if(oAnim.isAnimated()) {
            oAnim.stop();
        }

        // Clone container to grab current size offscreen
        var oClone = oContainer._oContent.cloneNode(true);
        oContainer.appendChild(oClone);
        oClone.style.top = "-9000px";
        oClone.style.display = "block";

        // Current size of the container is the EXPANDED size
        var wExp = oClone.offsetWidth;
        var hExp = oClone.offsetHeight;

        // Calculate COLLAPSED sizes based on horiz and vert anim
        var wColl = (this.animHoriz) ? 0 : wExp;
        var hColl = (this.animVert) ? 0 : hExp;

        // Set animation sizes
        oAnim.attributes = (bShow) ?
            {width: { to: wExp }, height: { to: hExp }} :
            {width: { to: wColl}, height: { to: hColl }};

        // If opening anew, set to a collapsed size...
        if(bShow && !this._bContainerOpen) {
            oContainer._oContent.style.width = wColl+"px";
            oContainer._oContent.style.height = hColl+"px";
        }
        // Else, set it to its last known size.
        else {
            oContainer._oContent.style.width = wExp+"px";
            oContainer._oContent.style.height = hExp+"px";
        }

        oContainer.removeChild(oClone);
        oClone = null;

    	var oSelf = this;
    	var onAnimComplete = function() {
            // Finish the collapse
    		oAnim.onComplete.unsubscribeAll();

            if(bShow) {
                oSelf.containerExpandEvent.fire(oSelf);
            }
            else {
                oContainer._oContent.style.display = "none";
                oSelf.containerCollapseEvent.fire(oSelf);
            }
            oSelf._toggleContainerHelpers(bShow);
     	};

        // Display container and animate it
        oContainer._oContent.style.display = "block";
        oAnim.onComplete.subscribe(onAnimComplete);
        oAnim.animate();
        this._bContainerOpen = bShow;
    }
    // Else don't animate, just show or hide
    else {
        if(bShow) {
            oContainer._oContent.style.display = "block";
            this.containerExpandEvent.fire(this);
        }
        else {
            oContainer._oContent.style.display = "none";
            this.containerCollapseEvent.fire(this);
        }
        this._toggleContainerHelpers(bShow);
        this._bContainerOpen = bShow;
   }

};

/**
 * Toggles the highlight on or off for an item in the container, and also cleans
 * up highlighting of any previous item.
 *
 * @param {object} oNewItem New The &lt;li&gt; element item to receive highlight
 *                              behavior
 * @param {string} sType "mouseover" will toggle highlight on, and "mouseout"
 *                       will toggle highlight off.
 * @private
 */
YAHOO.widget.AutoComplete.prototype._toggleHighlight = function(oNewItem, sType) {
    var sHighlight = this.highlightClassName;
    if(this._oCurItem) {
        // Remove highlight from old item
        YAHOO.util.Dom.removeClass(this._oCurItem, sHighlight);
    }

    if((sType == "to") && sHighlight) {
        // Apply highlight to new item
        YAHOO.util.Dom.addClass(oNewItem, sHighlight);
        this._oCurItem = oNewItem;
    }
};

/**
 * Toggles the pre-highlight on or off for an item in the container.
 *
 * @param {object} oNewItem New The &lt;li&gt; element item to receive highlight
 *                              behavior
 * @param {string} sType "mouseover" will toggle highlight on, and "mouseout"
 *                       will toggle highlight off.
 * @private
 */
YAHOO.widget.AutoComplete.prototype._togglePrehighlight = function(oNewItem, sType) {
    if(oNewItem == this._oCurItem) {
        return;
    }

    var sPrehighlight = this.prehighlightClassName;
    if((sType == "mouseover") && sPrehighlight) {
        // Apply prehighlight to new item
        YAHOO.util.Dom.addClass(oNewItem, sPrehighlight);
    }
    else {
        // Remove prehighlight from old item
        YAHOO.util.Dom.removeClass(oNewItem, sPrehighlight);
    }
};

/**
 * Updates the text input box value with selected query result. If a delimiter
 * has been defined, then the value gets appended with the delimiter.
 *
 * @param {object} oItem The &lt;li&gt; element item with which to update the value
 * @private
 */
YAHOO.widget.AutoComplete.prototype._updateValue = function(oItem) {
    var oTextbox = this._oTextbox;
    var sDelimChar = (this.delimChar) ? this.delimChar[0] : null;
    var sSavedQuery = this._sSavedQuery;
    var sResultKey = oItem._sResultKey;
    oTextbox.focus();

    // First clear text field
    oTextbox.value = "";
    // Grab data to put into text field
    if(sDelimChar) {
        if(sSavedQuery) {
            oTextbox.value = sSavedQuery;
        }
        oTextbox.value += sResultKey + sDelimChar;
        if(sDelimChar != " ") {
            oTextbox.value += " ";
        }
    }
    else { oTextbox.value = sResultKey; }

    // scroll to bottom of textarea if necessary
    if(oTextbox.type == "textarea") {
        oTextbox.scrollTop = oTextbox.scrollHeight;
    }

    // move cursor to end
    var end = oTextbox.value.length;
    this._selectText(oTextbox,end,end);

    this._oCurItem = oItem;
};

/**
 * Selects a result item from the container
 *
 * @param {object} oItem The selected &lt;li&gt; element item
 * @private
 */
YAHOO.widget.AutoComplete.prototype._selectItem = function(oItem) {
    this._bItemSelected = true;
    this._updateValue(oItem);
    this.itemSelectEvent.fire(this, oItem, oItem._oResultData);
    this._clearList();
};

/**
 * For values updated by type-ahead, the right arrow key jumps to the end
 * of the textbox, otherwise the container is closed.
 *
 * @private
 */
YAHOO.widget.AutoComplete.prototype._jumpSelection = function() {
    if(!this.typeAhead) {
        return;
    }
    else {
        this._clearList();
    }
};

/**
 * Triggered by up and down arrow keys, changes the current highlighted
 * &lt;li&gt; element item. Scrolls container if necessary.
 *
 * @param {number} nKeyCode Code of key pressed
 * @private
 */
YAHOO.widget.AutoComplete.prototype._moveSelection = function(nKeyCode) {
    if(this._bContainerOpen) {
        // Determine current item's id number
        var oCurItem = this._oCurItem;
        var nCurItemIndex = -1;

        if (oCurItem) {
            nCurItemIndex = oCurItem._nItemIndex;
        }

        var nNewItemIndex = (nKeyCode == 40) ?
                (nCurItemIndex + 1) : (nCurItemIndex - 1);

        // Out of bounds
        if (nNewItemIndex < -2 || nNewItemIndex >= this._nDisplayedItems) {
            return;
        }

        if (oCurItem) {
            // Unhighlight current item
            this._toggleHighlight(oCurItem, "from");
            this.itemArrowFromEvent.fire(this, oCurItem);
        }
        if (nNewItemIndex == -1) {
           // Go back to query (remove type-ahead string)
            if(this.delimChar && this._sSavedQuery) {
                if (!this._textMatchesOption()) {
                    this._oTextbox.value = this._sSavedQuery;
                }
                else {
                    this._oTextbox.value = this._sSavedQuery + this._sCurQuery;
                }
            }
            else {
                this._oTextbox.value = this._sCurQuery;
            }
            this._oCurItem = null;
            return;
        }
        if (nNewItemIndex == -2) {
            // Close container
            this._clearList();
            return;
        }

        var oNewItem = this._aListItems[nNewItemIndex];

        // Scroll the container if necessary
        var oContent = this._oContainer._oContent;
        var scrollOn = ((YAHOO.util.Dom.getStyle(oContent,"overflow") == "auto") ||
            (YAHOO.util.Dom.getStyle(oContent,"overflowY") == "auto"));
        if(scrollOn && (nNewItemIndex > -1) &&
        (nNewItemIndex < this._nDisplayedItems)) {
            // User is keying down
            if(nKeyCode == 40) {
                // Bottom of selected item is below scroll area...
                if((oNewItem.offsetTop+oNewItem.offsetHeight) > (oContent.scrollTop + oContent.offsetHeight)) {
                    // Set bottom of scroll area to bottom of selected item
                    oContent.scrollTop = (oNewItem.offsetTop+oNewItem.offsetHeight) - oContent.offsetHeight;
                }
                // Bottom of selected item is above scroll area...
                else if((oNewItem.offsetTop+oNewItem.offsetHeight) < oContent.scrollTop) {
                    // Set top of selected item to top of scroll area
                    oContent.scrollTop = oNewItem.offsetTop;

                }
            }
            // User is keying up
            else {
                // Top of selected item is above scroll area
                if(oNewItem.offsetTop < oContent.scrollTop) {
                    // Set top of scroll area to top of selected item
                    this._oContainer._oContent.scrollTop = oNewItem.offsetTop;
                }
                // Top of selected item is below scroll area
                else if(oNewItem.offsetTop > (oContent.scrollTop + oContent.offsetHeight)) {
                    // Set bottom of selected item to bottom of scroll area
                    this._oContainer._oContent.scrollTop = (oNewItem.offsetTop+oNewItem.offsetHeight) - oContent.offsetHeight;
                }
            }
        }

        this._toggleHighlight(oNewItem, "to");
        this.itemArrowToEvent.fire(this, oNewItem);
        if(this.typeAhead) {
            this._updateValue(oNewItem);
        }
    }
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Class providing encapsulation of a data source. 
 *  
 * @constructor
 *
 */
YAHOO.widget.DataSource = function() { 
    /* abstract class */
};


/***************************************************************************
 * Public constants
 ***************************************************************************/
/**
 * Error message for null data responses.
 *
 * @type constant
 * @final
 */
YAHOO.widget.DataSource.prototype.ERROR_DATANULL = "Response data was null";

/**
 * Error message for data responses with parsing errors.
 *
 * @type constant
 * @final
 */
YAHOO.widget.DataSource.prototype.ERROR_DATAPARSE = "Response data could not be parsed";


/***************************************************************************
 * Public member variables
 ***************************************************************************/
/**
 * Max size of the local cache.  Set to 0 to turn off caching.  Caching is
 * useful to reduce the number of server connections.  Recommended only for data
 * sources that return comprehensive results for queries or when stale data is
 * not an issue. Default: 15.
 *
 * @type number
 */
YAHOO.widget.DataSource.prototype.maxCacheEntries = 15;

/**
 * Use this to equate cache matching with the type of matching done by your live
 * data source. If caching is on and queryMatchContains is true, the cache
 * returns results that "contain" the query string. By default,
 * queryMatchContains is set to false, meaning the cache only returns results
 * that "start with" the query string. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.DataSource.prototype.queryMatchContains = false;

/**
 * Data source query subset matching. If caching is on and queryMatchSubset is
 * true, substrings of queries will return matching cached results. For
 * instance, if the first query is for "abc" susequent queries that start with
 * "abc", like "abcd", will be queried against the cache, and not the live data
 * source. Recommended only for data sources that return comprehensive results
 * for queries with very few characters. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.DataSource.prototype.queryMatchSubset = false;

/**
 * Data source query case-sensitivity matching. If caching is on and
 * queryMatchCase is true, queries will only return results for case-sensitive
 * matches. Default: false.
 *
 * @type boolean
 */
YAHOO.widget.DataSource.prototype.queryMatchCase = false;


/***************************************************************************
 * Public methods
 ***************************************************************************/
 /**
 * Public accessor to the unique name of the data source instance.
 *
 * @return {string} Unique name of the data source instance
 */
YAHOO.widget.DataSource.prototype.getName = function() {
    return this._sName;
};

 /**
 * Public accessor to the unique name of the data source instance.
 *
 * @return {string} Unique name of the data source instance
 */
YAHOO.widget.DataSource.prototype.toString = function() {
    return "DataSource " + this._sName;
};

/**
 * Retrieves query results, first checking the local cache, then making the
 * query request to the live data source as defined by the function doQuery.
 *
 * @param {object} oCallbackFn Callback function defined by oParent object to
 *                             which to return results 
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 */
YAHOO.widget.DataSource.prototype.getResults = function(oCallbackFn, sQuery, oParent) {
    
    // First look in cache
    var aResults = this._doQueryCache(oCallbackFn,sQuery,oParent);
    
    // Not in cache, so get results from server
    if(aResults.length === 0) {
        this.queryEvent.fire(this, oParent, sQuery);
        this.doQuery(oCallbackFn, sQuery, oParent);
    }
};

/**
 * Abstract method implemented by subclasses to make a query to the live data
 * source. Must call the callback function with the response returned from the
 * query. Populates cache (if enabled).
 *
 * @param {object} oCallbackFn Callback function implemented by oParent to
 *                             which to return results 
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 */
YAHOO.widget.DataSource.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
    /* override this */ 
};

/**
 * Flushes cache.
 */
YAHOO.widget.DataSource.prototype.flushCache = function() {
    if(this._aCache) {
        this._aCache = [];
    }
    if(this._aCacheHelper) {
        this._aCacheHelper = [];
    }
    this.cacheFlushEvent.fire(this);
};

/***************************************************************************
 * Events
 ***************************************************************************/
/**
 * Fired when a query is made to the live data source. Subscribers receive the
 * following array:<br>
 *     - args[0] The data source instance
 *     - args[1] The requesting object
 *     - args[2] The query string
 */
YAHOO.widget.DataSource.prototype.queryEvent = null;

/**
 * Fired when a query is made to the local cache. Subscribers receive the
 * following array:<br>
 *     - args[0] The data source instance
 *     - args[1] The requesting object
 *     - args[2] The query string
 */
YAHOO.widget.DataSource.prototype.cacheQueryEvent = null;

/**
 * Fired when data is retrieved from the live data source. Subscribers receive
 * the following array:<br>
 *     - args[0] The data source instance
 *     - args[1] The requesting object
 *     - args[2] The query string
 *     - args[3] Array of result objects
 */
YAHOO.widget.DataSource.prototype.getResultsEvent = null;
    
/**
 * Fired when data is retrieved from the local cache. Subscribers receive the
 * following array :<br>
 *     - args[0] The data source instance
 *     - args[1] The requesting object
 *     - args[2] The query string
 *     - args[3] Array of result objects
 */
YAHOO.widget.DataSource.prototype.getCachedResultsEvent = null;

/**
 * Fired when an error is encountered with the live data source. Subscribers
 * receive the following array:<br>
 *     - args[0] The data source instance
 *     - args[1] The requesting object
 *     - args[2] The query string
 *     - args[3] Error message string
 */
YAHOO.widget.DataSource.prototype.dataErrorEvent = null;

/**
 * Fired when the local cache is flushed. Subscribers receive the following
 * array :<br>
 *     - args[0] The data source instance
 */
YAHOO.widget.DataSource.prototype.cacheFlushEvent = null;

/***************************************************************************
 * Private member variables
 ***************************************************************************/
/**
 * Internal class variable to index multiple data source instances.
 *
 * @type number
 * @private
 */
YAHOO.widget.DataSource._nIndex = 0;

/**
 * Name of data source instance.
 *
 * @type string
 * @private
 */
YAHOO.widget.DataSource.prototype._sName = null;

/**
 * Local cache of data result objects indexed chronologically.
 *
 * @type array
 * @private
 */
YAHOO.widget.DataSource.prototype._aCache = null;


/***************************************************************************
 * Private methods
 ***************************************************************************/
/**
 * Initializes data source instance.
 *  
 * @private
 */
YAHOO.widget.DataSource.prototype._init = function() {
    // Validate and initialize public configs
    var maxCacheEntries = this.maxCacheEntries;
    if(isNaN(maxCacheEntries) || (maxCacheEntries < 0)) {
        maxCacheEntries = 0;
    }
    // Initialize local cache
    if(maxCacheEntries > 0 && !this._aCache) {
        this._aCache = [];
    }
    
    this._sName = "instance" + YAHOO.widget.DataSource._nIndex;
    YAHOO.widget.DataSource._nIndex++;
    
    this.queryEvent = new YAHOO.util.CustomEvent("query", this);
    this.cacheQueryEvent = new YAHOO.util.CustomEvent("cacheQuery", this);
    this.getResultsEvent = new YAHOO.util.CustomEvent("getResults", this);
    this.getCachedResultsEvent = new YAHOO.util.CustomEvent("getCachedResults", this);
    this.dataErrorEvent = new YAHOO.util.CustomEvent("dataError", this);
    this.cacheFlushEvent = new YAHOO.util.CustomEvent("cacheFlush", this);
};

/**
 * Adds a result object to the local cache, evicting the oldest element if the 
 * cache is full. Newer items will have higher indexes, the oldest item will have
 * index of 0. 
 *
 * @param {object} resultObj  Object literal of data results, including internal
 *                            properties and an array of result objects
 * @private
 */
YAHOO.widget.DataSource.prototype._addCacheElem = function(resultObj) {
    var aCache = this._aCache;
    // Don't add if anything important is missing.
    if(!aCache || !resultObj || !resultObj.query || !resultObj.results) {
        return;
    }
    
    // If the cache is full, make room by removing from index=0
    if(aCache.length >= this.maxCacheEntries) {
        aCache.shift();
    }
        
    // Add to cache, at the end of the array
    aCache.push(resultObj);
};

/**
 * Queries the local cache for results. If query has been cached, the callback
 * function is called with the results, and the cached is refreshed so that it
 * is now the newest element.  
 *
 * @param {object} oCallbackFn Callback function defined by oParent object to 
 *                             which to return results 
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 * @return {array} aResults Result object from local cache if found, otherwise 
 *                          null
 * @private 
 */
YAHOO.widget.DataSource.prototype._doQueryCache = function(oCallbackFn, sQuery, oParent) {
    var aResults = [];
    var bMatchFound = false;
    var aCache = this._aCache;
    var nCacheLength = (aCache) ? aCache.length : 0;
    var bMatchContains = this.queryMatchContains;
    
    // If cache is enabled...
    if((this.maxCacheEntries > 0) && aCache && (nCacheLength > 0)) {
        this.cacheQueryEvent.fire(this, oParent, sQuery);
        // If case is unimportant, normalize query now instead of in loops
        if(!this.queryMatchCase) {
            var sOrigQuery = sQuery;
            sQuery = sQuery.toLowerCase();
        }

        // Loop through each cached element's query property...
        for(var i = nCacheLength-1; i >= 0; i--) {
            var resultObj = aCache[i];
            var aAllResultItems = resultObj.results;
            // If case is unimportant, normalize match key for comparison
            var matchKey = (!this.queryMatchCase) ?
                encodeURIComponent(resultObj.query.toLowerCase()):
                encodeURIComponent(resultObj.query);
            
            // If a cached match key exactly matches the query...
            if(matchKey == sQuery) {
                    // Stash all result objects into aResult[] and stop looping through the cache.
                    bMatchFound = true;
                    aResults = aAllResultItems;
                    
                    // The matching cache element was not the most recent,
                    // so now we need to refresh the cache.
                    if(i != nCacheLength-1) {                        
                        // Remove element from its original location
                        aCache.splice(i,1);
                        // Add element as newest
                        this._addCacheElem(resultObj);
                    }
                    break;
            }
            // Else if this query is not an exact match and subset matching is enabled...
            else if(this.queryMatchSubset) {
                // Loop through substrings of each cached element's query property...
                for(var j = sQuery.length-1; j >= 0 ; j--) {
                    var subQuery = sQuery.substr(0,j);
                    
                    // If a substring of a cached sQuery exactly matches the query...
                    if(matchKey == subQuery) {                    
                        bMatchFound = true;
                        
                        // Go through each cached result object to match against the query...
                        for(var k = aAllResultItems.length-1; k >= 0; k--) {
                            var aRecord = aAllResultItems[k];
                            var sKeyIndex = (this.queryMatchCase) ?
                                encodeURIComponent(aRecord[0]).indexOf(sQuery):
                                encodeURIComponent(aRecord[0]).toLowerCase().indexOf(sQuery);
                            
                            // A STARTSWITH match is when the query is found at the beginning of the key string...
                            if((!bMatchContains && (sKeyIndex === 0)) ||
                            // A CONTAINS match is when the query is found anywhere within the key string...
                            (bMatchContains && (sKeyIndex > -1))) {
                                // Stash a match into aResults[].
                                aResults.unshift(aRecord);
                            }
                        }
                        
                        // Add the subset match result set object as the newest element to cache,
                        // and stop looping through the cache.
                        resultObj = {};
                        resultObj.query = sQuery;
                        resultObj.results = aResults;
                        this._addCacheElem(resultObj);
                        break;
                    }
                }
                if(bMatchFound) {
                    break;
                }
            }
        }
        
        // If there was a match, send along the results.
        if(bMatchFound) {
            this.getCachedResultsEvent.fire(this, oParent, sOrigQuery, aResults);
            oCallbackFn(sOrigQuery, aResults, oParent);
        }
    }
    return aResults;
};


/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Implementation of YAHOO.widget.DataSource using XML HTTP requests that return
 * query results.
 * requires YAHOO.util.Connect XMLHTTPRequest library
 * extends YAHOO.widget.DataSource
 *  
 * @constructor
 * @param {string} sScriptURI Absolute or relative URI to script that returns 
 *                            query results as JSON, XML, or delimited flat data
 * @param {array} aSchema Data schema definition of results
 * @param {object} oConfigs Optional object literal of config params
 */
YAHOO.widget.DS_XHR = function(sScriptURI, aSchema, oConfigs) {
    // Set any config params passed in to override defaults
    if(typeof oConfigs == "object") {
        for(var sConfig in oConfigs) {
            this[sConfig] = oConfigs[sConfig];
        }
    }
    
    // Initialization sequence
    if(!aSchema || (aSchema.constructor != Array)) {
        return;
    }
    else {
        this.schema = aSchema;
    }
    this.scriptURI = sScriptURI;
    this._init();
};

YAHOO.widget.DS_XHR.prototype = new YAHOO.widget.DataSource();

/***************************************************************************
 * Public constants
 ***************************************************************************/
/**
 * JSON data type
 *
 * @type constant
 * @final
 */
YAHOO.widget.DS_XHR.prototype.TYPE_JSON = 0;

/**
 * XML data type
 *
 * @type constant
 * @final
 */
YAHOO.widget.DS_XHR.prototype.TYPE_XML = 1;

/**
 * Flat file data type
 *
 * @type constant
 * @final
 */
YAHOO.widget.DS_XHR.prototype.TYPE_FLAT = 2;

/**
 * Error message for XHR failure.
 *
 * @type constant
 * @final
 */
YAHOO.widget.DS_XHR.prototype.ERROR_DATAXHR = "XHR response failed";

/***************************************************************************
 * Public member variables
 ***************************************************************************/
/**
 * Number of milliseconds the XHR connection will wait for a server response. A
 * a value of zero indicates the XHR connection will wait forever. Any value
 * greater than zero will use the Connection utility's Auto-Abort feature.
 * Default: 0.
 *
 * @type number
 */
YAHOO.widget.DS_XHR.prototype.connTimeout = 0;


/**
 * Absolute or relative URI to script that returns query results. For instance,
 * queries will be sent to
 *   <scriptURI>?<scriptQueryParam>=userinput
 *
 * @type string
 */
YAHOO.widget.DS_XHR.prototype.scriptURI = null;

/**
 * Query string parameter name sent to scriptURI. For instance, queries will be
 * sent to
 *   <scriptURI>?<scriptQueryParam>=userinput
 * Default: "query".
 *
 * @type string
 */
YAHOO.widget.DS_XHR.prototype.scriptQueryParam = "query";

/**
 * String of key/value pairs to append to requests made to scriptURI. Define
 * this string when you want to send additional query parameters to your script.
 * When defined, queries will be sent to
 *   <scriptURI>?<scriptQueryParam>=userinput&<scriptQueryAppend>
 * Default: "".
 *
 * @type string
 */
YAHOO.widget.DS_XHR.prototype.scriptQueryAppend = "";

/**
 * XHR response data type. Other types that may be defined are TYPE_XML and
 * TYPE_FLAT. Default: TYPE_JSON.
 *
 * @type type
 */
YAHOO.widget.DS_XHR.prototype.responseType = YAHOO.widget.DS_XHR.prototype.TYPE_JSON;

/**
 * String after which to strip results. If the results from the XHR are sent
 * back as HTML, the gzip HTML comment appears at the end of the data and should
 * be ignored.  Default: "\n&lt;!--"
 *
 * @type string
 */
YAHOO.widget.DS_XHR.prototype.responseStripAfter = "\n<!--";

/***************************************************************************
 * Public methods
 ***************************************************************************/
/**
 * Queries the live data source defined by scriptURI for results. Results are
 * passed back to a callback function.
 *  
 * @param {object} oCallbackFn Callback function defined by oParent object to 
 *                             which to return results 
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 */
YAHOO.widget.DS_XHR.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
    var isXML = (this.responseType == this.TYPE_XML);
    var sUri = this.scriptURI+"?"+this.scriptQueryParam+"="+sQuery;
    if(this.scriptQueryAppend.length > 0) {
        sUri += "&" + this.scriptQueryAppend;
    }
    var oResponse = null;
    
    var oSelf = this;
    /**
     * Sets up ajax request callback
     *
     * @param {object} oReq          HTTPXMLRequest object
     * @private
     */
    var responseSuccess = function(oResp) {
        // Response ID does not match last made request ID.
        if(!oSelf._oConn || (oResp.tId != oSelf._oConn.tId)) {
            oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, oSelf.ERROR_DATANULL);
            return;
        }
//DEBUG
for(var foo in oResp) {
}
        if(!isXML) {
            oResp = oResp.responseText;
        }
        else { 
            oResp = oResp.responseXML;
        }
        if(oResp === null) {
            oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, oSelf.ERROR_DATANULL);
            return;
        }

        var aResults = oSelf.parseResponse(sQuery, oResp, oParent);
        var resultObj = {};
        resultObj.query = decodeURIComponent(sQuery);
        resultObj.results = aResults;
        if(aResults === null) {
            oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, oSelf.ERROR_DATAPARSE);
            return;
        }
        else {
            oSelf.getResultsEvent.fire(oSelf, oParent, sQuery, aResults);
            oSelf._addCacheElem(resultObj);
            oCallbackFn(sQuery, aResults, oParent);
        }
    };

    var responseFailure = function(oResp) {
        oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, oSelf.ERROR_DATAXHR);
        return;
    };
    
    var oCallback = {
        success:responseSuccess,
        failure:responseFailure
    };
    
    if(!isNaN(this.connTimeout) && this.connTimeout > 0) {
        oCallback.timeout = this.connTimeout;
    }
    
    if(this._oConn) {
        YAHOO.util.Connect.abort(this._oConn);
    }
    
    oSelf._oConn = YAHOO.util.Connect.asyncRequest("GET", sUri, oCallback, null);
};

/**
 * Parses raw response data into an array of result objects. The result data key
 * is always stashed in the [0] element of each result object. 
 *
 * @param {string} sQuery Query string
 * @param {object} oResponse The raw response data to parse
 * @param {object} oParent The object instance that has requested data
 * @returns {array} Array of result objects
 */
YAHOO.widget.DS_XHR.prototype.parseResponse = function(sQuery, oResponse, oParent) {
    var aSchema = this.schema;
    var aResults = [];
    var bError = false;

    // Strip out comment at the end of results
    var nEnd = ((this.responseStripAfter !== "") && (oResponse.indexOf)) ?
        oResponse.indexOf(this.responseStripAfter) : -1;
    if(nEnd != -1) {
        oResponse = oResponse.substring(0,nEnd);
    }

    switch (this.responseType) {
        case this.TYPE_JSON:
            var jsonList;
            // Divert KHTML clients from JSON lib
            if(window.JSON && (navigator.userAgent.toLowerCase().indexOf('khtml')== -1)) {
                // Use the JSON utility if available
                var jsonObjParsed = JSON.parse(oResponse);
                if(!jsonObjParsed) {
                    bError = true;
                    break;
                }
                else {
                    // eval is necessary here since aSchema[0] is of unknown depth
                    jsonList = eval("jsonObjParsed." + aSchema[0]);
                }
            }
            else {
                // Parse the JSON response as a string
                try {
                    // Trim leading spaces
                    while (oResponse.substring(0,1) == " ") {
                        oResponse = oResponse.substring(1, oResponse.length);
                    }

                    // Invalid JSON response
                    if(oResponse.indexOf("{") < 0) {
                        bError = true;
                        break;
                    }

                    // Empty (but not invalid) JSON response
                    if(oResponse.indexOf("{}") === 0) {
                        break;
                    }

                    // Turn the string into an object literal...
                    // ...eval is necessary here
                    var jsonObjRaw = eval("(" + oResponse + ")");
                    if(!jsonObjRaw) {
                        bError = true;
                        break;
                    }

                    // Grab the object member that contains an array of all reponses...
                    // ...eval is necessary here since aSchema[0] is of unknown depth
                    jsonList = eval("(jsonObjRaw." + aSchema[0]+")");
                }
                catch(e) {
                    bError = true;
                    break;
               }
            }

            if(!jsonList) {
                bError = true;
                break;
            }

            if(jsonList.constructor != Array) {
                jsonList = [jsonList];
            }
            
            // Loop through the array of all responses...
            for(var i = jsonList.length-1; i >= 0 ; i--) {
                var aResultItem = [];
                var jsonResult = jsonList[i];
                // ...and loop through each data field value of each response
                for(var j = aSchema.length-1; j >= 1 ; j--) {
                    // ...and capture data into an array mapped according to the schema...
                    var dataFieldValue = jsonResult[aSchema[j]];
                    if(!dataFieldValue) {
                        dataFieldValue = "";
                    }
                    aResultItem.unshift(dataFieldValue);
                }
                // Capture the array of data field values in an array of results
                aResults.unshift(aResultItem);
            }
            break;
        case this.TYPE_XML:
            // Get the collection of results
            var xmlList = oResponse.getElementsByTagName(aSchema[0]);
            if(!xmlList) {
                bError = true;
                break;
            }
            // Loop through each result
            for(var k = xmlList.length-1; k >= 0 ; k--) {
                var result = xmlList.item(k);
                var aFieldSet = [];
                // Loop through each data field in each result using the schema
                for(var m = aSchema.length-1; m >= 1 ; m--) {
                    var sValue = null;
                    // Values may be held in an attribute...
                    var xmlAttr = result.attributes.getNamedItem(aSchema[m]);
                    if(xmlAttr) {
                        sValue = xmlAttr.value;
                    }
                    // ...or in a node
                    else{
                        var xmlNode = result.getElementsByTagName(aSchema[m]);
                        if(xmlNode && xmlNode.item(0) && xmlNode.item(0).firstChild) {
                            sValue = xmlNode.item(0).firstChild.nodeValue;
                        }
                        else {
                            sValue = "";
                        }
                    }
                    // Capture the schema-mapped data field values into an array
                    aFieldSet.unshift(sValue);
                }
                // Capture each array of values into an array of results
                aResults.unshift(aFieldSet);
            }
            break;
        case this.TYPE_FLAT:
            if(oResponse.length > 0) {
                // Delete the last line delimiter at the end of the data if it exists
                var newLength = oResponse.length-aSchema[0].length;
                if(oResponse.substr(newLength) == aSchema[0]) {
                    oResponse = oResponse.substr(0, newLength);
                }
                var aRecords = oResponse.split(aSchema[0]);
                for(var n = aRecords.length-1; n >= 0; n--) {
                    aResults[n] = aRecords[n].split(aSchema[1]);
                }
            }
            break;
        default:
            break;
    }
    sQuery = null;
    oResponse = null;
    oParent = null;
    if(bError) {
        return null;
    }
    else {
        return aResults;
    }
};            


/***************************************************************************
 * Private member variables
 ***************************************************************************/
/**
 * XHR connection object.
 *
 * @type object
 * @private
 */
YAHOO.widget.DS_XHR.prototype._oConn = null;


/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Implementation of YAHOO.widget.DataSource using a native Javascript struct as
 * its live data source.
 *  
 * @constructor
 * extends YAHOO.widget.DataSource 
 *  
 * @param {string} oFunction In-memory Javascript function that returns query
 *                           results as an array of objects
 * @param {object} oConfigs Optional object literal of config params
 */
YAHOO.widget.DS_JSFunction = function(oFunction, oConfigs) {
    // Set any config params passed in to override defaults
    if(typeof oConfigs == "object") {
        for(var sConfig in oConfigs) {
            this[sConfig] = oConfigs[sConfig];
        }
    }

    // Initialization sequence
    if(!oFunction  || (oFunction.constructor != Function)) {
        return;
    }
    else {
        this.dataFunction = oFunction;
        this._init();
    }
};

YAHOO.widget.DS_JSFunction.prototype = new YAHOO.widget.DataSource();

/***************************************************************************
 * Public member variables
 ***************************************************************************/
/**
 * In-memory Javascript function that returns query results.
 *
 * @type function
 */
YAHOO.widget.DS_JSFunction.prototype.dataFunction = null;


/***************************************************************************
 * Public methods
 ***************************************************************************/
/**
 * Queries the live data source defined by function for results. Results are
 * passed back to a callback function.
 *  
 * @param {object} oCallbackFn Callback function defined by oParent object to 
 *                             which to return results 
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 */
YAHOO.widget.DS_JSFunction.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
    var oFunction = this.dataFunction;
    var aResults = [];
    
    aResults = oFunction(sQuery);
    if(aResults === null) {
        this.dataErrorEvent.fire(this, oParent, sQuery, this.ERROR_DATANULL);
        return;
    }
    
    var resultObj = {};
    resultObj.query = decodeURIComponent(sQuery);
    resultObj.results = aResults;
    this._addCacheElem(resultObj);
    
    this.getResultsEvent.fire(this, oParent, sQuery, aResults);
    oCallbackFn(sQuery, aResults, oParent);
    return;
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Implementation of YAHOO.widget.DataSource using a native Javascript array as
 * its live data source.
 *
 * @constructor
 * extends YAHOO.widget.DataSource
 *
 * @param {string} aData In-memory Javascript array of simple string data
 * @param {object} oConfigs Optional object literal of config params
 */
YAHOO.widget.DS_JSArray = function(aData, oConfigs) {
    // Set any config params passed in to override defaults
    if(typeof oConfigs == "object") {
        for(var sConfig in oConfigs) {
            this[sConfig] = oConfigs[sConfig];
        }
    }

    // Initialization sequence
    if(!aData || (aData.constructor != Array)) {
        return;
    }
    else {
        this.data = aData;
        this._init();
    }
};

YAHOO.widget.DS_JSArray.prototype = new YAHOO.widget.DataSource();

/***************************************************************************
 * Public member variables
 ***************************************************************************/
/**
 * In-memory Javascript array of strings.
 *
 * @type array
 */
YAHOO.widget.DS_JSArray.prototype.data = null;

/***************************************************************************
 * Public methods
 ***************************************************************************/
/**
 * Queries the live data source defined by data for results. Results are passed
 * back to a callback function.
 *
 * @param {object} oCallbackFn Callback function defined by oParent object to
 *                             which to return results
 * @param {string} sQuery Query string
 * @param {object} oParent The object instance that has requested data
 */
YAHOO.widget.DS_JSArray.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
    var aData = this.data; // the array
    var aResults = []; // container for results
    var bMatchFound = false;
    var bMatchContains = this.queryMatchContains;
    if(!this.queryMatchCase) {
        sQuery = sQuery.toLowerCase();
    }

    // Loop through each element of the array...
    // which can be a string or an array of strings
    for(var i = aData.length-1; i >= 0; i--) {
        var aDataset = [];

        if(aData[i]) {
            if(aData[i].constructor == String) {
                aDataset[0] = aData[i];
            }
            else if(aData[i].constructor == Array) {
                aDataset = aData[i];
            }
        }

        if(aDataset[0] && (aDataset[0].constructor == String)) {
            var sKeyIndex = (this.queryMatchCase) ?
            encodeURIComponent(aDataset[0]).indexOf(sQuery):
            encodeURIComponent(aDataset[0]).toLowerCase().indexOf(sQuery);

            // A STARTSWITH match is when the query is found at the beginning of the key string...
            if((!bMatchContains && (sKeyIndex === 0)) ||
            // A CONTAINS match is when the query is found anywhere within the key string...
            (bMatchContains && (sKeyIndex > -1))) {
                // Stash a match into aResults[].
                aResults.unshift(aDataset);
            }
        }
    }

    this.getResultsEvent.fire(this, oParent, sQuery, aResults);
    oCallbackFn(sQuery, aResults, oParent);
};

/*** End of file, autocomplete.js ***/

/*** Start of file, slider.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.3
*/

/**
 * A DragDrop implementation that can be used as a background for a
 * slider.  It takes a reference to the thumb instance
 * so it can delegate some of the events to it.  The goal is to make the
 * thumb jump to the location on the background when the background is
 * clicked.
 *
 * @extends YAHOO.util.DragDrop
 * @constructor
 * @param {String} id     The id of the element linked to this instance
 * @param {String} sGroup The group of related DragDrop items
 * @param {String} sType  The type of slider (horiz, vert, region)
 */
YAHOO.widget.Slider = function(sElementId, sGroup, oThumb, sType) {
	if (sElementId) {

        /**
         * The type of the slider (horiz, vert, region)
         * @type string
         */
        this.type = sType;

		this.init(sElementId, sGroup, true);


		var self = this;

		/**
		 * a YAHOO.widget.SliderThumb instance that we will use to
		 * reposition the thumb when the background is clicked
		 *
		 * @type Slider
		 */
		this.thumb = oThumb;

		// add handler for the handle onchange event
		oThumb.onChange = function() {
			self.onThumbChange();
		};

		/**
		 * Overrides the isTarget property in YAHOO.util.DragDrop
		 * @private
		 */
		this.isTarget = false;

		/**
		 * Flag that determines if the thumb will animate when moved
		 * @type boolean
		 */
		this.animate = YAHOO.widget.Slider.ANIM_AVAIL;

        /**
         * Set to false to disable a background click thumb move
         */
        this.backgroundEnabled = true;

		/**
		 * Adjustment factor for tick animation, the more ticks, the
		 * faster the animation (by default)
		 *
		 * @type int
		 */
		this.tickPause = 40;
		if (oThumb._isHoriz && oThumb.xTicks && oThumb.xTicks.length) {
			this.tickPause = Math.round(360 / oThumb.xTicks.length);
		} else if (oThumb.yTicks && oThumb.yTicks.length) {
			this.tickPause = Math.round(360 / oThumb.yTicks.length);
		}


		// delegate thumb methods
		oThumb.onMouseDown = function () { return self.focus(); };
		//oThumb.b4MouseDown = function () { return self.b4MouseDown(); };
		// oThumb.lock = function() { self.lock(); };
		// oThumb.unlock = function() { self.unlock(); };
		oThumb.onMouseUp = function() { self.thumbMouseUp(); };
		oThumb.onDrag = function() { self.fireEvents(); };
		oThumb.onAvailable = function() { return self.setStartSliderState(); };
	}
};

//YAHOO.widget.Slider.prototype = new YAHOO.util.DragDrop();
YAHOO.extend(YAHOO.widget.Slider, YAHOO.util.DragDrop);

// YAHOO.widget.Slider.prototype.onAvailable = function() {
// };

/**
 * Factory method for creating a horizontal slider
 *
 * @param {String} sBGElId the id of the slider's background element
 * @param {String} sHandleElId the id of the thumb element
 * @param {int} iLeft the number of pixels the element can move left
 * @param {int} iRight the number of pixels the element can move right
 * @param {int} iTickSize optional parameter for specifying that the element
 * should move a certain number pixels at a time.
 * @return {Slider} a horizontal slider control
 */
YAHOO.widget.Slider.getHorizSlider =
	function (sBGElId, sHandleElId, iLeft, iRight, iTickSize) {
		return new YAHOO.widget.Slider(sBGElId, sBGElId,
			new YAHOO.widget.SliderThumb(sHandleElId, sBGElId,
							   iLeft, iRight, 0, 0, iTickSize), "horiz");
};

/**
 * Factory method for creating a vertical slider
 *
 * @param {String} sBGElId the id of the slider's background element
 * @param {String} sHandleElId the id of the thumb element
 * @param {int} iUp the number of pixels the element can move up
 * @param {int} iDown the number of pixels the element can move down
 * @param {int} iTickSize optional parameter for specifying that the element
 * should move a certain number pixels at a time.
 * @return {Slider} a vertical slider control
 */
YAHOO.widget.Slider.getVertSlider =
	function (sBGElId, sHandleElId, iUp, iDown, iTickSize) {
		return new YAHOO.widget.Slider(sBGElId, sBGElId,
			new YAHOO.widget.SliderThumb(sHandleElId, sBGElId, 0, 0,
							   iUp, iDown, iTickSize), "vert");
};

/**
 * Factory method for creating a slider region like the one in the color
 * picker example
 *
 * @param {String} sBGElId the id of the slider's background element
 * @param {String} sHandleElId the id of the thumb element
 * @param {int} iLeft the number of pixels the element can move left
 * @param {int} iRight the number of pixels the element can move right
 * @param {int} iUp the number of pixels the element can move up
 * @param {int} iDown the number of pixels the element can move down
 * @param {int} iTickSize optional parameter for specifying that the element
 * should move a certain number pixels at a time.
 * @return {Slider} a slider region control
 */
YAHOO.widget.Slider.getSliderRegion =
	function (sBGElId, sHandleElId, iLeft, iRight, iUp, iDown, iTickSize) {
		return new YAHOO.widget.Slider(sBGElId, sBGElId,
			new YAHOO.widget.SliderThumb(sHandleElId, sBGElId, iLeft, iRight,
							   iUp, iDown, iTickSize), "region");
};

/**
 * By default, animation is available if the animation library is detected.
 * @type boolean
 */
YAHOO.widget.Slider.ANIM_AVAIL = true;

YAHOO.widget.Slider.prototype.setStartSliderState = function() {


    this.setThumbCenterPoint();

    /**
     * The basline position of the background element, used
     * to determine if the background has moved since the last
     * operation.
     * @type int[]
     */
    this.baselinePos = YAHOO.util.Dom.getXY(this.getEl());

    this.thumb.startOffset = this.thumb.getOffsetFromParent(this.baselinePos);

    if (this.thumb._isRegion) {
        if (this.deferredSetRegionValue) {
            this.setRegionValue.apply(this, this.deferredSetRegionValue, true);
        } else {
            this.setRegionValue(0, 0, true);
        }
    } else {
        if (this.deferredSetValue) {
            this.setValue.apply(this, this.deferredSetValue, true);
        } else {
            this.setValue(0, true, true);
        }
    }
};

YAHOO.widget.Slider.prototype.setThumbCenterPoint = function() {

    var el = this.thumb.getEl();

    if (el) {
        /**
         * the center of the slider element is stored so we can position
         * place it in the correct position when the background is clicked
         *
         * @type Coordinate
         */
        this.thumbCenterPoint = {
                x: parseInt(el.offsetWidth/2, 10),
                y: parseInt(el.offsetHeight/2, 10)
        };
    }

};

/**
 * Lock the slider, overrides YAHOO.util.DragDrop
 */
YAHOO.widget.Slider.prototype.lock = function() {
	this.thumb.lock();
	this.locked = true;
};

/**
 * Unlock the slider, overrides YAHOO.util.DragDrop
 */
YAHOO.widget.Slider.prototype.unlock = function() {
	this.thumb.unlock();
	this.locked = false;
};

/**
 * handles mouseup event on the slider background
 *
 * @private
 */
YAHOO.widget.Slider.prototype.thumbMouseUp = function() {
    if (!this.isLocked() && !this.moveComplete) {
	    this.endMove();
    }

};

/**
 * Returns a reference to this slider's thumb
 */
YAHOO.widget.Slider.prototype.getThumb = function() {
    return this.thumb;
};

/**
 * Try to focus the element when clicked so we can add
 * accessibility features
 *
 * @private
 */
YAHOO.widget.Slider.prototype.focus = function() {

    // Focus the background element if possible
    var el = this.getEl();

    if (el.focus) {
        el.focus();
    }

    this.verifyOffset();

    if (this.isLocked()) {
        return false;
    } else {
        this.onSlideStart();
	    return true;
    }
};

/**
 * Event that fires when the value of the slider has changed
 *
 * @param {int} offsetFromStart the number of pixels the thumb has moved
 * from its start position. Normal horizontal and vertical sliders will only
 * have the firstOffset.  Regions will have both, the first is the horizontal
 * offset, the second the vertical.
 */
YAHOO.widget.Slider.prototype.onChange = function (firstOffset, secondOffset) {
	/* override me */
};

/**
 * Event that fires when the at the beginning of the slider thumb move
 */
YAHOO.widget.Slider.prototype.onSlideStart = function () {
	/* override me */
};

/**
 * Event that fires at the end of a slider thumb move
 */
YAHOO.widget.Slider.prototype.onSlideEnd = function () {
	/* override me */
};

/**
 * Returns the slider's thumb offset from the start position
 *
 * @return {int} the current value
 */
YAHOO.widget.Slider.prototype.getValue = function () {
	return this.thumb.getValue();
};

/**
 * Returns the slider's thumb X offset from the start position
 *
 * @return {int} the current horizontal offset
 */
YAHOO.widget.Slider.prototype.getXValue = function () {
	return this.thumb.getXValue();
};

/**
 * Returns the slider's thumb Y offset from the start position
 *
 * @return {int} the current vertical offset
 */
YAHOO.widget.Slider.prototype.getYValue = function () {
	return this.thumb.getYValue();
};

/**
 * Internal handler for the slider thumb's onChange event
 * @private
 */
YAHOO.widget.Slider.prototype.onThumbChange = function () {
	var t = this.thumb;
	if (t._isRegion) {
		t.onChange(t.getXValue(), t.getYValue());
	} else {
		t.onChange(t.getValue());
	}

};

/**
 * Provides a way to set the value of the slider in code.
 *
 * @param {int} newOffset the number of pixels the thumb should be
 * positioned away from the initial start point
 * @param {boolean} skipAnim set to true to disable the animation
 * for this move action (but not others).
 * @param {boolean} force ignore the locked setting and set value anyway
 * @return {boolean} true if the move was performed, false if it failed
 */
YAHOO.widget.Slider.prototype.setValue = function(newOffset, skipAnim, force) {

    if (!this.thumb.available) {
        this.deferredSetValue = arguments;
        return false;
    }

    if (this.isLocked() && !force) {
        return false;
    }

	if ( isNaN(newOffset) ) {
		return false;
	}

	var t = this.thumb;
	var newX, newY;
    this.verifyOffset();
	if (t._isRegion) {
        return false;
	} else if (t._isHoriz) {
        this.onSlideStart();
		newX = t.initPageX + newOffset + this.thumbCenterPoint.x;
		this.moveThumb(newX, t.initPageY, skipAnim);
	} else {
        this.onSlideStart();
		newY = t.initPageY + newOffset + this.thumbCenterPoint.y;
		this.moveThumb(t.initPageX, newY, skipAnim);
	}

	return true;
};

/**
 * Provides a way to set the value of the region slider in code.
 *
 * @param {int} newOffset the number of pixels the thumb should be
 * positioned away from the initial start point
 * @param {int} newOffset2 the number of pixels the thumb should be
 * positioned away from the initial start point (y axis for region)
 * @param {boolean} skipAnim set to true to disable the animation
 * for this move action (but not others).
 * @param {boolean} force ignore the locked setting and set value anyway
 * @return {boolean} true if the move was performed, false if it failed
 */
YAHOO.widget.Slider.prototype.setRegionValue = function(newOffset, newOffset2, skipAnim) {

    if (!this.thumb.available) {
        this.deferredSetRegionValue = arguments;
        return false;
    }

    if (this.isLocked() && !force) {
        return false;
    }

	if ( isNaN(newOffset) ) {
		return false;
	}

	var t = this.thumb;
	if (t._isRegion) {
        this.onSlideStart();
		var newX = t.initPageX + newOffset + this.thumbCenterPoint.x;
		var newY = t.initPageY + newOffset2 + this.thumbCenterPoint.y;
		this.moveThumb(newX, newY, skipAnim);
	    return true;
	}

    return false;

};

/**
 * Checks the background position element position.  If it has moved from the
 * baseline position, the constraints for the thumb are reset
 * @return {boolean} True if the offset is the same as the baseline.
 */
YAHOO.widget.Slider.prototype.verifyOffset = function() {

    var newPos = YAHOO.util.Dom.getXY(this.getEl());

    if (newPos[0] != this.baselinePos[0] || newPos[1] != this.baselinePos[1]) {
        this.thumb.resetConstraints();
        this.baselinePos = newPos;
        return false;
    }

    return true;
};

/**
 * Move the associated slider moved to a timeout to try to get around the
 * mousedown stealing moz does when I move the slider element between the
 * cursor and the background during the mouseup event
 *
 * @param {int} x the X coordinate of the click
 * @param {int} y the Y coordinate of the click
 * @param {boolean} skipAnim don't animate if the move happend onDrag
 * @private
 */
YAHOO.widget.Slider.prototype.moveThumb = function(x, y, skipAnim) {


	var t = this.thumb;
	var self = this;

    if (!t.available) {
        return;
    }


    // this.verifyOffset();

	t.setDelta(this.thumbCenterPoint.x, this.thumbCenterPoint.y);

	var _p = t.getTargetCoord(x, y);
    var p = [_p.x, _p.y];

	if (this.animate && YAHOO.widget.Slider.ANIM_AVAIL && t._graduated && !skipAnim) {
		// this.thumb._animating = true;
		this.lock();

		setTimeout( function() { self.moveOneTick(p); }, this.tickPause );

	} else if (this.animate && YAHOO.widget.Slider.ANIM_AVAIL && !skipAnim) {

		// this.thumb._animating = true;
		this.lock();

		var oAnim = new YAHOO.util.Motion(
                t.id, { points: { to: p } }, 0.4, YAHOO.util.Easing.easeOut );

		oAnim.onComplete.subscribe( function() { self.endMove(); } );
		oAnim.animate();
	} else {
		t.setDragElPos(x, y);
		// this.fireEvents();
		this.endMove();
	}
};

/**
 * Move the slider one tick mark towards its final coordinate.  Used
 * for the animation when tick marks are defined
 *
 * @param {int[]} the destination coordinate
 * @private
 */
YAHOO.widget.Slider.prototype.moveOneTick = function(finalCoord) {

	var t = this.thumb;
	var curCoord = YAHOO.util.Dom.getXY(t.getEl());
	var tmp;

    // var thresh = Math.min(t.tickSize + (Math.floor(t.tickSize/2)), 10);
    // var thresh = 10;
    // var thresh = t.tickSize + (Math.floor(t.tickSize/2));

	var nextCoord = null;

	if (t._isRegion) {
        nextCoord = this._getNextX(curCoord, finalCoord);
		var tmpX = (nextCoord) ? nextCoord[0] : curCoord[0];
        nextCoord = this._getNextY([tmpX, curCoord[1]], finalCoord);

	} else if (t._isHoriz) {
        nextCoord = this._getNextX(curCoord, finalCoord);
	} else {
        nextCoord = this._getNextY(curCoord, finalCoord);
	}


	if (nextCoord) {

		// move to the next coord
		// YAHOO.util.Dom.setXY(t.getEl(), nextCoord);

        // var el = t.getEl();
        // YAHOO.util.Dom.setStyle(el, "left", (nextCoord[0] + this.thumb.deltaSetXY[0]) + "px");
        // YAHOO.util.Dom.setStyle(el, "top",  (nextCoord[1] + this.thumb.deltaSetXY[1]) + "px");

        this.thumb.alignElWithMouse(t.getEl(), nextCoord[0], nextCoord[1]);

		// check if we are in the final position, if not make a recursive call
		if (!(nextCoord[0] == finalCoord[0] && nextCoord[1] == finalCoord[1])) {
			var self = this;
			setTimeout(function() { self.moveOneTick(finalCoord); },
					this.tickPause);
		} else {
            this.endMove();
		}
	} else {
        this.endMove();
	}

	//this.tickPause = Math.round(this.tickPause/2);
};

/**
 * Returns the next X tick value based on the current coord and the target coord.
 * @private/
 */
YAHOO.widget.Slider.prototype._getNextX = function(curCoord, finalCoord) {
    var t = this.thumb;
    var thresh;
    var tmp = [];
    var nextCoord = null;
    if (curCoord[0] > finalCoord[0]) {
        thresh = t.tickSize - this.thumbCenterPoint.x;
        tmp = t.getTargetCoord( curCoord[0] - thresh, curCoord[1] );
        nextCoord = [tmp.x, tmp.y];
    } else if (curCoord[0] < finalCoord[0]) {
        thresh = t.tickSize + this.thumbCenterPoint.x;
        tmp = t.getTargetCoord( curCoord[0] + thresh, curCoord[1] );
        nextCoord = [tmp.x, tmp.y];
    } else {
        // equal, do nothing
    }

    return nextCoord;
};

/**
 * Returns the next Y tick value based on the current coord and the target coord.
 * @private/
 */
YAHOO.widget.Slider.prototype._getNextY = function(curCoord, finalCoord) {
    var t = this.thumb;
    // var thresh = t.tickSize;
    // var thresh = t.tickSize + this.thumbCenterPoint.y;
    var thresh;
    var tmp = [];
    var nextCoord = null;

    if (curCoord[1] > finalCoord[1]) {
        thresh = t.tickSize - this.thumbCenterPoint.y;
        tmp = t.getTargetCoord( curCoord[0], curCoord[1] - thresh );
        nextCoord = [tmp.x, tmp.y];
    } else if (curCoord[1] < finalCoord[1]) {
        thresh = t.tickSize + this.thumbCenterPoint.y;
        tmp = t.getTargetCoord( curCoord[0], curCoord[1] + thresh );
        nextCoord = [tmp.x, tmp.y];
    } else {
        // equal, do nothing
    }

    return nextCoord;
};

/**
 * Resets the constraints before moving the thumb.
 * @private
 */
YAHOO.widget.Slider.prototype.b4MouseDown = function(e) {
    this.thumb.autoOffset();
    this.thumb.resetConstraints();
};

/**
 * Handles the mousedown event for the slider background
 *
 * @private
 */
YAHOO.widget.Slider.prototype.onMouseDown = function(e) {
    // this.resetConstraints(true);
    // this.thumb.resetConstraints(true);

	if (! this.isLocked() && this.backgroundEnabled) {
		var x = YAHOO.util.Event.getPageX(e);
		var y = YAHOO.util.Event.getPageY(e);

		this.focus();
		this.moveThumb(x, y);
	}

};

/**
 * Handles the onDrag event for the slider background
 *
 * @private
 */
YAHOO.widget.Slider.prototype.onDrag = function(e) {
	if (! this.isLocked()) {
		var x = YAHOO.util.Event.getPageX(e);
		var y = YAHOO.util.Event.getPageY(e);
		this.moveThumb(x, y, true);
	}
};

/**
 * Fired when the slider movement ends
 *
 * @private
 */
YAHOO.widget.Slider.prototype.endMove = function () {
	// this._animating = false;
	this.unlock();
	this.moveComplete = true;
	this.fireEvents();

};

/**
 * Fires the change event if the value has been changed.  Ignored if we are in
 * the middle of an animation as the event will fire when the animation is
 * complete
 *
 * @private
 */
YAHOO.widget.Slider.prototype.fireEvents = function () {

	var t = this.thumb;

	t.cachePosition();

	if (! this.isLocked()) {
		if (t._isRegion) {
			var newX = t.getXValue();
			var newY = t.getYValue();

			if (newX != this.previousX || newY != this.previousY) {
				this.onChange( newX, newY );
			}

			this.previousX = newX;
			this.previousY = newY;

		} else {
			var newVal = t.getValue();
			if (newVal != this.previousVal) {
				this.onChange( newVal );
			}
			this.previousVal = newVal;
		}

		if (this.moveComplete) {
			this.onSlideEnd();
			this.moveComplete = false;
		}

	}
};

/**
 * toString
 * @return {string} string representation of the instance
 */
YAHOO.widget.Slider.prototype.toString = function () {
    return ("Slider (" + this.type +") " + this.id);
};

/**
 * A drag and drop implementation to be used as the thumb of a slider.
 *
 * @extends YAHOO.util.DD
 * @constructor
 * @param {String} id the id of the slider html element
 * @param {String} sGroup the group of related DragDrop items
 * @param {int} iLeft the number of pixels the element can move left
 * @param {int} iRight the number of pixels the element can move right
 * @param {int} iUp the number of pixels the element can move up
 * @param {int} iDown the number of pixels the element can move down
 * @param {int} iTickSize optional parameter for specifying that the element
 * should move a certain number pixels at a time.
 */
YAHOO.widget.SliderThumb = function(id, sGroup, iLeft, iRight, iUp, iDown, iTickSize) {

	if (id) {
		this.init(id, sGroup);

        /**
         * The id of the thumbs parent HTML element (the slider background element).
         * @type string
         */
        this.parentElId = sGroup;
	}


	/**
	 * Overrides the isTarget property in YAHOO.util.DragDrop
	 * @private
	 */
	this.isTarget = false;

    /**
     * The tick size for this slider
     * @type int
     */
	this.tickSize = iTickSize;

    /**
     * Informs the drag and drop util that the offsets should remain when
     * resetting the constraints.  This preserves the slider value when
     * the constraints are reset
     * @type boolean
     */
    this.maintainOffset = true;

	this.initSlider(iLeft, iRight, iUp, iDown, iTickSize);

    this.scroll = false;

};

// YAHOO.widget.SliderThumb.prototype = new YAHOO.util.DD();
YAHOO.extend(YAHOO.widget.SliderThumb, YAHOO.util.DD);

/**
 * Returns the difference between the location of the thumb and its parent.
 * @param {Array} Optionally accepts the position of the parent
 * @type int[]
 */
YAHOO.widget.SliderThumb.prototype.getOffsetFromParent = function(parentPos) {
    var myPos = YAHOO.util.Dom.getXY(this.getEl());
    var ppos  = parentPos || YAHOO.util.Dom.getXY(this.parentElId);

    return [ (myPos[0] - ppos[0]), (myPos[1] - ppos[1]) ];
};

/**
 * The (X and Y) difference between the thumb location and its parent
 * (the slider background) when the control is instantiated.
 * @type int[]
 */
YAHOO.widget.SliderThumb.prototype.startOffset = null;

/**
 * Flag used to figure out if this is a horizontal or vertical slider
 *
 * @type boolean
 * @private
 */
YAHOO.widget.SliderThumb.prototype._isHoriz = false;

/**
 * Cache the last value so we can check for change
 *
 * @type int
 * @private
 */
YAHOO.widget.SliderThumb.prototype._prevVal = 0;

/**
 * initial element X
 *
 * @type int
 * @private
 */
// YAHOO.widget.SliderThumb.prototype._initX = 0;

/**
 * initial element Y
 *
 * @type int
 * @private
 */
// YAHOO.widget.SliderThumb.prototype._initY = 0;

/**
 * The slider is _graduated if there is a tick interval defined
 *
 * @type boolean
 * @private
 */
YAHOO.widget.SliderThumb.prototype._graduated = false;

/**
 * Set up the slider, must be called in the constructor of all subclasses
 *
 * @param {int} iLeft the number of pixels the element can move left
 * @param {int} iRight the number of pixels the element can move right
 * @param {int} iUp the number of pixels the element can move up
 * @param {int} iDown the number of pixels the element can move down
 * @param {int} iTickSize the width of the tick interval.
 */
YAHOO.widget.SliderThumb.prototype.initSlider = function (iLeft, iRight, iUp, iDown,
		iTickSize) {

	this.setXConstraint(iLeft, iRight, iTickSize);
	this.setYConstraint(iUp, iDown, iTickSize);

	if (iTickSize && iTickSize > 1) {
		this._graduated = true;
	}

	this._isHoriz = (iLeft > 0 || iRight > 0);
	this._isVert   = (iUp > 0 ||  iDown > 0);
	this._isRegion = (this._isHoriz && this._isVert);

};

/**
 * Clear's the slider's ticks
 */
YAHOO.widget.SliderThumb.prototype.clearTicks = function () {
    YAHOO.widget.SliderThumb.superclass.clearTicks.call(this);
    this._graduated = false;
};

/**
 * Gets the current offset from the element's start position in
 * pixels.
 *
 * @return {int} the number of pixels (positive or negative) the
 * slider has moved from the start position.
 */
YAHOO.widget.SliderThumb.prototype.getValue = function () {
    if (!this.available) { return 0; }
    var val = (this._isHoriz) ? this.getXValue() : this.getYValue();
    return val;
};

/**
 * Gets the current X offset from the element's start position in
 * pixels.
 *
 * @return {int} the number of pixels (positive or negative) the
 * slider has moved horizontally from the start position.
 */
YAHOO.widget.SliderThumb.prototype.getXValue = function () {
    if (!this.available) { return 0; }
    var newOffset = this.getOffsetFromParent();
	return (newOffset[0] - this.startOffset[0]);
};

/**
 * Gets the current Y offset from the element's start position in
 * pixels.
 *
 * @return {int} the number of pixels (positive or negative) the
 * slider has moved vertically from the start position.
 */
YAHOO.widget.SliderThumb.prototype.getYValue = function () {
    if (!this.available) { return 0; }
    var newOffset = this.getOffsetFromParent();
	return (newOffset[1] - this.startOffset[1]);
};

/**
 * toString
 * @return {string} string representation of the instance
 */
YAHOO.widget.SliderThumb.prototype.toString = function () {
    return "SliderThumb " + this.id;
};

/**
 * The onchange event for the handle/thumb is delegated to the YAHOO.widget.Slider
 * instance it belongs to.
 *
 * @private
 */
YAHOO.widget.SliderThumb.prototype.onChange = function (x, y) { };

if ("undefined" == typeof YAHOO.util.Anim) {
	YAHOO.widget.Slider.ANIM_AVAIL = false;
}


/*** End of file, slider.js ***/

/*** Start of file, animation.js ***/
/*
Copyright (c) 2006, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
Version: 0.11.3
*/
/**
 *
 * Base class for animated DOM objects.
 * @class Base animation class that provides the interface for building animated effects.
 * <p>Usage: var myAnim = new YAHOO.util.Anim(el, { width: { from: 10, to: 100 } }, 1, YAHOO.util.Easing.easeOut);</p>
 * @requires YAHOO.util.AnimMgr
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String or HTMLElement} el Reference to the element that will be animated
 * @param {Object} attributes The attribute(s) to be animated.
 * Each attribute is an object with at minimum a "to" or "by" member defined.
 * Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
 * All attribute names use camelCase.
 * @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
 * @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
 */

YAHOO.util.Anim = function(el, attributes, duration, method) {
   if (el) {
      this.init(el, attributes, duration, method);
   }
};

YAHOO.util.Anim.prototype = {
   /**
    * toString method
    * @return {String} string represenation of anim obj
    */
   toString: function() {
      var el = this.getEl();
      var id = el.id || el.tagName;
      return ("Anim " + id);
   },

   patterns: { // cached for performance
      noNegatives:      /width|height|opacity|padding/i, // keep at zero or above
      offsetAttribute:  /^((width|height)|(top|left))$/, // use offsetValue as default
      defaultUnit:      /width|height|top$|bottom$|left$|right$/i, // use 'px' by default
      offsetUnit:       /\d+(em|%|en|ex|pt|in|cm|mm|pc)$/i // IE may return these, so convert these to offset
   },

   /**
    * Returns the value computed by the animation's "method".
    * @param {String} attr The name of the attribute.
    * @param {Number} start The value this attribute should start from for this animation.
    * @param {Number} end  The value this attribute should end at for this animation.
    * @return {Number} The Value to be applied to the attribute.
    */
   doMethod: function(attr, start, end) {
      return this.method(this.currentFrame, start, end - start, this.totalFrames);
   },

   /**
    * Applies a value to an attribute
    * @param {String} attr The name of the attribute.
    * @param {Number} val The value to be applied to the attribute.
    * @param {String} unit The unit ('px', '%', etc.) of the value.
    */
   setAttribute: function(attr, val, unit) {
      if ( this.patterns.noNegatives.test(attr) ) {
         val = (val > 0) ? val : 0;
      }

      YAHOO.util.Dom.setStyle(this.getEl(), attr, val + unit);
   },

   /**
    * Returns current value of the attribute.
    * @param {String} attr The name of the attribute.
    * @return {Number} val The current value of the attribute.
    */
   getAttribute: function(attr) {
      var el = this.getEl();
      var val = YAHOO.util.Dom.getStyle(el, attr);

      if (val !== 'auto' && !this.patterns.offsetUnit.test(val)) {
         return parseFloat(val);
      }

      var a = this.patterns.offsetAttribute.exec(attr) || [];
      var pos = !!( a[3] ); // top or left
      var box = !!( a[2] ); // width or height

      // use offsets for width/height and abs pos top/left
      if ( box || (YAHOO.util.Dom.getStyle(el, 'position') == 'absolute' && pos) ) {
         val = el['offset' + a[0].charAt(0).toUpperCase() + a[0].substr(1)];
      } else { // default to zero for other 'auto'
         val = 0;
      }

      return val;
   },

   /**
    * Returns the unit to use when none is supplied.
    * Applies the "defaultUnit" test to decide whether to use pixels or not
    * @param {attr} attr The name of the attribute.
    * @return {String} The default unit to be used.
    */
   getDefaultUnit: function(attr) {
       if ( this.patterns.defaultUnit.test(attr) ) {
         return 'px';
       }

       return '';
   },

   /**
    * Sets the actual values to be used during the animation.
    * Should only be needed for subclass use.
    * @param {Object} attr The attribute object
    * @private
    */
   setRuntimeAttribute: function(attr) {
      var start;
      var end;
      var attributes = this.attributes;

      this.runtimeAttributes[attr] = {};

      var isset = function(prop) {
         return (typeof prop !== 'undefined');
      };

      if ( !isset(attributes[attr]['to']) && !isset(attributes[attr]['by']) ) {
         return false; // note return; nothing to animate to
      }

      start = ( isset(attributes[attr]['from']) ) ? attributes[attr]['from'] : this.getAttribute(attr);

      // To beats by, per SMIL 2.1 spec
      if ( isset(attributes[attr]['to']) ) {
         end = attributes[attr]['to'];
      } else if ( isset(attributes[attr]['by']) ) {
         if (start.constructor == Array) {
            end = [];
            for (var i = 0, len = start.length; i < len; ++i) {
               end[i] = start[i] + attributes[attr]['by'][i];
            }
         } else {
            end = start + attributes[attr]['by'];
         }
      }

      this.runtimeAttributes[attr].start = start;
      this.runtimeAttributes[attr].end = end;

      // set units if needed
      this.runtimeAttributes[attr].unit = ( isset(attributes[attr].unit) ) ? attributes[attr]['unit'] : this.getDefaultUnit(attr);
   },

   /**
    * @param {String or HTMLElement} el Reference to the element that will be animated
    * @param {Object} attributes The attribute(s) to be animated.
    * Each attribute is an object with at minimum a "to" or "by" member defined.
    * Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
    * All attribute names use camelCase.
    * @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
    * @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
    */
   init: function(el, attributes, duration, method) {
      /**
       * Whether or not the animation is running.
       * @private
       * @type Boolean
       */
      var isAnimated = false;

      /**
       * A Date object that is created when the animation begins.
       * @private
       * @type Date
       */
      var startTime = null;

      /**
       * The number of frames this animation was able to execute.
       * @private
       * @type Int
       */
      var actualFrames = 0;

      /**
       * The element to be animated.
       * @private
       * @type HTMLElement
       */
      el = YAHOO.util.Dom.get(el);

      /**
       * The collection of attributes to be animated.
       * Each attribute must have at least a "to" or "by" defined in order to animate.
       * If "to" is supplied, the animation will end with the attribute at that value.
       * If "by" is supplied, the animation will end at that value plus its starting value.
       * If both are supplied, "to" is used, and "by" is ignored.
       * @member YAHOO#util#Anim
       * Optional additional member include "from" (the value the attribute should start animating from, defaults to current value), and "unit" (the units to apply to the values).
       * @type Object
       */
      this.attributes = attributes || {};

      /**
       * The length of the animation.  Defaults to "1" (second).
       * @type Number
       */
      this.duration = duration || 1;

      /**
       * The method that will provide values to the attribute(s) during the animation.
       * Defaults to "YAHOO.util.Easing.easeNone".
       * @type Function
       */
      this.method = method || YAHOO.util.Easing.easeNone;

      /**
       * Whether or not the duration should be treated as seconds.
       * Defaults to true.
       * @type Boolean
       */
      this.useSeconds = true; // default to seconds

      /**
       * The location of the current animation on the timeline.
       * In time-based animations, this is used by AnimMgr to ensure the animation finishes on time.
       * @type Int
       */
      this.currentFrame = 0;

      /**
       * The total number of frames to be executed.
       * In time-based animations, this is used by AnimMgr to ensure the animation finishes on time.
       * @type Int
       */
      this.totalFrames = YAHOO.util.AnimMgr.fps;


      /**
       * Returns a reference to the animated element.
       * @return {HTMLElement}
       */
      this.getEl = function() { return el; };

      /**
       * Checks whether the element is currently animated.
       * @return {Boolean} current value of isAnimated.
       */
      this.isAnimated = function() {
         return isAnimated;
      };

      /**
       * Returns the animation start time.
       * @return {Date} current value of startTime.
       */
      this.getStartTime = function() {
         return startTime;
      };

      this.runtimeAttributes = {};



      /**
       * Starts the animation by registering it with the animation manager.
       */
      this.animate = function() {
         if ( this.isAnimated() ) { return false; }

         this.currentFrame = 0;

         this.totalFrames = ( this.useSeconds ) ? Math.ceil(YAHOO.util.AnimMgr.fps * this.duration) : this.duration;

         YAHOO.util.AnimMgr.registerElement(this);
      };

      /**
       * Stops the animation.  Normally called by AnimMgr when animation completes.
       */
      this.stop = function() {
         YAHOO.util.AnimMgr.stop(this);
      };

      var onStart = function() {
         this.onStart.fire();
         for (var attr in this.attributes) {
            this.setRuntimeAttribute(attr);
         }

         isAnimated = true;
         actualFrames = 0;
         startTime = new Date();
      };

      /**
       * Feeds the starting and ending values for each animated attribute to doMethod once per frame, then applies the resulting value to the attribute(s).
       * @private
       */

      var onTween = function() {
         var data = {
            duration: new Date() - this.getStartTime(),
            currentFrame: this.currentFrame
         };

         data.toString = function() {
            return (
               'duration: ' + data.duration +
               ', currentFrame: ' + data.currentFrame
            );
         };

         this.onTween.fire(data);

         var runtimeAttributes = this.runtimeAttributes;

         for (var attr in runtimeAttributes) {
            this.setAttribute(attr, this.doMethod(attr, runtimeAttributes[attr].start, runtimeAttributes[attr].end), runtimeAttributes[attr].unit);
         }

         actualFrames += 1;
      };

      var onComplete = function() {
         var actual_duration = (new Date() - startTime) / 1000 ;

         var data = {
            duration: actual_duration,
            frames: actualFrames,
            fps: actualFrames / actual_duration
         };

         data.toString = function() {
            return (
               'duration: ' + data.duration +
               ', frames: ' + data.frames +
               ', fps: ' + data.fps
            );
         };

         isAnimated = false;
         actualFrames = 0;
         this.onComplete.fire(data);
      };

      /**
       * Custom event that fires after onStart, useful in subclassing
       * @private
       */
      this._onStart = new YAHOO.util.CustomEvent('_start', this, true);

      /**
       * Custom event that fires when animation begins
       * Listen via subscribe method (e.g. myAnim.onStart.subscribe(someFunction)
       */
      this.onStart = new YAHOO.util.CustomEvent('start', this);

      /**
       * Custom event that fires between each frame
       * Listen via subscribe method (e.g. myAnim.onTween.subscribe(someFunction)
       */
      this.onTween = new YAHOO.util.CustomEvent('tween', this);

      /**
       * Custom event that fires after onTween
       * @private
       */
      this._onTween = new YAHOO.util.CustomEvent('_tween', this, true);

      /**
       * Custom event that fires when animation ends
       * Listen via subscribe method (e.g. myAnim.onComplete.subscribe(someFunction)
       */
      this.onComplete = new YAHOO.util.CustomEvent('complete', this);
      /**
       * Custom event that fires after onComplete
       * @private
       */
      this._onComplete = new YAHOO.util.CustomEvent('_complete', this, true);

      this._onStart.subscribe(onStart);
      this._onTween.subscribe(onTween);
      this._onComplete.subscribe(onComplete);
   }
};

/**
 * @class Handles animation queueing and threading.
 * Used by Anim and subclasses.
 */
YAHOO.util.AnimMgr = new function() {
   /**
    * Reference to the animation Interval
    * @private
    * @type Int
    */
   var thread = null;

   /**
    * The current queue of registered animation objects.
    * @private
    * @type Array
    */
   var queue = [];

   /**
    * The number of active animations.
    * @private
    * @type Int
    */
   var tweenCount = 0;

   /**
    * Base frame rate (frames per second).
    * Arbitrarily high for better x-browser calibration (slower browsers drop more frames).
    * @type Int
    *
    */
   this.fps = 200;

   /**
    * Interval delay in milliseconds, defaults to fastest possible.
    * @type Int
    *
    */
   this.delay = 1;

   /**
    * Adds an animation instance to the animation queue.
    * All animation instances must be registered in order to animate.
    * @param {object} tween The Anim instance to be be registered
    */
   this.registerElement = function(tween) {
      queue[queue.length] = tween;
      tweenCount += 1;
      tween._onStart.fire();
      this.start();
   };

   this.unRegister = function(tween, index) {
      tween._onComplete.fire();
      index = index || getIndex(tween);
      if (index != -1) { queue.splice(index, 1); }

      tweenCount -= 1;
      if (tweenCount <= 0) { this.stop(); }
   };

   /**
    * Starts the animation thread.
	 * Only one thread can run at a time.
    */
   this.start = function() {
      if (thread === null) { thread = setInterval(this.run, this.delay); }
   };

   /**
    * Stops the animation thread or a specific animation instance.
    * @param {object} tween A specific Anim instance to stop (optional)
    * If no instance given, Manager stops thread and all animations.
    */
   this.stop = function(tween) {
      if (!tween) {
         clearInterval(thread);
         for (var i = 0, len = queue.length; i < len; ++i) {
            if (queue[i].isAnimated()) {
               this.unRegister(tween, i);
            }
         }
         queue = [];
         thread = null;
         tweenCount = 0;
      }
      else {
         this.unRegister(tween);
      }
   };

   /**
    * Called per Interval to handle each animation frame.
    */
   this.run = function() {
      for (var i = 0, len = queue.length; i < len; ++i) {
         var tween = queue[i];
         if ( !tween || !tween.isAnimated() ) { continue; }

         if (tween.currentFrame < tween.totalFrames || tween.totalFrames === null)
         {
            tween.currentFrame += 1;

            if (tween.useSeconds) {
               correctFrame(tween);
            }
            tween._onTween.fire();
         }
         else { YAHOO.util.AnimMgr.stop(tween, i); }
      }
   };

   var getIndex = function(anim) {
      for (var i = 0, len = queue.length; i < len; ++i) {
         if (queue[i] == anim) {
            return i; // note return;
         }
      }
      return -1;
   };

   /**
    * On the fly frame correction to keep animation on time.
    * @private
    * @param {Object} tween The Anim instance being corrected.
    */
   var correctFrame = function(tween) {
      var frames = tween.totalFrames;
      var frame = tween.currentFrame;
      var expected = (tween.currentFrame * tween.duration * 1000 / tween.totalFrames);
      var elapsed = (new Date() - tween.getStartTime());
      var tweak = 0;

      if (elapsed < tween.duration * 1000) { // check if falling behind
         tweak = Math.round((elapsed / expected - 1) * tween.currentFrame);
      } else { // went over duration, so jump to end
         tweak = frames - (frame + 1);
      }
      if (tweak > 0 && isFinite(tweak)) { // adjust if needed
         if (tween.currentFrame + tweak >= frames) {// dont go past last frame
            tweak = frames - (frame + 1);
         }

         tween.currentFrame += tweak;
      }
   };
};
/**
 *
 * @class Used to calculate Bezier splines for any number of control points.
 *
 */
YAHOO.util.Bezier = new function()
{
   /**
    * Get the current position of the animated element based on t.
    * Each point is an array of "x" and "y" values (0 = x, 1 = y)
    * At least 2 points are required (start and end).
    * First point is start. Last point is end.
    * Additional control points are optional.
    * @param {Array} points An array containing Bezier points
    * @param {Number} t A number between 0 and 1 which is the basis for determining current position
    * @return {Array} An array containing int x and y member data
    */
   this.getPosition = function(points, t)
   {
      var n = points.length;
      var tmp = [];

      for (var i = 0; i < n; ++i){
         tmp[i] = [points[i][0], points[i][1]]; // save input
      }

      for (var j = 1; j < n; ++j) {
         for (i = 0; i < n - j; ++i) {
            tmp[i][0] = (1 - t) * tmp[i][0] + t * tmp[parseInt(i + 1, 10)][0];
            tmp[i][1] = (1 - t) * tmp[i][1] + t * tmp[parseInt(i + 1, 10)][1];
         }
      }

      return [ tmp[0][0], tmp[0][1] ];

   };
};
/**
 * @class ColorAnim subclass for color fading
 * <p>Usage: <code>var myAnim = new Y.ColorAnim(el, { backgroundColor: { from: '#FF0000', to: '#FFFFFF' } }, 1, Y.Easing.easeOut);</code></p>
 * <p>Color values can be specified with either 112233, #112233, [255,255,255], or rgb(255,255,255)
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.AnimMgr
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Bezier
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @constructor
 * @param {HTMLElement | String} el Reference to the element that will be animated
 * @param {Object} attributes The attribute(s) to be animated.
 * Each attribute is an object with at minimum a "to" or "by" member defined.
 * Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
 * All attribute names use camelCase.
 * @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
 * @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
 */
(function() {
   YAHOO.util.ColorAnim = function(el, attributes, duration,  method) {
      YAHOO.util.ColorAnim.superclass.constructor.call(this, el, attributes, duration, method);
   };

   YAHOO.extend(YAHOO.util.ColorAnim, YAHOO.util.Anim);

   // shorthand
   var Y = YAHOO.util;
   var superclass = Y.ColorAnim.superclass;
   var proto = Y.ColorAnim.prototype;

   /**
    * toString method
    * @return {String} string represenation of anim obj
    */
   proto.toString = function() {
      var el = this.getEl();
      var id = el.id || el.tagName;
      return ("ColorAnim " + id);
   };

   /**
    * Only certain attributes should be treated as colors.
    * @type Object
    */
   proto.patterns.color = /color$/i;
   proto.patterns.rgb    = /^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i;
   proto.patterns.hex    = /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i;
   proto.patterns.hex3   = /^#?([0-9A-F]{1})([0-9A-F]{1})([0-9A-F]{1})$/i;

   /**
    * Attempts to parse the given string and return a 3-tuple.
    * @param {String} s The string to parse.
    * @return {Array} The 3-tuple of rgb values.
    */
   proto.parseColor = function(s) {
      if (s.length == 3) { return s; }

      var c = this.patterns.hex.exec(s);
      if (c && c.length == 4) {
         return [ parseInt(c[1], 16), parseInt(c[2], 16), parseInt(c[3], 16) ];
      }

      c = this.patterns.rgb.exec(s);
      if (c && c.length == 4) {
         return [ parseInt(c[1], 10), parseInt(c[2], 10), parseInt(c[3], 10) ];
      }

      c = this.patterns.hex3.exec(s);
      if (c && c.length == 4) {
         return [ parseInt(c[1] + c[1], 16), parseInt(c[2] + c[2], 16), parseInt(c[3] + c[3], 16) ];
      }

      return null;
   };

   /**
    * Returns current value of the attribute.
    * @param {String} attr The name of the attribute.
    * @return {Number} val The current value of the attribute.
    */
   proto.getAttribute = function(attr) {
      var el = this.getEl();
      if (  this.patterns.color.test(attr) ) {
         var val = YAHOO.util.Dom.getStyle(el, attr);

         if (val == 'transparent') { // bgcolor default
            var parent = el.parentNode; // try and get from an ancestor
            val = Y.Dom.getStyle(parent, attr);

            while (parent && val == 'transparent') {
               parent = parent.parentNode;
               val = Y.Dom.getStyle(parent, attr);
               if (parent.tagName.toUpperCase() == 'HTML') {
                  val = 'ffffff';
               }
            }
         }
      } else {
         val = superclass.getAttribute.call(this, attr);
      }

      return val;
   };

   /**
    * Returns the value computed by the animation's "method".
    * @param {String} attr The name of the attribute.
    * @param {Number} start The value this attribute should start from for this animation.
    * @param {Number} end  The value this attribute should end at for this animation.
    * @return {Number} The Value to be applied to the attribute.
    */
   proto.doMethod = function(attr, start, end) {
      var val;

      if ( this.patterns.color.test(attr) ) {
         val = [];
         for (var i = 0, len = start.length; i < len; ++i) {
            val[i] = superclass.doMethod.call(this, attr, start[i], end[i]);
         }

         val = 'rgb('+Math.floor(val[0])+','+Math.floor(val[1])+','+Math.floor(val[2])+')';
      }
      else {
         val = superclass.doMethod.call(this, attr, start, end);
      }

      return val;
   };

   /**
    * Sets the actual values to be used during the animation.
    * Should only be needed for subclass use.
    * @param {Object} attr The attribute object
    * @private
    */
   proto.setRuntimeAttribute = function(attr) {
      superclass.setRuntimeAttribute.call(this, attr);

      if ( this.patterns.color.test(attr) ) {
         var attributes = this.attributes;
         var start = this.parseColor(this.runtimeAttributes[attr].start);
         var end = this.parseColor(this.runtimeAttributes[attr].end);
         // fix colors if going "by"
         if ( typeof attributes[attr]['to'] === 'undefined' && typeof attributes[attr]['by'] !== 'undefined' ) {
            end = this.parseColor(attributes[attr].by);

            for (var i = 0, len = start.length; i < len; ++i) {
               end[i] = start[i] + end[i];
            }
         }

         this.runtimeAttributes[attr].start = start;
         this.runtimeAttributes[attr].end = end;
      }
   };
})();/*
TERMS OF USE - EASING EQUATIONS
Open source under the BSD License.
Copyright © 2001 Robert Penner All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

YAHOO.util.Easing = {

   /**
    * Uniform speed between points.
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeNone: function (t, b, c, d) {
   	return c*t/d + b;
   },

   /**
    * Begins slowly and accelerates towards end. (quadratic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeIn: function (t, b, c, d) {
   	return c*(t/=d)*t + b;
   },

   /**
    * Begins quickly and decelerates towards end.  (quadratic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeOut: function (t, b, c, d) {
   	return -c *(t/=d)*(t-2) + b;
   },

   /**
    * Begins slowly and decelerates towards end. (quadratic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeBoth: function (t, b, c, d) {
   	if ((t/=d/2) < 1) return c/2*t*t + b;
   	return -c/2 * ((--t)*(t-2) - 1) + b;
   },

   /**
    * Begins slowly and accelerates towards end. (quartic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeInStrong: function (t, b, c, d) {
   	return c*(t/=d)*t*t*t + b;
   },

   /**
    * Begins quickly and decelerates towards end.  (quartic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeOutStrong: function (t, b, c, d) {
   	return -c * ((t=t/d-1)*t*t*t - 1) + b;
   },

   /**
    * Begins slowly and decelerates towards end. (quartic)
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   easeBothStrong: function (t, b, c, d) {
   	if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
   	return -c/2 * ((t-=2)*t*t*t - 2) + b;
   },

   /**
    * snap in elastic effect
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number} p Period (optional)
    * @return {Number} The computed value for the current animation frame.
    */

   elasticIn: function (t, b, c, d, a, p) {
   	if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
   	if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
   	else var s = p/(2*Math.PI) * Math.asin (c/a);
   	return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
   },

   /**
    * snap out elastic effect
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number} p Period (optional)
    * @return {Number} The computed value for the current animation frame.
    */
   elasticOut: function (t, b, c, d, a, p) {
   	if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
   	if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
   	else var s = p/(2*Math.PI) * Math.asin (c/a);
   	return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
   },

   /**
    * snap both elastic effect
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number} p Period (optional)
    * @return {Number} The computed value for the current animation frame.
    */
   elasticBoth: function (t, b, c, d, a, p) {
   	if (t==0) return b;  if ((t/=d/2)==2) return b+c;  if (!p) p=d*(.3*1.5);
   	if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
   	else var s = p/(2*Math.PI) * Math.asin (c/a);
   	if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
   	return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
   },

   /**
    * back easing in - backtracking slightly, then reversing direction and moving to target
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number) s Overshoot (optional)
    * @return {Number} The computed value for the current animation frame.
    */
   backIn: function (t, b, c, d, s) {
   	if (typeof s == 'undefined') s = 1.70158;
   	return c*(t/=d)*t*((s+1)*t - s) + b;
   },

   /**
    * back easing out - moving towards target, overshooting it slightly,
    * then reversing and coming back to target
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number) s Overshoot (optional)
    * @return {Number} The computed value for the current animation frame.
    */
   backOut: function (t, b, c, d, s) {
   	if (typeof s == 'undefined') s = 1.70158;
   	return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
   },

   /**
    * back easing in/out - backtracking slightly, then reversing direction and moving to target,
    * then overshooting target, reversing, and finally coming back to target
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @param {Number) s Overshoot (optional)
    * @return {Number} The computed value for the current animation frame.
    */
   backBoth: function (t, b, c, d, s) {
   	if (typeof s == 'undefined') s = 1.70158;
   	if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
   	return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
   },

   /**
    * bounce in
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   bounceIn: function (t, b, c, d) {
   	return c - YAHOO.util.Easing.bounceOut(d-t, 0, c, d) + b;
   },

   /**
    * bounce out
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   bounceOut: function (t, b, c, d) {
   	if ((t/=d) < (1/2.75)) {
   		return c*(7.5625*t*t) + b;
   	} else if (t < (2/2.75)) {
   		return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
   	} else if (t < (2.5/2.75)) {
   		return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
   	} else {
   		return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
   	}
   },

   /**
    * bounce both
    * @param {Number} t Time value used to compute current value.
    * @param {Number} b Starting value.
    * @param {Number} c Delta between start and end values.
    * @param {Number} d Total length of animation.
    * @return {Number} The computed value for the current animation frame.
    */
   bounceBoth: function (t, b, c, d) {
   	if (t < d/2) return YAHOO.util.Easing.bounceIn(t*2, 0, c, d) * .5 + b;
   	return YAHOO.util.Easing.bounceOut(t*2-d, 0, c, d) * .5 + c*.5 + b;
   }
};

/**
 * @class Anim subclass for moving elements along a path defined by the "points" member of "attributes".  All "points" are arrays with x, y coordinates.
 * <p>Usage: <code>var myAnim = new YAHOO.util.Motion(el, { points: { to: [800, 800] } }, 1, YAHOO.util.Easing.easeOut);</code></p>
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.AnimMgr
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Bezier
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String or HTMLElement} el Reference to the element that will be animated
 * @param {Object} attributes The attribute(s) to be animated.
 * Each attribute is an object with at minimum a "to" or "by" member defined.
 * Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
 * All attribute names use camelCase.
 * @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
 * @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
 */
(function() {
   YAHOO.util.Motion = function(el, attributes, duration,  method) {
      if (el) { // dont break existing subclasses not using YAHOO.extend
         YAHOO.util.Motion.superclass.constructor.call(this, el, attributes, duration, method);
      }
   };

   YAHOO.extend(YAHOO.util.Motion, YAHOO.util.ColorAnim);

   // shorthand
   var Y = YAHOO.util;
   var superclass = Y.Motion.superclass;
   var proto = Y.Motion.prototype;

   /**
    * toString method
    * @return {String} string represenation of anim obj
    */
   proto.toString = function() {
      var el = this.getEl();
      var id = el.id || el.tagName;
      return ("Motion " + id);
   };

   proto.patterns.points = /^points$/i;

   /**
    * Applies a value to an attribute
    * @param {String} attr The name of the attribute.
    * @param {Number} val The value to be applied to the attribute.
    * @param {String} unit The unit ('px', '%', etc.) of the value.
    */
   proto.setAttribute = function(attr, val, unit) {
      if (  this.patterns.points.test(attr) ) {
         unit = unit || 'px';
         superclass.setAttribute.call(this, 'left', val[0], unit);
         superclass.setAttribute.call(this, 'top', val[1], unit);
      } else {
         superclass.setAttribute.call(this, attr, val, unit);
      }
   };

   /**
    * Sets the default value to be used when "from" is not supplied.
    * @param {String} attr The attribute being set.
    * @param {Number} val The default value to be applied to the attribute.
    */
   proto.getAttribute = function(attr) {
      if (  this.patterns.points.test(attr) ) {
         var val = [
            superclass.getAttribute.call(this, 'left'),
            superclass.getAttribute.call(this, 'top')
         ];
      } else {
         val = superclass.getAttribute.call(this, attr);
      }

      return val;
   };

   /**
    * Returns the value computed by the animation's "method".
    * @param {String} attr The name of the attribute.
    * @param {Number} start The value this attribute should start from for this animation.
    * @param {Number} end  The value this attribute should end at for this animation.
    * @return {Number} The Value to be applied to the attribute.
    */
   proto.doMethod = function(attr, start, end) {
      var val = null;

      if ( this.patterns.points.test(attr) ) {
         var t = this.method(this.currentFrame, 0, 100, this.totalFrames) / 100;
         val = Y.Bezier.getPosition(this.runtimeAttributes[attr], t);
      } else {
         val = superclass.doMethod.call(this, attr, start, end);
      }
      return val;
   };

   /**
    * Sets the actual values to be used during the animation.
    * Should only be needed for subclass use.
    * @param {Object} attr The attribute object
    * @private
    */
   proto.setRuntimeAttribute = function(attr) {
      if ( this.patterns.points.test(attr) ) {
         var el = this.getEl();
         var attributes = this.attributes;
         var start;
         var control = attributes['points']['control'] || [];
         var end;
         var i, len;

         if (control.length > 0 && !(control[0] instanceof Array) ) { // could be single point or array of points
            control = [control];
         } else { // break reference to attributes.points.control
            var tmp = [];
            for (i = 0, len = control.length; i< len; ++i) {
               tmp[i] = control[i];
            }
            control = tmp;
         }

         if (Y.Dom.getStyle(el, 'position') == 'static') { // default to relative
            Y.Dom.setStyle(el, 'position', 'relative');
         }

         if ( isset(attributes['points']['from']) ) {
            Y.Dom.setXY(el, attributes['points']['from']); // set position to from point
         }
         else { Y.Dom.setXY( el, Y.Dom.getXY(el) ); } // set it to current position

         start = this.getAttribute('points'); // get actual top & left

         // TO beats BY, per SMIL 2.1 spec
         if ( isset(attributes['points']['to']) ) {
            end = translateValues.call(this, attributes['points']['to'], start);

            var pageXY = Y.Dom.getXY(this.getEl());
            for (i = 0, len = control.length; i < len; ++i) {
               control[i] = translateValues.call(this, control[i], start);
            }


         } else if ( isset(attributes['points']['by']) ) {
            end = [ start[0] + attributes['points']['by'][0], start[1] + attributes['points']['by'][1] ];

            for (i = 0, len = control.length; i < len; ++i) {
               control[i] = [ start[0] + control[i][0], start[1] + control[i][1] ];
            }
         }

         this.runtimeAttributes[attr] = [start];

         if (control.length > 0) {
            this.runtimeAttributes[attr] = this.runtimeAttributes[attr].concat(control);
         }

         this.runtimeAttributes[attr][this.runtimeAttributes[attr].length] = end;
      }
      else {
         superclass.setRuntimeAttribute.call(this, attr);
      }
   };

   var translateValues = function(val, start) {
      var pageXY = Y.Dom.getXY(this.getEl());
      val = [ val[0] - pageXY[0] + start[0], val[1] - pageXY[1] + start[1] ];

      return val;
   };

   var isset = function(prop) {
      return (typeof prop !== 'undefined');
   };
})();
/**
 * @class Anim subclass for scrolling elements to a position defined by the "scroll" member of "attributes".  All "scroll" members are arrays with x, y scroll positions.
 * <p>Usage: <code>var myAnim = new YAHOO.util.Scroll(el, { scroll: { to: [0, 800] } }, 1, YAHOO.util.Easing.easeOut);</code></p>
 * @requires YAHOO.util.Anim
 * @requires YAHOO.util.AnimMgr
 * @requires YAHOO.util.Easing
 * @requires YAHOO.util.Bezier
 * @requires YAHOO.util.Dom
 * @requires YAHOO.util.Event
 * @requires YAHOO.util.CustomEvent
 * @constructor
 * @param {String or HTMLElement} el Reference to the element that will be animated
 * @param {Object} attributes The attribute(s) to be animated.
 * Each attribute is an object with at minimum a "to" or "by" member defined.
 * Additional optional members are "from" (defaults to current value), "units" (defaults to "px").
 * All attribute names use camelCase.
 * @param {Number} duration (optional, defaults to 1 second) Length of animation (frames or seconds), defaults to time-based
 * @param {Function} method (optional, defaults to YAHOO.util.Easing.easeNone) Computes the values that are applied to the attributes per frame (generally a YAHOO.util.Easing method)
 */
(function() {
   YAHOO.util.Scroll = function(el, attributes, duration,  method) {
      if (el) { // dont break existing subclasses not using YAHOO.extend
         YAHOO.util.Scroll.superclass.constructor.call(this, el, attributes, duration, method);
      }
   };

   YAHOO.extend(YAHOO.util.Scroll, YAHOO.util.ColorAnim);

   // shorthand
   var Y = YAHOO.util;
   var superclass = Y.Scroll.superclass;
   var proto = Y.Scroll.prototype;

   /**
    * toString method
    * @return {String} string represenation of anim obj
    */
   proto.toString = function() {
      var el = this.getEl();
      var id = el.id || el.tagName;
      return ("Scroll " + id);
   };

   /**
    * Returns the value computed by the animation's "method".
    * @param {String} attr The name of the attribute.
    * @param {Number} start The value this attribute should start from for this animation.
    * @param {Number} end  The value this attribute should end at for this animation.
    * @return {Number} The Value to be applied to the attribute.
    */
   proto.doMethod = function(attr, start, end) {
      var val = null;

      if (attr == 'scroll') {
         val = [
            this.method(this.currentFrame, start[0], end[0] - start[0], this.totalFrames),
            this.method(this.currentFrame, start[1], end[1] - start[1], this.totalFrames)
         ];

      } else {
         val = superclass.doMethod.call(this, attr, start, end);
      }
      return val;
   };

   /**
    * Returns current value of the attribute.
    * @param {String} attr The name of the attribute.
    * @return {Number} val The current value of the attribute.
    */
   proto.getAttribute = function(attr) {
      var val = null;
      var el = this.getEl();

      if (attr == 'scroll') {
         val = [ el.scrollLeft, el.scrollTop ];
      } else {
         val = superclass.getAttribute.call(this, attr);
      }

      return val;
   };

   /**
    * Applies a value to an attribute
    * @param {String} attr The name of the attribute.
    * @param {Number} val The value to be applied to the attribute.
    * @param {String} unit The unit ('px', '%', etc.) of the value.
    */
   proto.setAttribute = function(attr, val, unit) {
      var el = this.getEl();

      if (attr == 'scroll') {
         el.scrollLeft = val[0];
         el.scrollTop = val[1];
      } else {
         superclass.setAttribute.call(this, attr, val, unit);
      }
   };
})();

/*** End of file, animation.js ***/

/*** Start of file, prototype.js ***/
/*  Prototype JavaScript framework, version 1.6.0.2
 *  (c) 2005-2008 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.0.2',

  Browser: {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div').__proto__ &&
      document.createElement('div').__proto__ !==
        document.createElement('form').__proto__
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value, value = Object.extend((function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method), {
          valueOf:  function() { return method },
          toString: function() { return method.toString() }
        });
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  toJSON: function(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (Object.isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = Object.toJSON(object[property]);
      if (!Object.isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  },

  toQueryString: function(object) {
    return $H(object).toQueryString();
  },

  toHTML: function(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({ }, object);
  },

  isElement: function(object) {
    return object && object.nodeType == 1;
  },

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

  isHash: function(object) {
    return object instanceof Hash;
  },

  isFunction: function(object) {
    return typeof object == "function";
  },

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

  isUndefined: function(object) {
    return typeof object == "undefined";
  }
});

Object.extend(Function.prototype, {
  argumentNames: function() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
    return names.length == 1 && !names[0] ? [] : names;
  },

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Function.prototype.defer = Function.prototype.delay.curry(0.01);

Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

  unescapeHTML: function() {
    var div = new Element('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  times: function(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

  capitalize: function() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  },

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  },

  toJSON: function() {
    return this.inspect(true);
  },

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  },

  evalJSON: function(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  startsWith: function(pattern) {
    return this.indexOf(pattern) === 0;
  },

  endsWith: function(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  },

  empty: function() {
    return this == '';
  },

  blank: function() {
    return /^\s*$/.test(this);
  },

  interpolate: function(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }
});

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (Object.isFunction(replacement)) return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
};

String.prototype.parseQuery = String.prototype.toQueryParams;

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

with (String.prototype.escapeHTML) div.appendChild(text);

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return '';

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    iterator = iterator.bind(context);
    try {
      this._each(function(value) {
        iterator(value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var index = -number, slices = [], array = this.toArray();
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  },

  all: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator(value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator(value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator(value, index));
    });
    return results;
  },

  detect: function(iterator, context) {
    iterator = iterator.bind(context);
    var result;
    this.each(function(value, index) {
      if (iterator(value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator, context) {
    iterator = iterator.bind(context);
    var results = [];
    this.each(function(value, index) {
      if (iterator(value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(filter, iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(filter);

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator(value, index));
    });
    return results;
  },

  include: function(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator, context) {
    iterator = iterator.bind(context);
    this.each(function(value, index) {
      memo = iterator(memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator(value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator(value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator, context) {
    iterator = iterator ? iterator.bind(context) : Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator(value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator, context) {
    iterator = iterator.bind(context);
    var results = [];
    this.each(function(value, index) {
      if (!iterator(value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator, context) {
    iterator = iterator.bind(context);
    return this.map(function(value, index) {
      return {value: value, criteria: iterator(value, index)};
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

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

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
};

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
        iterable.toArray) return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(Object.isArray(value) ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  },

  intersect: function(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  },

  clone: function() {
    return [].concat(this);
  },

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

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  },

  toJSON: function() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }
});

// use native browser JS 1.6 implementation if available
if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
  i || (i = 0);
  var length = this.length;
  if (i < 0) i = length + i;
  for (; i < length; i++)
    if (this[i] === item) return i;
  return -1;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
  var n = this.slice(0, i).reverse().indexOf(item);
  return (n < 0) ? n : i - n - 1;
};

Array.prototype.toArray = Array.prototype.clone;

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator) {
    $R(0, this, true).each(iterator);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.map(function(pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return values.map(toQueryPair.curry(key)).join('&');
        }
        return toQueryPair(key, values);
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
};

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});

Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,
  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  // DOM level 2 ECMAScript Language Binding
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}

(function() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(this.Element, element || { });
}).call(window);

Element.cache = { };

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    $(element).style.display = 'none';
    return element;
  },

  show: function(element) {
    $(element).style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $(element).select("*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = element.ancestors();
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return element.firstDescendant();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      element.select(expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = element.nextSiblings();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!element.hasClassName(className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return element[element.hasClassName(className) ?
      'removeClassName' : 'addClassName'](className);
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);
    var originalAncestor = ancestor;

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (element.sourceIndex && !Prototype.Browser.Opera) {
      var e = element.sourceIndex, a = ancestor.sourceIndex,
       nextAncestor = ancestor.nextSibling;
      if (!nextAncestor) {
        do { ancestor = ancestor.parentNode; }
        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
      }
      if (nextAncestor && nextAncestor.sourceIndex)
       return (e > a && e < nextAncestor.sourceIndex);
    }

    while (element = element.parentNode)
      if (element == originalAncestor) return true;
    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value) {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = $(element).getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (window.opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'absolute') return;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    var offsets = element.positionedOffset();
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'relative') return;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    // find page position of source
    source = $(source);
    var p = source.viewportOffset();

    // find coordinate system to use
    element = $(element);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = element.getOffsetParent();
      delta = parent.viewportOffset();
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Element.Methods.identify.counter = 1;

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,
  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          // returns '0px' for hidden elements; we want it to return null
          if (!Element.visible(element)) return null;

          // returns the border-box dimensions rather than the content-box
          // dimensions, so we subtract padding and borders from the value
          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  // IE doesn't report offsets correctly for static elements, so we change them
  // to "relative" to get the values, then change them back.
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        // Trigger hasLayout on the offset parent so that IE6 reports
        // accurate offsetTop and offsetLeft values for position: fixed.
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _flag: function(element, attribute) {
          return $(element).hasAttribute(attribute) ? attribute : null;
        },
        style: function(element) {
          return element.style.cssText.toLowerCase();
        },
        title: function(element) {
          return element.title;
        }
      }
    }
  };

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr,
      src:         v._getAttr,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);
}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  // Safari returns margins on body which is incorrect if the child is absolutely
  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
  // KHTML/WebKit only.
  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if (Prototype.Browser.IE || Prototype.Browser.Opera) {
  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
  Element.Methods.update = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);

    content = Object.toHTML(content);
    var tagName = element.tagName.toUpperCase();

    if (tagName in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

if ('outerHTML' in document.createElement('div')) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return node && node.specified;
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

if (!Prototype.BrowserFeatures.ElementExtensions &&
    document.createElement('div').__proto__) {
  window.HTMLElement = { };
  window.HTMLElement.prototype = document.createElement('div').__proto__;
  Prototype.BrowserFeatures.ElementExtensions = true;
}

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || element._extendedByPrototype ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
      tagName = element.tagName, property, value;

    // extend methods for specific tags
    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    for (property in methods) {
      value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      // extend methods for all tags (Safari doesn't need this)
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName).__proto__;
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};

document.viewport = {
  getDimensions: function() {
    var dimensions = { };
    var B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
        (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
    });
    return dimensions;
  },

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

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

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* Portions of the Selector class are derived from Jack Slocumâs DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();
    this.compileMatcher();
  },

  shouldUseXPath: function() {
    if (!Prototype.BrowserFeatures.XPath) return false;

    var e = this.expression;

    // Safari 3 chokes on :*-of-type and :empty
    if (Prototype.Browser.WebKit &&
     (e.include("-of-type") || e.include(":empty")))
      return false;

    // XPath can't do namespaced attributes, nor can it read
    // the "checked" property from DOM nodes
    if ((/(\[[\w-]*?:|:checked)/).test(this.expression))
      return false;

    return true;
  },

  compileMatcher: function() {
    if (this.shouldUseXPath())
      return this.compileXPathMatcher();

    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
    	      new Template(c[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
    return this.matcher(root);
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          // use the Selector.assertions methods unless the selector
          // is too complex.
          if (as[i]) {
            this.tokens.push([i, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            // reluctantly do a document-wide search
            // and look for a match in the array
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

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

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
      'checked':     "[@checked]",
      'disabled':    "[@disabled]",
      'enabled':     "[not(@disabled)]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: {
    // combinators must be listed first
    // (and descendant needs to be last combinator)
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    // selectors follow
    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[([\w]+)\]/,
    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
  },

  // for Selector.match and Element#match
  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    // UTILITY FUNCTIONS
    // joins two collections
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    // marks an array of nodes for counting
    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      return nodes;
    },

    // mark each child node with its position (for nth calls)
    // "ofType" flag indicates whether we're indexing for nth-of-type
    // rather than nth-child
    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    // filters out duplicates and extends all nodes
    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (!(n = nodes[i])._countedByPrototype) {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    // COMBINATOR FUNCTIONS
    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
	      if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    // TOKEN FUNCTIONS
    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          // fastlane for ordinary descendant combinators
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;
      if (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    // handles the an+b logic
    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        // IE treats comments as element nodes
        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled) results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv.startsWith(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    // IE returns comment nodes on getElementsByTagName("*").
    // Filter them out.
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    },

    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node.removeAttribute('_countedByPrototype');
      return nodes;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            // a key is already present; construct an array of values
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !['button', 'reset', 'submit'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.blur();
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;
var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, index) {
    if (Object.isUndefined(index))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, value, single = !Object.isArray(index);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        value = this.optionValue(opt);
        if (single) {
          if (value == index) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = index.include(value);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) var Event = { };

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,
  KEY_INSERT:   45,

  cache: { },

  relatedTarget: function(event) {
    var element;
    switch(event.type) {
      case 'mouseover': element = event.fromElement; break;
      case 'mouseout':  element = event.toElement;   break;
      default: return null;
    }
    return Element.extend(element);
  }
});

Event.Methods = (function() {
  var isButton;

  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if (Prototype.Browser.WebKit) {
    isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      var node = Event.extend(event).target;
      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
    },

    findElement: function(event, expression) {
      var element = Event.element(event);
      if (!expression) return element;
      var elements = [element].concat(element.ancestors());
      return Selector.findElement(elements, expression, 0);
    },

    pointer: function(event) {
      return {
        x: event.pageX || (event.clientX +
          (document.documentElement.scrollLeft || document.body.scrollLeft)),
        y: event.pageY || (event.clientY +
          (document.documentElement.scrollTop || document.body.scrollTop))
      };
    },

    pointerX: function(event) { return Event.pointer(event).x },
    pointerY: function(event) { return Event.pointer(event).y },

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

Event.extend = (function() {
  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return "[object Event]" }
    });

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

  } else {
    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
    Object.extend(Event.prototype, methods);
    return Prototype.K;
  }
})();

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }

  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }

  if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);

      Event.observe(window, "load", fireContentLoadedEvent);

    } else {
      document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }

  } else {
    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();
/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

// This should be moved to script.aculo.us; notice the deprecated methods
// further below, that map to the newer Element methods.
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  // Deprecation layer -- use newer Element methods now (1.5.2).

  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/

Element.addMethods();
/*** End of file, prototype.js ***/

/*** Start of file, js_core.js ***/
/*** Definisjon av hovednamespace ***/
if (typeof DX == 'undefined') {
	DX = {};
}

// DX.randomInt(5) => 0, 1, 2, 3 or 4 (that is, 5 different outcomes)
DX.randomInt = function(outcomes) { return Math.floor(Math.random()*outcomes); }

// Function "overloading", courtesy of the Prototype library...
// This method overwrites the previous one, but retains a reference to the "overloaded" function and delegates to it if the "new" signature doesn't match.
// DX.randomInt(5, 10) => 5, 6, 7, 8, 9, 10 (that is, 6 different outcomes)
DX.randomInt = DX.randomInt.wrap(function(wrapped, min, max) {
	if (max == null) { return wrapped(min); }
	var outcomes = max - min + 1;
	return wrapped(outcomes)+min;
});

DX.shuffle = function(iterable) { return $A(iterable).sortBy(Math.random); }
Array.prototype.shuffle = DX.shuffle.methodize();

function scrollToSelectedElement() {
	var elements = document.getElementsByClassName('design_element_selected');
	if (elements.length > 0) {
		var element = elements[0];
		var pos = Position.cumulativeOffset(element);
		var scrollHeight = pos[1] + (element.getHeight()/2) - (get_inner_height()/2);

		window.scrollTo(0, scrollHeight);
	}
}
Event.observe(window, 'load', scrollToSelectedElement);


/*** Utvidelser av kjerneobjektene i JS START ***/

var ourVeryOwnElementMethods = {
	center: function(element) {
		var innerTop    = get_inner_top();
		var innerLeft   = get_inner_left();
		var innerWidth  = get_inner_width();
		var innerHeight = get_inner_height();

		if (innerTop == null)  { innerTop  = 0; }
		if (innerLeft == null) { innerLeft = 0; }

		element.setStyle({
			top:  (innerTop  + (innerHeight / 2) - element.getHeight() / 2) + 'px',
			left: (innerLeft + (innerWidth  / 2) - element.getWidth()  / 2) + 'px'
		});
	},

	fitToScreen: function(element) {
		var innerTop    = get_inner_top();
		var innerLeft   = get_inner_left();
		var innerWidth  = get_inner_width();
		var innerHeight = get_inner_height();

		if (innerTop == null)  { innerTop  = 0; }
		if (innerLeft == null) { innerLeft = 0; }

		element.setStyle({
			top:    innerTop    + 'px',
			left:   innerLeft   + 'px',
			width:  innerWidth  + 'px',
			height: innerHeight + 'px'
		})
	}
}

Element.addMethods(ourVeryOwnElementMethods);

/*** Utvidelser av kjerneobjektene i JS END ***/

/*** Diverse hjelpefunksjoner START ***/

/* Funksjon knigget fra Dreamweaver. Redirecter nettleseren basert på 'targ'. Brukes gjerne til å lage menyer som automatisk går til gitt adresse ved valg. */
function jumpMenu(targ,selObj,restore){
	eval(targ+".location='"+selObj.options[selObj.selectedIndex].value+"'");
	if (restore) selObj.selectedIndex=0;
}

function swapImage() {
	var i,j=0,image,arguments=swapImage.arguments;
	document.sr=[];
	for(i=0;i<(arguments.length-2);i+=3) {
		if ((image=findObj(arguments[i]))!=null){
			document.sr[j++]=image;
			if(!image.oSrc) {
				image.oSrc=image.src;
			}
			image.src=arguments[i+2];
		}
	}
}

function swapImgRestore() {
	var i,x,a=document.sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}

/* Funksjon knigget fra Dreamweaver. Den finner og returnerer et element basert på elementets nav (eller id-tag). */
function findObj(n, d) {
	if(!n){return null;}
	var p,i,x;  if(!d) d=document; if((p=n.indexOf('?'))>0&&parent.frames.length) {
	d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
	if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
	for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=findObj(n,d.layers[i].document);
	if(!x && d.getElementById) x=d.getElementById(n); return x;
}

/* Funksjon som returnerer hvor bredt tegneområdet i nettleseren er. */
function get_inner_width() {
	var myWidth;
	if( typeof( window.innerWidth ) == 'number' ) {
		//Non-IE
		myWidth = window.innerWidth;
	} else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
		//IE 6+ in 'standards compliant mode'
		myWidth = document.documentElement.clientWidth;
	} else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
		//IE 4 compatible
		myWidth = document.body.clientWidth;
	}
	return myWidth;
}

/* Funksjon som returnerer hvor høyt tegneområdet i nettleseren er. */
function get_inner_height() {
	var myHeight;
	if( typeof( window.innerWidth ) == 'number' ) {
		//Non-IE
		myHeight = window.innerHeight;
	} else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
		//IE 6+ in 'standards compliant mode'
		myHeight = document.documentElement.clientHeight;
	} else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
		//IE 4 compatible
		myHeight = document.body.clientHeight;
	}
	return myHeight;
}

function get_inner_top() {
	var myTop;
	if( typeof( window.pageYOffset ) == 'number' ) {
		//Netscape compliant
		myTop = window.pageYOffset;
	} else if( document.body && ( document.body.scrollLeft || document.body.scrollTop ) ) {
		//DOM compliant
		myTop = document.body.scrollTop;
	} else if( document.documentElement &&( document.documentElement.scrollLeft || document.documentElement.scrollTop ) ) {
		//IE6 standards compliant mode
		myTop = document.documentElement.scrollTop;
	}
	else {
		myTop = document.documentElement.scrollTop;
	}
	return myTop;
}

function get_inner_left() {
	var myLeft;
	if( typeof( window.pageYOffset ) == 'number' ) {
		//Netscape compliant
		myLeft = window.pageXOffset;
	} else if( document.body && ( document.body.scrollLeft || document.body.scrollTop ) ) {
		//DOM compliant
		myLeft = document.body.scrollLeft;
	} else if( document.documentElement && ( document.documentElement.scrollLeft || document.documentElement.scrollTop ) ) {
		//IE6 standards compliant mode
		myLeft = document.documentElement.scrollLeft;
	}
	else {
		myLeft = document.documentElement.scrollLeft;
	}
	return myLeft;
}


/* Funksjon som renser et tall for eventuelle suffix (f.eks 250px -> 250). */
function clean_number(number) {
	var length = number.length;
	var validChars = '0123456789';
	var result = '';
	for (var i = 0; i<length; i++) {
		var c = number.charAt(i);
		if(validChars.indexOf(c) != -1) {
			result += c;
		}
		else {
			break;
		}
	}
	return (result == '') ? '0' : result;
}


/* Funksjon som redigerer et felt i en form ved å sett inn 'value' som ny verdi. */
function edit_field(fieldName, value) {
	fieldObj = findObj(fieldName);
	if (fieldObj != null) { fieldObj.value = value; }
}

var oldVisibilityToggledElements = [];

function toggleVisibility(element, group) {
	if (typeof element == 'string') { element = findObj(element); }
	if (!group) { group = element.id; }

	var oldElement = oldVisibilityToggledElements[group];

	setVisible(element, element.style.visibility != 'visible');
	if (oldElement && oldElement.id != element.id) {
		setVisible(oldElement, false);
	}

	oldVisibilityToggledElements[group] = element;
}

function setVisible(elementObj, setVisible) {
	if (!elementObj) { return; }

	if (setVisible) { elementObj.style.visibility = 'visible'; }
	else { elementObj.style.visibility = 'hidden'; }
}

function setDisplayed(elementObj, setDisplay) {
	if (!elementObj) { return; }

	if (setDisplay) {
		elementObj.style.display = elementObj.prevDisplay ? elementObj.prevDisplay : 'block';
	} else {
		elementObj.prevDisplay   = elementObj.style.display;
		elementObj.style.display = 'none';
	}
}

function arraySearch(needle, haystack) {
	for (key=0; key < haystack.length; key++) {
		if (haystack[key] == needle) { return key; }
	}
	return -1;
}

function appendToFilename(filename, appendage) {
	var extension = filename.substring(filename.lastIndexOf('.'));
	var basename  = filename.substring(0, filename.lastIndexOf('.'));

	return basename + appendage + extension;
}

function appendSelected(filename) {
	return appendToFilename(filename, '_selected');
}

function appendHover(filename) {
	return appendToFilename(filename, '_hover');
}

function appendSelectedHover(filename) {
	return appendToFilename(filename, '_selected_hover');
}

function mailto(user, domain) {
	window.location = 'mailto:' + user + '@' + domain;
}

function mailto_subject(user, domain, subject) {
	window.location = 'mailto:' + user + '@' + domain + '?subject=' + subject;
}

/*** Diverse hjelpefunksjoner END ***/

/*** MultiStateButton START ***/

MultiStateButton = function(name, values, images, imageId, currentValue, callbackFn) {
	this.name         = name;
	this.values       = values;
	this.images       = images;
	this.currentIndex = arraySearch(currentValue, values);
	this.imageObj     = findObj(imageId);
	this.callbackFn   = callbackFn != null ? callbackFn : edit_field;

	YAHOO.util.Event.addListener(this.imageObj, 'click',     this.onClick, this);
	YAHOO.util.Event.addListener(this.imageObj, 'mouseover', this.onOver,  this);
	YAHOO.util.Event.addListener(this.imageObj, 'mouseout',  this.onOut,   this);

	eventManager.registerSender(name, this);
}

MultiStateButton.prototype.onClick = function(v, self) {
	self.advanceState(true);
}

MultiStateButton.prototype.advanceState = function(executeCallback) {
	this.currentIndex += 1;
	if (this.currentIndex >= this.values.length) { this.currentIndex = 0; }

	this.imageObj.src = this.images[this.currentIndex];
	if (executeCallback) { this.callbackFn(this.name, this.values[this.currentIndex]); }
}

MultiStateButton.prototype.handlerActivated   = function() {}
MultiStateButton.prototype.handlerDeactivated = function() {}

MultiStateButton.prototype.onOver = function(v, self) {
	this.src = appendHover(self.images[self.currentIndex]);
}

MultiStateButton.prototype.onOut = function(v, self) {
	this.src = self.images[self.currentIndex];
}


/*** MultiStateButton END***/

/*** RadioButtons START ***/
RadioButtons = function(name, values, images, imageIds, currentValue, callbackFn) {
	this.name         = name;
	this.values       = values;
	this.images       = images;
	this.imageIds     = imageIds;
	this.currentIndex = arraySearch(currentValue, values);
	this.callbackFn   = callbackFn != null ? callbackFn : edit_field;

	this.selectCurrent();

	for (index = 0; index < imageIds.length; index++) {
		YAHOO.util.Event.addListener(imageIds[index], 'click',     this.onClick, this);
		YAHOO.util.Event.addListener(imageIds[index], 'mouseover', this.onOver,  this);
		YAHOO.util.Event.addListener(imageIds[index], 'mouseout',  this.onOut,   this);
		$(imageIds[index]).setStyle({cursor: "pointer"});
	}

	eventManager.registerSender(name, this);
}

RadioButtons.prototype.selectCurrent = function() {
	var currentImage = findObj(this.imageIds[this.currentIndex]);
	currentImage.src = appendSelected(this.images[this.currentIndex]);
}

RadioButtons.prototype.onClick = function(v, self) {
	var newIndex     = arraySearch(this.id, self.imageIds);
	var currentImage = findObj(self.imageIds[self.currentIndex]);
	currentImage.src = self.images[self.currentIndex];
	this.src         = appendSelected(self.images[newIndex]); // Kanskje unødvendig -> onOut? (holder ikke bare med onOut - man må få bekreftelse på at valget er gjort) [men den var unødvendig da hovring viste samme bilde uavhengig av om det var selected eller ikke (dvs. før appendSelectedHover-metoden kom på plass i forrige js-commit)]

	self.currentIndex = newIndex;
	self.callbackFn(self.name, self.values[self.currentIndex]);
}

RadioButtons.prototype.onOver = function(v, self) {
	var index = arraySearch(this.id, self.imageIds);
	if (index == self.currentIndex) {
		this.src = appendSelectedHover(self.images[index]);
	} else {
		this.src = appendHover(self.images[index]);
	}
}

RadioButtons.prototype.onOut = function(v, self) {
	var index = arraySearch(this.id, self.imageIds);
	if (index == self.currentIndex) {
		this.src = appendSelected(self.images[index]);
	} else {
		this.src = self.images[index];
	}
}

/*** RadioButtons END ***/

/*** EditableLabel START **/

function toggleEditLabel(name, value) {
	var editableLabel = eventManager.getHandler(name);
	if (editableLabel != null) { editableLabel.enabled(value == 'enabled', false); }
}

EditableLabel = function(name, fieldId, targetId, callbackFn, wrapperClass, staticContent) {
	this.name            = name;
	this.fieldId         = fieldId;
	this.targetId        = targetId;
	this.targetObj       = findObj(targetId);
	this.callbackFn      = callbackFn != null ? callbackFn : edit_field;
	this.wrapperClass    = wrapperClass;
	this.staticContent   = staticContent;
	this.editableContent = null;
	this.fieldObj        = null;
	this.oldClassName    = this.targetObj.className;

	YAHOO.util.Event.addListener(this.targetId, 'mouseover', this.onOver,  this);
	YAHOO.util.Event.addListener(this.targetId, 'mouseout',  this.onOut,   this);
	YAHOO.util.Event.addListener(this.targetId, 'click',     this.onClick, this);

	eventManager.registerHandler(name, this);
	this.initField(); // Callback/init hidden fields.
}

EditableLabel.prototype.deactivated = function() {
	this.stopEditing();
}

EditableLabel.prototype.enabled = function(enabled, notifySender) {
	if (enabled) {
		eventManager.activateHandler(this.name, notifySender);
		this.startEditing();
	} else {
		this.stopEditing();
		eventManager.deactivateActiveHandler(notifySender, false);
	}
}

EditableLabel.prototype.initField = function() {
	this.editableContent = this.targetObj.innerHTML;
	this.callbackFn(this.fieldId, this.editableContent);
	this.swapStaticContent();
}

EditableLabel.prototype.swapStaticContent = function() {
	if (this.staticContent != null) {
		// Poenget med denne siste ?:-en er å ikke vise link-ikonet (dvs. staticContent) dersom det ikke er noen link (dvs. ingen innerHTML).
		this.targetObj.innerHTML = this.targetObj.innerHTML ? this.staticContent : '';
	}
}

EditableLabel.prototype.onOver = function(v, self) {
	self.oldClassName           = self.targetObj.className ? self.targetObj.className : self.oldClassName;
	self.targetObj.className    = self.oldClassName != null ? self.oldClassName + ' design_element_hover' : 'design_element_hover';
	self.targetObj.style.cursor = 'text';
}

EditableLabel.prototype.onOut = function(v, self) {
	self.targetObj.className    = self.oldClassName != null ? self.oldClassName : '';
	self.targetObj.style.cursor = 'pointer';
}

EditableLabel.prototype.onBlur = function(v, self) {
	self.enabled(false, true);
}

EditableLabel.prototype.onClick = function(v, self) {
	self.enabled(true, true);
}

EditableLabel.prototype.startEditing = function() {
	// Feltet får ikke name-atributten, ettersom dette kan føre til submitting av input-elementet.
	this.fieldObj             = document.createElement('input');
	this.fieldObj.type        = 'text';
	this.fieldObj.value       = this.editableContent;
	this.fieldObj.style.width = this.targetObj.offsetWidth + 'px';

	this.targetObj.innerHTML = '';
	this.targetObj.appendChild(this.fieldObj);
	this.fieldObj.focus();

	YAHOO.util.Event.addListener(this.fieldObj, 'blur', this.onBlur, this);
	YAHOO.util.Event.removeListener(this.targetId, 'click', this.onClick);
}

EditableLabel.prototype.stopEditing = function() {
	YAHOO.util.Event.removeListener(this.fieldObj, 'blur', this.onBlur);

	this.editableContent = this.fieldObj.value;
	this.targetObj.removeChild(this.fieldObj);
	this.fieldObj = null;
	this.targetObj.innerHTML = this.editableContent;
	this.swapStaticContent();

	this.callbackFn(this.fieldId, this.editableContent);

	if (this.wrapperClass != null && this.targetObj.innerHTML != null && this.targetObj.innerHTML.length >= 1) { this.targetObj.className = this.wrapperClass; this.targetObj.style.fontSize = ''; }
	else { this.targetObj.className = ''; this.targetObj.style.fontSize = '0px'; }

	YAHOO.util.Event.addListener(this.targetId, 'click', this.onClick, this);
}

/*** EditableLabel END **/

/*** AlignableElement START ***/

function triggerAlignmentChange(name, value) {
	var alignableElement = eventManager.getHandler(name);
	if (alignableElement != null) { alignableElement.alignmentChanged(value); }
}

AlignableElement = function(name, fieldId, targetId, callbackFn) {
	this.name       = name;
	this.fieldId    = fieldId;
	this.targetObj  = findObj(targetId);
	this.callbackFn = callbackFn != null ? callbackFn : edit_field;

	eventManager.registerHandler(name, this);
}

AlignableElement.prototype.alignmentChanged = function(alignment) {
	if (this.targetObj != null) { this.targetObj.align = alignment; }
	this.callbackFn(this.fieldId, alignment);
	eventManager.refreshActiveHandler();
}

/*** AlignableElement END ***/

/*** ResizableElement START ***/

function toggleResize(name, value) {
	var resizableElement = eventManager.getHandler(name);
	if (resizableElement != null) { resizableElement.setResizing(value == 'resize_enabled'); }
}

var RESIZE_LOCK_NONE   = 0;
var RESIZE_LOCK_ASPECT = 1;
var RESIZE_LOCK_HEIGHT = 2;
var RESIZE_LOCK_WIDTH  = 3;

ResizableElement = function(name, fieldPrefix, targetId, mode, statusId, callbackFn, alignedId, snapWidth, snapHeight) {
	this.name        = name;
	this.fieldPrefix = fieldPrefix;
	this.targetObj   = findObj(targetId);
	this.wrapperObj  = findObj(targetId + '_wrapper');
	this.mode        = mode;
	this.statusObj   = findObj(statusId);
	this.callbackFn  = callbackFn != null ? callbackFn : edit_field;
	this.handle      = null;
	this.alignedObj  = (alignedId != null) ? findObj(alignedId) : this.targetObj;
	this.snapWidth   = snapWidth;
	this.snapHeight  = snapHeight;

	eventManager.registerHandler(name, this);

	// Callback/init hidden fields.
	var self = this;
	setTimeout(function() { self.init(); }, 200);
	// Tror denne vil være bedre ved bruk av delay eller defer (fra Prototype): init.delay(0.2);
}

ResizableElement.prototype.init = function() {
	var currentWidth  = this.targetObj.offsetWidth;
	var currentHeight = this.targetObj.offsetHeight;
	this.executeCallback(currentWidth, currentHeight);
	this.changeStatus(currentWidth, currentHeight, currentWidth == this.snapWidth);
}

ResizableElement.prototype.deactivated = function() {
	this.deleteElements();
}

ResizableElement.prototype.refresh = function() {
	this.setResizing(false);
	this.setResizing(true);
}

ResizableElement.prototype.setResizing = function(enabled) {
	if (this.targetObj == null) { return; }
	if (enabled) {
		eventManager.activateHandler(this.name);
		this.createElements();
		this.handle = new ResizeHandle('resize_outline', 'resize_handle', this);
	}
	else {
		// Fjerne alle eventlistenere og slett div'er
		this.deleteElements();
		eventManager.unsetActiveHandler();
	}
}

ResizableElement.prototype.createElements = function() {
	var outline = document.createElement('div');
	var handle  = document.createElement('div');
	var top     = YAHOO.util.Dom.getY(this.targetObj);
	var left    = YAHOO.util.Dom.getX(this.targetObj);
	var height  = this.targetObj.offsetHeight;
	var width   = this.targetObj.offsetWidth;

	outline.id              = 'resize_outline';
	outline.style.top       = top    + 'px';
	outline.style.left      = left   + 'px';
	outline.style.width     = width  + 'px';
	outline.style.height    = height + 'px';
	outline.style.position  = 'absolute';

	handle.id               = 'resize_handle';
	handle.style.bottom     = '0px';
	handle.style.width      = '10px';
	handle.style.height     = '10px';
	handle.style.position   = 'absolute';
	handle.style.border     = '1px solid #000';
	handle.style.background = '#f00';
	handle.style.fontSize   = '1px';
	if (this.getAlignment() == 'right') {
		handle.style.left   = '0px';
		handle.style.cursor = 'ne-resize'
	}
	else {
		handle.style.right  = '0px';
		handle.style.cursor = 'nw-resize'
	}

	this.targetObj.parentNode.insertBefore(outline, this.targetObj);
	outline.appendChild(handle, null);
}

ResizableElement.prototype.getAlignment = function() {
	var alignment = 'left';
	if (this.alignedObj.align == 'right' || this.alignedObj.style.cssFloat == 'right' || this.alignedObj.styleFloat == 'right')  { alignment = 'right'; }
	if (this.alignedObj.align == 'center') { alignment = 'center'; }
	return alignment;
}

ResizableElement.prototype.deleteElements = function() {
	var outline = findObj('resize_outline');
	var handle  = findObj('resize_handle');

	if (handle  != null) { handle.parentNode.removeChild(handle); }
	if (outline != null) { outline.parentNode.removeChild(outline); }
}

ResizableElement.prototype.changeSize = function(width, height, snapped) {
	this.targetObj.style.width  = width  + 'px';
	this.targetObj.style.height = height + 'px';
	if (this.wrapperObj != null) { this.wrapperObj.style.width = width + 'px'; }

	var outline = findObj('resize_outline');
	if (outline != null) { outline.style.left = YAHOO.util.Dom.getX(this.targetObj) + 'px'; }

	this.executeCallback(width, height);
	this.changeStatus(width, height, snapped);
}

ResizableElement.prototype.changeStatus = function(width, height, snapped) {
	if (this.statusObj != null) {
		this.statusObj.innerHTML = width + 'x' + height;
		this.statusObj.style.fontWeight = snapped ? 'bold' : 'normal';
	}
}

ResizableElement.prototype.doneResizing = function () {
}

ResizableElement.prototype.executeCallback = function(width, height) {
	this.callbackFn(this.fieldPrefix + '_width',  width);
	this.callbackFn(this.fieldPrefix + '_height', height);
}

ResizeHandle = function(outlineId, handleId, elementObj) {
	this.init(outlineId);
	this.handleElId = handleId;
	this.setHandleElId(handleId);
	this.elementObj = elementObj;
}

// Extender ResizeHandle fra DragDrop. Må kalles like etter resizeHandles konstr.
YAHOO.extend(ResizeHandle, YAHOO.util.DragDrop);

ResizeHandle.prototype.startDrag = function(x,y) {
	var panel        = this.getEl();
	this.startWidth  = panel.offsetWidth;
	this.startHeight = panel.offsetHeight;
	this.startPos    = [x, y];
};

ResizeHandle.prototype.onDrag = function(e) {
	var mode   = this.elementObj.mode;
	var newPos = [YAHOO.util.Event.getPageX(e), YAHOO.util.Event.getPageY(e)];

	var offsetX = newPos[0] - this.startPos[0];
	var offsetY = newPos[1] - this.startPos[1];
	if (this.elementObj.getAlignment() == 'center') { offsetX *= 2; }
	if (this.elementObj.getAlignment() == 'right')  { offsetX  = -offsetX; }

	var newWidth  = Math.max(this.startWidth  + offsetX, 10);
	var newHeight = Math.max(this.startHeight + offsetY, 10);

	switch(mode) {
		case RESIZE_LOCK_ASPECT: newHeight = newWidth * (this.startHeight/this.startWidth); break;
		case RESIZE_LOCK_WIDTH : newWidth  = this.startWidth;  break;
		case RESIZE_LOCK_HEIGHT: newHeight = this.startHeight; break;
	}

	// Avrunding.
	var width  = parseInt(newWidth  + 0.5);
	var height = parseInt(newHeight + 0.5);

	// Snapping.
	var snapped = false;
	if (width  > this.elementObj.snapWidth  - 5 && width  < this.elementObj.snapWidth  + 5) {
		snapped = true;
		width   = this.elementObj.snapWidth;
		if (mode == RESIZE_LOCK_ASPECT) { height = this.elementObj.snapHeight; }
	}
	if (height > this.elementObj.snapHeight - 5 && height < this.elementObj.snapHeight + 5) {
		snapped = true;
		height  = this.elementObj.snapHeight;
		if (mode == RESIZE_LOCK_ASPECT) { width = this.elementObj.snapWidth; }
	}

	var panel = this.getEl();
	panel.style.width  = width  + 'px';
	panel.style.height = height + 'px';

	this.elementObj.changeSize(width, height, snapped);
};

ResizeHandle.prototype.endDrag = function(e) {
	this.elementObj.doneResizing();
};

/*** ResizableElement END ***/

/*** EventManager START ***/

// TODO -- Se på om watch-funksjonen kan hjelpe oss ifm. eventmanager-rammeverket, på noen som helst måte: http://davidkellogg.com/blog/2006/12/14/using-watch-to-monitor-javascript/

EventManager = function() {
	this.handlers      = [];
	this.senders       = [];
	this.activeHandler = null;
}

EventManager.prototype.registerSender = function(name, sender) {
	this.senders[name] = sender;
}

EventManager.prototype.registerHandler = function(name, handler) {
	this.handlers[name] = handler;
}

EventManager.prototype.activateHandler = function(name, notifySender) {
	this.deactivateActiveHandler(true, true);
	this.setActiveHandler(name);
	if (notifySender) {
		var sender = this.getSender(name);
		if (sender != null && sender.handlerActivated != null) { sender.handlerActivated(); }
	}
}

EventManager.prototype.refreshActiveHandler = function() {
	if (this.activeHandler != null && this.activeHandler.refresh) {
		this.activeHandler.refresh();
	}
}

EventManager.prototype.deactivateActiveHandler = function(notifySender, notifyHandler) {
	if (this.activeHandler != null) {
		if (notifyHandler && this.activeHandler.deactivated != null) { this.activeHandler.deactivated(); }
		if (notifySender  && this.activeHandler.name != null) {
			var sender = this.getSender(this.activeHandler.name);
			if (sender != null && sender.handlerDeactivated != null) { sender.handlerDeactivated(); }
		}
	}
	this.unsetActiveHandler();
}

EventManager.prototype.setActiveHandler = function(name) {
	this.activeHandler = this.getHandler(name);
}

EventManager.prototype.unsetActiveHandler = function() {
	this.activeHandler = null;
}

EventManager.prototype.getSender = function(name) {
	return this.senders[name];
}

EventManager.prototype.getHandler = function(name) {
	return this.handlers[name];
}

var eventManager = new EventManager();

/*** EventManager END ***/

/*** SlidableElement START ***/

function scaledValueToSliderValue(scaledValue, tickInterval) {
	if (scaledValue >= 200) { return (scaledValue/100 + 20) * tickInterval; }
	if (scaledValue >= 101) { return 21 * tickInterval; }
	if (scaledValue >=  20) { return (scaledValue/10  + 10) * tickInterval; }
	if (scaledValue >=  11) { return 11 * tickInterval; }
	return scaledValue * tickInterval;
}

function sliderValueToScaledValue(sliderValue, tickInterval) {
	var ticks = sliderValue / tickInterval;
	if (ticks >= 22) { return (ticks-20) * 100; }
	if (ticks == 21) { return 150; }
	if (ticks >= 12) { return (ticks-10) * 10; }
	if (ticks == 11) { return 15; }
	return ticks;
}

function toggleSlider(name, value) {
	var slidableElement = eventManager.getHandler(name);
	if (slidableElement != null) { slidableElement.editSlider(value == 'enabled'); }
}

SlidableElement = function(name, fieldId, statusId, initValue, callbackFn) {
	this.name          = name;
	this.fieldId       = fieldId;
	this.statusObj     = findObj(statusId);
	this.callbackFn    = callbackFn != null ? callbackFn : edit_field;
	this.toolbarExtObj = findObj('dx_toolbar_extension');
	this.slider        = null;
	this.scaledValue   = initValue;
	this.tickInterval  = 3;

	eventManager.registerHandler(name, this);
	this.callbackFn(fieldId, initValue);
}

SlidableElement.prototype.deactivated = function() {
	this.deleteSlider();
}

SlidableElement.prototype.editSlider = function(edit) {
	if (edit) {
		eventManager.activateHandler(this.name);
		this.toolbarExtObj.style.display = 'block';
		this.createSlider();
	} else {
		this.deleteSlider();
		eventManager.unsetActiveHandler();
	}
}

SlidableElement.prototype.createSlider = function() {
	this.toolbarExtObj.innerHTML  = "<div id='design_slider_editor_box'><div id='design_slider_editor_slider'><div id='design_slider_editor_thumb'><img src='/system/images/edit_image_slider_horiz_thumb.gif'/></div></div></div>";
	this.slider          = YAHOO.widget.Slider.getHorizSlider('design_slider_editor_slider', 'design_slider_editor_thumb', 0, 90, this.tickInterval);
	this.slider.self     = this;
	this.slider.onChange = function(value) { this.self.sliderChanged(value); };
	this.slider.setValue(scaledValueToSliderValue(this.scaledValue, this.tickInterval));
}

SlidableElement.prototype.deleteSlider = function() {
	this.slider = null;
	this.toolbarExtObj.innerHTML     = '';
	this.toolbarExtObj.style.display = 'none';
	if (this.statusObj != null) { this.statusObj.style.fontWeight = 'normal'; }
}

SlidableElement.prototype.sliderChanged = function(sliderValue) {
	this.scaledValue = sliderValueToScaledValue(sliderValue, this.tickInterval);
	this.changeStatus(this.scaledValue, true);
	this.callbackFn(this.fieldId, this.scaledValue);
}

SlidableElement.prototype.changeStatus = function(value, editing) {
	if (this.statusObj != null) {
		this.statusObj.innerHTML = value;
		this.statusObj.style.fontWeight = editing ? 'bold' : 'normal';
	}
}

/*** SlidableElement END ***/

/*** MarginableElement START ***/

function toggleMargins(name, value) {
	var marginableElement = eventManager.getHandler(name);
	if (marginableElement != null) { marginableElement.editMargins(value == 'margins_enabled'); }
}

MarginableElement = function(name, fieldPrefix, targetId, callbackFn) {
	this.name          = name;
	this.fieldPrefix   = fieldPrefix;
	this.targetObj     = findObj(targetId);
	this.wrapperObj    = findObj(targetId + '_wrapper');
	this.toolbarExtObj = findObj('dx_toolbar_extension');
	this.callbackFn    = callbackFn != null ? callbackFn : edit_field;
	this.marginObj     = (this.wrapperObj == null) ? this.targetObj : this.wrapperObj;
	this.tickInterval  = 3;

	eventManager.registerHandler(name, this);
	this.initSliders(); // Callback/init hidden fields.
}

MarginableElement.prototype.deactivated = function() {
	this.toolbarExtObj.style.display = 'none';
}

MarginableElement.prototype.editMargins = function(edit) {
	if (edit) {
		eventManager.activateHandler(this.name);
		this.toolbarExtObj.style.display = 'block';
		this.createSliders();
		this.initSliders();
	} else {
		this.toolbarExtObj.style.display = 'none';
		eventManager.unsetActiveHandler();
	}
}

MarginableElement.prototype.createSliders = function() {
	this.toolbarExtObj.innerHTML  = "<div id='design_margin_editor_slider_top_box' style='float: left'><div id='design_margin_editor_slider_top'><div id='design_margin_editor_slider_top_thumb'><img src='/system/images/edit_image_slider_horiz_thumb.gif'/></div></div></div><div id='design_margin_editor_slider_left_box' style='float: left'><div id='design_margin_editor_slider_left'><div id='design_margin_editor_slider_left_thumb'><img src='/system/images/edit_image_slider_horiz_thumb.gif'/></div></div></div><div id='design_margin_editor_slider_right_box' style='float: left'><div id='design_margin_editor_slider_right'><div id='design_margin_editor_slider_right_thumb'><img src='/system/images/edit_image_slider_horiz_thumb.gif'/></div></div></div><div id='design_margin_editor_slider_bottom_box' style='float: left'><div id='design_margin_editor_slider_bottom'><div id='design_margin_editor_slider_bottom_thumb'><img src='/system/images/edit_image_slider_horiz_thumb.gif'/></div></div></div><div style='clear: both;'></div>";

	this.topSlider    = YAHOO.widget.Slider.getHorizSlider('design_margin_editor_slider_top',    'design_margin_editor_slider_top_thumb',    0, 90, this.tickInterval);
	this.leftSlider   = YAHOO.widget.Slider.getHorizSlider('design_margin_editor_slider_left',   'design_margin_editor_slider_left_thumb',   0, 90, this.tickInterval);
	this.rightSlider  = YAHOO.widget.Slider.getHorizSlider('design_margin_editor_slider_right',  'design_margin_editor_slider_right_thumb',  0, 90, this.tickInterval);
	this.bottomSlider = YAHOO.widget.Slider.getHorizSlider('design_margin_editor_slider_bottom', 'design_margin_editor_slider_bottom_thumb', 0, 90, this.tickInterval);

	this.topSlider.self    = this;
	this.leftSlider.self   = this;
	this.rightSlider.self  = this;
	this.bottomSlider.self = this;

	this.topSlider.onChange    = function(value) { this.self.sliderChanged('top',    value); };
	this.leftSlider.onChange   = function(value) { this.self.sliderChanged('left',   value); };
	this.rightSlider.onChange  = function(value) { this.self.sliderChanged('right',  value); };
	this.bottomSlider.onChange = function(value) { this.self.sliderChanged('bottom', value); };
}

MarginableElement.prototype.initSliders = function() {
	var top    = scaledValueToSliderValue(parseInt(this.marginObj.style.marginTop),    this.tickInterval);
	var left   = scaledValueToSliderValue(parseInt(this.marginObj.style.marginLeft),   this.tickInterval);
	var right  = scaledValueToSliderValue(parseInt(this.marginObj.style.marginRight),  this.tickInterval);
	var bottom = scaledValueToSliderValue(parseInt(this.marginObj.style.marginBottom), this.tickInterval);

	if (this.topSlider != null)    { this.topSlider.setValue(top); }
	if (this.leftSlider != null)   { this.leftSlider.setValue(left); }
	if (this.rightSlider != null)  { this.rightSlider.setValue(right); }
	if (this.bottomSlider != null) { this.bottomSlider.setValue(bottom); }

	this.sliderChanged('top',    top);
	this.sliderChanged('left',   left);
	this.sliderChanged('right',  right);
	this.sliderChanged('bottom', bottom);
}

MarginableElement.prototype.sliderChanged = function(side, value) {
	var scaledValue = sliderValueToScaledValue(value, this.tickInterval);
	if (side == 'top')    { this.marginObj.style.marginTop    = scaledValue + 'px'; }
	if (side == 'left')   { this.marginObj.style.marginLeft   = scaledValue + 'px'; }
	if (side == 'right')  { this.marginObj.style.marginRight  = scaledValue + 'px'; }
	if (side == 'bottom') { this.marginObj.style.marginBottom = scaledValue + 'px'; }

	var fieldId = this.fieldPrefix + '_' + side;
	this.callbackFn(fieldId, scaledValue);
}

/*** MarginableElement END ***/

/*** Dragdrop START ***/

// Konfigurasjon
YAHOO.util.DDM.mode = YAHOO.util.DDM.INTERSECT;

// Klassedefinisjoner
DragableContent = function(id, draghandle, sGroup, reference) {
	if (id)         { this.init(id, sGroup, null); }
	if (draghandle) { this.setHandleElId(draghandle); }
	this.initFrame();
	this.currentTarget = null;
	this.reference     = reference;
}

YAHOO.extend(DragableContent, YAHOO.util.DDProxy);

DragableContent.prototype.endDrag = function(e) {
	// Do nothing.
}

DragableContent.prototype.onDragDrop = function(e, targets) {
	var target = this.findClosest(targets, e);
	if (target != null && target.droppedOn) {
		target.droppedOn(this, e.ctrlKey, e.shiftKey);
	}
}

DragableContent.prototype.onDragOver = function(e, targets) {
	var target = this.findClosest(targets, e);
	if (target != null && target.draggedOver) {
		if (this.currentTarget) { this.currentTarget.draggedOut(this); }
		target.draggedOver(this);
		this.currentTarget = target;
	}
}

DragableContent.prototype.onDragOut = function(e, targets) {
	if (targets instanceof Array) {
		for (key = 0; key < targets.length; key++) {
			var target = targets[key];
			if (target != null && target.draggedOut) { target.draggedOut(this); }
		}
	} else {
		var target = YAHOO.util.DDM.getDDById(targets);
		if (target != null && target.draggedOut) { target.draggedOut(this); }
	}

}

DragableContent.prototype.findClosest = function(targets, e) {
	var closest = null;
	if (targets instanceof Array) {
		for (key = 0; key < targets.length; key++) {
			var target = targets[key];
			if (target.isDragTarget && this.isCloser(target, closest, e)) {
				closest = target;
			}
		}
		return closest;
	}
	return YAHOO.util.DDM.getDDById(targets);
}

DragableContent.prototype.isCloser = function(targetA, targetB, e) {
	if (targetB == null) { return true; }
	if (targetA == null) { return false; }

	var regionA = YAHOO.util.DDM.getLocation(targetA);
	var regionB = YAHOO.util.DDM.getLocation(targetB);

	var centreAX = regionA.left + ((regionA.right  - regionA.left) / 2);
	var centreAY = regionA.top  + ((regionA.bottom - regionA.top)  / 2);
	var centreBX = regionB.left + ((regionA.right  - regionA.left) / 2);
	var centreBY = regionB.top  + ((regionA.bottom - regionA.top)  / 2);
	var mouseX   = YAHOO.util.Event.getPageX(e);
	var mouseY   = YAHOO.util.Event.getPageY(e);

	return this.distance(mouseX, mouseY, centreAX, centreAY) < this.distance(mouseX, mouseY, centreBX, centreBY);
}

DragableContent.prototype.distance = function(x1, y1, x2, y2) {
	return Math.sqrt(Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));
}

DragTarget = function(id, sGroup) {
	if (id) { this.init(id, sGroup, null); }
}

YAHOO.extend(DragTarget, YAHOO.util.DDTarget);

DragTarget.prototype.isDragTarget = function() {
	// Markerfunksjon.... :roll:
	// Denne trengs fordi targets-arrayen som findClosest() får inn når YAHOO.util.DDM.mode er YAHOO.util.DDM.INTERSECT inneholder både DD-, DDProxy- og DDTarget-objekter, mens vi kun er interessert i våre egendefinerte DDTarget-objekter. findClosest() bruker denne funksjonen for å filtrere ut de uinteressante target-ene.
}

DragTarget.prototype.getCopyQueryParam = function(ctrlKey) {
	return ctrlKey  ? '&duplicate=yes' : '&duplicate=no';
}

DragTarget.prototype.dragToChild = function(elId) {
	return $(this.id).descendantOf(elId);
}

DragTarget.prototype.dragToImmediateSibling = function(elId) {
	var targetEl  = $(this.id);
	var draggedEl = $(elId);
	return this.isFirstSibling(targetEl.previousSiblings(), draggedEl) || this.isFirstSibling(targetEl.nextSiblings(), draggedEl);
}

DragTarget.prototype.isFirstSibling = function(siblings, el) {
	var isFirst = false;
	for (var i=0; i<siblings.length; i++) {
		var sibling = siblings[i];
		if (sibling.id.indexOf('content_') == 0) {
			return sibling == el;
		}
	}

	return isFirst;
}

/* "Interface" som må implementeres for alle "subklasser" av DragTarget (senere DropTarget). */
DragTarget.prototype.droppedOn   = function(draggedContent, ctrlKey, shiftKey) { }
DragTarget.prototype.draggedOver = function(draggedContent) { }
DragTarget.prototype.draggedOut  = function(draggedContent) { }

InsertAfterContentTarget = function(id, sGroup, docId, afterId) {
	if (id) { this.init(id, sGroup, null); }

	this.docId   = docId;
	this.afterId = afterId;
}

YAHOO.extend(InsertAfterContentTarget, DragTarget);

InsertAfterContentTarget.prototype.droppedOn = function(draggedContent, ctrlKey, shiftKey) {
	if (this.dragToChild(draggedContent.id))            { return; } // Sjekker om dette elementet er et barn av det andre (for å forhindre at containere dras inn i seg selv)
	if (this.dragToImmediateSibling(draggedContent.id)) { return; } // Sjekker om dette elementet er nærmeste søsken

	this.getEl().style.background = 'url("/system/images/dragarrows.png") center no-repeat #eef4ee';
	var copy = this.getCopyQueryParam(ctrlKey);
	window.location = '?service=content_edit&action=move_after&doc_id=' + this.docId + '&content_id=' + draggedContent.reference + '&target_id=' + this.afterId + copy;
}

InsertAfterContentTarget.prototype.draggedOver = function(draggedContent) {
	if (this.dragToChild(draggedContent.id))            { return; } // Sjekker om dette elementet er et barn av det andre (for å forhindre at containere dras inn i seg selv)
	if (this.dragToImmediateSibling(draggedContent.id)) { return; } // Sjekker om dette elementet er nærmeste søsken

	this.getEl().style.background = 'url("/system/images/dragarrows.png") center no-repeat #eee';
}

InsertAfterContentTarget.prototype.draggedOut = function(draggedContent) {
	this.getEl().style.background = 'none';
}

InsertFirstInContainerTarget = function(id, sGroup, docId, containerId) {
	if (id) { this.init(id, sGroup, null); }

	this.docId       = docId;
	this.containerId = containerId;
}

YAHOO.extend(InsertFirstInContainerTarget, InsertAfterContentTarget);

InsertFirstInContainerTarget.prototype.droppedOn = function(draggedContent, ctrlKey, shiftKey) {
	if (this.dragToChild(draggedContent.id))            { return; } // Sjekker om dette elementet er et barn av det andre (for å forhindre at containere dras inn i seg selv)
	if (this.dragToImmediateSibling(draggedContent.id)) { return; } // Sjekker om dette elementet er nærmeste søsken

	this.getEl().style.background = 'url("/system/images/dragarrows.png") center no-repeat #eef4ee';
	var copy = this.getCopyQueryParam(ctrlKey);
	window.location = '?service=content_edit&action=move_first&doc_id=' + this.docId + '&content_id=' + draggedContent.reference + '&target_id=' + this.containerId + copy;
}

/*** Dragdrop END***/

/*** TabPane START ***/

TabPane = function(elId, initialTabId, initiallyOpen) {
	this.elId      = elId;
	this.headId    = elId + '_head';
	this.bodyId    = elId + '_body';
	this.tailId    = elId + '_tail';
	this.tabsId    = elId + '_tabs';
	this.contentId = elId + '_content';
	this.handleId  = elId + '_handle';

	this.open         = (initiallyOpen != null) ? initiallyOpen : true;
	this.initialTabId = initialTabId;
	this.currentTab   = null;

	this.propertyChangeHandler = DX.getSessionPropertyHandler(elId);

	this.tabs   = new Hash();
	this.policy = new Hash();

	this.setDefaultPolicies();
}

TabPane.prototype.setDefaultPolicies = function() {
	this.policy.set('unselected', 'icon_and_text');
	this.policy.set('selected',   'icon_and_text');
	this.policy.set('hover',      'icon_and_text');
	this.policy.set('icon_alignment', 'left');
}

TabPane.prototype.createTab = function(contentId, tabText, iconUrl) {
	var newTab = new Tab(contentId, tabText, iconUrl, this);
	this.tabs.set(contentId, newTab);
	if (this.currentTab == null || contentId == this.initialTabId) { this.currentTab = newTab; } // Aktiv tab er den med initialTabId eller, alternativt, den som lages først.
}

TabPane.prototype.render = function() {
	// Lager HTML for selve tab-området.
	$(this.elId).update("<div id='" + this.headId + "'>\n\t<div id='" + this.tabsId + "'></div>\n</div>\n<div id='" + this.bodyId + "'>\n\t<div id='" + this.contentId + "'></div>\n</div>\n<div id='" + this.tailId + "'></div>\n<a id='" + this.handleId + "' href='javascript: void(0);'></a>");
	$(this.elId).addClassName('tabpane_main');
	if (!this.open) { $(this.elId).addClassName('closed'); }

	// Legger til klasser.
	$(this.headId).addClassName('tabpane_head_outer');
	$(this.tabsId).addClassName('tabpane_head_inner');
	$(this.bodyId).addClassName('tabpane_body_outer');
	$(this.contentId).addClassName('tabpane_body_inner');
	$(this.tailId).addClassName('tabpane_tail_outer');
	$(this.handleId).addClassName('tabpane_toggle_handle');
	if (this.open) { $(this.handleId).hide(); }

	// Rendrer hver enkelt tab. Aktiverer gjeldende tab.
	this.tabs.values().invoke('render');
	this.currentTab.activate();

	Event.observe(this.handleId, 'click', this.onHandleClick.bindAsEventListener(this));
}

TabPane.prototype.showToggleHandle = function(show) {
	if (show) {
		$(this.handleId).style.display = '';
	} else {
		$(this.handleId).style.display = 'none';
		this.setOpen(true); // For å hindre et lukket pane uten mulighet for å åpne (kan aldri ha både open = false og toggleHandle = false).
	}
}

TabPane.prototype.setOpen = function(open) {
	this.open = open;
	if (open) {
		$(this.elId).removeClassName('closed');
		if (this.currentTab != null) { $(this.currentTab.contentId).show(); }
	} else {
		$(this.elId).addClassName('closed');
		if (this.currentTab != null) { $(this.currentTab.contentId).hide(); }
		this.showToggleHandle(true); // For å hindre et lukket pane uten mulighet for å åpne (kan aldri ha både open = false og toggleHandle = false).
	}
	this.propertyChangeHandler.setProperty('open', open);
}

TabPane.prototype.setCurrentTabId = function(contentId) {
	this.setCurrentTabObj(this.tabs.get(contentId));
}

TabPane.prototype.setCurrentTabObj = function(tab) {
	if (tab == null || tab == this.currentTab) { return; }

	if (this.currentTab) { this.currentTab.deactivate(); }
	this.currentTab = tab;
	this.currentTab.activate();
	this.propertyChangeHandler.setProperty('current_tab', tab.contentId);
}

TabPane.prototype.onHandleClick = function(e) {
	$(this.handleId).blur();
	this.setOpen(!this.open);
}

Tab = function(contentId, tabText, iconUrl, tabPane) {
	this.contentId = contentId;
	this.tabText   = tabText;
	this.iconUrl   = iconUrl;
	this.tabPane   = tabPane;

	this.elId = contentId + '_tab';
}

Tab.prototype.render = function() {
	if (!this.tabPane) { return; } // Ikke noen rendring uten at tab-en er lagt til et tabPane.

	var tabMarkup  = "<a id='" + this.elId + "' href='javascript: void(0);'><span class='door'><span></span></span></a>";
	$(this.tabPane.tabsId).insert(tabMarkup);
	this.setTabContents('unselected');
	$(this.elId).addClassName('tab');

	$(this.tabPane.contentId).appendChild($(this.contentId));
	$(this.contentId).hide();

	Event.observe(this.elId, 'click',     this.onClick.bindAsEventListener(this));
	Event.observe(this.elId, 'mouseover', this.onMouseOver.bindAsEventListener(this));
	Event.observe(this.elId, 'mouseout',  this.onMouseOut.bindAsEventListener(this));
}

Tab.prototype.setTabContents = function(policyKey) {
	var spanEl = $$('#' + this.elId + ' .door span').first();

	var policy = this.tabPane.policy.get(policyKey);
	if (policy != 'icon_only') { spanEl.update(this.tabText); }
	if (policy != 'text_only') {
		var alignment = this.tabPane.policy.get('icon_alignment');
		var iconStyle = (this.iconUrl != null) ? "url('" + this.iconUrl + "') no-repeat " + alignment + " center" : '';
		spanEl.style.background = iconStyle;
	}
}

Tab.prototype.activate = function() {
	this.setTabContents('selected');
	$(this.elId).addClassName('current');
	if (this.tabPane.open) { $(this.contentId).show(); }
}

Tab.prototype.deactivate = function() {
	this.setTabContents('unselected');
	$(this.elId).removeClassName('current');
	$(this.contentId).hide();
}

Tab.prototype.onClick = function(e) {
	$(this.elId).blur();
	this.tabPane.setCurrentTabObj(this);
}

Tab.prototype.onMouseOver = function(e) {
	if (this == this.tabPane.currentTab) { return; }
	this.setTabContents('hover');
	$(this.elId).addClassName('hover');
}

Tab.prototype.onMouseOut = function(e) {
	if (this == this.tabPane.currentTab) { return; }
	this.setTabContents('unselected');
	$(this.elId).removeClassName('hover');
}

DX.getSessionPropertyHandler = function(group) { return new DX.PropertyHandler(group, '?service=properties'); }
// DX.getPersistentPropertyHandler = function(group) { return new DX.PropertyHandler(group, '?service=properties&type=persistent'); } // Ikke implementert ennå (type=X ignoreres på server-siden).

DX.PropertyHandler = function(group, url) {
	this.group = group;
	this.url   = url;
	this.properties = [];
}

DX.PropertyHandler.prototype.getProperty = function(property, defaultValue) {
	var value = null;
	new Ajax.Request(this.url, {
		asynchronous: false, // NB! Bruk getProperty så lite som mulig.
		parameters: {
			action:   'get',
			group:    this.group,
			property: property
		},
		onSuccess: function(res) { value = res.responseJSON; }
	});
	return value == null ? defaultValue : value;
}

DX.PropertyHandler.prototype.setProperty = function(property, value) {
	new Ajax.Request(this.url, {
		parameters: {
			action:   'set',
			group:    this.group,
			property: property,
			value:    Object.toJSON(value)
		}
	});
}

/*** TabPane END ***/

/*** HoverableTriggerItem START ***/

HoverableTriggerItem = function(elId, otherId, delay) {
	this.elId     = elId;
	this.otherObj = $(otherId);
	this.delay    = delay;

	YAHOO.util.Event.addListener(this.elId, 'mouseover', this.onOver, this, true);
	YAHOO.util.Event.addListener(this.elId, 'mouseout',  this.onOut,  this, true);
}

HoverableTriggerItem.prototype.onOver = function() {
	if (this.otherObj.hideTimeout) { clearTimeout(this.otherObj.hideTimeout); }
	setVisible(this.otherObj, true);
}

HoverableTriggerItem.prototype.onOut = function() {
	this.otherObj.hideTimeout = setTimeout('setVisible($("' + this.otherObj.id + '"), false)', this.delay);
	// Tror denne vil være bedre ved bruk av delay (fra Prototype): setVisible.delay(this.delay/1000, this.otherObj, false);
}

/*** HoverableTriggerItem END ***/

/*** End of file, js_core.js ***/

/*** Start of file, js_dialogs.js ***/
/**
* Agathons Dialog Boxes
* Versjon: 0.1
*
* Dette er et skript som gjør det mulig å vise dialogbokser på en side.
*/
var dialogProducer = new DialogProducer();
var OK_PRESSED     = 1;
var CANCEL_PRESSED = 2;
var EXIT_PRESSED   = 3;

function DialogProducer() {

	var documntObj;
	var okAction;
	var cancelAction;
	var exitAction;

	var dialogWidth;
	var dialogHeight;
	var dialogTop;
	var dialogLeft;

	this.showConfirmDialog  = showConfirmDialog;
	this.showMessageDialog  = showMessageDialog;
	this.showCustomDialog   = showCustomDialog;
	this.showProgressDialog = showProgressDialog;
	this.eventHappened      = eventHappened;
	this.setVisible         = setVisible;
	this.placeDialog        = placeDialog;
	this.clearActions       = clearActions;

	function showConfirmDialog(title, text, action) {
		dialogObj         = findObj('dialog_close');
		dialogTitleObj    = findObj('dialog_title');
		dialogTextObj     = findObj('dialog_text');
		dialogButtonsObj  = findObj('dialog_button_div');

		dialogTitleObj.innerHTML   = title;
		dialogTextObj.innerHTML    = text;
		dialogTextObj.style.textAlign = 'center';
		dialogButtonsObj.innerHTML = "<input type='submit' value='Ok' class='dialog_buttons' onMouseUp='dialogProducer.eventHappened(OK_PRESSED)' /><input type='submit' value='Cancel' class='dialog_buttons' onMouseUp='dialogProducer.eventHappened(CANCEL_PRESSED)' />";

		this.okAction     = action;
		this.cancelAction = new CloseAction();
		this.exitAction = new CloseAction();

		this.placeDialog();
		this.setVisible(true);
	}

	function showMessageDialog(title, text) {
		dialogObj         = findObj('dialog_close');
		dialogTitleObj    = findObj('dialog_title');
		dialogTextObj     = findObj('dialog_text');
		dialogButtonsObj  = findObj('dialog_button_div');

		this.setVisible(false);

		dialogTitleObj.innerHTML   = title;
		dialogTextObj.innerHTML    = text;
		dialogTextObj.style.textAlign = 'center';
		dialogButtonsObj.innerHTML = "<input type='submit' value='Ok' class='dialog_buttons' onMouseUp='dialogProducer.eventHappened(OK_PRESSED)' />";

		this.okAction = new CloseAction();
		this.exitAction = new CloseAction();

		this.placeDialog();
		this.setVisible(true);
	}

	function showCustomDialog(titleElementName, contentElementName, okAction, cancelAction, exitAction) {
		dialogObj         = findObj('dialog_close');
		dialogTitleObj    = findObj('dialog_title');
		dialogTextObj     = findObj('dialog_text');
		titleElementObj   = findObj(titleElementName);
		contentElementObj = findObj(contentElementName);

		if (contentElementObj == null) { return; }

		this.setVisible(false);

		dialogTitleObj.innerHTML      = titleElementObj.innerHTML;
		dialogTextObj.innerHTML       = contentElementObj.innerHTML;
		dialogTextObj.style.textAlign = 'center';

		this.okAction = okAction;
		this.cancelAction = cancelAction;
		this.exitAction = exitAction;

		this.placeDialog();
		this.setVisible(true);
	}

	function showProgressDialog(title, text) {
		dialogObj         = findObj('dialog_close');
		dialogTitleObj    = findObj('dialog_title');
		dialogTextObj     = findObj('dialog_text');
		dialogButtonsObj  = findObj('dialog_button_div');

		this.setVisible(false);

		dialogTitleObj.innerHTML   = title;
		dialogTextObj.innerHTML    = text;
		dialogTextObj.style.textAlign = 'center';
		dialogButtonsObj.innerHTML = "<img src='" + WEB_IMAGE + "dialog_progress.gif'>";

		this.placeDialog();
		this.setVisible(true);
	}

	function eventHappened(eventCode) {
		if (eventCode == OK_PRESSED && this.okAction != null) {
			this.okAction.doAction();
		}
		else if (eventCode == EXIT_PRESSED && this.exitAction != null) {
			this.exitAction.doAction();
		}
		else if (eventCode == CANCEL_PRESSED && this.cancelAction != null) {
			this.cancelAction.doAction();
		}
	}

	function setVisible(visible) {
		dialogObj = findObj('dialog_close');
		modalObj  = findObj('dialog_modal_div');

		if (visible) {
			dialogObj.style.visibility = 'visible';
			modalObj.style.visibility  = 'visible';
		}
		else {
			dialogObj.style.visibility = 'hidden';
			modalObj.style.visibility  = 'hidden';
		}
	}

	function placeDialog() {
		dialogObj = findObj('dialog_close');
		modalObj  = findObj('dialog_modal_div');

		var innerTop    = get_inner_top();
		var innerLeft   = get_inner_left();
		var innerWidth  = get_inner_width();
		var innerHeight = get_inner_height();

		if (innerTop == null)  { innerTop = 0; }
		if (innerLeft == null) {innerLeft = 0; }

		//alert(innerTop + ', ' + innerLeft + ', ' + innerWidth + ', ' + innerHeight);

		modalObj.style.top    = innerTop;    // + 'px';
		modalObj.style.left   = innerLeft;   // + 'px';
		modalObj.style.width  = innerWidth;  // + 'px';
		modalObj.style.height = innerHeight; // + 'px';

		dialogObj.style.top  = (innerTop  + (innerHeight / 2) -  50); // + 'px';
		dialogObj.style.left = (innerLeft + (innerWidth  / 2) - 200); // + 'px';

		setTimeout('dialogProducer.placeDialog()', 100);
	}

	function clearActions() {
		this.okAction     = null;
		this.cancelAction = null;
		this.exitAction   = null;
	}
}

function CloseAction() {
	this.doAction = doAction;

	function doAction() {
		dialogProducer.setVisible(false);
		dialogProducer.clearActions();
	}
}

function RedirectAction(URL) {
	var redirectURL;
	this.redirectURL = URL;

	this.doAction = doAction;

	function doAction() {
		dialogProducer.setVisible(false);
		dialogProducer.clearActions();
		window.location=this.redirectURL;
	}
}
/*** End of file, js_dialogs.js ***/

/*** Start of file, js_tag_area.js ***/
/**
 * Dette er hovedklassen til TagArea
 *
 * @param elementIDorObj ID-en til, eller selve elementet som skal bli et tag-redigeringsområde
 * @param initialTags
 */
TagArea = function(elementIdOrObj, callBackObj, initialTags) {
	this.element     = $(elementIdOrObj);
	this.tags        = [];
	this.callBackObj = callBackObj;
	this.notify      = false;

	this.initialize();

	if (initialTags instanceof Array) {
		for (var i=0; i<initialTags.length; i++) {
			this.addTag(initialTags[i]);
		}
	}

	this.notify = true;

	if (initialTags instanceof Array) {
		this.notifyOfChange();
	}
}

TagArea.prototype.initialize = function() {
	this.element.addClassName('tag_container');
	this.createInputField();
	Event.observe(this.element, 'click', this.handleClick.bindAsEventListener(this));
}

TagArea.prototype.createInputField = function() {
	var inputFieldId = this.element.id + '_input_field';
	//var suggestBoxId = inputFieldId + '_suggest';
	new Insertion.Bottom(this.element, "<input type='text' id='" + inputFieldId + "' name='" + inputFieldId + "' autocomplete='off' />");
	//new Insertion.Bottom(this.element, "<div class='tag_suggester_box' id='" + suggestBoxId + "'></div>");
	new Insertion.Bottom(this.element, "<div style='clear: both;'></div>");
	this.inputField = $(inputFieldId);
	//this.suggestBox = $(suggestBoxId);
	Event.observe(this.inputField, 'keydown', this.handleKeyDown.bindAsEventListener(this));
	Event.observe(this.inputField, 'blur',    this.handleBlur.bindAsEventListener(this));

	// Autocomplete
	/*
	var myServer = '.';
	var mySchema = ['result', 'query', 'attachment'];
	var myDataSource = new YAHOO.widget.DS_XHR(myServer, mySchema);
	myDataSource.responseType = myDataSource.TYPE_XML;
	myDataSource.scriptQueryAppend = 'service=tag_suggest&rand=' + DX.randomInt(100000, 999999); // Legger på en random slik at ikke nettleseren cacher kallet mellom siderequest.
	var myAutoComp = new YAHOO.widget.AutoComplete(inputFieldId, suggestBoxId, myDataSource);
	myAutoComp.highlightClassName = 'highlighted';
	myAutoComp.typeAhead     = false;
	myAutoComp.autoHighlight = false;
	myAutoComp.containerExpandEvent.subscribe(function() { this._oContainer.style.display = 'block'; });
	myAutoComp.containerCollapseEvent.subscribe(function() { this._oContainer.style.display = 'none'; });
	myAutoComp.formatResult = function(aResultItem, sQuery) {
		return "<div>" + aResultItem[0] + "</div>";
	}
	*/
}

TagArea.prototype.addTag = function(tagName) {
	tagName = tagName.strip().toLowerCase();
	if (tagName == '') { return; }
	this.inputField.value = '';
	var tag = this.findTagByName(tagName);

	if (tag == null) {
		tag = new Tag(tagName, this);
		tag.insertBefore(this.inputField);
		this.tags[this.tags.length] = tag;

		this.notifyOfChange();
	}
	else {
		tag.blink();
	}
}

TagArea.prototype.findTagByName = function(tagName) {
	var index = this.findTagIndexByName(tagName);
	return index >= 0 ? this.tags[index] : null;
}

TagArea.prototype.findTagIndexByName = function(tagName) {
	var resultIndex = -1;
	this.tags.each(function(tag, index) {
		if (tag.name == tagName) { resultIndex = index; }
	});
	return resultIndex;
}

TagArea.prototype.removeTag = function(tag) {
	var index = this.findTagIndexByName(tag.name);
	this.tags.splice(index, 1);

	this.notifyOfChange();
}

TagArea.prototype.notifyOfChange = function() {
	//this.positionSuggestBox();
	if (this.notify && this.callBackObj && this.callBackObj.tagsChanged) {
		this.callBackObj.tagsChanged(this.tags, this.element.id);
	}
}

TagArea.prototype.positionSuggestBox = function() {
	var position = Position.cumulativeOffset(this.inputField);
	this.suggestBox.setStyle({
		left: position[0] + 'px',
		top: (position[1] + this.inputField.getHeight()) + 'px'
	});
}

TagArea.prototype.handleKeyDown = function(e) {
	// Velger å håndtere keydown istedenfor keypressed, ettersom sistnevnte ikke trigges for tab-er i IE, virker det som, og førstnevnte gir konsistente resultat for de aller fleste nettlesere, jf. http://unixpapa.com/js/key.html
	if (e.keyCode == Event.KEY_RETURN || e.keyCode == Event.KEY_TAB || e.keyCode == 188 /* komma */) {
		this.addTag(this.inputField.value);
		e.stop();
	}
	var chars = Math.max(6, this.inputField.value.length + 2);
	this.inputField.setStyle({width : chars + 'em'});
}

TagArea.prototype.handleBlur = function(e) {
	this.addTag(this.inputField.value);
	e.stop();
	//this.inputField.value = '';
}

TagArea.prototype.handleClick = function(e) {
	this.inputField.focus();
}

/**
 * Denne klassen representerer en tag
 *
 * @param name Navnet til taggen (nøkkelordet om en vil)
 * @param tagArea TagArea-objektet som taggen er koblet til
 */
Tag = function(name, tagArea) {
	this.name    = name;
	this.tagArea = tagArea;
}

Tag.prototype.insertBefore = function(element) {
	var tagId = this.tagArea.id + '_tag_' + this.name;
	new Insertion.Before(element, "<div id='" + tagId + "' class='tag'><span>" + this.name + "</span></div>");
	this.element = $(tagId);

	Event.observe(this.element, 'mouseover', this.handleMouseOver.bindAsEventListener(this));
	Event.observe(this.element, 'mouseout',  this.handleMouseOut.bindAsEventListener(this));
	Event.observe(this.element, 'click',     this.handleClick.bindAsEventListener(this));
}

Tag.prototype.handleMouseOver = function(event) {
	this.element.addClassName('tag_hover');
}

Tag.prototype.handleMouseOut = function(event) {
	this.element.removeClassName('tag_hover');
}

Tag.prototype.handleClick = function(event) {
	this.deleteTag();
}

Tag.prototype.deleteTag = function() {
	this.tagArea.removeTag(this);
	this.element.remove();
}

Tag.prototype.blink = function() {
	var me = this;
	this.element.addClassName('tag_blink');
	setTimeout(function() { me.toggleBlinking(); }, 500);
	setTimeout(function() { me.toggleBlinking(); }, 1000);
	setTimeout(function() { me.toggleBlinking(); }, 1500);
	setTimeout(function() { me.toggleBlinking(); }, 2000);
	setTimeout(function() { me.toggleBlinking(); }, 2500);
}

Tag.prototype.toggleBlinking = function() {
	if (this.element.hasClassName('tag_blink')) {
		this.element.removeClassName('tag_blink');
	}
	else {
		this.element.addClassName('tag_blink');
	}
}

/**
 * FieldUpdater-objekter sørger for å oppdatere verdien i et felt med en kommaseparert liste av alle taggene
 */

FieldUpdater = function(elOrId) {
	this.el = $(elOrId);
}

FieldUpdater.prototype.tagsChanged = function(tags, elId) {
	this.el.value = tags.collect(function(tag, index) { return tag.name; } ).join(',');
}
/*** End of file, js_tag_area.js ***/

/*** Start of file, js_fieldset_toggler.js ***/
FieldsetToggler = function(name) {
	this.name    = name;
	this.buttons = $$('input[name='+name+']'); // Finner radiobuttons med angitt navn.

	this.toggle();
	YAHOO.util.Event.addListener(this.buttons, 'click', this.toggle, this, true);
}

FieldsetToggler.prototype.toggle = function() {
	this.buttons.each(function(buttonObj) {
		var fieldsetObj = $('fieldset_' + buttonObj.id);
		fieldsetObj.descendants().each(function(elementObj) {
			var tag = elementObj.nodeName;
			if (elementObj.id == buttonObj.id || !(tag == 'INPUT' || tag == 'SELECT' || tag == 'TEXTAREA')) { return; } // return = continue
			elementObj.disabled = !buttonObj.checked;
		});
	});
}

/*** End of file, js_fieldset_toggler.js ***/

/*** Start of file, js_image_rotator.js ***/
ImageRotator = function(elOrId, moduleId) {
	this.el       = $(elOrId);
	this.innerEl  = this.createInnerEl();
	this.moduleId = moduleId;

	this.url = '?service=file_image_rotator&module_id=' + moduleId;
	this.currentIndex = 0;

	this.fetchImageList();
}

ImageRotator.prototype.createInnerEl = function() {
	var id = this.el.id + '_inner_el';
	new Insertion.Bottom(this.el, "<div id='" + id + "'></div>");
	return $(id);
}

ImageRotator.prototype.createBuffers = function() {
	this.innerEl.setStyle({
		width:    this.width,
		height:   this.height,
		overflow: 'hidden',
		padding:  '0px',
		margin:   '0px'
	});

	var bufferOneId = this.el.id + '_buffer_one';
	new Insertion.Bottom(this.innerEl, "<div id='" + bufferOneId + "'></div>");
	this.bufferOneEl = $(bufferOneId);

	this.bufferOneEl.setStyle({
		width:              this.width,
		height:             this.height,
		backgroundPosition: 'center center',
		backgroundRepeat:   'no-repeat',
		backgroundColor:    'white',
		backgroundImage:    "url('" + this.getFrontBufferImage().path + "')",
		position:           'relative',
		top:                '0px',
		zIndex:             4,
		padding:            '0px',
		margin:             '0px'
	});

	var bufferTwoId = this.el.id + '_buffer_two';
	new Insertion.Bottom(this.innerEl, "<div id='" + bufferTwoId + "'></div>");
	this.bufferTwoEl = $(bufferTwoId);

	this.bufferTwoEl.setStyle({
		width:              this.width,
		height:             this.height,
		backgroundPosition: 'center center',
		backgroundRepeat:   'no-repeat',
		backgroundColor:    'white',
		backgroundImage:    "url('" + this.getBackBufferImage().path + "')",
		position:           'relative',
		top:                (this.height * -1) + 'px',
		zIndex:             3,
		padding:            '0px',
		margin:             '0px'
	});

	this.activeBuffer = this.bufferOneEl;
}


ImageRotator.prototype.fetchImageList = function() {
	new Ajax.Request(this.url, {
		onSuccess: this.imageListFetched.bind(this)
	});
}

ImageRotator.prototype.imageListFetched = function(res) {
	var json = res.responseJSON;
	this.rotatetime = json.rotatetime;
	this.width      = json.width;
	this.height     = json.height;
	this.transition = json.transition;
	this.imageList  = json.randomplay ? json.images.shuffle() : json.images;

	this.createBuffers();
	this.pauseBeforeAdvancing();
}

ImageRotator.prototype.pauseBeforeAdvancing = function() {
	setTimeout(this.advance.bind(this), this.rotatetime * 1000);
}

ImageRotator.prototype.advance = function() {
	if (this.transition == 'fade') { this.fadeBeforeAdvancing(); }
	else                           { this.advanceInternal(); }
}

ImageRotator.prototype.fadeBeforeAdvancing = function() {
	var anim = new YAHOO.util.Anim(this.activeBuffer, { opacity: { by: -1.0 }}, this.getFadeDuration());
	anim.onComplete.subscribe(this.advanceInternal.bind(this));
	anim.animate();
}

ImageRotator.prototype.advanceInternal = function() {
	this.swapBuffer();
	this.pauseBeforeAdvancing();
	this.updateBackBuffer();
	this.currentIndex = this.getNextIndex();
}

ImageRotator.prototype.getFadeDuration = function() {
	return Math.min(this.rotatetime / 2, 3);
}

ImageRotator.prototype.getNextIndex = function() {
	var nextIndex = ((this.currentIndex + 1) >= this.imageList.size()) ? 0 : this.currentIndex + 1;
	return nextIndex;
}

ImageRotator.prototype.updateBackBuffer = function() {
	var backBuffer =  this.activeBuffer == this.bufferOneEl ? this.bufferTwoEl : this.bufferOneEl;
	backBuffer.setStyle({
		backgroundImage: "url('" + this.getBackBufferImage().path + "')",
		opacity: 1.0,
		filter: 'alpha(opacity=100)'
	});
}

ImageRotator.prototype.getFrontBufferImage = function() {
	return this.imageList[this.currentIndex];
}

ImageRotator.prototype.getBackBufferImage = function() {
	return this.imageList[this.getNextIndex()];
}

ImageRotator.prototype.swapBuffer = function() {
	this.activeBuffer.setStyle({zIndex: 3});
	this.activeBuffer = this.activeBuffer == this.bufferOneEl ? this.bufferTwoEl : this.bufferOneEl;
	this.activeBuffer.setStyle({zIndex: 4});
}
/*** End of file, js_image_rotator.js ***/

/*** Start of file, js_imagepicker.js ***/
DX.ImagePicker = function(elOrId, imgListOrUrl) {

}

DX.ScrollSlider = function(elOrId, scrollSize, config) {
	config = config || {};

	this.element    = $(elOrId);

	this.scrollSize = scrollSize; // størrelsen på området som skal beveges
	this.duration   = config.duration          || 1;  // hvor lenge hver animasjon skal vare
	this.steps      = config.steps             || scrollSize; // antall steg (tilsv. line-height for jevnest mulig scrolling)
	this.delay      = config.delay             || 3;  // antall sekunder mellom hver animasjon (dvs. hvor lenge hver linje blir stående)
	this.automatic  = config.automatic == null || true; // ikke-automatisk "steppig" drives utenfra av kall til step-funksjonen
	this.cyclic     = config.cyclic == null    || true; // sykliske animasjoner krever at innholdet kan dupliseres
	this.firstDelay = config.firstDelay        || 1; // hvis animasjonen skal starte 'onLoad' kan den første animasjonen startes litt tidligere en standard delay
	this.vertical   = config.vertical == null  || true;

	this.stepDelay  = this.duration / this.steps; // antall sekunder mellom hvert steg i animasjonen, f.eks. 0.05
	this.pxPerStep  = this.scrollSize / this.steps; // antall pixler i hvert steg i animasjonen, f.eks. 1
	this.lines      = Math.ceil((this.vertical ? this.element.scrollHeight: this.element.scrollWidth) / this.scrollSize); // antall linjer det skal scrolles mellom

	if (this.cyclic) { this.element.innerHTML = this.element.innerHTML + '<br>' + this.element.innerHTML; } // dupliserer innhold for å ha muligheten til å scrolle _til_ den første linjen
	if (this.automatic) { this.nextStep.bind(this).delay(this.firstDelay, 1, 1); }
}

DX.ScrollSlider.prototype.nextStep = function(line, step) {
	this.currentLine = line; // i tilfelle det ikke er automatisk "stepping"

	var targetScroll = line * this.scrollSize || this.lines * this.scrollSize; // eller-konstruksjonen er der for å kunne "scrolle til" linje 0, dvs. linje 3 -- som er duplisert inn vha. innerHTML-greiene over
	if ((this.vertical ? this.element.scrollTop : this.element.scrollLeft) >= targetScroll) {
		this.vertical ? this.element.scrollTop = line * this.scrollSize : this.element.scrollLeft = line * this.scrollSize;
		if (this.automatic) { this.nextStep.bind(this).delay(this.delay, (line+1) % this.lines, 1); }
		return;
	}
	this.vertical ? this.element.scrollTop = this.element.scrollTop = targetScroll - this.scrollSize + (step * this.pxPerStep) : this.element.scrollLeft = targetScroll - this.scrollSize + (step * this.pxPerStep);
	this.nextStep.bind(this).delay(this.stepDelay, line, step+1);
}

DX.ScrollSlider.prototype.previousStep = function(line, step) { // denne er ikke ferdig
	this.currentLine = line; // i tilfelle det ikke er automatisk "stepping"

	var targetScroll = line * this.scrollSize || this.lines * this.scrollSize; // eller-konstruksjonen er der for å kunne "scrolle til" linje 0, dvs. linje 3 -- som er duplisert inn vha. innerHTML-greiene over
	if ((this.vertical ? this.element.scrollTop : this.element.scrollLeft) >= targetScroll) {
		this.vertical ? this.element.scrollTop = line * this.scrollSize : this.element.scrollLeft = line * this.scrollSize;
		if (this.automatic) { this.nextStep.bind(this).delay(this.delay, (line-1) % this.lines, this.steps); }
		return;
	}
	this.vertical ? this.element.scrollTop = this.element.scrollTop = targetScroll - this.scrollSize + (step * this.pxPerStep) : this.element.scrollLeft = targetScroll - this.scrollSize + (step * this.pxPerStep);
	this.nextStep.bind(this).delay(this.stepDelay, line, step-1);
}
/*** End of file, js_imagepicker.js ***/

