/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.7.0
*/
/**
 * 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
 * @title  YAHOO Global
 */

/**
 * YAHOO_config is not included as part of the library.  Instead it is an 
 * object that can be defined by the implementer immediately before 
 * including the YUI library.  The properties included in this object
 * will be used to configure global properties needed as soon as the 
 * library begins to load.
 * @class YAHOO_config
 * @static
 */

/**
 * A reference to a function that will be executed every time a YAHOO module
 * is loaded.  As parameter, this function will receive the version
 * information for the module. See <a href="YAHOO.env.html#getVersion">
 * YAHOO.env.getVersion</a> for the description of the version data structure.
 * @property listener
 * @type Function
 * @static
 * @default undefined
 */

/**
 * Set to true if the library will be dynamically loaded after window.onload.
 * Defaults to false 
 * @property injecting
 * @type boolean
 * @static
 * @default undefined
 */

/**
 * Instructs the yuiloader component to dynamically load yui components and
 * their dependencies.  See the yuiloader documentation for more information
 * about dynamic loading
 * @property load
 * @static
 * @default undefined
 * @see yuiloader
 */

/**
 * Forces the use of the supplied locale where applicable in the library
 * @property locale
 * @type string
 * @static
 * @default undefined
 */

if (typeof YAHOO == "undefined" || !YAHOO) {
    /**
     * The YAHOO global namespace object.  If YAHOO is already defined, the
     * existing YAHOO object will not be overwritten so that defined
     * namespaces are preserved.
     * @class YAHOO
     * @static
     */
    var YAHOO = {};
}

/**
 * Returns the namespace specified and creates it if it doesn't exist
 * <pre>
 * YAHOO.namespace("property.package");
 * YAHOO.namespace("YAHOO.property.package");
 * </pre>
 * 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:
 * <pre>
 * YAHOO.namespace("really.long.nested.namespace");
 * </pre>
 * This fails because "long" is a future reserved word in ECMAScript
 *
 * For implementation code that uses YUI, do not create your components
 * in the namespaces created by the library.  defined by YUI -- create 
 * your own (YAHOO.util, YAHOO.widget, YAHOO.lang, YAHOO.env)
 *
 * @method namespace
 * @static
 * @param  {String*} arguments 1-n namespaces to create 
 * @return {Object}  A reference to the last namespace object created
 */
YAHOO.namespace = function() {
    var a=arguments, o=null, i, j, d;
    for (i=0; i<a.length; i=i+1) {
        d=(""+a[i]).split(".");
        o=YAHOO;

        // YAHOO is implied, so it is ignored if it is included
        for (j=(d[0] == "YAHOO") ? 1 : 0; j<d.length; j=j+1) {
            o[d[j]]=o[d[j]] || {};
            o=o[d[j]];
        }
    }

    return o;
};

/**
 * Uses YAHOO.widget.Logger to output a log message, if the widget is
 * available.
 *
 * @method log
 * @static
 * @param  {String}  msg  The message to log.
 * @param  {String}  cat  The log category for the message.  Default
 *                        categories are "info", "warn", "error", time".
 *                        Custom categories can be used as well. (opt)
 * @param  {String}  src  The source of the the message (opt)
 * @return {Boolean}      True if the log operation was successful.
 */
YAHOO.log = function(msg, cat, src) {
    var l=YAHOO.widget.Logger;
    if(l && l.log) {
        return l.log(msg, cat, src);
    } else {
        return false;
    }
};

/**
 * Registers a module with the YAHOO object
 * @method register
 * @static
 * @param {String}   name    the name of the module (event, slider, etc)
 * @param {Function} mainClass a reference to class in the module.  This
 *                             class will be tagged with the version info
 *                             so that it will be possible to identify the
 *                             version that is in use when multiple versions
 *                             have loaded
 * @param {Object}   data      metadata object for the module.  Currently it
 *                             is expected to contain a "version" property
 *                             and a "build" property at minimum.
 */
YAHOO.register = function(name, mainClass, data) {
    var mods = YAHOO.env.modules, m, v, b, ls, i;

    if (!mods[name]) {
        mods[name] = { 
            versions:[], 
            builds:[] 
        };
    }

    m  = mods[name];
    v  = data.version;
    b  = data.build;
    ls = YAHOO.env.listeners;

    m.name = name;
    m.version = v;
    m.build = b;
    m.versions.push(v);
    m.builds.push(b);
    m.mainClass = mainClass;

    // fire the module load listeners
    for (i=0;i<ls.length;i=i+1) {
        ls[i](m);
    }
    // label the main class
    if (mainClass) {
        mainClass.VERSION = v;
        mainClass.BUILD = b;
    } else {
        YAHOO.log("mainClass is undefined for module " + name, "warn");
    }
};

/**
 * YAHOO.env is used to keep track of what is known about the YUI library and
 * the browsing environment
 * @class YAHOO.env
 * @static
 */
YAHOO.env = YAHOO.env || {

    /**
     * Keeps the version info for all YUI modules that have reported themselves
     * @property modules
     * @type Object[]
     */
    modules: [],
    
    /**
     * List of functions that should be executed every time a YUI module
     * reports itself.
     * @property listeners
     * @type Function[]
     */
    listeners: []
};

/**
 * Returns the version data for the specified module:
 *      <dl>
 *      <dt>name:</dt>      <dd>The name of the module</dd>
 *      <dt>version:</dt>   <dd>The version in use</dd>
 *      <dt>build:</dt>     <dd>The build number in use</dd>
 *      <dt>versions:</dt>  <dd>All versions that were registered</dd>
 *      <dt>builds:</dt>    <dd>All builds that were registered.</dd>
 *      <dt>mainClass:</dt> <dd>An object that was was stamped with the
 *                 current version and build. If 
 *                 mainClass.VERSION != version or mainClass.BUILD != build,
 *                 multiple versions of pieces of the library have been
 *                 loaded, potentially causing issues.</dd>
 *       </dl>
 *
 * @method getVersion
 * @static
 * @param {String}  name the name of the module (event, slider, etc)
 * @return {Object} The version info
 */
YAHOO.env.getVersion = function(name) {
    return YAHOO.env.modules[name] || null;
};

/**
 * Do not fork for a browser if it can be avoided.  Use feature detection when
 * you can.  Use the user agent as a last resort.  YAHOO.env.ua stores a version
 * number for the browser engine, 0 otherwise.  This value may or may not map
 * to the version number of the browser using the engine.  The value is 
 * presented as a float so that it can easily be used for boolean evaluation 
 * as well as for looking for a particular range of versions.  Because of this, 
 * some of the granularity of the version info may be lost (e.g., Gecko 1.8.0.9 
 * reports 1.8).
 * @class YAHOO.env.ua
 * @static
 */
YAHOO.env.ua = function() {
    var o={

        /**
         * Internet Explorer version number or 0.  Example: 6
         * @property ie
         * @type float
         */
        ie:0,

        /**
         * Opera version number or 0.  Example: 9.2
         * @property opera
         * @type float
         */
        opera:0,

        /**
         * Gecko engine revision number.  Will evaluate to 1 if Gecko 
         * is detected but the revision could not be found. Other browsers
         * will be 0.  Example: 1.8
         * <pre>
         * Firefox 1.0.0.4: 1.7.8   <-- Reports 1.7
         * Firefox 1.5.0.9: 1.8.0.9 <-- Reports 1.8
         * Firefox 2.0.0.3: 1.8.1.3 <-- Reports 1.8
         * Firefox 3 alpha: 1.9a4   <-- Reports 1.9
         * </pre>
         * @property gecko
         * @type float
         */
        gecko:0,

        /**
         * AppleWebKit version.  KHTML browsers that are not WebKit browsers 
         * will evaluate to 1, other browsers 0.  Example: 418.9.1
         * <pre>
         * Safari 1.3.2 (312.6): 312.8.1 <-- Reports 312.8 -- currently the 
         *                                   latest available for Mac OSX 10.3.
         * Safari 2.0.2:         416     <-- hasOwnProperty introduced
         * Safari 2.0.4:         418     <-- preventDefault fixed
         * Safari 2.0.4 (419.3): 418.9.1 <-- One version of Safari may run
         *                                   different versions of webkit
         * Safari 2.0.4 (419.3): 419     <-- Tiger installations that have been
         *                                   updated, but not updated
         *                                   to the latest patch.
         * Webkit 212 nightly:   522+    <-- Safari 3.0 precursor (with native SVG
         *                                   and many major issues fixed).  
         * 3.x yahoo.com, flickr:422     <-- Safari 3.x hacks the user agent
         *                                   string when hitting yahoo.com and 
         *                                   flickr.com.
         * Safari 3.0.4 (523.12):523.12  <-- First Tiger release - automatic update
         *                                   from 2.x via the 10.4.11 OS patch
         * Webkit nightly 1/2008:525+    <-- Supports DOMContentLoaded event.
         *                                   yahoo.com user agent hack removed.
         *                                   
         * </pre>
         * http://developer.apple.com/internet/safari/uamatrix.html
         * @property webkit
         * @type float
         */
        webkit: 0,

        /**
         * The mobile property will be set to a string containing any relevant
         * user agent information when a modern mobile browser is detected.
         * Currently limited to Safari on the iPhone/iPod Touch, Nokia N-series
         * devices with the WebKit-based browser, and Opera Mini.  
         * @property mobile 
         * @type string
         */
        mobile: null,

        /**
         * Adobe AIR version number or 0.  Only populated if webkit is detected.
         * Example: 1.0
         * @property air
         * @type float
         */
        air: 0,

        /**
         * Google Caja version number or 0.
         * @property caja
         * @type float
         */
        caja: 0

    },

    ua = navigator.userAgent, 
    
    m;

    // Modern KHTML browsers should qualify as Safari X-Grade
    if ((/KHTML/).test(ua)) {
        o.webkit=1;
    }
    // Modern WebKit browsers are at least X-Grade
    m=ua.match(/AppleWebKit\/([^\s]*)/);
    if (m&&m[1]) {
        o.webkit=parseFloat(m[1]);

        // Mobile browser check
        if (/ Mobile\//.test(ua)) {
            o.mobile = "Apple"; // iPhone or iPod Touch
        } else {
            m=ua.match(/NokiaN[^\/]*/);
            if (m) {
                o.mobile = m[0]; // Nokia N-series, ex: NokiaN95
            }
        }

        m=ua.match(/AdobeAIR\/([^\s]*)/);
        if (m) {
            o.air = m[0]; // Adobe AIR 1.0 or better
        }

    }

    if (!o.webkit) { // not webkit
        // @todo check Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4509/1316; fi; U; ssr)
        m=ua.match(/Opera[\s\/]([^\s]*)/);
        if (m&&m[1]) {
            o.opera=parseFloat(m[1]);
            m=ua.match(/Opera Mini[^;]*/);
            if (m) {
                o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316
            }
        } else { // not opera or webkit
            m=ua.match(/MSIE\s([^;]*)/);
            if (m&&m[1]) {
                o.ie=parseFloat(m[1]);
            } else { // not opera, webkit, or ie
                m=ua.match(/Gecko\/([^\s]*)/);
                if (m) {
                    o.gecko=1; // Gecko detected, look for revision
                    m=ua.match(/rv:([^\s\)]*)/);
                    if (m&&m[1]) {
                        o.gecko=parseFloat(m[1]);
                    }
                }
            }
        }
    }

    m=ua.match(/Caja\/([^\s]*)/);
    if (m&&m[1]) {
        o.caja=parseFloat(m[1]);
    }
    
    return o;
}();

/*
 * Initializes the global by creating the default namespaces and applying
 * any new configuration information that is detected.  This is the setup
 * for env.
 * @method init
 * @static
 * @private
 */
(function() {
    YAHOO.namespace("util", "widget", "example");
    /*global YAHOO_config*/
    if ("undefined" !== typeof YAHOO_config) {
        var l=YAHOO_config.listener,ls=YAHOO.env.listeners,unique=true,i;
        if (l) {
            // if YAHOO is loaded multiple times we need to check to see if
            // this is a new config object.  If it is, add the new component
            // load listener to the stack
            for (i=0;i<ls.length;i=i+1) {
                if (ls[i]==l) {
                    unique=false;
                    break;
                }
            }
            if (unique) {
                ls.push(l);
            }
        }
    }
})();
/**
 * Provides the language utilites and extensions used by the library
 * @class YAHOO.lang
 */
YAHOO.lang = YAHOO.lang || {};

(function() {


var L = YAHOO.lang,

    ARRAY_TOSTRING = '[object Array]',
    FUNCTION_TOSTRING = '[object Function]',
    OP = Object.prototype,

    // ADD = ["toString", "valueOf", "hasOwnProperty"],
    ADD = ["toString", "valueOf"],

    OB = {

    /**
     * Determines wheather or not the provided object is an array.
     * @method isArray
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isArray: function(o) { 
        return OP.toString.apply(o) === ARRAY_TOSTRING;
    },

    /**
     * Determines whether or not the provided object is a boolean
     * @method isBoolean
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isBoolean: function(o) {
        return typeof o === 'boolean';
    },
    
    /**
     * Determines whether or not the provided object is a function.
     * Note: Internet Explorer thinks certain functions are objects:
     *
     * var obj = document.createElement("object");
     * YAHOO.lang.isFunction(obj.getAttribute) // reports false in IE
     *
     * var input = document.createElement("input"); // append to body
     * YAHOO.lang.isFunction(input.focus) // reports false in IE
     *
     * You will have to implement additional tests if these functions
     * matter to you.
     *
     * @method isFunction
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isFunction: function(o) {
        return OP.toString.apply(o) === FUNCTION_TOSTRING;
    },
        
    /**
     * Determines whether or not the provided object is null
     * @method isNull
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isNull: function(o) {
        return o === null;
    },
        
    /**
     * Determines whether or not the provided object is a legal number
     * @method isNumber
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isNumber: function(o) {
        return typeof o === 'number' && isFinite(o);
    },
      
    /**
     * Determines whether or not the provided object is of type object
     * or function
     * @method isObject
     * @param {any} o The object being testing
     * @return {boolean} the result
     */  
    isObject: function(o) {
return (o && (typeof o === 'object' || L.isFunction(o))) || false;
    },
        
    /**
     * Determines whether or not the provided object is a string
     * @method isString
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isString: function(o) {
        return typeof o === 'string';
    },
        
    /**
     * Determines whether or not the provided object is undefined
     * @method isUndefined
     * @param {any} o The object being testing
     * @return {boolean} the result
     */
    isUndefined: function(o) {
        return typeof o === 'undefined';
    },
    
 
    /**
     * IE will not enumerate native functions in a derived object even if the
     * function was overridden.  This is a workaround for specific functions 
     * we care about on the Object prototype. 
     * @property _IEEnumFix
     * @param {Function} r  the object to receive the augmentation
     * @param {Function} s  the object that supplies the properties to augment
     * @static
     * @private
     */
    _IEEnumFix: (YAHOO.env.ua.ie) ? function(r, s) {
            var i, fname, f;
            for (i=0;i<ADD.length;i=i+1) {

                fname = ADD[i];
                f = s[fname];

                if (L.isFunction(f) && f!=OP[fname]) {
                    r[fname]=f;
                }
            }
    } : function(){},
       
    /**
     * Utility to set up the prototype, constructor and superclass properties to
     * support an inheritance strategy that can chain constructors and methods.
     * Static members will not be inherited.
     *
     * @method extend
     * @static
     * @param {Function} subc   the object to modify
     * @param {Function} superc the object to inherit
     * @param {Object} overrides  additional properties/methods to add to the
     *                              subclass prototype.  These will override the
     *                              matching items obtained from the superclass 
     *                              if present.
     */
    extend: function(subc, superc, overrides) {
        if (!superc||!subc) {
            throw new Error("extend failed, please check that " +
                            "all dependencies are included.");
        }
        var F = function() {}, i;
        F.prototype=superc.prototype;
        subc.prototype=new F();
        subc.prototype.constructor=subc;
        subc.superclass=superc.prototype;
        if (superc.prototype.constructor == OP.constructor) {
            superc.prototype.constructor=superc;
        }
    
        if (overrides) {
            for (i in overrides) {
                if (L.hasOwnProperty(overrides, i)) {
                    subc.prototype[i]=overrides[i];
                }
            }

            L._IEEnumFix(subc.prototype, overrides);
        }
    },
   
    /**
     * Applies all properties in the supplier to the receiver if the
     * receiver does not have these properties yet.  Optionally, one or 
     * more methods/properties can be specified (as additional 
     * parameters).  This option will overwrite the property if receiver 
     * has it already.  If true is passed as the third parameter, all 
     * properties will be applied and _will_ overwrite properties in 
     * the receiver.
     *
     * @method augmentObject
     * @static
     * @since 2.3.0
     * @param {Function} r  the object to receive the augmentation
     * @param {Function} s  the object that supplies the properties to augment
     * @param {String*|boolean}  arguments zero or more properties methods 
     *        to augment the receiver with.  If none specified, everything
     *        in the supplier will be used unless it would
     *        overwrite an existing property in the receiver. If true
     *        is specified as the third parameter, all properties will
     *        be applied and will overwrite an existing property in
     *        the receiver
     */
    augmentObject: function(r, s) {
        if (!s||!r) {
            throw new Error("Absorb failed, verify dependencies.");
        }
        var a=arguments, i, p, overrideList=a[2];
        if (overrideList && overrideList!==true) { // only absorb the specified properties
            for (i=2; i<a.length; i=i+1) {
                r[a[i]] = s[a[i]];
            }
        } else { // take everything, overwriting only if the third parameter is true
            for (p in s) { 
                if (overrideList || !(p in r)) {
                    r[p] = s[p];
                }
            }
            
            L._IEEnumFix(r, s);
        }
    },
 
    /**
     * Same as YAHOO.lang.augmentObject, except it only applies prototype properties
     * @see YAHOO.lang.augmentObject
     * @method augmentProto
     * @static
     * @param {Function} r  the object to receive the augmentation
     * @param {Function} s  the object that supplies the properties to augment
     * @param {String*|boolean}  arguments zero or more properties methods 
     *        to augment the receiver with.  If none specified, everything 
     *        in the supplier will be used unless it would overwrite an existing 
     *        property in the receiver.  if true is specified as the third 
     *        parameter, all properties will be applied and will overwrite an 
     *        existing property in the receiver
     */
    augmentProto: function(r, s) {
        if (!s||!r) {
            throw new Error("Augment failed, verify dependencies.");
        }
        //var a=[].concat(arguments);
        var a=[r.prototype,s.prototype], i;
        for (i=2;i<arguments.length;i=i+1) {
            a.push(arguments[i]);
        }
        L.augmentObject.apply(this, a);
    },

      
    /**
     * Returns a simple string representation of the object or array.
     * Other types of objects will be returned unprocessed.  Arrays
     * are expected to be indexed.  Use object notation for
     * associative arrays.
     * @method dump
     * @since 2.3.0
     * @param o {Object} The object to dump
     * @param d {int} How deep to recurse child objects, default 3
     * @return {String} the dump result
     */
    dump: function(o, d) {
        var i,len,s=[],OBJ="{...}",FUN="f(){...}",
            COMMA=', ', ARROW=' => ';

        // Cast non-objects to string
        // Skip dates because the std toString is what we want
        // Skip HTMLElement-like objects because trying to dump 
        // an element will cause an unhandled exception in FF 2.x
        if (!L.isObject(o)) {
            return o + "";
        } else if (o instanceof Date || ("nodeType" in o && "tagName" in o)) {
            return o;
        } else if  (L.isFunction(o)) {
            return FUN;
        }

        // dig into child objects the depth specifed. Default 3
        d = (L.isNumber(d)) ? d : 3;

        // arrays [1, 2, 3]
        if (L.isArray(o)) {
            s.push("[");
            for (i=0,len=o.length;i<len;i=i+1) {
                if (L.isObject(o[i])) {
                    s.push((d > 0) ? L.dump(o[i], d-1) : OBJ);
                } else {
                    s.push(o[i]);
                }
                s.push(COMMA);
            }
            if (s.length > 1) {
                s.pop();
            }
            s.push("]");
        // objects {k1 => v1, k2 => v2}
        } else {
            s.push("{");
            for (i in o) {
                if (L.hasOwnProperty(o, i)) {
                    s.push(i + ARROW);
                    if (L.isObject(o[i])) {
                        s.push((d > 0) ? L.dump(o[i], d-1) : OBJ);
                    } else {
                        s.push(o[i]);
                    }
                    s.push(COMMA);
                }
            }
            if (s.length > 1) {
                s.pop();
            }
            s.push("}");
        }

        return s.join("");
    },

    /**
     * Does variable substitution on a string. It scans through the string 
     * looking for expressions enclosed in { } braces. If an expression 
     * is found, it is used a key on the object.  If there is a space in
     * the key, the first word is used for the key and the rest is provided
     * to an optional function to be used to programatically determine the
     * value (the extra information might be used for this decision). If 
     * the value for the key in the object, or what is returned from the
     * function has a string value, number value, or object value, it is 
     * substituted for the bracket expression and it repeats.  If this
     * value is an object, it uses the Object's toString() if this has
     * been overridden, otherwise it does a shallow dump of the key/value
     * pairs.
     * @method substitute
     * @since 2.3.0
     * @param s {String} The string that will be modified.
     * @param o {Object} An object containing the replacement values
     * @param f {Function} An optional function that can be used to
     *                     process each match.  It receives the key,
     *                     value, and any extra metadata included with
     *                     the key inside of the braces.
     * @return {String} the substituted string
     */
    substitute: function (s, o, f) {
        var i, j, k, key, v, meta, saved=[], token, 
            DUMP='dump', SPACE=' ', LBRACE='{', RBRACE='}',
            dump;


        for (;;) {
            i = s.lastIndexOf(LBRACE);
            if (i < 0) {
                break;
            }
            j = s.indexOf(RBRACE, i);
            if (i + 1 >= j) {
                break;
            }

            //Extract key and meta info 
            token = s.substring(i + 1, j);
            key = token;
            meta = null;
            k = key.indexOf(SPACE);
            if (k > -1) {
                meta = key.substring(k + 1);
                key = key.substring(0, k);
            }

            // lookup the value
            v = o[key];

            // if a substitution function was provided, execute it
            if (f) {
                v = f(key, v, meta);
            }

            if (L.isObject(v)) {
                if (L.isArray(v)) {
                    v = L.dump(v, parseInt(meta, 10));
                } else {
                    meta = meta || "";

                    // look for the keyword 'dump', if found force obj dump
                    dump = meta.indexOf(DUMP);
                    if (dump > -1) {
                        meta = meta.substring(4);
                    }

                    // use the toString if it is not the Object toString 
                    // and the 'dump' meta info was not found
                    if (v.toString===OP.toString || dump>-1) {
                        v = L.dump(v, parseInt(meta, 10));
                    } else {
                        v = v.toString();
                    }
                }
            } else if (!L.isString(v) && !L.isNumber(v)) {
                // This {block} has no replace string. Save it for later.
                v = "~-" + saved.length + "-~";
                saved[saved.length] = token;

                // break;
            }

            s = s.substring(0, i) + v + s.substring(j + 1);


        }

        // restore saved {block}s
        for (i=saved.length-1; i>=0; i=i-1) {
            s = s.replace(new RegExp("~-" + i + "-~"), "{"  + saved[i] + "}", "g");
        }

        return s;
    },


    /**
     * Returns a string without any leading or trailing whitespace.  If 
     * the input is not a string, the input will be returned untouched.
     * @method trim
     * @since 2.3.0
     * @param s {string} the string to trim
     * @return {string} the trimmed string
     */
    trim: function(s){
        try {
            return s.replace(/^\s+|\s+$/g, "");
        } catch(e) {
            return s;
        }
    },

    /**
     * Returns a new object containing all of the properties of
     * all the supplied objects.  The properties from later objects
     * will overwrite those in earlier objects.
     * @method merge
     * @since 2.3.0
     * @param arguments {Object*} the objects to merge
     * @return the new merged object
     */
    merge: function() {
        var o={}, a=arguments, l=a.length, i;
        for (i=0; i<l; i=i+1) {
            L.augmentObject(o, a[i], true);
        }
        return o;
    },

    /**
     * Executes the supplied function in the context of the supplied 
     * object 'when' milliseconds later.  Executes the function a 
     * single time unless periodic is set to true.
     * @method later
     * @since 2.4.0
     * @param when {int} the number of milliseconds to wait until the fn 
     * is executed
     * @param o the context object
     * @param fn {Function|String} the function to execute or the name of 
     * the method in the 'o' object to execute
     * @param data [Array] data that is provided to the function.  This accepts
     * either a single item or an array.  If an array is provided, the
     * function is executed with one parameter for each array item.  If
     * you need to pass a single array parameter, it needs to be wrapped in
     * an array [myarray]
     * @param periodic {boolean} if true, executes continuously at supplied 
     * interval until canceled
     * @return a timer object. Call the cancel() method on this object to 
     * stop the timer.
     */
    later: function(when, o, fn, data, periodic) {
        when = when || 0; 
        o = o || {};
        var m=fn, d=data, f, r;

        if (L.isString(fn)) {
            m = o[fn];
        }

        if (!m) {
            throw new TypeError("method undefined");
        }

        if (!L.isArray(d)) {
            d = [data];
        }

        f = function() {
            m.apply(o, d);
        };

        r = (periodic) ? setInterval(f, when) : setTimeout(f, when);

        return {
            interval: periodic,
            cancel: function() {
                if (this.interval) {
                    clearInterval(r);
                } else {
                    clearTimeout(r);
                }
            }
        };
    },
    
    /**
     * A convenience method for detecting a legitimate non-null value.
     * Returns false for null/undefined/NaN, true for other values, 
     * including 0/false/''
     * @method isValue
     * @since 2.3.0
     * @param o {any} the item to test
     * @return {boolean} true if it is not null/undefined/NaN || false
     */
    isValue: function(o) {
        // return (o || o === false || o === 0 || o === ''); // Infinity fails
return (L.isObject(o) || L.isString(o) || L.isNumber(o) || L.isBoolean(o));
    }

};

/**
 * Determines whether or not the property was added
 * to the object instance.  Returns false if the property is not present
 * in the object, or was inherited from the prototype.
 * This abstraction is provided to enable hasOwnProperty for Safari 1.3.x.
 * There is a discrepancy between YAHOO.lang.hasOwnProperty and
 * Object.prototype.hasOwnProperty when the property is a primitive added to
 * both the instance AND prototype with the same value:
 * <pre>
 * var A = function() {};
 * A.prototype.foo = 'foo';
 * var a = new A();
 * a.foo = 'foo';
 * alert(a.hasOwnProperty('foo')); // true
 * alert(YAHOO.lang.hasOwnProperty(a, 'foo')); // false when using fallback
 * </pre>
 * @method hasOwnProperty
 * @param {any} o The object being testing
 * @param prop {string} the name of the property to test
 * @return {boolean} the result
 */
L.hasOwnProperty = (OP.hasOwnProperty) ?
    function(o, prop) {
        return o && o.hasOwnProperty(prop);
    } : function(o, prop) {
        return !L.isUndefined(o[prop]) && 
                o.constructor.prototype[prop] !== o[prop];
    };

// new lang wins
OB.augmentObject(L, OB, true);

/*
 * An alias for <a href="YAHOO.lang.html">YAHOO.lang</a>
 * @class YAHOO.util.Lang
 */
YAHOO.util.Lang = L;
 
/**
 * Same as YAHOO.lang.augmentObject, except it only applies prototype 
 * properties.  This is an alias for augmentProto.
 * @see YAHOO.lang.augmentObject
 * @method augment
 * @static
 * @param {Function} r  the object to receive the augmentation
 * @param {Function} s  the object that supplies the properties to augment
 * @param {String*|boolean}  arguments zero or more properties methods to 
 *        augment the receiver with.  If none specified, everything
 *        in the supplier will be used unless it would
 *        overwrite an existing property in the receiver.  if true
 *        is specified as the third parameter, all properties will
 *        be applied and will overwrite an existing property in
 *        the receiver
 */
L.augment = L.augmentProto;

/**
 * An alias for <a href="YAHOO.lang.html#augment">YAHOO.lang.augment</a>
 * @for YAHOO
 * @method augment
 * @static
 * @param {Function} r  the object to receive the augmentation
 * @param {Function} s  the object that supplies the properties to augment
 * @param {String*}  arguments zero or more properties methods to 
 *        augment the receiver with.  If none specified, everything
 *        in the supplier will be used unless it would
 *        overwrite an existing property in the receiver
 */
YAHOO.augment = L.augmentProto;
       
/**
 * An alias for <a href="YAHOO.lang.html#extend">YAHOO.lang.extend</a>
 * @method extend
 * @static
 * @param {Function} subc   the object to modify
 * @param {Function} superc the object to inherit
 * @param {Object} overrides  additional properties/methods to add to the
 *        subclass prototype.  These will override the
 *        matching items obtained from the superclass if present.
 */
YAHOO.extend = L.extend;

})();
YAHOO.register("yahoo", YAHOO, {version: "2.7.0", build: "1799"});
/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.7.0
*/

/**
 * 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}  context 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 debugsystem
 * @param {int}     signature the signature that the custom event subscriber
 *                  will receive. YAHOO.util.CustomEvent.LIST or 
 *                  YAHOO.util.CustomEvent.FLAT.  The default is
 *                  YAHOO.util.CustomEvent.LIST.
 * @namespace YAHOO.util
 * @class CustomEvent
 * @constructor
 */
YAHOO.util.CustomEvent = function(type, context, silent, signature) {

    /**
     * The type of event, returned to subscribers when the event fires
     * @property type
     * @type string
     */
    this.type = type;

    /**
     * The context the the event will fire from by default.  Defaults to the window 
     * obj
     * @property scope
     * @type object
     */
    this.scope = context || window;

    /**
     * By default all custom events are logged in the debug build, set silent
     * to true to disable debug outpu for this event.
     * @property silent
     * @type boolean
     */
    this.silent = silent;

    /**
     * Custom events support two styles of arguments provided to the event
     * subscribers.  
     * <ul>
     * <li>YAHOO.util.CustomEvent.LIST: 
     *   <ul>
     *   <li>param1: event name</li>
     *   <li>param2: array of arguments sent to fire</li>
     *   <li>param3: <optional> a custom object supplied by the subscriber</li>
     *   </ul>
     * </li>
     * <li>YAHOO.util.CustomEvent.FLAT
     *   <ul>
     *   <li>param1: the first argument passed to fire.  If you need to
     *           pass multiple parameters, use and array or object literal</li>
     *   <li>param2: <optional> a custom object supplied by the subscriber</li>
     *   </ul>
     * </li>
     * </ul>
     *   @property signature
     *   @type int
     */
    this.signature = signature || YAHOO.util.CustomEvent.LIST;

    /**
     * The subscribers to this event
     * @property subscribers
     * @type Subscriber[]
     */
    this.subscribers = [];

    if (!this.silent) {
    }

    var onsubscribeType = "_YUICEOnSubscribe";

    // Only add subscribe events for events that are not generated by 
    // CustomEvent
    if (type !== onsubscribeType) {

        /**
         * 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.  
         *
         * @event subscribeEvent
         * @type YAHOO.util.CustomEvent
         * @param {Function} fn The function to execute
         * @param {Object}   obj An object to be passed along when the event 
         *                       fires defaults to the custom event
         * @param {boolean|Object}  override If true, the obj passed in becomes 
         *                                   the execution context of the listener.
         *                                   if an object, that object becomes the
         *                                   the execution context. defaults to
         *                                   the custom event
         */
        this.subscribeEvent = 
                new YAHOO.util.CustomEvent(onsubscribeType, this, true);

    } 


    /**
     * In order to make it possible to execute the rest of the subscriber
     * stack when one thows an exception, the subscribers exceptions are
     * caught.  The most recent exception is stored in this property
     * @property lastError
     * @type Error
     */
    this.lastError = null;
};

/**
 * Subscriber listener sigature constant.  The LIST type returns three
 * parameters: the event type, the array of args passed to fire, and
 * the optional custom object
 * @property YAHOO.util.CustomEvent.LIST
 * @static
 * @type int
 */
YAHOO.util.CustomEvent.LIST = 0;

/**
 * Subscriber listener sigature constant.  The FLAT type returns two
 * parameters: the first argument passed to fire and the optional 
 * custom object
 * @property YAHOO.util.CustomEvent.FLAT
 * @static
 * @type int
 */
YAHOO.util.CustomEvent.FLAT = 1;

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|Object}  overrideContext If true, the obj passed in becomes 
     *                                   the execution context of the listener.
     *                                   if an object, that object becomes the
     *                                   the execution context.
     */
    subscribe: function(fn, obj, overrideContext) {

        if (!fn) {
throw new Error("Invalid callback for subscriber to '" + this.type + "'");
        }

        if (this.subscribeEvent) {
            this.subscribeEvent.fire(fn, obj, overrideContext);
        }

        this.subscribers.push( new YAHOO.util.Subscriber(fn, obj, overrideContext) );
    },

    /**
     * Unsubscribes subscribers.
     * @method unsubscribe
     * @param {Function} fn  The subscribed function to remove, if not supplied
     *                       all will be removed
     * @param {Object}   obj  The custom object passed to subscribe.  This is
     *                        optional, but if supplied will be used to
     *                        disambiguate multiple listeners that are the same
     *                        (e.g., you subscribe many object using a function
     *                        that lives on the prototype)
     * @return {boolean} True if the subscriber was found and detached.
     */
    unsubscribe: function(fn, obj) {

        if (!fn) {
            return this.unsubscribeAll();
        }

        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 context specified when the event was created, and with the 
     * following parameters:
     *   <ul>
     *   <li>The type of event</li>
     *   <li>All of the arguments fire() was executed with as an array</li>
     *   <li>The custom object (if any) that was passed into the subscribe() 
     *       method</li>
     *   </ul>
     * @method fire 
     * @param {Object*} arguments an arbitrary set of parameters to pass to 
     *                            the handler.
     * @return {boolean} false if one of the subscribers returned false, 
     *                   true otherwise
     */
    fire: function() {

        this.lastError = null;

        var errors = [],
            len=this.subscribers.length;

        if (!len && this.silent) {
            return true;
        }

        var args=[].slice.call(arguments, 0), ret=true, i, rebuild=false;

        if (!this.silent) {
        }

        // make a copy of the subscribers so that there are
        // no index problems if one subscriber removes another.
        var subs = this.subscribers.slice(), throwErrors = YAHOO.util.Event.throwErrors;

        for (i=0; i<len; ++i) {
            var s = subs[i];
            if (!s) {
                rebuild=true;
            } else {
                if (!this.silent) {
                }

                var scope = s.getScope(this.scope);

                if (this.signature == YAHOO.util.CustomEvent.FLAT) {
                    var param = null;
                    if (args.length > 0) {
                        param = args[0];
                    }

                    try {
                        ret = s.fn.call(scope, param, s.obj);
                    } catch(e) {
                        this.lastError = e;
                        // errors.push(e);
                        if (throwErrors) {
                            throw e;
                        }
                    }
                } else {
                    try {
                        ret = s.fn.call(scope, this.type, args, s.obj);
                    } catch(ex) {
                        this.lastError = ex;
                        if (throwErrors) {
                            throw ex;
                        }
                    }
                }

                if (false === ret) {
                    if (!this.silent) {
                    }

                    break;
                    // return false;
                }
            }
        }

        return (ret !== false);
    },

    /**
     * Removes all listeners
     * @method unsubscribeAll
     * @return {int} The number of listeners unsubscribed
     */
    unsubscribeAll: function() {
        var l = this.subscribers.length, i;
        for (i=l-1; i>-1; i--) {
            this._delete(i);
        }

        this.subscribers=[];

        return l;
    },

    /**
     * @method _delete
     * @private
     */
    _delete: function(index) {
        var s = this.subscribers[index];
        if (s) {
            delete s.fn;
            delete s.obj;
        }

        // this.subscribers[index]=null;
        this.subscribers.splice(index, 1);
    },

    /**
     * @method toString
     */
    toString: function() {
         return "CustomEvent: " + "'" + this.type  + "', " + 
             "context: " + 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}  overrideContext If true, the obj passed in becomes the execution
 *                            context of the listener
 * @class Subscriber
 * @constructor
 */
YAHOO.util.Subscriber = function(fn, obj, overrideContext) {

    /**
     * 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 = YAHOO.lang.isUndefined(obj) ? null : obj;

    /**
     * The default execution context for the event listener is defined when the
     * event is created (usually the object which contains the event).
     * By setting overrideContext to true, the execution context becomes the custom
     * object passed in by the subscriber.  If overrideContext is an object, that 
     * object becomes the context.
     * @property overrideContext
     * @type boolean|object
     */
    this.overrideContext = overrideContext;

};

/**
 * Returns the execution context for this listener.  If overrideContext was set to true
 * the custom obj will be the context.  If overrideContext is an object, that is the
 * context, otherwise the default context will be used.
 * @method getScope
 * @param {Object} defaultScope the context to use if this listener does not
 *                              override it.
 */
YAHOO.util.Subscriber.prototype.getScope = function(defaultScope) {
    if (this.overrideContext) {
        if (this.overrideContext === true) {
            return this.obj;
        } else {
            return this.overrideContext;
        }
    }
    return defaultScope;
};

/**
 * 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) {
    if (obj) {
        return (this.fn == fn && this.obj == obj);
    } else {
        return (this.fn == fn);
    }
};

/**
 * @method toString
 */
YAHOO.util.Subscriber.prototype.toString = function() {
    return "Subscriber { obj: " + this.obj  + 
           ", overrideContext: " +  (this.overrideContext || "no") + " }";
};

/**
 * The Event Utility provides utilities for managing DOM Events and tools
 * for building event systems
 *
 * @module event
 * @title Event Utility
 * @namespace YAHOO.util
 * @requires yahoo
 */

// The first instance of Event will win if it is loaded more than once.
// @TODO this needs to be changed so that only the state data that needs to
// be preserved is kept, while methods are overwritten/added as needed.
// This means that the module pattern can't be used.
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.
 *
 * @class Event
 * @static
 */
    YAHOO.util.Event = function() {

        /**
         * True after the onload event has fired
         * @property loadComplete
         * @type boolean
         * @static
         * @private
         */
        var loadComplete =  false;

        /**
         * Cache of wrapped listeners
         * @property listeners
         * @type array
         * @static
         * @private
         */
        var listeners = [];

        /**
         * User-defined unload function that will be fired before all events
         * are detached
         * @property unloadListeners
         * @type array
         * @static
         * @private
         */
        var unloadListeners = [];

        /**
         * Cache of DOM0 event handlers to work around issues with DOM2 events
         * in Safari
         * @property legacyEvents
         * @static
         * @private
         */
        var legacyEvents = [];

        /**
         * Listener stack for DOM0 events
         * @property legacyHandlers
         * @static
         * @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
         * @static
         * @private
         */
        var retryCount = 0;

        /**
         * onAvailable listeners
         * @property onAvailStack
         * @static
         * @private
         */
        var onAvailStack = [];

        /**
         * Lookup table for legacy events
         * @property legacyMap
         * @static
         * @private
         */
        var legacyMap = [];

        /**
         * Counter for auto id generation
         * @property counter
         * @static
         * @private
         */
        var counter = 0;
        
        /**
         * Normalized keycodes for webkit/safari
         * @property webkitKeymap
         * @type {int: int}
         * @private
         * @static
         * @final
         */
        var webkitKeymap = {
            63232: 38, // up
            63233: 40, // down
            63234: 37, // left
            63235: 39, // right
            63276: 33, // page up
            63277: 34, // page down
            25: 9      // SHIFT-TAB (Safari provides a different key code in
                       // this case, even though the shiftKey modifier is set)
        };
        
        // String constants used by the addFocusListener and removeFocusListener methods
        var _FOCUS = YAHOO.env.ua.ie ? "focusin" : "focus";
        var _BLUR = YAHOO.env.ua.ie ? "focusout" : "blur";      

        return {

            /**
             * 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 2000@amp;20 ms, so it will poll
             * for 40 seconds or until all outstanding handlers are bound
             * (whichever comes first).
             * @property POLL_RETRYS
             * @type int
             * @static
             * @final
             */
            POLL_RETRYS: 2000,

            /**
             * The poll interval in milliseconds
             * @property POLL_INTERVAL
             * @type int
             * @static
             * @final
             */
            POLL_INTERVAL: 20,

            /**
             * Element to bind, int constant
             * @property EL
             * @type int
             * @static
             * @final
             */
            EL: 0,

            /**
             * Type of event, int constant
             * @property TYPE
             * @type int
             * @static
             * @final
             */
            TYPE: 1,

            /**
             * Function to execute, int constant
             * @property FN
             * @type int
             * @static
             * @final
             */
            FN: 2,

            /**
             * Function wrapped for context correction and cleanup, int constant
             * @property WFN
             * @type int
             * @static
             * @final
             */
            WFN: 3,

            /**
             * Object passed in by the user that will be returned as a 
             * parameter to the callback, int constant.  Specific to
             * unload listeners
             * @property OBJ
             * @type int
             * @static
             * @final
             */
            UNLOAD_OBJ: 3,

            /**
             * Adjusted context, 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
             * @static
             * @final
             */
            ADJ_SCOPE: 4,

            /**
             * The original obj passed into addListener
             * @property OBJ
             * @type int
             * @static
             * @final
             */
            OBJ: 5,

            /**
             * The original context parameter passed into addListener
             * @property OVERRIDE
             * @type int
             * @static
             * @final
             */
            OVERRIDE: 6,

            /**
             * addListener/removeListener can throw errors in unexpected scenarios.
             * These errors are suppressed, the method returns false, and this property
             * is set
             * @property lastError
             * @static
             * @type Error
             */
            lastError: null,

            /**
             * Safari detection
             * @property isSafari
             * @private
             * @static
             * @deprecated use YAHOO.env.ua.webkit
             */
            isSafari: YAHOO.env.ua.webkit,
            
            /**
             * webkit version
             * @property webkit
             * @type string
             * @private
             * @static
             * @deprecated use YAHOO.env.ua.webkit
             */
            webkit: YAHOO.env.ua.webkit,
            
            /**
             * IE detection 
             * @property isIE
             * @private
             * @static
             * @deprecated use YAHOO.env.ua.ie
             */
            isIE: YAHOO.env.ua.ie,

            /**
             * poll handle
             * @property _interval
             * @static
             * @private
             */
            _interval: null,

            /**
             * document readystate poll handle
             * @property _dri
             * @static
             * @private
             */
             _dri: null,

            /**
             * True when the document is initially usable
             * @property DOMReady
             * @type boolean
             * @static
             */
            DOMReady: false,

            /**
             * Errors thrown by subscribers of custom events are caught
             * and the error message is written to the debug console.  If
             * this property is set to true, it will also re-throw the
             * error.
             * @property throwErrors
             * @type boolean
             * @default false
             */
            throwErrors: false,

            /**
             * @method startInterval
             * @static
             * @private
             */
            startInterval: function() {
                if (!this._interval) {
                    var self = this;
                    var callback = function() { self._tryPreloadAttach(); };
                    this._interval = setInterval(callback, this.POLL_INTERVAL);
                }
            },

            /**
             * 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.
             *
             * <p>The callback is executed with a single parameter:
             * the custom object parameter, if provided.</p>
             *
             * @method onAvailable
             *
             * @param {string||string[]}   id the id of the element, or an array
             * of ids to look for.
             * @param {function} fn what to execute when the element is found.
             * @param {object}   obj an optional object to be passed back as
             *                   a parameter to fn.
             * @param {boolean|object}  overrideContext If set to true, fn will execute
             *                   in the context of obj, if set to an object it
             *                   will execute in the context of that object
             * @param checkContent {boolean} check child node readiness (onContentReady)
             * @static
             */
            onAvailable: function(id, fn, obj, overrideContext, checkContent) {

                var a = (YAHOO.lang.isString(id)) ? [id] : id;

                for (var i=0; i<a.length; i=i+1) {
                    onAvailStack.push({id:         a[i], 
                                       fn:         fn, 
                                       obj:        obj, 
                                       overrideContext:   overrideContext, 
                                       checkReady: checkContent });
                }

                retryCount = this.POLL_RETRYS;

                this.startInterval();
            },

            /**
             * Works the same way as onAvailable, but additionally checks the
             * state of sibling elements to determine if the content of the
             * available element is safe to modify.
             *
             * <p>The callback is executed with a single parameter:
             * the custom object parameter, if provided.</p>
             *
             * @method onContentReady
             *
             * @param {string}   id the id of the element to look for.
             * @param {function} fn what to execute when the element is ready.
             * @param {object}   obj an optional object to be passed back as
             *                   a parameter to fn.
             * @param {boolean|object}  overrideContext If set to true, fn will execute
             *                   in the context of obj.  If an object, fn will
             *                   exectute in the context of that object
             *
             * @static
             */
            onContentReady: function(id, fn, obj, overrideContext) {
                this.onAvailable(id, fn, obj, overrideContext, true);
            },

            /**
             * Executes the supplied callback when the DOM is first usable.  This
             * will execute immediately if called after the DOMReady event has
             * fired.   @todo the DOMContentReady event does not fire when the
             * script is dynamically injected into the page.  This means the
             * DOMReady custom event will never fire in FireFox or Opera when the
             * library is injected.  It _will_ fire in Safari, and the IE 
             * implementation would allow for us to fire it if the defered script
             * is not available.  We want this to behave the same in all browsers.
             * Is there a way to identify when the script has been injected 
             * instead of included inline?  Is there a way to know whether the 
             * window onload event has fired without having had a listener attached 
             * to it when it did so?
             *
             * <p>The callback is a CustomEvent, so the signature is:</p>
             * <p>type &lt;string&gt;, args &lt;array&gt;, customobject &lt;object&gt;</p>
             * <p>For DOMReady events, there are no fire argments, so the
             * signature is:</p>
             * <p>"DOMReady", [], obj</p>
             *
             *
             * @method onDOMReady
             *
             * @param {function} fn what to execute when the element is found.
             * @param {object}   obj an optional object to be passed back as
             *                   a parameter to fn.
             * @param {boolean|object}  overrideContext If set to true, fn will execute
             *                   in the context of obj, if set to an object it
             *                   will execute in the context of that object
             *
             * @static
             */
            onDOMReady: function(fn, obj, overrideContext) {
                if (this.DOMReady) {
                    setTimeout(function() {
                        var s = window;
                        if (overrideContext) {
                            if (overrideContext === true) {
                                s = obj;
                            } else {
                                s = overrideContext;
                            }
                        }
                        fn.call(s, "DOMReady", [], obj);
                    }, 0);
                } else {
                    this.DOMReadyEvent.subscribe(fn, obj, overrideContext);
                }
            },


            /**
             * Appends an event handler
             *
             * @method _addListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to assign the 
             *  listener to.
             * @param {String}   sType     The type of event to append
             * @param {Function} fn        The method the event invokes
             * @param {Object}   obj    An arbitrary object that will be 
             *                             passed as a parameter to the handler
             * @param {Boolean|object}  overrideContext  If true, the obj passed in becomes
             *                             the execution context of the listener. If an
             *                             object, this object becomes the execution
             *                             context.
             * @param {boolen}      capture capture or bubble phase
             * @return {Boolean} True if the action was successful or defered,
             *                        false if one or more of the elements 
             *                        could not have the listener attached,
             *                        or if the operation throws an exception.
             * @private
             * @static
             */
            _addListener: function(el, sType, fn, obj, overrideContext, bCapture) {

                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, 
                                       obj, 
                                       overrideContext) && ok;
                    }
                    return ok;

                } else if (YAHOO.lang.isString(el)) {
                    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 (oEl) {
                        el = oEl;
                    } else {
                        // defer adding the event until the element is available
                        this.onAvailable(el, function() {
                           YAHOO.util.Event.on(el, sType, fn, obj, overrideContext);
                        });

                        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 && obj !== this) {
                    unloadListeners[unloadListeners.length] =
                            [el, sType, fn, obj, overrideContext];
                    return true;
                }


                // if the user chooses to override the context, we use the custom
                // object passed in, otherwise the executing context will be the
                // HTML element that the event is registered on
                var context = el;
                if (overrideContext) {
                    if (overrideContext === true) {
                        context = obj;
                    } else {
                        context = overrideContext;
                    }
                }

                // wrap the function so we can return the obj object when
                // the event fires;
                var wrappedFn = function(e) {
                        return fn.call(context, YAHOO.util.Event.getEvent(e, el), 
                                obj);
                    };

                var li = [el, sType, fn, wrappedFn, context, obj, overrideContext];
                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);

                } else {
                    try {
                        this._simpleAdd(el, sType, wrappedFn, bCapture);
                    } catch(ex) {
                        // handle an error trying to attach an event.  If it fails
                        // we need to clean up the cache
                        this.lastError = ex;
                        this.removeListener(el, sType, fn);
                        return false;
                    }
                }

                return true;
                
            },


            /**
             * Appends an event handler
             *
             * @method addListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to assign the 
             *  listener to.
             * @param {String}   sType     The type of event to append
             * @param {Function} fn        The method the event invokes
             * @param {Object}   obj    An arbitrary object that will be 
             *                             passed as a parameter to the handler
             * @param {Boolean|object}  overrideContext  If true, the obj passed in becomes
             *                             the execution context of the listener. If an
             *                             object, this object becomes the execution
             *                             context.
             * @return {Boolean} True if the action was successful or defered,
             *                        false if one or more of the elements 
             *                        could not have the listener attached,
             *                        or if the operation throws an exception.
             * @static
             */
            addListener: function (el, sType, fn, obj, overrideContext) {
                return this._addListener(el, sType, fn, obj, overrideContext, false);
            },

            /**
             * Appends a focus event handler.  (The focusin event is used for Internet Explorer, 
             * the focus, capture-event for Opera, WebKit.)
             *
             * @method addFocusListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to assign the 
             *  listener to.
             * @param {Function} fn        The method the event invokes
             * @param {Object}   obj    An arbitrary object that will be 
             *                             passed as a parameter to the handler
             * @param {Boolean|object}  overrideContext  If true, the obj passed in becomes
             *                             the execution context of the listener. If an
             *                             object, this object becomes the execution
             *                             context.
             * @return {Boolean} True if the action was successful or defered,
             *                        false if one or more of the elements 
             *                        could not have the listener attached,
             *                        or if the operation throws an exception.
             * @static
             */
            addFocusListener: function (el, fn, obj, overrideContext) {
                return this._addListener(el, _FOCUS, fn, obj, overrideContext, true);
            },          


            /**
             * Removes a focus event listener
             *
             * @method removeListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to remove
             *  the listener from.
             * @param {Function} fn the method the event invokes.  If fn is
             *  undefined, then all event handlers for the type of event are 
             *  removed.
             * @return {boolean} true if the unbind was successful, false 
             *  otherwise.
             * @static
             */
            removeFocusListener: function (el, fn) { 
                return this.removeListener(el, _FOCUS, fn);
            },

            /**
             * Appends a blur event handler.  (The focusout event is used for Internet Explorer, 
             * the focusout, capture-event for Opera, WebKit.)
             *
             * @method addBlurListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to assign the 
             *  listener to.
             * @param {Function} fn        The method the event invokes
             * @param {Object}   obj    An arbitrary object that will be 
             *                             passed as a parameter to the handler
             * @param {Boolean|object}  overrideContext  If true, the obj passed in becomes
             *                             the execution context of the listener. If an
             *                             object, this object becomes the execution
             *                             context.
             * @return {Boolean} True if the action was successful or defered,
             *                        false if one or more of the elements 
             *                        could not have the listener attached,
             *                        or if the operation throws an exception.
             * @static
             */
            addBlurListener: function (el, fn, obj, overrideContext) {
                return this._addListener(el, _BLUR, fn, obj, overrideContext, true);
            },          

            /**
             * Removes a blur event listener
             *
             * @method removeListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to remove
             *  the listener from.
             * @param {Function} fn the method the event invokes.  If fn is
             *  undefined, then all event handlers for the type of event are 
             *  removed.
             * @return {boolean} true if the unbind was successful, false 
             *  otherwise.
             * @static
             */
            removeBlurListener: function (el, fn) { 
            
                return this.removeListener(el, _BLUR, fn);
            
            },

            /**
             * When using legacy events, the handler is routed to this object
             * so we can fire our custom listener stack.
             * @method fireLegacyEvent
             * @static
             * @private
             */
            fireLegacyEvent: function(e, legacyIndex) {
                var ok=true, le, lh, li, context, ret;
                
                lh = legacyHandlers[legacyIndex].slice();
                for (var i=0, len=lh.length; i<len; ++i) {
                // for (var i in lh.length) {
                    li = lh[i];
                    if ( li && li[this.WFN] ) {
                        context = li[this.ADJ_SCOPE];
                        ret = li[this.WFN].call(context, e);
                        ok = (ok && ret);
                    }
                }

                // Fire the original handler if we replaced one.  We fire this
                // after the other events to keep stopPropagation/preventDefault
                // that happened in the DOM0 handler from touching our DOM2
                // substitute
                le = legacyEvents[legacyIndex];
                if (le && le[2]) {
                    le[2](e);
                }
                
                return ok;
            },

            /**
             * Returns the legacy event index that matches the supplied 
             * signature
             * @method getLegacyIndex
             * @static
             * @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.  Currently this is limited to old
             * Safari browsers with a broken preventDefault
             * @method useLegacyEvent
             * @static
             * @private
             */
            useLegacyEvent: function(el, sType) {
return (this.webkit && this.webkit < 419 && ("click"==sType || "dblclick"==sType));
            },
                    
            /**
             * Removes an event listener
             *
             * @method removeListener
             *
             * @param {String|HTMLElement|Array|NodeList} el An id, an element 
             *  reference, or a collection of ids and/or elements to remove
             *  the listener from.
             * @param {String} sType the type of event to remove.
             * @param {Function} fn the method the event invokes.  If fn is
             *  undefined, then all event handlers for the type of event are 
             *  removed.
             * @return {boolean} true if the unbind was successful, false 
             *  otherwise.
             * @static
             */
            removeListener: function(el, sType, fn) {
                var i, len, li;

                // 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=el.length-1; i>-1; i--) {
                        ok = ( this.removeListener(el[i], sType, fn) && ok );
                    }
                    return ok;
                }

                if (!fn || !fn.call) {
                    //return false;
                    return this.purgeElement(el, false, sType);
                }

                if ("unload" == sType) {

                    for (i=unloadListeners.length-1; i>-1; i--) {
                        li = unloadListeners[i];
                        if (li && 
                            li[0] == el && 
                            li[1] == sType && 
                            li[2] == fn) {
                                unloadListeners.splice(i, 1);
                                // unloadListeners[i]=null;
                                return true;
                        }
                    }

                    return false;
                }

                var cacheItem = null;

                // The index is a hidden parameter; needed to remove it from
                // the method signature because it was tempting users to
                // try and take advantage of it, which is not possible.
                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) {
                        // for (i in llist.length) {
                            li = llist[i];
                            if (li && 
                                li[this.EL] == el && 
                                li[this.TYPE] == sType && 
                                li[this.FN] == fn) {
                                    llist.splice(i, 1);
                                    // llist[i]=null;
                                    break;
                            }
                        }
                    }

                } else {
                    try {
                        this._simpleRemove(el, sType, cacheItem[this.WFN], false);
                    } catch(ex) {
                        this.lastError = ex;
                        return false;
                    }
                }

                // removed the wrapped handler
                delete listeners[index][this.WFN];
                delete listeners[index][this.FN];
                listeners.splice(index, 1);
                // listeners[index]=null;

                return true;

            },

            /**
             * Returns the event's target element.  Safari sometimes provides
             * a text node, and this is automatically resolved to the text
             * node's parent so that it behaves like other browsers.
             * @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
             * @static
             */
            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 node to resolve
             * @return {HTMLElement} the normized node
             * @static
             */
            resolveTextNode: function(n) {
                try {
                    if (n && 3 == n.nodeType) {
                        return n.parentNode;
                    }
                } catch(e) { }

                return n;
            },

            /**
             * Returns the event's pageX
             * @method getPageX
             * @param {Event} ev the event
             * @return {int} the event's pageX
             * @static
             */
            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
             * @static
             */
            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
             * @param {Event} ev the event
             * @return {[x, y]} the pageX and pageY properties of the event
             * @static
             */
            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
             * @static
             */
            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
             * @static
             */
            getTime: function(ev) {
                if (!ev.time) {
                    var t = new Date().getTime();
                    try {
                        ev.time = t;
                    } catch(ex) { 
                        this.lastError = ex;
                        return t;
                    }
                }

                return ev.time;
            },

            /**
             * Convenience method for stopPropagation + preventDefault
             * @method stopEvent
             * @param {Event} ev the event
             * @static
             */
            stopEvent: function(ev) {
                this.stopPropagation(ev);
                this.preventDefault(ev);
            },

            /**
             * Stops event propagation
             * @method stopPropagation
             * @param {Event} ev the event
             * @static
             */
            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
             * @static
             */
            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} e the event parameter from the handler
             * @param {HTMLElement} boundEl the element the listener is attached to
             * @return {Event} the event 
             * @static
             */
            getEvent: function(e, boundEl) {
                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
             * @static
             */
            getCharCode: function(ev) {
                var code = ev.keyCode || ev.charCode || 0;

                // webkit key normalization
                if (YAHOO.env.ua.webkit && (code in webkitKeymap)) {
                    code = webkitKeymap[code];
                }
                return code;
            },

            /**
             * Locating the saved event handler data by function ref
             *
             * @method _getCacheIndex
             * @static
             * @private
             */
            _getCacheIndex: function(el, sType, fn) {
                for (var i=0, l=listeners.length; i<l; i=i+1) {
                    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 to create the id for
             * @return {string} the resulting id of the element
             * @static
             */
            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
             * @static
             * @private
             */
            _isValidCollection: function(o) {
                try {
                    return ( o                     && // o is something
                             typeof o !== "string" && // o is not a string
                             o.length              && // o is indexed
                             !o.tagName            && // o is not an HTML element
                             !o.alert              && // o is not a window
                             typeof o[0] !== "undefined" );
                } catch(ex) {
                    return false;
                }

            },

            /**
             * @private
             * @property elCache
             * DOM element cache
             * @static
             * @deprecated Elements are not cached due to issues that arise when
             * elements are removed and re-added
             */
            elCache: {},

            /**
             * We cache elements bound by id because when the unload event 
             * fires, we can no longer use document.getElementById
             * @method getEl
             * @static
             * @private
             * @deprecated Elements are not cached any longer
             */
            getEl: function(id) {
                return (typeof id === "string") ? document.getElementById(id) : id;
            },

            /**
             * Clears the element cache
             * @deprecated Elements are not cached any longer
             * @method clearCache
             * @static
             * @private
             */
            clearCache: function() { },

            /**
             * Custom event the fires when the dom is initially usable
             * @event DOMReadyEvent
             */
            DOMReadyEvent: new YAHOO.util.CustomEvent("DOMReady", this),

            /**
             * hook up any deferred listeners
             * @method _load
             * @static
             * @private
             */
            _load: function(e) {

                if (!loadComplete) {
                    loadComplete = true;
                    var EU = YAHOO.util.Event;

                    // Just in case DOMReady did not go off for some reason
                    EU._ready();

                    // Available elements may not have been detected before the
                    // window load event fires. Try to find them now so that the
                    // the user is more likely to get the onAvailable notifications
                    // before the window load notification
                    EU._tryPreloadAttach();

                }
            },

            /**
             * Fires the DOMReady event listeners the first time the document is
             * usable.
             * @method _ready
             * @static
             * @private
             */
            _ready: function(e) {
                var EU = YAHOO.util.Event;
                if (!EU.DOMReady) {
                    EU.DOMReady=true;

                    // Fire the content ready custom event
                    EU.DOMReadyEvent.fire();

                    // Remove the DOMContentLoaded (FF/Opera)
                    EU._simpleRemove(document, "DOMContentLoaded", EU._ready);
                }
            },

            /**
             * Polling function that runs before the onload event fires, 
             * attempting to attach to DOM Nodes as soon as they are 
             * available
             * @method _tryPreloadAttach
             * @static
             * @private
             */
            _tryPreloadAttach: function() {

                if (onAvailStack.length === 0) {
                    retryCount = 0;
                    if (this._interval) {
                        clearInterval(this._interval);
                        this._interval = null;
                    } 
                    return;
                }

                if (this.locked) {
                    return;
                }

                if (this.isIE) {
                    // Hold off if DOMReady has not fired and check current
                    // readyState to protect against the IE operation aborted
                    // issue.
                    if (!this.DOMReady) {
                        this.startInterval();
                        return;
                    }
                }

                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 && onAvailStack.length > 0);
                }

                // onAvailable
                var notAvail = [];

                var executeItem = function (el, item) {
                    var context = el;
                    if (item.overrideContext) {
                        if (item.overrideContext === true) {
                            context = item.obj;
                        } else {
                            context = item.overrideContext;
                        }
                    }
                    item.fn.call(context, item.obj);
                };

                var i, len, item, el, ready=[];

                // onAvailable onContentReady
                for (i=0, len=onAvailStack.length; i<len; i=i+1) {
                    item = onAvailStack[i];
                    if (item) {
                        el = this.getEl(item.id);
                        if (el) {
                            if (item.checkReady) {
                                if (loadComplete || el.nextSibling || !tryAgain) {
                                    ready.push(item);
                                    onAvailStack[i] = null;
                                }
                            } else {
                                executeItem(el, item);
                                onAvailStack[i] = null;
                            }
                        } else {
                            notAvail.push(item);
                        }
                    }
                }
                
                // make sure onContentReady fires after onAvailable
                for (i=0, len=ready.length; i<len; i=i+1) {
                    item = ready[i];
                    executeItem(this.getEl(item.id), item);
                }


                retryCount--;

                if (tryAgain) {
                    for (i=onAvailStack.length-1; i>-1; i--) {
                        item = onAvailStack[i];
                        if (!item || !item.id) {
                            onAvailStack.splice(i, 1);
                        }
                    }

                    this.startInterval();
                } else {
                    if (this._interval) {
                        clearInterval(this._interval);
                        this._interval = null;
                    }
                }

                this.locked = false;

            },

            /**
             * 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
             * @static
             */
            purgeElement: function(el, recurse, sType) {
                var oEl = (YAHOO.lang.isString(el)) ? this.getEl(el) : el;
                var elListeners = this.getListeners(oEl, sType), i, len;
                if (elListeners) {
                    for (i=elListeners.length-1; i>-1; i--) {
                        var l = elListeners[i];
                        this.removeListener(oEl, l.type, l.fn);
                    }
                }

                if (recurse && oEl && oEl.childNodes) {
                    for (i=0,len=oEl.childNodes.length; i<len ; ++i) {
                        this.purgeElement(oEl.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|string} the element or element id 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:
             * &nbsp;&nbsp;type:   (string)   the type of event
             * &nbsp;&nbsp;fn:     (function) the callback supplied to addListener
             * &nbsp;&nbsp;obj:    (object)   the custom object supplied to addListener
             * &nbsp;&nbsp;adjust: (boolean|object)  whether or not to adjust the default context
             * &nbsp;&nbsp;scope: (boolean)  the derived context based on the adjust parameter
             * &nbsp;&nbsp;index:  (int)      its position in the Event util listener cache
             * @static
             */           
            getListeners: function(el, sType) {
                var results=[], searchLists;
                if (!sType) {
                    searchLists = [listeners, unloadListeners];
                } else if (sType === "unload") {
                    searchLists = [unloadListeners];
                } else {
                    searchLists = [listeners];
                }

                var oEl = (YAHOO.lang.isString(el)) ? this.getEl(el) : el;

                for (var j=0;j<searchLists.length; j=j+1) {
                    var searchList = searchLists[j];
                    if (searchList) {
                        for (var i=0,len=searchList.length; i<len ; ++i) {
                            var l = searchList[i];
                            if ( l  && l[this.EL] === oEl && 
                                    (!sType || sType === l[this.TYPE]) ) {
                                results.push({
                                    type:   l[this.TYPE],
                                    fn:     l[this.FN],
                                    obj:    l[this.OBJ],
                                    adjust: l[this.OVERRIDE],
                                    scope:  l[this.ADJ_SCOPE],
                                    index:  i
                                });
                            }
                        }
                    }
                }

                return (results.length) ? results : null;
            },

            /**
             * Removes all listeners registered by pe.event.  Called 
             * automatically during the unload event.
             * @method _unload
             * @static
             * @private
             */
            _unload: function(e) {

                var EU = YAHOO.util.Event, i, j, l, len, index,
                         ul = unloadListeners.slice(), context;

                // execute and clear stored unload listeners
                for (i=0, len=unloadListeners.length; i<len; ++i) {
                    l = ul[i];
                    if (l) {
                        context = window;
                        if (l[EU.ADJ_SCOPE]) {
                            if (l[EU.ADJ_SCOPE] === true) {
                                context = l[EU.UNLOAD_OBJ];
                            } else {
                                context = l[EU.ADJ_SCOPE];
                            }
                        }
                        l[EU.FN].call(context, EU.getEvent(e, l[EU.EL]), l[EU.UNLOAD_OBJ] );
                        ul[i] = null;
                    }
                }

                l = null;
                context = null;
                unloadListeners = null;

                // Remove listeners to handle IE memory leaks
                //if (YAHOO.env.ua.ie && listeners && listeners.length > 0) {
                
                // 2.5.0 listeners are removed for all browsers again.  FireFox preserves
                // at least some listeners between page refreshes, potentially causing
                // errors during page load (mouseover listeners firing before they
                // should if the user moves the mouse at the correct moment).
                if (listeners) {
                    for (j=listeners.length-1; j>-1; j--) {
                        l = listeners[j];
                        if (l) {
                            EU.removeListener(l[EU.EL], l[EU.TYPE], l[EU.FN], j);
                        } 
                    }
                    l=null;
                }

                legacyEvents = null;

                EU._simpleRemove(window, "unload", EU._unload);

            },

            /**
             * Returns scrollLeft
             * @method _getScrollLeft
             * @static
             * @private
             */
            _getScrollLeft: function() {
                return this._getScroll()[1];
            },

            /**
             * Returns scrollTop
             * @method _getScrollTop
             * @static
             * @private
             */
            _getScrollTop: function() {
                return this._getScroll()[0];
            },

            /**
             * Returns the scrollTop and scrollLeft.  Used to calculate the 
             * pageX and pageY in Internet Explorer
             * @method _getScroll
             * @static
             * @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];
                }
            },
            
            /**
             * Used by old versions of CustomEvent, restored for backwards
             * compatibility
             * @method regCE
             * @private
             * @static
             * @deprecated still here for backwards compatibility
             */
            regCE: function() {
                // does nothing
            },

            /**
             * Adds a DOM event directly without the caching, cleanup, context adj, etc
             *
             * @method _simpleAdd
             * @param {HTMLElement} el      the element to bind the handler to
             * @param {string}      sType   the type of event handler
             * @param {function}    fn      the callback to invoke
             * @param {boolen}      capture capture or bubble phase
             * @static
             * @private
             */
            _simpleAdd: function () {
                if (window.addEventListener) {
                    return function(el, sType, fn, capture) {
                        el.addEventListener(sType, fn, (capture));
                    };
                } else if (window.attachEvent) {
                    return function(el, sType, fn, capture) {
                        el.attachEvent("on" + sType, fn);
                    };
                } else {
                    return function(){};
                }
            }(),

            /**
             * Basic remove listener
             *
             * @method _simpleRemove
             * @param {HTMLElement} el      the element to bind the handler to
             * @param {string}      sType   the type of event handler
             * @param {function}    fn      the callback to invoke
             * @param {boolen}      capture capture or bubble phase
             * @static
             * @private
             */
            _simpleRemove: function() {
                if (window.removeEventListener) {
                    return function (el, sType, fn, capture) {
                        el.removeEventListener(sType, fn, (capture));
                    };
                } else if (window.detachEvent) {
                    return function (el, sType, fn) {
                        el.detachEvent("on" + sType, fn);
                    };
                } else {
                    return function(){};
                }
            }()
        };

    }();

    (function() {
        var EU = YAHOO.util.Event;

        /**
         * YAHOO.util.Event.on is an alias for addListener
         * @method on
         * @see addListener
         * @static
         */
        EU.on = EU.addListener;

        /**
         * YAHOO.util.Event.onFocus is an alias for addFocusListener
         * @method on
         * @see addFocusListener
         * @static
         */
        EU.onFocus = EU.addFocusListener;

        /**
         * YAHOO.util.Event.onBlur is an alias for addBlurListener
         * @method onBlur
         * @see addBlurListener
         * @static
         */     
        EU.onBlur = EU.addBlurListener;


/*! DOMReady: based on work by: Dean Edwards/John Resig/Matthias Miller */

        // Internet Explorer: use the readyState of a defered script.
        // This isolates what appears to be a safe moment to manipulate
        // the DOM prior to when the document's readyState suggests
        // it is safe to do so.
        if (EU.isIE) {

            // Process onAvailable/onContentReady items when the 
            // DOM is ready.
            YAHOO.util.Event.onDOMReady(
                    YAHOO.util.Event._tryPreloadAttach,
                    YAHOO.util.Event, true);
            
            var n = document.createElement('p');  

            EU._dri = setInterval(function() {
                try {
                    // throws an error if doc is not ready
                    n.doScroll('left');
                    clearInterval(EU._dri);
                    EU._dri = null;
                    EU._ready();
                    n = null;
                } catch (ex) { 
                }
            }, EU.POLL_INTERVAL); 

        
        // The document's readyState in Safari currently will
        // change to loaded/complete before images are loaded.
        } else if (EU.webkit && EU.webkit < 525) {

            EU._dri = setInterval(function() {
                var rs=document.readyState;
                if ("loaded" == rs || "complete" == rs) {
                    clearInterval(EU._dri);
                    EU._dri = null;
                    EU._ready();
                }
            }, EU.POLL_INTERVAL); 

        // FireFox and Opera: These browsers provide a event for this
        // moment.  The latest WebKit releases now support this event.
        } else {

            EU._simpleAdd(document, "DOMContentLoaded", EU._ready);

        }
        /////////////////////////////////////////////////////////////


        EU._simpleAdd(window, "load", EU._load);
        EU._simpleAdd(window, "unload", EU._unload);
        EU._tryPreloadAttach();
    })();

}
/**
 * EventProvider is designed to be used with YAHOO.augment to wrap 
 * CustomEvents in an interface that allows events to be subscribed to 
 * and fired by name.  This makes it possible for implementing code to
 * subscribe to an event that either has not been created yet, or will
 * not be created at all.
 *
 * @Class EventProvider
 */
YAHOO.util.EventProvider = function() { };

YAHOO.util.EventProvider.prototype = {

    /**
     * Private storage of custom events
     * @property __yui_events
     * @type Object[]
     * @private
     */
    __yui_events: null,

    /**
     * Private storage of custom event subscribers
     * @property __yui_subscribers
     * @type Object[]
     * @private
     */
    __yui_subscribers: null,
    
    /**
     * Subscribe to a CustomEvent by event type
     *
     * @method subscribe
     * @param p_type     {string}   the type, or name of the event
     * @param p_fn       {function} the function to exectute when the event fires
     * @param p_obj      {Object}   An object to be passed along when the event 
     *                              fires
     * @param overrideContext {boolean}  If true, the obj passed in becomes the 
     *                              execution scope of the listener
     */
    subscribe: function(p_type, p_fn, p_obj, overrideContext) {

        this.__yui_events = this.__yui_events || {};
        var ce = this.__yui_events[p_type];

        if (ce) {
            ce.subscribe(p_fn, p_obj, overrideContext);
        } else {
            this.__yui_subscribers = this.__yui_subscribers || {};
            var subs = this.__yui_subscribers;
            if (!subs[p_type]) {
                subs[p_type] = [];
            }
            subs[p_type].push(
                { fn: p_fn, obj: p_obj, overrideContext: overrideContext } );
        }
    },

    /**
     * Unsubscribes one or more listeners the from the specified event
     * @method unsubscribe
     * @param p_type {string}   The type, or name of the event.  If the type
     *                          is not specified, it will attempt to remove
     *                          the listener from all hosted events.
     * @param p_fn   {Function} The subscribed function to unsubscribe, if not
     *                          supplied, all subscribers will be removed.
     * @param p_obj  {Object}   The custom object passed to subscribe.  This is
     *                        optional, but if supplied will be used to
     *                        disambiguate multiple listeners that are the same
     *                        (e.g., you subscribe many object using a function
     *                        that lives on the prototype)
     * @return {boolean} true if the subscriber was found and detached.
     */
    unsubscribe: function(p_type, p_fn, p_obj) {
        this.__yui_events = this.__yui_events || {};
        var evts = this.__yui_events;
        if (p_type) {
            var ce = evts[p_type];
            if (ce) {
                return ce.unsubscribe(p_fn, p_obj);
            }
        } else {
            var ret = true;
            for (var i in evts) {
                if (YAHOO.lang.hasOwnProperty(evts, i)) {
                    ret = ret && evts[i].unsubscribe(p_fn, p_obj);
                }
            }
            return ret;
        }

        return false;
    },
    
    /**
     * Removes all listeners from the specified event.  If the event type
     * is not specified, all listeners from all hosted custom events will
     * be removed.
     * @method unsubscribeAll
     * @param p_type {string}   The type, or name of the event
     */
    unsubscribeAll: function(p_type) {
        return this.unsubscribe(p_type);
    },

    /**
     * Creates a new custom event of the specified type.  If a custom event
     * by that name already exists, it will not be re-created.  In either
     * case the custom event is returned. 
     *
     * @method createEvent
     *
     * @param p_type {string} the type, or name of the event
     * @param p_config {object} optional config params.  Valid properties are:
     *
     *  <ul>
     *    <li>
     *      scope: defines the default execution scope.  If not defined
     *      the default scope will be this instance.
     *    </li>
     *    <li>
     *      silent: if true, the custom event will not generate log messages.
     *      This is false by default.
     *    </li>
     *    <li>
     *      onSubscribeCallback: specifies a callback to execute when the
     *      event has a new subscriber.  This will fire immediately for
     *      each queued subscriber if any exist prior to the creation of
     *      the event.
     *    </li>
     *  </ul>
     *
     *  @return {CustomEvent} the custom event
     *
     */
    createEvent: function(p_type, p_config) {

        this.__yui_events = this.__yui_events || {};
        var opts = p_config || {};
        var events = this.__yui_events;

        if (events[p_type]) {
        } else {

            var scope  = opts.scope  || this;
            var silent = (opts.silent);

            var ce = new YAHOO.util.CustomEvent(p_type, scope, silent,
                    YAHOO.util.CustomEvent.FLAT);
            events[p_type] = ce;

            if (opts.onSubscribeCallback) {
                ce.subscribeEvent.subscribe(opts.onSubscribeCallback);
            }

            this.__yui_subscribers = this.__yui_subscribers || {};
            var qs = this.__yui_subscribers[p_type];

            if (qs) {
                for (var i=0; i<qs.length; ++i) {
                    ce.subscribe(qs[i].fn, qs[i].obj, qs[i].overrideContext);
                }
            }
        }

        return events[p_type];
    },


   /**
     * Fire a custom event by name.  The callback functions will be executed
     * from the scope specified when the event was created, and with the 
     * following parameters:
     *   <ul>
     *   <li>The first argument fire() was executed with</li>
     *   <li>The custom object (if any) that was passed into the subscribe() 
     *       method</li>
     *   </ul>
     * @method fireEvent
     * @param p_type    {string}  the type, or name of the event
     * @param arguments {Object*} an arbitrary set of parameters to pass to 
     *                            the handler.
     * @return {boolean} the return value from CustomEvent.fire
     *                   
     */
    fireEvent: function(p_type, arg1, arg2, etc) {

        this.__yui_events = this.__yui_events || {};
        var ce = this.__yui_events[p_type];

        if (!ce) {
            return null;
        }

        var args = [];
        for (var i=1; i<arguments.length; ++i) {
            args.push(arguments[i]);
        }
        return ce.fire.apply(ce, args);
    },

    /**
     * Returns true if the custom event of the provided type has been created
     * with createEvent.
     * @method hasEvent
     * @param type {string} the type, or name of the event
     */
    hasEvent: function(type) {
        if (this.__yui_events) {
            if (this.__yui_events[type]) {
                return true;
            }
        }
        return false;
    }

};

(function() {

    var Event = YAHOO.util.Event, Lang = YAHOO.lang;

/**
* KeyListener is a utility that provides an easy interface for listening for
* keydown/keyup events fired against DOM elements.
* @namespace YAHOO.util
* @class KeyListener
* @constructor
* @param {HTMLElement} attachTo The element or element ID to which the key 
*                               event should be attached
* @param {String}      attachTo The element or element ID to which the key
*                               event should be attached
* @param {Object}      keyData  The object literal representing the key(s) 
*                               to detect. Possible attributes are 
*                               shift(boolean), alt(boolean), ctrl(boolean) 
*                               and keys(either an int or an array of ints 
*                               representing keycodes).
* @param {Function}    handler  The CustomEvent handler to fire when the 
*                               key event is detected
* @param {Object}      handler  An object literal representing the handler. 
* @param {String}      event    Optional. The event (keydown or keyup) to 
*                               listen for. Defaults automatically to keydown.
*
* @knownissue the "keypress" event is completely broken in Safari 2.x and below.
*             the workaround is use "keydown" for key listening.  However, if
*             it is desired to prevent the default behavior of the keystroke,
*             that can only be done on the keypress event.  This makes key
*             handling quite ugly.
* @knownissue keydown is also broken in Safari 2.x and below for the ESC key.
*             There currently is no workaround other than choosing another
*             key to listen for.
*/
YAHOO.util.KeyListener = function(attachTo, keyData, handler, event) {
    if (!attachTo) {
    } else if (!keyData) {
    } else if (!handler) {
    } 
    
    if (!event) {
        event = YAHOO.util.KeyListener.KEYDOWN;
    }

    /**
    * The CustomEvent fired internally when a key is pressed
    * @event keyEvent
    * @private
    * @param {Object} keyData The object literal representing the key(s) to 
    *                         detect. Possible attributes are shift(boolean), 
    *                         alt(boolean), ctrl(boolean) and keys(either an 
    *                         int or an array of ints representing keycodes).
    */
    var keyEvent = new YAHOO.util.CustomEvent("keyPressed");
    
    /**
    * The CustomEvent fired when the KeyListener is enabled via the enable() 
    * function
    * @event enabledEvent
    * @param {Object} keyData The object literal representing the key(s) to 
    *                         detect. Possible attributes are shift(boolean), 
    *                         alt(boolean), ctrl(boolean) and keys(either an 
    *                         int or an array of ints representing keycodes).
    */
    this.enabledEvent = new YAHOO.util.CustomEvent("enabled");

    /**
    * The CustomEvent fired when the KeyListener is disabled via the 
    * disable() function
    * @event disabledEvent
    * @param {Object} keyData The object literal representing the key(s) to 
    *                         detect. Possible attributes are shift(boolean), 
    *                         alt(boolean), ctrl(boolean) and keys(either an 
    *                         int or an array of ints representing keycodes).
    */
    this.disabledEvent = new YAHOO.util.CustomEvent("disabled");

    if (Lang.isString(attachTo)) {
        attachTo = document.getElementById(attachTo); // No Dom util
    }

    if (Lang.isFunction(handler)) {
        keyEvent.subscribe(handler);
    } else {
        keyEvent.subscribe(handler.fn, handler.scope, handler.correctScope);
    }

    /**
    * Handles the key event when a key is pressed.
    * @method handleKeyPress
    * @param {DOMEvent} e   The keypress DOM event
    * @param {Object}   obj The DOM event scope object
    * @private
    */
    function handleKeyPress(e, obj) {
        if (! keyData.shift) {  
            keyData.shift = false; 
        }
        if (! keyData.alt) {    
            keyData.alt = false;
        }
        if (! keyData.ctrl) {
            keyData.ctrl = false;
        }

        // check held down modifying keys first
        if (e.shiftKey == keyData.shift && 
            e.altKey   == keyData.alt &&
            e.ctrlKey  == keyData.ctrl) { // if we pass this, all modifiers match
            
            var dataItem, keys = keyData.keys, key;

            if (YAHOO.lang.isArray(keys)) {
                for (var i=0;i<keys.length;i++) {
                    dataItem = keys[i];
                    key = Event.getCharCode(e);

                    if (dataItem == key) {
                        keyEvent.fire(key, e);
                        break;
                    }
                }
            } else {
                key = Event.getCharCode(e);
                if (keys == key ) {
                    keyEvent.fire(key, e);
                }
            }
        }
    }

    /**
    * Enables the KeyListener by attaching the DOM event listeners to the 
    * target DOM element
    * @method enable
    */
    this.enable = function() {
        if (! this.enabled) {
            Event.on(attachTo, event, handleKeyPress);
            this.enabledEvent.fire(keyData);
        }
        /**
        * Boolean indicating the enabled/disabled state of the Tooltip
        * @property enabled
        * @type Boolean
        */
        this.enabled = true;
    };

    /**
    * Disables the KeyListener by removing the DOM event listeners from the 
    * target DOM element
    * @method disable
    */
    this.disable = function() {
        if (this.enabled) {
            Event.removeListener(attachTo, event, handleKeyPress);
            this.disabledEvent.fire(keyData);
        }
        this.enabled = false;
    };

    /**
    * Returns a String representation of the object.
    * @method toString
    * @return {String}  The string representation of the KeyListener
    */ 
    this.toString = function() {
        return "KeyListener [" + keyData.keys + "] " + attachTo.tagName + 
                (attachTo.id ? "[" + attachTo.id + "]" : "");
    };

};

var KeyListener = YAHOO.util.KeyListener;

/**
 * Constant representing the DOM "keydown" event.
 * @property YAHOO.util.KeyListener.KEYDOWN
 * @static
 * @final
 * @type String
 */
KeyListener.KEYDOWN = "keydown";

/**
 * Constant representing the DOM "keyup" event.
 * @property YAHOO.util.KeyListener.KEYUP
 * @static
 * @final
 * @type String
 */
KeyListener.KEYUP = "keyup";

/**
 * keycode constants for a subset of the special keys
 * @property KEY
 * @static
 * @final
 */
KeyListener.KEY = {
    ALT          : 18,
    BACK_SPACE   : 8,
    CAPS_LOCK    : 20,
    CONTROL      : 17,
    DELETE       : 46,
    DOWN         : 40,
    END          : 35,
    ENTER        : 13,
    ESCAPE       : 27,
    HOME         : 36,
    LEFT         : 37,
    META         : 224,
    NUM_LOCK     : 144,
    PAGE_DOWN    : 34,
    PAGE_UP      : 33, 
    PAUSE        : 19,
    PRINTSCREEN  : 44,
    RIGHT        : 39,
    SCROLL_LOCK  : 145,
    SHIFT        : 16,
    SPACE        : 32,
    TAB          : 9,
    UP           : 38
};

})();
YAHOO.register("event", YAHOO.util.Event, {version: "2.7.0", build: "1799"});
/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.7.0
*/
/**
 * The dom module provides helper methods for manipulating Dom elements.
 * @module dom
 *
 */

(function() {
    YAHOO.env._id_counter = YAHOO.env._id_counter || 0;     // for use with generateId (global to save state if Dom is overwritten)

        // internal shorthand
    var Y = YAHOO.util,
        lang = YAHOO.lang,
        UA = YAHOO.env.ua,
        trim = YAHOO.lang.trim,
        propertyCache = {}, // for faster hyphen converts
        reCache = {}, // cache className regexes
        RE_TABLE = /^t(?:able|d|h)$/i, // for _calcBorders
        RE_COLOR = /color$/i,

        // DOM aliases 
        document = window.document,     
        documentElement = document.documentElement,

        // string constants
        OWNER_DOCUMENT = 'ownerDocument',
        DEFAULT_VIEW = 'defaultView',
        DOCUMENT_ELEMENT = 'documentElement',
        COMPAT_MODE = 'compatMode',
        OFFSET_LEFT = 'offsetLeft',
        OFFSET_TOP = 'offsetTop',
        OFFSET_PARENT = 'offsetParent',
        PARENT_NODE = 'parentNode',
        NODE_TYPE = 'nodeType',
        TAG_NAME = 'tagName',
        SCROLL_LEFT = 'scrollLeft',
        SCROLL_TOP = 'scrollTop',
        GET_BOUNDING_CLIENT_RECT = 'getBoundingClientRect',
        GET_COMPUTED_STYLE = 'getComputedStyle',
        CURRENT_STYLE = 'currentStyle',
        CSS1_COMPAT = 'CSS1Compat',
        _BACK_COMPAT = 'BackCompat',
        _CLASS = 'class', // underscore due to reserved word
        CLASS_NAME = 'className',
        EMPTY = '',
        SPACE = ' ',
        C_START = '(?:^|\\s)',
        C_END = '(?= |$)',
        G = 'g',
        POSITION = 'position',
        FIXED = 'fixed',
        RELATIVE = 'relative',
        LEFT = 'left',
        TOP = 'top',
        MEDIUM = 'medium',
        BORDER_LEFT_WIDTH = 'borderLeftWidth',
        BORDER_TOP_WIDTH = 'borderTopWidth',
    
    // brower detection
        isOpera = UA.opera,
        isSafari = UA.webkit, 
        isGecko = UA.gecko, 
        isIE = UA.ie; 
    
    /**
     * Provides helper methods for DOM elements.
     * @namespace YAHOO.util
     * @class Dom
     * @requires yahoo, event
     */
    Y.Dom = {
        CUSTOM_ATTRIBUTES: (!documentElement.hasAttribute) ? { // IE < 8
            'for': 'htmlFor',
            'class': CLASS_NAME
        } : { // w3c
            'htmlFor': 'for',
            'className': _CLASS
        },

        /**
         * Returns an HTMLElement reference.
         * @method get
         * @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) {
            var id, nodes, c, i, len;

            if (el) {
                if (el[NODE_TYPE] || el.item) { // Node, or NodeList
                    return el;
                }

                if (typeof el === 'string') { // id
                    id = el;
                    el = document.getElementById(el);
                    if (el && el.id === id) { // IE: avoid false match on "name" attribute
                    return el;
                    } else if (el && document.all) { // filter by name
                        el = null;
                        nodes = document.all[id];
                        for (i = 0, len = nodes.length; i < len; ++i) {
                            if (nodes[i].id === id) {
                                return nodes[i];
                            }
                        }
                    }
                    return el;
                }
                
                if (el.DOM_EVENTS) { // YAHOO.util.Element
                    el = el.get('element');
                }

                if ('length' in el) { // array-like 
                    c = [];
                    for (i = 0, len = el.length; i < len; ++i) {
                        c[c.length] = Y.Dom.get(el[i]);
                    }
                    
                    return c;
                }

                return el; // some other object, just pass it back
            }

            return null;
        },
    
        getComputedStyle: function(el, property) {
            if (window[GET_COMPUTED_STYLE]) {
                return el[OWNER_DOCUMENT][DEFAULT_VIEW][GET_COMPUTED_STYLE](el, null)[property];
            } else if (el[CURRENT_STYLE]) {
                return Y.Dom.IE_ComputedStyle.get(el, property);
            }
        },

        /**
         * Normalizes currentStyle and ComputedStyle.
         * @method getStyle
         * @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) {
            return Y.Dom.batch(el, Y.Dom._getStyle, property);
        },

        // branching at load instead of runtime
        _getStyle: function() {
            if (window[GET_COMPUTED_STYLE]) { // W3C DOM method
                return function(el, property) {
                    property = (property === 'float') ? property = 'cssFloat' :
                            Y.Dom._toCamel(property);

                    var value = el.style[property],
                        computed;
                    
                    if (!value) {
                        computed = el[OWNER_DOCUMENT][DEFAULT_VIEW][GET_COMPUTED_STYLE](el, null);
                        if (computed) { // test computed before touching for safari
                            value = computed[property];
                        }
                    }
                    
                    return value;
                };
            } else if (documentElement[CURRENT_STYLE]) {
                return function(el, property) {                         
                    var value;

                    switch(property) {
                        case 'opacity' :// IE opacity uses filter
                            value = 100;
                            try { // will error if no DXImageTransform
                                value = el.filters['DXImageTransform.Microsoft.Alpha'].opacity;

                            } catch(e) {
                                try { // make sure its in the document
                                    value = el.filters('alpha').opacity;
                                } catch(err) {
                                }
                            }
                            return value / 100;
                        case 'float': // fix reserved word
                            property = 'styleFloat'; // fall through
                        default: 
                            property = Y.Dom._toCamel(property);
                            value = el[CURRENT_STYLE] ? el[CURRENT_STYLE][property] : null;
                            return ( el.style[property] || value );
                    }
                };
            }
        }(),
    
        /**
         * Wrapper for setting style properties of HTMLElements.  Normalizes "opacity" across modern browsers.
         * @method setStyle
         * @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) {
            Y.Dom.batch(el, Y.Dom._setStyle, { prop: property, val: val });
        },

        _setStyle: function() {
            if (isIE) {
                return function(el, args) {
                    var property = Y.Dom._toCamel(args.prop),
                        val = args.val;

                    if (el) {
                        switch (property) {
                            case 'opacity':
                                if ( lang.isString(el.style.filter) ) { // in case not appended
                                    el.style.filter = 'alpha(opacity=' + val * 100 + ')';
                                    
                                    if (!el[CURRENT_STYLE] || !el[CURRENT_STYLE].hasLayout) {
                                        el.style.zoom = 1; // when no layout or cant tell
                                    }
                                }
                                break;
                            case 'float':
                                property = 'styleFloat';
                            default:
                            el.style[property] = val;
                        }
                    } else {
                    }
                };
            } else {
                return function(el, args) {
                    var property = Y.Dom._toCamel(args.prop),
                        val = args.val;
                    if (el) {
                        if (property == 'float') {
                            property = 'cssFloat';
                        }
                        el.style[property] = val;
                    } else {
                    }
                };
            }

        }(),
        
        /**
         * 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).
         * @method getXY
         * @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) {
            return Y.Dom.batch(el, Y.Dom._getXY);
        },

        _canPosition: function(el) {
            return ( Y.Dom._getStyle(el, 'display') !== 'none' && Y.Dom._inDoc(el) );
        },

        _getXY: function() {
            if (document[DOCUMENT_ELEMENT][GET_BOUNDING_CLIENT_RECT]) {
                return function(node) {
                    var scrollLeft, scrollTop, box, doc,
                        off1, off2, mode, bLeft, bTop,
                        floor = Math.floor, // TODO: round?
                        xy = false;

                    if (Y.Dom._canPosition(node)) {
                        box = node[GET_BOUNDING_CLIENT_RECT]();
                        doc = node[OWNER_DOCUMENT];
                        scrollLeft = Y.Dom.getDocumentScrollLeft(doc);
                        scrollTop = Y.Dom.getDocumentScrollTop(doc);
                        xy = [floor(box[LEFT]), floor(box[TOP])];

                        if (isIE && UA.ie < 8) { // IE < 8: viewport off by 2
                            off1 = 2;
                            off2 = 2;
                            mode = doc[COMPAT_MODE];
                            bLeft = _getComputedStyle(doc[DOCUMENT_ELEMENT], BORDER_LEFT_WIDTH);
                            bTop = _getComputedStyle(doc[DOCUMENT_ELEMENT], BORDER_TOP_WIDTH);

                            if (UA.ie === 6) {
                                if (mode !== _BACK_COMPAT) {
                                    off1 = 0;
                                    off2 = 0;
                                }
                            }
                            
                            if ((mode == _BACK_COMPAT)) {
                                if (bLeft !== MEDIUM) {
                                    off1 = parseInt(bLeft, 10);
                                }
                                if (bTop !== MEDIUM) {
                                    off2 = parseInt(bTop, 10);
                                }
                            }
                            
                            xy[0] -= off1;
                            xy[1] -= off2;

                        }

                        if ((scrollTop || scrollLeft)) {
                            xy[0] += scrollLeft;
                            xy[1] += scrollTop;
                        }

                        // gecko may return sub-pixel (non-int) values
                        xy[0] = floor(xy[0]);
                        xy[1] = floor(xy[1]);
                    } else {
                    }

                    return xy;
                };
            } else {
                return function(node) { // ff2, safari: manually calculate by crawling up offsetParents
                    var docScrollLeft, docScrollTop,
                        scrollTop, scrollLeft,
                        bCheck,
                        xy = false,
                        parentNode = node;

                    if  (Y.Dom._canPosition(node) ) {
                        xy = [node[OFFSET_LEFT], node[OFFSET_TOP]];
                        docScrollLeft = Y.Dom.getDocumentScrollLeft(node[OWNER_DOCUMENT]);
                        docScrollTop = Y.Dom.getDocumentScrollTop(node[OWNER_DOCUMENT]);

                        // TODO: refactor with !! or just falsey
                        bCheck = ((isGecko || UA.webkit > 519) ? true : false);

                        // TODO: worth refactoring for TOP/LEFT only?
                        while ((parentNode = parentNode[OFFSET_PARENT])) {
                            xy[0] += parentNode[OFFSET_LEFT];
                            xy[1] += parentNode[OFFSET_TOP];
                            if (bCheck) {
                                xy = Y.Dom._calcBorders(parentNode, xy);
                            }
                        }

                        // account for any scrolled ancestors
                        if (Y.Dom._getStyle(node, POSITION) !== FIXED) {
                            parentNode = node;

                            while ((parentNode = parentNode[PARENT_NODE]) && parentNode[TAG_NAME]) {
                                scrollTop = parentNode[SCROLL_TOP];
                                scrollLeft = parentNode[SCROLL_LEFT];

                                //Firefox does something funky with borders when overflow is not visible.
                                if (isGecko && (Y.Dom._getStyle(parentNode, 'overflow') !== 'visible')) {
                                        xy = Y.Dom._calcBorders(parentNode, xy);
                                }

                                if (scrollTop || scrollLeft) {
                                    xy[0] -= scrollLeft;
                                    xy[1] -= scrollTop;
                                }
                            }
                            xy[0] += docScrollLeft;
                            xy[1] += docScrollTop;

                        } else {
                            //Fix FIXED position -- add scrollbars
                            if (isOpera) {
                                xy[0] -= docScrollLeft;
                                xy[1] -= docScrollTop;
                            } else if (isSafari || isGecko) {
                                xy[0] += docScrollLeft;
                                xy[1] += docScrollTop;
                            }
                        }
                        //Round the numbers so we get sane data back
                        xy[0] = Math.floor(xy[0]);
                        xy[1] = Math.floor(xy[1]);
                    } else {
                    }
                    return xy;                
                };
            }
        }(), // NOTE: Executing for loadtime branching
        
        /**
         * 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).
         * @method getX
         * @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 {Number | Array} The X position of the element(s)
         */
        getX: function(el) {
            var f = function(el) {
                return Y.Dom.getXY(el)[0];
            };
            
            return Y.Dom.batch(el, f, Y.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).
         * @method getY
         * @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 {Number | Array} The Y position of the element(s)
         */
        getY: function(el) {
            var f = function(el) {
                return Y.Dom.getXY(el)[1];
            };
            
            return Y.Dom.batch(el, f, Y.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).
         * @method setXY
         * @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) {
            Y.Dom.batch(el, Y.Dom._setXY, { pos: pos, noRetry: noRetry });
        },

        _setXY: function(node, args) {
            var pos = Y.Dom._getStyle(node, POSITION),
                setStyle = Y.Dom.setStyle,
                xy = args.pos,
                noRetry = args.noRetry,

                delta = [ // assuming pixels; if not we will have to retry
                    parseInt( Y.Dom.getComputedStyle(node, LEFT), 10 ),
                    parseInt( Y.Dom.getComputedStyle(node, TOP), 10 )
                ],

                currentXY,
                newXY;
        
            if (pos == 'static') { // default to relative
                pos = RELATIVE;
                setStyle(node, POSITION, pos);
            }

            currentXY = Y.Dom._getXY(node);

            if (!xy || currentXY === false) { // has to be part of doc to have xy
                return false; 
            }
            
            if ( isNaN(delta[0]) ) {// in case of 'auto'
                delta[0] = (pos == RELATIVE) ? 0 : node[OFFSET_LEFT];
            } 
            if ( isNaN(delta[1]) ) { // in case of 'auto'
                delta[1] = (pos == RELATIVE) ? 0 : node[OFFSET_TOP];
            } 

            if (xy[0] !== null) { // from setX
                setStyle(node, LEFT, xy[0] - currentXY[0] + delta[0] + 'px');
            }

            if (xy[1] !== null) { // from setY
                setStyle(node, TOP, xy[1] - currentXY[1] + delta[1] + 'px');
            }
          
            if (!noRetry) {
                newXY = Y.Dom._getXY(node);

                // if retry is true, try one more time if we miss 
               if ( (xy[0] !== null && newXY[0] != xy[0]) || 
                    (xy[1] !== null && newXY[1] != xy[1]) ) {
                   Y.Dom._setXY(node, { pos: xy, noRetry: 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).
         * @method setX
         * @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 The value to use as the X coordinate for the element(s).
         */
        setX: function(el, x) {
            Y.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).
         * @method setY
         * @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) {
            Y.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).
         * @method getRegion
         * @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 = false;
                if ( Y.Dom._canPosition(el) ) {
                    region = Y.Region.getRegion(el);
                } else {
                }

                return region;
            };
            
            return Y.Dom.batch(el, f, Y.Dom, true);
        },
        
        /**
         * Returns the width of the client (viewport).
         * @method getClientWidth
         * @deprecated Now using getViewportWidth.  This interface left intact for back compat.
         * @return {Int} The width of the viewable area of the page.
         */
        getClientWidth: function() {
            return Y.Dom.getViewportWidth();
        },
        
        /**
         * Returns the height of the client (viewport).
         * @method getClientHeight
         * @deprecated Now using getViewportHeight.  This interface left intact for back compat.
         * @return {Int} The height of the viewable area of the page.
         */
        getClientHeight: function() {
            return Y.Dom.getViewportHeight();
        },

        /**
         * Returns a array of HTMLElements with the given class.
         * For optimized performance, include a tag and/or root node when possible.
         * Note: This method operates against a live collection, so modifying the 
         * collection in the callback (removing/appending nodes, etc.) will have
         * side effects.  Instead you should iterate the returned nodes array,
         * as you would with the native "getElementsByTagName" method. 
         * @method getElementsByClassName
         * @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 
         * @param {Function} apply (optional) A function to apply to each element when found 
         * @param {Any} o (optional) An optional arg that is passed to the supplied method
         * @param {Boolean} overrides (optional) Whether or not to override the scope of "method" with "o"
         * @return {Array} An array of elements that have the given class name
         */
        getElementsByClassName: function(className, tag, root, apply, o, overrides) {
            className = lang.trim(className);
            tag = tag || '*';
            root = (root) ? Y.Dom.get(root) : null || document; 
            if (!root) {
                return [];
            }

            var nodes = [],
                elements = root.getElementsByTagName(tag),
                hasClass = Y.Dom.hasClass;

            for (var i = 0, len = elements.length; i < len; ++i) {
                if ( hasClass(elements[i], className) ) {
                    nodes[nodes.length] = elements[i];
                }
            }
            
            if (apply) {
                Y.Dom.batch(nodes, apply, o, overrides);
            }

            return nodes;
        },

        /**
         * Determines whether an HTMLElement has the given className.
         * @method hasClass
         * @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) {
            return Y.Dom.batch(el, Y.Dom._hasClass, className);
        },

        _hasClass: function(el, className) {
            var ret = false,
                current;
            
            if (el && className) {
                current = Y.Dom.getAttribute(el, CLASS_NAME) || EMPTY;
                if (className.exec) {
                    ret = className.test(current);
                } else {
                    ret = className && (SPACE + current + SPACE).
                        indexOf(SPACE + className + SPACE) > -1;
                }
            } else {
            }

            return ret;
        },
    
        /**
         * Adds a class name to a given element or collection of elements.
         * @method addClass         
         * @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
         * @return {Boolean | Array} A pass/fail boolean or array of booleans
         */
        addClass: function(el, className) {
            return Y.Dom.batch(el, Y.Dom._addClass, className);
        },

        _addClass: function(el, className) {
            var ret = false,
                current;

            if (el && className) {
                current = Y.Dom.getAttribute(el, CLASS_NAME) || EMPTY;
                if ( !Y.Dom._hasClass(el, className) ) {
                    Y.Dom.setAttribute(el, CLASS_NAME, trim(current + SPACE + className));
                    ret = true;
                }
            } else {
            }

            return ret;
        },
    
        /**
         * Removes a class name from a given element or collection of elements.
         * @method removeClass         
         * @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
         * @return {Boolean | Array} A pass/fail boolean or array of booleans
         */
        removeClass: function(el, className) {
            return Y.Dom.batch(el, Y.Dom._removeClass, className);
        },
        
        _removeClass: function(el, className) {
            var ret = false,
                current,
                newClass,
                attr;

            if (el && className) {
                current = Y.Dom.getAttribute(el, CLASS_NAME) || EMPTY;
                Y.Dom.setAttribute(el, CLASS_NAME, current.replace(Y.Dom._getClassRegex(className), EMPTY));

                newClass = Y.Dom.getAttribute(el, CLASS_NAME);
                if (current !== newClass) { // else nothing changed
                    Y.Dom.setAttribute(el, CLASS_NAME, trim(newClass)); // trim after comparing to current class
                    ret = true;

                    if (Y.Dom.getAttribute(el, CLASS_NAME) === '') { // remove class attribute if empty
                        attr = (el.hasAttribute && el.hasAttribute(_CLASS)) ? _CLASS : CLASS_NAME;
                        el.removeAttribute(attr);
                    }
                }

            } else {
            }

            return ret;
        },
        
        /**
         * Replace a class with another class for a given element or collection of elements.
         * If no oldClassName is present, the newClassName is simply added.
         * @method replaceClass  
         * @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
         * @return {Boolean | Array} A pass/fail boolean or array of booleans
         */
        replaceClass: function(el, oldClassName, newClassName) {
            return Y.Dom.batch(el, Y.Dom._replaceClass, { from: oldClassName, to: newClassName });
        },

        _replaceClass: function(el, classObj) {
            var className,
                from,
                to,
                ret = false,
                current;

            if (el && classObj) {
                from = classObj.from;
                to = classObj.to;

                if (!to) {
                    ret = false;
                }  else if (!from) { // just add if no "from"
                    ret = Y.Dom._addClass(el, classObj.to);
                } else if (from !== to) { // else nothing to replace
                    // May need to lead with DBLSPACE?
                    current = Y.Dom.getAttribute(el, CLASS_NAME) || EMPTY;
                    className = (SPACE + current.replace(Y.Dom._getClassRegex(from), SPACE + to)).
                               split(Y.Dom._getClassRegex(to));

                    // insert to into what would have been the first occurrence slot
                    className.splice(1, 0, SPACE + to);
                    Y.Dom.setAttribute(el, CLASS_NAME, trim(className.join(EMPTY)));
                    ret = true;
                }
            } else {
            }

            return ret;
        },
        
        /**
         * Returns an ID and applies it to the element "el", if provided.
         * @method generateId  
         * @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';

            var f = function(el) {
                if (el && el.id) { // do not override existing ID
                    return el.id;
                }

                var id = prefix + YAHOO.env._id_counter++;

                if (el) {
                    if (el[OWNER_DOCUMENT].getElementById(id)) { // in case one already exists
                        // use failed id plus prefix to help ensure uniqueness
                        return Y.Dom.generateId(el, id + prefix);
                    }
                    el.id = id;
                }
                
                return id;
            };

            // batch fails when no element, so just generate and return single ID
            return Y.Dom.batch(el, f, Y.Dom, true) || f.apply(Y.Dom, arguments);
        },
        
        /**
         * Determines whether an HTMLElement is an ancestor of another HTML element in the DOM hierarchy.
         * @method isAncestor
         * @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 = Y.Dom.get(haystack);
            needle = Y.Dom.get(needle);
            
            var ret = false;

            if ( (haystack && needle) && (haystack[NODE_TYPE] && needle[NODE_TYPE]) ) {
                if (haystack.contains && haystack !== needle) { // contains returns true when equal
                    ret = haystack.contains(needle);
                }
                else if (haystack.compareDocumentPosition) { // gecko
                    ret = !!(haystack.compareDocumentPosition(needle) & 16);
                }
            } else {
            }
            return ret;
        },
        
        /**
         * Determines whether an HTMLElement is present in the current document.
         * @method inDocument         
         * @param {String | HTMLElement} el The element to search for
         * @param {Object} doc An optional document to search, defaults to element's owner document 
         * @return {Boolean} Whether or not the element is present in the current document
         */
        inDocument: function(el, doc) {
            return Y.Dom._inDoc(Y.Dom.get(el), doc);
        },

        _inDoc: function(el, doc) {
            var ret = false;
            if (el && el[TAG_NAME]) {
                doc = doc || el[OWNER_DOCUMENT]; 
                ret = Y.Dom.isAncestor(doc[DOCUMENT_ELEMENT], el);
            } else {
            }
            return ret;
        },
        
        /**
         * Returns a array of HTMLElements that pass the test applied by supplied boolean method.
         * For optimized performance, include a tag and/or root node when possible.
         * Note: This method operates against a live collection, so modifying the 
         * collection in the callback (removing/appending nodes, etc.) will have
         * side effects.  Instead you should iterate the returned nodes array,
         * as you would with the native "getElementsByTagName" method. 
         * @method getElementsBy
         * @param {Function} method - A boolean method for testing elements which receives the element as its only argument.
         * @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 
         * @param {Function} apply (optional) A function to apply to each element when found 
         * @param {Any} o (optional) An optional arg that is passed to the supplied method
         * @param {Boolean} overrides (optional) Whether or not to override the scope of "method" with "o"
         * @return {Array} Array of HTMLElements
         */
        getElementsBy: function(method, tag, root, apply, o, overrides, firstOnly) {
            tag = tag || '*';
            root = (root) ? Y.Dom.get(root) : null || document; 

            if (!root) {
                return [];
            }

            var nodes = [],
                elements = root.getElementsByTagName(tag);
            
            for (var i = 0, len = elements.length; i < len; ++i) {
                if ( method(elements[i]) ) {
                    if (firstOnly) {
                        nodes = elements[i]; 
                        break;
                    } else {
                        nodes[nodes.length] = elements[i];
                    }
                }
            }

            if (apply) {
                Y.Dom.batch(nodes, apply, o, overrides);
            }

            
            return nodes;
        },
        
        /**
         * Returns the first HTMLElement that passes the test applied by the supplied boolean method.
         * @method getElementBy
         * @param {Function} method - A boolean method for testing elements which receives the element as its only argument.
         * @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 {HTMLElement}
         */
        getElementBy: function(method, tag, root) {
            return Y.Dom.getElementsBy(method, tag, root, null, null, null, true); 
        },

        /**
         * Runs the supplied method against each item in the Collection/Array.
         * The method is called with the element(s) as the first arg, and the optional param as the second ( method(el, o) ).
         * @method batch
         * @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 {Any} o (optional) An optional arg that is passed to the supplied method
         * @param {Boolean} overrides (optional) Whether or not to override the scope of "method" with "o"
         * @return {Any | Array} The return value(s) from the supplied method
         */
        batch: function(el, method, o, overrides) {
            var collection = [],
                scope = (overrides) ? o : window;
                
            el = (el && (el[TAG_NAME] || el.item)) ? el : Y.Dom.get(el); // skip get() when possible
            if (el && method) {
                if (el[TAG_NAME] || el.length === undefined) { // element or not array-like 
                    return method.call(scope, el, o);
                } 

                for (var i = 0; i < el.length; ++i) {
                    collection[collection.length] = method.call(scope, el[i], o);
                }
            } else {
                return false;
            } 
            return collection;
        },
        
        /**
         * Returns the height of the document.
         * @method getDocumentHeight
         * @return {Int} The height of the actual document (which includes the body and its margin).
         */
        getDocumentHeight: function() {
            var scrollHeight = (document[COMPAT_MODE] != CSS1_COMPAT || isSafari) ? document.body.scrollHeight : documentElement.scrollHeight,
                h = Math.max(scrollHeight, Y.Dom.getViewportHeight());

            return h;
        },
        
        /**
         * Returns the width of the document.
         * @method getDocumentWidth
         * @return {Int} The width of the actual document (which includes the body and its margin).
         */
        getDocumentWidth: function() {
            var scrollWidth = (document[COMPAT_MODE] != CSS1_COMPAT || isSafari) ? document.body.scrollWidth : documentElement.scrollWidth,
                w = Math.max(scrollWidth, Y.Dom.getViewportWidth());
            return w;
        },

        /**
         * Returns the current height of the viewport.
         * @method getViewportHeight
         * @return {Int} The height of the viewable area of the page (excludes scrollbars).
         */
        getViewportHeight: function() {
            var height = self.innerHeight, // Safari, Opera
                mode = document[COMPAT_MODE];
        
            if ( (mode || isIE) && !isOpera ) { // IE, Gecko
                height = (mode == CSS1_COMPAT) ?
                        documentElement.clientHeight : // Standards
                        document.body.clientHeight; // Quirks
            }
        
            return height;
        },
        
        /**
         * Returns the current width of the viewport.
         * @method getViewportWidth
         * @return {Int} The width of the viewable area of the page (excludes scrollbars).
         */
        
        getViewportWidth: function() {
            var width = self.innerWidth,  // Safari
                mode = document[COMPAT_MODE];
            
            if (mode || isIE) { // IE, Gecko, Opera
                width = (mode == CSS1_COMPAT) ?
                        documentElement.clientWidth : // Standards
                        document.body.clientWidth; // Quirks
            }
            return width;
        },

       /**
         * Returns the nearest ancestor that passes the test applied by supplied boolean method.
         * For performance reasons, IDs are not accepted and argument validation omitted.
         * @method getAncestorBy
         * @param {HTMLElement} node The HTMLElement to use as the starting point 
         * @param {Function} method - A boolean method for testing elements which receives the element as its only argument.
         * @return {Object} HTMLElement or null if not found
         */
        getAncestorBy: function(node, method) {
            while ( (node = node[PARENT_NODE]) ) { // NOTE: assignment
                if ( Y.Dom._testElement(node, method) ) {
                    return node;
                }
            } 

            return null;
        },
        
        /**
         * Returns the nearest ancestor with the given className.
         * @method getAncestorByClassName
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @param {String} className
         * @return {Object} HTMLElement
         */
        getAncestorByClassName: function(node, className) {
            node = Y.Dom.get(node);
            if (!node) {
                return null;
            }
            var method = function(el) { return Y.Dom.hasClass(el, className); };
            return Y.Dom.getAncestorBy(node, method);
        },

        /**
         * Returns the nearest ancestor with the given tagName.
         * @method getAncestorByTagName
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @param {String} tagName
         * @return {Object} HTMLElement
         */
        getAncestorByTagName: function(node, tagName) {
            node = Y.Dom.get(node);
            if (!node) {
                return null;
            }
            var method = function(el) {
                 return el[TAG_NAME] && el[TAG_NAME].toUpperCase() == tagName.toUpperCase();
            };

            return Y.Dom.getAncestorBy(node, method);
        },

        /**
         * Returns the previous sibling that is an HTMLElement. 
         * For performance reasons, IDs are not accepted and argument validation omitted.
         * Returns the nearest HTMLElement sibling if no method provided.
         * @method getPreviousSiblingBy
         * @param {HTMLElement} node The HTMLElement to use as the starting point 
         * @param {Function} method A boolean function used to test siblings
         * that receives the sibling node being tested as its only argument
         * @return {Object} HTMLElement or null if not found
         */
        getPreviousSiblingBy: function(node, method) {
            while (node) {
                node = node.previousSibling;
                if ( Y.Dom._testElement(node, method) ) {
                    return node;
                }
            }
            return null;
        }, 

        /**
         * Returns the previous sibling that is an HTMLElement 
         * @method getPreviousSibling
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @return {Object} HTMLElement or null if not found
         */
        getPreviousSibling: function(node) {
            node = Y.Dom.get(node);
            if (!node) {
                return null;
            }

            return Y.Dom.getPreviousSiblingBy(node);
        }, 

        /**
         * Returns the next HTMLElement sibling that passes the boolean method. 
         * For performance reasons, IDs are not accepted and argument validation omitted.
         * Returns the nearest HTMLElement sibling if no method provided.
         * @method getNextSiblingBy
         * @param {HTMLElement} node The HTMLElement to use as the starting point 
         * @param {Function} method A boolean function used to test siblings
         * that receives the sibling node being tested as its only argument
         * @return {Object} HTMLElement or null if not found
         */
        getNextSiblingBy: function(node, method) {
            while (node) {
                node = node.nextSibling;
                if ( Y.Dom._testElement(node, method) ) {
                    return node;
                }
            }
            return null;
        }, 

        /**
         * Returns the next sibling that is an HTMLElement 
         * @method getNextSibling
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @return {Object} HTMLElement or null if not found
         */
        getNextSibling: function(node) {
            node = Y.Dom.get(node);
            if (!node) {
                return null;
            }

            return Y.Dom.getNextSiblingBy(node);
        }, 

        /**
         * Returns the first HTMLElement child that passes the test method. 
         * @method getFirstChildBy
         * @param {HTMLElement} node The HTMLElement to use as the starting point 
         * @param {Function} method A boolean function used to test children
         * that receives the node being tested as its only argument
         * @return {Object} HTMLElement or null if not found
         */
        getFirstChildBy: function(node, method) {
            var child = ( Y.Dom._testElement(node.firstChild, method) ) ? node.firstChild : null;
            return child || Y.Dom.getNextSiblingBy(node.firstChild, method);
        }, 

        /**
         * Returns the first HTMLElement child. 
         * @method getFirstChild
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @return {Object} HTMLElement or null if not found
         */
        getFirstChild: function(node, method) {
            node = Y.Dom.get(node);
            if (!node) {
                return null;
            }
            return Y.Dom.getFirstChildBy(node);
        }, 

        /**
         * Returns the last HTMLElement child that passes the test method. 
         * @method getLastChildBy
         * @param {HTMLElement} node The HTMLElement to use as the starting point 
         * @param {Function} method A boolean function used to test children
         * that receives the node being tested as its only argument
         * @return {Object} HTMLElement or null if not found
         */
        getLastChildBy: function(node, method) {
            if (!node) {
                return null;
            }
            var child = ( Y.Dom._testElement(node.lastChild, method) ) ? node.lastChild : null;
            return child || Y.Dom.getPreviousSiblingBy(node.lastChild, method);
        }, 

        /**
         * Returns the last HTMLElement child. 
         * @method getLastChild
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @return {Object} HTMLElement or null if not found
         */
        getLastChild: function(node) {
            node = Y.Dom.get(node);
            return Y.Dom.getLastChildBy(node);
        }, 

        /**
         * Returns an array of HTMLElement childNodes that pass the test method. 
         * @method getChildrenBy
         * @param {HTMLElement} node The HTMLElement to start from
         * @param {Function} method A boolean function used to test children
         * that receives the node being tested as its only argument
         * @return {Array} A static array of HTMLElements
         */
        getChildrenBy: function(node, method) {
            var child = Y.Dom.getFirstChildBy(node, method),
                children = child ? [child] : [];

            Y.Dom.getNextSiblingBy(child, function(node) {
                if ( !method || method(node) ) {
                    children[children.length] = node;
                }
                return false; // fail test to collect all children
            });

            return children;
        },
 
        /**
         * Returns an array of HTMLElement childNodes. 
         * @method getChildren
         * @param {String | HTMLElement} node The HTMLElement or an ID to use as the starting point 
         * @return {Array} A static array of HTMLElements
         */
        getChildren: function(node) {
            node = Y.Dom.get(node);
            if (!node) {
            }

            return Y.Dom.getChildrenBy(node);
        },

        /**
         * Returns the left scroll value of the document 
         * @method getDocumentScrollLeft
         * @param {HTMLDocument} document (optional) The document to get the scroll value of
         * @return {Int}  The amount that the document is scrolled to the left
         */
        getDocumentScrollLeft: function(doc) {
            doc = doc || document;
            return Math.max(doc[DOCUMENT_ELEMENT].scrollLeft, doc.body.scrollLeft);
        }, 

        /**
         * Returns the top scroll value of the document 
         * @method getDocumentScrollTop
         * @param {HTMLDocument} document (optional) The document to get the scroll value of
         * @return {Int}  The amount that the document is scrolled to the top
         */
        getDocumentScrollTop: function(doc) {
            doc = doc || document;
            return Math.max(doc[DOCUMENT_ELEMENT].scrollTop, doc.body.scrollTop);
        },

        /**
         * Inserts the new node as the previous sibling of the reference node 
         * @method insertBefore
         * @param {String | HTMLElement} newNode The node to be inserted
         * @param {String | HTMLElement} referenceNode The node to insert the new node before 
         * @return {HTMLElement} The node that was inserted (or null if insert fails) 
         */
        insertBefore: function(newNode, referenceNode) {
            newNode = Y.Dom.get(newNode); 
            referenceNode = Y.Dom.get(referenceNode); 
            
            if (!newNode || !referenceNode || !referenceNode[PARENT_NODE]) {
                return null;
            }       

            return referenceNode[PARENT_NODE].insertBefore(newNode, referenceNode); 
        },

        /**
         * Inserts the new node as the next sibling of the reference node 
         * @method insertAfter
         * @param {String | HTMLElement} newNode The node to be inserted
         * @param {String | HTMLElement} referenceNode The node to insert the new node after 
         * @return {HTMLElement} The node that was inserted (or null if insert fails) 
         */
        insertAfter: function(newNode, referenceNode) {
            newNode = Y.Dom.get(newNode); 
            referenceNode = Y.Dom.get(referenceNode); 
            
            if (!newNode || !referenceNode || !referenceNode[PARENT_NODE]) {
                return null;
            }       

            if (referenceNode.nextSibling) {
                return referenceNode[PARENT_NODE].insertBefore(newNode, referenceNode.nextSibling); 
            } else {
                return referenceNode[PARENT_NODE].appendChild(newNode);
            }
        },

        /**
         * Creates a Region based on the viewport relative to the document. 
         * @method getClientRegion
         * @return {Region} A Region object representing the viewport which accounts for document scroll
         */
        getClientRegion: function() {
            var t = Y.Dom.getDocumentScrollTop(),
                l = Y.Dom.getDocumentScrollLeft(),
                r = Y.Dom.getViewportWidth() + l,
                b = Y.Dom.getViewportHeight() + t;

            return new Y.Region(t, r, b, l);
        },

        /**
         * Provides a normalized attribute interface. 
         * @method setAttibute
         * @param {String | HTMLElement} el The target element for the attribute.
         * @param {String} attr The attribute to set.
         * @param {String} val The value of the attribute.
         */
        setAttribute: function(el, attr, val) {
            attr = Y.Dom.CUSTOM_ATTRIBUTES[attr] || attr;
            el.setAttribute(attr, val);
        },


        /**
         * Provides a normalized attribute interface. 
         * @method getAttibute
         * @param {String | HTMLElement} el The target element for the attribute.
         * @param {String} attr The attribute to get.
         * @return {String} The current value of the attribute. 
         */
        getAttribute: function(el, attr) {
            attr = Y.Dom.CUSTOM_ATTRIBUTES[attr] || attr;
            return el.getAttribute(attr);
        },

        _toCamel: function(property) {
            var c = propertyCache;

            function tU(x,l) {
                return l.toUpperCase();
            }

            return c[property] || (c[property] = property.indexOf('-') === -1 ? 
                                    property :
                                    property.replace( /-([a-z])/gi, tU ));
        },

        _getClassRegex: function(className) {
            var re;
            if (className !== undefined) { // allow empty string to pass
                if (className.exec) { // already a RegExp
                    re = className;
                } else {
                    re = reCache[className];
                    if (!re) {
                        // escape special chars (".", "[", etc.)
                        className = className.replace(Y.Dom._patterns.CLASS_RE_TOKENS, '\\$1');
                        re = reCache[className] = new RegExp(C_START + className + C_END, G);
                    }
                }
            }
            return re;
        },

        _patterns: {
            ROOT_TAG: /^body|html$/i, // body for quirks mode, html for standards,
            CLASS_RE_TOKENS: /([\.\(\)\^\$\*\+\?\|\[\]\{\}])/g
        },


        _testElement: function(node, method) {
            return node && node[NODE_TYPE] == 1 && ( !method || method(node) );
        },

        _calcBorders: function(node, xy2) {
            var t = parseInt(Y.Dom[GET_COMPUTED_STYLE](node, BORDER_TOP_WIDTH), 10) || 0,
                l = parseInt(Y.Dom[GET_COMPUTED_STYLE](node, BORDER_LEFT_WIDTH), 10) || 0;
            if (isGecko) {
                if (RE_TABLE.test(node[TAG_NAME])) {
                    t = 0;
                    l = 0;
                }
            }
            xy2[0] += l;
            xy2[1] += t;
            return xy2;
        }
    };
        
    var _getComputedStyle = Y.Dom[GET_COMPUTED_STYLE];
    // fix opera computedStyle default color unit (convert to rgb)
    if (UA.opera) {
        Y.Dom[GET_COMPUTED_STYLE] = function(node, att) {
            var val = _getComputedStyle(node, att);
            if (RE_COLOR.test(att)) {
                val = Y.Dom.Color.toRGB(val);
            }

            return val;
        };

    }

    // safari converts transparent to rgba(), others use "transparent"
    if (UA.webkit) {
        Y.Dom[GET_COMPUTED_STYLE] = function(node, att) {
            var val = _getComputedStyle(node, att);

            if (val === 'rgba(0, 0, 0, 0)') {
                val = 'transparent'; 
            }

            return val;
        };

    }
})();
/**
 * 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.
 * @namespace YAHOO.util
 * @class Region
 * @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
     * @property top
     * @type Int
     */
    this.top = t;
    
    /**
     * The region's top extent
     * @property y
     * @type Int
     */
    this.y = t;
    
    /**
     * The region's top extent as index, for symmetry with set/getXY
     * @property 1
     * @type Int
     */
    this[1] = t;

    /**
     * The region's right extent
     * @property right
     * @type int
     */
    this.right = r;

    /**
     * The region's bottom extent
     * @property bottom
     * @type Int
     */
    this.bottom = b;

    /**
     * The region's left extent
     * @property left
     * @type Int
     */
    this.left = l;
    
    /**
     * The region's left extent
     * @property x
     * @type Int
     */
    this.x = l;
    
    /**
     * The region's left extent as index, for symmetry with set/getXY
     * @property 0
     * @type Int
     */
    this[0] = l;

    /**
     * The region's total width 
     * @property width 
     * @type Int
     */
    this.width = this.right - this.left;

    /**
     * The region's total height 
     * @property height 
     * @type Int
     */
    this.height = this.bottom - this.top;
};

/**
 * Returns true if this region contains the region passed in
 * @method contains
 * @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
 * @method getArea
 * @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
 * @method intersect
 * @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    ),
        r = Math.min( this.right,  region.right  ),
        b = Math.min( this.bottom, region.bottom ),
        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.
 * @method union
 * @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    ),
        r = Math.max( this.right,  region.right  ),
        b = Math.max( this.bottom, region.bottom ),
        l = Math.min( this.left,   region.left   );

    return new YAHOO.util.Region(t, r, b, l);
};

/**
 * toString
 * @method 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   + 
             ", height: "  + this.height + 
             ", width: "    + this.width   + 
             "}" );
};

/**
 * Returns a region that is occupied by the DOM element
 * @method getRegion
 * @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),
        t = p[1],
        r = p[0] + el.offsetWidth,
        b = p[1] + el.offsetHeight,
        l = p[0];

    return new YAHOO.util.Region(t, r, b, l);
};

/////////////////////////////////////////////////////////////////////////////


/**
 * A point is a region that is special in that it represents a single point on 
 * the grid.
 * @namespace YAHOO.util
 * @class Point
 * @param {Int} x The X position of the point
 * @param {Int} y The Y position of the point
 * @constructor
 * @extends YAHOO.util.Region
 */
YAHOO.util.Point = function(x, y) {
   if (YAHOO.lang.isArray(x)) { // accept input from Dom.getXY, Event.getXY, etc.
      y = x[1]; // dont blow away x yet
      x = x[0];
   }
 
    YAHOO.util.Point.superclass.constructor.call(this, y, x, y, x);
};

YAHOO.extend(YAHOO.util.Point, YAHOO.util.Region);

(function() {
/**
 * Add style management functionality to DOM.
 * @module dom
 * @for Dom
 */

var Y = YAHOO.util, 
    CLIENT_TOP = 'clientTop',
    CLIENT_LEFT = 'clientLeft',
    PARENT_NODE = 'parentNode',
    RIGHT = 'right',
    HAS_LAYOUT = 'hasLayout',
    PX = 'px',
    OPACITY = 'opacity',
    AUTO = 'auto',
    BORDER_LEFT_WIDTH = 'borderLeftWidth',
    BORDER_TOP_WIDTH = 'borderTopWidth',
    BORDER_RIGHT_WIDTH = 'borderRightWidth',
    BORDER_BOTTOM_WIDTH = 'borderBottomWidth',
    VISIBLE = 'visible',
    TRANSPARENT = 'transparent',
    HEIGHT = 'height',
    WIDTH = 'width',
    STYLE = 'style',
    CURRENT_STYLE = 'currentStyle',

// IE getComputedStyle
// TODO: unit-less lineHeight (e.g. 1.22)
    re_size = /^width|height$/,
    re_unit = /^(\d[.\d]*)+(em|ex|px|gd|rem|vw|vh|vm|ch|mm|cm|in|pt|pc|deg|rad|ms|s|hz|khz|%){1}?/i,

    ComputedStyle = {
        get: function(el, property) {
            var value = '',
                current = el[CURRENT_STYLE][property];

            if (property === OPACITY) {
                value = Y.Dom.getStyle(el, OPACITY);        
            } else if (!current || (current.indexOf && current.indexOf(PX) > -1)) { // no need to convert
                value = current;
            } else if (Y.Dom.IE_COMPUTED[property]) { // use compute function
                value = Y.Dom.IE_COMPUTED[property](el, property);
            } else if (re_unit.test(current)) { // convert to pixel
                value = Y.Dom.IE.ComputedStyle.getPixel(el, property);
            } else {
                value = current;
            }

            return value;
        },

        getOffset: function(el, prop) {
            var current = el[CURRENT_STYLE][prop],                        // value of "width", "top", etc.
                capped = prop.charAt(0).toUpperCase() + prop.substr(1), // "Width", "Top", etc.
                offset = 'offset' + capped,                             // "offsetWidth", "offsetTop", etc.
                pixel = 'pixel' + capped,                               // "pixelWidth", "pixelTop", etc.
                value = '',
                actual;

            if (current == AUTO) {
                actual = el[offset]; // offsetHeight/Top etc.
                if (actual === undefined) { // likely "right" or "bottom"
                    value = 0;
                }

                value = actual;
                if (re_size.test(prop)) { // account for box model diff 
                    el[STYLE][prop] = actual; 
                    if (el[offset] > actual) {
                        // the difference is padding + border (works in Standards & Quirks modes)
                        value = actual - (el[offset] - actual);
                    }
                    el[STYLE][prop] = AUTO; // revert to auto
                }
            } else { // convert units to px
                if (!el[STYLE][pixel] && !el[STYLE][prop]) { // need to map style.width to currentStyle (no currentStyle.pixelWidth)
                    el[STYLE][prop] = current;              // no style.pixelWidth if no style.width
                }
                value = el[STYLE][pixel];
            }
            return value + PX;
        },

        getBorderWidth: function(el, property) {
            // clientHeight/Width = paddingBox (e.g. offsetWidth - borderWidth)
            // clientTop/Left = borderWidth
            var value = null;
            if (!el[CURRENT_STYLE][HAS_LAYOUT]) { // TODO: unset layout?
                el[STYLE].zoom = 1; // need layout to measure client
            }

            switch(property) {
                case BORDER_TOP_WIDTH:
                    value = el[CLIENT_TOP];
                    break;
                case BORDER_BOTTOM_WIDTH:
                    value = el.offsetHeight - el.clientHeight - el[CLIENT_TOP];
                    break;
                case BORDER_LEFT_WIDTH:
                    value = el[CLIENT_LEFT];
                    break;
                case BORDER_RIGHT_WIDTH:
                    value = el.offsetWidth - el.clientWidth - el[CLIENT_LEFT];
                    break;
            }
            return value + PX;
        },

        getPixel: function(node, att) {
            // use pixelRight to convert to px
            var val = null,
                styleRight = node[CURRENT_STYLE][RIGHT],
                current = node[CURRENT_STYLE][att];

            node[STYLE][RIGHT] = current;
            val = node[STYLE].pixelRight;
            node[STYLE][RIGHT] = styleRight; // revert

            return val + PX;
        },

        getMargin: function(node, att) {
            var val;
            if (node[CURRENT_STYLE][att] == AUTO) {
                val = 0 + PX;
            } else {
                val = Y.Dom.IE.ComputedStyle.getPixel(node, att);
            }
            return val;
        },

        getVisibility: function(node, att) {
            var current;
            while ( (current = node[CURRENT_STYLE]) && current[att] == 'inherit') { // NOTE: assignment in test
                node = node[PARENT_NODE];
            }
            return (current) ? current[att] : VISIBLE;
        },

        getColor: function(node, att) {
            return Y.Dom.Color.toRGB(node[CURRENT_STYLE][att]) || TRANSPARENT;
        },

        getBorderColor: function(node, att) {
            var current = node[CURRENT_STYLE],
                val = current[att] || current.color;
            return Y.Dom.Color.toRGB(Y.Dom.Color.toHex(val));
        }

    },

//fontSize: getPixelFont,
    IEComputed = {};

IEComputed.top = IEComputed.right = IEComputed.bottom = IEComputed.left = 
        IEComputed[WIDTH] = IEComputed[HEIGHT] = ComputedStyle.getOffset;

IEComputed.color = ComputedStyle.getColor;

IEComputed[BORDER_TOP_WIDTH] = IEComputed[BORDER_RIGHT_WIDTH] =
        IEComputed[BORDER_BOTTOM_WIDTH] = IEComputed[BORDER_LEFT_WIDTH] =
        ComputedStyle.getBorderWidth;

IEComputed.marginTop = IEComputed.marginRight = IEComputed.marginBottom =
        IEComputed.marginLeft = ComputedStyle.getMargin;

IEComputed.visibility = ComputedStyle.getVisibility;
IEComputed.borderColor = IEComputed.borderTopColor =
        IEComputed.borderRightColor = IEComputed.borderBottomColor =
        IEComputed.borderLeftColor = ComputedStyle.getBorderColor;

Y.Dom.IE_COMPUTED = IEComputed;
Y.Dom.IE_ComputedStyle = ComputedStyle;
})();
(function() {
/**
 * Add style management functionality to DOM.
 * @module dom
 * @for Dom
 */

var TO_STRING = 'toString',
    PARSE_INT = parseInt,
    RE = RegExp,
    Y = YAHOO.util;

Y.Dom.Color = {
    KEYWORDS: {
        black: '000',
        silver: 'c0c0c0',
        gray: '808080',
        white: 'fff',
        maroon: '800000',
        red: 'f00',
        purple: '800080',
        fuchsia: 'f0f',
        green: '008000',
        lime: '0f0',
        olive: '808000',
        yellow: 'ff0',
        navy: '000080',
        blue: '00f',
        teal: '008080',
        aqua: '0ff'
    },

    re_RGB: /^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,
    re_hex: /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,
    re_hex3: /([0-9A-F])/gi,

    toRGB: function(val) {
        if (!Y.Dom.Color.re_RGB.test(val)) {
            val = Y.Dom.Color.toHex(val);
        }

        if(Y.Dom.Color.re_hex.exec(val)) {
            val = 'rgb(' + [
                PARSE_INT(RE.$1, 16),
                PARSE_INT(RE.$2, 16),
                PARSE_INT(RE.$3, 16)
            ].join(', ') + ')';
        }
        return val;
    },

    toHex: function(val) {
        val = Y.Dom.Color.KEYWORDS[val] || val;
        if (Y.Dom.Color.re_RGB.exec(val)) {
            var r = (RE.$1.length === 1) ? '0' + RE.$1 : Number(RE.$1),
                g = (RE.$2.length === 1) ? '0' + RE.$2 : Number(RE.$2),
                b = (RE.$3.length === 1) ? '0' + RE.$3 : Number(RE.$3);

            val = [
                r[TO_STRING](16),
                g[TO_STRING](16),
                b[TO_STRING](16)
            ].join('');
        }

        if (val.length < 6) {
            val = val.replace(Y.Dom.Color.re_hex3, '$1$1');
        }

        if (val !== 'transparent' && val.indexOf('#') < 0) {
            val = '#' + val;
        }

        return val.toLowerCase();
    }
};
}());
YAHOO.register("dom", YAHOO.util.Dom, {version: "2.7.0", build: "1799"});
/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.7.0
*/
(function () {

    /**
    * Config is a utility used within an Object to allow the implementer to
    * maintain a list of local configuration properties and listen for changes 
    * to those properties dynamically using CustomEvent. The initial values are 
    * also maintained so that the configuration can be reset at any given point 
    * to its initial state.
    * @namespace YAHOO.util
    * @class Config
    * @constructor
    * @param {Object} owner The owner Object to which this Config Object belongs
    */
    YAHOO.util.Config = function (owner) {

        if (owner) {
            this.init(owner);
        }


    };


    var Lang = YAHOO.lang,
        CustomEvent = YAHOO.util.CustomEvent,
        Config = YAHOO.util.Config;


    /**
     * Constant representing the CustomEvent type for the config changed event.
     * @property YAHOO.util.Config.CONFIG_CHANGED_EVENT
     * @private
     * @static
     * @final
     */
    Config.CONFIG_CHANGED_EVENT = "configChanged";
    
    /**
     * Constant representing the boolean type string
     * @property YAHOO.util.Config.BOOLEAN_TYPE
     * @private
     * @static
     * @final
     */
    Config.BOOLEAN_TYPE = "boolean";
    
    Config.prototype = {
     
        /**
        * Object reference to the owner of this Config Object
        * @property owner
        * @type Object
        */
        owner: null,
        
        /**
        * Boolean flag that specifies whether a queue is currently 
        * being executed
        * @property queueInProgress
        * @type Boolean
        */
        queueInProgress: false,
        
        /**
        * Maintains the local collection of configuration property objects and 
        * their specified values
        * @property config
        * @private
        * @type Object
        */ 
        config: null,
        
        /**
        * Maintains the local collection of configuration property objects as 
        * they were initially applied.
        * This object is used when resetting a property.
        * @property initialConfig
        * @private
        * @type Object
        */ 
        initialConfig: null,
        
        /**
        * Maintains the local, normalized CustomEvent queue
        * @property eventQueue
        * @private
        * @type Object
        */ 
        eventQueue: null,
        
        /**
        * Custom Event, notifying subscribers when Config properties are set 
        * (setProperty is called without the silent flag
        * @event configChangedEvent
        */
        configChangedEvent: null,
    
        /**
        * Initializes the configuration Object and all of its local members.
        * @method init
        * @param {Object} owner The owner Object to which this Config 
        * Object belongs
        */
        init: function (owner) {
    
            this.owner = owner;
    
            this.configChangedEvent = 
                this.createEvent(Config.CONFIG_CHANGED_EVENT);
    
            this.configChangedEvent.signature = CustomEvent.LIST;
            this.queueInProgress = false;
            this.config = {};
            this.initialConfig = {};
            this.eventQueue = [];
        
        },
        
        /**
        * Validates that the value passed in is a Boolean.
        * @method checkBoolean
        * @param {Object} val The value to validate
        * @return {Boolean} true, if the value is valid
        */ 
        checkBoolean: function (val) {
            return (typeof val == Config.BOOLEAN_TYPE);
        },
        
        /**
        * Validates that the value passed in is a number.
        * @method checkNumber
        * @param {Object} val The value to validate
        * @return {Boolean} true, if the value is valid
        */
        checkNumber: function (val) {
            return (!isNaN(val));
        },
        
        /**
        * Fires a configuration property event using the specified value. 
        * @method fireEvent
        * @private
        * @param {String} key The configuration property's name
        * @param {value} Object The value of the correct type for the property
        */ 
        fireEvent: function ( key, value ) {
            var property = this.config[key];
        
            if (property && property.event) {
                property.event.fire(value);
            } 
        },
        
        /**
        * Adds a property to the Config Object's private config hash.
        * @method addProperty
        * @param {String} key The configuration property's name
        * @param {Object} propertyObject The Object containing all of this 
        * property's arguments
        */
        addProperty: function ( key, propertyObject ) {
            key = key.toLowerCase();
        
            this.config[key] = propertyObject;
        
            propertyObject.event = this.createEvent(key, { scope: this.owner });
            propertyObject.event.signature = CustomEvent.LIST;
            
            
            propertyObject.key = key;
        
            if (propertyObject.handler) {
                propertyObject.event.subscribe(propertyObject.handler, 
                    this.owner);
            }
        
            this.setProperty(key, propertyObject.value, true);
            
            if (! propertyObject.suppressEvent) {
                this.queueProperty(key, propertyObject.value);
            }
            
        },
        
        /**
        * Returns a key-value configuration map of the values currently set in  
        * the Config Object.
        * @method getConfig
        * @return {Object} The current config, represented in a key-value map
        */
        getConfig: function () {
        
            var cfg = {},
                currCfg = this.config,
                prop,
                property;
                
            for (prop in currCfg) {
                if (Lang.hasOwnProperty(currCfg, prop)) {
                    property = currCfg[prop];
                    if (property && property.event) {
                        cfg[prop] = property.value;
                    }
                }
            }

            return cfg;
        },
        
        /**
        * Returns the value of specified property.
        * @method getProperty
        * @param {String} key The name of the property
        * @return {Object}  The value of the specified property
        */
        getProperty: function (key) {
            var property = this.config[key.toLowerCase()];
            if (property && property.event) {
                return property.value;
            } else {
                return undefined;
            }
        },
        
        /**
        * Resets the specified property's value to its initial value.
        * @method resetProperty
        * @param {String} key The name of the property
        * @return {Boolean} True is the property was reset, false if not
        */
        resetProperty: function (key) {
    
            key = key.toLowerCase();
        
            var property = this.config[key];
    
            if (property && property.event) {
    
                if (this.initialConfig[key] && 
                    !Lang.isUndefined(this.initialConfig[key])) {
    
                    this.setProperty(key, this.initialConfig[key]);

                    return true;
    
                }
    
            } else {
    
                return false;
            }
    
        },
        
        /**
        * Sets the value of a property. If the silent property is passed as 
        * true, the property's event will not be fired.
        * @method setProperty
        * @param {String} key The name of the property
        * @param {String} value The value to set the property to
        * @param {Boolean} silent Whether the value should be set silently, 
        * without firing the property event.
        * @return {Boolean} True, if the set was successful, false if it failed.
        */
        setProperty: function (key, value, silent) {
        
            var property;
        
            key = key.toLowerCase();
        
            if (this.queueInProgress && ! silent) {
                // Currently running through a queue... 
                this.queueProperty(key,value);
                return true;
    
            } else {
                property = this.config[key];
                if (property && property.event) {
                    if (property.validator && !property.validator(value)) {
                        return false;
                    } else {
                        property.value = value;
                        if (! silent) {
                            this.fireEvent(key, value);
                            this.configChangedEvent.fire([key, value]);
                        }
                        return true;
                    }
                } else {
                    return false;
                }
            }
        },
        
        /**
        * Sets the value of a property and queues its event to execute. If the 
        * event is already scheduled to execute, it is
        * moved from its current position to the end of the queue.
        * @method queueProperty
        * @param {String} key The name of the property
        * @param {String} value The value to set the property to
        * @return {Boolean}  true, if the set was successful, false if 
        * it failed.
        */ 
        queueProperty: function (key, value) {
        
            key = key.toLowerCase();
        
            var property = this.config[key],
                foundDuplicate = false,
                iLen,
                queueItem,
                queueItemKey,
                queueItemValue,
                sLen,
                supercedesCheck,
                qLen,
                queueItemCheck,
                queueItemCheckKey,
                queueItemCheckValue,
                i,
                s,
                q;
                                
            if (property && property.event) {
    
                if (!Lang.isUndefined(value) && property.validator && 
                    !property.validator(value)) { // validator
                    return false;
                } else {
        
                    if (!Lang.isUndefined(value)) {
                        property.value = value;
                    } else {
                        value = property.value;
                    }
        
                    foundDuplicate = false;
                    iLen = this.eventQueue.length;
        
                    for (i = 0; i < iLen; i++) {
                        queueItem = this.eventQueue[i];
        
                        if (queueItem) {
                            queueItemKey = queueItem[0];
                            queueItemValue = queueItem[1];

                            if (queueItemKey == key) {
    
                                /*
                                    found a dupe... push to end of queue, null 
                                    current item, and break
                                */
    
                                this.eventQueue[i] = null;
    
                                this.eventQueue.push(
                                    [key, (!Lang.isUndefined(value) ? 
                                    value : queueItemValue)]);
    
                                foundDuplicate = true;
                                break;
                            }
                        }
                    }
                    
                    // this is a refire, or a new property in the queue
    
                    if (! foundDuplicate && !Lang.isUndefined(value)) { 
                        this.eventQueue.push([key, value]);
                    }
                }
        
                if (property.supercedes) {

                    sLen = property.supercedes.length;

                    for (s = 0; s < sLen; s++) {

                        supercedesCheck = property.supercedes[s];
                        qLen = this.eventQueue.length;

                        for (q = 0; q < qLen; q++) {
                            queueItemCheck = this.eventQueue[q];

                            if (queueItemCheck) {
                                queueItemCheckKey = queueItemCheck[0];
                                queueItemCheckValue = queueItemCheck[1];

                                if (queueItemCheckKey == 
                                    supercedesCheck.toLowerCase() ) {

                                    this.eventQueue.push([queueItemCheckKey, 
                                        queueItemCheckValue]);

                                    this.eventQueue[q] = null;
                                    break;

                                }
                            }
                        }
                    }
                }


                return true;
            } else {
                return false;
            }
        },
        
        /**
        * Fires the event for a property using the property's current value.
        * @method refireEvent
        * @param {String} key The name of the property
        */
        refireEvent: function (key) {
    
            key = key.toLowerCase();
        
            var property = this.config[key];
    
            if (property && property.event && 
    
                !Lang.isUndefined(property.value)) {
    
                if (this.queueInProgress) {
    
                    this.queueProperty(key);
    
                } else {
    
                    this.fireEvent(key, property.value);
    
                }
    
            }
        },
        
        /**
        * Applies a key-value Object literal to the configuration, replacing  
        * any existing values, and queueing the property events.
        * Although the values will be set, fireQueue() must be called for their 
        * associated events to execute.
        * @method applyConfig
        * @param {Object} userConfig The configuration Object literal
        * @param {Boolean} init  When set to true, the initialConfig will 
        * be set to the userConfig passed in, so that calling a reset will 
        * reset the properties to the passed values.
        */
        applyConfig: function (userConfig, init) {
        
            var sKey,
                oConfig;

            if (init) {
                oConfig = {};
                for (sKey in userConfig) {
                    if (Lang.hasOwnProperty(userConfig, sKey)) {
                        oConfig[sKey.toLowerCase()] = userConfig[sKey];
                    }
                }
                this.initialConfig = oConfig;
            }

            for (sKey in userConfig) {
                if (Lang.hasOwnProperty(userConfig, sKey)) {
                    this.queueProperty(sKey, userConfig[sKey]);
                }
            }
        },
        
        /**
        * Refires the events for all configuration properties using their 
        * current values.
        * @method refresh
        */
        refresh: function () {

            var prop;

            for (prop in this.config) {
                if (Lang.hasOwnProperty(this.config, prop)) {
                    this.refireEvent(prop);
                }
            }
        },
        
        /**
        * Fires the normalized list of queued property change events
        * @method fireQueue
        */
        fireQueue: function () {
        
            var i, 
                queueItem,
                key,
                value,
                property;
        
            this.queueInProgress = true;
            for (i = 0;i < this.eventQueue.length; i++) {
                queueItem = this.eventQueue[i];
                if (queueItem) {
        
                    key = queueItem[0];
                    value = queueItem[1];
                    property = this.config[key];

                    property.value = value;

                    // Clear out queue entry, to avoid it being 
                    // re-added to the queue by any queueProperty/supercedes
                    // calls which are invoked during fireEvent
                    this.eventQueue[i] = null;

                    this.fireEvent(key,value);
                }
            }
            
            this.queueInProgress = false;
            this.eventQueue = [];
        },
        
        /**
        * Subscribes an external handler to the change event for any 
        * given property. 
        * @method subscribeToConfigEvent
        * @param {String} key The property name
        * @param {Function} handler The handler function to use subscribe to 
        * the property's event
        * @param {Object} obj The Object to use for scoping the event handler 
        * (see CustomEvent documentation)
        * @param {Boolean} override Optional. If true, will override "this"  
        * within the handler to map to the scope Object passed into the method.
        * @return {Boolean} True, if the subscription was successful, 
        * otherwise false.
        */ 
        subscribeToConfigEvent: function (key, handler, obj, override) {
    
            var property = this.config[key.toLowerCase()];
    
            if (property && property.event) {
                if (!Config.alreadySubscribed(property.event, handler, obj)) {
                    property.event.subscribe(handler, obj, override);
                }
                return true;
            } else {
                return false;
            }
    
        },
        
        /**
        * Unsubscribes an external handler from the change event for any 
        * given property. 
        * @method unsubscribeFromConfigEvent
        * @param {String} key The property name
        * @param {Function} handler The handler function to use subscribe to 
        * the property's event
        * @param {Object} obj The Object to use for scoping the event 
        * handler (see CustomEvent documentation)
        * @return {Boolean} True, if the unsubscription was successful, 
        * otherwise false.
        */
        unsubscribeFromConfigEvent: function (key, handler, obj) {
            var property = this.config[key.toLowerCase()];
            if (property && property.event) {
                return property.event.unsubscribe(handler, obj);
            } else {
                return false;
            }
        },
        
        /**
        * Returns a string representation of the Config object
        * @method toString
        * @return {String} The Config object in string format.
        */
        toString: function () {
            var output = "Config";
            if (this.owner) {
                output += " [" + this.owner.toString() + "]";
            }
            return output;
        },
        
        /**
        * Returns a string representation of the Config object's current 
        * CustomEvent queue
        * @method outputEventQueue
        * @return {String} The string list of CustomEvents currently queued 
        * for execution
        */
        outputEventQueue: function () {

            var output = "",
                queueItem,
                q,
                nQueue = this.eventQueue.length;
              
            for (q = 0; q < nQueue; q++) {
                queueItem = this.eventQueue[q];
                if (queueItem) {
                    output += queueItem[0] + "=" + queueItem[1] + ", ";
                }
            }
            return output;
        },

        /**
        * Sets all properties to null, unsubscribes all listeners from each 
        * property's change event and all listeners from the configChangedEvent.
        * @method destroy
        */
        destroy: function () {

            var oConfig = this.config,
                sProperty,
                oProperty;


            for (sProperty in oConfig) {
            
                if (Lang.hasOwnProperty(oConfig, sProperty)) {

                    oProperty = oConfig[sProperty];

                    oProperty.event.unsubscribeAll();
                    oProperty.event = null;

                }
            
            }
            
            this.configChangedEvent.unsubscribeAll();
            
            this.configChangedEvent = null;
            this.owner = null;
            this.config = null;
            this.initialConfig = null;
            this.eventQueue = null;
        
        }

    };
    
    
    
    /**
    * Checks to determine if a particular function/Object pair are already 
    * subscribed to the specified CustomEvent
    * @method YAHOO.util.Config.alreadySubscribed
    * @static
    * @param {YAHOO.util.CustomEvent} evt The CustomEvent for which to check 
    * the subscriptions
    * @param {Function} fn The function to look for in the subscribers list
    * @param {Object} obj The execution scope Object for the subscription
    * @return {Boolean} true, if the function/Object pair is already subscribed 
    * to the CustomEvent passed in
    */
    Config.alreadySubscribed = function (evt, fn, obj) {
    
        var nSubscribers = evt.subscribers.length,
            subsc,
            i;

        if (nSubscribers > 0) {
            i = nSubscribers - 1;
            do {
                subsc = evt.subscribers[i];
                if (subsc && subsc.obj == obj && subsc.fn == fn) {
                    return true;
                }
            }
            while (i--);
        }

        return false;

    };

    YAHOO.lang.augmentProto(Config, YAHOO.util.EventProvider);

}());
/**
* YAHOO.widget.DateMath is used for simple date manipulation. The class is a static utility
* used for adding, subtracting, and comparing dates.
* @namespace YAHOO.widget
* @class DateMath
*/
YAHOO.widget.DateMath = {
	/**
	* Constant field representing Day
	* @property DAY
	* @static
	* @final
	* @type String
	*/
	DAY : "D",

	/**
	* Constant field representing Week
	* @property WEEK
	* @static
	* @final
	* @type String
	*/
	WEEK : "W",

	/**
	* Constant field representing Year
	* @property YEAR
	* @static
	* @final
	* @type String
	*/
	YEAR : "Y",

	/**
	* Constant field representing Month
	* @property MONTH
	* @static
	* @final
	* @type String
	*/
	MONTH : "M",

	/**
	* Constant field representing one day, in milliseconds
	* @property ONE_DAY_MS
	* @static
	* @final
	* @type Number
	*/
	ONE_DAY_MS : 1000*60*60*24,
	
	/**
	 * Constant field representing the date in first week of January
	 * which identifies the first week of the year.
	 * <p>
	 * In the U.S, Jan 1st is normally used based on a Sunday start of week.
	 * ISO 8601, used widely throughout Europe, uses Jan 4th, based on a Monday start of week.
	 * </p>
	 * @property WEEK_ONE_JAN_DATE
	 * @static
	 * @type Number
	 */
	WEEK_ONE_JAN_DATE : 1,

	/**
	* Adds the specified amount of time to the this instance.
	* @method add
	* @param {Date} date	The JavaScript Date object to perform addition on
	* @param {String} field	The field constant to be used for performing addition.
	* @param {Number} amount	The number of units (measured in the field constant) to add to the date.
	* @return {Date} The resulting Date object
	*/
	add : function(date, field, amount) {
		var d = new Date(date.getTime());
		switch (field) {
			case this.MONTH:
				var newMonth = date.getMonth() + amount;
				var years = 0;

				if (newMonth < 0) {
					while (newMonth < 0) {
						newMonth += 12;
						years -= 1;
					}
				} else if (newMonth > 11) {
					while (newMonth > 11) {
						newMonth -= 12;
						years += 1;
					}
				}

				d.setMonth(newMonth);
				d.setFullYear(date.getFullYear() + years);
				break;
			case this.DAY:
				this._addDays(d, amount);
				// d.setDate(date.getDate() + amount);
				break;
			case this.YEAR:
				d.setFullYear(date.getFullYear() + amount);
				break;
			case this.WEEK:
				this._addDays(d, (amount * 7));
				// d.setDate(date.getDate() + (amount * 7));
				break;
		}
		return d;
	},

	/**
	 * Private helper method to account for bug in Safari 2 (webkit < 420)
	 * when Date.setDate(n) is called with n less than -128 or greater than 127.
	 * <p>
	 * Fix approach and original findings are available here:
	 * http://brianary.blogspot.com/2006/03/safari-date-bug.html
	 * </p>
	 * @method _addDays
	 * @param {Date} d JavaScript date object
	 * @param {Number} nDays The number of days to add to the date object (can be negative)
	 * @private
	 */
	_addDays : function(d, nDays) {
		if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420) {
			if (nDays < 0) {
				// Ensure we don't go below -128 (getDate() is always 1 to 31, so we won't go above 127)
				for(var min = -128; nDays < min; nDays -= min) {
					d.setDate(d.getDate() + min);
				}
			} else {
				// Ensure we don't go above 96 + 31 = 127
				for(var max = 96; nDays > max; nDays -= max) {
					d.setDate(d.getDate() + max);
				}
			}
			// nDays should be remainder between -128 and 96
		}
		d.setDate(d.getDate() + nDays);
	},

	/**
	* Subtracts the specified amount of time from the this instance.
	* @method subtract
	* @param {Date} date	The JavaScript Date object to perform subtraction on
	* @param {Number} field	The this field constant to be used for performing subtraction.
	* @param {Number} amount	The number of units (measured in the field constant) to subtract from the date.
	* @return {Date} The resulting Date object
	*/
	subtract : function(date, field, amount) {
		return this.add(date, field, (amount*-1));
	},

	/**
	* Determines whether a given date is before another date on the calendar.
	* @method before
	* @param {Date} date		The Date object to compare with the compare argument
	* @param {Date} compareTo	The Date object to use for the comparison
	* @return {Boolean} true if the date occurs before the compared date; false if not.
	*/
	before : function(date, compareTo) {
		var ms = compareTo.getTime();
		if (date.getTime() < ms) {
			return true;
		} else {
			return false;
		}
	},

	/**
	* Determines whether a given date is after another date on the calendar.
	* @method after
	* @param {Date} date		The Date object to compare with the compare argument
	* @param {Date} compareTo	The Date object to use for the comparison
	* @return {Boolean} true if the date occurs after the compared date; false if not.
	*/
	after : function(date, compareTo) {
		var ms = compareTo.getTime();
		if (date.getTime() > ms) {
			return true;
		} else {
			return false;
		}
	},

	/**
	* Determines whether a given date is between two other dates on the calendar.
	* @method between
	* @param {Date} date		The date to check for
	* @param {Date} dateBegin	The start of the range
	* @param {Date} dateEnd		The end of the range
	* @return {Boolean} true if the date occurs between the compared dates; false if not.
	*/
	between : function(date, dateBegin, dateEnd) {
		if (this.after(date, dateBegin) && this.before(date, dateEnd)) {
			return true;
		} else {
			return false;
		}
	},
	
	/**
	* Retrieves a JavaScript Date object representing January 1 of any given year.
	* @method getJan1
	* @param {Number} calendarYear		The calendar year for which to retrieve January 1
	* @return {Date}	January 1 of the calendar year specified.
	*/
	getJan1 : function(calendarYear) {
		return this.getDate(calendarYear,0,1);
	},

	/**
	* Calculates the number of days the specified date is from January 1 of the specified calendar year.
	* Passing January 1 to this function would return an offset value of zero.
	* @method getDayOffset
	* @param {Date}	date	The JavaScript date for which to find the offset
	* @param {Number} calendarYear	The calendar year to use for determining the offset
	* @return {Number}	The number of days since January 1 of the given year
	*/
	getDayOffset : function(date, calendarYear) {
		var beginYear = this.getJan1(calendarYear); // Find the start of the year. This will be in week 1.
		
		// Find the number of days the passed in date is away from the calendar year start
		var dayOffset = Math.ceil((date.getTime()-beginYear.getTime()) / this.ONE_DAY_MS);
		return dayOffset;
	},

	/**
	* Calculates the week number for the given date. Can currently support standard
	* U.S. week numbers, based on Jan 1st defining the 1st week of the year, and 
	* ISO8601 week numbers, based on Jan 4th defining the 1st week of the year.
	* 
	* @method getWeekNumber
	* @param {Date}	date The JavaScript date for which to find the week number
	* @param {Number} firstDayOfWeek The index of the first day of the week (0 = Sun, 1 = Mon ... 6 = Sat).
	* Defaults to 0
	* @param {Number} janDate The date in the first week of January which defines week one for the year
	* Defaults to the value of YAHOO.widget.DateMath.WEEK_ONE_JAN_DATE, which is 1 (Jan 1st). 
	* For the U.S, this is normally Jan 1st. ISO8601 uses Jan 4th to define the first week of the year.
	* 
	* @return {Number} The number of the week containing the given date.
	*/
	getWeekNumber : function(date, firstDayOfWeek, janDate) {

		// Setup Defaults
		firstDayOfWeek = firstDayOfWeek || 0;
		janDate = janDate || this.WEEK_ONE_JAN_DATE;

		var targetDate = this.clearTime(date),
			startOfWeek,
			endOfWeek;

		if (targetDate.getDay() === firstDayOfWeek) { 
			startOfWeek = targetDate;
		} else {
			startOfWeek = this.getFirstDayOfWeek(targetDate, firstDayOfWeek);
		}

		var startYear = startOfWeek.getFullYear(),
			startTime = startOfWeek.getTime();

		// DST shouldn't be a problem here, math is quicker than setDate();
		endOfWeek = new Date(startOfWeek.getTime() + 6*this.ONE_DAY_MS);

		var weekNum;
		if (startYear !== endOfWeek.getFullYear() && endOfWeek.getDate() >= janDate) {
			// If years don't match, endOfWeek is in Jan. and if the 
			// week has WEEK_ONE_JAN_DATE in it, it's week one by definition.
			weekNum = 1;
		} else {
			// Get the 1st day of the 1st week, and 
			// find how many days away we are from it.
			var weekOne = this.clearTime(this.getDate(startYear, 0, janDate)),
				weekOneDayOne = this.getFirstDayOfWeek(weekOne, firstDayOfWeek);

			// Round days to smoothen out 1 hr DST diff
			var daysDiff  = Math.round((targetDate.getTime() - weekOneDayOne.getTime())/this.ONE_DAY_MS);

			// Calc. Full Weeks
			var rem = daysDiff % 7;
			var weeksDiff = (daysDiff - rem)/7;
			weekNum = weeksDiff + 1;
		}
		return weekNum;
	},

	/**
	 * Get the first day of the week, for the give date. 
	 * @param {Date} dt The date in the week for which the first day is required.
	 * @param {Number} startOfWeek The index for the first day of the week, 0 = Sun, 1 = Mon ... 6 = Sat (defaults to 0)
	 * @return {Date} The first day of the week
	 */
	getFirstDayOfWeek : function (dt, startOfWeek) {
		startOfWeek = startOfWeek || 0;
		var dayOfWeekIndex = dt.getDay(),
			dayOfWeek = (dayOfWeekIndex - startOfWeek + 7) % 7;

		return this.subtract(dt, this.DAY, dayOfWeek);
	},

	/**
	* Determines if a given week overlaps two different years.
	* @method isYearOverlapWeek
	* @param {Date}	weekBeginDate	The JavaScript Date representing the first day of the week.
	* @return {Boolean}	true if the date overlaps two different years.
	*/
	isYearOverlapWeek : function(weekBeginDate) {
		var overlaps = false;
		var nextWeek = this.add(weekBeginDate, this.DAY, 6);
		if (nextWeek.getFullYear() != weekBeginDate.getFullYear()) {
			overlaps = true;
		}
		return overlaps;
	},

	/**
	* Determines if a given week overlaps two different months.
	* @method isMonthOverlapWeek
	* @param {Date}	weekBeginDate	The JavaScript Date representing the first day of the week.
	* @return {Boolean}	true if the date overlaps two different months.
	*/
	isMonthOverlapWeek : function(weekBeginDate) {
		var overlaps = false;
		var nextWeek = this.add(weekBeginDate, this.DAY, 6);
		if (nextWeek.getMonth() != weekBeginDate.getMonth()) {
			overlaps = true;
		}
		return overlaps;
	},

	/**
	* Gets the first day of a month containing a given date.
	* @method findMonthStart
	* @param {Date}	date	The JavaScript Date used to calculate the month start
	* @return {Date}		The JavaScript Date representing the first day of the month
	*/
	findMonthStart : function(date) {
		var start = this.getDate(date.getFullYear(), date.getMonth(), 1);
		return start;
	},

	/**
	* Gets the last day of a month containing a given date.
	* @method findMonthEnd
	* @param {Date}	date	The JavaScript Date used to calculate the month end
	* @return {Date}		The JavaScript Date representing the last day of the month
	*/
	findMonthEnd : function(date) {
		var start = this.findMonthStart(date);
		var nextMonth = this.add(start, this.MONTH, 1);
		var end = this.subtract(nextMonth, this.DAY, 1);
		return end;
	},

	/**
	* Clears the time fields from a given date, effectively setting the time to 12 noon.
	* @method clearTime
	* @param {Date}	date	The JavaScript Date for which the time fields will be cleared
	* @return {Date}		The JavaScript Date cleared of all time fields
	*/
	clearTime : function(date) {
		date.setHours(12,0,0,0);
		return date;
	},

	/**
	 * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object
	 * are set to 0. The method allows Date instances to be created with the a year less than 100. "new Date(year, month, date)" implementations 
	 * set the year to 19xx if a year (xx) which is less than 100 is provided.
	 * <p>
	 * <em>NOTE:</em>Validation on argument values is not performed. It is the caller's responsibility to ensure
	 * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor.
	 * </p>
	 * @method getDate
	 * @param {Number} y Year.
	 * @param {Number} m Month index from 0 (Jan) to 11 (Dec).
	 * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1.
	 * @return {Date} The JavaScript date object with year, month, date set as provided.
	 */
	getDate : function(y, m, d) {
		var dt = null;
		if (YAHOO.lang.isUndefined(d)) {
			d = 1;
		}
		if (y >= 100) {
			dt = new Date(y, m, d);
		} else {
			dt = new Date();
			dt.setFullYear(y);
			dt.setMonth(m);
			dt.setDate(d);
			dt.setHours(0,0,0,0);
		}
		return dt;
	}
};

/**
* The Calendar component is a UI control that enables users to choose one or more dates from a graphical calendar presented in a one-month or
* multi-month interface. Calendars are generated entirely via script and can be navigated without any page refreshes.
* @module    calendar
* @title    Calendar
* @namespace  YAHOO.widget
* @requires  yahoo,dom,event
*/
(function(){

	var Dom = YAHOO.util.Dom,
		Event = YAHOO.util.Event,
		Lang = YAHOO.lang,
		DateMath = YAHOO.widget.DateMath;

/**
* Calendar is the base class for the Calendar widget. In its most basic
* implementation, it has the ability to render a calendar widget on the page
* that can be manipulated to select a single date, move back and forth between
* months and years.
* <p>To construct the placeholder for the calendar widget, the code is as
* follows:
*	<xmp>
*		<div id="calContainer"></div>
*	</xmp>
* </p>
* <p>
* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong>
* The Calendar can be constructed by simply providing a container ID string, 
* or a reference to a container DIV HTMLElement (the element needs to exist 
* in the document).
* 
* E.g.:
*	<xmp>
*		var c = new YAHOO.widget.Calendar("calContainer", configOptions);
*	</xmp>
* or:
*   <xmp>
*       var containerDiv = YAHOO.util.Dom.get("calContainer");
*		var c = new YAHOO.widget.Calendar(containerDiv, configOptions);
*	</xmp>
* </p>
* <p>
* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix.
* For example if an ID is not provided, and the container's ID is "calContainer", the Calendar's ID will be set to "calContainer_t".
* </p>
* 
* @namespace YAHOO.widget
* @class Calendar
* @constructor
* @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional.
* @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document.
* @param {Object} config optional The configuration object containing the initial configuration values for the Calendar.
*/
function Calendar(id, containerId, config) {
	this.init.apply(this, arguments);
}

/**
* The path to be used for images loaded for the Calendar
* @property YAHOO.widget.Calendar.IMG_ROOT
* @static
* @deprecated	You can now customize images by overriding the calclose, calnavleft and calnavright default CSS classes for the close icon, left arrow and right arrow respectively
* @type String
*/
Calendar.IMG_ROOT = null;

/**
* Type constant used for renderers to represent an individual date (M/D/Y)
* @property YAHOO.widget.Calendar.DATE
* @static
* @final
* @type String
*/
Calendar.DATE = "D";

/**
* Type constant used for renderers to represent an individual date across any year (M/D)
* @property YAHOO.widget.Calendar.MONTH_DAY
* @static
* @final
* @type String
*/
Calendar.MONTH_DAY = "MD";

/**
* Type constant used for renderers to represent a weekday
* @property YAHOO.widget.Calendar.WEEKDAY
* @static
* @final
* @type String
*/
Calendar.WEEKDAY = "WD";

/**
* Type constant used for renderers to represent a range of individual dates (M/D/Y-M/D/Y)
* @property YAHOO.widget.Calendar.RANGE
* @static
* @final
* @type String
*/
Calendar.RANGE = "R";

/**
* Type constant used for renderers to represent a month across any year
* @property YAHOO.widget.Calendar.MONTH
* @static
* @final
* @type String
*/
Calendar.MONTH = "M";

/**
* Constant that represents the total number of date cells that are displayed in a given month
* @property YAHOO.widget.Calendar.DISPLAY_DAYS
* @static
* @final
* @type Number
*/
Calendar.DISPLAY_DAYS = 42;

/**
* Constant used for halting the execution of the remainder of the render stack
* @property YAHOO.widget.Calendar.STOP_RENDER
* @static
* @final
* @type String
*/
Calendar.STOP_RENDER = "S";

/**
* Constant used to represent short date field string formats (e.g. Tu or Feb)
* @property YAHOO.widget.Calendar.SHORT
* @static
* @final
* @type String
*/
Calendar.SHORT = "short";

/**
* Constant used to represent long date field string formats (e.g. Monday or February)
* @property YAHOO.widget.Calendar.LONG
* @static
* @final
* @type String
*/
Calendar.LONG = "long";

/**
* Constant used to represent medium date field string formats (e.g. Mon)
* @property YAHOO.widget.Calendar.MEDIUM
* @static
* @final
* @type String
*/
Calendar.MEDIUM = "medium";

/**
* Constant used to represent single character date field string formats (e.g. M, T, W)
* @property YAHOO.widget.Calendar.ONE_CHAR
* @static
* @final
* @type String
*/
Calendar.ONE_CHAR = "1char";

/**
* The set of default Config property keys and values for the Calendar
* @property YAHOO.widget.Calendar._DEFAULT_CONFIG
* @final
* @static
* @private
* @type Object
*/
Calendar._DEFAULT_CONFIG = {
	// Default values for pagedate and selected are not class level constants - they are set during instance creation 
	PAGEDATE : {key:"pagedate", value:null},
	SELECTED : {key:"selected", value:null},
	TITLE : {key:"title", value:""},
	CLOSE : {key:"close", value:false},
	IFRAME : {key:"iframe", value:(YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) ? true : false},
	MINDATE : {key:"mindate", value:null},
	MAXDATE : {key:"maxdate", value:null},
	MULTI_SELECT : {key:"multi_select", value:false},
	START_WEEKDAY : {key:"start_weekday", value:0},
	SHOW_WEEKDAYS : {key:"show_weekdays", value:true},
	SHOW_WEEK_HEADER : {key:"show_week_header", value:false},
	SHOW_WEEK_FOOTER : {key:"show_week_footer", value:false},
	HIDE_BLANK_WEEKS : {key:"hide_blank_weeks", value:false},
	NAV_ARROW_LEFT: {key:"nav_arrow_left", value:null} ,
	NAV_ARROW_RIGHT : {key:"nav_arrow_right", value:null} ,
	MONTHS_SHORT : {key:"months_short", value:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]},
	MONTHS_LONG: {key:"months_long", value:["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]},
	WEEKDAYS_1CHAR: {key:"weekdays_1char", value:["S", "M", "T", "W", "T", "F", "S"]},
	WEEKDAYS_SHORT: {key:"weekdays_short", value:["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]},
	WEEKDAYS_MEDIUM: {key:"weekdays_medium", value:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]},
	WEEKDAYS_LONG: {key:"weekdays_long", value:["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]},
	LOCALE_MONTHS:{key:"locale_months", value:"long"},
	LOCALE_WEEKDAYS:{key:"locale_weekdays", value:"short"},
	DATE_DELIMITER:{key:"date_delimiter", value:","},
	DATE_FIELD_DELIMITER:{key:"date_field_delimiter", value:"/"},
	DATE_RANGE_DELIMITER:{key:"date_range_delimiter", value:"-"},
	MY_MONTH_POSITION:{key:"my_month_position", value:1},
	MY_YEAR_POSITION:{key:"my_year_position", value:2},
	MD_MONTH_POSITION:{key:"md_month_position", value:1},
	MD_DAY_POSITION:{key:"md_day_position", value:2},
	MDY_MONTH_POSITION:{key:"mdy_month_position", value:1},
	MDY_DAY_POSITION:{key:"mdy_day_position", value:2},
	MDY_YEAR_POSITION:{key:"mdy_year_position", value:3},
	MY_LABEL_MONTH_POSITION:{key:"my_label_month_position", value:1},
	MY_LABEL_YEAR_POSITION:{key:"my_label_year_position", value:2},
	MY_LABEL_MONTH_SUFFIX:{key:"my_label_month_suffix", value:" "},
	MY_LABEL_YEAR_SUFFIX:{key:"my_label_year_suffix", value:""},
	NAV: {key:"navigator", value: null},
	STRINGS : { 
		key:"strings",
		value: {
			previousMonth : "Previous Month",
			nextMonth : "Next Month",
			close: "Close"
		},
		supercedes : ["close", "title"]
	}
};

var DEF_CFG = Calendar._DEFAULT_CONFIG;

/**
* The set of Custom Event types supported by the Calendar
* @property YAHOO.widget.Calendar._EVENT_TYPES
* @final
* @static
* @private
* @type Object
*/
Calendar._EVENT_TYPES = {
	BEFORE_SELECT : "beforeSelect", 
	SELECT : "select",
	BEFORE_DESELECT : "beforeDeselect",
	DESELECT : "deselect",
	CHANGE_PAGE : "changePage",
	BEFORE_RENDER : "beforeRender",
	RENDER : "render",
	BEFORE_DESTROY : "beforeDestroy",
	DESTROY : "destroy",
	RESET : "reset",
	CLEAR : "clear",
	BEFORE_HIDE : "beforeHide",
	HIDE : "hide",
	BEFORE_SHOW : "beforeShow",
	SHOW : "show",
	BEFORE_HIDE_NAV : "beforeHideNav",
	HIDE_NAV : "hideNav",
	BEFORE_SHOW_NAV : "beforeShowNav",
	SHOW_NAV : "showNav",
	BEFORE_RENDER_NAV : "beforeRenderNav",
	RENDER_NAV : "renderNav"
};

/**
* The set of default style constants for the Calendar
* @property YAHOO.widget.Calendar._STYLES
* @final
* @static
* @private
* @type Object
*/
Calendar._STYLES = {
	CSS_ROW_HEADER: "calrowhead",
	CSS_ROW_FOOTER: "calrowfoot",
	CSS_CELL : "calcell",
	CSS_CELL_SELECTOR : "selector",
	CSS_CELL_SELECTED : "selected",
	CSS_CELL_SELECTABLE : "selectable",
	CSS_CELL_RESTRICTED : "restricted",
	CSS_CELL_TODAY : "today",
	CSS_CELL_OOM : "oom",
	CSS_CELL_OOB : "previous",
	CSS_HEADER : "calheader",
	CSS_HEADER_TEXT : "calhead",
	CSS_BODY : "calbody",
	CSS_WEEKDAY_CELL : "calweekdaycell",
	CSS_WEEKDAY_ROW : "calweekdayrow",
	CSS_FOOTER : "calfoot",
	CSS_CALENDAR : "yui-calendar",
	CSS_SINGLE : "single",
	CSS_CONTAINER : "yui-calcontainer",
	CSS_NAV_LEFT : "calnavleft",
	CSS_NAV_RIGHT : "calnavright",
	CSS_NAV : "calnav",
	CSS_CLOSE : "calclose",
	CSS_CELL_TOP : "calcelltop",
	CSS_CELL_LEFT : "calcellleft",
	CSS_CELL_RIGHT : "calcellright",
	CSS_CELL_BOTTOM : "calcellbottom",
	CSS_CELL_HOVER : "calcellhover",
	CSS_CELL_HIGHLIGHT1 : "highlight1",
	CSS_CELL_HIGHLIGHT2 : "highlight2",
	CSS_CELL_HIGHLIGHT3 : "highlight3",
	CSS_CELL_HIGHLIGHT4 : "highlight4"
};

Calendar.prototype = {

	/**
	* The configuration object used to set up the calendars various locale and style options.
	* @property Config
	* @private
	* @deprecated Configuration properties should be set by calling Calendar.cfg.setProperty.
	* @type Object
	*/
	Config : null,

	/**
	* The parent CalendarGroup, only to be set explicitly by the parent group
	* @property parent
	* @type CalendarGroup
	*/	
	parent : null,

	/**
	* The index of this item in the parent group
	* @property index
	* @type Number
	*/
	index : -1,

	/**
	* The collection of calendar table cells
	* @property cells
	* @type HTMLTableCellElement[]
	*/
	cells : null,

	/**
	* The collection of calendar cell dates that is parallel to the cells collection. The array contains dates field arrays in the format of [YYYY, M, D].
	* @property cellDates
	* @type Array[](Number[])
	*/
	cellDates : null,

	/**
	* The id that uniquely identifies this Calendar.
	* @property id
	* @type String
	*/
	id : null,

	/**
	* The unique id associated with the Calendar's container
	* @property containerId
	* @type String
	*/
	containerId: null,

	/**
	* The DOM element reference that points to this calendar's container element. The calendar will be inserted into this element when the shell is rendered.
	* @property oDomContainer
	* @type HTMLElement
	*/
	oDomContainer : null,

	/**
	* A Date object representing today's date.
	* @property today
	* @type Date
	*/
	today : null,

	/**
	* The list of render functions, along with required parameters, used to render cells. 
	* @property renderStack
	* @type Array[]
	*/
	renderStack : null,

	/**
	* A copy of the initial render functions created before rendering.
	* @property _renderStack
	* @private
	* @type Array
	*/
	_renderStack : null,

	/**
	* A reference to the CalendarNavigator instance created for this Calendar.
	* Will be null if the "navigator" configuration property has not been set
	* @property oNavigator
	* @type CalendarNavigator
	*/
	oNavigator : null,

	/**
	* The private list of initially selected dates.
	* @property _selectedDates
	* @private
	* @type Array
	*/
	_selectedDates : null,

	/**
	* A map of DOM event handlers to attach to cells associated with specific CSS class names
	* @property domEventMap
	* @type Object
	*/
	domEventMap : null,

	/**
	 * Protected helper used to parse Calendar constructor/init arguments.
	 *
	 * As of 2.4.0, Calendar supports a simpler constructor 
	 * signature. This method reconciles arguments
	 * received in the pre 2.4.0 and 2.4.0 formats.
	 * 
	 * @protected
	 * @method _parseArgs
	 * @param {Array} Function "arguments" array
	 * @return {Object} Object with id, container, config properties containing
	 * the reconciled argument values.
	 **/
	_parseArgs : function(args) {
		/*
		   2.4.0 Constructors signatures

		   new Calendar(String)
		   new Calendar(HTMLElement)
		   new Calendar(String, ConfigObject)
		   new Calendar(HTMLElement, ConfigObject)

		   Pre 2.4.0 Constructor signatures

		   new Calendar(String, String)
		   new Calendar(String, HTMLElement)
		   new Calendar(String, String, ConfigObject)
		   new Calendar(String, HTMLElement, ConfigObject)
		 */
		var nArgs = {id:null, container:null, config:null};

		if (args && args.length && args.length > 0) {
			switch (args.length) {
				case 1:
					nArgs.id = null;
					nArgs.container = args[0];
					nArgs.config = null;
					break;
				case 2:
					if (Lang.isObject(args[1]) && !args[1].tagName && !(args[1] instanceof String)) {
						nArgs.id = null;
						nArgs.container = args[0];
						nArgs.config = args[1];
					} else {
						nArgs.id = args[0];
						nArgs.container = args[1];
						nArgs.config = null;
					}
					break;
				default: // 3+
					nArgs.id = args[0];
					nArgs.container = args[1];
					nArgs.config = args[2];
					break;
			}
		} else {
		}
		return nArgs;
	},

	/**
	* Initializes the Calendar widget.
	* @method init
	*
	* @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional.
	* @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document.
	* @param {Object} config optional The configuration object containing the initial configuration values for the Calendar.
	*/
	init : function(id, container, config) {
		// Normalize 2.4.0, pre 2.4.0 args
		var nArgs = this._parseArgs(arguments);

		id = nArgs.id;
		container = nArgs.container;
		config = nArgs.config;

		this.oDomContainer = Dom.get(container);

		if (!this.oDomContainer.id) {
			this.oDomContainer.id = Dom.generateId();
		}
		if (!id) {
			id = this.oDomContainer.id + "_t";
		}

		this.id = id;
		this.containerId = this.oDomContainer.id;

		this.initEvents();

		this.today = new Date();
		DateMath.clearTime(this.today);

		/**
		* The Config object used to hold the configuration variables for the Calendar
		* @property cfg
		* @type YAHOO.util.Config
		*/
		this.cfg = new YAHOO.util.Config(this);

		/**
		* The local object which contains the Calendar's options
		* @property Options
		* @type Object
		*/
		this.Options = {};

		/**
		* The local object which contains the Calendar's locale settings
		* @property Locale
		* @type Object
		*/
		this.Locale = {};

		this.initStyles();

		Dom.addClass(this.oDomContainer, this.Style.CSS_CONTAINER);
		Dom.addClass(this.oDomContainer, this.Style.CSS_SINGLE);

		this.cellDates = [];
		this.cells = [];
		this.renderStack = [];
		this._renderStack = [];

		this.setupConfig();

		if (config) {
			this.cfg.applyConfig(config, true);
		}

		this.cfg.fireQueue();
	},

	/**
	* Default Config listener for the iframe property. If the iframe config property is set to true, 
	* renders the built-in IFRAME shim if the container is relatively or absolutely positioned.
	* 
	* @method configIframe
	*/
	configIframe : function(type, args, obj) {
		var useIframe = args[0];
	
		if (!this.parent) {
			if (Dom.inDocument(this.oDomContainer)) {
				if (useIframe) {
					var pos = Dom.getStyle(this.oDomContainer, "position");
					
					if (pos == "absolute" || pos == "relative") {
						
						if (!Dom.inDocument(this.iframe)) {
							this.iframe = document.createElement("iframe");
							this.iframe.src = "javascript:false;";
	
							Dom.setStyle(this.iframe, "opacity", "0");
	
							if (YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) {
								Dom.addClass(this.iframe, "fixedsize");
							}
	
							this.oDomContainer.insertBefore(this.iframe, this.oDomContainer.firstChild);
						}
					}
				} else {
					if (this.iframe) {
						if (this.iframe.parentNode) {
							this.iframe.parentNode.removeChild(this.iframe);
						}
						this.iframe = null;
					}
				}
			}
		}
	},

	/**
	* Default handler for the "title" property
	* @method configTitle
	*/
	configTitle : function(type, args, obj) {
		var title = args[0];

		// "" disables title bar
		if (title) {
			this.createTitleBar(title);
		} else {
			var close = this.cfg.getProperty(DEF_CFG.CLOSE.key);
			if (!close) {
				this.removeTitleBar();
			} else {
				this.createTitleBar("&#160;");
			}
		}
	},
	
	/**
	* Default handler for the "close" property
	* @method configClose
	*/
	configClose : function(type, args, obj) {
		var close = args[0],
			title = this.cfg.getProperty(DEF_CFG.TITLE.key);
	
		if (close) {
			if (!title) {
				this.createTitleBar("&#160;");
			}
			this.createCloseButton();
		} else {
			this.removeCloseButton();
			if (!title) {
				this.removeTitleBar();
			}
		}
	},

	/**
	* Initializes Calendar's built-in CustomEvents
	* @method initEvents
	*/
	initEvents : function() {

		var defEvents = Calendar._EVENT_TYPES,
			CE = YAHOO.util.CustomEvent,
			cal = this; // To help with minification

		/**
		* Fired before a date selection is made
		* @event beforeSelectEvent
		*/
		cal.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT); 

		/**
		* Fired when a date selection is made
		* @event selectEvent
		* @param {Array}	Array of Date field arrays in the format [YYYY, MM, DD].
		*/
		cal.selectEvent = new CE(defEvents.SELECT);

		/**
		* Fired before a date or set of dates is deselected
		* @event beforeDeselectEvent
		*/
		cal.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT);

		/**
		* Fired when a date or set of dates is deselected
		* @event deselectEvent
		* @param {Array}	Array of Date field arrays in the format [YYYY, MM, DD].
		*/
		cal.deselectEvent = new CE(defEvents.DESELECT);
	
		/**
		* Fired when the Calendar page is changed
		* @event changePageEvent
		*/
		cal.changePageEvent = new CE(defEvents.CHANGE_PAGE);
	
		/**
		* Fired before the Calendar is rendered
		* @event beforeRenderEvent
		*/
		cal.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER);
	
		/**
		* Fired when the Calendar is rendered
		* @event renderEvent
		*/
		cal.renderEvent = new CE(defEvents.RENDER);

		/**
		* Fired just before the Calendar is to be destroyed
		* @event beforeDestroyEvent
		*/
		cal.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY);

		/**
		* Fired after the Calendar is destroyed. This event should be used
		* for notification only. When this event is fired, important Calendar instance
		* properties, dom references and event listeners have already been 
		* removed/dereferenced, and hence the Calendar instance is not in a usable 
		* state.
		*
		* @event destroyEvent
		*/
		cal.destroyEvent = new CE(defEvents.DESTROY);

		/**
		* Fired when the Calendar is reset
		* @event resetEvent
		*/
		cal.resetEvent = new CE(defEvents.RESET);

		/**
		* Fired when the Calendar is cleared
		* @event clearEvent
		*/
		cal.clearEvent = new CE(defEvents.CLEAR);

		/**
		* Fired just before the Calendar is to be shown
		* @event beforeShowEvent
		*/
		cal.beforeShowEvent = new CE(defEvents.BEFORE_SHOW);

		/**
		* Fired after the Calendar is shown
		* @event showEvent
		*/
		cal.showEvent = new CE(defEvents.SHOW);

		/**
		* Fired just before the Calendar is to be hidden
		* @event beforeHideEvent
		*/
		cal.beforeHideEvent = new CE(defEvents.BEFORE_HIDE);

		/**
		* Fired after the Calendar is hidden
		* @event hideEvent
		*/
		cal.hideEvent = new CE(defEvents.HIDE);

		/**
		* Fired just before the CalendarNavigator is to be shown
		* @event beforeShowNavEvent
		*/
		cal.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV);
	
		/**
		* Fired after the CalendarNavigator is shown
		* @event showNavEvent
		*/
		cal.showNavEvent = new CE(defEvents.SHOW_NAV);
	
		/**
		* Fired just before the CalendarNavigator is to be hidden
		* @event beforeHideNavEvent
		*/
		cal.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV);
	
		/**
		* Fired after the CalendarNavigator is hidden
		* @event hideNavEvent
		*/
		cal.hideNavEvent = new CE(defEvents.HIDE_NAV);

		/**
		* Fired just before the CalendarNavigator is to be rendered
		* @event beforeRenderNavEvent
		*/
		cal.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV);

		/**
		* Fired after the CalendarNavigator is rendered
		* @event renderNavEvent
		*/
		cal.renderNavEvent = new CE(defEvents.RENDER_NAV);

		cal.beforeSelectEvent.subscribe(cal.onBeforeSelect, this, true);
		cal.selectEvent.subscribe(cal.onSelect, this, true);
		cal.beforeDeselectEvent.subscribe(cal.onBeforeDeselect, this, true);
		cal.deselectEvent.subscribe(cal.onDeselect, this, true);
		cal.changePageEvent.subscribe(cal.onChangePage, this, true);
		cal.renderEvent.subscribe(cal.onRender, this, true);
		cal.resetEvent.subscribe(cal.onReset, this, true);
		cal.clearEvent.subscribe(cal.onClear, this, true);
	},

	/**
	* The default event handler for clicks on the "Previous Month" navigation UI
	*
	* @method doPreviousMonthNav
	* @param {DOMEvent} e	The DOM event
	* @param {Calendar} cal	A reference to the calendar
	*/
	doPreviousMonthNav : function(e, cal) {
		Event.preventDefault(e);
		// previousMonth invoked in a timeout, to allow
		// event to bubble up, with correct target. Calling
		// previousMonth, will call render which will remove 
		// HTML which generated the event, resulting in an 
		// invalid event target in certain browsers.
		setTimeout(function() {
			cal.previousMonth();
			var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_LEFT, "a", cal.oDomContainer);
			if (navs && navs[0]) {
				try {
					navs[0].focus();
				} catch (e) {
					// ignore
				}
			}
		}, 0);
	},

	/**
	 * The default event handler for clicks on the "Next Month" navigation UI
	 *
	 * @method doNextMonthNav
	 * @param {DOMEvent} e	The DOM event
	 * @param {Calendar} cal	A reference to the calendar
	 */
	doNextMonthNav : function(e, cal) {
		Event.preventDefault(e);
		setTimeout(function() {
			cal.nextMonth();
			var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_RIGHT, "a", cal.oDomContainer);
			if (navs && navs[0]) {
				try {
					navs[0].focus();
				} catch (e) {
					// ignore
				}
			}
		}, 0);
	},

	/**
	* The default event handler for date cell selection. Currently attached to 
	* the Calendar's bounding box, referenced by it's <a href="#property_oDomContainer">oDomContainer</a> property.
	*
	* @method doSelectCell
	* @param {DOMEvent} e	The DOM event
	* @param {Calendar} cal	A reference to the calendar
	*/
	doSelectCell : function(e, cal) {
		var cell, d, date, index;

		var target = Event.getTarget(e),
			tagName = target.tagName.toLowerCase(),
			defSelector = false;

		while (tagName != "td" && !Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {

			if (!defSelector && tagName == "a" && Dom.hasClass(target, cal.Style.CSS_CELL_SELECTOR)) {
				defSelector = true;
			}

			target = target.parentNode;
			tagName = target.tagName.toLowerCase();

			if (target == this.oDomContainer || tagName == "html") {
				return;
			}
		}

		if (defSelector) {
			// Stop link href navigation for default renderer
			Event.preventDefault(e);
		}
	
		cell = target;

		if (Dom.hasClass(cell, cal.Style.CSS_CELL_SELECTABLE)) {
			index = cal.getIndexFromId(cell.id);
			if (index > -1) {
				d = cal.cellDates[index];
				if (d) {
					date = DateMath.getDate(d[0],d[1]-1,d[2]);
				
					var link;

					if (cal.Options.MULTI_SELECT) {
						link = cell.getElementsByTagName("a")[0];
						if (link) {
							link.blur();
						}

						var cellDate = cal.cellDates[index];
						var cellDateIndex = cal._indexOfSelectedFieldArray(cellDate);

						if (cellDateIndex > -1) {	
							cal.deselectCell(index);
						} else {
							cal.selectCell(index);
						}	

					} else {
						link = cell.getElementsByTagName("a")[0];
						if (link) {
							link.blur();
						}
						cal.selectCell(index);
					}
				}
			}
		}
	},

	/**
	* The event that is executed when the user hovers over a cell
	* @method doCellMouseOver
	* @param {DOMEvent} e	The event
	* @param {Calendar} cal	A reference to the calendar passed by the Event utility
	*/
	doCellMouseOver : function(e, cal) {
		var target;
		if (e) {
			target = Event.getTarget(e);
		} else {
			target = this;
		}

		while (target.tagName && target.tagName.toLowerCase() != "td") {
			target = target.parentNode;
			if (!target.tagName || target.tagName.toLowerCase() == "html") {
				return;
			}
		}

		if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {
			Dom.addClass(target, cal.Style.CSS_CELL_HOVER);
		}
	},

	/**
	* The event that is executed when the user moves the mouse out of a cell
	* @method doCellMouseOut
	* @param {DOMEvent} e	The event
	* @param {Calendar} cal	A reference to the calendar passed by the Event utility
	*/
	doCellMouseOut : function(e, cal) {
		var target;
		if (e) {
			target = Event.getTarget(e);
		} else {
			target = this;
		}

		while (target.tagName && target.tagName.toLowerCase() != "td") {
			target = target.parentNode;
			if (!target.tagName || target.tagName.toLowerCase() == "html") {
				return;
			}
		}

		if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {
			Dom.removeClass(target, cal.Style.CSS_CELL_HOVER);
		}
	},

	setupConfig : function() {
		var cfg = this.cfg;

		/**
		* The month/year representing the current visible Calendar date (mm/yyyy)
		* @config pagedate
		* @type String | Date
		* @default today's date
		*/
		cfg.addProperty(DEF_CFG.PAGEDATE.key, { value:new Date(), handler:this.configPageDate } );

		/**
		* The date or range of dates representing the current Calendar selection
		* @config selected
		* @type String
		* @default []
		*/
		cfg.addProperty(DEF_CFG.SELECTED.key, { value:[], handler:this.configSelected } );

		/**
		* The title to display above the Calendar's month header
		* @config title
		* @type String
		* @default ""
		*/
		cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } );

		/**
		* Whether or not a close button should be displayed for this Calendar
		* @config close
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } );

		/**
		* Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below.
		* This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be 
		* enabled if required.
		* 
		* @config iframe
		* @type Boolean
		* @default true for IE6 and below, false for all other browsers
		*/
		cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } );

		/**
		* The minimum selectable date in the current Calendar (mm/dd/yyyy)
		* @config mindate
		* @type String | Date
		* @default null
		*/
		cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.configMinDate } );

		/**
		* The maximum selectable date in the current Calendar (mm/dd/yyyy)
		* @config maxdate
		* @type String | Date
		* @default null
		*/
		cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.configMaxDate } );
	
	
		// Options properties
	
		/**
		* True if the Calendar should allow multiple selections. False by default.
		* @config MULTI_SELECT
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.MULTI_SELECT.key,	{ value:DEF_CFG.MULTI_SELECT.value, handler:this.configOptions, validator:cfg.checkBoolean } );

		/**
		* The weekday the week begins on. Default is 0 (Sunday = 0, Monday = 1 ... Saturday = 6).
		* @config START_WEEKDAY
		* @type number
		* @default 0
		*/
		cfg.addProperty(DEF_CFG.START_WEEKDAY.key,	{ value:DEF_CFG.START_WEEKDAY.value, handler:this.configOptions, validator:cfg.checkNumber  } );
	
		/**
		* True if the Calendar should show weekday labels. True by default.
		* @config SHOW_WEEKDAYS
		* @type Boolean
		* @default true
		*/
		cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key,	{ value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.configOptions, validator:cfg.checkBoolean  } );
	
		/**
		* True if the Calendar should show week row headers. False by default.
		* @config SHOW_WEEK_HEADER
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key, { value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.configOptions, validator:cfg.checkBoolean } );
	
		/**
		* True if the Calendar should show week row footers. False by default.
		* @config SHOW_WEEK_FOOTER
		* @type Boolean
		* @default false
		*/	
		cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.configOptions, validator:cfg.checkBoolean } );
	
		/**
		* True if the Calendar should suppress weeks that are not a part of the current month. False by default.
		* @config HIDE_BLANK_WEEKS
		* @type Boolean
		* @default false
		*/	
		cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key, { value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.configOptions, validator:cfg.checkBoolean } );
		
		/**
		* The image that should be used for the left navigation arrow.
		* @config NAV_ARROW_LEFT
		* @type String
		* @deprecated	You can customize the image by overriding the default CSS class for the left arrow - "calnavleft"  
		* @default null
		*/	
		cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key,	{ value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.configOptions } );
	
		/**
		* The image that should be used for the right navigation arrow.
		* @config NAV_ARROW_RIGHT
		* @type String
		* @deprecated	You can customize the image by overriding the default CSS class for the right arrow - "calnavright"
		* @default null
		*/	
		cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key, { value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.configOptions } );
	
		// Locale properties
	
		/**
		* The short month labels for the current locale.
		* @config MONTHS_SHORT
		* @type String[]
		* @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
		*/
		cfg.addProperty(DEF_CFG.MONTHS_SHORT.key,	{ value:DEF_CFG.MONTHS_SHORT.value, handler:this.configLocale } );
		
		/**
		* The long month labels for the current locale.
		* @config MONTHS_LONG
		* @type String[]
		* @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
		*/	
		cfg.addProperty(DEF_CFG.MONTHS_LONG.key,		{ value:DEF_CFG.MONTHS_LONG.value, handler:this.configLocale } );

		/**
		* The 1-character weekday labels for the current locale.
		* @config WEEKDAYS_1CHAR
		* @type String[]
		* @default ["S", "M", "T", "W", "T", "F", "S"]
		*/	
		cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key,	{ value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.configLocale } );
		
		/**
		* The short weekday labels for the current locale.
		* @config WEEKDAYS_SHORT
		* @type String[]
		* @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
		*/	
		cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key,	{ value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.configLocale } );
		
		/**
		* The medium weekday labels for the current locale.
		* @config WEEKDAYS_MEDIUM
		* @type String[]
		* @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
		*/	
		cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key,	{ value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.configLocale } );
		
		/**
		* The long weekday labels for the current locale.
		* @config WEEKDAYS_LONG
		* @type String[]
		* @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
		*/	
		cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key,	{ value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.configLocale } );
	
		/**
		* Refreshes the locale values used to build the Calendar.
		* @method refreshLocale
		* @private
		*/
		var refreshLocale = function() {
			cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key);
			cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key);
		};
	
		cfg.subscribeToConfigEvent(DEF_CFG.START_WEEKDAY.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_SHORT.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_LONG.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_1CHAR.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_SHORT.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_MEDIUM.key, refreshLocale, this, true);
		cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_LONG.key, refreshLocale, this, true);
		
		/**
		* The setting that determines which length of month labels should be used. Possible values are "short" and "long".
		* @config LOCALE_MONTHS
		* @type String
		* @default "long"
		*/	
		cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key,	{ value:DEF_CFG.LOCALE_MONTHS.value, handler:this.configLocaleValues } );
		
		/**
		* The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long".
		* @config LOCALE_WEEKDAYS
		* @type String
		* @default "short"
		*/	
		cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key,	{ value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.configLocaleValues } );
	
		/**
		* The value used to delimit individual dates in a date string passed to various Calendar functions.
		* @config DATE_DELIMITER
		* @type String
		* @default ","
		*/	
		cfg.addProperty(DEF_CFG.DATE_DELIMITER.key,		{ value:DEF_CFG.DATE_DELIMITER.value, handler:this.configLocale } );
	
		/**
		* The value used to delimit date fields in a date string passed to various Calendar functions.
		* @config DATE_FIELD_DELIMITER
		* @type String
		* @default "/"
		*/	
		cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key, { value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.configLocale } );
	
		/**
		* The value used to delimit date ranges in a date string passed to various Calendar functions.
		* @config DATE_RANGE_DELIMITER
		* @type String
		* @default "-"
		*/
		cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key, { value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.configLocale } );
	
		/**
		* The position of the month in a month/year date string
		* @config MY_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key,	{ value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the year in a month/year date string
		* @config MY_YEAR_POSITION
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key,	{ value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the month in a month/day date string
		* @config MD_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key,	{ value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the day in a month/year date string
		* @config MD_DAY_POSITION
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key,		{ value:DEF_CFG.MD_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the month in a month/day/year date string
		* @config MDY_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key,	{ value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the day in a month/day/year date string
		* @config MDY_DAY_POSITION
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key,	{ value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the year in a month/day/year date string
		* @config MDY_YEAR_POSITION
		* @type Number
		* @default 3
		*/
		cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key,	{ value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
		
		/**
		* The position of the month in the month year label string used as the Calendar header
		* @config MY_LABEL_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key,	{ value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
	
		/**
		* The position of the year in the month year label string used as the Calendar header
		* @config MY_LABEL_YEAR_POSITION
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key,	{ value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
		
		/**
		* The suffix used after the month when rendering the Calendar header
		* @config MY_LABEL_MONTH_SUFFIX
		* @type String
		* @default " "
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key,	{ value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.configLocale } );
		
		/**
		* The suffix used after the year when rendering the Calendar header
		* @config MY_LABEL_YEAR_SUFFIX
		* @type String
		* @default ""
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.configLocale } );

		/**
		* Configuration for the Month/Year CalendarNavigator UI which allows the user to jump directly to a 
		* specific Month/Year without having to scroll sequentially through months.
		* <p>
		* Setting this property to null (default value) or false, will disable the CalendarNavigator UI.
		* </p>
		* <p>
		* Setting this property to true will enable the CalendarNavigatior UI with the default CalendarNavigator configuration values.
		* </p>
		* <p>
		* This property can also be set to an object literal containing configuration properties for the CalendarNavigator UI.
		* The configuration object expects the the following case-sensitive properties, with the "strings" property being a nested object.
		* Any properties which are not provided will use the default values (defined in the CalendarNavigator class).
		* </p>
		* <dl>
		* <dt>strings</dt>
		* <dd><em>Object</em> :  An object with the properties shown below, defining the string labels to use in the Navigator's UI
		*     <dl>
		*         <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd>
		*         <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd>
		*         <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd>
		*         <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd>
		*         <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd>
		*     </dl>
		* </dd>
		* <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd>
		* <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd>
		* </dl>
		* <p>E.g.</p>
		* <pre>
		* var navConfig = {
		*	  strings: {
		*		  month:"Calendar Month",
		*		  year:"Calendar Year",
		*		  submit: "Submit",
		*		  cancel: "Cancel",
		*		  invalidYear: "Please enter a valid year"
		*	  },
		*	  monthFormat: YAHOO.widget.Calendar.SHORT,
		*	  initialFocus: "month"
		* }
		* </pre>
		* @config navigator
		* @type {Object|Boolean}
		* @default null
		*/
		cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } );

		/**
		 * The map of UI strings which the Calendar UI uses.
		 *
		 * @config strings
		 * @type {Object}
		 * @default An object with the properties shown below:
		 *     <dl>
		 *         <dt>previousMonth</dt><dd><em>String</em> : The string to use for the "Previous Month" navigation UI. Defaults to "Previous Month".</dd>
		 *         <dt>nextMonth</dt><dd><em>String</em> : The string to use for the "Next Month" navigation UI. Defaults to "Next Month".</dd>
		 *         <dt>close</dt><dd><em>String</em> : The string to use for the close button label. Defaults to "Close".</dd>
		 *     </dl>
		 */
		cfg.addProperty(DEF_CFG.STRINGS.key, { 
			value:DEF_CFG.STRINGS.value,
			handler:this.configStrings,
			validator: function(val) {
				return Lang.isObject(val);
			},
			supercedes:DEF_CFG.STRINGS.supercedes
		});
	},

	/**
	* The default handler for the "strings" property
	* @method configStrings
	*/
	configStrings : function(type, args, obj) {
		var val = Lang.merge(DEF_CFG.STRINGS.value, args[0]);
		this.cfg.setProperty(DEF_CFG.STRINGS.key, val, true);
	},

	/**
	* The default handler for the "pagedate" property
	* @method configPageDate
	*/
	configPageDate : function(type, args, obj) {
		this.cfg.setProperty(DEF_CFG.PAGEDATE.key, this._parsePageDate(args[0]), true);
	},

	/**
	* The default handler for the "mindate" property
	* @method configMinDate
	*/
	configMinDate : function(type, args, obj) {
		var val = args[0];
		if (Lang.isString(val)) {
			val = this._parseDate(val);
			this.cfg.setProperty(DEF_CFG.MINDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2]));
		}
	},

	/**
	* The default handler for the "maxdate" property
	* @method configMaxDate
	*/
	configMaxDate : function(type, args, obj) {
		var val = args[0];
		if (Lang.isString(val)) {
			val = this._parseDate(val);
			this.cfg.setProperty(DEF_CFG.MAXDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2]));
		}
	},

	/**
	* The default handler for the "selected" property
	* @method configSelected
	*/
	configSelected : function(type, args, obj) {
		var selected = args[0],
			cfgSelected = DEF_CFG.SELECTED.key;
		
		if (selected) {
			if (Lang.isString(selected)) {
				this.cfg.setProperty(cfgSelected, this._parseDates(selected), true);
			} 
		}
		if (! this._selectedDates) {
			this._selectedDates = this.cfg.getProperty(cfgSelected);
		}
	},
	
	/**
	* The default handler for all configuration options properties
	* @method configOptions
	*/
	configOptions : function(type, args, obj) {
		this.Options[type.toUpperCase()] = args[0];
	},

	/**
	* The default handler for all configuration locale properties
	* @method configLocale
	*/
	configLocale : function(type, args, obj) {
		this.Locale[type.toUpperCase()] = args[0];

		this.cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key);
		this.cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key);
	},
	
	/**
	* The default handler for all configuration locale field length properties
	* @method configLocaleValues
	*/
	configLocaleValues : function(type, args, obj) {

		type = type.toLowerCase();

		var val = args[0],
			cfg = this.cfg,
			Locale = this.Locale;

		switch (type) {
			case DEF_CFG.LOCALE_MONTHS.key:
				switch (val) {
					case Calendar.SHORT:
						Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_SHORT.key).concat();
						break;
					case Calendar.LONG:
						Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_LONG.key).concat();
						break;
				}
				break;
			case DEF_CFG.LOCALE_WEEKDAYS.key:
				switch (val) {
					case Calendar.ONE_CHAR:
						Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_1CHAR.key).concat();
						break;
					case Calendar.SHORT:
						Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_SHORT.key).concat();
						break;
					case Calendar.MEDIUM:
						Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_MEDIUM.key).concat();
						break;
					case Calendar.LONG:
						Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_LONG.key).concat();
						break;
				}
				
				var START_WEEKDAY = cfg.getProperty(DEF_CFG.START_WEEKDAY.key);
	
				if (START_WEEKDAY > 0) {
					for (var w=0; w < START_WEEKDAY; ++w) {
						Locale.LOCALE_WEEKDAYS.push(Locale.LOCALE_WEEKDAYS.shift());
					}
				}
				break;
		}
	},

	/**
	 * The default handler for the "navigator" property
	 * @method configNavigator
	 */
	configNavigator : function(type, args, obj) {
		var val = args[0];
		if (YAHOO.widget.CalendarNavigator && (val === true || Lang.isObject(val))) {
			if (!this.oNavigator) {
				this.oNavigator = new YAHOO.widget.CalendarNavigator(this);
				// Cleanup DOM Refs/Events before innerHTML is removed.
				this.beforeRenderEvent.subscribe(function () {
					if (!this.pages) {
						this.oNavigator.erase();
					}
				}, this, true);
			}
		} else {
			if (this.oNavigator) {
				this.oNavigator.destroy();
				this.oNavigator = null;
			}
		}
	},

	/**
	* Defines the style constants for the Calendar
	* @method initStyles
	*/
	initStyles : function() {

		var defStyle = Calendar._STYLES;

		this.Style = {
			/**
			* @property Style.CSS_ROW_HEADER
			*/
			CSS_ROW_HEADER: defStyle.CSS_ROW_HEADER,
			/**
			* @property Style.CSS_ROW_FOOTER
			*/
			CSS_ROW_FOOTER: defStyle.CSS_ROW_FOOTER,
			/**
			* @property Style.CSS_CELL
			*/
			CSS_CELL : defStyle.CSS_CELL,
			/**
			* @property Style.CSS_CELL_SELECTOR
			*/
			CSS_CELL_SELECTOR : defStyle.CSS_CELL_SELECTOR,
			/**
			* @property Style.CSS_CELL_SELECTED
			*/
			CSS_CELL_SELECTED : defStyle.CSS_CELL_SELECTED,
			/**
			* @property Style.CSS_CELL_SELECTABLE
			*/
			CSS_CELL_SELECTABLE : defStyle.CSS_CELL_SELECTABLE,
			/**
			* @property Style.CSS_CELL_RESTRICTED
			*/
			CSS_CELL_RESTRICTED : defStyle.CSS_CELL_RESTRICTED,
			/**
			* @property Style.CSS_CELL_TODAY
			*/
			CSS_CELL_TODAY : defStyle.CSS_CELL_TODAY,
			/**
			* @property Style.CSS_CELL_OOM
			*/
			CSS_CELL_OOM : defStyle.CSS_CELL_OOM,
			/**
			* @property Style.CSS_CELL_OOB
			*/
			CSS_CELL_OOB : defStyle.CSS_CELL_OOB,
			/**
			* @property Style.CSS_HEADER
			*/
			CSS_HEADER : defStyle.CSS_HEADER,
			/**
			* @property Style.CSS_HEADER_TEXT
			*/
			CSS_HEADER_TEXT : defStyle.CSS_HEADER_TEXT,
			/**
			* @property Style.CSS_BODY
			*/
			CSS_BODY : defStyle.CSS_BODY,
			/**
			* @property Style.CSS_WEEKDAY_CELL
			*/
			CSS_WEEKDAY_CELL : defStyle.CSS_WEEKDAY_CELL,
			/**
			* @property Style.CSS_WEEKDAY_ROW
			*/
			CSS_WEEKDAY_ROW : defStyle.CSS_WEEKDAY_ROW,
			/**
			* @property Style.CSS_FOOTER
			*/
			CSS_FOOTER : defStyle.CSS_FOOTER,
			/**
			* @property Style.CSS_CALENDAR
			*/
			CSS_CALENDAR : defStyle.CSS_CALENDAR,
			/**
			* @property Style.CSS_SINGLE
			*/
			CSS_SINGLE : defStyle.CSS_SINGLE,
			/**
			* @property Style.CSS_CONTAINER
			*/
			CSS_CONTAINER : defStyle.CSS_CONTAINER,
			/**
			* @property Style.CSS_NAV_LEFT
			*/
			CSS_NAV_LEFT : defStyle.CSS_NAV_LEFT,
			/**
			* @property Style.CSS_NAV_RIGHT
			*/
			CSS_NAV_RIGHT : defStyle.CSS_NAV_RIGHT,
			/**
			* @property Style.CSS_NAV
			*/
			CSS_NAV : defStyle.CSS_NAV,
			/**
			* @property Style.CSS_CLOSE
			*/
			CSS_CLOSE : defStyle.CSS_CLOSE,
			/**
			* @property Style.CSS_CELL_TOP
			*/
			CSS_CELL_TOP : defStyle.CSS_CELL_TOP,
			/**
			* @property Style.CSS_CELL_LEFT
			*/
			CSS_CELL_LEFT : defStyle.CSS_CELL_LEFT,
			/**
			* @property Style.CSS_CELL_RIGHT
			*/
			CSS_CELL_RIGHT : defStyle.CSS_CELL_RIGHT,
			/**
			* @property Style.CSS_CELL_BOTTOM
			*/
			CSS_CELL_BOTTOM : defStyle.CSS_CELL_BOTTOM,
			/**
			* @property Style.CSS_CELL_HOVER
			*/
			CSS_CELL_HOVER : defStyle.CSS_CELL_HOVER,
			/**
			* @property Style.CSS_CELL_HIGHLIGHT1
			*/
			CSS_CELL_HIGHLIGHT1 : defStyle.CSS_CELL_HIGHLIGHT1,
			/**
			* @property Style.CSS_CELL_HIGHLIGHT2
			*/
			CSS_CELL_HIGHLIGHT2 : defStyle.CSS_CELL_HIGHLIGHT2,
			/**
			* @property Style.CSS_CELL_HIGHLIGHT3
			*/
			CSS_CELL_HIGHLIGHT3 : defStyle.CSS_CELL_HIGHLIGHT3,
			/**
			* @property Style.CSS_CELL_HIGHLIGHT4
			*/
			CSS_CELL_HIGHLIGHT4 : defStyle.CSS_CELL_HIGHLIGHT4
		};
	},

	/**
	* Builds the date label that will be displayed in the calendar header or
	* footer, depending on configuration.
	* @method buildMonthLabel
	* @return	{String}	The formatted calendar month label
	*/
	buildMonthLabel : function() {
		return this._buildMonthLabel(this.cfg.getProperty(DEF_CFG.PAGEDATE.key));
	},

    /**
     * Helper method, to format a Month Year string, given a JavaScript Date, based on the 
     * Calendar localization settings
     * 
     * @method _buildMonthLabel
     * @private
     * @param {Date} date
     * @return {String} Formated month, year string
     */
	_buildMonthLabel : function(date) {
		var	monthLabel  = this.Locale.LOCALE_MONTHS[date.getMonth()] + this.Locale.MY_LABEL_MONTH_SUFFIX,
			yearLabel = date.getFullYear() + this.Locale.MY_LABEL_YEAR_SUFFIX;

		if (this.Locale.MY_LABEL_MONTH_POSITION == 2 || this.Locale.MY_LABEL_YEAR_POSITION == 1) {
			return yearLabel + monthLabel;
		} else {
			return monthLabel + yearLabel;
		}
	},
	
	/**
	* Builds the date digit that will be displayed in calendar cells
	* @method buildDayLabel
	* @param {Date}	workingDate	The current working date
	* @return	{String}	The formatted day label
	*/
	buildDayLabel : function(workingDate) {
		return workingDate.getDate();
	},
	
	/**
	 * Creates the title bar element and adds it to Calendar container DIV
	 * 
	 * @method createTitleBar
	 * @param {String} strTitle The title to display in the title bar
	 * @return The title bar element
	 */
	createTitleBar : function(strTitle) {
		var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || document.createElement("div");
		tDiv.className = YAHOO.widget.CalendarGroup.CSS_2UPTITLE;
		tDiv.innerHTML = strTitle;
		this.oDomContainer.insertBefore(tDiv, this.oDomContainer.firstChild);
	
		Dom.addClass(this.oDomContainer, "withtitle");
	
		return tDiv;
	},
	
	/**
	 * Removes the title bar element from the DOM
	 * 
	 * @method removeTitleBar
	 */
	removeTitleBar : function() {
		var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || null;
		if (tDiv) {
			Event.purgeElement(tDiv);
			this.oDomContainer.removeChild(tDiv);
		}
		Dom.removeClass(this.oDomContainer, "withtitle");
	},
	
	/**
	 * Creates the close button HTML element and adds it to Calendar container DIV
	 * 
	 * @method createCloseButton
	 * @return The close HTML element created
	 */
	createCloseButton : function() {
		var cssClose = YAHOO.widget.CalendarGroup.CSS_2UPCLOSE,
			DEPR_CLOSE_PATH = "us/my/bn/x_d.gif",
			lnk = Dom.getElementsByClassName("link-close", "a", this.oDomContainer)[0],
			strings = this.cfg.getProperty(DEF_CFG.STRINGS.key),
			closeStr = (strings && strings.close) ? strings.close : "";

		if (!lnk) {
			lnk = document.createElement("a");
			Event.addListener(lnk, "click", function(e, cal) {
				cal.hide(); 
				Event.preventDefault(e);
			}, this);
		}

		lnk.href = "#";
		lnk.className = "link-close";

		if (Calendar.IMG_ROOT !== null) {
			var img = Dom.getElementsByClassName(cssClose, "img", lnk)[0] || document.createElement("img");
			img.src = Calendar.IMG_ROOT + DEPR_CLOSE_PATH;
			img.className = cssClose;
			lnk.appendChild(img);
		} else {
			lnk.innerHTML = '<span class="' + cssClose + ' ' + this.Style.CSS_CLOSE + '">' + closeStr + '</span>';
		}
		this.oDomContainer.appendChild(lnk);

		return lnk;
	},
	
	/**
	 * Removes the close button HTML element from the DOM
	 * 
	 * @method removeCloseButton
	 */
	removeCloseButton : function() {
		var btn = Dom.getElementsByClassName("link-close", "a", this.oDomContainer)[0] || null;
		if (btn) {
			Event.purgeElement(btn);
			this.oDomContainer.removeChild(btn);
		}
	},

	/**
	* Renders the calendar header.
	* @method renderHeader
	* @param {Array}	html	The current working HTML array
	* @return {Array} The current working HTML array
	*/
	renderHeader : function(html) {


		var colSpan = 7,
			DEPR_NAV_LEFT = "us/tr/callt.gif",
			DEPR_NAV_RIGHT = "us/tr/calrt.gif",
			cfg = this.cfg,
			pageDate = cfg.getProperty(DEF_CFG.PAGEDATE.key),
			strings= cfg.getProperty(DEF_CFG.STRINGS.key),
			prevStr = (strings && strings.previousMonth) ?  strings.previousMonth : "",
			nextStr = (strings && strings.nextMonth) ? strings.nextMonth : "",
            monthLabel;

		if (cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) {
			colSpan += 1;
		}
	
		if (cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) {
			colSpan += 1;
		}

		html[html.length] = "<thead>";
		html[html.length] =		"<tr>";
		html[html.length] =			'<th colspan="' + colSpan + '" class="' + this.Style.CSS_HEADER_TEXT + '">';
		html[html.length] =				'<div class="' + this.Style.CSS_HEADER + '">';

		var renderLeft, renderRight = false;

		if (this.parent) {
			if (this.index === 0) {
				renderLeft = true;
			}
			if (this.index == (this.parent.cfg.getProperty("pages") -1)) {
				renderRight = true;
			}
		} else {
			renderLeft = true;
			renderRight = true;
		}

		if (renderLeft) {
			monthLabel  = this._buildMonthLabel(DateMath.subtract(pageDate, DateMath.MONTH, 1));

			var leftArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_LEFT.key);
			// Check for deprecated customization - If someone set IMG_ROOT, but didn't set NAV_ARROW_LEFT, then set NAV_ARROW_LEFT to the old deprecated value
			if (leftArrow === null && Calendar.IMG_ROOT !== null) {
				leftArrow = Calendar.IMG_ROOT + DEPR_NAV_LEFT;
			}
			var leftStyle = (leftArrow === null) ? "" : ' style="background-image:url(' + leftArrow + ')"';
			html[html.length] = '<a class="' + this.Style.CSS_NAV_LEFT + '"' + leftStyle + ' href="#">' + prevStr + ' (' + monthLabel + ')' + '</a>';
		}

		var lbl = this.buildMonthLabel();
		var cal = this.parent || this;
		if (cal.cfg.getProperty("navigator")) {
			lbl = "<a class=\"" + this.Style.CSS_NAV + "\" href=\"#\">" + lbl + "</a>";
		}
		html[html.length] = lbl;

		if (renderRight) {
			monthLabel  = this._buildMonthLabel(DateMath.add(pageDate, DateMath.MONTH, 1));

			var rightArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_RIGHT.key);
			if (rightArrow === null && Calendar.IMG_ROOT !== null) {
				rightArrow = Calendar.IMG_ROOT + DEPR_NAV_RIGHT;
			}
			var rightStyle = (rightArrow === null) ? "" : ' style="background-image:url(' + rightArrow + ')"';
			html[html.length] = '<a class="' + this.Style.CSS_NAV_RIGHT + '"' + rightStyle + ' href="#">' + nextStr + ' (' + monthLabel + ')' + '</a>';
		}

		html[html.length] =	'</div>\n</th>\n</tr>';

		if (cfg.getProperty(DEF_CFG.SHOW_WEEKDAYS.key)) {
			html = this.buildWeekdays(html);
		}
		
		html[html.length] = '</thead>';
	
		return html;
	},
	
	/**
	* Renders the Calendar's weekday headers.
	* @method buildWeekdays
	* @param {Array}	html	The current working HTML array
	* @return {Array} The current working HTML array
	*/
	buildWeekdays : function(html) {

		html[html.length] = '<tr class="' + this.Style.CSS_WEEKDAY_ROW + '">';

		if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) {
			html[html.length] = '<th>&#160;</th>';
		}

		for(var i=0;i < this.Locale.LOCALE_WEEKDAYS.length; ++i) {
			html[html.length] = '<th class="calweekdaycell">' + this.Locale.LOCALE_WEEKDAYS[i] + '</th>';
		}

		if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) {
			html[html.length] = '<th>&#160;</th>';
		}

		html[html.length] = '</tr>';

		return html;
	},
	
	/**
	* Renders the calendar body.
	* @method renderBody
	* @param {Date}	workingDate	The current working Date being used for the render process
	* @param {Array}	html	The current working HTML array
	* @return {Array} The current working HTML array
	*/
	renderBody : function(workingDate, html) {

		var startDay = this.cfg.getProperty(DEF_CFG.START_WEEKDAY.key);

		this.preMonthDays = workingDate.getDay();
		if (startDay > 0) {
			this.preMonthDays -= startDay;
		}
		if (this.preMonthDays < 0) {
			this.preMonthDays += 7;
		}

		this.monthDays = DateMath.findMonthEnd(workingDate).getDate();
		this.postMonthDays = Calendar.DISPLAY_DAYS-this.preMonthDays-this.monthDays;


		workingDate = DateMath.subtract(workingDate, DateMath.DAY, this.preMonthDays);
	
		var weekNum,
			weekClass,
			weekPrefix = "w",
			cellPrefix = "_cell",
			workingDayPrefix = "wd",
			dayPrefix = "d",
			cellRenderers,
			renderer,
			t = this.today,
			cfg = this.cfg,
			todayYear = t.getFullYear(),
			todayMonth = t.getMonth(),
			todayDate = t.getDate(),
			useDate = cfg.getProperty(DEF_CFG.PAGEDATE.key),
			hideBlankWeeks = cfg.getProperty(DEF_CFG.HIDE_BLANK_WEEKS.key),
			showWeekFooter = cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key),
			showWeekHeader = cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key),
			mindate = cfg.getProperty(DEF_CFG.MINDATE.key),
			maxdate = cfg.getProperty(DEF_CFG.MAXDATE.key);

		if (mindate) {
			mindate = DateMath.clearTime(mindate);
		}
		if (maxdate) {
			maxdate = DateMath.clearTime(maxdate);
		}

		html[html.length] = '<tbody class="m' + (useDate.getMonth()+1) + ' ' + this.Style.CSS_BODY + '">';

		var i = 0,
			tempDiv = document.createElement("div"),
			cell = document.createElement("td");

		tempDiv.appendChild(cell);

		var cal = this.parent || this;

		for (var r=0;r<6;r++) {
			weekNum = DateMath.getWeekNumber(workingDate, startDay);
			weekClass = weekPrefix + weekNum;

			// Local OOM check for performance, since we already have pagedate
			if (r !== 0 && hideBlankWeeks === true && workingDate.getMonth() != useDate.getMonth()) {
				break;
			} else {
				html[html.length] = '<tr class="' + weekClass + '">';

				if (showWeekHeader) { html = this.renderRowHeader(weekNum, html); }

				for (var d=0; d < 7; d++){ // Render actual days

					cellRenderers = [];

					this.clearElement(cell);
					cell.className = this.Style.CSS_CELL;
					cell.id = this.id + cellPrefix + i;

					if (workingDate.getDate()		== todayDate && 
						workingDate.getMonth()		== todayMonth &&
						workingDate.getFullYear()	== todayYear) {
						cellRenderers[cellRenderers.length]=cal.renderCellStyleToday;
					}

					var workingArray = [workingDate.getFullYear(),workingDate.getMonth()+1,workingDate.getDate()];
					this.cellDates[this.cellDates.length] = workingArray; // Add this date to cellDates

					// Local OOM check for performance, since we already have pagedate
					if (workingDate.getMonth() != useDate.getMonth()) {
						cellRenderers[cellRenderers.length]=cal.renderCellNotThisMonth;
					} else {
						Dom.addClass(cell, workingDayPrefix + workingDate.getDay());
						Dom.addClass(cell, dayPrefix + workingDate.getDate());

						for (var s=0;s<this.renderStack.length;++s) {

							renderer = null;

							var rArray = this.renderStack[s],
								type = rArray[0],
								month,
								day,
								year;

							switch (type) {
								case Calendar.DATE:
									month = rArray[1][1];
									day = rArray[1][2];
									year = rArray[1][0];

									if (workingDate.getMonth()+1 == month && workingDate.getDate() == day && workingDate.getFullYear() == year) {
										renderer = rArray[2];
										this.renderStack.splice(s,1);
									}
									break;
								case Calendar.MONTH_DAY:
									month = rArray[1][0];
									day = rArray[1][1];

									if (workingDate.getMonth()+1 == month && workingDate.getDate() == day) {
										renderer = rArray[2];
										this.renderStack.splice(s,1);
									}
									break;
								case Calendar.RANGE:
									var date1 = rArray[1][0],
										date2 = rArray[1][1],
										d1month = date1[1],
										d1day = date1[2],
										d1year = date1[0],
										d1 = DateMath.getDate(d1year, d1month-1, d1day),
										d2month = date2[1],
										d2day = date2[2],
										d2year = date2[0],
										d2 = DateMath.getDate(d2year, d2month-1, d2day);

									if (workingDate.getTime() >= d1.getTime() && workingDate.getTime() <= d2.getTime()) {
										renderer = rArray[2];

										if (workingDate.getTime()==d2.getTime()) { 
											this.renderStack.splice(s,1);
										}
									}
									break;
								case Calendar.WEEKDAY:
									var weekday = rArray[1][0];
									if (workingDate.getDay()+1 == weekday) {
										renderer = rArray[2];
									}
									break;
								case Calendar.MONTH:
									month = rArray[1][0];
									if (workingDate.getMonth()+1 == month) {
										renderer = rArray[2];
									}
									break;
							}

							if (renderer) {
								cellRenderers[cellRenderers.length]=renderer;
							}
						}

					}

					if (this._indexOfSelectedFieldArray(workingArray) > -1) {
						cellRenderers[cellRenderers.length]=cal.renderCellStyleSelected; 
					}

					if ((mindate && (workingDate.getTime() < mindate.getTime())) ||
						(maxdate && (workingDate.getTime() > maxdate.getTime()))
					) {
						cellRenderers[cellRenderers.length]=cal.renderOutOfBoundsDate;
					} else {
						cellRenderers[cellRenderers.length]=cal.styleCellDefault;
						cellRenderers[cellRenderers.length]=cal.renderCellDefault;	
					}

					for (var x=0; x < cellRenderers.length; ++x) {
						if (cellRenderers[x].call(cal, workingDate, cell) == Calendar.STOP_RENDER) {
							break;
						}
					}

					workingDate.setTime(workingDate.getTime() + DateMath.ONE_DAY_MS);
					// Just in case we crossed DST/Summertime boundaries
					workingDate = DateMath.clearTime(workingDate);

					if (i >= 0 && i <= 6) {
						Dom.addClass(cell, this.Style.CSS_CELL_TOP);
					}
					if ((i % 7) === 0) {
						Dom.addClass(cell, this.Style.CSS_CELL_LEFT);
					}
					if (((i+1) % 7) === 0) {
						Dom.addClass(cell, this.Style.CSS_CELL_RIGHT);
					}

					var postDays = this.postMonthDays; 
					if (hideBlankWeeks && postDays >= 7) {
						var blankWeeks = Math.floor(postDays/7);
						for (var p=0;p<blankWeeks;++p) {
							postDays -= 7;
						}
					}
					
					if (i >= ((this.preMonthDays+postDays+this.monthDays)-7)) {
						Dom.addClass(cell, this.Style.CSS_CELL_BOTTOM);
					}
	
					html[html.length] = tempDiv.innerHTML;
					i++;
				}
	
				if (showWeekFooter) { html = this.renderRowFooter(weekNum, html); }
	
				html[html.length] = '</tr>';
			}
		}
	
		html[html.length] = '</tbody>';
	
		return html;
	},
	
	/**
	* Renders the calendar footer. In the default implementation, there is
	* no footer.
	* @method renderFooter
	* @param {Array}	html	The current working HTML array
	* @return {Array} The current working HTML array
	*/
	renderFooter : function(html) { return html; },
	
	/**
	* Renders the calendar after it has been configured. The render() method has a specific call chain that will execute
	* when the method is called: renderHeader, renderBody, renderFooter.
	* Refer to the documentation for those methods for information on 
	* individual render tasks.
	* @method render
	*/
	render : function() {
		this.beforeRenderEvent.fire();

		// Find starting day of the current month
		var workingDate = DateMath.findMonthStart(this.cfg.getProperty(DEF_CFG.PAGEDATE.key));

		this.resetRenderers();
		this.cellDates.length = 0;

		Event.purgeElement(this.oDomContainer, true);

		var html = [];

		html[html.length] = '<table cellSpacing="0" class="' + this.Style.CSS_CALENDAR + ' y' + workingDate.getFullYear() + '" id="' + this.id + '">';
		html = this.renderHeader(html);
		html = this.renderBody(workingDate, html);
		html = this.renderFooter(html);
		html[html.length] = '</table>';

		this.oDomContainer.innerHTML = html.join("\n");

		this.applyListeners();
		this.cells = this.oDomContainer.getElementsByTagName("td");
	
		this.cfg.refireEvent(DEF_CFG.TITLE.key);
		this.cfg.refireEvent(DEF_CFG.CLOSE.key);
		this.cfg.refireEvent(DEF_CFG.IFRAME.key);

		this.renderEvent.fire();
	},

	/**
	* Applies the Calendar's DOM listeners to applicable elements.
	* @method applyListeners
	*/
	applyListeners : function() {
		var root = this.oDomContainer,
			cal = this.parent || this,
			anchor = "a",
			click = "click";

		var linkLeft = Dom.getElementsByClassName(this.Style.CSS_NAV_LEFT, anchor, root),
			linkRight = Dom.getElementsByClassName(this.Style.CSS_NAV_RIGHT, anchor, root);

		if (linkLeft && linkLeft.length > 0) {
			this.linkLeft = linkLeft[0];
			Event.addListener(this.linkLeft, click, this.doPreviousMonthNav, cal, true);
		}

		if (linkRight && linkRight.length > 0) {
			this.linkRight = linkRight[0];
			Event.addListener(this.linkRight, click, this.doNextMonthNav, cal, true);
		}

		if (cal.cfg.getProperty("navigator") !== null) {
			this.applyNavListeners();
		}

		if (this.domEventMap) {
			var el,elements;
			for (var cls in this.domEventMap) {	
				if (Lang.hasOwnProperty(this.domEventMap, cls)) {
					var items = this.domEventMap[cls];
	
					if (! (items instanceof Array)) {
						items = [items];
					}
	
					for (var i=0;i<items.length;i++)	{
						var item = items[i];
						elements = Dom.getElementsByClassName(cls, item.tag, this.oDomContainer);
	
						for (var c=0;c<elements.length;c++) {
							el = elements[c];
							 Event.addListener(el, item.event, item.handler, item.scope, item.correct );
						}
					}
				}
			}
		}

		Event.addListener(this.oDomContainer, "click", this.doSelectCell, this);
		Event.addListener(this.oDomContainer, "mouseover", this.doCellMouseOver, this);
		Event.addListener(this.oDomContainer, "mouseout", this.doCellMouseOut, this);
	},

	applyNavListeners : function() {
		var calParent = this.parent || this,
			cal = this,
			navBtns = Dom.getElementsByClassName(this.Style.CSS_NAV, "a", this.oDomContainer);

		if (navBtns.length > 0) {

			Event.addListener(navBtns, "click", function (e, obj) {
				var target = Event.getTarget(e);
				// this == navBtn
				if (this === target || Dom.isAncestor(this, target)) {
					Event.preventDefault(e);
				}
				var navigator = calParent.oNavigator;
				if (navigator) {
					var pgdate = cal.cfg.getProperty("pagedate");
					navigator.setYear(pgdate.getFullYear());
					navigator.setMonth(pgdate.getMonth());
					navigator.show();
				}
			});
		}
	},

	/**
	* Retrieves the Date object for the specified Calendar cell
	* @method getDateByCellId
	* @param {String}	id	The id of the cell
	* @return {Date} The Date object for the specified Calendar cell
	*/
	getDateByCellId : function(id) {
		var date = this.getDateFieldsByCellId(id);
		return (date) ? DateMath.getDate(date[0],date[1]-1,date[2]) : null;
	},
	
	/**
	* Retrieves the Date object for the specified Calendar cell
	* @method getDateFieldsByCellId
	* @param {String}	id	The id of the cell
	* @return {Array}	The array of Date fields for the specified Calendar cell
	*/
	getDateFieldsByCellId : function(id) {
		id = this.getIndexFromId(id);
		return (id > -1) ? this.cellDates[id] : null;
	},

	/**
	 * Find the Calendar's cell index for a given date.
	 * If the date is not found, the method returns -1.
	 * <p>
	 * The returned index can be used to lookup the cell HTMLElement  
	 * using the Calendar's cells array or passed to selectCell to select 
	 * cells by index. 
	 * </p>
	 *
	 * See <a href="#cells">cells</a>, <a href="#selectCell">selectCell</a>.
	 *
	 * @method getCellIndex
	 * @param {Date} date JavaScript Date object, for which to find a cell index.
	 * @return {Number} The index of the date in Calendars cellDates/cells arrays, or -1 if the date 
	 * is not on the curently rendered Calendar page.
	 */
	getCellIndex : function(date) {
		var idx = -1;
		if (date) {
			var m = date.getMonth(),
				y = date.getFullYear(),
				d = date.getDate(),
				dates = this.cellDates;

			for (var i = 0; i < dates.length; ++i) {
				var cellDate = dates[i];
				if (cellDate[0] === y && cellDate[1] === m+1 && cellDate[2] === d) {
					idx = i;
					break;
				}
			}
		}
		return idx;
	},

	/**
	 * Given the id used to mark each Calendar cell, this method
	 * extracts the index number from the id.
	 * 
	 * @param {String} strId The cell id
	 * @return {Number} The index of the cell, or -1 if id does not contain an index number
	 */
	getIndexFromId : function(strId) {
		var idx = -1,
			li = strId.lastIndexOf("_cell");

		if (li > -1) {
			idx = parseInt(strId.substring(li + 5), 10);
		}

		return idx;
	},
	
	// BEGIN BUILT-IN TABLE CELL RENDERERS
	
	/**
	* Renders a cell that falls before the minimum date or after the maximum date.
	* widget class.
	* @method renderOutOfBoundsDate
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	* @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
	*			should not be terminated
	*/
	renderOutOfBoundsDate : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_OOB);
		cell.innerHTML = workingDate.getDate();
		return Calendar.STOP_RENDER;
	},
	
	/**
	* Renders the row header for a week.
	* @method renderRowHeader
	* @param {Number}	weekNum	The week number of the current row
	* @param {Array}	cell	The current working HTML array
	*/
	renderRowHeader : function(weekNum, html) {
		html[html.length] = '<th class="calrowhead">' + weekNum + '</th>';
		return html;
	},
	
	/**
	* Renders the row footer for a week.
	* @method renderRowFooter
	* @param {Number}	weekNum	The week number of the current row
	* @param {Array}	cell	The current working HTML array
	*/
	renderRowFooter : function(weekNum, html) {
		html[html.length] = '<th class="calrowfoot">' + weekNum + '</th>';
		return html;
	},
	
	/**
	* Renders a single standard calendar cell in the calendar widget table.
	* All logic for determining how a standard default cell will be rendered is 
	* encapsulated in this method, and must be accounted for when extending the
	* widget class.
	* @method renderCellDefault
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellDefault : function(workingDate, cell) {
		cell.innerHTML = '<a href="#" class="' + this.Style.CSS_CELL_SELECTOR + '">' + this.buildDayLabel(workingDate) + "</a>";
	},
	
	/**
	* Styles a selectable cell.
	* @method styleCellDefault
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	styleCellDefault : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_SELECTABLE);
	},
	
	
	/**
	* Renders a single standard calendar cell using the CSS hightlight1 style
	* @method renderCellStyleHighlight1
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellStyleHighlight1 : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT1);
	},
	
	/**
	* Renders a single standard calendar cell using the CSS hightlight2 style
	* @method renderCellStyleHighlight2
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellStyleHighlight2 : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT2);
	},
	
	/**
	* Renders a single standard calendar cell using the CSS hightlight3 style
	* @method renderCellStyleHighlight3
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellStyleHighlight3 : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT3);
	},
	
	/**
	* Renders a single standard calendar cell using the CSS hightlight4 style
	* @method renderCellStyleHighlight4
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellStyleHighlight4 : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT4);
	},
	
	/**
	* Applies the default style used for rendering today's date to the current calendar cell
	* @method renderCellStyleToday
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	*/
	renderCellStyleToday : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_TODAY);
	},
	
	/**
	* Applies the default style used for rendering selected dates to the current calendar cell
	* @method renderCellStyleSelected
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	* @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
	*			should not be terminated
	*/
	renderCellStyleSelected : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_SELECTED);
	},
	
	/**
	* Applies the default style used for rendering dates that are not a part of the current
	* month (preceding or trailing the cells for the current month)
	* @method renderCellNotThisMonth
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	* @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
	*			should not be terminated
	*/
	renderCellNotThisMonth : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL_OOM);
		cell.innerHTML=workingDate.getDate();
		return Calendar.STOP_RENDER;
	},
	
	/**
	* Renders the current calendar cell as a non-selectable "black-out" date using the default
	* restricted style.
	* @method renderBodyCellRestricted
	* @param {Date}					workingDate		The current working Date object being used to generate the calendar
	* @param {HTMLTableCellElement}	cell			The current working cell in the calendar
	* @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
	*			should not be terminated
	*/
	renderBodyCellRestricted : function(workingDate, cell) {
		Dom.addClass(cell, this.Style.CSS_CELL);
		Dom.addClass(cell, this.Style.CSS_CELL_RESTRICTED);
		cell.innerHTML=workingDate.getDate();
		return Calendar.STOP_RENDER;
	},
	
	// END BUILT-IN TABLE CELL RENDERERS
	
	// BEGIN MONTH NAVIGATION METHODS
	
	/**
	* Adds the designated number of months to the current calendar month, and sets the current
	* calendar page date to the new month.
	* @method addMonths
	* @param {Number}	count	The number of months to add to the current calendar
	*/
	addMonths : function(count) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key;
		this.cfg.setProperty(cfgPageDate, DateMath.add(this.cfg.getProperty(cfgPageDate), DateMath.MONTH, count));
		this.resetRenderers();
		this.changePageEvent.fire();
	},
	
	/**
	* Subtracts the designated number of months from the current calendar month, and sets the current
	* calendar page date to the new month.
	* @method subtractMonths
	* @param {Number}	count	The number of months to subtract from the current calendar
	*/
	subtractMonths : function(count) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key;
		this.cfg.setProperty(cfgPageDate, DateMath.subtract(this.cfg.getProperty(cfgPageDate), DateMath.MONTH, count));
		this.resetRenderers();
		this.changePageEvent.fire();
	},

	/**
	* Adds the designated number of years to the current calendar, and sets the current
	* calendar page date to the new month.
	* @method addYears
	* @param {Number}	count	The number of years to add to the current calendar
	*/
	addYears : function(count) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key;
		this.cfg.setProperty(cfgPageDate, DateMath.add(this.cfg.getProperty(cfgPageDate), DateMath.YEAR, count));
		this.resetRenderers();
		this.changePageEvent.fire();
	},
	
	/**
	* Subtcats the designated number of years from the current calendar, and sets the current
	* calendar page date to the new month.
	* @method subtractYears
	* @param {Number}	count	The number of years to subtract from the current calendar
	*/
	subtractYears : function(count) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key;
		this.cfg.setProperty(cfgPageDate, DateMath.subtract(this.cfg.getProperty(cfgPageDate), DateMath.YEAR, count));
		this.resetRenderers();
		this.changePageEvent.fire();
	},
	
	/**
	* Navigates to the next month page in the calendar widget.
	* @method nextMonth
	*/
	nextMonth : function() {
		this.addMonths(1);
	},
	
	/**
	* Navigates to the previous month page in the calendar widget.
	* @method previousMonth
	*/
	previousMonth : function() {
		this.subtractMonths(1);
	},
	
	/**
	* Navigates to the next year in the currently selected month in the calendar widget.
	* @method nextYear
	*/
	nextYear : function() {
		this.addYears(1);
	},
	
	/**
	* Navigates to the previous year in the currently selected month in the calendar widget.
	* @method previousYear
	*/
	previousYear : function() {
		this.subtractYears(1);
	},
	
	// END MONTH NAVIGATION METHODS
	
	// BEGIN SELECTION METHODS
	
	/**
	* Resets the calendar widget to the originally selected month and year, and 
	* sets the calendar to the initial selection(s).
	* @method reset
	*/
	reset : function() {
		this.cfg.resetProperty(DEF_CFG.SELECTED.key);
		this.cfg.resetProperty(DEF_CFG.PAGEDATE.key);
		this.resetEvent.fire();
	},
	
	/**
	* Clears the selected dates in the current calendar widget and sets the calendar
	* to the current month and year.
	* @method clear
	*/
	clear : function() {
		this.cfg.setProperty(DEF_CFG.SELECTED.key, []);
		this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.today.getTime()));
		this.clearEvent.fire();
	},
	
	/**
	* Selects a date or a collection of dates on the current calendar. This method, by default,
	* does not call the render method explicitly. Once selection has completed, render must be 
	* called for the changes to be reflected visually.
	*
	* Any dates which are OOB (out of bounds, not selectable) will not be selected and the array of 
	* selected dates passed to the selectEvent will not contain OOB dates.
	* 
	* If all dates are OOB, the no state change will occur; beforeSelect and select events will not be fired.
	*
	* @method select
	* @param	{String/Date/Date[]}	date	The date string of dates to select in the current calendar. Valid formats are
	*								individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
	*								Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
	*								This method can also take a JavaScript Date object or an array of Date objects.
	* @return	{Date[]}			Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	select : function(date) {

		var aToBeSelected = this._toFieldArray(date),
			validDates = [],
			selected = [],
			cfgSelected = DEF_CFG.SELECTED.key;

		
		for (var a=0; a < aToBeSelected.length; ++a) {
			var toSelect = aToBeSelected[a];

			if (!this.isDateOOB(this._toDate(toSelect))) {

				if (validDates.length === 0) {
					this.beforeSelectEvent.fire();
					selected = this.cfg.getProperty(cfgSelected);
				}
				validDates.push(toSelect);

				if (this._indexOfSelectedFieldArray(toSelect) == -1) { 
					selected[selected.length] = toSelect;
				}
			}
		}


		if (validDates.length > 0) {
			if (this.parent) {
				this.parent.cfg.setProperty(cfgSelected, selected);
			} else {
				this.cfg.setProperty(cfgSelected, selected);
			}
			this.selectEvent.fire(validDates);
		}

		return this.getSelectedDates();
	},
	
	/**
	* Selects a date on the current calendar by referencing the index of the cell that should be selected.
	* This method is used to easily select a single cell (usually with a mouse click) without having to do
	* a full render. The selected style is applied to the cell directly.
	*
	* If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month 
	* or out of bounds cells), it will not be selected and in such a case beforeSelect and select events will not be fired.
	* 
	* @method selectCell
	* @param	{Number}	cellIndex	The index of the cell to select in the current calendar. 
	* @return	{Date[]}	Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	selectCell : function(cellIndex) {

		var cell = this.cells[cellIndex],
			cellDate = this.cellDates[cellIndex],
			dCellDate = this._toDate(cellDate),
			selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE);


		if (selectable) {
	
			this.beforeSelectEvent.fire();
	
			var cfgSelected = DEF_CFG.SELECTED.key;
			var selected = this.cfg.getProperty(cfgSelected);
	
			var selectDate = cellDate.concat();
	
			if (this._indexOfSelectedFieldArray(selectDate) == -1) {
				selected[selected.length] = selectDate;
			}
			if (this.parent) {
				this.parent.cfg.setProperty(cfgSelected, selected);
			} else {
				this.cfg.setProperty(cfgSelected, selected);
			}
			this.renderCellStyleSelected(dCellDate,cell);
			this.selectEvent.fire([selectDate]);
	
			this.doCellMouseOut.call(cell, null, this);		
		}
	
		return this.getSelectedDates();
	},
	
	/**
	* Deselects a date or a collection of dates on the current calendar. This method, by default,
	* does not call the render method explicitly. Once deselection has completed, render must be 
	* called for the changes to be reflected visually.
	* 
	* The method will not attempt to deselect any dates which are OOB (out of bounds, and hence not selectable) 
	* and the array of deselected dates passed to the deselectEvent will not contain any OOB dates.
	* 
	* If all dates are OOB, beforeDeselect and deselect events will not be fired.
	* 
	* @method deselect
	* @param	{String/Date/Date[]}	date	The date string of dates to deselect in the current calendar. Valid formats are
	*								individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
	*								Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
	*								This method can also take a JavaScript Date object or an array of Date objects.	
	* @return	{Date[]}			Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	deselect : function(date) {

		var aToBeDeselected = this._toFieldArray(date),
			validDates = [],
			selected = [],
			cfgSelected = DEF_CFG.SELECTED.key;


		for (var a=0; a < aToBeDeselected.length; ++a) {
			var toDeselect = aToBeDeselected[a];
	
			if (!this.isDateOOB(this._toDate(toDeselect))) {
	
				if (validDates.length === 0) {
					this.beforeDeselectEvent.fire();
					selected = this.cfg.getProperty(cfgSelected);
				}
	
				validDates.push(toDeselect);
	
				var index = this._indexOfSelectedFieldArray(toDeselect);
				if (index != -1) {	
					selected.splice(index,1);
				}
			}
		}
	
	
		if (validDates.length > 0) {
			if (this.parent) {
				this.parent.cfg.setProperty(cfgSelected, selected);
			} else {
				this.cfg.setProperty(cfgSelected, selected);
			}
			this.deselectEvent.fire(validDates);
		}
	
		return this.getSelectedDates();
	},
	
	/**
	* Deselects a date on the current calendar by referencing the index of the cell that should be deselected.
	* This method is used to easily deselect a single cell (usually with a mouse click) without having to do
	* a full render. The selected style is removed from the cell directly.
	* 
	* If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month 
	* or out of bounds cells), the method will not attempt to deselect it and in such a case, beforeDeselect and 
	* deselect events will not be fired.
	* 
	* @method deselectCell
	* @param	{Number}	cellIndex	The index of the cell to deselect in the current calendar. 
	* @return	{Date[]}	Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	deselectCell : function(cellIndex) {
		var cell = this.cells[cellIndex],
			cellDate = this.cellDates[cellIndex],
			cellDateIndex = this._indexOfSelectedFieldArray(cellDate);

		var selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE);

		if (selectable) {

			this.beforeDeselectEvent.fire();

			var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key),
				dCellDate = this._toDate(cellDate),
				selectDate = cellDate.concat();

			if (cellDateIndex > -1) {
				if (this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth() == dCellDate.getMonth() &&
					this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getFullYear() == dCellDate.getFullYear()) {
					Dom.removeClass(cell, this.Style.CSS_CELL_SELECTED);
				}
				selected.splice(cellDateIndex, 1);
			}

			if (this.parent) {
				this.parent.cfg.setProperty(DEF_CFG.SELECTED.key, selected);
			} else {
				this.cfg.setProperty(DEF_CFG.SELECTED.key, selected);
			}

			this.deselectEvent.fire([selectDate]);
		}

		return this.getSelectedDates();
	},

	/**
	* Deselects all dates on the current calendar.
	* @method deselectAll
	* @return {Date[]}		Array of JavaScript Date objects representing all individual dates that are currently selected.
	*						Assuming that this function executes properly, the return value should be an empty array.
	*						However, the empty array is returned for the sake of being able to check the selection status
	*						of the calendar.
	*/
	deselectAll : function() {
		this.beforeDeselectEvent.fire();
		
		var cfgSelected = DEF_CFG.SELECTED.key,
			selected = this.cfg.getProperty(cfgSelected),
			count = selected.length,
			sel = selected.concat();

		if (this.parent) {
			this.parent.cfg.setProperty(cfgSelected, []);
		} else {
			this.cfg.setProperty(cfgSelected, []);
		}
		
		if (count > 0) {
			this.deselectEvent.fire(sel);
		}
	
		return this.getSelectedDates();
	},
	
	// END SELECTION METHODS
	
	// BEGIN TYPE CONVERSION METHODS
	
	/**
	* Converts a date (either a JavaScript Date object, or a date string) to the internal data structure
	* used to represent dates: [[yyyy,mm,dd],[yyyy,mm,dd]].
	* @method _toFieldArray
	* @private
	* @param	{String/Date/Date[]}	date	The date string of dates to deselect in the current calendar. Valid formats are
	*								individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
	*								Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
	*								This method can also take a JavaScript Date object or an array of Date objects.	
	* @return {Array[](Number[])}	Array of date field arrays
	*/
	_toFieldArray : function(date) {
		var returnDate = [];
	
		if (date instanceof Date) {
			returnDate = [[date.getFullYear(), date.getMonth()+1, date.getDate()]];
		} else if (Lang.isString(date)) {
			returnDate = this._parseDates(date);
		} else if (Lang.isArray(date)) {
			for (var i=0;i<date.length;++i) {
				var d = date[i];
				returnDate[returnDate.length] = [d.getFullYear(),d.getMonth()+1,d.getDate()];
			}
		}
		
		return returnDate;
	},
	
	/**
	* Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. The date field array
	* is the format in which dates are as provided as arguments to selectEvent and deselectEvent listeners.
	* 
	* @method toDate
	* @param	{Number[]}	dateFieldArray	The date field array to convert to a JavaScript Date.
	* @return	{Date}	JavaScript Date object representing the date field array.
	*/
	toDate : function(dateFieldArray) {
		return this._toDate(dateFieldArray);
	},
	
	/**
	* Converts a date field array [yyyy,mm,dd] to a JavaScript Date object.
	* @method _toDate
	* @private
	* @deprecated Made public, toDate 
	* @param	{Number[]}		dateFieldArray	The date field array to convert to a JavaScript Date.
	* @return	{Date}	JavaScript Date object representing the date field array
	*/
	_toDate : function(dateFieldArray) {
		if (dateFieldArray instanceof Date) {
			return dateFieldArray;
		} else {
			return DateMath.getDate(dateFieldArray[0],dateFieldArray[1]-1,dateFieldArray[2]);
		}
	},
	
	// END TYPE CONVERSION METHODS 
	
	// BEGIN UTILITY METHODS
	
	/**
	* Converts a date field array [yyyy,mm,dd] to a JavaScript Date object.
	* @method _fieldArraysAreEqual
	* @private
	* @param	{Number[]}	array1	The first date field array to compare
	* @param	{Number[]}	array2	The first date field array to compare
	* @return	{Boolean}	The boolean that represents the equality of the two arrays
	*/
	_fieldArraysAreEqual : function(array1, array2) {
		var match = false;
	
		if (array1[0]==array2[0]&&array1[1]==array2[1]&&array1[2]==array2[2]) {
			match=true;	
		}
	
		return match;
	},
	
	/**
	* Gets the index of a date field array [yyyy,mm,dd] in the current list of selected dates.
	* @method	_indexOfSelectedFieldArray
	* @private
	* @param	{Number[]}		find	The date field array to search for
	* @return	{Number}			The index of the date field array within the collection of selected dates.
	*								-1 will be returned if the date is not found.
	*/
	_indexOfSelectedFieldArray : function(find) {
		var selected = -1,
			seldates = this.cfg.getProperty(DEF_CFG.SELECTED.key);
	
		for (var s=0;s<seldates.length;++s) {
			var sArray = seldates[s];
			if (find[0]==sArray[0]&&find[1]==sArray[1]&&find[2]==sArray[2]) {
				selected = s;
				break;
			}
		}
	
		return selected;
	},
	
	/**
	* Determines whether a given date is OOM (out of month).
	* @method	isDateOOM
	* @param	{Date}	date	The JavaScript Date object for which to check the OOM status
	* @return	{Boolean}	true if the date is OOM
	*/
	isDateOOM : function(date) {
		return (date.getMonth() != this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth());
	},
	
	/**
	* Determines whether a given date is OOB (out of bounds - less than the mindate or more than the maxdate).
	*
	* @method	isDateOOB
	* @param	{Date}	date	The JavaScript Date object for which to check the OOB status
	* @return	{Boolean}	true if the date is OOB
	*/
	isDateOOB : function(date) {
		var minDate = this.cfg.getProperty(DEF_CFG.MINDATE.key),
			maxDate = this.cfg.getProperty(DEF_CFG.MAXDATE.key),
			dm = DateMath;
		
		if (minDate) {
			minDate = dm.clearTime(minDate);
		} 
		if (maxDate) {
			maxDate = dm.clearTime(maxDate);
		}
	
		var clearedDate = new Date(date.getTime());
		clearedDate = dm.clearTime(clearedDate);
	
		return ((minDate && clearedDate.getTime() < minDate.getTime()) || (maxDate && clearedDate.getTime() > maxDate.getTime()));
	},
	
	/**
	 * Parses a pagedate configuration property value. The value can either be specified as a string of form "mm/yyyy" or a Date object 
	 * and is parsed into a Date object normalized to the first day of the month. If no value is passed in, the month and year from today's date are used to create the Date object 
	 * @method	_parsePageDate
	 * @private
	 * @param {Date|String}	date	Pagedate value which needs to be parsed
	 * @return {Date}	The Date object representing the pagedate
	 */
	_parsePageDate : function(date) {
		var parsedDate;

		if (date) {
			if (date instanceof Date) {
				parsedDate = DateMath.findMonthStart(date);
			} else {
				var month, year, aMonthYear;
				aMonthYear = date.split(this.cfg.getProperty(DEF_CFG.DATE_FIELD_DELIMITER.key));
				month = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_MONTH_POSITION.key)-1], 10)-1;
				year = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_YEAR_POSITION.key)-1], 10);

				parsedDate = DateMath.getDate(year, month, 1);
			}
		} else {
			parsedDate = DateMath.getDate(this.today.getFullYear(), this.today.getMonth(), 1);
		}
		return parsedDate;
	},
	
	// END UTILITY METHODS
	
	// BEGIN EVENT HANDLERS
	
	/**
	* Event executed before a date is selected in the calendar widget.
	* @deprecated Event handlers for this event should be susbcribed to beforeSelectEvent.
	*/
	onBeforeSelect : function() {
		if (this.cfg.getProperty(DEF_CFG.MULTI_SELECT.key) === false) {
			if (this.parent) {
				this.parent.callChildFunction("clearAllBodyCellStyles", this.Style.CSS_CELL_SELECTED);
				this.parent.deselectAll();
			} else {
				this.clearAllBodyCellStyles(this.Style.CSS_CELL_SELECTED);
				this.deselectAll();
			}
		}
	},
	
	/**
	* Event executed when a date is selected in the calendar widget.
	* @param	{Array}	selected	An array of date field arrays representing which date or dates were selected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ]
	* @deprecated Event handlers for this event should be susbcribed to selectEvent.
	*/
	onSelect : function(selected) { },
	
	/**
	* Event executed before a date is deselected in the calendar widget.
	* @deprecated Event handlers for this event should be susbcribed to beforeDeselectEvent.
	*/
	onBeforeDeselect : function() { },
	
	/**
	* Event executed when a date is deselected in the calendar widget.
	* @param	{Array}	selected	An array of date field arrays representing which date or dates were deselected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ]
	* @deprecated Event handlers for this event should be susbcribed to deselectEvent.
	*/
	onDeselect : function(deselected) { },
	
	/**
	* Event executed when the user navigates to a different calendar page.
	* @deprecated Event handlers for this event should be susbcribed to changePageEvent.
	*/
	onChangePage : function() {
		this.render();
	},

	/**
	* Event executed when the calendar widget is rendered.
	* @deprecated Event handlers for this event should be susbcribed to renderEvent.
	*/
	onRender : function() { },

	/**
	* Event executed when the calendar widget is reset to its original state.
	* @deprecated Event handlers for this event should be susbcribed to resetEvemt.
	*/
	onReset : function() { this.render(); },

	/**
	* Event executed when the calendar widget is completely cleared to the current month with no selections.
	* @deprecated Event handlers for this event should be susbcribed to clearEvent.
	*/
	onClear : function() { this.render(); },
	
	/**
	* Validates the calendar widget. This method has no default implementation
	* and must be extended by subclassing the widget.
	* @return	Should return true if the widget validates, and false if
	* it doesn't.
	* @type Boolean
	*/
	validate : function() { return true; },
	
	// END EVENT HANDLERS
	
	// BEGIN DATE PARSE METHODS
	
	/**
	* Converts a date string to a date field array
	* @private
	* @param	{String}	sDate			Date string. Valid formats are mm/dd and mm/dd/yyyy.
	* @return				A date field array representing the string passed to the method
	* @type Array[](Number[])
	*/
	_parseDate : function(sDate) {
		var aDate = sDate.split(this.Locale.DATE_FIELD_DELIMITER),
			rArray;
	
		if (aDate.length == 2) {
			rArray = [aDate[this.Locale.MD_MONTH_POSITION-1],aDate[this.Locale.MD_DAY_POSITION-1]];
			rArray.type = Calendar.MONTH_DAY;
		} else {
			rArray = [aDate[this.Locale.MDY_YEAR_POSITION-1],aDate[this.Locale.MDY_MONTH_POSITION-1],aDate[this.Locale.MDY_DAY_POSITION-1]];
			rArray.type = Calendar.DATE;
		}
	
		for (var i=0;i<rArray.length;i++) {
			rArray[i] = parseInt(rArray[i], 10);
		}
	
		return rArray;
	},
	
	/**
	* Converts a multi or single-date string to an array of date field arrays
	* @private
	* @param	{String}	sDates		Date string with one or more comma-delimited dates. Valid formats are mm/dd, mm/dd/yyyy, mm/dd/yyyy-mm/dd/yyyy
	* @return							An array of date field arrays
	* @type Array[](Number[])
	*/
	_parseDates : function(sDates) {
		var aReturn = [],
			aDates = sDates.split(this.Locale.DATE_DELIMITER);
		
		for (var d=0;d<aDates.length;++d) {
			var sDate = aDates[d];
	
			if (sDate.indexOf(this.Locale.DATE_RANGE_DELIMITER) != -1) {
				// This is a range
				var aRange = sDate.split(this.Locale.DATE_RANGE_DELIMITER),
					dateStart = this._parseDate(aRange[0]),
					dateEnd = this._parseDate(aRange[1]),
					fullRange = this._parseRange(dateStart, dateEnd);

				aReturn = aReturn.concat(fullRange);
			} else {
				// This is not a range
				var aDate = this._parseDate(sDate);
				aReturn.push(aDate);
			}
		}
		return aReturn;
	},
	
	/**
	* Converts a date range to the full list of included dates
	* @private
	* @param	{Number[]}	startDate	Date field array representing the first date in the range
	* @param	{Number[]}	endDate		Date field array representing the last date in the range
	* @return							An array of date field arrays
	* @type Array[](Number[])
	*/
	_parseRange : function(startDate, endDate) {
		var dCurrent = DateMath.add(DateMath.getDate(startDate[0],startDate[1]-1,startDate[2]),DateMath.DAY,1),
			dEnd     = DateMath.getDate(endDate[0],  endDate[1]-1,  endDate[2]),
			results = [];

		results.push(startDate);
		while (dCurrent.getTime() <= dEnd.getTime()) {
			results.push([dCurrent.getFullYear(),dCurrent.getMonth()+1,dCurrent.getDate()]);
			dCurrent = DateMath.add(dCurrent,DateMath.DAY,1);
		}
		return results;
	},
	
	// END DATE PARSE METHODS
	
	// BEGIN RENDERER METHODS
	
	/**
	* Resets the render stack of the current calendar to its original pre-render value.
	*/
	resetRenderers : function() {
		this.renderStack = this._renderStack.concat();
	},
	
	/**
	 * Removes all custom renderers added to the Calendar through the addRenderer, addMonthRenderer and 
	 * addWeekdayRenderer methods. Calendar's render method needs to be called after removing renderers 
	 * to re-render the Calendar without custom renderers applied.
	 */
	removeRenderers : function() {
		this._renderStack = [];
		this.renderStack = [];
	},

	/**
	* Clears the inner HTML, CSS class and style information from the specified cell.
	* @method clearElement
	* @param	{HTMLTableCellElement} cell The cell to clear
	*/ 
	clearElement : function(cell) {
		cell.innerHTML = "&#160;";
		cell.className="";
	},
	
	/**
	* Adds a renderer to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the conditions specified in the date string for this renderer.
	* @method addRenderer
	* @param	{String}	sDates		A date string to associate with the specified renderer. Valid formats
	*									include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005)
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addRenderer : function(sDates, fnRender) {
		var aDates = this._parseDates(sDates);
		for (var i=0;i<aDates.length;++i) {
			var aDate = aDates[i];
		
			if (aDate.length == 2) { // this is either a range or a month/day combo
				if (aDate[0] instanceof Array) { // this is a range
					this._addRenderer(Calendar.RANGE,aDate,fnRender);
				} else { // this is a month/day combo
					this._addRenderer(Calendar.MONTH_DAY,aDate,fnRender);
				}
			} else if (aDate.length == 3) {
				this._addRenderer(Calendar.DATE,aDate,fnRender);
			}
		}
	},
	
	/**
	* The private method used for adding cell renderers to the local render stack.
	* This method is called by other methods that set the renderer type prior to the method call.
	* @method _addRenderer
	* @private
	* @param	{String}	type		The type string that indicates the type of date renderer being added.
	*									Values are YAHOO.widget.Calendar.DATE, YAHOO.widget.Calendar.MONTH_DAY, YAHOO.widget.Calendar.WEEKDAY,
	*									YAHOO.widget.Calendar.RANGE, YAHOO.widget.Calendar.MONTH
	* @param	{Array}		aDates		An array of dates used to construct the renderer. The format varies based
	*									on the renderer type
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	_addRenderer : function(type, aDates, fnRender) {
		var add = [type,aDates,fnRender];
		this.renderStack.unshift(add);	
		this._renderStack = this.renderStack.concat();
	},

	/**
	* Adds a month to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the month passed to this method.
	* @method addMonthRenderer
	* @param	{Number}	month		The month (1-12) to associate with this renderer
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addMonthRenderer : function(month, fnRender) {
		this._addRenderer(Calendar.MONTH,[month],fnRender);
	},

	/**
	* Adds a weekday to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the weekday passed to this method.
	* @method addWeekdayRenderer
	* @param	{Number}	weekday		The weekday (Sunday = 1, Monday = 2 ... Saturday = 7) to associate with this renderer
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addWeekdayRenderer : function(weekday, fnRender) {
		this._addRenderer(Calendar.WEEKDAY,[weekday],fnRender);
	},

	// END RENDERER METHODS
	
	// BEGIN CSS METHODS
	
	/**
	* Removes all styles from all body cells in the current calendar table.
	* @method clearAllBodyCellStyles
	* @param	{style}	style The CSS class name to remove from all calendar body cells
	*/
	clearAllBodyCellStyles : function(style) {
		for (var c=0;c<this.cells.length;++c) {
			Dom.removeClass(this.cells[c],style);
		}
	},
	
	// END CSS METHODS
	
	// BEGIN GETTER/SETTER METHODS
	/**
	* Sets the calendar's month explicitly
	* @method setMonth
	* @param {Number}	month		The numeric month, from 0 (January) to 11 (December)
	*/
	setMonth : function(month) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key,
			current = this.cfg.getProperty(cfgPageDate);
		current.setMonth(parseInt(month, 10));
		this.cfg.setProperty(cfgPageDate, current);
	},

	/**
	* Sets the calendar's year explicitly.
	* @method setYear
	* @param {Number}	year		The numeric 4-digit year
	*/
	setYear : function(year) {
		var cfgPageDate = DEF_CFG.PAGEDATE.key,
			current = this.cfg.getProperty(cfgPageDate);

		current.setFullYear(parseInt(year, 10));
		this.cfg.setProperty(cfgPageDate, current);
	},

	/**
	* Gets the list of currently selected dates from the calendar.
	* @method getSelectedDates
	* @return {Date[]} An array of currently selected JavaScript Date objects.
	*/
	getSelectedDates : function() {
		var returnDates = [],
			selected = this.cfg.getProperty(DEF_CFG.SELECTED.key);

		for (var d=0;d<selected.length;++d) {
			var dateArray = selected[d];

			var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]);
			returnDates.push(date);
		}

		returnDates.sort( function(a,b) { return a-b; } );
		return returnDates;
	},

	/// END GETTER/SETTER METHODS ///
	
	/**
	* Hides the Calendar's outer container from view.
	* @method hide
	*/
	hide : function() {
		if (this.beforeHideEvent.fire()) {
			this.oDomContainer.style.display = "none";
			this.hideEvent.fire();
		}
	},

	/**
	* Shows the Calendar's outer container.
	* @method show
	*/
	show : function() {
		if (this.beforeShowEvent.fire()) {
			this.oDomContainer.style.display = "block";
			this.showEvent.fire();
		}
	},

	/**
	* Returns a string representing the current browser.
	* @deprecated As of 2.3.0, environment information is available in YAHOO.env.ua
	* @see YAHOO.env.ua
	* @property browser
	* @type String
	*/
	browser : (function() {
				var ua = navigator.userAgent.toLowerCase();
					  if (ua.indexOf('opera')!=-1) { // Opera (check first in case of spoof)
						 return 'opera';
					  } else if (ua.indexOf('msie 7')!=-1) { // IE7
						 return 'ie7';
					  } else if (ua.indexOf('msie') !=-1) { // IE
						 return 'ie';
					  } else if (ua.indexOf('safari')!=-1) { // Safari (check before Gecko because it includes "like Gecko")
						 return 'safari';
					  } else if (ua.indexOf('gecko') != -1) { // Gecko
						 return 'gecko';
					  } else {
						 return false;
					  }
				})(),
	/**
	* Returns a string representation of the object.
	* @method toString
	* @return {String}	A string representation of the Calendar object.
	*/
	toString : function() {
		return "Calendar " + this.id;
	},

	/**
	 * Destroys the Calendar instance. The method will remove references
	 * to HTML elements, remove any event listeners added by the Calendar,
	 * and destroy the Config and CalendarNavigator instances it has created.
	 *
	 * @method destroy
	 */
	destroy : function() {

		if (this.beforeDestroyEvent.fire()) {
			var cal = this;

			// Child objects
			if (cal.navigator) {
				cal.navigator.destroy();
			}

			if (cal.cfg) {
				cal.cfg.destroy();
			}

			// DOM event listeners
			Event.purgeElement(cal.oDomContainer, true);

			// Generated markup/DOM - Not removing the container DIV since we didn't create it.
			Dom.removeClass(cal.oDomContainer, "withtitle");
			Dom.removeClass(cal.oDomContainer, cal.Style.CSS_CONTAINER);
			Dom.removeClass(cal.oDomContainer, cal.Style.CSS_SINGLE);
			cal.oDomContainer.innerHTML = "";

			// JS-to-DOM references
			cal.oDomContainer = null;
			cal.cells = null;

			this.destroyEvent.fire();
		}
	}
};

YAHOO.widget.Calendar = Calendar;

/**
* @namespace YAHOO.widget
* @class Calendar_Core
* @extends YAHOO.widget.Calendar
* @deprecated The old Calendar_Core class is no longer necessary.
*/
YAHOO.widget.Calendar_Core = YAHOO.widget.Calendar;

YAHOO.widget.Cal_Core = YAHOO.widget.Calendar;

})();

(function() {

	var Dom = YAHOO.util.Dom,
		DateMath = YAHOO.widget.DateMath,
		Event = YAHOO.util.Event,
		Lang = YAHOO.lang,
		Calendar = YAHOO.widget.Calendar;

/**
* YAHOO.widget.CalendarGroup is a special container class for YAHOO.widget.Calendar. This class facilitates
* the ability to have multi-page calendar views that share a single dataset and are
* dependent on each other.
*
* The calendar group instance will refer to each of its elements using a 0-based index.
* For example, to construct the placeholder for a calendar group widget with id "cal1" and
* containerId of "cal1Container", the markup would be as follows:
*	<xmp>
*		<div id="cal1Container_0"></div>
*		<div id="cal1Container_1"></div>
*	</xmp>
* The tables for the calendars ("cal1_0" and "cal1_1") will be inserted into those containers.
* 
* <p>
* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong>
* The CalendarGroup can be constructed by simply providing a container ID string, 
* or a reference to a container DIV HTMLElement (the element needs to exist 
* in the document).
* 
* E.g.:
*	<xmp>
*		var c = new YAHOO.widget.CalendarGroup("calContainer", configOptions);
*	</xmp>
* or:
*   <xmp>
*       var containerDiv = YAHOO.util.Dom.get("calContainer");
*		var c = new YAHOO.widget.CalendarGroup(containerDiv, configOptions);
*	</xmp>
* </p>
* <p>
* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix.
* For example if an ID is not provided, and the container's ID is "calContainer", the CalendarGroup's ID will be set to "calContainer_t".
* </p>
* 
* @namespace YAHOO.widget
* @class CalendarGroup
* @constructor
* @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional.
* @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document.
* @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup.
*/
function CalendarGroup(id, containerId, config) {
	if (arguments.length > 0) {
		this.init.apply(this, arguments);
	}
}

/**
* The set of default Config property keys and values for the CalendarGroup
* @property YAHOO.widget.CalendarGroup._DEFAULT_CONFIG
* @final
* @static
* @private
* @type Object
*/
CalendarGroup._DEFAULT_CONFIG = Calendar._DEFAULT_CONFIG;
CalendarGroup._DEFAULT_CONFIG.PAGES = {key:"pages", value:2};

var DEF_CFG = CalendarGroup._DEFAULT_CONFIG;

CalendarGroup.prototype = {

	/**
	* Initializes the calendar group. All subclasses must call this method in order for the
	* group to be initialized properly.
	* @method init
	* @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional.
	* @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document.
	* @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup.
	*/
	init : function(id, container, config) {

		// Normalize 2.4.0, pre 2.4.0 args
		var nArgs = this._parseArgs(arguments);

		id = nArgs.id;
		container = nArgs.container;
		config = nArgs.config;

		this.oDomContainer = Dom.get(container);

		if (!this.oDomContainer.id) {
			this.oDomContainer.id = Dom.generateId();
		}
		if (!id) {
			id = this.oDomContainer.id + "_t";
		}

		/**
		* The unique id associated with the CalendarGroup
		* @property id
		* @type String
		*/
		this.id = id;

		/**
		* The unique id associated with the CalendarGroup container
		* @property containerId
		* @type String
		*/
		this.containerId = this.oDomContainer.id;

		this.initEvents();
		this.initStyles();

		/**
		* The collection of Calendar pages contained within the CalendarGroup
		* @property pages
		* @type YAHOO.widget.Calendar[]
		*/
		this.pages = [];

		Dom.addClass(this.oDomContainer, CalendarGroup.CSS_CONTAINER);
		Dom.addClass(this.oDomContainer, CalendarGroup.CSS_MULTI_UP);

		/**
		* The Config object used to hold the configuration variables for the CalendarGroup
		* @property cfg
		* @type YAHOO.util.Config
		*/
		this.cfg = new YAHOO.util.Config(this);

		/**
		* The local object which contains the CalendarGroup's options
		* @property Options
		* @type Object
		*/
		this.Options = {};

		/**
		* The local object which contains the CalendarGroup's locale settings
		* @property Locale
		* @type Object
		*/
		this.Locale = {};

		this.setupConfig();

		if (config) {
			this.cfg.applyConfig(config, true);
		}

		this.cfg.fireQueue();

		// OPERA HACK FOR MISWRAPPED FLOATS
		if (YAHOO.env.ua.opera){
			this.renderEvent.subscribe(this._fixWidth, this, true);
			this.showEvent.subscribe(this._fixWidth, this, true);
		}

	},

	setupConfig : function() {

		var cfg = this.cfg;

		/**
		* The number of pages to include in the CalendarGroup. This value can only be set once, in the CalendarGroup's constructor arguments.
		* @config pages
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.PAGES.key, { value:DEF_CFG.PAGES.value, validator:cfg.checkNumber, handler:this.configPages } );

		/**
		* The month/year representing the current visible Calendar date (mm/yyyy)
		* @config pagedate
		* @type String | Date
		* @default today's date
		*/
		cfg.addProperty(DEF_CFG.PAGEDATE.key, { value:new Date(), handler:this.configPageDate } );

		/**
		* The date or range of dates representing the current Calendar selection
		*
		* @config selected
		* @type String
		* @default []
		*/
		cfg.addProperty(DEF_CFG.SELECTED.key, { value:[], handler:this.configSelected } );

		/**
		* The title to display above the CalendarGroup's month header
		* @config title
		* @type String
		* @default ""
		*/
		cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } );

		/**
		* Whether or not a close button should be displayed for this CalendarGroup
		* @config close
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } );

		/**
		* Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below.
		* This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be 
		* enabled if required.
		* 
		* @config iframe
		* @type Boolean
		* @default true for IE6 and below, false for all other browsers
		*/
		cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } );
	
		/**
		* The minimum selectable date in the current Calendar (mm/dd/yyyy)
		* @config mindate
		* @type String | Date
		* @default null
		*/
		cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.delegateConfig } );
	
		/**
		* The maximum selectable date in the current Calendar (mm/dd/yyyy)
		* @config maxdate
		* @type String | Date
		* @default null
		*/
		cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.delegateConfig  } );
	
		// Options properties

		/**
		* True if the Calendar should allow multiple selections. False by default.
		* @config MULTI_SELECT
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.MULTI_SELECT.key,	{ value:DEF_CFG.MULTI_SELECT.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );

		/**
		* The weekday the week begins on. Default is 0 (Sunday).
		* @config START_WEEKDAY
		* @type number
		* @default 0
		*/	
		cfg.addProperty(DEF_CFG.START_WEEKDAY.key,	{ value:DEF_CFG.START_WEEKDAY.value, handler:this.delegateConfig, validator:cfg.checkNumber  } );
		
		/**
		* True if the Calendar should show weekday labels. True by default.
		* @config SHOW_WEEKDAYS
		* @type Boolean
		* @default true
		*/	
		cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key,	{ value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
		
		/**
		* True if the Calendar should show week row headers. False by default.
		* @config SHOW_WEEK_HEADER
		* @type Boolean
		* @default false
		*/	
		cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key,{ value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
		
		/**
		* True if the Calendar should show week row footers. False by default.
		* @config SHOW_WEEK_FOOTER
		* @type Boolean
		* @default false
		*/
		cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
		
		/**
		* True if the Calendar should suppress weeks that are not a part of the current month. False by default.
		* @config HIDE_BLANK_WEEKS
		* @type Boolean
		* @default false
		*/		
		cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key,{ value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
		
		/**
		* The image that should be used for the left navigation arrow.
		* @config NAV_ARROW_LEFT
		* @type String
		* @deprecated	You can customize the image by overriding the default CSS class for the left arrow - "calnavleft"
		* @default null
		*/		
		cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key,	{ value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.delegateConfig } );
		
		/**
		* The image that should be used for the right navigation arrow.
		* @config NAV_ARROW_RIGHT
		* @type String
		* @deprecated	You can customize the image by overriding the default CSS class for the right arrow - "calnavright"
		* @default null
		*/		
		cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key,	{ value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.delegateConfig } );
	
		// Locale properties
		
		/**
		* The short month labels for the current locale.
		* @config MONTHS_SHORT
		* @type String[]
		* @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
		*/
		cfg.addProperty(DEF_CFG.MONTHS_SHORT.key,	{ value:DEF_CFG.MONTHS_SHORT.value, handler:this.delegateConfig } );
		
		/**
		* The long month labels for the current locale.
		* @config MONTHS_LONG
		* @type String[]
		* @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
		*/		
		cfg.addProperty(DEF_CFG.MONTHS_LONG.key,		{ value:DEF_CFG.MONTHS_LONG.value, handler:this.delegateConfig } );
		
		/**
		* The 1-character weekday labels for the current locale.
		* @config WEEKDAYS_1CHAR
		* @type String[]
		* @default ["S", "M", "T", "W", "T", "F", "S"]
		*/		
		cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key,	{ value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.delegateConfig } );
		
		/**
		* The short weekday labels for the current locale.
		* @config WEEKDAYS_SHORT
		* @type String[]
		* @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
		*/		
		cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key,	{ value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.delegateConfig } );
		
		/**
		* The medium weekday labels for the current locale.
		* @config WEEKDAYS_MEDIUM
		* @type String[]
		* @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
		*/		
		cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key,	{ value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.delegateConfig } );
		
		/**
		* The long weekday labels for the current locale.
		* @config WEEKDAYS_LONG
		* @type String[]
		* @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
		*/		
		cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key,	{ value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.delegateConfig } );
	
		/**
		* The setting that determines which length of month labels should be used. Possible values are "short" and "long".
		* @config LOCALE_MONTHS
		* @type String
		* @default "long"
		*/
		cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key,	{ value:DEF_CFG.LOCALE_MONTHS.value, handler:this.delegateConfig } );
	
		/**
		* The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long".
		* @config LOCALE_WEEKDAYS
		* @type String
		* @default "short"
		*/	
		cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key,	{ value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.delegateConfig } );
	
		/**
		* The value used to delimit individual dates in a date string passed to various Calendar functions.
		* @config DATE_DELIMITER
		* @type String
		* @default ","
		*/
		cfg.addProperty(DEF_CFG.DATE_DELIMITER.key,		{ value:DEF_CFG.DATE_DELIMITER.value, handler:this.delegateConfig } );
	
		/**
		* The value used to delimit date fields in a date string passed to various Calendar functions.
		* @config DATE_FIELD_DELIMITER
		* @type String
		* @default "/"
		*/	
		cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key,{ value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.delegateConfig } );
	
		/**
		* The value used to delimit date ranges in a date string passed to various Calendar functions.
		* @config DATE_RANGE_DELIMITER
		* @type String
		* @default "-"
		*/
		cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key,{ value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.delegateConfig } );
	
		/**
		* The position of the month in a month/year date string
		* @config MY_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key,	{ value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the year in a month/year date string
		* @config MY_YEAR_POSITION
		* @type Number
		* @default 2
		*/	
		cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key,	{ value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the month in a month/day date string
		* @config MD_MONTH_POSITION
		* @type Number
		* @default 1
		*/	
		cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key,	{ value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the day in a month/year date string
		* @config MD_DAY_POSITION
		* @type Number
		* @default 2
		*/	
		cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key,		{ value:DEF_CFG.MD_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the month in a month/day/year date string
		* @config MDY_MONTH_POSITION
		* @type Number
		* @default 1
		*/	
		cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key,	{ value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the day in a month/day/year date string
		* @config MDY_DAY_POSITION
		* @type Number
		* @default 2
		*/	
		cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key,	{ value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
		
		/**
		* The position of the year in a month/day/year date string
		* @config MDY_YEAR_POSITION
		* @type Number
		* @default 3
		*/	
		cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key,	{ value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
	
		/**
		* The position of the month in the month year label string used as the Calendar header
		* @config MY_LABEL_MONTH_POSITION
		* @type Number
		* @default 1
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key,	{ value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
	
		/**
		* The position of the year in the month year label string used as the Calendar header
		* @config MY_LABEL_YEAR_POSITION
		* @type Number
		* @default 2
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key,	{ value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );

		/**
		* The suffix used after the month when rendering the Calendar header
		* @config MY_LABEL_MONTH_SUFFIX
		* @type String
		* @default " "
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key,	{ value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.delegateConfig } );
		
		/**
		* The suffix used after the year when rendering the Calendar header
		* @config MY_LABEL_YEAR_SUFFIX
		* @type String
		* @default ""
		*/
		cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.delegateConfig } );

		/**
		* Configuration for the Month Year Navigation UI. By default it is disabled
		* @config NAV
		* @type Object
		* @default null
		*/
		cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } );

		/**
		 * The map of UI strings which the CalendarGroup UI uses.
		 *
		 * @config strings
		 * @type {Object}
		 * @default An object with the properties shown below:
		 *     <dl>
		 *         <dt>previousMonth</dt><dd><em>String</em> : The string to use for the "Previous Month" navigation UI. Defaults to "Previous Month".</dd>
		 *         <dt>nextMonth</dt><dd><em>String</em> : The string to use for the "Next Month" navigation UI. Defaults to "Next Month".</dd>
		 *         <dt>close</dt><dd><em>String</em> : The string to use for the close button label. Defaults to "Close".</dd>
		 *     </dl>
		 */
		cfg.addProperty(DEF_CFG.STRINGS.key, { 
			value:DEF_CFG.STRINGS.value, 
			handler:this.configStrings, 
			validator: function(val) {
				return Lang.isObject(val);
			},
			supercedes: DEF_CFG.STRINGS.supercedes
		});
	},

	/**
	* Initializes CalendarGroup's built-in CustomEvents
	* @method initEvents
	*/
	initEvents : function() {

		var me = this,
			strEvent = "Event",
			CE = YAHOO.util.CustomEvent;

		/**
		* Proxy subscriber to subscribe to the CalendarGroup's child Calendars' CustomEvents
		* @method sub
		* @private
		* @param {Function} fn	The function to subscribe to this CustomEvent
		* @param {Object}	obj	The CustomEvent's scope object
		* @param {Boolean}	bOverride	Whether or not to apply scope correction
		*/
		var sub = function(fn, obj, bOverride) {
			for (var p=0;p<me.pages.length;++p) {
				var cal = me.pages[p];
				cal[this.type + strEvent].subscribe(fn, obj, bOverride);
			}
		};

		/**
		* Proxy unsubscriber to unsubscribe from the CalendarGroup's child Calendars' CustomEvents
		* @method unsub
		* @private
		* @param {Function} fn	The function to subscribe to this CustomEvent
		* @param {Object}	obj	The CustomEvent's scope object
		*/
		var unsub = function(fn, obj) {
			for (var p=0;p<me.pages.length;++p) {
				var cal = me.pages[p];
				cal[this.type + strEvent].unsubscribe(fn, obj);
			}
		};

		var defEvents = Calendar._EVENT_TYPES;

		/**
		* Fired before a date selection is made
		* @event beforeSelectEvent
		*/
		me.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT);
		me.beforeSelectEvent.subscribe = sub; me.beforeSelectEvent.unsubscribe = unsub;

		/**
		* Fired when a date selection is made
		* @event selectEvent
		* @param {Array}	Array of Date field arrays in the format [YYYY, MM, DD].
		*/
		me.selectEvent = new CE(defEvents.SELECT); 
		me.selectEvent.subscribe = sub; me.selectEvent.unsubscribe = unsub;

		/**
		* Fired before a date or set of dates is deselected
		* @event beforeDeselectEvent
		*/
		me.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT); 
		me.beforeDeselectEvent.subscribe = sub; me.beforeDeselectEvent.unsubscribe = unsub;

		/**
		* Fired when a date or set of dates has been deselected
		* @event deselectEvent
		* @param {Array}	Array of Date field arrays in the format [YYYY, MM, DD].
		*/
		me.deselectEvent = new CE(defEvents.DESELECT); 
		me.deselectEvent.subscribe = sub; me.deselectEvent.unsubscribe = unsub;
		
		/**
		* Fired when the Calendar page is changed
		* @event changePageEvent
		*/
		me.changePageEvent = new CE(defEvents.CHANGE_PAGE); 
		me.changePageEvent.subscribe = sub; me.changePageEvent.unsubscribe = unsub;

		/**
		* Fired before the Calendar is rendered
		* @event beforeRenderEvent
		*/
		me.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER);
		me.beforeRenderEvent.subscribe = sub; me.beforeRenderEvent.unsubscribe = unsub;
	
		/**
		* Fired when the Calendar is rendered
		* @event renderEvent
		*/
		me.renderEvent = new CE(defEvents.RENDER);
		me.renderEvent.subscribe = sub; me.renderEvent.unsubscribe = unsub;
	
		/**
		* Fired when the Calendar is reset
		* @event resetEvent
		*/
		me.resetEvent = new CE(defEvents.RESET); 
		me.resetEvent.subscribe = sub; me.resetEvent.unsubscribe = unsub;
	
		/**
		* Fired when the Calendar is cleared
		* @event clearEvent
		*/
		me.clearEvent = new CE(defEvents.CLEAR);
		me.clearEvent.subscribe = sub; me.clearEvent.unsubscribe = unsub;

		/**
		* Fired just before the CalendarGroup is to be shown
		* @event beforeShowEvent
		*/
		me.beforeShowEvent = new CE(defEvents.BEFORE_SHOW);
	
		/**
		* Fired after the CalendarGroup is shown
		* @event showEvent
		*/
		me.showEvent = new CE(defEvents.SHOW);
	
		/**
		* Fired just before the CalendarGroup is to be hidden
		* @event beforeHideEvent
		*/
		me.beforeHideEvent = new CE(defEvents.BEFORE_HIDE);
	
		/**
		* Fired after the CalendarGroup is hidden
		* @event hideEvent
		*/
		me.hideEvent = new CE(defEvents.HIDE);

		/**
		* Fired just before the CalendarNavigator is to be shown
		* @event beforeShowNavEvent
		*/
		me.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV);
	
		/**
		* Fired after the CalendarNavigator is shown
		* @event showNavEvent
		*/
		me.showNavEvent = new CE(defEvents.SHOW_NAV);
	
		/**
		* Fired just before the CalendarNavigator is to be hidden
		* @event beforeHideNavEvent
		*/
		me.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV);

		/**
		* Fired after the CalendarNavigator is hidden
		* @event hideNavEvent
		*/
		me.hideNavEvent = new CE(defEvents.HIDE_NAV);

		/**
		* Fired just before the CalendarNavigator is to be rendered
		* @event beforeRenderNavEvent
		*/
		me.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV);

		/**
		* Fired after the CalendarNavigator is rendered
		* @event renderNavEvent
		*/
		me.renderNavEvent = new CE(defEvents.RENDER_NAV);

		/**
		* Fired just before the CalendarGroup is to be destroyed
		* @event beforeDestroyEvent
		*/
		me.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY);

		/**
		* Fired after the CalendarGroup is destroyed. This event should be used
		* for notification only. When this event is fired, important CalendarGroup instance
		* properties, dom references and event listeners have already been 
		* removed/dereferenced, and hence the CalendarGroup instance is not in a usable 
		* state.
		*
		* @event destroyEvent
		*/
		me.destroyEvent = new CE(defEvents.DESTROY);
	},
	
	/**
	* The default Config handler for the "pages" property
	* @method configPages
	* @param {String} type	The CustomEvent type (usually the property name)
	* @param {Object[]}	args	The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property.
	* @param {Object} obj	The scope object. For configuration handlers, this will usually equal the owner.
	*/
	configPages : function(type, args, obj) {
		var pageCount = args[0],
			cfgPageDate = DEF_CFG.PAGEDATE.key,
			sep = "_",
			caldate,
			firstPageDate = null,
			groupCalClass = "groupcal",
			firstClass = "first-of-type",
			lastClass = "last-of-type";

		for (var p=0;p<pageCount;++p) {
			var calId = this.id + sep + p,
				calContainerId = this.containerId + sep + p,
				childConfig = this.cfg.getConfig();

			childConfig.close = false;
			childConfig.title = false;
			childConfig.navigator = null;

			if (p > 0) {
				caldate = new Date(firstPageDate);
				this._setMonthOnDate(caldate, caldate.getMonth() + p);
				childConfig.pageDate = caldate;
			}

			var cal = this.constructChild(calId, calContainerId, childConfig);

			Dom.removeClass(cal.oDomContainer, this.Style.CSS_SINGLE);
			Dom.addClass(cal.oDomContainer, groupCalClass);

			if (p===0) {
				firstPageDate = cal.cfg.getProperty(cfgPageDate);
				Dom.addClass(cal.oDomContainer, firstClass);
			}
	
			if (p==(pageCount-1)) {
				Dom.addClass(cal.oDomContainer, lastClass);
			}
	
			cal.parent = this;
			cal.index = p; 
	
			this.pages[this.pages.length] = cal;
		}
	},
	
	/**
	* The default Config handler for the "pagedate" property
	* @method configPageDate
	* @param {String} type	The CustomEvent type (usually the property name)
	* @param {Object[]}	args	The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property.
	* @param {Object} obj	The scope object. For configuration handlers, this will usually equal the owner.
	*/
	configPageDate : function(type, args, obj) {
		var val = args[0],
			firstPageDate;

		var cfgPageDate = DEF_CFG.PAGEDATE.key;
		
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			if (p === 0) {
				firstPageDate = cal._parsePageDate(val);
				cal.cfg.setProperty(cfgPageDate, firstPageDate);
			} else {
				var pageDate = new Date(firstPageDate);
				this._setMonthOnDate(pageDate, pageDate.getMonth() + p);
				cal.cfg.setProperty(cfgPageDate, pageDate);
			}
		}
	},
	
	/**
	* The default Config handler for the CalendarGroup "selected" property
	* @method configSelected
	* @param {String} type	The CustomEvent type (usually the property name)
	* @param {Object[]}	args	The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property.
	* @param {Object} obj	The scope object. For configuration handlers, this will usually equal the owner.
	*/
	configSelected : function(type, args, obj) {
		var cfgSelected = DEF_CFG.SELECTED.key;
		this.delegateConfig(type, args, obj);
		var selected = (this.pages.length > 0) ? this.pages[0].cfg.getProperty(cfgSelected) : []; 
		this.cfg.setProperty(cfgSelected, selected, true);
	},

	
	/**
	* Delegates a configuration property to the CustomEvents associated with the CalendarGroup's children
	* @method delegateConfig
	* @param {String} type	The CustomEvent type (usually the property name)
	* @param {Object[]}	args	The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property.
	* @param {Object} obj	The scope object. For configuration handlers, this will usually equal the owner.
	*/
	delegateConfig : function(type, args, obj) {
		var val = args[0];
		var cal;
	
		for (var p=0;p<this.pages.length;p++) {
			cal = this.pages[p];
			cal.cfg.setProperty(type, val);
		}
	},

	/**
	* Adds a function to all child Calendars within this CalendarGroup.
	* @method setChildFunction
	* @param {String}		fnName		The name of the function
	* @param {Function}		fn			The function to apply to each Calendar page object
	*/
	setChildFunction : function(fnName, fn) {
		var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key);
	
		for (var p=0;p<pageCount;++p) {
			this.pages[p][fnName] = fn;
		}
	},

	/**
	* Calls a function within all child Calendars within this CalendarGroup.
	* @method callChildFunction
	* @param {String}		fnName		The name of the function
	* @param {Array}		args		The arguments to pass to the function
	*/
	callChildFunction : function(fnName, args) {
		var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key);

		for (var p=0;p<pageCount;++p) {
			var page = this.pages[p];
			if (page[fnName]) {
				var fn = page[fnName];
				fn.call(page, args);
			}
		}	
	},

	/**
	* Constructs a child calendar. This method can be overridden if a subclassed version of the default
	* calendar is to be used.
	* @method constructChild
	* @param {String}	id			The id of the table element that will represent the calendar widget
	* @param {String}	containerId	The id of the container div element that will wrap the calendar table
	* @param {Object}	config		The configuration object containing the Calendar's arguments
	* @return {YAHOO.widget.Calendar}	The YAHOO.widget.Calendar instance that is constructed
	*/
	constructChild : function(id,containerId,config) {
		var container = document.getElementById(containerId);
		if (! container) {
			container = document.createElement("div");
			container.id = containerId;
			this.oDomContainer.appendChild(container);
		}
		return new Calendar(id,containerId,config);
	},
	
	/**
	* Sets the calendar group's month explicitly. This month will be set into the first
	* page of the multi-page calendar, and all other months will be iterated appropriately.
	* @method setMonth
	* @param {Number}	month		The numeric month, from 0 (January) to 11 (December)
	*/
	setMonth : function(month) {
		month = parseInt(month, 10);
		var currYear;

		var cfgPageDate = DEF_CFG.PAGEDATE.key;

		for (var p=0; p<this.pages.length; ++p) {
			var cal = this.pages[p];
			var pageDate = cal.cfg.getProperty(cfgPageDate);
			if (p === 0) {
				currYear = pageDate.getFullYear();
			} else {
				pageDate.setFullYear(currYear);
			}
			this._setMonthOnDate(pageDate, month+p); 
			cal.cfg.setProperty(cfgPageDate, pageDate);
		}
	},

	/**
	* Sets the calendar group's year explicitly. This year will be set into the first
	* page of the multi-page calendar, and all other months will be iterated appropriately.
	* @method setYear
	* @param {Number}	year		The numeric 4-digit year
	*/
	setYear : function(year) {
	
		var cfgPageDate = DEF_CFG.PAGEDATE.key;
	
		year = parseInt(year, 10);
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			var pageDate = cal.cfg.getProperty(cfgPageDate);
	
			if ((pageDate.getMonth()+1) == 1 && p>0) {
				year+=1;
			}
			cal.setYear(year);
		}
	},

	/**
	* Calls the render function of all child calendars within the group.
	* @method render
	*/
	render : function() {
		this.renderHeader();
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.render();
		}
		this.renderFooter();
	},

	/**
	* Selects a date or a collection of dates on the current calendar. This method, by default,
	* does not call the render method explicitly. Once selection has completed, render must be 
	* called for the changes to be reflected visually.
	* @method select
	* @param	{String/Date/Date[]}	date	The date string of dates to select in the current calendar. Valid formats are
	*								individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
	*								Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
	*								This method can also take a JavaScript Date object or an array of Date objects.
	* @return	{Date[]}			Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	select : function(date) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.select(date);
		}
		return this.getSelectedDates();
	},

	/**
	* Selects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly.
	* The value of the MULTI_SELECT Configuration attribute will determine the set of dates which get selected. 
	* <ul>
	*    <li>If MULTI_SELECT is false, selectCell will select the cell at the specified index for only the last displayed Calendar page.</li>
	*    <li>If MULTI_SELECT is true, selectCell will select the cell at the specified index, on each displayed Calendar page.</li>
	* </ul>
	* @method selectCell
	* @param	{Number}	cellIndex	The index of the cell to be selected. 
	* @return	{Date[]}	Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	selectCell : function(cellIndex) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.selectCell(cellIndex);
		}
		return this.getSelectedDates();
	},
	
	/**
	* Deselects a date or a collection of dates on the current calendar. This method, by default,
	* does not call the render method explicitly. Once deselection has completed, render must be 
	* called for the changes to be reflected visually.
	* @method deselect
	* @param	{String/Date/Date[]}	date	The date string of dates to deselect in the current calendar. Valid formats are
	*								individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
	*								Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
	*								This method can also take a JavaScript Date object or an array of Date objects.	
	* @return	{Date[]}			Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	deselect : function(date) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.deselect(date);
		}
		return this.getSelectedDates();
	},
	
	/**
	* Deselects all dates on the current calendar.
	* @method deselectAll
	* @return {Date[]}		Array of JavaScript Date objects representing all individual dates that are currently selected.
	*						Assuming that this function executes properly, the return value should be an empty array.
	*						However, the empty array is returned for the sake of being able to check the selection status
	*						of the calendar.
	*/
	deselectAll : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.deselectAll();
		}
		return this.getSelectedDates();
	},

	/**
	* Deselects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly.
	* deselectCell will deselect the cell at the specified index on each displayed Calendar page.
	*
	* @method deselectCell
	* @param	{Number}	cellIndex	The index of the cell to deselect. 
	* @return	{Date[]}	Array of JavaScript Date objects representing all individual dates that are currently selected.
	*/
	deselectCell : function(cellIndex) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.deselectCell(cellIndex);
		}
		return this.getSelectedDates();
	},

	/**
	* Resets the calendar widget to the originally selected month and year, and 
	* sets the calendar to the initial selection(s).
	* @method reset
	*/
	reset : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.reset();
		}
	},

	/**
	* Clears the selected dates in the current calendar widget and sets the calendar
	* to the current month and year.
	* @method clear
	*/
	clear : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.clear();
		}

		this.cfg.setProperty(DEF_CFG.SELECTED.key, []);
		this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.pages[0].today.getTime()));
		this.render();
	},

	/**
	* Navigates to the next month page in the calendar widget.
	* @method nextMonth
	*/
	nextMonth : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.nextMonth();
		}
	},
	
	/**
	* Navigates to the previous month page in the calendar widget.
	* @method previousMonth
	*/
	previousMonth : function() {
		for (var p=this.pages.length-1;p>=0;--p) {
			var cal = this.pages[p];
			cal.previousMonth();
		}
	},
	
	/**
	* Navigates to the next year in the currently selected month in the calendar widget.
	* @method nextYear
	*/
	nextYear : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.nextYear();
		}
	},

	/**
	* Navigates to the previous year in the currently selected month in the calendar widget.
	* @method previousYear
	*/
	previousYear : function() {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.previousYear();
		}
	},

	/**
	* Gets the list of currently selected dates from the calendar.
	* @return			An array of currently selected JavaScript Date objects.
	* @type Date[]
	*/
	getSelectedDates : function() { 
		var returnDates = [];
		var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key);
		for (var d=0;d<selected.length;++d) {
			var dateArray = selected[d];

			var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]);
			returnDates.push(date);
		}

		returnDates.sort( function(a,b) { return a-b; } );
		return returnDates;
	},

	/**
	* Adds a renderer to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the conditions specified in the date string for this renderer.
	* @method addRenderer
	* @param	{String}	sDates		A date string to associate with the specified renderer. Valid formats
	*									include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005)
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addRenderer : function(sDates, fnRender) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.addRenderer(sDates, fnRender);
		}
	},

	/**
	* Adds a month to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the month passed to this method.
	* @method addMonthRenderer
	* @param	{Number}	month		The month (1-12) to associate with this renderer
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addMonthRenderer : function(month, fnRender) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.addMonthRenderer(month, fnRender);
		}
	},

	/**
	* Adds a weekday to the render stack. The function reference passed to this method will be executed
	* when a date cell matches the weekday passed to this method.
	* @method addWeekdayRenderer
	* @param	{Number}	weekday		The weekday (1-7) to associate with this renderer. 1=Sunday, 2=Monday etc.
	* @param	{Function}	fnRender	The function executed to render cells that match the render rules for this renderer.
	*/
	addWeekdayRenderer : function(weekday, fnRender) {
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			cal.addWeekdayRenderer(weekday, fnRender);
		}
	},

	/**
	 * Removes all custom renderers added to the CalendarGroup through the addRenderer, addMonthRenderer and 
	 * addWeekRenderer methods. CalendarGroup's render method needs to be called to after removing renderers 
	 * to see the changes applied.
	 * 
	 * @method removeRenderers
	 */
	removeRenderers : function() {
		this.callChildFunction("removeRenderers");
	},

	/**
	* Renders the header for the CalendarGroup.
	* @method renderHeader
	*/
	renderHeader : function() {
		// EMPTY DEFAULT IMPL
	},

	/**
	* Renders a footer for the 2-up calendar container. By default, this method is
	* unimplemented.
	* @method renderFooter
	*/
	renderFooter : function() {
		// EMPTY DEFAULT IMPL
	},

	/**
	* Adds the designated number of months to the current calendar month, and sets the current
	* calendar page date to the new month.
	* @method addMonths
	* @param {Number}	count	The number of months to add to the current calendar
	*/
	addMonths : function(count) {
		this.callChildFunction("addMonths", count);
	},
	
	/**
	* Subtracts the designated number of months from the current calendar month, and sets the current
	* calendar page date to the new month.
	* @method subtractMonths
	* @param {Number}	count	The number of months to subtract from the current calendar
	*/
	subtractMonths : function(count) {
		this.callChildFunction("subtractMonths", count);
	},

	/**
	* Adds the designated number of years to the current calendar, and sets the current
	* calendar page date to the new month.
	* @method addYears
	* @param {Number}	count	The number of years to add to the current calendar
	*/
	addYears : function(count) {
		this.callChildFunction("addYears", count);
	},

	/**
	* Subtcats the designated number of years from the current calendar, and sets the current
	* calendar page date to the new month.
	* @method subtractYears
	* @param {Number}	count	The number of years to subtract from the current calendar
	*/
	subtractYears : function(count) {
		this.callChildFunction("subtractYears", count);
	},

	/**
	 * Returns the Calendar page instance which has a pagedate (month/year) matching the given date. 
	 * Returns null if no match is found.
	 * 
	 * @method getCalendarPage
	 * @param {Date} date The JavaScript Date object for which a Calendar page is to be found.
	 * @return {Calendar} The Calendar page instance representing the month to which the date 
	 * belongs.
	 */
	getCalendarPage : function(date) {
		var cal = null;
		if (date) {
			var y = date.getFullYear(),
				m = date.getMonth();

			var pages = this.pages;
			for (var i = 0; i < pages.length; ++i) {
				var pageDate = pages[i].cfg.getProperty("pagedate");
				if (pageDate.getFullYear() === y && pageDate.getMonth() === m) {
					cal = pages[i];
					break;
				}
			}
		}
		return cal;
	},

	/**
	* Sets the month on a Date object, taking into account year rollover if the month is less than 0 or greater than 11.
	* The Date object passed in is modified. It should be cloned before passing it into this method if the original value needs to be maintained
	* @method	_setMonthOnDate
	* @private
	* @param	{Date}	date	The Date object on which to set the month index
	* @param	{Number}	iMonth	The month index to set
	*/
	_setMonthOnDate : function(date, iMonth) {
		// Bug in Safari 1.3, 2.0 (WebKit build < 420), Date.setMonth does not work consistently if iMonth is not 0-11
		if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420 && (iMonth < 0 || iMonth > 11)) {
			var newDate = DateMath.add(date, DateMath.MONTH, iMonth-date.getMonth());
			date.setTime(newDate.getTime());
		} else {
			date.setMonth(iMonth);
		}
	},
	
	/**
	 * Fixes the width of the CalendarGroup container element, to account for miswrapped floats
	 * @method _fixWidth
	 * @private
	 */
	_fixWidth : function() {
		var w = 0;
		for (var p=0;p<this.pages.length;++p) {
			var cal = this.pages[p];
			w += cal.oDomContainer.offsetWidth;
		}
		if (w > 0) {
			this.oDomContainer.style.width = w + "px";
		}
	},
	
	/**
	* Returns a string representation of the object.
	* @method toString
	* @return {String}	A string representation of the CalendarGroup object.
	*/
	toString : function() {
		return "CalendarGroup " + this.id;
	},

	/**
	 * Destroys the CalendarGroup instance. The method will remove references
	 * to HTML elements, remove any event listeners added by the CalendarGroup.
	 * 
	 * It will also destroy the Config and CalendarNavigator instances created by the 
	 * CalendarGroup and the individual Calendar instances created for each page.
	 *
	 * @method destroy
	 */
	destroy : function() {

		if (this.beforeDestroyEvent.fire()) {

			var cal = this;
	
			// Child objects
			if (cal.navigator) {
				cal.navigator.destroy();
			}
	
			if (cal.cfg) {
				cal.cfg.destroy();
			}
	
			// DOM event listeners
			Event.purgeElement(cal.oDomContainer, true);
	
			// Generated markup/DOM - Not removing the container DIV since we didn't create it.
			Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_CONTAINER);
			Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_MULTI_UP);
			
			for (var i = 0, l = cal.pages.length; i < l; i++) {
				cal.pages[i].destroy();
				cal.pages[i] = null;
			}
	
			cal.oDomContainer.innerHTML = "";
	
			// JS-to-DOM references
			cal.oDomContainer = null;
	
			this.destroyEvent.fire();
		}
	}
};

/**
* CSS class representing the container for the calendar
* @property YAHOO.widget.CalendarGroup.CSS_CONTAINER
* @static
* @final
* @type String
*/
CalendarGroup.CSS_CONTAINER = "yui-calcontainer";

/**
* CSS class representing the container for the calendar
* @property YAHOO.widget.CalendarGroup.CSS_MULTI_UP
* @static
* @final
* @type String
*/
CalendarGroup.CSS_MULTI_UP = "multi";

/**
* CSS class representing the title for the 2-up calendar
* @property YAHOO.widget.CalendarGroup.CSS_2UPTITLE
* @static
* @final
* @type String
*/
CalendarGroup.CSS_2UPTITLE = "title";

/**
* CSS class representing the close icon for the 2-up calendar
* @property YAHOO.widget.CalendarGroup.CSS_2UPCLOSE
* @static
* @final
* @deprecated	Along with Calendar.IMG_ROOT and NAV_ARROW_LEFT, NAV_ARROW_RIGHT configuration properties.
*					Calendar's <a href="YAHOO.widget.Calendar.html#Style.CSS_CLOSE">Style.CSS_CLOSE</a> property now represents the CSS class used to render the close icon
* @type String
*/
CalendarGroup.CSS_2UPCLOSE = "close-icon";

YAHOO.lang.augmentProto(CalendarGroup, Calendar, "buildDayLabel",
												 "buildMonthLabel",
												 "renderOutOfBoundsDate",
												 "renderRowHeader",
												 "renderRowFooter",
												 "renderCellDefault",
												 "styleCellDefault",
												 "renderCellStyleHighlight1",
												 "renderCellStyleHighlight2",
												 "renderCellStyleHighlight3",
												 "renderCellStyleHighlight4",
												 "renderCellStyleToday",
												 "renderCellStyleSelected",
												 "renderCellNotThisMonth",
												 "renderBodyCellRestricted",
												 "initStyles",
												 "configTitle",
												 "configClose",
												 "configIframe",
												 "configStrings",
												 "configNavigator",
												 "createTitleBar",
												 "createCloseButton",
												 "removeTitleBar",
												 "removeCloseButton",
												 "hide",
												 "show",
												 "toDate",
												 "_toDate",
												 "_parseArgs",
												 "browser");

YAHOO.widget.CalGrp = CalendarGroup;
YAHOO.widget.CalendarGroup = CalendarGroup;

/**
* @class YAHOO.widget.Calendar2up
* @extends YAHOO.widget.CalendarGroup
* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default.
*/
YAHOO.widget.Calendar2up = function(id, containerId, config) {
	this.init(id, containerId, config);
};

YAHOO.extend(YAHOO.widget.Calendar2up, CalendarGroup);

/**
* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default.
*/
YAHOO.widget.Cal2up = YAHOO.widget.Calendar2up;

})();

/**
 * The CalendarNavigator is used along with a Calendar/CalendarGroup to 
 * provide a Month/Year popup navigation control, allowing the user to navigate 
 * to a specific month/year in the Calendar/CalendarGroup without having to 
 * scroll through months sequentially
 *
 * @namespace YAHOO.widget
 * @class CalendarNavigator
 * @constructor
 * @param {Calendar|CalendarGroup} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached.
 */
YAHOO.widget.CalendarNavigator = function(cal) {
	this.init(cal);
};

(function() {
	// Setup static properties (inside anon fn, so that we can use shortcuts)
	var CN = YAHOO.widget.CalendarNavigator;

	/**
	 * YAHOO.widget.CalendarNavigator.CLASSES contains constants
	 * for the class values applied to the CalendarNaviatgator's 
	 * DOM elements
	 * @property YAHOO.widget.CalendarNavigator.CLASSES
	 * @type Object
	 * @static
	 */
	CN.CLASSES = {
		/**
		 * Class applied to the Calendar Navigator's bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV
		 * @type String
		 * @static
		 */
		NAV :"yui-cal-nav",
		/**
		 * Class applied to the Calendar/CalendarGroup's bounding box to indicate
		 * the Navigator is currently visible
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV_VISIBLE
		 * @type String
		 * @static
		 */
		NAV_VISIBLE: "yui-cal-nav-visible",
		/**
		 * Class applied to the Navigator mask's bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.MASK
		 * @type String
		 * @static
		 */
		MASK : "yui-cal-nav-mask",
		/**
		 * Class applied to the year label/control bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR
		 * @type String
		 * @static
		 */
		YEAR : "yui-cal-nav-y",
		/**
		 * Class applied to the month label/control bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH
		 * @type String
		 * @static
		 */
		MONTH : "yui-cal-nav-m",
		/**
		 * Class applied to the submit/cancel button's bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTONS
		 * @type String
		 * @static
		 */
		BUTTONS : "yui-cal-nav-b",
		/**
		 * Class applied to buttons wrapping element
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTON
		 * @type String
		 * @static
		 */
		BUTTON : "yui-cal-nav-btn",
		/**
		 * Class applied to the validation error area's bounding box
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.ERROR
		 * @type String
		 * @static
		 */
		ERROR : "yui-cal-nav-e",
		/**
		 * Class applied to the year input control
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR_CTRL
		 * @type String
		 * @static
		 */
		YEAR_CTRL : "yui-cal-nav-yc",
		/**
		 * Class applied to the month input control
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH_CTRL
		 * @type String
		 * @static
		 */
		MONTH_CTRL : "yui-cal-nav-mc",
		/**
		 * Class applied to controls with invalid data (e.g. a year input field with invalid an year)
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.INVALID
		 * @type String
		 * @static
		 */
		INVALID : "yui-invalid",
		/**
		 * Class applied to default controls
		 * @property YAHOO.widget.CalendarNavigator.CLASSES.DEFAULT
		 * @type String
		 * @static
		 */
		DEFAULT : "yui-default"
	};

	/**
	 * Object literal containing the default configuration values for the CalendarNavigator
	 * The configuration object is expected to follow the format below, with the properties being
	 * case sensitive.
	 * <dl>
	 * <dt>strings</dt>
	 * <dd><em>Object</em> :  An object with the properties shown below, defining the string labels to use in the Navigator's UI
	 *     <dl>
	 *         <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd>
	 *         <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd>
	 *         <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd>
	 *         <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd>
	 *         <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd>
	 *     </dl>
	 * </dd>
	 * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd>
	 * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd>
	 * </dl>
	 * @property _DEFAULT_CFG
	 * @protected
	 * @type Object
	 * @static
	 */
	CN._DEFAULT_CFG = {
		strings : {
			month: "Month",
			year: "Year",
			submit: "Okay",
			cancel: "Cancel",
			invalidYear : "Year needs to be a number"
		},
		monthFormat: YAHOO.widget.Calendar.LONG,
		initialFocus: "year"
	};

	/**
	 * The suffix added to the Calendar/CalendarGroup's ID, to generate
	 * a unique ID for the Navigator and it's bounding box.
	 * @property YAHOO.widget.CalendarNavigator.ID_SUFFIX
	 * @static
	 * @type String
	 * @final
	 */
	CN.ID_SUFFIX = "_nav";
	/**
	 * The suffix added to the Navigator's ID, to generate
	 * a unique ID for the month control.
	 * @property YAHOO.widget.CalendarNavigator.MONTH_SUFFIX
	 * @static
	 * @type String 
	 * @final
	 */
	CN.MONTH_SUFFIX = "_month";
	/**
	 * The suffix added to the Navigator's ID, to generate
	 * a unique ID for the year control.
	 * @property YAHOO.widget.CalendarNavigator.YEAR_SUFFIX
	 * @static
	 * @type String
	 * @final
	 */
	CN.YEAR_SUFFIX = "_year";
	/**
	 * The suffix added to the Navigator's ID, to generate
	 * a unique ID for the error bounding box.
	 * @property YAHOO.widget.CalendarNavigator.ERROR_SUFFIX
	 * @static
	 * @type String
	 * @final
	 */
	CN.ERROR_SUFFIX = "_error";
	/**
	 * The suffix added to the Navigator's ID, to generate
	 * a unique ID for the "Cancel" button.
	 * @property YAHOO.widget.CalendarNavigator.CANCEL_SUFFIX
	 * @static
	 * @type String
	 * @final
	 */
	CN.CANCEL_SUFFIX = "_cancel";
	/**
	 * The suffix added to the Navigator's ID, to generate
	 * a unique ID for the "Submit" button.
	 * @property YAHOO.widget.CalendarNavigator.SUBMIT_SUFFIX
	 * @static
	 * @type String
	 * @final
	 */
	CN.SUBMIT_SUFFIX = "_submit";

	/**
	 * The number of digits to which the year input control is to be limited.
	 * @property YAHOO.widget.CalendarNavigator.YR_MAX_DIGITS
	 * @static
	 * @type Number
	 */
	CN.YR_MAX_DIGITS = 4;

	/**
	 * The amount by which to increment the current year value,
	 * when the arrow up/down key is pressed on the year control
	 * @property YAHOO.widget.CalendarNavigator.YR_MINOR_INC
	 * @static
	 * @type Number
	 */
	CN.YR_MINOR_INC = 1;

	/**
	 * The amount by which to increment the current year value,
	 * when the page up/down key is pressed on the year control
	 * @property YAHOO.widget.CalendarNavigator.YR_MAJOR_INC
	 * @static
	 * @type Number
	 */
	CN.YR_MAJOR_INC = 10;

	/**
	 * Artificial delay (in ms) between the time the Navigator is hidden
	 * and the Calendar/CalendarGroup state is updated. Allows the user
	 * the see the Calendar/CalendarGroup page changing. If set to 0
	 * the Calendar/CalendarGroup page will be updated instantly
	 * @property YAHOO.widget.CalendarNavigator.UPDATE_DELAY
	 * @static
	 * @type Number
	 */
	CN.UPDATE_DELAY = 50;

	/**
	 * Regular expression used to validate the year input
	 * @property YAHOO.widget.CalendarNavigator.YR_PATTERN
	 * @static
	 * @type RegExp
	 */
	CN.YR_PATTERN = /^\d+$/;
	/**
	 * Regular expression used to trim strings
	 * @property YAHOO.widget.CalendarNavigator.TRIM
	 * @static
	 * @type RegExp
	 */
	CN.TRIM = /^\s*(.*?)\s*$/;
})();

YAHOO.widget.CalendarNavigator.prototype = {

	/**
	 * The unique ID for this CalendarNavigator instance
	 * @property id
	 * @type String
	 */
	id : null,

	/**
	 * The Calendar/CalendarGroup instance to which the navigator belongs
	 * @property cal
	 * @type {Calendar|CalendarGroup}
	 */
	cal : null,

	/**
	 * Reference to the HTMLElement used to render the navigator's bounding box
	 * @property navEl
	 * @type HTMLElement
	 */
	navEl : null,

	/**
	 * Reference to the HTMLElement used to render the navigator's mask
	 * @property maskEl
	 * @type HTMLElement
	 */
	maskEl : null,

	/**
	 * Reference to the HTMLElement used to input the year
	 * @property yearEl
	 * @type HTMLElement
	 */
	yearEl : null,

	/**
	 * Reference to the HTMLElement used to input the month
	 * @property monthEl
	 * @type HTMLElement
	 */
	monthEl : null,

	/**
	 * Reference to the HTMLElement used to display validation errors
	 * @property errorEl
	 * @type HTMLElement
	 */
	errorEl : null,

	/**
	 * Reference to the HTMLElement used to update the Calendar/Calendar group
	 * with the month/year values
	 * @property submitEl
	 * @type HTMLElement
	 */
	submitEl : null,
	
	/**
	 * Reference to the HTMLElement used to hide the navigator without updating the 
	 * Calendar/Calendar group
	 * @property cancelEl
	 * @type HTMLElement
	 */
	cancelEl : null,

	/** 
	 * Reference to the first focusable control in the navigator (by default monthEl)
	 * @property firstCtrl
	 * @type HTMLElement
	 */
	firstCtrl : null,
	
	/** 
	 * Reference to the last focusable control in the navigator (by default cancelEl)
	 * @property lastCtrl
	 * @type HTMLElement
	 */
	lastCtrl : null,

	/**
	 * The document containing the Calendar/Calendar group instance
	 * @protected
	 * @property _doc
	 * @type HTMLDocument
	 */
	_doc : null,

	/**
	 * Internal state property for the current year displayed in the navigator
	 * @protected
	 * @property _year
	 * @type Number
	 */
	_year: null,
	
	/**
	 * Internal state property for the current month index displayed in the navigator
	 * @protected
	 * @property _month
	 * @type Number
	 */
	_month: 0,

	/**
	 * Private internal state property which indicates whether or not the 
	 * Navigator has been rendered.
	 * @private
	 * @property __rendered
	 * @type Boolean
	 */
	__rendered: false,

	/**
	 * Init lifecycle method called as part of construction
	 * 
	 * @method init
	 * @param {Calendar} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached
	 */
	init : function(cal) {
		var calBox = cal.oDomContainer;

		this.cal = cal;
		this.id = calBox.id + YAHOO.widget.CalendarNavigator.ID_SUFFIX;
		this._doc = calBox.ownerDocument;

		/**
		 * Private flag, to identify IE Quirks
		 * @private
		 * @property __isIEQuirks
		 */
		var ie = YAHOO.env.ua.ie;
		this.__isIEQuirks = (ie && ((ie <= 6) || (this._doc.compatMode == "BackCompat")));
	},

	/**
	 * Displays the navigator and mask, updating the input controls to reflect the 
	 * currently set month and year. The show method will invoke the render method
	 * if the navigator has not been renderered already, allowing for lazy rendering
	 * of the control.
	 * 
	 * The show method will fire the Calendar/CalendarGroup's beforeShowNav and showNav events
	 * 
	 * @method show
	 */
	show : function() {
		var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES;

		if (this.cal.beforeShowNavEvent.fire()) {
			if (!this.__rendered) {
				this.render();
			}
			this.clearErrors();

			this._updateMonthUI();
			this._updateYearUI();
			this._show(this.navEl, true);

			this.setInitialFocus();
			this.showMask();

			YAHOO.util.Dom.addClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE);
			this.cal.showNavEvent.fire();
		}
	},

	/**
	 * Hides the navigator and mask
	 * 
	 * The show method will fire the Calendar/CalendarGroup's beforeHideNav event and hideNav events
	 * @method hide
	 */
	hide : function() {
		var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES;

		if (this.cal.beforeHideNavEvent.fire()) {
			this._show(this.navEl, false);
			this.hideMask();
			YAHOO.util.Dom.removeClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE);
			this.cal.hideNavEvent.fire();
		}
	},
	

	/**
	 * Displays the navigator's mask element
	 * 
	 * @method showMask
	 */
	showMask : function() {
		this._show(this.maskEl, true);
		if (this.__isIEQuirks) {
			this._syncMask();
		}
	},

	/**
	 * Hides the navigator's mask element
	 * 
	 * @method hideMask
	 */
	hideMask : function() {
		this._show(this.maskEl, false);
	},

	/**
	 * Returns the current month set on the navigator
	 * 
	 * Note: This may not be the month set in the UI, if 
	 * the UI contains an invalid value.
	 * 
	 * @method getMonth
	 * @return {Number} The Navigator's current month index
	 */
	getMonth: function() {
		return this._month;
	},

	/**
	 * Returns the current year set on the navigator
	 * 
	 * Note: This may not be the year set in the UI, if 
	 * the UI contains an invalid value.
	 * 
	 * @method getYear
	 * @return {Number} The Navigator's current year value
	 */
	getYear: function() {
		return this._year;
	},

	/**
	 * Sets the current month on the Navigator, and updates the UI
	 * 
	 * @method setMonth
	 * @param {Number} nMonth The month index, from 0 (Jan) through 11 (Dec).
	 */
	setMonth : function(nMonth) {
		if (nMonth >= 0 && nMonth < 12) {
			this._month = nMonth;
		}
		this._updateMonthUI();
	},

	/**
	 * Sets the current year on the Navigator, and updates the UI. If the 
	 * provided year is invalid, it will not be set.
	 * 
	 * @method setYear
	 * @param {Number} nYear The full year value to set the Navigator to.
	 */
	setYear : function(nYear) {
		var yrPattern = YAHOO.widget.CalendarNavigator.YR_PATTERN;
		if (YAHOO.lang.isNumber(nYear) && yrPattern.test(nYear+"")) {
			this._year = nYear;
		}
		this._updateYearUI();
	},

	/**
	 * Renders the HTML for the navigator, adding it to the 
	 * document and attaches event listeners if it has not 
	 * already been rendered.
	 * 
	 * @method render
	 */
	render: function() {
		this.cal.beforeRenderNavEvent.fire();
		if (!this.__rendered) {
			this.createNav();
			this.createMask();
			this.applyListeners();
			this.__rendered = true;
		}
		this.cal.renderNavEvent.fire();
	},

	/**
	 * Creates the navigator's containing HTMLElement, it's contents, and appends 
	 * the containg element to the Calendar/CalendarGroup's container.
	 * 
	 * @method createNav
	 */
	createNav : function() {
		var NAV = YAHOO.widget.CalendarNavigator;
		var doc = this._doc;

		var d = doc.createElement("div");
		d.className = NAV.CLASSES.NAV;

		var htmlBuf = this.renderNavContents([]);

		d.innerHTML = htmlBuf.join('');
		this.cal.oDomContainer.appendChild(d);

		this.navEl = d;

		this.yearEl = doc.getElementById(this.id + NAV.YEAR_SUFFIX);
		this.monthEl = doc.getElementById(this.id + NAV.MONTH_SUFFIX);
		this.errorEl = doc.getElementById(this.id + NAV.ERROR_SUFFIX);
		this.submitEl = doc.getElementById(this.id + NAV.SUBMIT_SUFFIX);
		this.cancelEl = doc.getElementById(this.id + NAV.CANCEL_SUFFIX);

		if (YAHOO.env.ua.gecko && this.yearEl && this.yearEl.type == "text") {
			// Avoid XUL error on focus, select [ https://bugzilla.mozilla.org/show_bug.cgi?id=236791, 
			// supposedly fixed in 1.8.1, but there are reports of it still being around for methods other than blur ]
			this.yearEl.setAttribute("autocomplete", "off");
		}

		this._setFirstLastElements();
	},

	/**
	 * Creates the Mask HTMLElement and appends it to the Calendar/CalendarGroups
	 * container.
	 * 
	 * @method createMask
	 */
	createMask : function() {
		var C = YAHOO.widget.CalendarNavigator.CLASSES;

		var d = this._doc.createElement("div");
		d.className = C.MASK;

		this.cal.oDomContainer.appendChild(d);
		this.maskEl = d;
	},

	/**
	 * Used to set the width/height of the mask in pixels to match the Calendar Container.
	 * Currently only used for IE6 or IE in quirks mode. The other A-Grade browser are handled using CSS (width/height 100%).
	 * <p>
	 * The method is also registered as an HTMLElement resize listener on the Calendars container element.
	 * </p>
	 * @protected
	 * @method _syncMask
	 */
	_syncMask : function() {
		var c = this.cal.oDomContainer;
		if (c && this.maskEl) {
			var r = YAHOO.util.Dom.getRegion(c);
			YAHOO.util.Dom.setStyle(this.maskEl, "width", r.right - r.left + "px");
			YAHOO.util.Dom.setStyle(this.maskEl, "height", r.bottom - r.top + "px");
		}
	},

	/**
	 * Renders the contents of the navigator
	 * 
	 * @method renderNavContents
	 * 
	 * @param {Array} html The HTML buffer to append the HTML to.
	 * @return {Array} A reference to the buffer passed in.
	 */
	renderNavContents : function(html) {
		var NAV = YAHOO.widget.CalendarNavigator,
			C = NAV.CLASSES,
			h = html; // just to use a shorter name

		h[h.length] = '<div class="' + C.MONTH + '">';
		this.renderMonth(h);
		h[h.length] = '</div>';
		h[h.length] = '<div class="' + C.YEAR + '">';
		this.renderYear(h);
		h[h.length] = '</div>';
		h[h.length] = '<div class="' + C.BUTTONS + '">';
		this.renderButtons(h);
		h[h.length] = '</div>';
		h[h.length] = '<div class="' + C.ERROR + '" id="' + this.id + NAV.ERROR_SUFFIX + '"></div>';

		return h;
	},

	/**
	 * Renders the month label and control for the navigator
	 * 
	 * @method renderNavContents
	 * @param {Array} html The HTML buffer to append the HTML to.
	 * @return {Array} A reference to the buffer passed in.
	 */
	renderMonth : function(html) {
		var NAV = YAHOO.widget.CalendarNavigator,
			C = NAV.CLASSES;

		var id = this.id + NAV.MONTH_SUFFIX,
			mf = this.__getCfg("monthFormat"),
			months = this.cal.cfg.getProperty((mf == YAHOO.widget.Calendar.SHORT) ? "MONTHS_SHORT" : "MONTHS_LONG"),
			h = html;

		if (months && months.length > 0) {
			h[h.length] = '<label for="' + id + '">';
			h[h.length] = this.__getCfg("month", true);
			h[h.length] = '</label>';
			h[h.length] = '<select name="' + id + '" id="' + id + '" class="' + C.MONTH_CTRL + '">';
			for (var i = 0; i < months.length; i++) {
				h[h.length] = '<option value="' + i + '">';
				h[h.length] = months[i];
				h[h.length] = '</option>';
			}
			h[h.length] = '</select>';
		}
		return h;
	},

	/**
	 * Renders the year label and control for the navigator
	 * 
	 * @method renderYear
	 * @param {Array} html The HTML buffer to append the HTML to.
	 * @return {Array} A reference to the buffer passed in.
	 */
	renderYear : function(html) {
		var NAV = YAHOO.widget.CalendarNavigator,
			C = NAV.CLASSES;

		var id = this.id + NAV.YEAR_SUFFIX,
			size = NAV.YR_MAX_DIGITS,
			h = html;

		h[h.length] = '<label for="' + id + '">';
		h[h.length] = this.__getCfg("year", true);
		h[h.length] = '</label>';
		h[h.length] = '<input type="text" name="' + id + '" id="' + id + '" class="' + C.YEAR_CTRL + '" maxlength="' + size + '"/>';
		return h;
	},

	/**
	 * Renders the submit/cancel buttons for the navigator
	 * 
	 * @method renderButton
	 * @return {String} The HTML created for the Button UI
	 */
	renderButtons : function(html) {
		var C = YAHOO.widget.CalendarNavigator.CLASSES;
		var h = html;

		h[h.length] = '<span class="' + C.BUTTON + ' ' + C.DEFAULT + '">';
		h[h.length] = '<button type="button" id="' + this.id + '_submit' + '">';
		h[h.length] = this.__getCfg("submit", true);
		h[h.length] = '</button>';
		h[h.length] = '</span>';
		h[h.length] = '<span class="' + C.BUTTON +'">';
		h[h.length] = '<button type="button" id="' + this.id + '_cancel' + '">';
		h[h.length] = this.__getCfg("cancel", true);
		h[h.length] = '</button>';
		h[h.length] = '</span>';

		return h;
	},

	/**
	 * Attaches DOM event listeners to the rendered elements
	 * <p>
	 * The method will call applyKeyListeners, to setup keyboard specific 
	 * listeners
	 * </p>
	 * @method applyListeners
	 */
	applyListeners : function() {
		var E = YAHOO.util.Event;

		function yearUpdateHandler() {
			if (this.validate()) {
				this.setYear(this._getYearFromUI());
			}
		}

		function monthUpdateHandler() {
			this.setMonth(this._getMonthFromUI());
		}

		E.on(this.submitEl, "click", this.submit, this, true);
		E.on(this.cancelEl, "click", this.cancel, this, true);
		E.on(this.yearEl, "blur", yearUpdateHandler, this, true);
		E.on(this.monthEl, "change", monthUpdateHandler, this, true);

		if (this.__isIEQuirks) {
			YAHOO.util.Event.on(this.cal.oDomContainer, "resize", this._syncMask, this, true);
		}

		this.applyKeyListeners();
	},

	/**
	 * Removes/purges DOM event listeners from the rendered elements
	 * 
	 * @method purgeListeners
	 */
	purgeListeners : function() {
		var E = YAHOO.util.Event;
		E.removeListener(this.submitEl, "click", this.submit);
		E.removeListener(this.cancelEl, "click", this.cancel);
		E.removeListener(this.yearEl, "blur");
		E.removeListener(this.monthEl, "change");
		if (this.__isIEQuirks) {
			E.removeListener(this.cal.oDomContainer, "resize", this._syncMask);
		}

		this.purgeKeyListeners();
	},

	/**
	 * Attaches DOM listeners for keyboard support. 
	 * Tab/Shift-Tab looping, Enter Key Submit on Year element,
	 * Up/Down/PgUp/PgDown year increment on Year element
	 * <p>
	 * NOTE: MacOSX Safari 2.x doesn't let you tab to buttons and 
	 * MacOSX Gecko does not let you tab to buttons or select controls,
	 * so for these browsers, Tab/Shift-Tab looping is limited to the 
	 * elements which can be reached using the tab key.
	 * </p>
	 * @method applyKeyListeners
	 */
	applyKeyListeners : function() {
		var E = YAHOO.util.Event,
			ua = YAHOO.env.ua;

		// IE/Safari 3.1 doesn't fire keypress for arrow/pg keys (non-char keys)
		var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress";

		// - IE/Safari 3.1 doesn't fire keypress for non-char keys
		// - Opera doesn't allow us to cancel keydown or keypress for tab, but 
		//   changes focus successfully on keydown (keypress is too late to change focus - opera's already moved on).
		var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress";

		// Everyone likes keypress for Enter (char keys) - whoo hoo!
		E.on(this.yearEl, "keypress", this._handleEnterKey, this, true);

		E.on(this.yearEl, arrowEvt, this._handleDirectionKeys, this, true);
		E.on(this.lastCtrl, tabEvt, this._handleTabKey, this, true);
		E.on(this.firstCtrl, tabEvt, this._handleShiftTabKey, this, true);
	},

	/**
	 * Removes/purges DOM listeners for keyboard support
	 *
	 * @method purgeKeyListeners
	 */
	purgeKeyListeners : function() {
		var E = YAHOO.util.Event,
			ua = YAHOO.env.ua;

		var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress";
		var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress";

		E.removeListener(this.yearEl, "keypress", this._handleEnterKey);
		E.removeListener(this.yearEl, arrowEvt, this._handleDirectionKeys);
		E.removeListener(this.lastCtrl, tabEvt, this._handleTabKey);
		E.removeListener(this.firstCtrl, tabEvt, this._handleShiftTabKey);
	},

	/**
	 * Updates the Calendar/CalendarGroup's pagedate with the currently set month and year if valid.
	 * <p>
	 * If the currently set month/year is invalid, a validation error will be displayed and the 
	 * Calendar/CalendarGroup's pagedate will not be updated.
	 * </p>
	 * @method submit
	 */
	submit : function() {
		if (this.validate()) {
			this.hide();

			this.setMonth(this._getMonthFromUI());
			this.setYear(this._getYearFromUI());

			var cal = this.cal;

			// Artificial delay, just to help the user see something changed
			var delay = YAHOO.widget.CalendarNavigator.UPDATE_DELAY;
			if (delay > 0) {
				var nav = this;
				window.setTimeout(function(){ nav._update(cal); }, delay);
			} else {
				this._update(cal);
			}
		}
	},

	/**
	 * Updates the Calendar rendered state, based on the state of the CalendarNavigator
	 * @method _update
	 * @param cal The Calendar instance to update
	 * @protected
	 */
	_update : function(cal) {
		cal.setYear(this.getYear());
		cal.setMonth(this.getMonth());
		cal.render();
	},

	/**
	 * Hides the navigator and mask, without updating the Calendar/CalendarGroup's state
	 * 
	 * @method cancel
	 */
	cancel : function() {
		this.hide();
	},

	/**
	 * Validates the current state of the UI controls
	 * 
	 * @method validate
	 * @return {Boolean} true, if the current UI state contains valid values, false if not
	 */
	validate : function() {
		if (this._getYearFromUI() !== null) {
			this.clearErrors();
			return true;
		} else {
			this.setYearError();
			this.setError(this.__getCfg("invalidYear", true));
			return false;
		}
	},

	/**
	 * Displays an error message in the Navigator's error panel
	 * @method setError
	 * @param {String} msg The error message to display
	 */
	setError : function(msg) {
		if (this.errorEl) {
			this.errorEl.innerHTML = msg;
			this._show(this.errorEl, true);
		}
	},

	/**
	 * Clears the navigator's error message and hides the error panel
	 * @method clearError 
	 */
	clearError : function() {
		if (this.errorEl) {
			this.errorEl.innerHTML = "";
			this._show(this.errorEl, false);
		}
	},

	/**
	 * Displays the validation error UI for the year control
	 * @method setYearError
	 */
	setYearError : function() {
		YAHOO.util.Dom.addClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID);
	},

	/**
	 * Removes the validation error UI for the year control
	 * @method clearYearError
	 */
	clearYearError : function() {
		YAHOO.util.Dom.removeClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID);
	},

	/**
	 * Clears all validation and error messages in the UI
	 * @method clearErrors
	 */
	clearErrors : function() {
		this.clearError();
		this.clearYearError();
	},

	/**
	 * Sets the initial focus, based on the configured value
	 * @method setInitialFocus
	 */
	setInitialFocus : function() {
		var el = this.submitEl,
			f = this.__getCfg("initialFocus");

		if (f && f.toLowerCase) {
			f = f.toLowerCase();
			if (f == "year") {
				el = this.yearEl;
				try {
					this.yearEl.select();
				} catch (selErr) {
					// Ignore;
				}
			} else if (f == "month") {
				el = this.monthEl;
			}
		}

		if (el && YAHOO.lang.isFunction(el.focus)) {
			try {
				el.focus();
			} catch (focusErr) {
				// TODO: Fall back if focus fails?
			}
		}
	},

	/**
	 * Removes all renderered HTML elements for the Navigator from
	 * the DOM, purges event listeners and clears (nulls) any property
	 * references to HTML references
	 * @method erase
	 */
	erase : function() {
		if (this.__rendered) {
			this.purgeListeners();

			// Clear out innerHTML references
			this.yearEl = null;
			this.monthEl = null;
			this.errorEl = null;
			this.submitEl = null;
			this.cancelEl = null;
			this.firstCtrl = null;
			this.lastCtrl = null;
			if (this.navEl) {
				this.navEl.innerHTML = "";
			}

			var p = this.navEl.parentNode;
			if (p) {
				p.removeChild(this.navEl);
			}
			this.navEl = null;

			var pm = this.maskEl.parentNode;
			if (pm) {
				pm.removeChild(this.maskEl);
			}
			this.maskEl = null;
			this.__rendered = false;
		}
	},

	/**
	 * Destroys the Navigator object and any HTML references
	 * @method destroy
	 */
	destroy : function() {
		this.erase();
		this._doc = null;
		this.cal = null;
		this.id = null;
	},

	/**
	 * Protected implementation to handle how UI elements are 
	 * hidden/shown.
	 *
	 * @method _show
	 * @protected
	 */
	_show : function(el, bShow) {
		if (el) {
			YAHOO.util.Dom.setStyle(el, "display", (bShow) ? "block" : "none");
		}
	},

	/**
	 * Returns the month value (index), from the month UI element
	 * @protected
	 * @method _getMonthFromUI
	 * @return {Number} The month index, or 0 if a UI element for the month
	 * is not found
	 */
	_getMonthFromUI : function() {
		if (this.monthEl) {
			return this.monthEl.selectedIndex;
		} else {
			return 0; // Default to Jan
		}
	},

	/**
	 * Returns the year value, from the Navitator's year UI element
	 * @protected
	 * @method _getYearFromUI
	 * @return {Number} The year value set in the UI, if valid. null is returned if 
	 * the UI does not contain a valid year value.
	 */
	_getYearFromUI : function() {
		var NAV = YAHOO.widget.CalendarNavigator;

		var yr = null;
		if (this.yearEl) {
			var value = this.yearEl.value;
			value = value.replace(NAV.TRIM, "$1");

			if (NAV.YR_PATTERN.test(value)) {
				yr = parseInt(value, 10);
			}
		}
		return yr;
	},

	/**
	 * Updates the Navigator's year UI, based on the year value set on the Navigator object
	 * @protected
	 * @method _updateYearUI
	 */
	_updateYearUI : function() {
		if (this.yearEl && this._year !== null) {
			this.yearEl.value = this._year;
		}
	},

	/**
	 * Updates the Navigator's month UI, based on the month value set on the Navigator object
	 * @protected
	 * @method _updateMonthUI
	 */
	_updateMonthUI : function() {
		if (this.monthEl) {
			this.monthEl.selectedIndex = this._month;
		}
	},

	/**
	 * Sets up references to the first and last focusable element in the Navigator's UI
	 * in terms of tab order (Naviagator's firstEl and lastEl properties). The references
	 * are used to control modality by looping around from the first to the last control
	 * and visa versa for tab/shift-tab navigation.
	 * <p>
	 * See <a href="#applyKeyListeners">applyKeyListeners</a>
	 * </p>
	 * @protected
	 * @method _setFirstLastElements
	 */
	_setFirstLastElements : function() {
		this.firstCtrl = this.monthEl;
		this.lastCtrl = this.cancelEl;

		// Special handling for MacOSX.
		// - Safari 2.x can't focus on buttons
		// - Gecko can't focus on select boxes or buttons
		if (this.__isMac) {
			if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420){
				this.firstCtrl = this.monthEl;
				this.lastCtrl = this.yearEl;
			}
			if (YAHOO.env.ua.gecko) {
				this.firstCtrl = this.yearEl;
				this.lastCtrl = this.yearEl;
			}
		}
	},

	/**
	 * Default Keyboard event handler to capture Enter 
	 * on the Navigator's year control (yearEl)
	 * 
	 * @method _handleEnterKey
	 * @protected
	 * @param {Event} e The DOM event being handled
	 */
	_handleEnterKey : function(e) {
		var KEYS = YAHOO.util.KeyListener.KEY;

		if (YAHOO.util.Event.getCharCode(e) == KEYS.ENTER) {
			YAHOO.util.Event.preventDefault(e);
			this.submit();
		}
	},

	/**
	 * Default Keyboard event handler to capture up/down/pgup/pgdown
	 * on the Navigator's year control (yearEl).
	 * 
	 * @method _handleDirectionKeys
	 * @protected
	 * @param {Event} e The DOM event being handled
	 */
	_handleDirectionKeys : function(e) {
		var E = YAHOO.util.Event,
			KEYS = YAHOO.util.KeyListener.KEY,
			NAV = YAHOO.widget.CalendarNavigator;

		var value = (this.yearEl.value) ? parseInt(this.yearEl.value, 10) : null;
		if (isFinite(value)) {
			var dir = false;
			switch(E.getCharCode(e)) {
				case KEYS.UP:
					this.yearEl.value = value + NAV.YR_MINOR_INC;
					dir = true;
					break;
				case KEYS.DOWN:
					this.yearEl.value = Math.max(value - NAV.YR_MINOR_INC, 0);
					dir = true;
					break;
				case KEYS.PAGE_UP:
					this.yearEl.value = value + NAV.YR_MAJOR_INC;
					dir = true;
					break;
				case KEYS.PAGE_DOWN:
					this.yearEl.value = Math.max(value - NAV.YR_MAJOR_INC, 0);
					dir = true;
					break;
				default:
					break;
			}
			if (dir) {
				E.preventDefault(e);
				try {
					this.yearEl.select();
				} catch(err) {
					// Ignore
				}
			}
		}
	},

	/**
	 * Default Keyboard event handler to capture Tab 
	 * on the last control (lastCtrl) in the Navigator.
	 * 
	 * @method _handleTabKey
	 * @protected
	 * @param {Event} e The DOM event being handled
	 */
	_handleTabKey : function(e) {
		var E = YAHOO.util.Event,
			KEYS = YAHOO.util.KeyListener.KEY;

		if (E.getCharCode(e) == KEYS.TAB && !e.shiftKey) {
			try {
				E.preventDefault(e);
				this.firstCtrl.focus();
			} catch (err) {
				// Ignore - mainly for focus edge cases
			}
		}
	},

	/**
	 * Default Keyboard event handler to capture Shift-Tab 
	 * on the first control (firstCtrl) in the Navigator.
	 * 
	 * @method _handleShiftTabKey
	 * @protected
	 * @param {Event} e The DOM event being handled
	 */
	_handleShiftTabKey : function(e) {
		var E = YAHOO.util.Event,
			KEYS = YAHOO.util.KeyListener.KEY;

		if (e.shiftKey && E.getCharCode(e) == KEYS.TAB) {
			try {
				E.preventDefault(e);
				this.lastCtrl.focus();
			} catch (err) {
				// Ignore - mainly for focus edge cases
			}
		}
	},

	/**
	 * Retrieve Navigator configuration values from 
	 * the parent Calendar/CalendarGroup's config value.
	 * <p>
	 * If it has not been set in the user provided configuration, the method will 
	 * return the default value of the configuration property, as set in _DEFAULT_CFG
	 * </p>
	 * @private
	 * @method __getCfg
	 * @param {String} Case sensitive property name.
	 * @param {Boolean} true, if the property is a string property, false if not.
	 * @return The value of the configuration property
	 */
	__getCfg : function(prop, bIsStr) {
		var DEF_CFG = YAHOO.widget.CalendarNavigator._DEFAULT_CFG;
		var cfg = this.cal.cfg.getProperty("navigator");

		if (bIsStr) {
			return (cfg !== true && cfg.strings && cfg.strings[prop]) ? cfg.strings[prop] : DEF_CFG.strings[prop];
		} else {
			return (cfg !== true && cfg[prop]) ? cfg[prop] : DEF_CFG[prop];
		}
	},

	/**
	 * Private flag, to identify MacOS
	 * @private
	 * @property __isMac
	 */
	__isMac : (navigator.userAgent.toLowerCase().indexOf("macintosh") != -1)

};

YAHOO.register("calendar", YAHOO.widget.Calendar, {version: "2.7.0", build: "1799"});
/*  Prototype JavaScript framework, version 1.5.0_rc1
 *  (c) 2005 Sam Stephenson <sam@conio.net>
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://prototype.conio.net/
 *
/*--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.5.0_rc1',
  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',

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

var Class = {
  create: function() {
    return function() {
      this.initialize.apply(this, arguments);
    }
  }
}

var Abstract = new Object();

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 == undefined) return 'undefined';
      if (object == null) return 'null';
      return object.inspect ? object.inspect() : object.toString();
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  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);
  }
});

Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}

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

Object.extend(Number.prototype, {
  toColorPart: function() {
    var digits = this.toString(16);
    if (this < 16) return '0' + digits;
    return digits;
  },

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

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

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

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

    return returnValue;
  }
}

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

var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
  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);
  },

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

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.callback(this);
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
}
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 += (replacement(match) || '').toString();
        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 = count === undefined ? 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 this;
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = truncation === undefined ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : 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 div = document.createElement('div');
    var text = document.createTextNode(this);
    div.appendChild(text);
    return div.innerHTML;
  },

  unescapeHTML: function() {
    var div = document.createElement('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
  },

  toQueryParams: function() {
    var pairs = this.match(/^\??(.*)$/)[1].split('&');
    return pairs.inject({}, function(params, pairString) {
      var pair  = pairString.split('=');
      var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
      params[decodeURIComponent(pair[0])] = value;
      return params;
    });
  },

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

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

    var camelizedString = this.indexOf('-') == 0
      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
      : oStringList[0];

    for (var i = 1, len = oStringList.length; i < len; i++) {
      var s = oStringList[i];
      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
    }

    return camelizedString;
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.replace(/\\/g, '\\\\');
    if (useDoubleQuotes)
      return '"' + escapedString.replace(/"/g, '\\"') + '"';
    else
      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  }
});

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

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

var Template = Class.create();
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
Template.prototype = {
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern  = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    return this.template.gsub(this.pattern, function(match) {
      var before = match[1];
      if (before == '\\') return match[2];
      return before + (object[match[3]] || '').toString();
    });
  }
}

var $break    = new Object();
var $continue = new Object();

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

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

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

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

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

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

  grep: function(pattern, iterator) {
    var results = [];
    this.each(function(value, index) {
      var stringValue = value.toString();
      if (stringValue.match(pattern))
        results.push((iterator || Prototype.K)(value, index));
    })
    return results;
  },

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

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

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

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

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

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

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

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

  sortBy: function(iterator) {
    return this.collect(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.collect(Prototype.K);
  },

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

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

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

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray
});
var $A = Array.from = function(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) {
    return iterable.toArray();
  } else {
    var results = [];
    for (var i = 0; i < iterable.length; i++)
      results.push(iterable[i]);
    return results;
  }
}

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; i < this.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 != undefined || value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(value && value.constructor == Array ?
        value.flatten() : [value]);
    });
  },

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

  indexOf: function(object) {
    for (var i = 0; i < this.length; i++)
      if (this[i] == object) return i;
    return -1;
  },

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

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

  uniq: function() {
    return this.inject([], function(array, value) {
      return array.include(value) ? array : array.concat([value]);
    });
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  }
});
var Hash = {
  _each: function(iterator) {
    for (var key in this) {
      var value = this[key];
      if (typeof value == 'function') continue;

      var pair = [key, value];
      pair.key = key;
      pair.value = value;
      iterator(pair);
    }
  },

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

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

  merge: function(hash) {
    return $H(hash).inject($H(this), function(mergedHash, pair) {
      mergedHash[pair.key] = pair.value;
      return mergedHash;
    });
  },

  toQueryString: function() {
    return this.map(function(pair) {
      return pair.map(encodeURIComponent).join('=');
    }).join('&');
  },

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

function $H(object) {
  var hash = Object.extend({}, object || {});
  Object.extend(hash, Enumerable);
  Object.extend(hash, Hash);
  return hash;
}
ObjectRange = Class.create();
Object.extend(ObjectRange.prototype, Enumerable);
Object.extend(ObjectRange.prototype, {
  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(responderToAdd) {
    if (!this.include(responderToAdd))
      this.responders.push(responderToAdd);
  },

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

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (responder[callback] && typeof responder[callback] == 'function') {
        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 = function() {};
Ajax.Base.prototype = {
  setOptions: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      parameters:   ''
    }
    Object.extend(this.options, options || {});
  },

  responseIsSuccess: function() {
    return this.transport.status == undefined
        || this.transport.status == 0
        || (this.transport.status >= 200 && this.transport.status < 300);
  },

  responseIsFailure: function() {
    return !this.responseIsSuccess();
  }
}

Ajax.Request = Class.create();
Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(url, options) {
    this.transport = Ajax.getTransport();
    this.setOptions(options);
    this.request(url);
  },

  request: function(url) {
    var parameters = this.options.parameters || '';
    if (parameters.length > 0) parameters += '&_=';

    /* Simulate other verbs over post */
    if (this.options.method != 'get' && this.options.method != 'post') {
      parameters += (parameters.length > 0 ? '&' : '') + '_method=' + this.options.method;
      this.options.method = 'post';
    }

    try {
      this.url = url;
      if (this.options.method == 'get' && parameters.length > 0)
        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;

      Ajax.Responders.dispatch('onCreate', this, this.transport);

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

      if (this.options.asynchronous)
        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);

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

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

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

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

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

    if (this.options.method == 'post') {
      requestHeaders.push('Content-type', this.options.contentType);

      /* Force "Connection: close" for Mozilla browsers to work around
       * a bug where XMLHttpReqeuest sends an incorrect Content-length
       * header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType)
        requestHeaders.push('Connection', 'close');
    }

    if (this.options.requestHeaders)
      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);

    for (var i = 0; i < requestHeaders.length; i += 2)
      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
  },

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

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

  evalJSON: function() {
    try {
      return eval('(' + this.header('X-JSON') + ')');
    } catch (e) {}
  },

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

  respondToReadyState: function(readyState) {
    var event = Ajax.Request.Events[readyState];
    var transport = this.transport, json = this.evalJSON();

    if (event == 'Complete') {
      try {
        (this.options['on' + this.transport.status]
         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(transport, json);
      } catch (e) {
        this.dispatchException(e);
      }

      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
        this.evalResponse();
    }

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

    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
    if (event == 'Complete')
      this.transport.onreadystatechange = Prototype.emptyFunction;
  },

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

Ajax.Updater = Class.create();

Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
  initialize: function(container, url, options) {
    this.containers = {
      success: container.success ? $(container.success) : $(container),
      failure: container.failure ? $(container.failure) :
        (container.success ? null : $(container))
    }

    this.transport = Ajax.getTransport();
    this.setOptions(options);

    var onComplete = this.options.onComplete || Prototype.emptyFunction;
    this.options.onComplete = (function(transport, object) {
      this.updateContent();
      onComplete(transport, object);
    }).bind(this);

    this.request(url);
  },

  updateContent: function() {
    var receiver = this.responseIsSuccess() ?
      this.containers.success : this.containers.failure;
    var response = this.transport.responseText;

    if (!this.options.evalScripts)
      response = response.stripScripts();

    if (receiver) {
      if (this.options.insertion) {
        new this.options.insertion(receiver, response);
      } else {
        Element.update(receiver, response);
      }
    }

    if (this.responseIsSuccess()) {
      if (this.onComplete)
        setTimeout(this.onComplete.bind(this), 10);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
  initialize: function(container, url, options) {
    this.setOptions(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(request) {
    if (this.options.decay) {
      this.decay = (request.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = request.responseText;
    }
    this.timer = setTimeout(this.onTimerEvent.bind(this),
      this.decay * this.frequency * 1000);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $() {
  var results = [], element;
  for (var i = 0; i < arguments.length; i++) {
    element = arguments[i];
    if (typeof element == 'string')
      element = document.getElementById(element);
    results.push(Element.extend(element));
  }
  return results.reduce();
}

document.getElementsByClassName = function(className, parentElement) {
  var children = ($(parentElement) || document.body).getElementsByTagName('*');
  return $A(children).inject([], function(elements, child) {
    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
      elements.push(Element.extend(child));
    return elements;
  });
}

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

if (!window.Element)
  var Element = new Object();

Element.extend = function(element) {
  if (!element) return;
  if (_nativeExtensions || element.nodeType == 3) return element;

  if (!element._extended && element.tagName && element != window) {
    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;

    if (element.tagName == 'FORM')
      Object.extend(methods, Form.Methods);
    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
      Object.extend(methods, Form.Element.Methods);

    for (var property in methods) {
      var value = methods[property];
      if (typeof value == 'function')
        element[property] = cache.findOrStore(value);
    }
  }

  element._extended = true;
  return element;
}

Element.extend.cache = {
  findOrStore: function(value) {
    return this[value] = this[value] || function() {
      return value.apply(null, [this].concat($A(arguments)));
    }
  }
}

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, html) {
    $(element).innerHTML = html.stripScripts();
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  replace: function(element, html) {
    element = $(element);
    if (element.outerHTML) {
      element.outerHTML = html.stripScripts();
    } else {
      var range = element.ownerDocument.createRange();
      range.selectNodeContents(element);
      element.parentNode.replaceChild(
        range.createContextualFragment(html.stripScripts()), element);
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  },

  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) {
    element = $(element);
    return $A(element.getElementsByTagName('*'));
  },

  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) {
    element = $(element);
    if (typeof selector == 'string')
      selector = new Selector(selector);
    return selector.match(element);
  },

  up: function(element, expression, index) {
    return Selector.findElement($(element).ancestors(), expression, index);
  },

  down: function(element, expression, index) {
    return Selector.findElement($(element).descendants(), expression, index);
  },

  previous: function(element, expression, index) {
    return Selector.findElement($(element).previousSiblings(), expression, index);
  },

  next: function(element, expression, index) {
    return Selector.findElement($(element).nextSiblings(), expression, index);
  },

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

  getElementsByClassName: function(element, className) {
    element = $(element);
    return document.getElementsByClassName(className, element);
  },

  getHeight: function(element) {
    element = $(element);
    return element.offsetHeight;
  },

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

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    return Element.classNames(element).include(className);
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).add(className);
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    Element.classNames(element).remove(className);
    return element;
  },

  observe: function() {
    Event.observe.apply(Event, arguments);
    return $A(arguments).first();
  },

  stopObserving: function() {
    Event.stopObserving.apply(Event, arguments);
    return $A(arguments).first();
  },

  // 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.match(/^\s*$/);
  },

  childOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);
    while (element = element.parentNode)
      if (element == ancestor) return true;
    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var x = element.x ? element.x : element.offsetLeft,
        y = element.y ? element.y : element.offsetTop;
    window.scrollTo(x, y);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    var value = element.style[style.camelize()];
    if (!value) {
      if (document.defaultView && document.defaultView.getComputedStyle) {
        var css = document.defaultView.getComputedStyle(element, null);
        value = css ? css.getPropertyValue(style) : null;
      } else if (element.currentStyle) {
        value = element.currentStyle[style.camelize()];
      }
    }

    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
      if (Element.getStyle(element, 'position') == 'static') value = 'auto';

    return value == 'auto' ? null : value;
  },

  setStyle: function(element, style) {
    element = $(element);
    for (var name in style)
      element.style[name.camelize()] = style[name];
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    if (Element.getStyle(element, 'display') != 'none')
      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;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = '';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = 'none';
    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._overflow = element.style.overflow || 'auto';
    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

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

// IE is missing .innerHTML support for TABLE-related elements
if(document.all){
  Element.Methods.update = function(element, html) {
    element = $(element);
    var tagName = element.tagName.toUpperCase();
    if (['THEAD','TBODY','TR','TD'].indexOf(tagName) > -1) {
      var div = document.createElement('div');
      switch (tagName) {
        case 'THEAD':
        case 'TBODY':
          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
          depth = 2;
          break;
        case 'TR':
          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
          depth = 3;
          break;
        case 'TD':
          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
          depth = 4;
      }
      $A(element.childNodes).each(function(node){
        element.removeChild(node)
      });
      depth.times(function(){ div = div.firstChild });

      $A(div.childNodes).each(
        function(node){ element.appendChild(node) });
    } else {
      element.innerHTML = html.stripScripts();
    }
    setTimeout(function() {html.evalScripts()}, 10);
    return element;
  }
}

Object.extend(Element, Element.Methods);

var _nativeExtensions = false;

if (!window.HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  /* Emulate HTMLElement, HTMLFormElement, HTMLInputElement, HTMLTextAreaElement,
     and HTMLSelectElement in Safari */
  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
    var klass = window['HTML' + tag + 'Element'] = {};
    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
  });
}

Element.addMethods = function(methods) {
  Object.extend(Element.Methods, methods || {});

  function copy(methods, destination) {
    var cache = Element.extend.cache;
    for (var property in methods) {
      var value = methods[property];
      destination[property] = cache.findOrStore(value);
    }
  }

  if (typeof HTMLElement != 'undefined') {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Form.Methods, HTMLFormElement.prototype);
    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
      copy(Form.Element.Methods, klass.prototype);
    });
    _nativeExtensions = true;
  }
}

var Toggle = new Object();
Toggle.display = Element.toggle;

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

Abstract.Insertion = function(adjacency) {
  this.adjacency = adjacency;
}

Abstract.Insertion.prototype = {
  initialize: function(element, content) {
    this.element = $(element);
    this.content = content.stripScripts();

    if (this.adjacency && this.element.insertAdjacentHTML) {
      try {
        this.element.insertAdjacentHTML(this.adjacency, this.content);
      } catch (e) {
        var tagName = this.element.tagName.toLowerCase();
        if (tagName == 'tbody' || tagName == 'tr') {
          this.insertContent(this.contentFromAnonymousTable());
        } else {
          throw e;
        }
      }
    } else {
      this.range = this.element.ownerDocument.createRange();
      if (this.initializeRange) this.initializeRange();
      this.insertContent([this.range.createContextualFragment(this.content)]);
    }

    setTimeout(function() {content.evalScripts()}, 10);
  },

  contentFromAnonymousTable: function() {
    var div = document.createElement('div');
    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
    return $A(div.childNodes[0].childNodes[0].childNodes);
  }
}

var Insertion = new Object();

Insertion.Before = Class.create();
Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
  initializeRange: function() {
    this.range.setStartBefore(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment, this.element);
    }).bind(this));
  }
});

Insertion.Top = Class.create();
Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(true);
  },

  insertContent: function(fragments) {
    fragments.reverse(false).each((function(fragment) {
      this.element.insertBefore(fragment, this.element.firstChild);
    }).bind(this));
  }
});

Insertion.Bottom = Class.create();
Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
  initializeRange: function() {
    this.range.selectNodeContents(this.element);
    this.range.collapse(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.appendChild(fragment);
    }).bind(this));
  }
});

Insertion.After = Class.create();
Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
  initializeRange: function() {
    this.range.setStartAfter(this.element);
  },

  insertContent: function(fragments) {
    fragments.each((function(fragment) {
      this.element.parentNode.insertBefore(fragment,
        this.element.nextSibling);
    }).bind(this));
  }
});

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

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(this.toArray().concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set(this.select(function(className) {
      return className != classNameToRemove;
    }).join(' '));
  },

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

Object.extend(Element.ClassNames.prototype, Enumerable);
var Selector = Class.create();
Selector.prototype = {
  initialize: function(expression) {
    this.params = {classNames: []};
    this.expression = expression.toString().strip();
    this.parseExpression();
    this.compileMatcher();
  },

  parseExpression: function() {
    function abort(message) { throw 'Parse error in selector: ' + message; }

    if (this.expression == '')  abort('empty expression');

    var params = this.params, expr = this.expression, match, modifier, clause, rest;
    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
      params.attributes = params.attributes || [];
      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
      expr = match[1];
    }

    if (expr == '*') return this.params.wildcard = true;

    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
      modifier = match[1], clause = match[2], rest = match[3];
      switch (modifier) {
        case '#':       params.id = clause; break;
        case '.':       params.classNames.push(clause); break;
        case '':
        case undefined: params.tagName = clause.toUpperCase(); break;
        default:        abort(expr.inspect());
      }
      expr = rest;
    }

    if (expr.length > 0) abort(expr.inspect());
  },

  buildMatchExpression: function() {
    var params = this.params, conditions = [], clause;

    if (params.wildcard)
      conditions.push('true');
    if (clause = params.id)
      conditions.push('element.id == ' + clause.inspect());
    if (clause = params.tagName)
      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
    if ((clause = params.classNames).length > 0)
      for (var i = 0; i < clause.length; i++)
        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
    if (clause = params.attributes) {
      clause.each(function(attribute) {
        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
        var splitValueBy = function(delimiter) {
          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
        }

        switch (attribute.operator) {
          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
          case '|=':      conditions.push(
                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
                          ); break;
          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
          case '':
          case undefined: conditions.push(value + ' != null'); break;
          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
        }
      });
    }

    return conditions.join(' && ');
  },

  compileMatcher: function() {
    this.match = new Function('element', 'if (!element.tagName) return false; \
      return ' + this.buildMatchExpression());
  },

  findElements: function(scope) {
    var element;

    if (element = $(this.params.id))
      if (this.match(element))
        if (!scope || Element.childOf(element, scope))
          return [element];

    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');

    var results = [];
    for (var i = 0; i < scope.length; i++)
      if (this.match(element = scope[i]))
        results.push(Element.extend(element));

    return results;
  },

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

Object.extend(Selector, {
  matchElements: function(elements, expression) {
    var selector = new Selector(expression);
    return elements.select(selector.match.bind(selector));
  },

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

  findChildElements: function(element, expressions) {
    return expressions.map(function(expression) {
      return expression.strip().split(/\s+/).inject([null], function(results, expr) {
        var selector = new Selector(expr);
        return results.inject([], function(elements, result) {
          return elements.concat(selector.findElements(result || element));
        });
      });
    }).flatten();
  }
});

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

Form.Methods = {
  serialize: function(form) {
    var elements = Form.getElements($(form));
    var queryComponents = new Array();

    for (var i = 0; i < elements.length; i++) {
      var queryComponent = Form.Element.serialize(elements[i]);
      if (queryComponent)
        queryComponents.push(queryComponent);
    }

    return queryComponents.join('&');
  },

  getElements: function(form) {
    form = $(form);
    var elements = new Array();

    for (var tagName in Form.Element.Serializers) {
      var tagElements = form.getElementsByTagName(tagName);
      for (var j = 0; j < tagElements.length; j++)
        elements.push(tagElements[j]);
    }
    return elements;
  },

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

    if (!typeName && !name)
      return inputs;

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

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.blur();
      element.disabled = 'true';
    }
    return form;
  },

  enable: function(form) {
    form = $(form);
    var elements = Form.getElements(form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      element.disabled = '';
    }
    return form;
  },

  findFirstElement: function(form) {
    return Form.getElements(form).find(function(element) {
      return element.type != 'hidden' && !element.disabled &&
        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

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

Object.extend(Form, Form.Methods);

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

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

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

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    var parameter = Form.Element.Serializers[method](element);

    if (parameter) {
      var key = encodeURIComponent(parameter[0]);
      if (key.length == 0) return;

      if (parameter[1].constructor != Array)
        parameter[1] = [parameter[1]];

      return parameter[1].map(function(value) {
        return key + '=' + encodeURIComponent(value);
      }).join('&');
    }
  },

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

    if (parameter)
      return parameter[1];
  },

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

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

  activate: function(element) {
    element = $(element);
    element.focus();
    if (element.select)
      element.select();
    return element;
  },

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

  enable: function(element) {
    element = $(element);
    element.blur();
    element.disabled = 'true';
    return element;
  }
}

Object.extend(Form.Element, Form.Element.Methods);
var Field = Form.Element;

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

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

  inputSelector: function(element) {
    if (element.checked)
      return [element.name, element.value];
  },

  textarea: function(element) {
    return [element.name, element.value];
  },

  select: function(element) {
    return Form.Element.Serializers[element.type == 'select-one' ?
      'selectOne' : 'selectMany'](element);
  },

  selectOne: function(element) {
    var value = '', opt, index = element.selectedIndex;
    if (index >= 0) {
      opt = element.options[index];
      value = opt.value || opt.text;
    }
    return [element.name, value];
  },

  selectMany: function(element) {
    var value = [];
    for (var i = 0; i < element.length; i++) {
      var opt = element.options[i];
      if (opt.selected)
        value.push(opt.value || opt.text);
    }
    return [element.name, value];
  }
}

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

var $F = Form.Element.getValue;

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

Abstract.TimedObserver = function() {}
Abstract.TimedObserver.prototype = {
  initialize: function(element, frequency, callback) {
    this.frequency = frequency;
    this.element   = $(element);
    this.callback  = callback;

    this.lastValue = this.getValue();
    this.registerCallback();
  },

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

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

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

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

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

Abstract.EventObserver = function() {}
Abstract.EventObserver.prototype = {
  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() {
    var elements = Form.getElements(this.element);
    for (var i = 0; i < elements.length; i++)
      this.registerCallback(elements[i]);
  },

  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();
Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

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

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,

  element: function(event) {
    return event.target || event.srcElement;
  },

  isLeftClick: function(event) {
    return (((event.which) && (event.which == 1)) ||
            ((event.button) && (event.button == 1)));
  },

  pointerX: function(event) {
    return event.pageX || (event.clientX +
      (document.documentElement.scrollLeft || document.body.scrollLeft));
  },

  pointerY: function(event) {
    return event.pageY || (event.clientY +
      (document.documentElement.scrollTop || document.body.scrollTop));
  },

  stop: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    } else {
      event.returnValue = false;
      event.cancelBubble = true;
    }
  },

  // find the first node with the given tagName, starting from the
  // node the event was triggered on; traverses the DOM upwards
  findElement: function(event, tagName) {
    var element = Event.element(event);
    while (element.parentNode && (!element.tagName ||
        (element.tagName.toUpperCase() != tagName.toUpperCase())))
      element = element.parentNode;
    return element;
  },

  observers: false,

  _observeAndCache: function(element, name, observer, useCapture) {
    if (!this.observers) this.observers = [];
    if (element.addEventListener) {
      this.observers.push([element, name, observer, useCapture]);
      element.addEventListener(name, observer, useCapture);
    } else if (element.attachEvent) {
      this.observers.push([element, name, observer, useCapture]);
      element.attachEvent('on' + name, observer);
    }
  },

  unloadCache: function() {
    if (!Event.observers) return;
    for (var i = 0; i < Event.observers.length; i++) {
      Event.stopObserving.apply(this, Event.observers[i]);
      Event.observers[i][0] = null;
    }
    Event.observers = false;
  },

  observe: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.attachEvent))
      name = 'keydown';

    Event._observeAndCache(element, name, observer, useCapture);
  },

  stopObserving: function(element, name, observer, useCapture) {
    element = $(element);
    useCapture = useCapture || false;

    if (name == 'keypress' &&
        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
        || element.detachEvent))
      name = 'keydown';

    if (element.removeEventListener) {
      element.removeEventListener(name, observer, useCapture);
    } else if (element.detachEvent) {
      try {
        element.detachEvent('on' + name, observer);
      } catch (e) {}
    }
  }
});

/* prevent memory leaks in IE */
if (navigator.appVersion.match(/\bMSIE\b/))
  Event.observe(window, 'unload', Event.unloadCache, false);
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;
  },

  realOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return [valueL, valueT];
  },

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

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        p = Element.getStyle(element, 'position');
        if (p == 'relative' || p == 'absolute') break;
      }
    } while (element);
    return [valueL, valueT];
  },

  offsetParent: 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;
  },

  // 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 = this.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 = this.realOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = this.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;
  },

  page: 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)
        if (Element.getStyle(element,'position')=='absolute') break;

    } while (element = element.offsetParent);

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

    return [valueL, valueT];
  },

  clone: function(source, target) {
    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 = Position.page(source);

    // find coordinate system to use
    target = $(target);
    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(target,'position') == 'absolute') {
      parent = Position.offsetParent(target);
      delta = Position.page(parent);
    }

    // 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)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
  },

  absolutize: function(element) {
    element = $(element);
    if (element.style.position == 'absolute') return;
    Position.prepare();

    var offsets = Position.positionedOffset(element);
    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';;
  },

  relativize: function(element) {
    element = $(element);
    if (element.style.position == 'relative') return;
    Position.prepare();

    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;
  }
}

// Safari returns margins on body which is incorrect if the child is absolutely
// positioned.  For performance reasons, redefine Position.cumulativeOffset for
// KHTML/WebKit only.
if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
  Position.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 [valueL, valueT];
  }
}

Element.addMethods();// script.aculo.us scriptaculous.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

var Scriptaculous = {
  Version: '1.6.4',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
  },
  load: function() {
    if((typeof Prototype=='undefined') || 
       (typeof Element == 'undefined') || 
       (typeof Element.Methods=='undefined') ||
       parseFloat(Prototype.Version.split(".")[0] + "." +
                  Prototype.Version.split(".")[1]) < 1.5)
       throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0");
    
    $A(document.getElementsByTagName("script")).findAll( function(s) {
      return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
    }).each( function(s) {
      var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
      var includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
}

//Scriptaculous.load();// script.aculo.us builder.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// See scriptaculous.js for full license.

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();
    
    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;
      
    // see if browser added wrapping tags
    if(element && (element.tagName != elementName))
      element = element.getElementsByTagName(elementName)[0];
    
    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);
    
    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array)) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1]) 
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
            }
        } 

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return element;
  },
  _text: function(text) {
     return document.createTextNode(text);
  },
  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute=='className' ? 'class' : attribute) +
          '="' + attributes[attribute].toString().escapeHTML() + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e)
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children)) 
         element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  dump: function(scope) { 
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 
  
    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
  
    tags.each( function(tag){ 
      scope[tag] = function() { 
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));  
      } 
    });
  }
}// script.aculo.us effects.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
// 
// See scriptaculous.js for full license.  

// converts rgb() and #xxx to #xxxxxx format,  
// returns self (or first argument) if not convertable  
String.prototype.parseColor = function() {  
  var color = '#';  
  if(this.slice(0,4) == 'rgb(') {  
    var cols = this.slice(4,this.length-1).split(',');  
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
  } else {  
    if(this.slice(0,1) == '#') {  
      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
      if(this.length==7) color = this.toLowerCase();  
    }  
  }  
  return(color.length==7 ? color : (arguments[0] || this));  
}

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

Element.collectTextNodes = function(element) {  
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue : 
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
}

Element.collectTextNodesIgnoreClass = function(element, className) {  
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue : 
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
}

Element.setContentZoom = function(element, percent) {
  element = $(element);  
  Element.setStyle(element, {fontSize: (percent/100) + 'em'});   
  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}

Element.getOpacity = function(element){  
  var opacity;
  if (opacity = Element.getStyle(element, 'opacity'))  
    return parseFloat(opacity);  
  if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))  
    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
  return 1.0;  
}

Element.setOpacity = function(element, value){  
  element= $(element);  
  if (value == 1){
    Element.setStyle(element, { opacity: 
      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
      0.999999 : 1.0 });
    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
      Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
  } else {  
    if(value < 0.00001) value = 0;  
    Element.setStyle(element, {opacity: value});
    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
     Element.setStyle(element, 
       { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
                 'alpha(opacity='+value*100+')' });  
  }
}  
 
Element.getInlineOpacity = function(element){  
  return $(element).style.opacity || '';
}  

Element.childrenWithClassName = function(element, className, findFirst) {
  var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
  var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { 
    return (c.className && c.className.match(classNameRegExp));
  });
  if(!results) results = [];
  return results;
}

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

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

Array.prototype.call = function() {
  var args = arguments;
  this.each(function(f){ f.apply(this, args) });
}

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

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  tagifyText: function(element) {
    if(typeof Builder == 'undefined')
      throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
      
    var tagifyStyle = 'position:relative';
    if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
    element = $(element);
    $A(element.childNodes).each( function(child) {
      if(child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            Builder.node('span',{style: tagifyStyle},
              character == ' ' ? String.fromCharCode(160) : character), 
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if(((typeof element == 'object') || 
        (typeof element == 'function')) && 
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;
      
    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || {});
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || {});
    Effect[element.visible() ? 
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

var Effect2 = Effect; // deprecated

/* ------------- transitions ------------- */

Effect.Transitions = {}

Effect.Transitions.linear = Prototype.K;

Effect.Transitions.sinoidal = function(pos) {
  return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse  = function(pos) {
  return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
  return (Math.floor(pos*10) % 2 == 0 ? 
    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
  return 0;
}
Effect.Transitions.full = function(pos) {
  return 1;
}

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create();
Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();
    
    var position = (typeof effect.options.queue == 'string') ? 
      effect.options.queue : effect.options.queue.position;
    
    switch(position) {
      case 'front':
        // move unstarted effects after this effect  
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }
    
    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);
    
    if(!this.interval) 
      this.interval = setInterval(this.loop.bind(this), 40);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if(this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    this.effects.invoke('loop', timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if(typeof queueName != 'string') return queueName;
    
    if(!this.instances[queueName])
      this.instances[queueName] = new Effect.ScopedQueue();
      
    return this.instances[queueName];
  }
}
Effect.Queue = Effect.Queues.get('global');

Effect.DefaultOptions = {
  transition: Effect.Transitions.sinoidal,
  duration:   1.0,   // seconds
  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
  sync:       false, // true for combining
  from:       0.0,
  to:         1.0,
  delay:      0.0,
  queue:      'parallel'
}

Effect.Base = function() {};
Effect.Base.prototype = {
  position: null,
  start: function(options) {
    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn + (this.options.duration*1000);
    this.event('beforeStart');
    if(!this.options.sync)
      Effect.Queues.get(typeof this.options.queue == 'string' ? 
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if(timePos >= this.startOn) {
      if(timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if(this.finish) this.finish(); 
        this.event('afterFinish');
        return;  
      }
      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
      var frame = Math.round(pos * this.options.fps * this.options.duration);
      if(frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  render: function(pos) {
    if(this.state == 'idle') {
      this.state = 'running';
      this.event('beforeSetup');
      if(this.setup) this.setup();
      this.event('afterSetup');
    }
    if(this.state == 'running') {
      if(this.options.transition) pos = this.options.transition(pos);
      pos *= (this.options.to-this.options.from);
      pos += this.options.from;
      this.position = pos;
      this.event('beforeUpdate');
      if(this.update) this.update(pos);
      this.event('afterUpdate');
    }
  },
  cancel: function() {
    if(!this.options.sync)
      Effect.Queues.get(typeof this.options.queue == 'string' ? 
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if(this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
}

Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if(effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    if(!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || {});
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    if(!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || {});
    this.start(options);
  },
  setup: function() {
    // Bug in Opera: Opera returns the "real" position of a static element or
    // relative element that does not have top/left explicitly set.
    // ==> Always set top and left for position relative elements in your stylesheets 
    // (to 0 if you do not need them) 
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if(this.options.mode == 'absolute') {
      // absolute movement, so we need to calc deltaX and deltaY
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {      
    this.element.setStyle({
      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element, 
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
};

Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
  initialize: function(element, percent) {
    this.element = $(element);
    if(!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || {});
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');
    
    this.originalStyle = {};
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));
      
    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;
    
    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if(fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));
    
    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
    
    this.dims = null;
    if(this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if(/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if(!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if(this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = {};
    if(this.options.scaleX) d.width = Math.round(width) + 'px';
    if(this.options.scaleY) d.height = Math.round(height) + 'px';
    if(this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if(this.elementPositioning == 'absolute') {
        if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if(this.options.scaleY) d.top = -topd + 'px';
        if(this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    if(!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = {
      backgroundImage: this.element.getStyle('background-image') };
    this.element.setStyle({backgroundImage: 'none'});
    if(!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if(!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
  initialize: function(element) {
    this.element = $(element);
    this.start(arguments[1] || {});
  },
  setup: function() {
    Position.prepare();
    var offsets = Position.cumulativeOffset(this.element);
    if(this.options.offset) offsets[1] += this.options.offset;
    var max = window.innerHeight ? 
      window.height - window.innerHeight :
      document.body.scrollHeight - 
        (document.documentElement.clientHeight ? 
          document.documentElement.clientHeight : document.body.clientHeight);
    this.scrollStart = Position.deltaY;
    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
  },
  update: function(position) {
    Position.prepare();
    window.scrollTo(Position.deltaX, 
      this.scrollStart + (position*this.delta));
  }
});

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
  from: element.getOpacity() || 1.0,
  to:   0.0,
  afterFinishInternal: function(effect) { 
    if(effect.options.to!=0) return;
    effect.element.hide();
    effect.element.setStyle({opacity: oldOpacity}); 
  }}, arguments[1] || {});
  return new Effect.Opacity(element,options);
}

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from);
    effect.element.show(); 
  }}, arguments[1] || {});
  return new Effect.Opacity(element,options);
}

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = { 
    opacity: element.getInlineOpacity(), 
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200, 
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
     Object.extend({ duration: 1.0, 
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element)
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide();
         effect.effects[0].element.setStyle(oldStyle); }
     }, arguments[1] || {})
   );
}

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false, 
      scaleX: false, 
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide();
        effect.element.undoClipping();
      } 
    }, arguments[1] || {})
  );
}

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({ 
    scaleContent: false, 
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping();
      effect.element.setStyle({height: '0px'});
      effect.element.show(); 
    },  
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || {}));
}

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, { 
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) { 
          effect.element.makePositioned();
          effect.element.makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide();
          effect.element.undoClipping();
          effect.element.undoPositioned();
          effect.element.setStyle({opacity: oldOpacity});
        }
      })
    }
  }, arguments[1] || {}));
}

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned(); 
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide();
          effect.effects[0].element.undoPositioned();
          effect.effects[0].element.setStyle(oldStyle);
        } 
      }, arguments[1] || {}));
}

Effect.Shake = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element, 
      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
        effect.element.undoPositioned();
        effect.element.setStyle(oldStyle);
  }}) }}) }}) }}) }}) }});
}

Effect.SlideDown = function(element) {
  element = $(element);
  element.cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({ 
    scaleContent: false, 
    scaleX: false, 
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.firstChild.makePositioned();
      if(window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping();
      effect.element.setStyle({height: '0px'});
      effect.element.show(); },
    afterUpdateInternal: function(effect) {
      effect.element.firstChild.setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping(); 
      // IE will crash if child is undoPositioned first
      if(/MSIE/.test(navigator.userAgent) && !window.opera){
        effect.element.undoPositioned();
        effect.element.firstChild.undoPositioned();
      }else{
        effect.element.firstChild.undoPositioned();
        effect.element.undoPositioned();
      }
      effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || {})
  );
}

Effect.SlideUp = function(element) {
  element = $(element);
  element.cleanWhitespace();
  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false, 
    scaleX: false, 
    scaleMode: 'box',
    scaleFrom: 100,
    restoreAfterFinish: true,
    beforeStartInternal: function(effect) {
      effect.element.makePositioned();
      effect.element.firstChild.makePositioned();
      if(window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping();
      effect.element.show(); },  
    afterUpdateInternal: function(effect) {
      effect.element.firstChild.setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' }); },
    afterFinishInternal: function(effect) {
      effect.element.hide();
      effect.element.undoClipping();
      effect.element.firstChild.undoPositioned();
      effect.element.undoPositioned();
      effect.element.setStyle({bottom: oldInnerBottom}); }
   }, arguments[1] || {})
  );
}

// Bug in opera makes the TD containing this element expand for a instance after finish 
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, 
    { restoreAfterFinish: true,
      beforeSetup: function(effect) {
        effect.element.makeClipping(effect.element); },  
      afterFinishInternal: function(effect) {
        effect.element.hide(effect.element); 
        effect.element.undoClipping(effect.element); }
  });
}

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || {});
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();    
  var initialMoveX, initialMoveY;
  var moveX, moveY;
  
  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0; 
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }
  
  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01, 
    beforeSetup: function(effect) {
      effect.element.hide();
      effect.element.makeClipping();
      effect.element.makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'});
               effect.effects[0].element.show(); 
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping();
               effect.effects[0].element.undoPositioned();
               effect.effects[0].element.setStyle(oldStyle); 
             }
           }, options)
      )
    }
  });
}

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || {});
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;
  
  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':  
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }
  
  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({            
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned();
           effect.effects[0].element.makeClipping(); },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide();
           effect.effects[0].element.undoClipping();
           effect.effects[0].element.undoPositioned();
           effect.effects[0].element.setStyle(oldStyle); }
       }, options)
  );
}

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || {};
  var oldOpacity = element.getInlineOpacity();
  var transition = options.transition || Effect.Transitions.sinoidal;
  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
  reverser.bind(transition);
  return new Effect.Opacity(element, 
    Object.extend(Object.extend({  duration: 3.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
}

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  Element.makeClipping(element);
  return new Effect.Scale(element, 5, Object.extend({   
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, { 
      scaleContent: false, 
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide();
        effect.element.undoClipping(); 
        effect.element.setStyle(oldStyle);
      } });
  }}, arguments[1] || {}));
};

['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( 
  function(f) { Element.Methods[f] = Element[f]; }
);

Element.Methods.visualEffect = function(element, effect, options) {
  s = effect.gsub(/_/, '-').camelize();
  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
  new Effect[effect_class](element, options);
  return $(element);
};

Element.addMethods();// script.aculo.us dragdrop.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
// 
// See scriptaculous.js for full license.

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

if(typeof Effect == 'undefined')
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || {});

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if((typeof containment == 'object') && 
        (containment.constructor == Array)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }
    
    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },
  
  findDeepestChild: function(drops) {
    deepest = drops[0];
      
    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];
    
    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode; 
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },
  
  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect( 
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var affected = [];
    
    if(this.last_active) this.deactivate(this.last_active);
    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });
        
    if(affected.length>0) {
      drop = Droppables.findDeepestChild(affected);
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
      
      Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) 
        this.last_active.onDrop(element, this.last_active.element, event);
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
}

var Draggables = {
  drags: [],
  observers: [],
  lockDrag: false, //Jason edit
  
  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
      
      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },
  
  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },
  
  activate: function(draggable) {
    if(draggable.options.delay) { 
      this._timeout = setTimeout(function() { 
        Draggables._timeout = null; 
        window.focus(); 
        Draggables.activeDraggable = draggable; 
      }.bind(this), draggable.options.delay); 
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },
  
  deactivate: function() {
    this.activeDraggable = null;
  },
  
  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;
    
    this.activeDraggable.updateDrag(event, pointer);
  },
  
  endDrag: function(event) {
    if(this._timeout) { 
      clearTimeout(this._timeout); 
      this._timeout = null; 
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },
  
  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },
  
  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },
  
  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },
  
  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },
  
  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
}

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

var Draggable = Class.create();
Draggable._dragging    = {};

Draggable.prototype = {
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){ 
            Draggable._dragging[element] = false 
          }
        }); 
      },
      zindex: 1000,
      revert: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };
    
    if(arguments[1] && typeof arguments[1].endeffect == 'undefined')
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
        }
      });
    
    var options = Object.extend(defaults, arguments[1] || {});

    this.element = $(element);
    
    if(options.handle && (typeof options.handle == 'string')) {
      var h = Element.childrenWithClassName(this.element, options.handle, true);
      if(h.length>0) this.handle = h[0];
    }
    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;
    
    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE    

    this.delta    = this.currentDelta();
    this.options  = options;
    this.dragging = false;   

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
    
    Draggables.register(this);
  },
  
  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },
  
  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },
  
  initDrag: function(event) {
    if(typeof Draggable._dragging[this.element] != 'undefined' &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {    
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if(src.tagName && (
        src.tagName=='INPUT' ||
        src.tagName=='SELECT' ||
        src.tagName=='OPTION' ||
        src.tagName=='BUTTON' ||
        src.tagName=='TEXTAREA')) return;
        
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
      
      Draggables.activate(this);
      Event.stop(event);
    }
  },
  
  startDrag: function(event) {
    this.dragging = true;
    
    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }
    
    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      var dim = Element.getDimensions(this.element); //Jason's dimensions Fix
      Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
      Element.setStyle(this.element, {width: dim.width+"px", height: dim.height+"px"}); //Jason's dimensions Fix
    }
    
    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }
    
    Draggables.notify('onStart', this, event);
        
    if(this.options.starteffect) this.options.starteffect(this.element);
  },
  
  updateDrag: function(event, pointer) {
    if (Draggables.lockDrag) return; //Jason edit

    if(!this.dragging) this.startDrag(event);
    Position.prepare();
    Droppables.show(pointer, this.element);
    Draggables.notify('onDrag', this, event);
    
    this.draw(pointer);
    if(this.options.change) this.options.change(this);
    
    if(this.options.scroll) {
      this.stopScrolling();
      
      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft;
        p[1] += this.options.scroll.scrollTop;
        
        p[0] += (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0);
        p[1] += (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
        
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }
    
    // fix AppleWebKit rendering
    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
    
    Event.stop(event);
  },
  
  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.ghosting) {
      Position.relativize(this.element);
      Element.remove(this._clone);
      this._clone = null;
    }

    if(success) Droppables.fire(event, this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && typeof revert == 'function') revert = revert(this.element);
    
    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      this.options.reverteffect(this.element, 
        d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect) 
      this.options.endeffect(this.element);
      
    Draggables.deactivate(this);
    Droppables.reset();
  },
  
  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },
  
  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },
  
  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      window.status = r.inspect();
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }
    
    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];
    
    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }
    
    var p = [0,1].map(function(i){ 
      return (point[i]-pos[i]-this.offset[i]) 
    }.bind(this));
    
    if(this.options.snap) {
      if(typeof this.options.snap == 'function') {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(this.options.snap instanceof Array) {
        p = p.map( function(v, i) {
          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
      } else {
        p = p.map( function(v) {
          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
      }
    }}
    
    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";
    
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },
  
  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },
  
  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },
  
  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }
    
    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }
    
    if(this.options.change) this.options.change(this);
  },
  
  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
}

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

var SortableObserver = Class.create();
SortableObserver.prototype = {
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },
  
  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },
  
  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
}

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
  
  sortables: {},
  
  _findRootElement: function(element) {
    while (element.tagName != "BODY") {  
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },
  
  destroy: function(element){
    var s = Sortable.options(element);
    
    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');
      
      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({ 
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,
      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || {});

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables  
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
      //greedy:      !options.dropOnEmpty
    }
    
    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    }

    // fix for gecko engine
    Element.cleanWhitespace(element); 

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (this.findElements(element, options) || []).each( function(e) {
      // handles are per-draggable
      var handle = options.handle ? 
        Element.childrenWithClassName(e, options.handle)[0] : e;    
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);      
    });
    
    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },
  
  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode) 
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode) 
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },
  
  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);
        
    if(!Element.isParent(dropon, element)) {
      var index;
      
      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;
            
      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
        
        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }
      
      dropon.insertBefore(element, child);
      
      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Element.hide(Sortable._marker);
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return; 

    if(!Sortable._marker) {
      Sortable._marker = $('dropmarker') || document.createElement('DIV');
      Element.hide(Sortable._marker);
      Element.addClassName(Sortable._marker, 'dropmarker');
      Sortable._marker.style.position = 'absolute';
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }    
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.style.left = offsets[0] + 'px';
    Sortable._marker.style.top = offsets[1] + 'px';
    
    if(position=='after')
      if(sortable.overlap == 'horizontal') 
        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
      else
        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
    
    Element.show(Sortable._marker);
  },
  
  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];
  
    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;
      
      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: new Array,
        position: parent.children.length,
        container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
      }
      
      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child)
      
      parent.children.push (child);
    }

    return parent; 
  },

  /* Finds the first element of the given tag type within a parent element.
    Used for finding the first LI[ST] within a L[IST]I[TEM].*/
  _findChildrenElement: function (element, containerTag) {
    if (element && element.hasChildNodes)
      for (var i = 0; i < element.childNodes.length; ++i)
        if (element.childNodes[i].tagName == containerTag)
          return element.childNodes[i];
  
    return null;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || {});
    
    var root = {
      id: null,
      parent: null,
      children: new Array,
      container: element,
      position: 0
    }
    
    return Sortable._tree (element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || {});
    
    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || {});
    
    var nodeMap = {};
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });
   
    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },
  
  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || {});
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
    
    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" + 
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
}

/* Returns true if child is contained within element */
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;

  if (child.parentNode == element) return true;

  return Element.isParent(child.parentNode, element);
}

Element.findChildren = function(element, only, recursive, tagName) {    
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
}

Element.offsetSize = function (element, type) {
  if (type == 'vertical' || type == 'height')
    return element.offsetHeight;
  else
    return element.offsetWidth;
}// script.aculo.us controls.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
// 
// See scriptaculous.js for full license.

// Autocompleter.Base handles all the autocompletion functionality 
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least, 
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method 
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most 
// useful when one of the tokens is \n (a newline), as it 
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
  baseInitialize: function(element, update, options) {
    this.element     = $(element); 
    this.update      = $(update);  
    this.hasFocus    = false; 
    this.changed     = false; 
    this.active      = false; 
    this.index       = 0;     
    this.entryCount  = 0;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || {};

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow || 
      function(element, update){ 
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false, 
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide || 
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);

    this.observer = null;
    
    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix && 
      (navigator.appVersion.indexOf('MSIE')>0) &&
      (navigator.userAgent.indexOf('Opera')<0) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update, 
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },
  
  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
      }
     else 
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
         (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex) 
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },
  
  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },
  
  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;     
  }, 
  
  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ? 
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");
        
      if(this.hasFocus) { 
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },
  
  markPrevious: function() {
    if(this.index > 0) this.index--
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },
  
  markNext: function() {
    if(this.index < this.entryCount-1) this.index++
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },
  
  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },
  
  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },
  
  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    
    var lastTokenPos = this.findLastToken();
    if (lastTokenPos != -1) {
      var newValue = this.element.value.substr(0, lastTokenPos + 1);
      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value;
    } else {
      this.element.value = value;
    }
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.firstChild);

      if(this.update.firstChild && this.update.firstChild.childNodes) {
        this.entryCount = 
          this.update.firstChild.childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else { 
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;
      
      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;   
    if(this.getToken().length>=this.options.minChars) {
      this.startIndicator();
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
  },

  getToken: function() {
    var tokenPos = this.findLastToken();
    if (tokenPos != -1)
      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
    else
      var ret = this.element.value;

    return /\n/.test(ret) ? '' : ret;
  },

  findLastToken: function() {
    var lastTokenPos = -1;

    for (var i=0; i<this.options.tokens.length; i++) {
      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
      if (thisTokenPos > lastTokenPos)
        lastTokenPos = thisTokenPos;
    }
    return lastTokenPos;
  }
}

Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams) 
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }

});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the 
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector' 
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&  
          ret.length < instance.options.choices ; i++) { 

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ? 
            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) { 
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars && 
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ? 
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || {});
  }
});

// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
}

Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
  initialize: function(element, url, options) {
    this.url = url;
    this.element = $(element);

    this.options = Object.extend({
      okButton: true,
      okText: "ok",
      cancelLink: true,
      cancelText: "cancel",
      savingText: "Saving...",
      clickToEditText: "Click to edit",
      okText: "ok",
      rows: 1,
      onComplete: function(transport, element) {
        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
      },
      onFailure: function(transport) {
        alert("Error communicating with the server: " + transport.responseText.stripTags());
      },
      callback: function(form) {
        return Form.serialize(form);
      },
      handleLineBreaks: true,
      loadingText: 'Loading...',
      savingClassName: 'inplaceeditor-saving',
      loadingClassName: 'inplaceeditor-loading',
      formClassName: 'inplaceeditor-form',
      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
      highlightendcolor: "#FFFFFF",
      externalControl: null,
      submitOnBlur: false,
      ajaxOptions: {},
      evalScripts: false
    }, options || {});

    if(!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + "-inplaceeditor";
      if ($(this.options.formId)) {
        // there's already a form with that name, don't specify an id
        this.options.formId = null;
      }
    }
    
    if (this.options.externalControl) {
      this.options.externalControl = $(this.options.externalControl);
    }
    
    this.originalBackground = Element.getStyle(this.element, 'background-color');
    if (!this.originalBackground) {
      this.originalBackground = "transparent";
    }
    
    this.element.title = this.options.clickToEditText;
    
    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
    //Event.observe(this.element, 'click', this.onclickListener);
    //Event.observe(this.element, 'mouseover', this.mouseoverListener);
    //Event.observe(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.observe(this.options.externalControl, 'click', this.onclickListener);
      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  },
  enterEditMode: function(evt) {
    if (this.saving) return;
    if (this.editing) return;
    this.editing = true;
    this.onEnterEditMode();
    if (this.options.externalControl) {
      Element.hide(this.options.externalControl);
    }
    Element.hide(this.element);
    this.createForm();
    this.element.parentNode.insertBefore(this.form, this.element);
    if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
    // stop the event to avoid a page refresh in Safari
    if (evt) {
      Event.stop(evt);
    }
    return false;
  },
  createForm: function() {
    this.form = document.createElement("form");
    this.form.id = this.options.formId;
    Element.addClassName(this.form, this.options.formClassName)
    this.form.onsubmit = this.onSubmit.bind(this);

    this.createEditField();

    if (this.options.textarea) {
      var br = document.createElement("br");
      this.form.appendChild(br);
    }

    if (this.options.okButton) {
      okButton = document.createElement("input");
      okButton.type = "submit";
      okButton.value = this.options.okText;
      okButton.className = 'editor_ok_button';
      this.form.appendChild(okButton);
    }

    if (this.options.cancelLink) {
      cancelLink = document.createElement("a");
      cancelLink.href = "#";
      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
      cancelLink.onclick = this.onclickCancel.bind(this);
      cancelLink.className = 'editor_cancel';      
      this.form.appendChild(cancelLink);
    }
  },
  hasHTMLLineBreaks: function(string) {
    if (!this.options.handleLineBreaks) return false;
    return string.match(/<br/i) || string.match(/<p>/i);
  },
  convertHTMLLineBreaks: function(string) {
    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
  },
  createEditField: function() {
    var text;
    if(this.options.loadTextURL) {
      text = this.options.loadingText;
    } else {
      text = this.getText();
    }

    var obj = this;
    
    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
      this.options.textarea = false;
      var textField = document.createElement("input");
      textField.obj = this;
      textField.type = "text";
      textField.name = "value";
      textField.value = text;
      textField.style.backgroundColor = this.options.highlightcolor;
      textField.className = 'editor_field';
      var size = this.options.size || this.options.cols || 0;
      if (size != 0) textField.size = size;
      if (this.options.submitOnBlur)
        textField.onblur = this.onSubmit.bind(this);
      this.editField = textField;
    } else {
      this.options.textarea = true;
      var textArea = document.createElement("textarea");
      textArea.obj = this;
      textArea.name = "value";
      textArea.value = this.convertHTMLLineBreaks(text);
      textArea.rows = this.options.rows;
      textArea.cols = this.options.cols || 40;
      textArea.className = 'editor_field';      
      if (this.options.submitOnBlur)
        textArea.onblur = this.onSubmit.bind(this);
      this.editField = textArea;
    }
    
    if(this.options.loadTextURL) {
      this.loadExternalText();
    }
    this.form.appendChild(this.editField);
  },
  getText: function() {
    return this.element.innerHTML;
  },
  loadExternalText: function() {
    Element.addClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = true;
    new Ajax.Request(
      this.options.loadTextURL,
      Object.extend({
        asynchronous: true,
        onComplete: this.onLoadedExternalText.bind(this)
      }, this.options.ajaxOptions)
    );
  },
  onLoadedExternalText: function(transport) {
    Element.removeClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = false;
    this.editField.value = transport.responseText;
    Field.scrollFreeActivate(this.editField);
  },
  onclickCancel: function() {
    this.onComplete();
    this.leaveEditMode();
    return false;
  },
  onFailure: function(transport) {
    this.options.onFailure(transport);
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
      this.oldInnerHTML = null;
    }
    return false;
  },
  onSubmit: function() {
    // onLoading resets these so we need to save them away for the Ajax call
    var form = this.form;
    var value = this.editField.value;
    
    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
    // to be displayed indefinitely
    this.onLoading();
    
    if (this.options.evalScripts) {
      new Ajax.Request(
        this.url, Object.extend({
          parameters: this.options.callback(form, value),
          onComplete: this.onComplete.bind(this),
          onFailure: this.onFailure.bind(this),
          asynchronous:true, 
          evalScripts:true
        }, this.options.ajaxOptions));
    } else  {
      new Ajax.Updater(
        { success: this.element,
          // don't update on failure (this could be an option)
          failure: null }, 
        this.url, Object.extend({
          parameters: this.options.callback(form, value),
          onComplete: this.onComplete.bind(this),
          onFailure: this.onFailure.bind(this)
        }, this.options.ajaxOptions));
    }
    // stop the event to avoid a page refresh in Safari
    if (arguments.length > 1) {
      Event.stop(arguments[0]);
    }
    return false;
  },
  onLoading: function() {
    this.saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  showSaving: function() {
    this.oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    Element.addClassName(this.element, this.options.savingClassName);
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
  },
  removeForm: function() {
    if(this.form) {
      if (this.form.parentNode) Element.remove(this.form);
      this.form = null;
    }
  },
  enterHover: function() {
    if (this.saving) return;
    this.element.style.backgroundColor = this.options.highlightcolor;
    if (this.effect) {
      this.effect.cancel();
    }
    Element.addClassName(this.element, this.options.hoverClassName)
  },
  leaveHover: function() {
    if (this.options.backgroundColor) {
      this.element.style.backgroundColor = this.oldBackground;
    }
    Element.removeClassName(this.element, this.options.hoverClassName)
    if (this.saving) return;
    this.effect = new Effect.Highlight(this.element, {
      startcolor: this.options.highlightcolor,
      endcolor: this.options.highlightendcolor,
      restorecolor: this.originalBackground
    });
  },
  leaveEditMode: function() {
    Element.removeClassName(this.element, this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
    if (this.options.externalControl) {
      Element.show(this.options.externalControl);
    }
    this.editing = false;
    this.saving = false;
    this.oldInnerHTML = null;
    this.onLeaveEditMode();
  },
  onComplete: function(transport) {
    this.leaveEditMode();
    this.options.onComplete.bind(this)(transport, this.element);
  },
  onEnterEditMode: function() {},
  onLeaveEditMode: function() {},
  dispose: function() {
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
    }
    this.leaveEditMode();
    Event.stopObserving(this.element, 'click', this.onclickListener);
    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  }
};

Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
  createEditField: function() {
    if (!this.cached_selectTag) {
      var selectTag = document.createElement("select");
      var collection = this.options.collection || [];
      var optionTag;
      collection.each(function(e,i) {
        optionTag = document.createElement("option");
        optionTag.value = (e instanceof Array) ? e[0] : e;
        if((typeof this.options.value == 'undefined') && 
          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
        if(this.options.value==optionTag.value) optionTag.selected = true;
        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
        selectTag.appendChild(optionTag);
      }.bind(this));
      this.cached_selectTag = selectTag;
    }

    this.editField = this.cached_selectTag;
    if(this.options.loadTextURL) this.loadExternalText();
    this.form.appendChild(this.editField);
    this.options.callback = function(form, value) {
      return "value=" + encodeURIComponent(value);
    }
  }
});

// Delayed observer, like Form.Element.Observer, 
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element); 
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
};
// script.aculo.us slider.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006

// Copyright (c) 2005 Marty Haught, Thomas Fuchs 
//
// See http://script.aculo.us for more info
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

if(!Control) var Control = {};
Control.Slider = Class.create();

// options:
//  axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
//  onChange(value)
//  onSlide(value)
Control.Slider.prototype = {
  initialize: function(handle, track, options) {
    var slider = this;
    
    if(handle instanceof Array) {
      this.handles = handle.collect( function(e) { return $(e) });
    } else {
      this.handles = [$(handle)];
    }
    
    this.track   = $(track);
    this.options = options || {};

    this.axis      = this.options.axis || 'horizontal';
    this.increment = this.options.increment || 1;
    this.step      = parseInt(this.options.step || '1');
    this.range     = this.options.range || $R(0,1);
    
    this.value     = 0; // assure backwards compat
    this.values    = this.handles.map( function() { return 0 });
    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
    this.options.startSpan = $(this.options.startSpan || null);
    this.options.endSpan   = $(this.options.endSpan || null);

    this.restricted = this.options.restricted || false;

    this.maximum   = this.options.maximum || this.range.end;
    this.minimum   = this.options.minimum || this.range.start;

    // Will be used to align the handle onto the track, if necessary
    this.alignX = parseInt(this.options.alignX || '0');
    this.alignY = parseInt(this.options.alignY || '0');
    
    this.trackLength = this.maximumOffset() - this.minimumOffset();

    this.handleLength = this.isVertical() ? 
      (this.handles[0].offsetHeight != 0 ? 
        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : 
      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : 
        this.handles[0].style.width.replace(/px$/,""));

    this.active   = false;
    this.dragging = false;
    this.disabled = false;

    if(this.options.disabled) this.setDisabled();

    // Allowed values array
    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
    if(this.allowedValues) {
      this.minimum = this.allowedValues.min();
      this.maximum = this.allowedValues.max();
    }

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);

    // Initialize handles in reverse (make sure first handle is active)
    this.handles.each( function(h,i) {
      i = slider.handles.length-1-i;
      slider.setValue(parseFloat(
        (slider.options.sliderValue instanceof Array ? 
          slider.options.sliderValue[i] : slider.options.sliderValue) || 
         slider.range.start), i);
      Element.makePositioned(h); // fix IE
      Event.observe(h, "mousedown", slider.eventMouseDown);
    });
    
    Event.observe(this.track, "mousedown", this.eventMouseDown);
    Event.observe(document, "mouseup", this.eventMouseUp);
    Event.observe(document, "mousemove", this.eventMouseMove);
    
    this.initialized = true;
  },
  dispose: function() {
    var slider = this;    
    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
    Event.stopObserving(document, "mouseup", this.eventMouseUp);
    Event.stopObserving(document, "mousemove", this.eventMouseMove);
    this.handles.each( function(h) {
      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
    });
  },
  setDisabled: function(){
    this.disabled = true;
  },
  setEnabled: function(){
    this.disabled = false;
  },  
  getNearestValue: function(value){
    if(this.allowedValues){
      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
      
      var offset = Math.abs(this.allowedValues[0] - value);
      var newValue = this.allowedValues[0];
      this.allowedValues.each( function(v) {
        var currentOffset = Math.abs(v - value);
        if(currentOffset <= offset){
          newValue = v;
          offset = currentOffset;
        } 
      });
      return newValue;
    }
    if(value > this.range.end) return this.range.end;
    if(value < this.range.start) return this.range.start;
    return value;
  },
  setValue: function(sliderValue, handleIdx){
    if(!this.active) {
      this.activeHandleIdx = handleIdx || 0;
      this.activeHandle    = this.handles[this.activeHandleIdx];
      this.updateStyles();
    }
    handleIdx = handleIdx || this.activeHandleIdx || 0;
    if(this.initialized && this.restricted) {
      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
        sliderValue = this.values[handleIdx-1];
      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
        sliderValue = this.values[handleIdx+1];
    }
    sliderValue = this.getNearestValue(sliderValue);
    this.values[handleIdx] = sliderValue;
    this.value = this.values[0]; // assure backwards compat
    
    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
      this.translateToPx(sliderValue);
    
    this.drawSpans();
    if(!this.dragging || !this.event) this.updateFinished();
  },
  setValueBy: function(delta, handleIdx) {
    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
      handleIdx || this.activeHandleIdx || 0);
  },
  translateToPx: function(value) {
    return Math.round(
      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 
      (value - this.range.start)) + "px";
  },
  translateToValue: function(offset) {
    return ((offset/(this.trackLength-this.handleLength) * 
      (this.range.end-this.range.start)) + this.range.start);
  },
  getRange: function(range) {
    var v = this.values.sortBy(Prototype.K); 
    range = range || 0;
    return $R(v[range],v[range+1]);
  },
  minimumOffset: function(){
    return(this.isVertical() ? this.alignY : this.alignX);
  },
  maximumOffset: function(){
    return(this.isVertical() ? 
      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
        this.track.style.height.replace(/px$/,"")) - this.alignY : 
      (this.track.offsetWidth != 0 ? this.track.offsetWidth : 
        this.track.style.width.replace(/px$/,"")) - this.alignY);
  },  
  isVertical:  function(){
    return (this.axis == 'vertical');
  },
  drawSpans: function() {
    var slider = this;
    if(this.spans)
      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
    if(this.options.startSpan)
      this.setSpan(this.options.startSpan,
        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
    if(this.options.endSpan)
      this.setSpan(this.options.endSpan, 
        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
  },
  setSpan: function(span, range) {
    if(this.isVertical()) {
      span.style.top = this.translateToPx(range.start);
      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
    } else {
      span.style.left = this.translateToPx(range.start);
      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
    }
  },
  updateStyles: function() {
    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
    Element.addClassName(this.activeHandle, 'selected');
  },
  startDrag: function(event) {
    if(Event.isLeftClick(event)) {
      if(!this.disabled){
        this.active = true;
        
        var handle = Event.element(event);
        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
        var track = handle;
        if(track==this.track) {
          var offsets  = Position.cumulativeOffset(this.track); 
          this.event = event;
          this.setValue(this.translateToValue( 
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        } else {
          // find the handle (prevents issues with Safari)
          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
            handle = handle.parentNode;
        
          this.activeHandle    = handle;
          this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
          this.updateStyles();
        
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        }
      }
      Event.stop(event);
    }
  },
  update: function(event) {
   if(this.active) {
      if(!this.dragging) this.dragging = true;
      this.draw(event);
      // fix AppleWebKit rendering
      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
      Event.stop(event);
   }
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.track);
    pointer[0] -= this.offsetX + offsets[0];
    pointer[1] -= this.offsetY + offsets[1];
    this.event = event;
    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
    if(this.initialized && this.options.onSlide)
      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
  },
  endDrag: function(event) {
    if(this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },  
  finishDrag: function(event, success) {
    this.active = false;
    this.dragging = false;
    this.updateFinished();
  },
  updateFinished: function() {
    if(this.initialized && this.options.onChange) 
      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
    this.event = null;
  }
}/**
 * Pops up a window that is focused
 *
 * Author: Jason Ho
 */
var FocusedWindow = {

    show: function(content, options) {
        if ($('qaboomFocusedWindow')) return;

        var span = document.createElement(span);
		span.id = "qaboomFocusedWindow";
        if (options && options.className) {
            span.addClassName(options.className);
        }
        span.innerHTML = content;
        Element.setOpacity($('main'), 0.3);
        Element.hide(span);
        document.body.appendChild(span);
        //Reposition the element
        var dim = Element.getDimensions(span);

        var clientHeight, clientWidth;
        if (self.innerHeight) {
            clientHeight = self.innerHeight;
            clientWidth = self.innerWidth;
        } else if (document.documentElement && document.documentElement.clientHeight) {
            clientHeight = document.documentElement.clientHeight;
            clientWidth = document.documentElement.clientWidth;
        } else if (document.body) {
            clientHeight = document.body.clientHeight;
            clientWidth = document.body.clientWidth;
        }
        span.style.top = (clientHeight/2-dim.height/2)+"px";
		span.style.left = (clientWidth/2-dim.width/2)+"px";
        //Element.show(span);
        new Effect.Appear(span, {duration: 0.6});
        Event.observe(document.body, "mouseup", FocusedWindow._hideOnBlur, false);
    },

    center: function() {
        var elementNode = $('qaboomFocusedWindow');
        var dim = Element.getDimensions(elementNode);
        var clientHeight, clientWidth;
        if (self.innerHeight) {
            clientHeight = self.innerHeight;
            clientWidth = self.innerWidth;
        } else if (document.documentElement && document.documentElement.clientHeight) {
            clientHeight = document.documentElement.clientHeight;
            clientWidth = document.documentElement.clientWidth;
        } else if (document.body) {
            clientHeight = document.body.clientHeight;
            clientWidth = document.body.clientWidth;
        }
        elementNode.style.top = (clientHeight/2-dim.height/2)+"px";
		elementNode.style.left = (clientWidth/2-dim.width/2)+"px";
    },

    hide: function() {
        Element.remove('qaboomFocusedWindow');
        Element.setOpacity($('main'), 1.0);
    },

    _hideOnBlur: function(e) {
        Position.prepare();
        var scrollOffsetX = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollOffsetY = document.documentElement.scrollTop || document.body.scrollTop;
        if ($('qaboomFocusedWindow') && !Position.within($('qaboomFocusedWindow'), Event.pointerX(e)-scrollOffsetX, Event.pointerY(e)-scrollOffsetY)) {
            FocusedWindow.hide();
            Event.stopObserving(document.body, "mouseup", FocusedWindow._hideOnBlur, false);
        }
    }
}/**
 * POSTs a form via Ajax Request. If any errors are returned,
 * error messages are added to the form.
 *
 * Author: Jason Ho
 */

var AjaxForm = Class.create();

Object.extend(AjaxForm, {
    /**
     * Used in conjunction with Validator.php
     * Error data should be in the following format:
     * errors\n[fieldID_1]|[errorMessage_1]\n[fieldID_2]|[errorMessage_2]\n ...etc
     */
    handleResponse: function(response) {
        if (!response) return;
        var newline_index = response.indexOf("\n");
        if (newline_index == -1) {
            var status = response;
            var data = "";
        } else {
            var status = response.substring(0,newline_index);
            var data = response.substring(newline_index+1, response.length);
        }
        
        if (status == "success") {
        //Do nothing
        } else if (status == "failure") {
            alert(data);
        } else if (status == "errors") {
            var errors = data.split("\n");
            for (var i = 0; i < errors.length; i++) {
                var error_tuple = errors[i].split("|");
                var fieldID = error_tuple[0];
                var error_message = error_tuple[1];
                AjaxForm.showError(fieldID, error_message);
            }
        }

        return {
            status: status,
            data: data
        };
    },

    showError: function(fieldID, errorMessage) {
        var field = $(fieldID);
        var errorID = fieldID+"_error";
        var errorContainerID = fieldID+"_error_container";
        if ($(errorContainerID)) Element.remove(errorContainerID);
        var errorHTML = "<div id='"+errorContainerID+"' class='errorLine'><span id='"+errorID+"' class='error' style='display:none'>"+errorMessage+"</span></div>";
        new Insertion.After(field, errorHTML);
        field.style.border='1px solid red';
        new Effect.Appear($(errorID), {duration: 0.3});
    },

    hideErrors: function(parentNode) {
        document.getElementsByClassName("errorLine", parentNode).each(Element.remove);
    }
});

AjaxForm.prototype = {
    initialize: function(form, options) {        
        this.options = options || {};
        this.element = $(form);
        Event.observe(this.element, "submit", this.submit.bind(this), false);
    },
	
    submit: function(event) {
        AjaxForm.hideErrors(this.element);
        new Ajax.Request(this.options.handler, {
            parameters: Form.serialize(this.element),
            onSuccess: this._onSuccess.bind(this)
        });
        if (event) Event.stop(event);
        Form.disable(this.element);        
    },
	
    _onSuccess: function(request) {
        Form.enable(this.element);
        if (request.responseText == "") return;
        var response = AjaxForm.handleResponse(request.responseText);
        if (response.status == "success") {
            if (this.options.onSuccess) {this.options.onSuccess.call(this, response.data);}
            if (this.options.redirectOnSuccess) {window.location=this.options.redirectOnSuccess;}
        } else if (response.status == "failure") {
            if (this.options.onFailure) {this.options.onFailure.call();}
        } else if (response.status == "errors") {
            if (this.options.onError) {this.options.onError.call();}
        }
    }
}/**
 * Functions for manipulating cookies.
 *
 * Author: Jason Ho
 */

var Cookie = {
	set: function(name, value, days) {
        if (days) {
            var date = new Date();
            date.setTime(date.getTime()+(days*24*60*60*1000));
            var expires = "; expires="+date.toGMTString();
        } else {
            var expires = "";
        }
        document.cookie = name+"="+value+expires+"; path=/";
    },

	get: function(name) {
		var startIndex = document.cookie.indexOf(name+"=");
		if (startIndex == -1) return null;
		var endIndex = document.cookie.indexOf(";", startIndex);
		if (endIndex == -1) endIndex=document.cookie.length;
		var valueIndex = startIndex + (name+"=").length;
		return document.cookie.substring(valueIndex, endIndex);
	},

	unset: function(name) {
        document.cookie = name + "=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/";
	}
}
// Copyright (c) 2005 Thomas Fakes (http://craz8.com)
// 
// This code is substantially based on code from script.aculo.us which has the 
// following copyright and permission notice
//
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

var Resizeable = Class.create();
Resizeable.prototype = {
  initialize: function(element) {
    var options = Object.extend({
      top: 6,
      bottom: 6,
      left: 6,
      right: 6,
      snap: [1,1],
      bound: null,
      minHeight: 0,
      minWidth: 0,
      zindex: 1000,
      onStart: null,
      onEnd: null,
      onDrag: null
    }, arguments[1] || {});

    this.element      = $(element);
    this.handle 	  = this.element;

    Element.makePositioned(this.element); // fix IE    

    this.options      = options;

    this.active       = false;
    this.resizing     = false;   
    this.currentDirection = '';

    this.eventMouseDown = this.startResize.bindAsEventListener(this);
    this.eventMouseUp   = this.endResize.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);
    this.eventCursorCheck = this.cursor.bindAsEventListener(this);
    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
    
    this.registerEvents();
  },
  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    this.unregisterEvents();
  },
  registerEvents: function() {
    Event.observe(document, "mouseup", this.eventMouseUp);
    Event.observe(document, "mousemove", this.eventMouseMove);
    Event.observe(document, "keypress", this.eventKeypress);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
    Event.observe(this.element, "mousemove", this.eventCursorCheck);
  },
  unregisterEvents: function() {
    //if(!this.active) return;
    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
    //Event.stopObserving(document, "mousemove", this.eventCursorCheck);
    //Event.stopObserving(document, "keypress", this.eventKeypress);
  },
  startResize: function(event) {
    if(Event.isLeftClick(event)) {
      
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if(src.tagName && (
        src.tagName=='INPUT' ||
        src.tagName=='SELECT' ||
        src.tagName=='BUTTON' ||
        src.tagName=='TEXTAREA')) return;

	  var dir = this.directions(event);
	  if (dir.length > 0) {      
	      this.active = true;
    	  var offsets = Position.cumulativeOffset(this.element);
	      this.startTop = offsets[1];
	      this.startLeft = offsets[0];
	      this.startWidth = parseInt(Element.getStyle(this.element, 'width'));
	      this.startHeight = parseInt(Element.getStyle(this.element, 'height'));
	      this.startX = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
	      this.startY = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
	      
	      this.currentDirection = dir;
	      Event.stop(event);

          if (this.options.onStart) this.options.onStart(this.element);
      }
    }
  },
  finishResize: function(event, success) {
    // this.unregisterEvents();
    this.active = false;
    this.resizing = false;

    if(this.options.zindex) {
      this.element.style.zIndex = this.originalZ;
    }
  },
  keyPress: function(event) {
    if(this.active) {
      if(event.keyCode==Event.KEY_ESC) {
        this.finishResize(event, false);
        Event.stop(event);
      }
    }
  },
  endResize: function(event) {
    if (this.active && this.options.onEnd) {
        this.options.onEnd(this.element);
    }
    if(this.active && this.resizing) {
      this.finishResize(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.resizing = false;
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];

    if(this.options.bound && (typeof this.options.bound == 'function')) {
        pointer = this.options.bound(pointer[0],pointer[1]);
    }

    var style = this.element.style;
    if (this.currentDirection.indexOf('n') != -1) {
    	var pointerMoved = this.startY - pointer[1];
        pointerMoved = Math.round(pointerMoved/this.options.snap[1])*this.options.snap[1];
        var margin = Element.getStyle(this.element, 'margin-top') || "0";
    	var newHeight = this.startHeight + pointerMoved;
    	if (newHeight > this.options.minHeight) {
    		style.height = newHeight + "px";
    		style.top = (this.startTop - pointerMoved - parseInt(margin)) + "px";
    	}
    }
    if (this.currentDirection.indexOf('w') != -1) {
    	var pointerMoved = this.startX - pointer[0];
        pointerMoved = Math.round(pointerMoved/this.options.snap[0])*this.options.snap[0];
        var margin = Element.getStyle(this.element, 'margin-left') || "0";
    	var newWidth = this.startWidth + pointerMoved;
    	if (newWidth > this.options.minWidth) {
    		style.left = (this.startLeft - pointerMoved - parseInt(margin))  + "px";
    		style.width = newWidth + "px";
    	}
    }
    if (this.currentDirection.indexOf('s') != -1) {
        var pointerMoved = pointer[1] - this.startY;
        pointerMoved = Math.round(pointerMoved/this.options.snap[1])*this.options.snap[1];
        var newHeight = this.startHeight + pointerMoved;
    	if (newHeight > this.options.minHeight) {
    		style.height = newHeight + "px";
    	}
    }
    if (this.currentDirection.indexOf('e') != -1) {
        var pointerMoved = pointer[0] - this.startX;
        pointerMoved = Math.round(pointerMoved/this.options.snap[0])*this.options.snap[0];
        var newWidth = this.startWidth + pointerMoved;
    	if (newWidth > this.options.minWidth) {
    		style.width = newWidth + "px";
    	}
    }
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },
  between: function(val, low, high) {
  	return (val >= low && val < high);
  },
  directions: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.element);
    
	var cursor = '';
	if (this.between(pointer[1] - offsets[1], 0, this.options.top)) cursor += 'n';
	if (this.between((offsets[1] + this.element.offsetHeight) - pointer[1], 0, this.options.bottom)) cursor += 's';
	if (this.between(pointer[0] - offsets[0], 0, this.options.left)) cursor += 'w';
	if (this.between((offsets[0] + this.element.offsetWidth) - pointer[0], 0, this.options.right)) cursor += 'e';

	return cursor;
  },
  cursor: function(event) {
  	var cursor = this.directions(event);
	if (cursor.length > 0) {
		cursor += '-resize';
	} else {
		cursor = '';
	}
	this.element.style.cursor = cursor;		
  },
  update: function(event) {
   if(this.active) {
      if(!this.resizing) {
        var style = this.element.style;
        this.resizing = true;
        
        if(Element.getStyle(this.element,'position')=='') 
          style.position = "relative";
        
        if(this.options.zindex) {
          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
          style.zIndex = this.options.zindex;
        }
      }
      this.draw(event);

      if(this.options.onDrag) {
        this.options.onDrag(this.element);
      }

      // fix AppleWebKit rendering
      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
      Event.stop(event);
      return false;
   }
  }
}
var BrowserDetect = {
	init: function () {
		this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
		this.version = this.searchVersion(navigator.userAgent)
			|| this.searchVersion(navigator.appVersion)
			|| "an unknown version";
		this.OS = this.searchString(this.dataOS) || "an unknown OS";
	},
	searchString: function (data) {
		for (var i=0;i<data.length;i++)	{
			var dataString = data[i].string;
			var dataProp = data[i].prop;
			this.versionSearchString = data[i].versionSearch || data[i].identity;
			if (dataString) {
				if (dataString.indexOf(data[i].subString) != -1)
					return data[i].identity;
			}
			else if (dataProp)
				return data[i].identity;
		}
	},
	searchVersion: function (dataString) {
		var index = dataString.indexOf(this.versionSearchString);
		if (index == -1) return;
		return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
	},
	dataBrowser: [
		{ 	string: navigator.userAgent,
			subString: "OmniWeb",
			versionSearch: "OmniWeb/",
			identity: "OmniWeb"
		},
		{
			string: navigator.vendor,
			subString: "Apple",
			identity: "Safari"
		},
		{
			prop: window.opera,
			identity: "Opera"
		},
		{
			string: navigator.vendor,
			subString: "iCab",
			identity: "iCab"
		},
		{
			string: navigator.vendor,
			subString: "KDE",
			identity: "Konqueror"
		},
		{
			string: navigator.userAgent,
			subString: "Firefox",
			identity: "Firefox"
		},
		{
			string: navigator.vendor,
			subString: "Camino",
			identity: "Camino"
		},
		{		// for newer Netscapes (6+)
			string: navigator.userAgent,
			subString: "Netscape",
			identity: "Netscape"
		},
		{
			string: navigator.userAgent,
			subString: "MSIE",
			identity: "Explorer",
			versionSearch: "MSIE"
		},
		{
			string: navigator.userAgent,
			subString: "Gecko",
			identity: "Mozilla",
			versionSearch: "rv"
		},
		{ 		// for older Netscapes (4-)
			string: navigator.userAgent,
			subString: "Mozilla",
			identity: "Netscape",
			versionSearch: "Mozilla"
		}
	],
	dataOS : [
		{
			string: navigator.platform,
			subString: "Win",
			identity: "Windows"
		},
		{
			string: navigator.platform,
			subString: "Mac",
			identity: "Mac"
		},
		{
			string: navigator.platform,
			subString: "Linux",
			identity: "Linux"
		}
	]

};
BrowserDetect.init();/**
 * This class contains any function/information that is global to the site.
 * It also keeps Global.userID if the user is logged in,
 * which is set as PHP's Session::instance()->getUserID().
 *
 * Author: Jason Ho
 */

var Global = {
    userID: null,
    main_page: null,
    sub_page: null,
    _loading_timeout: null,
        
    init: function() {
        if (screen.width >= 1280) {
            Cookie.set("screen_width", "1280", 10);
        } else {
            Cookie.unset("screen_width");
        }
        Global._callPageInit();
    },

    //Checks if this specific page has an init function, then calls it.
    _callPageInit: function() {
        if (eval("typeof Page."+Global.main_page+"."+Global.sub_page+"=='object'")) {
            var initFunction = "Page."+Global.main_page+"."+Global.sub_page+".init";
            if (eval("typeof "+initFunction+"=='function'")) {
                eval(initFunction+"()");
            }
        }
    },

    refresh_content: function(options) {
        options = Object.extend({
            scroll: false
        }, options || {});
        
        var url = window.location.toString();
        var param_index = url.indexOf("&");
        var params = param_index == -1 ? "" : url.substr(param_index);

        new Ajax.Request("php/pages/"+Global.main_page+"_"+Global.sub_page+".php?action=echo_content"+params, {
            onSuccess: function(request) {
                $('main_content').innerHTML = request.responseText;
                Global._callPageInit();
                if (options.scroll) scroll(0,0);
            }
        });
    },

    loadPage: function(pageString, options) {
        options = Object.extend({
            showLoading: true,
            selectTab: true,
            scroll: true
        }, options || {});

        if (!pageString) {
            //pageString = "timesheet:daily_view";
        }
        var pageTuple = pageString.split(":");
        var page = pageTuple[0];
        var subpage = pageTuple[1];
        if (subpage.indexOf("?") != -1) { //Parse parameters
            var subpage_tuple = subpage.split("?");
            subpage = subpage_tuple[0];
            var params = subpage_tuple[1];
        }

        var location = "/?p="+page+":"+subpage;
        if (params) location += "&"+params;
        window.location = location;
    },

    initChat: function() {
        Event.observe(window, "focus", function(){Global.hasFocus=true;}, false);
        Event.observe(window, "blur", function(){Global.hasFocus=false;}, false);
        CometUpdater.setHandler("chatChannel", ChatManager.loadMessages);
        CometUpdater.setHandler("popupChannel", PopupBox.load);
        CometUpdater.sendRequest("login", {
            callback: "Global.loginSuccessful",
            parameters: "userID="+Global.userID+"&userName="+Global.userName
        });
    },

    loginSuccessful: function() {
		ChatManager.loadChatWindows();
		CometUpdater.startPoll();
	},

    //Shows a loading message after a certain amount of milliseconds.
    showLoading: function(delay) {
        var command = "Global.showStatusMessage(\"Still loading...\",{backgroundColor: \"#c3c3c3\",fadeOut: false,opacity: 0.8});";
        Global._loading_timeout = setTimeout(command, delay);
    },

    hideLoading: function() {
        clearTimeout(Global._loading_timeout);
        Global.hideStatusMessage();
    },

    showSystemMessage: function(message) {
        Global.showStatusMessage(message, {
            icon: "images/icons/accept.png",
            backgroundColor: "#fff936",
            fadeOut: true
        });
    },

    showStatusMessage: function(message, options) { //Message can be plain text or HTML
        options = Object.extend({
            backgroundColor: "#fff936",
            opacity: 0.8,
            color: "black",
            icon: "",
            fadeOut: true
        }, options || {});

        if ($('clockspot_system_message')) Element.remove('clockspot_system_message');
        var div = document.createElement("div");
        Element.hide(div);
        div.id = "clockspot_system_message";
        if (options.icon) {
            div.innerHTML = "<img src='"+options.icon+"' class='icon'/> "+message;
        } else {
            div.innerHTML = message;
        }
        div.style.backgroundColor = options.backgroundColor;
        div.style.color = options.color;
        Element.setOpacity(div, options.opacity);
        document.body.appendChild(div);

        //Bottom-align the status message
        var pagePos = Position.realOffset(document.body);
        var dim = Element.getDimensions(div);
        div.style.top = (pagePos[1]+document.documentElement.clientHeight-dim.height)+"px";

        //Update position if user scrolls
        window.onscroll = function() {
            pagePos = Position.realOffset(document.body);
            div.style.top = (pagePos[1]+document.documentElement.clientHeight-dim.height)+"px";
        };

        //Display the element
        Element.show(div);

        //Fade out after a while
        if (options.fadeOut) {
            setTimeout("new Effect.Fade('clockspot_system_message', {duration: 1.5})", 2000);
        }
    },

    hideStatusMessage: function() {
        Element.hide("clockspot_system_message");
    },

    logout: function() {
        Cookie.set('openChats', '');
        Cookie.set('openChatTitles', '');

        if (Global.chatEnabled) {
            CometUpdater.sendRequest("logout", {
                parameters: "userID="+Global.userID
            });
        }

        Cookie.unset('session_userID');
        Cookie.unset('session_businessID');
        Cookie.unset('session_password');
        new Ajax.Request('php/actions/logout.php', {
            onSuccess: function() {
                window.location = "/";
            }
        });
    }
}
var Util = {
    fadeToggle: function(node, duration) {
        node = $(node);
        duration = duration ? duration: 1.0;
        if (Element.visible(node)) {
            new Effect.Fade(node, {duration: duration});
        } else {
            new Effect.Appear(node, {duration: duration});
        }
    },

    format_time: function(hour, minute) {
        var ampm = hour >= 12 ? "pm" : "am";
        if (hour==24) ampm = "am";
        if (minute < 10) minute = "0"+minute;
        if (hour > 12) hour -= 12;
        if (hour==0) hour = 12;
        return hour+":"+minute+ampm;
    },

    //Parses mysql datetime string and returns javascript Date object
    //Input has to be in this format: 2007-06-05 15:26:02
    mysql_timestamp_to_js_date: function(timestamp) {        
        var regex=/^([0-9]{2,4})-([0-1][0-9])-([0-3][0-9]) (?:([0-2][0-9]):([0-5][0-9]):([0-5][0-9]))?$/;
        var parts=timestamp.replace(regex,"$1 $2 $3 $4 $5 $6").split(' ');
        return new Date(parts[0],parts[1]-1,parts[2],parts[3],parts[4],parts[5]);
    },
    
    to_javascript_date: function(sqlDate) {
        var parts = sqlDate.split("-");
        var year = parseInt(parts[0], 10);    //The 2nd param tells parseInt to use base 10
        var month = parseInt(parts[1], 10)-1; //We do this because "08" and "09" are treated as Octal
        var date = parseInt(parts[2], 10);
        var d = new Date();
        d.setFullYear(year, month, date);
        return d;
    },

    //Takes in a javascript date object
    to_sql_date: function(date) {
        var year = date.getFullYear();
        var month = date.getMonth()+1;
        var day = date.getDate();
        if (month < 10) month = "0"+month;
        if (day < 10) day = "0"+day;
        return year+"-"+month+"-"+day;
    },

    yui_to_sql_date: function(yui_date) {
        var year  = yui_date[0][0][0];
        var month = yui_date[0][0][1];
        var day   = yui_date[0][0][2];
        if (month < 10) month = "0"+month;
        if (day < 10) day = "0"+day;
        return year+"-"+month+"-"+day;
    },

    entry_show_delete: function(namespace, deleteFunction, options) {
        options = Object.extend({
            deleteCaption: "Confirm Delete"
        }, options || {});
        var hideNode = namespace+"_options";
        var highlightNode = namespace+"_row";

        Element.addClassName(highlightNode, "entry_delete_row");
        Element.hide(hideNode);
        var onmouseover = options.deleteCaption ? "onmouseover=\"Util.showHoverText(this, '"+options.deleteCaption+"')\"" : "";
        var html =
            "<div>"+
            "<input type='button' value='Delete' class='entry_delete_button' "+onmouseover+" onclick=\"this.disabled='disabled'; "+deleteFunction+"\"/> "+
            "<a class='normalLink' onclick=\"Element.show('"+hideNode+"'); Element.removeClassName('"+highlightNode+"', 'entry_delete_row'); Element.remove(this.parentNode);\">Cancel</a>"+
            "</div>";
        new Insertion.After(hideNode, html);
    },

    entry_show_edit: function(namespace, options) {
        var node_row = namespace+"_row";
        var node_options = namespace+"_options";
        var node_options_replacement = namespace+"_options_replacement";
        var node_main = namespace+"_main";
        var node_main_replacement = namespace+"_main_replacement";
        Element.addClassName(node_row, "entry_edit_row");

        Element.hide(node_options);
        var html =
            "<div id='"+namespace+"_options_replacement'>"+
            "<input id='"+namespace+"_save_button' type='button' value='Save' class='entry_save_button' onclick=\""+options.saveFunction+"\"/> "+
            "<a class='normalLink' onclick=\"Util.entry_hide_edit('"+namespace+"')\">Cancel</a>"+
            "</div>";
        new Insertion.After(node_options, html);

        Element.hide(node_main);
        var html = "<div id='"+namespace+"_main_replacement'></div>";
        new Insertion.After(node_main, html);

        //Load edit form
        eval(options.loadFunction);
    },

    entry_hide_edit: function(namespace) {
        var node_row = namespace+"_row";
        var node_options = namespace+"_options";
        var node_options_replacement = namespace+"_options_replacement";
        var node_main = namespace+"_main";
        var node_main_replacement = namespace+"_main_replacement";

        Element.remove(node_options_replacement);
        Element.show(node_options);
        Element.remove(node_main_replacement);
        Element.show(node_main);
        Element.removeClassName(node_row, 'entry_edit_row');
    },

    showHoverText: function(node, text, options) {
        options = Object.extend({
            position: "top",
            xoffset: 0,
            yoffset: 0
        }, options || {});

        //Create the text node
        if ($('hoverText')) Element.remove('hoverText');
        new Insertion.After(node,  "<div id='hoverText'></div>");
        var hoverTextNode = $('hoverText');

        //Calculate node position
        var nodeOffset = Position.positionedOffset(node);
        var nodeDim = Element.getDimensions(node);
        if (options.position == "top") {
            hoverTextNode.innerHTML =
                "<div id='hoverText_caption' class='hoverText_caption_top'>"+text+"</div>"+
                "<img id='hoverText_hand' src='images/hover_text_down.png' class='hoverText_hand_top'/>";
            var leftOffset = -10+options.xoffset;
            var topOffset = -4+options.yoffset;
            var hoverTextDim = Element.getDimensions(hoverTextNode);
            hoverTextNode.style.left = (nodeOffset[0]+leftOffset)+"px";
            hoverTextNode.style.top = (nodeOffset[1]-hoverTextDim.height+topOffset)+"px";
        } else if (options.position == "right") {
            hoverTextNode.innerHTML =
                "<div id='hoverText_caption' class='hoverText_caption_right'>"+text+"</div>"+
                "<img id='hoverText_hand' src='images/hover_text_left.png' class='hoverText_hand_right'/>";
            var leftOffset = 1+options.xoffset;
            var topOffset = -2+options.yoffset;
            var hoverTextDim = Element.getDimensions(hoverTextNode);
            hoverTextNode.style.left = (nodeOffset[0]+nodeDim.width+leftOffset)+"px";
            hoverTextNode.style.top = (nodeOffset[1]+topOffset)+"px";

            //Setting the width here fixes a 'float' CSS bug in IE
            var dim1 = Element.getDimensions('hoverText_caption');
            var dim2 = Element.getDimensions('hoverText_hand');
            hoverTextNode.style.width = (dim1.width+dim2.width)+"px";
        } else if (options.position == "left") {
            hoverTextNode.innerHTML =
                "<div id='hoverText_caption' class='hoverText_caption_left'>"+text+"</div>"+
                "<img id='hoverText_hand' src='images/hover_text_right.png' class='hoverText_hand_left'/>";
            var leftOffset = -14+options.xoffset;
            var topOffset = -4+options.yoffset;
            var hoverTextDim = Element.getDimensions(hoverTextNode);
            hoverTextNode.style.left = (nodeOffset[0]-hoverTextDim.width+leftOffset)+"px";
            hoverTextNode.style.top = (nodeOffset[1]+topOffset)+"px";

            //Setting the width here fixes a 'float' CSS bug in IE
            var dim1 = Element.getDimensions('hoverText_caption');
            var dim2 = Element.getDimensions('hoverText_hand');
            hoverTextNode.style.width = (dim1.width+dim2.width)+"px";
        }


        //Make the node slightly transparent
        Element.setOpacity(hoverTextNode, 0.9);

        //Disappear on mouse out
        Element.observe(node, 'mouseout', Util.hideHoverText, false);
    },

    hideHoverText: function() {
        if ($('hoverText')) Element.remove('hoverText');
    }
}/**
 * Initializes a YUI calendar so that you can select a date range
 * Author: Jason Ho
 */
var RangeSelector = Class.create();

RangeSelector.prototype = {
    calendar: null,
    startDate: null,
    endDate: null,

    initialize: function(yahooCalendar, options) {
        this.options = Object.extend({
            onSelect: null
        }, options || {});

        this.calendar = yahooCalendar;
        this.calendar.selectEvent.subscribe(this._select.bind(this));
        this.calendar.render();
    },

    show_selected: function(start, end) { //Takes in a JS date object
        var range = (start.getMonth()+1)+"/"+start.getDate()+"/"+start.getFullYear();
        if (end) {
            range += "-"+(end.getMonth()+1)+"/"+end.getDate()+"/"+end.getFullYear();
        }        
        this.calendar.cfg.setProperty("selected",range,false);
        this.calendar.setMonth(start.getMonth());
        this.calendar.setYear(start.getFullYear());
        this.calendar.render();
    },

    reset: function() {
        this.startDate = "";
        this.endDate = "";
    },

    _select: function(eventType, theDate) {
        if (this.startDate && this.endDate) {
            this.reset();
        }

        if (!this.startDate) {
            this.startDate = theDate;
        } else {
            this.endDate = theDate;
            var startArray = this.startDate[0][0];
            var d = theDate[0][0];

            //Check if start date > end date. If so, reverse them.
            var dateStart = new Date();
            dateStart.setFullYear(startArray[0], startArray[1], startArray[2]);
            var dateEnd = new Date();
            dateEnd.setFullYear(d[0], d[1], d[2]);
            if (dateEnd < dateStart) {
                var temp = d;
                d = startArray;
                startArray = temp;
                this.endDate = this.startDate;
                this.startDate = theDate;
            }

            var dateRange = startArray[1]+"/"+startArray[2]+"/"+startArray[0]+"-"+d[1]+"/"+d[2]+"/"+d[0];
            this.calendar.cfg.setProperty("selected",dateRange,false);
            this.calendar.render();

            if (this.options.onSelect) {
                this.options.onSelect(this.startDate, this.endDate);
            }
        }
    }
}/**
 * Employee scheduling interface
 * Author: Jason Ho
 */

var Schedule = {
    options: null,
    container: null,
    userID: null,
    columns: null, //Stores the column names - dates are in mysql format.
    opacityNormal: 0.92, //Opacity of a block
    opacityEditing: 0.6, //Opacity of a block when being edited
    _updateOptionsWindow: null, //Reference to PopupWindow that displays when you try to modify a block that repeats
    _deleteOptionsWindow: null, //Reference to PopupWindow that displays when you try to delete a block that repeats
    _repeatDateCalendar: null, //Reference to the PopupWindow that displays when you select a repeat date for a block.
    _blockEditWindow: null, //Reference to PopupWindow used when you click on a block
    _gridStartLeft: null,
    _gridStartTop: null,
    _gridEndLeft: null,
    _gridEndTop: null,
    _gridWidth: null,
    _gridHeight: null,
    _gridInterval: 30, //Indicates how many minutes 1 grid block represents
    _moveInterval: 5,
    _resizeInterval: 5,
    _beforeEdit: null, //Stores properties of a block before it's edited. Used for undos.
    _newBlock: null, //References the new node created when you drag on the schedule
    _newBlockX: null, //Stores the original X position of a new block
    _newBlockY: null, //Stores the original Y position of a new block
    _isCreating: 0, //True when in the middle of creating a new block
    _lockBlockClick: false, //Prevents a click from triggering right after a drag or move
    
    init: function(container, userID, columns, options) {
        Schedule.options = Object.extend({
            mode: "admin", //admin mode grants all rights, user mode restricts editing blocks.
            overlayClockTimes: false
        }, options || {});
        
        Schedule.container = $(container);
        Schedule.userID = userID;
        Schedule.columns = columns;

        //Make the schedule unselectable
        Schedule.container.style.MozUserSelect="none";
        Schedule.container.style.KhtmlUserSelect="none";
        Schedule.container.unselectable="on";

        //Get start position of the grid
        var grids = document.getElementsByClassName("grid", Schedule.container);
        var pos = Position.cumulativeOffset(grids[0]);
        Schedule._gridStartLeft = pos[0];
        Schedule._gridStartTop = pos[1];

        //Get end position of the grid
        pos = Position.cumulativeOffset(grids.last());
        var dim = Element.getDimensions(grids.last());
        Schedule._gridEndLeft = pos[0]+dim.width;
        Schedule._gridEndTop = pos[1]+dim.height;

        //Get grid dimensions
        dim = Element.getDimensions(grids[0]);
        Schedule._gridWidth = dim.width;
        Schedule._gridHeight = dim.height;

        //Initialize block caption
        Element.setOpacity("block_caption", 0.8);

        //Listen to mouse events
        if (Schedule.options.mode != "user") {
            Event.observe(Schedule.container, "mousedown", Schedule._mouseDown.bind());
            Event.observe(Schedule.container, "mousemove", Schedule._mouseMove.bind());
            Event.observe(Schedule.container, "mouseup", Schedule._mouseUp.bind());
        }
        
        //Initialize the block edit window
        Schedule._blockEditWindow = new PopupWindow("", {
            elementID: "block_edit_window",
            container: "schedule",
            hideOnEsc: true,
            hideOnBlur: true,
            hideOnBlurContainer: "schedule"
        });

        //Load existing blocks
        Schedule._loadBlocks();
    },

    changeDate: function(sqlDate) {
        var d = Util.to_javascript_date(sqlDate);
        
        //Update columns
        var labels = document.getElementsByClassName("column_label", Schedule.container);
        var day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        var month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        for (var i = 0; i < 7; i++) {
            Schedule.columns[i] = Util.to_sql_date(d);            
            labels[i].innerHTML = "<b>"+day_names[d.getDay()]+"</b>, "+month_names[d.getMonth()]+" "+d.getDate();
            d.setDate(d.getDate()+1);
        }

        //Reload the blocks
        Schedule._loadBlocks();
    },
    
    changeUserID: function(userID) {
        Schedule.userID=userID;
        Schedule._loadBlocks();
    },

    toggleOverlayClockTimes: function() {
        Schedule.options.overlayClockTimes = Schedule.options.overlayClockTimes ? false : true;
        Schedule._loadBlocks();
    },

    _loadBlocks: function() {
        //Dim and lock the schedule
        Element.setOpacity(Schedule.container, 0.5);
        Schedule.lock();
        Global.showLoading(3000);
        var blocks = document.getElementsByClassName("block", Schedule.container);
        var clockTimes = document.getElementsByClassName("clockTime", Schedule.container);

        //For some reason IE doesn't dim blocks when dimming the schedule.
        if (BrowserDetect.browser == "Explorer") {
            blocks.each(function(node) { Element.setOpacity(node, 0.5); });
            clockTimes.each(function(node) { Element.setOpacity(node, 0.5); });
        }

        new Ajax.Request("php/actions/schedule_load.php", {
            parameters: "userID="+Schedule.userID+"&dates="+Schedule.columns.join(",")+"&overlayClockTimes="+Schedule.options.overlayClockTimes,
            onSuccess: function(request) {
                var data = eval("("+request.responseText+")");                

                //Clear existing blocks
                blocks.each(Element.remove);

                //Clear existing clockTimes
                clockTimes.each(Element.remove);

                //Load each block
                data.schedules.each(function(schedule) {
                    var scheduleID = schedule[0];
                    var startTime = Util.mysql_timestamp_to_js_date(schedule[1]);
                    var endTime = Util.mysql_timestamp_to_js_date(schedule[2]);
                    var repeatType = schedule[3];
                    var repeatUntil = schedule[4];
                    
                    //Calculate block top
                    var startMinutes = startTime.getHours()*60+startTime.getMinutes();
                    var block_top = Schedule._gridStartTop + Math.round(startMinutes/Schedule._gridInterval*Schedule._gridHeight);

                    //Calculate block left
                    var column_number = Schedule.columns.indexOf(Util.to_sql_date(startTime));
                    var block_left = Schedule._gridStartLeft + column_number*Schedule._gridWidth;

                    //Fix Internet Explorer - the left border throws off the style.left off by 1 pixel.
                    if (BrowserDetect.browser == "Explorer" && (BrowserDetect.version == "6" || BrowserDetect.version=="7")) {
                        block_left++;
                    }

                    //Calculate block height
                    var endMinutes = endTime.getHours()*60+endTime.getMinutes();
                    if (startTime.getDate() != endTime.getDate()) { //If endTime is midnight of the next day
                        endMinutes = 24*60;
                    }
                    var block_height = Math.round((endMinutes-startMinutes)/Schedule._gridInterval*Schedule._gridHeight);

                    
                    //Create the block
                    var block = document.createElement("div");
                    Element.addClassName(block, "block");
                    if (repeatType) Element.addClassName(block, "repeat");
                    block.id = "schedule"+scheduleID;
                    block.style.height = (block_height-2)+"px"; //We subtract 2 due to borders
                    block.style.width = (Schedule._gridWidth-2)+"px"; //We subtract 2 due to borders
                    block.style.top = block_top+"px";
                    block.style.left = block_left+"px";
                    Schedule.container.appendChild(block);

                    //Set repeatUntil property
                    block.innerHTML = "<span style='display:none;' id='repeatUntil"+scheduleID+"'>"+repeatUntil+"</span>";

                    //Initialize the block
                    if (Schedule.options.mode != "user") {
                        Schedule._initBlock($(block.id));
                    } else {
                        block.style.cursor = "default";
                    }
                });

                //Load clock times
                if (data.clockTimes) {
                    data.clockTimes.each(function(clockTime) {
                        var clockIn = Util.mysql_timestamp_to_js_date(clockTime[0]);
                        var clockOut = Util.mysql_timestamp_to_js_date(clockTime[1]);

                        //Calculate block top
                        var startMinutes = clockIn.getHours()*60+clockIn.getMinutes();
                        var block_top = Schedule._gridStartTop + Math.round(startMinutes/Schedule._gridInterval*Schedule._gridHeight);

                        //Calculate block left
                        var column_number = Schedule.columns.indexOf(Util.to_sql_date(clockIn));
                        var block_left = Schedule._gridStartLeft + column_number*Schedule._gridWidth;

                        //Fix Internet Explorer - the left border throws off the style.left off by 1 pixel.
                        if (BrowserDetect.browser == "Explorer" && (BrowserDetect.version == "6" || BrowserDetect.version=="7")) {
                            block_left++;
                        }

                        //Calculate block height
                        var endMinutes = clockOut.getHours()*60+clockOut.getMinutes();
                        if (clockIn.getDate() != clockOut.getDate()) { //If clockOut is midnight of the next day
                            endMinutes = 24*60;
                        }
                        var block_height = Math.round((endMinutes-startMinutes)/Schedule._gridInterval*Schedule._gridHeight);

                        //Create the block
                        var block = document.createElement("div");
                        Element.addClassName(block, "clockTime");
                        block.style.height = (block_height-2)+"px"; //We subtract 2 due to borders
                        block.style.width = (Schedule._gridWidth-2)+"px"; //We subtract 2 due to borders
                        block.style.top = block_top+"px";
                        block.style.left = block_left+"px";
                        Element.setOpacity(block, 0.5);
                        Schedule.container.appendChild(block);
                    });
                }

                //Restore and unlock the schedule
                Element.setOpacity(Schedule.container, 1.0);
                Schedule.unlock();
                Global.hideLoading();
            }
        });
    },

    _mouseDown: function(e) {
        if (!Element.hasClassName(Event.element(e), "grid")) return; //Only allow clicks on the grid
        if (Schedule._isCreating) return; //Exit if we're already creating
        if (Schedule._newBlock != null) return; //Exit if _newBlock is being used

        //Update isCreating status
        Schedule._isCreating = 1;

        //Create a new block
        Schedule._newBlock = document.createElement("div");
        Element.addClassName(Schedule._newBlock, "block");
        Schedule._newBlock.style.height = (Schedule._gridHeight-2)+"px"; //We subtract 2 due to borders
        Schedule._newBlock.style.width = (Schedule._gridWidth-2)+"px"; //We subtract 2 due to borders
        Element.setOpacity(Schedule._newBlock, Schedule.opacityEditing);

        //Set x,y coordinates
        var mouseX = Event.pointerX(e);
        var mouseY = Event.pointerY(e);
        Schedule._newBlockY = mouseY-((mouseY-Schedule._gridStartTop)%Schedule._gridHeight);
        Schedule._newBlockX = mouseX-((mouseX-Schedule._gridStartLeft)%Schedule._gridWidth);

        //Fix Internet Explorer - the left border throws off the style.left off by 1 pixel.
        if (BrowserDetect.browser == "Explorer" && (BrowserDetect.version == "6" || BrowserDetect.version=="7")) {
            Schedule._newBlockX++;
        }

        Schedule._newBlock.style.top = Schedule._newBlockY+"px";
        Schedule._newBlock.style.left = Schedule._newBlockX+"px";

        //Add block to the schedule
        Schedule.container.appendChild(Schedule._newBlock);

        //Hide the edit window if it's open
        Schedule._hideBlockEditWindow();
    },

    _mouseMove: function(e) {
        if (!Schedule._isCreating) return; //Exit if we're not creating
        
        //Restrict clicking outside the grid
        var mouseY = Event.pointerY(e);
        if (mouseY <= Schedule._gridStartTop || mouseY >= Schedule._gridEndTop) {
            return;
        }

        //Calculate mouse offset
        var offsetY = mouseY-Schedule._newBlockY;
        var height;
        if (offsetY >= 0) {
            //Update y position of the block
            Schedule._newBlock.style.top = Schedule._newBlockY+"px";

            //Update height of block
            height = Math.ceil(offsetY/Schedule._gridHeight)*Schedule._gridHeight-2; //We subtract 2 because of the block borders
            Schedule._newBlock.style.height = height+"px";
        } else {
            //Update y position of the block
            var newY = mouseY-((mouseY-Schedule._gridStartTop)%Schedule._gridHeight);
            Schedule._newBlock.style.top = newY+"px";

            //Update height of block
            height = Schedule._newBlockY+Schedule._gridHeight-newY-2;
            Schedule._newBlock.style.height = height+"px";
        }
        
        //Update block caption
        Schedule._showBlockCaption(Schedule._newBlock);
    },

    _mouseUp: function(e) {
        if (!Schedule._isCreating) return; //Exit if we're not creating
        
        var d = Schedule._getBlockDetails(Schedule._newBlock);
        new Ajax.Request("php/actions/schedule_add.php", {
            parameters: "userID="+d.userID+"&startTime="+d.start_mysql+"&endTime="+d.end_mysql,
            onSuccess: function(request) {
                if (request.responseText == "") {
                    alert("ERROR: Invalid Date");
                    return;
                }
                //Set the new block's id
                var scheduleID = request.responseText;
                Schedule._newBlock.id = "schedule"+scheduleID;

                //Initialize the block
                Schedule._initBlock($(Schedule._newBlock.id));

                //Reset newBlock
                Schedule._newBlock = null;
            }
        });

        //Hide block caption
        Schedule._hideBlockCaption();
        
        //Update isCreating status
        Schedule._isCreating = 0;
    },

    _initBlock: function(block_node) {
        //Make block resizeable
        new Resizeable(block_node, {
            left: 0,
            right: 0,
            snap: [1,Schedule._gridHeight*(Schedule._resizeInterval/Schedule._gridInterval)],
            bound: function(x,y) {
                if (y > Schedule._gridEndTop) y = Schedule._gridEndTop;
                if (y < Schedule._gridStartTop) y = Schedule._gridStartTop;
                return [x,y];
            },
            onStart: function(node) {
                Draggables.lockDrag = true; //Prevent's dragging the block while resizing.
                Schedule._hideBlockEditWindow();
                Element.setOpacity(node, Schedule.opacityEditing);
                //Save the block so we can restore it later
                Schedule._saveLast(node);
            },
            onEnd: function(node) {
                Schedule._lockBlockClick = true; //Prevents a click from triggering right after the resize. Unset by the click event handler.
                Schedule._hideBlockCaption();
                Draggables.lockDrag = false; //Prevent's dragging the block while resizing.
                var d = Schedule._getBlockDetails(node);
                if (d.repeatType) {
                    Schedule._showUpdateOptions(node);
                } else {
                    Schedule._updateBlock(node);                    
                }
            },
            onDrag: function(node) {
                var pos = Position.cumulativeOffset(node);
                var dim = Element.getDimensions(node);

                //Set bounds for top of block
                if (pos[1] < Schedule._gridStartTop) {
                    node.style.top = (Schedule._gridStartTop) + "px";
                }
                //Set bounds for end of block
                if (pos[1]+dim.height > Schedule._gridEndTop) {
                    node.style.height = (Schedule._gridEndTop-pos[1]-2) + "px"; //We subtract 2 because of the block borders
                }

                Schedule._showBlockCaption(node);
            }
        });

        //Make block draggable
        new Draggable(block_node, {
            starteffect: null, //We manage this effect ourselves
            endeffect: null, //We manage this effect ourselves
            snap: function(x,y) {                
                x = Schedule._gridStartLeft+Math.round((x-Schedule._gridStartLeft)/Schedule._gridWidth)*Schedule._gridWidth;
                var move_pixels = Schedule._gridHeight*(Schedule._moveInterval/Schedule._gridInterval);
                y = Schedule._gridStartTop+Math.floor((y-Schedule._gridStartTop)/move_pixels)*move_pixels;

                //Set boundaries
                var dim = Element.getDimensions(block_node);
                var maxLeft = Schedule._gridEndLeft - dim.width;
                var maxTop = Schedule._gridEndTop - dim.height;

                if (x < Schedule._gridStartLeft) x = Schedule._gridStartLeft;
                if (x > maxLeft) x = maxLeft;
                if (y < Schedule._gridStartTop) y = Schedule._gridStartTop;
                if (y > maxTop) y = maxTop;

                //Fix Internet Explorer - the left border throws off the style.left off by 1 pixel.
                if (BrowserDetect.browser == "Explorer" && (BrowserDetect.version == "6" || BrowserDetect.version=="7")) {
                    x++;
                }

                return [x,y];
            },
            onStart: function(draggable, event) {
                Schedule._hideBlockEditWindow();
                Element.setOpacity(draggable.element, Schedule.opacityEditing);
                //Save the block so we can restore it later
                Schedule._saveLast(draggable.element);
            },
            onEnd: function(draggable, event) {                
                Schedule._lockBlockClick = true; //Prevents a click from triggering right after the move. Unset by the click event handler.
                Schedule._hideBlockCaption();
                var d = Schedule._getBlockDetails(draggable.element);
                if (d.repeatType) {
                    Schedule._showUpdateOptions(draggable.element);
                } else {
                    Schedule._updateBlock(draggable.element);                    
                }
            },
            onDrag: function(draggable, event) {
                Schedule._showBlockCaption(draggable.element);
            }
        });

        //Show block details on hover
        //Event.observe(block_node, "mouseover", function(e){Schedule._showBlockCaption(Event.element(e));});
        //Event.observe(block_node, "mouseout", function(){Schedule._hideBlockCaption();});

        //Show edit window when clicked
        Event.observe(block_node, "click", function(e) {
            if (Schedule._lockBlockClick) { //Prevents a click from triggering right after a move or resize.
                Schedule._lockBlockClick = false;
                return;
            }
            Schedule._showBlockEditWindow(Event.element(e));
        });

        Element.setOpacity(block_node, Schedule.opacityNormal);
    },

    //Makes it so you can't edit anything on the schedule
    lock: function() {
        //Intercept all mouse events and cancel them
        ["mousedown", "click", "mouseover", "mouseout"].each(function(eventType) {
            Event.observe("schedule", eventType, Event.stop, true);
        });
    },

    //Undo the Schedule.lock() function
    unlock: function() {
        //Cancel the mouse events we added during Schedule.lock()
        ["mousedown", "click", "mouseover", "mouseout"].each(function(eventType) {
            Event.stopObserving("schedule", eventType, Event.stop, true);
        });
        Schedule._lockBlockClick = false; //We reset this because _lockBlockCLick might not have been reset due to intercepting the mouse click event.
    },

    //Store the block's size and position before it's edited. Used to undo an edit.
    _saveLast: function(node) {        
        var pos = Position.cumulativeOffset(node);
        var dim = Element.getDimensions(node);
        Schedule._beforeEdit = {
            element: node,
            details: Schedule._getBlockDetails(node),
            top: pos[1],
            left: pos[0],
            height: dim.height
        };
    },

    //Undo an edit
    _restoreLast: function() {
        var node = Schedule._beforeEdit.element;
        node.style.left = Schedule._beforeEdit.left+"px";
        node.style.top = Schedule._beforeEdit.top+"px";
        node.style.height = (Schedule._beforeEdit.height-2)+"px"; //We subtract 2px due to borders
        Element.setOpacity(node, Schedule.opacityNormal);
    },

    //Pops up when you try to update a schedule that repeats
    _showUpdateOptions: function(node) {
        Schedule._hideBlockCaption();
        Schedule.lock();

        Schedule._updateOptionsWindow = new PopupWindow(
            "<div id='schedule_popup_window'>"+
            "<h3>You are changing a repeating schedule.</h3>"+
            "<p>Do you want to change just this block, or all future blocks in the series?</p>"+
            "<input type='button'  onclick=\"Schedule._submitUpdateOptions('"+node.id+"', 1)\" value='Change only this block'>"+
            "<input type='button'  onclick=\"Schedule._submitUpdateOptions('"+node.id+"', 2)\" value='Change all future blocks'>"+
            "<input type='button'  onclick=\"Schedule._submitUpdateOptions('"+node.id+"', 3)\" value='Cancel'>"+
            "</div>"
        );

        Schedule._updateOptionsWindow.showCentered();

        Element.setOpacity("schedule", 0.4);
    },

    //Handles your choice of how you want to update a repeating schedule
    _submitUpdateOptions: function(node, choice) {
        Schedule._updateOptionsWindow.hide();
        Element.setOpacity("schedule", 1.0);
        Schedule.unlock();

        if (choice==1) { //Change only this event
            Schedule._updateBlock(node, {changeAll: 0, pivotDate: Schedule._beforeEdit.details.start_mysql.split(" ")[0]});
        } else if (choice==2) { //Change all events
            Schedule._updateBlock(node, {changeAll: 1, pivotDate: Schedule._beforeEdit.details.start_mysql.split(" ")[0]});
        } else if (choice==3) { //Cancel
            Schedule._restoreLast(); //Undo the edit
        }
    },

    //Updates the database entry of the given schedule/block    
    _updateBlock: function(node, options) {
        options = Object.extend({
            changeAll: null, //For schedules that repeat - indicates whether to update all events in series.
            pivotDate: null //For schedules that repeat - the date/position where we're slicing the series.
        }, options || {});
        
        node = $(node);
        var d = Schedule._getBlockDetails(node);
        new Ajax.Request("php/actions/schedule_edit.php", {
            parameters: "scheduleID="+d.scheduleID+"&userID="+d.userID+"&startTime="+d.start_mysql+"&endTime="+d.end_mysql+"&changeAll="+options.changeAll+"&pivotDate="+options.pivotDate,
            onSuccess: function(request) {
                if (request.responseText == "") {
                    alert("ERROR: Invalid Date");
                    return;
                }
                var r = eval(request.responseText);
                node.id = "schedule"+r[0];
                if (Element.hasClassName(node, "repeat") && r[1]==0) {
                    Element.removeClassName(node, "repeat");
                }
            
                //Make the block solid
                Element.setOpacity(node, Schedule.opacityNormal);
            }
        });
    },

    _updateRepeat: function(scheduleID) {
        var node = $("schedule"+scheduleID);
        var pivotDate = Schedule._getBlockDetails(node).start_mysql.split(" ")[0];
        Element.setOpacity(node, Schedule.opacityEditing);

        //Get repeatUntil value
        var repeatUntil = "";
        if ($F("checkboxRepeatUntil")) {
            repeatUntil = $F("repeatUntilDate");
        }

        new Ajax.Request("php/actions/schedule_edit_repeat.php", {
            parameters: "scheduleID="+scheduleID+"&repeatUntil="+repeatUntil+"&pivotDate="+pivotDate,
            onSuccess: function(request) {
                var response = AjaxForm.handleResponse(request.responseText);
                if (response.status != "success") return;

                //Delete the block if it no longer exists
                if (!response.data) {
                    Element.remove(node);
                    Schedule._hideBlockEditWindow();
                    return;
                }

                //Parse return data
                var data = eval(response.data);
                var repeatType = data[0];
                var repeatUntil = data[1];

                //Update repeat status
                if (repeatType) {
                    Element.addClassName(node, "repeat");
                } else {
                    Element.removeClassName(node, "repeat");
                }

                //Update repeatUntil value
                node.innerHTML = "<span style='display:none;' id='repeatUntil"+scheduleID+"'>"+repeatUntil+"</span>";
                
                //Clean up
                Element.setOpacity(node, Schedule.opacityNormal);
                Schedule._hideBlockEditWindow();
            }
        });
    },

    _showBlockEditWindow: function(block_node) {
        var b = Schedule._getBlockDetails(block_node);

        //Create content
        if (b.repeatType) {
            var repeatUntil = $("repeatUntil"+b.scheduleID).innerHTML;
            var content =
                "<img class='window_pointer' src='images/schedule_edit_window_pointer.gif'/>"+
                "<div><input id='checkboxRepeatForever' name='repeatUntil' type='radio' "+(repeatUntil ? "" : "checked='checked'")+"/> Forever</div>"+
                "<div><input id='checkboxRepeatUntil' name='repeatUntil' type='radio' onclick=\"Schedule._chooseRepeatDate()\" "+(repeatUntil ? "checked='checked'" : "")+"/> Until <input type='textbox' value='"+repeatUntil+"' name='repeatUntilDate' id='repeatUntilDate' onclick=\"Schedule._chooseRepeatDate()\"/></div>"+
                "<div><input type='button' value='Save' onclick=\"Schedule._updateRepeat("+b.scheduleID+")\"/></div>"+
                "<br/><br/>"+
                "<div class='delete_button_container'><input type='button' value='Delete block' onclick=\"Schedule.deleteBlock("+b.scheduleID+")\"/></div>";
        } else {
            var content =
                "<img class='window_pointer' src='images/schedule_edit_window_pointer.gif'/>"+
                "<div><input type='checkbox' onclick=\"Element.toggle('repeatUntilOptions')\"/> Repeat weekly</div>"+
                    "<div id='repeatUntilOptions' style='display: none;'>"+
                    "<div><input id='checkboxRepeatForever' name='repeatUntil' type='radio' checked='checked'/> Forever</div>"+
                    "<div><input id='checkboxRepeatUntil' name='repeatUntil' type='radio' onclick=\"Schedule._chooseRepeatDate()\"/> Until <input type='textbox' name='repeatUntilDate' id='repeatUntilDate' onclick=\"Schedule._chooseRepeatDate()\"/></div>"+
                    "<div><input type='button' value='Save' onclick=\"Schedule._updateRepeat("+b.scheduleID+")\"/></div>"+
                    "</div>"+
                "<br/><br/>"+
                "<div class='delete_button_container'><input type='button' value='Delete block' onclick=\"Schedule.deleteBlock("+b.scheduleID+")\"/></div>";
        }

        Schedule._blockEditWindow.element.innerHTML = content;      
        Schedule._blockEditWindow.showBesideNode(block_node, {
            selectedClass: "selected",
            xoffset: 5,
            yoffset: 5
        });
    },

     _chooseRepeatDate: function() {
        $('repeatUntilDate').focus();
        $('checkboxRepeatUntil').checked = "checked";

        if (!Schedule._repeatDateCalendar) {
            Schedule._repeatDateCalendar = new PopupWindow("<div id='repeatDateCalendar'></div>", {hideOnBlur: true, hideOnEsc: true});
            Schedule._repeatDateCalendar.showBesideNode("repeatUntilDate", {valign: "bottom", halign: "left"});
            var c = new YAHOO.widget.Calendar("repeatDateCalendarID", "repeatDateCalendar", {navigator: true});
            c.selectEvent.subscribe(function(eventType, theDate) {
                $("repeatUntilDate").value = Util.yui_to_sql_date(theDate);
                Schedule._repeatDateCalendar.hide();
            });
            c.render();
        } else {
            Schedule._repeatDateCalendar.showBesideNode("repeatUntilDate", {valign: "bottom", halign: "left"});
        }
    },

    _hideBlockEditWindow: function() {
        Schedule._blockEditWindow.hide();
    },

    deleteBlock: function(scheduleID) {
        var block_node = $("schedule"+scheduleID);
        var b = Schedule._getBlockDetails(block_node);
        if (b.repeatType) {
            Schedule._showDeleteOptions(block_node);
        } else {
            new Ajax.Request("php/actions/schedule_delete.php", {
                parameters: "scheduleID="+scheduleID,
                onSuccess: function(request) {
                    new Effect.DropOut("schedule"+scheduleID);
                    Schedule._hideBlockEditWindow();
                }
            });
        }
    },

    deleteRepeatingBlock: function(node, deleteAll) {
        node = $(node);
        var d = Schedule._getBlockDetails(node);
        var pivotDate = d.start_mysql.split(" ")[0];

        new Ajax.Request("php/actions/schedule_delete.php", {
            parameters: "scheduleID="+d.scheduleID+"&deleteAll="+deleteAll+"&pivotDate="+pivotDate,
            onSuccess: function(request) {
                new Effect.DropOut(node);
                Schedule._hideBlockEditWindow();
            }
        });
    },

    //Pops up when you try to delete a schedule that repeats
    _showDeleteOptions: function(node) {
        Schedule.lock();

        Schedule._deleteOptionsWindow = new PopupWindow(
            "<div id='schedule_popup_window'>"+
            "<h3>You are deleting a repeating schedule.</h3>"+
            "<p>Do you want to delete just this block, or all future blocks in the series?</p>"+
            "<input type='button'  onclick=\"Schedule._submitDeleteOptions('"+node.id+"', 1)\" value='Delete only this block'>"+
            "<input type='button'  onclick=\"Schedule._submitDeleteOptions('"+node.id+"', 2)\" value='Delete all future blocks'>"+
            "<input type='button'  onclick=\"Schedule._submitDeleteOptions('"+node.id+"', 3)\" value='Cancel'>"+
            "</div>"
        );

        Schedule._deleteOptionsWindow.showCentered();

        Element.setOpacity("schedule", 0.4);
    },

    //Handles your choice of how you want to delete a repeating schedule
    _submitDeleteOptions: function(node, choice) {
        if (choice==1) { //Delete only this block
            Schedule.deleteRepeatingBlock(node, 0);
        } else if (choice==2) { //Delete all blocks in the series
            Schedule.deleteRepeatingBlock(node, 1);
        } else if (choice==3) { //Cancel

        }

        Schedule._deleteOptionsWindow.hide();
        Element.setOpacity("schedule", 1.0);
        Schedule.unlock();
    },

    _showBlockCaption: function(block_node) {
        var caption = $("block_caption");

        //Update caption x,y position
        var pos = Position.cumulativeOffset(block_node);
        var dim = Element.getDimensions(block_node);
        caption.style.left = pos[0]+"px";
        caption.style.top = (pos[1]+dim.height)+"px";

        //Update caption label
        var details = Schedule._getBlockDetails(block_node);
        caption.innerHTML = Util.format_time(details.start.hour, details.start.minute)+" - "+Util.format_time(details.end.hour, details.end.minute);

        //Show caption
        caption.style.display = "block";
    },

    _hideBlockCaption: function(node) {
        $("block_caption").style.display = "none";
    },

    _getBlockDetails: function(node) {
        var scheduleID = node.id.substr("schedule".length);
        var pos = Position.cumulativeOffset(node);
        var dim = Element.getDimensions(node);
        var repeatType = Element.hasClassName(node, "repeat") ? 1 : 0;

        //Fix Internet Explorer - the left border throws off the style.left off by 1 pixel.
        if (BrowserDetect.browser == "Explorer" && (BrowserDetect.version == "6" || BrowserDetect.version=="7")) {
            pos[0]--;
        }

        //Get which column the block is in
        var column = (pos[0]-Schedule._gridStartLeft)/Schedule._gridWidth;
        var column_name = Schedule.columns[column];

        
        //var move_pixels = Schedule._gridHeight*(Schedule._moveInterval/Schedule._gridInterval);

        //Get start time
        var start_minutes_from_top = Math.round(Schedule._gridInterval*(pos[1]-Schedule._gridStartTop)/Schedule._gridHeight);
        var start_minute = start_minutes_from_top%60;
        var start_hour = Math.floor(start_minutes_from_top/60);

        //Get end time
        var end_minutes_from_top = Math.round(Schedule._gridInterval*(pos[1]+dim.height-Schedule._gridStartTop)/Schedule._gridHeight);
        var end_minute = end_minutes_from_top%60;
        var end_hour = Math.floor(end_minutes_from_top/60);

        //Get start mysql
        var start_mysql = column_name+" "+start_hour+":"+start_minute+":00";

        //Get end mysql
        var end_mysql;
        if (end_hour==24) {
            var d = Util.to_javascript_date(column_name);
            d.setDate(d.getDate()+1);
            end_mysql = Util.to_sql_date(d)+" 00:"+end_minute+":00";
        } else {
            end_mysql = column_name+" "+end_hour+":"+end_minute+":00";
        }
        
        return {
            scheduleID: scheduleID,
            userID: Schedule.userID,
            start_mysql: start_mysql,
            end_mysql: end_mysql,
            start: {
                hour: start_hour,
                minute: start_minute
            },
            end: {
                hour: end_hour,
                minute: end_minute
            },
            repeatType: repeatType
        };
    }
}/**
 * Houses the javascript for each page in the app
 * Author: Jason Ho
 */

var Page = {
    timesheet: {},
    reports: {},
    payroll: {},
    users: {},
    timers: {},
    tasks: {},
    messages: {},
    files: {},
    projects: {},
    settings: {},
    timeclock: {},
    support: {}
}

Page.timesheet = {
    _editWindow: null,

    approve_clock_time: function(clockTimeID) {
        new Ajax.Request("php/actions/clock_time_approve.php", {
			parameters: "clockTimeID="+clockTimeID,
            onSuccess: function(request){
                $("approveTime"+clockTimeID).src = "images/icons/"+request.responseText;
            }
        });
	},

    hide_edit_clock_time: function() {
        Page.timesheet._editWindow.hide();
    },

    show_edit_clock_time: function(clockTimeID) {
        var content =
            "<div id='window_shadow_container'>"+
            "<div id='window_container'>"+
                "<div id='window_title_bar'>"+
                    "<div id='window_title_text'><img src='images/icons/pencil.png' class='icon'/> Edit clock time</div>"+
                    "<a class='close_button' onclick='Page.timesheet.hide_edit_clock_time();'><img src='images/window_close_button.gif'/></a>"+
                "</div>"+
                "<div id='window_result_container'>Loading...</div>"+
            "</div></div>";

        Page.timesheet._editWindow = new PopupWindow(content, {
            hideOnEsc: true,
            hideOnBlur: false,
            elementID: "edit_clock_time_window",
            handle: "window_title_bar"
        });
        Page.timesheet._editWindow.showCentered();

        new Ajax.Request("php/pages/timesheet_edit_clock_time.php?action=echo_content&clockTimeID="+clockTimeID, {
            onSuccess: function(request) {
                //Parse data
                var data = request.responseText.split("--JAVASCRIPT--");
                var content = data[0];
                var javascript = data[1];
                $('window_result_container').innerHTML = content;
                if (javascript) eval(javascript); //Execute javascript
                Page.timesheet._editWindow.center();
            }
        });
    },

    show_edit_request: function(clockTimeID, type) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Timesheet Edit Request</div>"+
            "<div class='popupWindowDesc' id='timeEditRequestDesc'>Loading...</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick=\"Page.timesheet.approve_edit_request("+clockTimeID+",'"+type+"','accept')\"><img src='images/icons/add.png' class='icon'/> <span class='dottedLink'>Ok, change it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick=\"Page.timesheet.approve_edit_request("+clockTimeID+",'"+type+"','deny')\"><img src='images/icons/delete.png' class='icon'/> <span class='dottedLink'>Deny this change.</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);

        new Ajax.Request("php/actions/time_edit_request_get.php", {
            parameters: "clockTimeID="+clockTimeID+"&type="+type,
            onSuccess: function(request) {
                $('timeEditRequestDesc').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },

    approve_edit_request: function(clockTimeID, type, action) {
		new Ajax.Request("php/actions/time_edit_request_accept.php", {
			parameters: "clockTimeID="+clockTimeID+"&type="+type+"&action="+action,
			onSuccess: function(response) {
                var sheet_row_id = "clockTime"+clockTimeID+"_row";
                Element.replace(sheet_row_id, response.responseText);
                
                FocusedWindow.hide();
                var msg = (action=="deny") ? "Edit request denied." : "Edit request accepted.";
                Global.showSystemMessage(msg);
                new Effect.Highlight(sheet_row_id, {duration: 2});
            }
		});
	},

    delete_absence: function(timeOffID) {
		new Ajax.Request("php/actions/absence_delete.php", {
			parameters: "timeOffID="+timeOffID,
            onSuccess: function(){
                Util.hideHoverText();
                new Effect.DropOut("timeOffRow"+timeOffID+"_row");
                Global.showSystemMessage("Absence deleted.");
            }
		});
	},

    show_absence_description: function(timeOffID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowTitle'><img src='images/icons/information.png' class='icon'/> <span class='dottedLink'>Absence Description</span></div>"+
            "<div class='popupWindowDesc' id='popupTimeOffDescription' style='max-width: 500px;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/absence_description_get.php", {
            parameters: "timeOffID="+timeOffID,
            onSuccess: function(request) {
                $('popupTimeOffDescription').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },

    delete_clock_time: function(clockTimeID) {
		new Ajax.Request("php/actions/clock_time_delete.php", {
			parameters: "clockTimeID="+clockTimeID,
			onSuccess: function(){
                Util.hideHoverText();
                new Effect.DropOut("clockTime"+clockTimeID+"_row");
                Global.showSystemMessage("Clock time deleted.");
            }
		});
	},

    show_voice_authentication: function(clockTimeID, clockType, year, month) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowTitle'><img src='images/icons/sound_grey.png' class='icon'/> <span class='dottedLink'>Voice Authentication</span></div>"+
            "<div class='popupWindowDesc' style='max-width: 500px;'>"+
            "<embed src='phone/voiceVerification/"+year+"/"+month+"/"+clockTimeID+"_"+clockType+".wav' width='250px' height='40px' autostart='true' loop='false'></embed>"+
            "</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
    },

    show_voice_report: function(clockTimeID, year, month) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowTitle'><img src='images/icons/sound_grey.png' class='icon'/> <span class='dottedLink'>Voice Report</span></div>"+
            "<div class='popupWindowDesc' style='max-width: 500px;'>"+
            "<embed src='phone/voiceReports/"+year+"/"+month+"/"+clockTimeID+".wav' width='250px' height='40px' autostart='true' loop='false'></embed>"+
            "</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
    },

    show_manager_comments: function(clockTimeID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupManagerComment' style='max-width: 500px; max-height: 350px; overflow: auto;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/clock_time_comments_get.php", {
            parameters: "clockTimeID="+clockTimeID,
            onSuccess: function(request) {
                $('popupManagerComment').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },

    show_shift_report: function(clockTimeID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupDailyReport' style='max-width: 500px; max-height: 350px; overflow: auto;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/clock_time_daily_report_get.php", {
            parameters: "clockTimeID="+clockTimeID,
            onSuccess: function(request) {
                $('popupDailyReport').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    }
}

Page.timesheet.daily_view = {
    _startDate: "",
    _endDate: "",
    filter_user: "",
    filter_status: "",
    _popup: null,
    _calendar: null,
    auto_refresh_rate: 10, //Minutes

    init: function() {
        /*
        if (screen.width >= 1280) {
            Element.addClassName(document.body, "screen_width_1280");

            //Force IE6 to refresh CSS. Yes this is a hack :(
            if (BrowserDetect.browser == "Explorer" && BrowserDetect.version == "6") {
                Util.showHoverText($("timesheet_calendar"), "");
                Util.hideHoverText();
            }
        }
        */
       
        //Init the calendar
        var c = new YAHOO.widget.Calendar("timesheet_calendar_id", "timesheet_calendar", {navigator: true});
        Page.timesheet.daily_view._calendar = new RangeSelector(c, {
            onSelect: Page.timesheet.daily_view._calendar_select
        });
        Page.timesheet.daily_view._startDate = Util.to_sql_date(new Date());
        Page.timesheet.daily_view._endDate = Page.timesheet.daily_view._startDate;
        Page.timesheet.daily_view._calendar.show_selected(new Date());

        //Init browser dropdown menus
        new DropDownMenu("dropdown_status", "dropdown_status_options", {
            selectedClass: "clockspot_dropdown_selected",
            container: "timesheet_browser_bar",
            onSelect: function(value) {
                Page.timesheet.daily_view.filter_status = value;
                Page.timesheet.daily_view.refresh_clock_times();
            }
        });
        new DropDownMenu("dropdown_date", "dropdown_date_options", {
            selectedClass: "clockspot_dropdown_selected",
            container: "timesheet_browser_bar",
            onSelect: function(value) {
                //Determine start and end dates
                var start, end;
                if (value == "today") {
                    start = end = new Date();
                } else if (value == "yesterday") {
                    start = new Date();
                    start.setDate(start.getDate()-1);
                    end = start;
                } else if (value == "this_week") {
                    start = new Date();
                    start.setDate(start.getDate()-start.getDay());
                    end = new Date();
                } else if (value == "last_week") {
                    end = new Date();
                    end.setDate(end.getDate()-end.getDay()-1);
                    start = new Date();
                    start.setTime(end.getTime()); //Clone end date
                    start.setDate(start.getDate()-6);
                } else if (value == "last_7_days") {
                    end = new Date();
                    start = new Date();
                    start.setDate(end.getDate()-6);
                }

                //Refresh view
                Page.timesheet.daily_view._startDate = Util.to_sql_date(start);
                Page.timesheet.daily_view._endDate = Util.to_sql_date(end);
                Page.timesheet.daily_view.refresh_clock_times();
                Page.timesheet.daily_view._calendar.reset();
                Page.timesheet.daily_view._calendar.show_selected(start,end);
            }
        });
        new DropDownMenu("dropdown_user", "dropdown_user_options", {
            selectedClass: "clockspot_dropdown_selected",
            container: "timesheet_browser_bar",
            onSelect: function(value) {
                Page.timesheet.daily_view.filter_user = value;
                Page.timesheet.daily_view.refresh_clock_times();
            }
        });
        new DropDownMenu("dropdown_actions", "dropdown_actions_options", {
            selectedClass: "timesheet_results_options_selected",
            updateSelector: false,
            onSelect: function(value) {
                if (value == "approve_all") {
                    new Ajax.Request("php/actions/clock_time_approve_all.php", {
                        parameters: "startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status,
                        onSuccess: function() {
                            $$(".sheet_col_options .sheet_status_img").each(function(node) {
                                node.src = "images/icons/tick.png";
                            });
                            Global.showSystemMessage("Clock times approved.");
                        }
                    });
                }
            }
        });
        new DropDownMenu("dropdown_save", "dropdown_save_options", {
            selectedClass: "timesheet_results_options_selected",
            updateSelector: false,
            onSelect: function(value) {
                if (value == "pdf") {
                    window.open("php/exports/timesheet_export_by_day.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&format=pdf&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status);
                } else if (value == "excel_by_day") {
                    window.open("php/exports/timesheet_export_by_day.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&format=xls&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status);
                }else if (value == "excel_by_employee") {
                    window.open("php/exports/timesheet_export_by_employee.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status);
                } else if (value == "shift_reports") {
                    window.open("php/exports/clock_time_daily_report_print.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status);
                }else if (value == "print_view") {
                    window.open("php/exports/timesheet_print.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status);
                }
            }
        });
        

        if (!$('timesheet_results_container').innerHTML) {
            $('timesheet_results_container').innerHTML = "No results found.";
            Element.hide('timesheet_results_options');
        }

        new Ajax.Request("php/pages/timesheet_daily_view.php?action=echo_filter_users", {
            onSuccess: function(request) {
                $('dropdown_user_options').innerHTML = request.responseText;
            }
        });

        setTimeout("Page.timesheet.daily_view._auto_refresh()", 1000*60*Page.timesheet.daily_view.auto_refresh_rate);
    },

    _auto_refresh: function() { //Refresh if you're looking at today
        if (Page.timesheet.daily_view._startDate == Util.to_sql_date(new Date()) ||
            Page.timesheet.daily_view._endDate == Util.to_sql_date(new Date())) {
            Page.timesheet.daily_view.refresh_clock_times();
        }
        setTimeout("Page.timesheet.daily_view._auto_refresh()", 1000*60*Page.timesheet.daily_view.auto_refresh_rate);
    },
    
    _calendar_select: function(startDate, endDate) {
        if (!endDate) {
            endDate = startDate;
        }

        //Set start date
        var year = startDate[0][0][0];
        var month = startDate[0][0][1];
        var day = startDate[0][0][2];
        if (month < 10) month = "0"+month;
        if (day < 10) day = "0"+day;
        Page.timesheet.daily_view._startDate = year+"-"+month+"-"+day;

        //Set end date
        year = endDate[0][0][0];
        month = endDate[0][0][1];
        day = endDate[0][0][2];
        if (month < 10) month = "0"+month;
        if (day < 10) day = "0"+day;
        Page.timesheet.daily_view._endDate = year+"-"+month+"-"+day;

        //Refresh the content
        Page.timesheet.daily_view.refresh_clock_times();

        //Update labels
        var format_date = function(jsdate) {
            var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
            var now = new Date();
            var year = jsdate[0][0][0];
            var month = jsdate[0][0][1];
            var day = jsdate[0][0][2];

            if (now.getFullYear() == year && now.getMonth() == month-1 && now.getDate() == day) {
                return "<b>Today</b>";
            }

            var formatted_date = months[month-1]+" "+day;
            if (now.getFullYear() != year) {
                formatted_date += ", "+year;
            }
            return formatted_date;
        }
        var startDate_formatted = format_date(startDate);
        var endDate_formatted = format_date(endDate);

        var date_string = [];
        date_string.push(startDate_formatted);
        if (startDate_formatted != endDate_formatted) {
            date_string.push(" - "+endDate_formatted);
        }
        $('dropdown_date').innerHTML = date_string.join("");
    },

    force_clock_out: function(clockTimeID) {
        var sheet_row_id = "clockTime"+clockTimeID+"_row";
        Element.setOpacity(sheet_row_id, 0.5);
        
        new Ajax.Request("php/actions/clock_time_force_clock_out.php", {
            parameters: "clockTimeID="+clockTimeID,
            onSuccess: function(request) {
                Element.replace(sheet_row_id, request.responseText);
                Element.setOpacity(sheet_row_id, 1.0);
                Global.showSystemMessage("You have forced a clock out.");
                new Effect.Highlight(sheet_row_id, {duration: 2, restorecolor: "#ffffff"});
            }
        });
    },
    
    refresh_clock_times: function() {
        Element.setOpacity('main_content', 0.5);
        Global.showLoading(3000);
        
        new Ajax.Request("php/actions/browse_daily_view.php?startDate="+Page.timesheet.daily_view._startDate+"&endDate="+Page.timesheet.daily_view._endDate+"&filter_user="+Page.timesheet.daily_view.filter_user+"&filter_status="+Page.timesheet.daily_view.filter_status, {
            onSuccess: function(request) {
                if (!request.responseText) {
                    $('timesheet_results_container').innerHTML = "No results found.";
                    Element.hide('timesheet_results_options');
                } else {
                    $('timesheet_results_container').innerHTML = request.responseText;
                    Element.show('timesheet_results_options');
                }
                Element.setOpacity('main_content', 1.0);
                Global.hideLoading();
            }
        });
    }
}

Page.timesheet.absence_view = {
    init: function() {
        if ($('startDateCalendar')) {
            var yahoo_calendar = new YAHOO.widget.CalendarGroup("startDateCalendarID", "startDateCalendar", {navigator: true});
            new RangeSelector(yahoo_calendar, {
                onSelect: function(startDate, endDate) {
                    var userID = $('employeeID').innerHTML;
                    $('searchResultsContainer').innerHTML = "Loading...";
                    new Ajax.Request("php/actions/browse_absences.php", {
                        parameters: "startDate="+startDate+"&endDate="+endDate+"&userID="+userID,
                        onSuccess: function(request){
                            $('searchResultsContainer').innerHTML = request.responseText;
                        }
                    });
                }
            });
	    }
    },

    updateSelection: function(node, absenceType) {
        Page.timesheet.absence_view.updateAbsenceType(absenceType);

        $$(".periodChooser a").each(function(node) {
            Element.removeClassName(node, "selected");
        });
        Element.addClassName(node, "selected");
    },

    updateAbsenceType: function(absenceType) {
        Element.setOpacity('absenceContainer', 0.5);
        new Ajax.Request("php/pages/timesheet_absence_view.php", {
            parameters: "absenceType="+absenceType,
            onSuccess: function(request) {
                $('absenceContainer').innerHTML = request.responseText;
                Element.setOpacity('absenceContainer', 1.0);
            }
        });
    },

    show_edit_hours: function(employeeID, absenceType, vacationLeft) {
        var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Edit "+absenceType+" hours</div>"+
            "<form name='editVacationHoursForm' id='editVacationHoursForm'>"+
            "<div class='popupWindowDesc popupWindowSubsection' id='editVacationInput' style='white-space: nowrap;'>This employee has <input type='text' name='vacationLeft' id='vacationLeft' style='width: 30px;' value='"+vacationLeft+"'/> hours of "+absenceType+" available.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><input type='image' src='images/icons/weather_sun.png' class='icon'/> <span class='dottedLink'>Save changes</span></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "<input type='hidden' value='"+absenceType+"' name='absenceType'/>"+
            "<input type='hidden' value='"+employeeID+"' name='employeeID'/>"+
            "</form></div>";
        FocusedWindow.show(content);

        new AjaxForm("editVacationHoursForm", {
            handler: 'php/actions/absence_accrual_edit.php',
            onSuccess: function(){
                Page.timesheet.absence_view.updateAbsenceType(absenceType);
                FocusedWindow.hide();
                Global.showSystemMessage("Available hours updated.");
            }
        });
    },

    show_start_accruing: function(employeeID, absenceType) {
        var date = new Date();
        var currentDate = date.getFullYear()+"-"+(1+date.getMonth())+"-"+date.getDate();
        var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Accrue "+absenceType+" hours</div>"+

            "<form name='accrueHoursForm1' id='accrueHoursForm1'>"+
            "<div style='text-align: left; font-weight: bold'>1. Accrue "+absenceType+" hours over time:</div>"+
            "<div onmouseover=\"this.style.backgroundColor='#ffff99;'\" onmouseout=\"this.style.backgroundColor='#fffed1;'\" class='popupWindowSubsection' id='accrueProperties1'>"+
            "Accrue <input type='text' name='accrueRate' id='accrueRate' style='width: 25px;' value='2'/> hours of "+absenceType+" every <input type='text' name='accrueInterval' id='accrueInterval' style='width: 25px;' value='14'/> days, <br/>starting on <input type='text' name='accrueStartDate' id='accrueStartDate' style='width: 75px;' value='"+currentDate+"'/> <span style='color: #969696; font-style: italic;'>(YYYY-MM-DD)</span>.<br/><input type='image' src='images/icons/weather_add.png' class='icon'/> <span class='dottedLink'>Start accruing hours</span>"+
            "</div>"+
            "<input type='hidden' value='"+absenceType+"' name='absenceType'/>"+
            "<input type='hidden' value='1' name='accrueType'/>"+
            "<input type='hidden' value='"+employeeID+"' name='employeeID'/>"+
            "</form>"+

            "<div style='font-weight: bold; margin-top: -3px; margin-bottom: 3px;'>OR</div>"+

            "<form name='accrueHoursForm2' id='accrueHoursForm2'>"+
            "<div style='text-align: left; font-weight: bold'>2. Accrue based on employee work hours:</div>"+
            "<div style='width: 370px' onmouseover=\"this.style.backgroundColor='#ffff99;'\" onmouseout=\"this.style.backgroundColor='#fffed1;'\" class='popupWindowSubsection' id='accrueProperties2'>"+
            "Accrue <input type='text' name='workHourPercent' id='workHourPercent' style='width: 25px;' value='4'/>% of total employee work hours to "+absenceType+" hours,<br/>starting on <input type='text' name='accrueStartDate2' id='accrueStartDate2' style='width: 75px;' value='"+currentDate+"'/> <span style='color: #969696; font-style: italic;'>(YYYY-MM-DD)</span>.<br/><input type='image' src='images/icons/weather_add.png' class='icon'/> <span class='dottedLink'>Start accruing hours</span>"+
            "</div>"+
            "<input type='hidden' value='"+absenceType+"' name='absenceType'/>"+
            "<input type='hidden' value='2' name='accrueType'/>"+
            "<input type='hidden' value='"+employeeID+"' name='employeeID'/>"+
            "</form>"+

            "<table class='trimmed' style='width: 370px;'><tr>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);

        new AjaxForm("accrueHoursForm1", {
            handler: 'php/actions/absence_accrual_start.php',
            onSuccess: function(){
                Page.timesheet.absence_view.updateAbsenceType(absenceType);
                FocusedWindow.hide();
                Global.showSystemMessage("Accrual started.");
            }
        });
        new AjaxForm("accrueHoursForm2", {
            handler: 'php/actions/absence_accrual_start.php',
            onSuccess: function(){
                Page.timesheet.absence_view.updateAbsenceType(absenceType);
                FocusedWindow.hide();
                Global.showSystemMessage("Accrual started.");
            }
        });
    },

    show_stop_accruing: function(employeeID, absenceType) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Stop accruing "+absenceType+" hours?</div>"+
            "<div class='popupWindowDesc'>This will discontinue the accruing of "+absenceType+" hours. You may always start the accrual later.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick=\"Page.timesheet.absence_view.stop_accruing('"+employeeID+"', '"+absenceType+"')\"><img src='images/icons/control_stop.png' class='icon'/> <span class='dottedLink'>Stop accruing hours</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    stop_accruing: function(employeeID, absenceType) {
		new Ajax.Request("php/actions/absence_accrual_stop.php", {
			parameters: "employeeID="+employeeID+"&absenceType="+absenceType,
			onSuccess: function(){
                Page.timesheet.absence_view.updateAbsenceType(absenceType);
                FocusedWindow.hide();
                Global.showSystemMessage("Accrual stopped.");
            }
		});
	}
}

Page.timesheet.job_codes = {
    init: function() {
        new AjaxForm("addJobCodeForm", {
            handler: 'php/actions/job_code_add.php',
            onSuccess: function(){
                Global.showSystemMessage("Job code added.");
                Global.refresh_content();
            }
        });
    },

    job_code_delete: function(jobCodeID) {
        new Ajax.Request("php/actions/job_code_delete.php", {
            parameters: "jobCodeID="+jobCodeID,
            onSuccess: function(request) {
                Element.remove("job_code"+jobCodeID+"_row");
                Global.showSystemMessage("Job code deleted.");
            }
        });
    },

    job_code_load_edit_form: function(namespace, columnValues) {
        $(namespace+"_main_replacement").innerHTML =
            "<form id='"+namespace+"_form'>"+
            "<input type='text' value='"+columnValues.jobCode+"' name='jobCode' id='"+namespace+"jobCode'/>"+
            "<input type='text' value='"+columnValues.name+"' name='name' id='"+namespace+"name'/>"+
            "<input type='text' value='"+columnValues.rate+"' name='rate' id='"+namespace+"rate'/>"+
            "<input type='hidden' value='"+columnValues.jobCodeID+"' name='jobCodeID'/>"+
            "<input type='hidden' value='"+namespace+"' name='namespace'/>"+
            "</form>";
    },

    job_code_save_edit_form: function(namespace) {
        var editForm = namespace+"_form";
        var saveButton = namespace+"_save_button";
        $(saveButton).disabled = "disabled";

        new Ajax.Request("php/actions/job_code_edit.php", {
            parameters: Form.serialize(editForm),
            onSuccess: function(request) {
                Global.showSystemMessage("Job code updated.");
                Global.refresh_content();
            }
        });
    }
}

Page.timesheet.add_clock_time = {
    init: function() {
        var d = new Date();
        var sql_date = Util.to_sql_date(d);
        $('inDate').value = sql_date;
        $('outDate').value = sql_date;
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();

        var calendarIn = new YAHOO.widget.Calendar("clockInCalendarID", "clockInCalendar", {selected: todaysDate});
        calendarIn.selectEvent.subscribe(Page.timesheet.add_clock_time.updateInDate);
        calendarIn.render();

        var calendarOut = new YAHOO.widget.Calendar("clockOutCalendarID", "clockOutCalendar", {selected: todaysDate});
        calendarOut.selectEvent.subscribe(Page.timesheet.add_clock_time.updateOutDate);
        calendarOut.render();

        new AjaxForm("addClockTimeForm", {
            handler: 'php/actions/clock_time_add.php',
            onSuccess: function(){
                calendarIn.reset();
                calendarOut.reset();
                Form.reset('addClockTimeForm');
                Global.showSystemMessage("Clock time added.");
            }
        });
    },

    updateInDate: function(eventType, inDate) {
        $('inDate').value = Util.yui_to_sql_date(inDate);
    },

    updateOutDate: function(eventType, outDate) {
        $('outDate').value = Util.yui_to_sql_date(outDate);
    },

    disableClockOut: function() {
        if ($F('dontSetClockOut')) {
            $('outHour').disabled = "disabled";
            $('outMinute').disabled = "disabled";
            $('outAmPm').disabled = "disabled";
            Element.setOpacity('clockOutCalendar', 0.3);
        } else {
            $('outHour').disabled = "";
            $('outMinute').disabled = "";
            $('outAmPm').disabled = "";
            Element.setOpacity('clockOutCalendar', 1.0);
        }
    }
}

Page.timesheet.add_absence = {
    offDateCalendar: null,
    _edit_absence_types_window: null,
        
    init: function() {
        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();
        Page.timesheet.add_absence.offDateCalendar = new YAHOO.widget.CalendarGroup("offDateCalendarID", "offDateCalendar", {
            MULTI_SELECT: true
        });
        Page.timesheet.add_absence.offDateCalendar.selectEvent.subscribe(Page.timesheet.add_absence.updateSelected);
        Page.timesheet.add_absence.offDateCalendar.deselectEvent.subscribe(Page.timesheet.add_absence.updateSelected);
        if (Page.timesheet.add_absence._selectedDates) {
            Page.timesheet.add_absence.offDateCalendar.select(Page.timesheet.add_absence._selectedDates);
        }
        if (Page.timesheet.add_absence._dateListValue) {
            $('dateList').value = Page.timesheet.add_absence._dateListValue;
        }
        Page.timesheet.add_absence.offDateCalendar.render();

	    new AjaxForm("addTimeOffForm", {
            handler: 'php/actions/absence_add.php',
            onSuccess: function(){
                Global.showSystemMessage("Absence added.");
                Form.reset("addTimeOffForm");
                Page.timesheet.add_absence.offDateCalendar.deselectAll();
                Page.timesheet.add_absence.offDateCalendar.render();
            }
        });
    },

    show_edit_absence_types: function() {
        var content =
            "<div id='window_shadow_container'>"+
            "<div id='window_container'>"+
                "<div id='window_title_bar'>"+
                    "<div id='window_title_text'><img src='images/icons/pencil.png' class='icon'/> Edit absence types</div>"+
                    "<a class='close_button' onclick='Page.timesheet.add_absence.hide_edit_absence_types();'><img src='images/window_close_button.gif'/></a>"+
                "</div>"+
                "<div id='window_result_container'>Loading...</div>"+
            "</div></div>";

        Page.timesheet.add_absence._edit_absence_types_window = new PopupWindow(content, {
            hideOnEsc: true,
            hideOnBlur: false,
            elementID: "edit_absence_types_window",
            handle: "window_title_bar"
        });
        Page.timesheet.add_absence._edit_absence_types_window.showCentered();

        new Ajax.Request("php/actions/absence_type_get.php", {
            onSuccess: function(request) {
                $('window_result_container').innerHTML = request.responseText;
                Form.Element.focus("new_absence_type");
            }
        });
    },

    hide_edit_absence_types: function() {
        Page.timesheet.add_absence._edit_absence_types_window.hide();
    },

    add_absence_type: function() {
        $("new_absence_type_submit").disabled = "disabled";
        new Ajax.Request("php/actions/absence_type_add.php?absenceType="+$("new_absence_type").value, {
            onSuccess: function(request) {
                $("new_absence_type_submit").disabled = "";
                var response = AjaxForm.handleResponse(request.responseText);
                if (response.status == "success") {
                    //Refresh the main page and edit window.
                    Global.refresh_content();
                    new Ajax.Request("php/actions/absence_type_get.php", {
                        onSuccess: function(request) {
                            $('window_result_container').innerHTML = request.responseText;
                            Form.Element.focus("new_absence_type");
                        }
                    });
                    Global.showSystemMessage("Absence type added.");
                }
            }
        });
    },

    delete_absence_type: function(row_id, absence_type) {        
        new Ajax.Request("php/actions/absence_type_delete.php?absenceType="+absence_type, {
            onSuccess: function(request) {
                new Effect.Fade(row_id, {duration: 0.5});
                Global.showSystemMessage("Absence type deleted.");
                Global.refresh_content();
            }
        });
    },

    updateSelected: function() {
        var dates = Page.timesheet.add_absence.offDateCalendar.getSelectedDates();
        var dateList = [];
        for (var i = 0; i < dates.length; i++) {
            var date = dates[i];
            var dateString = date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate();
            dateList.push(dateString);
        }
        $('dateList').value = dateList.join(",");
    }
}

Page.timesheet.edit_clock_time = {
    _init: function(startDate, endDate, pageDateIn, pageDateOut, inDateValue, outDateValue) {
        $('inDate').value = inDateValue;
        $('outDate').value = outDateValue;

        var calendarIn = new YAHOO.widget.Calendar("clockInCalendarID", "clockInCalendar", {pagedate: pageDateIn, selected: startDate});
        calendarIn.selectEvent.subscribe(Page.timesheet.edit_clock_time.updateInDate);
        calendarIn.render();

        var calendarOut = new YAHOO.widget.Calendar("clockOutCalendarID", "clockOutCalendar", {pagedate: pageDateOut, selected: endDate});
        calendarOut.selectEvent.subscribe(Page.timesheet.edit_clock_time.updateOutDate);
        calendarOut.render();
       
        new AjaxForm("editClockTimeForm", {
            handler: 'php/actions/clock_time_edit.php',
            onSuccess: function(response) {
                var clocktime_id = $F('editClockTimeForm_clockTimeID');
                var sheet_row_id = "clockTime"+clocktime_id+"_row";
                Element.replace(sheet_row_id, response);
                Page.timesheet.hide_edit_clock_time();
                Global.showSystemMessage("Clock time updated.");
                new Effect.Highlight(sheet_row_id, {duration: 2});
            }
        });

        if ($('dontSetClockOut').checked) {
            Page.timesheet.edit_clock_time.disableClockOut();
        }
    },

    updateInDate: function(eventType, inDate) {
        $('inDate').value = Util.yui_to_sql_date(inDate);
    },

    updateOutDate: function(eventType, outDate) {
        $('outDate').value = Util.yui_to_sql_date(outDate);
    },

    disableClockOut: function() {
        if ($F('dontSetClockOut')) {
            $('outHour').disabled = "disabled";
            $('outMinute').disabled = "disabled";
            $('outAmPm').disabled = "disabled";
            Element.setOpacity('clockOutCalendar', 0.3);
        } else {
            $('outHour').disabled = "";
            $('outMinute').disabled = "";
            $('outAmPm').disabled = "";
            Element.setOpacity('clockOutCalendar', 1.0);
        }
    }
}

Page.timesheet.edit_absence = {
    _init: function(absenceDate, pageDate, timeOffDateValue) {
        $('timeOffDate').value = timeOffDateValue;

        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();
        
        var offDateCalendar = new YAHOO.widget.CalendarGroup("offDateCalendarID", "offDateCalendar", {
            selected: absenceDate,
            pagedate: pageDate
        });
        offDateCalendar.selectEvent.subscribe(Page.timesheet.edit_absence.updateSelected);
	    offDateCalendar.render();

	    new AjaxForm("editTimeOffForm", {
            handler: 'php/actions/absence_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Absence updated.");
            }
        });
    },

    updateSelected: function(eventType, theDate) {
        $('timeOffDate').value = theDate;
    }
}

Page.timesheet.absence_requests_view = {
    show_deny: function(requestID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Deny this absence request?</div>"+
            "<div class='popupWindowDesc'>This will remove the absence request from the list. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.timesheet.absence_requests_view.deny_request("+requestID+")'><img src='images/icons/delete.png' class='icon'/> <span class='dottedLink'>Ok, deny it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    deny_request: function(requestID) {
		new Ajax.Request("php/actions/absence_request_deny.php", {
			parameters: "requestID="+requestID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Absence request denied.");
            }
		});
	},

    show_approve_all_requests: function() {
        var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Approve all absence requests?</div>"+
            "<div class='popupWindowDesc'>Paid hours set for absences: <input type='text' style='width: 30px;' value='8' id='absencesPaidHours' name='absencesPaidHours'/> hours.<br/>You are about to approve all pending absence requests.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.timesheet.absence_requests_view.approve_all_requests()'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, approve all requests.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
    },

    approve_all_requests: function() {
        new Ajax.Request("php/actions/absence_request_approve_all.php", {
			parameters: "paidHours="+$F('absencesPaidHours'),
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Absence requests approved.");
                Global.refresh_content();
            }
		});
    }
}

Page.reports.payroll_reports = {
    show_delete_pay_period: function(startDate, endDate) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this pay period?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this pay period from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick=\"Page.reports.payroll_reports.delete_pay_period('"+startDate+"', '"+endDate+"')\"><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_pay_period: function(startDate, endDate) {
		new Ajax.Request("php/actions/payment_delete.php", {
			parameters: "startDate="+startDate+"&endDate="+endDate,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Pay period deleted.");
                Global.refresh_content();
            }
		});
	},

    show_delete_payment: function(paymentID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this payment?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this payment from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.reports.payroll_reports.delete_payment("+paymentID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_payment: function(paymentID) {
		new Ajax.Request("php/actions/payment_delete.php", {
			parameters: "paymentID="+paymentID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Payment deleted.");
                Global.refresh_content();
            }
		});
	},

    show_payment_comment: function(paymentID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowTitle'><img src='images/icons/information.png' class='icon'/> <span class='dottedLink'>Payment Notes</span></div>"+
            "<div class='popupWindowDesc' id='popupPaymentComment' style='max-width: 500px;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/payment_comment_get.php", {
            parameters: "paymentID="+paymentID,
            onSuccess: function(request) {
                $('popupPaymentComment').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    }
}

Page.reports.generate_payroll = {
    init: function() {
        var yahoo_calendar = new YAHOO.widget.CalendarGroup("dateCalendarID", "dateCalendar", {navigator: true});
        new RangeSelector(yahoo_calendar, {
            onSelect: function(startDate, endDate) {
                $('startDate').value = startDate;
                $('endDate').value = endDate;
            }
        });
	    Event.observe('selectAllCheckbox', 'click', Page.reports.generate_payroll.selectAll, false);
    },

    selectAll: function() {
        var selectValue = $F('selectAllCheckbox') ? "checked" : "";
        $$('.userCheckbox').each(function(node) {
            node.checked = selectValue;
        });
    },

    update: function() {
        if (!$('startDate').value) return;
        $('searchResultsContainer').innerHTML = "Loading...";
        new Ajax.Request("php/actions/payment_generate.php", {
            parameters: Form.serialize('paymentOptionsForm'),
            onSuccess: function(request){
                $('searchResultsContainer').innerHTML = request.responseText;
                new AjaxForm("paymentForm", {
                    handler: 'php/actions/payment_add.php',
                    onSuccess: function(){
                        Global.showSystemMessage("Payments added.");
                        Global.refresh_content({scroll: true});
                    }
                });
            }
        });
    }
}

Page.reports.create_report = {
    _users_selected: true,
    
    init: function() {
        if (screen.width >= 1280) {
            var yahoo_calendar = new YAHOO.widget.CalendarGroup("report_calendar_id", "report_calendar", {navigator: true});
        } else {
            var yahoo_calendar = new YAHOO.widget.Calendar("report_calendar_id", "report_calendar", {navigator: true});
        }
        new RangeSelector(yahoo_calendar, {
            onSelect: function(start_date, end_date) {
                $('start_date').value = Util.yui_to_sql_date(start_date);
                $('end_date').value = Util.yui_to_sql_date(end_date);
            }
        });
    },

    toggle_section: function(section) {
        if (Element.visible(section+"_section")) {
            new Effect.Fade(section+"_section", {duration: 0.5});
            new Effect.Fade(section+"_section_body", {duration: 0.5});
        } else {
            new Effect.Appear(section+"_section", {duration: 0.5});
            new Effect.Appear(section+"_section_body", {duration: 0.5});
        }
    },

    select_all_users: function() {
        $$('.user_checkbox').each(function(node) {
            node.checked = !Page.reports.create_report._users_selected;
        });
        
        Page.reports.create_report._users_selected = !Page.reports.create_report._users_selected;
        $('reports_select_all_users').value = Page.reports.create_report._users_selected ? "Select none" : "Select all";
    }
}

Page.users.view = {
    show_contact_card: function(userID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupContactCard' style='max-width: 500px;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/user_contact_card_get.php", {
            parameters: "userID="+userID,
            onSuccess: function(request) {
                $('popupContactCard').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },

    show_disable_employee: function(employeeID) {
        if (Global.userID==employeeID) {
            var content =
                "<div class='popupWindow'>"+
                "<div class='popupWindowTitle'>You may not delete yourself.</div>"+
                "<a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a>";
                "</div>";
        } else {
            var content =
                "<div class='popupWindow'>"+
                "<div class='popupWindowTitle'>Delete this user?</div>"+
                "<div class='popupWindowDesc'>This action will restrict login access for the user. However, all information associated with this user will still be available. You may undo this action at any time.</div>"+
                "<table class='trimmed' style='width: 250px;'><tr>"+
                    "<td><a onclick='Page.users.view.disable_employee("+employeeID+")'><img src='images/icons/lock.png' class='icon'/> <span class='dottedLink'>Ok, delete this user.</span></a></td>"+
                    "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
                "</tr></table>"+
                "</div>";
        }
        FocusedWindow.show(content);
	},

    disable_employee: function(employeeID) {
		new Ajax.Request("php/actions/user_disable.php", {
			parameters: "employeeID="+employeeID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("User deleted.");
                Global.refresh_content();
            }
		});
	}
}

Page.users.view_deleted = {
    show_enable_employee: function(employeeID) {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Restore this user?</div>"+
        "<div class='popupWindowDesc'>This action will grant the user login access, and register the user as active.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
        "<td><a onclick='Page.users.view_deleted.enable_employee("+employeeID+")'><img src='images/icons/lock_open.png' class='icon'/> <span class='dottedLink'>Ok, restore this user.</span></a></td>"+
        "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
    },
    enable_employee: function(employeeID) {
        new Ajax.Request("php/actions/user_enable.php", {
            parameters: "employeeID="+employeeID,
            onSuccess: function(response) {
                if (response.responseText) {
                    FocusedWindow.hide();
                    Global.showStatusMessage(response.responseText, {
                        icon: "images/icons/exclamation.png",
                        backgroundColor: "#fff936",
                        fadeOut: true
                    });
                } else {
                    FocusedWindow.hide();
                    Global.showSystemMessage("User restored.");
                    Global.refresh_content();
                }
            }
        });
    }
}

Page.users.schedules = {
    init: function() {
        var c = new YAHOO.widget.Calendar("schedule_calendar_id", "schedule_calendar", {navigator: true});
        c.selectEvent.subscribe(function(eventType, theDate) {
            //Update schedule date range
            var sqlDate = Util.yui_to_sql_date(theDate);
            Schedule.changeDate(sqlDate);

            //Render calendar to show date range
            var start = Util.to_javascript_date(sqlDate);
            var end = Util.to_javascript_date(sqlDate);
            end.setDate(end.getDate()+6);

            var range = (start.getMonth()+1)+"/"+start.getDate()+"/"+start.getFullYear()+"-"+(end.getMonth()+1)+"/"+end.getDate()+"/"+end.getFullYear();
            c.cfg.setProperty("selected",range,false);
            c.render();
        });

        //Render the calendar
        var start = new Date();
        var end = new Date();
        end.setDate(end.getDate()+6);
        var range = (start.getMonth()+1)+"/"+start.getDate()+"/"+start.getFullYear()+"-"+(end.getMonth()+1)+"/"+end.getDate()+"/"+end.getFullYear();
        c.cfg.setProperty("selected",range,false);
        c.render();
    }
}

Page.users.add = {
    init: function() {
        new AjaxForm("addEmployeeForm", {
            handler: 'php/actions/user_add.php',
            onSuccess: function(){Form.reset('addEmployeeForm');Global.showSystemMessage("User added.");}
        });
    }
}

Page.users.edit_user = {
    init: function() {
        var employeeID = $('employeeID').innerHTML;
        if (!employeeID) return;

        if ($('edit_form_basic')) {
            new AjaxForm("edit_form_basic", {
                handler: 'php/actions/user_edit.php?s=basic',
                onSuccess: function(){Global.showSystemMessage("Basic information updated.");}
            });
        }
        if ($('edit_form_password')) {
            new AjaxForm("edit_form_password", {
                handler: 'php/actions/user_edit.php?s=password',
                onSuccess: function(){Form.reset('edit_form_password');Global.showSystemMessage("Password updated.");}
            });
        }
        if ($('edit_permissions')) {
            new AjaxForm("edit_permissions", {
                handler: 'php/actions/user_edit.php?s=permissions',
                onSuccess: function(){Global.showSystemMessage("Permissions updated.");}
            });
        }
    },

    clear_profile_pic: function(employeeID) {
        new Ajax.Request("php/actions/user_picture_update.php", {
            parameters: "userID="+employeeID+"&action=clear",
            onSuccess: function(request){$('profilePicture').innerHTML="";}
        });
    }
}

Page.users.add_note = {
    init: function() {
        new AjaxForm("notesForm", {
            handler: 'php/actions/user_note_add.php',
            onSuccess: function(){
                Global.showSystemMessage("Note added.");
                Global.refresh_content();
            }
        });
    }
}

Page.users.notes_browse = {
    init: function() {
        var yahoo_calendar = new YAHOO.widget.CalendarGroup("startDateCalendarID", "startDateCalendar", {navigator: true});
        new RangeSelector(yahoo_calendar, {
            onSelect: function(startDate, endDate) {
                $('searchResultsContainer').innerHTML = "Loading...";
                new Ajax.Request("php/actions/browse_user_notes.php", {
                    parameters: "startDate="+startDate+"&endDate="+endDate+"&employeeID="+$F('employeeID')+"&searchString="+$F('searchString'),
                    onSuccess: function(request){
                        $('searchResultsContainer').innerHTML = request.responseText;
                    }
                });
            }
        });
    }
}

Page.timers.view = {
    
    loadTimersData: function(node, action, startDate, endDate, id) {
        Element.setOpacity('timersContainer', 0.5);
        new Ajax.Request("php/pages/timers_view.php?action="+action, {
            parameters: "startDate="+startDate+"&endDate="+endDate+"&id="+id,
            onSuccess: function(request){
                $('timersContainer').innerHTML = request.responseText;
                Element.setOpacity('timersContainer', 1.0);
            }
        });
        $$(".periodChooser a").each(function(node) {
            Element.removeClassName(node, "selected");
        });
        Element.addClassName(node, "selected");
    },

    showDateSelector: function() {
        var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowTitle'>Select a date range</div>"+
            "<div class='range_selector_description'>First click a start date. Then click an end date.</div>"+
            "<div style='height: 185px; width: 300px;'>"+
            "<div id='startDateCalendar'></div>"+
            "</div>"+
            "<div style='text-align: right;'><a onclick='Page.timers.view.hideDateSelector();'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);

        var yahoo_calendar = new YAHOO.widget.Calendar("startDateCalendarID", "startDateCalendar", {navigator: true});
        new RangeSelector(yahoo_calendar, {
            onSelect: function(startDate, endDate) {
                var timerID = "";
                if ($('timerID')) timerID = $('timerID').innerHTML;
                var action = timerID ? "printTimersUsers" : "printTimers";
                Page.timers.view.loadTimersData(null, action, startDate, endDate, timerID);
                Page.timers.view.hideDateSelector();
            }
        });
    },

    toggleCategory: function(node) {
        Element.toggle(node);
        var state = Element.visible(node) ? "open" : "closed";
        Cookie.set(node, state, 90);
    },

    hideDateSelector: function() {
        FocusedWindow.hide();
    },

    show_timer_description: function(timerID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupTimerDescription' style='max-width: 500px; max-height: 350px; overflow: auto;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/timer_description_get.php", {
            parameters: "timerID="+timerID,
            onSuccess: function(request) {
                $('popupTimerDescription').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },

    show_delete_timer: function(timerID) {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Delete Timer?</div>"+
        "<div class='popupWindowDesc'>This will delete the timer and all related information from the system. This action cannot be undone.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
            "<td><a onclick='Page.timers.view.delete_timer("+timerID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Delete timer.</span></a></td>"+
            "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
	},

    delete_timer: function(timerID) {
		new Ajax.Request("php/actions/timer_delete.php", {
			parameters: "timerID="+timerID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Timer deleted.");
                Global.refresh_content();
            }
		});
	},

    show_delete_timer_entry: function(entryID) {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Delete this entry?</div>"+
        "<div class='popupWindowDesc'>This will delete the timer entry. This action cannot be undone.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
            "<td><a onclick='Page.timers.view.delete_timer_entry("+entryID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Delete timer entry.</span></a></td>"+
            "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
	},

    delete_timer_entry: function(entryID) {
		new Ajax.Request("php/actions/timer_entry_delete.php", {
			parameters: "entryID="+entryID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Timer entry deleted.");
                Global.refresh_content();
            }
		});
	},

    show_delete_timer_category: function(categoryID) {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Delete Category?</div>"+
        "<div class='popupWindowDesc'>This will delete the category, the timers under it, and all other related information from the system. This action cannot be undone.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
            "<td><a onclick='Page.timers.view.delete_timer_category("+categoryID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Delete category.</span></a></td>"+
            "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
	},

    delete_timer_category: function(categoryID) {
		new Ajax.Request("php/actions/timer_category_delete.php", {
			parameters: "categoryID="+categoryID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Category deleted.");
                Global.refresh_content();
            }
		});
	},

    show_delete_timer_user: function(timerID, userID) {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Delete this user's timer?</div>"+
        "<div class='popupWindowDesc'>This will delete this user's timer and all related information from the system. This action cannot be undone.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
            "<td><a onclick='Page.timers.view.delete_timer_user("+timerID+", "+userID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Delete this timer.</span></a></td>"+
            "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
	},

    delete_timer_user: function(timerID, userID) {
		new Ajax.Request("php/actions/timer_delete.php", {
			parameters: "timerID="+timerID+"&userID="+userID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Timer deleted.");
                Global.refresh_content();
            }
		});
	}
}

Page.timers.add = {
    init: function() {
        new AjaxForm("addTimerForm", {
            handler: 'php/actions/timer_add.php',
            onSuccess: function(){
                Form.reset('addTimerForm');
                Global.showSystemMessage("Timer added.");
            }
        });
    }
}

Page.timers.add_category = {
    init: function() {
        new AjaxForm("addCategoryForm", {
            handler: 'php/actions/timer_category_add.php',
            onSuccess: function(){
                Form.reset('addCategoryForm');
                Global.showSystemMessage("Category added.");
            }
        });
    }
}

Page.timers.add_timer_entry = {
    init: function() {
        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();
        var yuiDate = d.getFullYear()+","+(d.getMonth()+1)+","+d.getDate();
        $('inDate').value = yuiDate;
        $('outDate').value = yuiDate;

        var calendarIn = new YAHOO.widget.Calendar("clockInCalendarID", "clockInCalendar", {
            selected: todaysDate,
            maxdate: todaysDate
        });
        calendarIn.selectEvent.subscribe(Page.timers.add_timer_entry.updateInDate);
	    calendarIn.render();

	    var calendarOut = new YAHOO.widget.Calendar("clockOutCalendarID", "clockOutCalendar", {
	        selected: todaysDate,
	        maxdate: todaysDate
        });
        calendarOut.selectEvent.subscribe(Page.timers.add_timer_entry.updateOutDate);
	    calendarOut.render();

        new AjaxForm("addTimerTimeForm", {
            handler: 'php/actions/timer_entry_add.php',
            onSuccess: function(){
                calendarIn.reset();
                calendarOut.reset();
                Form.reset('addTimerTimeForm');
                Global.showSystemMessage("Timer entry added.");
            }
        });
    },

    updateInDate: function(eventType, inDate) {
        $('inDate').value = inDate;
    },

    updateOutDate: function(eventType, outDate) {
        $('outDate').value = outDate;
    },

    disableClockOut: function() {
        if ($F('dontSetClockOut')) {
            $('outHour').disabled = "disabled";
            $('outMinute').disabled = "disabled";
            $('outAmPm').disabled = "disabled";
            Element.setOpacity('clockOutCalendar', 0.3);
        } else {
            $('outHour').disabled = "";
            $('outMinute').disabled = "";
            $('outAmPm').disabled = "";
            Element.setOpacity('clockOutCalendar', 1.0);
        }
    }
}

Page.timers.edit_timer = {
    init: function() {
        new AjaxForm("editTimerForm", {
            handler: 'php/actions/timer_edit.php',
            onSuccess: function(){Global.showSystemMessage("Timer updated.");}
        });
    }
}

Page.timers.edit_timer_category = {
    init: function() {
        new AjaxForm("editCategoryForm", {
            handler: 'php/actions/timer_category_edit.php',
            onSuccess: function(){Global.showSystemMessage("Category updated.");}
        });
    }
}

Page.timers.edit_timer_entry = {
    _init: function(startDate, endDate, pageDateIn, pageDateOut, inDateValue, outDateValue) {
        $('inDate').value = inDateValue;
        $('outDate').value = outDateValue;
        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();
        var calendarIn = new YAHOO.widget.Calendar("clockInCalendarID", "clockInCalendar", {
            pagedate: pageDateIn,
            selected: startDate,
            maxdate: todaysDate
        });
        calendarIn.selectEvent.subscribe(Page.timers.edit_timer_entry.updateInDate);
	    calendarIn.render();

	    var calendarOut = new YAHOO.widget.Calendar("clockOutCalendarID", "clockOutCalendar", {
	        pagedate: pageDateOut,
	        selected: endDate,
	        maxdate: todaysDate
        });
        calendarOut.selectEvent.subscribe(Page.timers.edit_timer_entry.updateOutDate);
	    calendarOut.render();

        new AjaxForm("editTimeForm", {
            handler: 'php/actions/timer_entry_edit.php',
            onSuccess: function(){Global.showSystemMessage("Timer entry updated.");}
        });

        if ($('dontSetClockOut').checked) {
            Page.timers.edit_timer_entry.disableClockOut();
        }
    },

    updateInDate: function(eventType, inDate) {
        $('inDate').value = inDate;
    },

    updateOutDate: function(eventType, outDate) {
        $('outDate').value = outDate;
    },

    disableClockOut: function() {
        if ($F('dontSetClockOut')) {
            $('outHour').disabled = "disabled";
            $('outMinute').disabled = "disabled";
            $('outAmPm').disabled = "disabled";
            Element.setOpacity('clockOutCalendar', 0.3);
        } else {
            $('outHour').disabled = "";
            $('outMinute').disabled = "";
            $('outAmPm').disabled = "";
            Element.setOpacity('clockOutCalendar', 1.0);
        }
    }
}

Page.timers.open = {
	toggle_timer: function(node, timerID) {
		new Ajax.Request("php/pages/timers_open.php", {
            parameters: "timerID="+timerID,
            onSuccess: function(request) {
                $('timersContainer').innerHTML = request.responseText;
            }
        });
	},

	toggle_category: function(node) {
	    Element.toggle(node);
	    var state = Element.visible(node) ? "open" : "closed";
        Cookie.set(node, state, 90);
	},

    show_complete_timer: function(timerID) {
        var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Set this timer as complete?</div>"+
            "<div class='popupWindowDesc'>Performing this action will move the timer to the completed list. You may undo this action at any time.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.timers.open.complete_timer("+timerID+")'><img src='images/icons/accept.png' class='icon'/> <span class='dottedLink'>Ok, set as complete.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    complete_timer: function(timerID) {
		new Ajax.Request("php/actions/timer_complete.php", {
			parameters: "timerID="+timerID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Timer set as complete.");
                Global.refresh_content();
            }
		});
	}
}

Page.tasks.view = {
    show_description: function(taskID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupTaskDescription' style='max-width: 500px; max-height: 350px; overflow: auto;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/task_body_get.php", {
            parameters: "taskID="+taskID,
            onSuccess: function(request) {
                $('popupTaskDescription').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    },
        
    show_delete_task: function(taskID, employeeID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this task?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this task from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.tasks.view.delete_task("+taskID+", "+employeeID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_task: function(taskID, employeeID) {
		new Ajax.Request("php/actions/task_delete.php", {
			parameters: "taskID="+taskID+"&employeeID="+employeeID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Task deleted.");
                Global.refresh_content();
            }
		});
	}
}

Page.tasks.edit = {
    init: function() {
        new AjaxForm("editTaskForm", {
            handler: 'php/actions/task_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Task updated.");
            }
        });
    }
}

Page.tasks.assign = {
    init: function() {
        new AjaxForm("assignTaskForm", {
            handler: 'php/actions/task_assign.php',
            onSuccess: function(){Form.reset('assignTaskForm');Global.showSystemMessage("Task assigned.");}
        });
	    if ($('startDateCalendar')) Page.tasks.assign.initCalendar();
    },

    initCalendar: function() {
        var startDateCalendar = new YAHOO.widget.CalendarGroup("startDateCalendarID", "startDateCalendar");
        startDateCalendar.selectEvent.subscribe(Page.tasks.assign.updateStartDate);
        startDateCalendar.render();
    },

    updateStartDate: function(eventType, startDate) {
        $('startDate').value = startDate;
    }
}

Page.tasks.browse = {
    init: function() {
        var yahoo_calendar = new YAHOO.widget.CalendarGroup("startDateCalendarID", "startDateCalendar", {navigator: true});
        new RangeSelector(yahoo_calendar, {
            onSelect: function(startDate, endDate) {
                $('searchResultsContainer').innerHTML = "Loading...";
                new Ajax.Request("php/actions/browse_tasks.php", {
                    parameters: "startDate="+startDate+"&endDate="+endDate,
                    onSuccess: function(request){
                        $('searchResultsContainer').innerHTML = request.responseText;
                    }
                });
            }
        });
    }
}

Page.tasks.view_emp = {
	toggle_task: function(taskID) {
		new Ajax.Request("php/pages/tasks_view_emp.php", {
            parameters: "taskID="+taskID,
            onSuccess: function(request){
                $('taskTablesContainer').innerHTML = request.responseText;
            }
        });
	},

    show_task_description: function(taskID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div class='popupWindowDesc' id='popupTaskDescription' style='max-width: 500px; max-height: 350px; overflow: auto;'>Loading...</div>"+
            "<div style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Done</span></a></div>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/task_body_get.php", {
            parameters: "taskID="+taskID,
            onSuccess: function(request) {
                $('popupTaskDescription').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    }
}

Page.messages.inbox = {
    show_delete_message: function(messageID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this message?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this message from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.messages.inbox.delete_message("+messageID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_message: function(messageID) {
		new Ajax.Request("php/actions/message_delete.php", {
			parameters: "messageID="+messageID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Message deleted.");
                Global.refresh_content();
            }
		});
	},

    show_message: function(messageID) {
		var content =
            "<div class='popupWindow' style='text-align: left'>"+
            "<div id='thePopupMessage'>Loading...</div>"+
            "<table class='trimmed'><tr>"+
                "<td style='padding-right: 50px;'><a onclick=\"FocusedWindow.hide(); Global.loadPage('messages:compose?r="+messageID+"');\"><img src='images/icons/email_edit.png' class='icon'/> <span class='dottedLink'>Reply to this message.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Close message</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
        new Ajax.Request("php/actions/message_get.php", {
            parameters: "messageID="+messageID,
            onSuccess: function(request) {
                $('thePopupMessage').innerHTML = request.responseText;
                FocusedWindow.center();
            }
        });
    }
}

Page.messages.compose = {
    init: function() {
        new AjaxForm("composeMessageForm", {
            handler: 'php/actions/message_compose.php',
            onSuccess: function(){Form.reset('composeMessageForm');Global.showSystemMessage("Message sent.");}
        });
        Event.observe('selectAllCheckbox', 'click', Page.messages.compose.selectAll, false);
    },

    selectAll: function() {
        var selectValue = $F('selectAllCheckbox') ? "checked" : "";
        $$('.userCheckbox').each(function(node) {
            node.checked = selectValue;
        });
    }
}

Page.files.view = {
    show_delete_folder: function(folderID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this folder?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this folder, and all files under it from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.files.view.delete_folder("+folderID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_folder: function(folderID) {
		new Ajax.Request("php/actions/file_folder_delete.php", {
			parameters: "folderID="+folderID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Folder deleted.");
                Global.refresh_content();
            }
		});
	},

    show_delete_file: function(fileID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this file?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this file from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.files.view.delete_file("+fileID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_file: function(fileID) {
		new Ajax.Request("php/actions/file_delete.php", {
			parameters: "fileID="+fileID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("File deleted.");
                Global.refresh_content();
            }
		});
	}
}

Page.files.edit_file = {
    init: function() {
        new AjaxForm("editFileForm", {
            handler: 'php/actions/file_edit.php',
            onSuccess: function(){Global.showSystemMessage("File updated.");}
        });
    }
}

Page.files.edit_folder = {
    init: function() {
        new AjaxForm("editFolderForm", {
            handler: 'php/actions/file_folder_edit.php',
            onSuccess: function(){Global.showSystemMessage("Folder updated.");}
        });
    }
}

Page.files.add_folder = {
    init: function() {
        new AjaxForm("addFolderForm", {
            handler: 'php/actions/file_folder_add.php',
            onSuccess: function(){
                Form.reset('addFolderForm');
                Global.showSystemMessage("Folder added.");
            }
        });
    }
}

Page.projects.view = {
    show_delete_project: function(projectID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Delete this project?</div>"+
            "<div class='popupWindowDesc'>You are about to delete this project from the system. This action cannot be undone.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.projects.view.delete_project("+projectID+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, delete it.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    delete_project: function(projectID) {
		new Ajax.Request("php/actions/project_delete.php", {
			parameters: "projectID="+projectID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Project deleted.");
                Global.refresh_content();
            }
        });
	},

    show_archive_project: function(projectID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Set project as inactive?</div>"+
            "<div class='popupWindowDesc'>This will set the project as inactive. You may undo this action at any time.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.projects.view.archive_project("+projectID+")'><img src='images/icons/lock.png' class='icon'/> <span class='dottedLink'>Ok, set project as inactive.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    archive_project: function(projectID) {
		new Ajax.Request("php/actions/project_archive.php", {
			parameters: "projectID="+projectID,
			onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Project archived.");
                Global.refresh_content();
            }
		});
	},

    show_unarchive_project: function(projectID) {
		var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Set project as active?</div>"+
            "<div class='popupWindowDesc'>This will set the project as active. You may undo this action at any time.</div>"+
            "<table class='trimmed' style='width: 250px;'><tr>"+
                "<td><a onclick='Page.projects.view.unarchive_project("+projectID+")'><img src='images/icons/lock_open.png' class='icon'/> <span class='dottedLink'>Ok, set project as active.</span></a></td>"+
                "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
            "</tr></table>"+
            "</div>";
        FocusedWindow.show(content);
	},

    unarchive_project: function(projectID) {
		new Ajax.Request("php/actions/project_unarchive.php", {
			parameters: "projectID="+projectID,
			onSuccess: function(){
                FocusedWindow.hide();
                Global.showSystemMessage("Project unarchived.");
                Global.refresh_content();
            }
		});
	}
}

Page.projects.add = {
    init: function() {
        new AjaxForm("addProjectForm", {
            handler: 'php/actions/project_add.php',
            onSuccess: function(){
                Form.reset('addProjectForm');
                Global.showSystemMessage("Project added.");
            }
        });
    }
}

Page.projects.edit = {
    init: function() {
        new AjaxForm("editProjectForm", {
            handler: 'php/actions/project_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Project updated.");
            }
        });
    }
}

Page.settings.general = {
    init: function() {
        new AjaxForm("editAccountSettings", {
            handler: 'php/actions/settings_edit.php',
            onSuccess: function(response) {
                Global.showSystemMessage('Settings updated.');
                if (response == "1") {
                    window.location.reload(true);
                }
            }
        });
    }
}

Page.settings.business_info = {
    init: function() {
        new AjaxForm("editBusinessForm", {
            handler: 'php/actions/business_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Business info updated.");
            }
        });
    },

    clearLogo: function() {
        new Ajax.Request("php/actions/business_logo_update.php", {
            parameters: "action=clear",
            onSuccess: function(request){$('companyLogo').innerHTML="";}
        });
    }
}

Page.settings.notifications = {
    init: function() {
        new AjaxForm("edit_notifications_form", {
            handler: 'php/actions/business_notifications_edit.php',
            onSuccess: function() {
                Global.showSystemMessage('Notification settings updated.');
            }
        });
    }
}

Page.settings.clockspots = {
    init: function() {
        new AjaxForm("add_clockspot_form", {
            handler: 'php/actions/clockspot_add.php',
            onSuccess: function(response) {
                if (response) {
                    var stationID = response;
                    Cookie.set('stationID', stationID, 365*10);
                }
                Global.showSystemMessage('Clockspot added.');
                Global.refresh_content();
            }
        });
    },

    update_clockspot_type: function(type) {
        if (type == 1) {
            Element.hide("ip_address");
            Element.hide("phone_number");
        }
        if (type == 2) {
            Element.show("ip_address");
            Element.hide("phone_number");
            $("ip_address").focus();
        }
        if (type == 3) {
            Element.hide("ip_address");
            Element.show("phone_number");
            $("phone_number").focus();
        }
    },

    unsetStation: function() {
        Cookie.unset('stationID');
        Global.showSystemMessage("You have removed this computer as a clockspot.");
        Global.refresh_content();
    },

    delete_clockspot: function(clockspotID) {
        new Ajax.Request("php/actions/clockspot_delete.php", {
			parameters: "clockspotID="+clockspotID,
            onSuccess: function(){
                new Effect.DropOut("clockspot"+clockspotID+"_row");
                Global.showSystemMessage("Clockspot deleted.");
            }
		});
    },

    editClockStation: function(stationID, stationName) {
        stationName = stationName.replace("\"", "&quot;");
        var content =
            "<div class='popupWindow'>"+
            "<div class='popupWindowTitle'>Edit clockspot</div>"+
            "<form id='editStationForm'><div style='text-align: left'>"+
            "<label>Clockspot name</label><br/>"+
            "<input class='formTextInput' type='text' name='stationName' id='edit_stationName' value=\""+stationName+"\"/><br/>"+
            "<input type='hidden' name='stationID' value='"+stationID+"'/>"+
            "<input type='submit' value='Update name &#187;'/>"+
            "</div></form>"+
            "</div>";
        FocusedWindow.show(content);

        new AjaxForm("editStationForm", {
            handler: 'php/actions/clockspot_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Clockspot updated.");
                FocusedWindow.hide();
                Global.refresh_content();
            }
        });
    },

    show_use_blacklist: function(useBlackList) {
        if (useBlackList) {
            var content =
                "<div class='popupWindow'>"+
                "<div class='popupWindowTitle'>Make this list a black list?</div>"+
                "<div class='popupWindowDesc'>You are about to make this list a list of phone numbers that are NOT valid to clock in from.</div>"+
                "<table class='trimmed' style='width: 250px;'><tr>"+
                    "<td><a onclick='Page.settings.clockspots.use_blacklist("+useBlackList+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, make it a black list.</span></a></td>"+
                    "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
                "</tr></table>"+
                "</div>";
        } else {
            var content =
                "<div class='popupWindow'>"+
                "<div class='popupWindowTitle'>Make this list a white list?</div>"+
                "<div class='popupWindowDesc'>You are about to make this list a list of phone numbers that are valid to clock in from.</div>"+
                "<table class='trimmed' style='width: 250px;'><tr>"+
                    "<td><a onclick='Page.settings.clockspots.use_blacklist("+useBlackList+")'><img src='images/icons/bin.png' class='icon'/> <span class='dottedLink'>Ok, make it a white list.</span></a></td>"+
                    "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
                "</tr></table>"+
                "</div>";
        }
        FocusedWindow.show(content);
	},

    use_blacklist: function(useBlackList) {
		new Ajax.Request("php/actions/business_clockspot_blacklist_update.php", {
			parameters: "useBlackList="+useBlackList,
			onSuccess: function() {
                Global.showSystemMessage("List type updated.");
                FocusedWindow.hide();
                Global.refresh_content();
            }
		});
	}
}

Page.settings.my_subscription = {
    init: function() {
        new AjaxForm("change_plan_form", {
            handler: 'php/actions/business_change_plan.php',
            onSuccess: function() {
                Global.showSystemMessage("You have updated your plan.");
                Global.refresh_content();
            }
        });
        
        if ($('activateAccountForm')) {
            new AjaxForm('activateAccountForm', {
                handler: 'php/actions/account_activate.php',
                onSuccess: function(){
                    Global.refresh_content();
                    Global.showSystemMessage('Account activated!');
                }
            });
        }
    },

    toggleActivateButton: function() {
        if ($('activateSubmit').disabled=='') {
            $('activateSubmit').disabled = 'disabled';
        } else {
            $('activateSubmit').disabled = '';
        }
    },

    show_cancel_account: function() {
        var content =
        "<div class='popupWindow'>"+
        "<div class='popupWindowTitle'>Inactivate your account?</div>"+
        "<div class='popupWindowDesc'>This will remove your credit card information from the system. You will no longer be able to use Clockspot after the next payment date. However, you may reactivate your account at any time by coming back to this page.</div>"+
        "<table class='trimmed' style='width: 250px;'><tr>"+
            "<td><a onclick='Page.settings.my_subscription.cancel_account()'><img src='images/icons/delete.png' class='icon'/> <span class='dottedLink'>Ok, inactivate my account.</span></a></td>"+
            "<td style='text-align: right;'><a onclick='FocusedWindow.hide()'><img src='images/icons/arrow_right.png' class='icon'/> <span class='dottedLink'>Cancel</span></a></td>"+
        "</tr></table>"+
        "</div>";
        FocusedWindow.show(content);
    },

    cancel_account: function() {
        new Ajax.Request("php/actions/account_cancel.php", {
            onSuccess: function() {
                FocusedWindow.hide();
                Global.showSystemMessage("Account inactivated.");
                Global.refresh_content();
            }
        });
    },

    toggle_change_plan_agree: function() {
        if ($('change_plan_form_submit').disabled=='') {
            $('change_plan_form_submit').disabled = 'disabled';
        } else {
            $('change_plan_form_submit').disabled = '';
        }
    },
    
    toggle_cancel_special_rate_submit: function() {
        if ($('cancel_special_rate_submit').disabled=='') {
            $('cancel_special_rate_submit').disabled = 'disabled';
        } else {
            $('cancel_special_rate_submit').disabled = '';
        }
    },

    cancel_special_rate: function() {
        new Ajax.Request("php/actions/special_rate_cancel.php", {
            onSuccess: function() {
                Global.showSystemMessage("You have canceled your special rate.");
                Global.refresh_content({scroll: true});
            }
        });
    }
}

Page.settings.my_subscription_edit_credit_card = {
    init: function() {
        if ($('editCreditCardForm')) {
            new AjaxForm('editCreditCardForm', {
                handler: 'php/actions/credit_card_edit.php',
                onSuccess: function(){
                    Global.refresh_content();
                    Global.showSystemMessage('Credit card updated.');
                }
            });
        }
    },

    toggleActivateButton: function() {
        if ($('activateSubmit').disabled=='') {
            $('activateSubmit').disabled = 'disabled';
        } else {
            $('activateSubmit').disabled = '';
        }
    }
}

Page.settings.my_info = {
    init: function() {
        new AjaxForm("edit_form_basic", {
            handler: 'php/actions/user_edit_emp.php?s=basic',
            onSuccess: function(){Global.showSystemMessage("Basic information updated.");}
        });
        new AjaxForm("edit_form_password", {
            handler: 'php/actions/user_edit_emp.php?s=password',
            onSuccess: function(){Form.reset('edit_form_password');Global.showSystemMessage("Password updated.");}
        });
    },

    clearProfilePicture: function(employeeID) {
        new Ajax.Request("php/actions/user_picture_update.php", {
            parameters: "userID="+employeeID+"&action=clear",
            onSuccess: function(request){$('profilePicture').innerHTML="";}
        });
    }
}

Page.timeclock.today = {
    init: function() {
		if ($('dailyReportForm')) {
            new AjaxForm('dailyReportForm', {
                handler: 'php/actions/clock_time_daily_report_update.php',
                onSuccess: function(){Global.showSystemMessage("Shift report saved.");}
            });
		}
		if ($('projectForm')) {
            new AjaxForm('projectForm', {
                handler: 'php/actions/clock_time_project_update.php',
                onSuccess: function(){Global.showSystemMessage("Project updated.");}
            });
		}
		if ($('jobCodeForm')) {
            new AjaxForm('jobCodeForm', {
                handler: 'php/actions/clock_time_job_code_update.php',
                onSuccess: function(){Global.showSystemMessage("Job code updated.");}
            });
		}
		if ($('mileageForm')) {
            new AjaxForm('mileageForm', {
                handler: 'php/actions/clock_time_mileage_update.php',
                onSuccess: function(){Global.showSystemMessage("Mileage updated.");}
            });
		}
	},

	updateStatus: function() {
	    new Ajax.Request("php/actions/user_status_update.php", {
            parameters: Form.serialize('statusForm'),
            onSuccess: function(request){
                $('statusForm').innerHTML = request.responseText;
            }
        });
	},

	clockInOut: function() {
        new Ajax.Request("php/actions/clockInOut.php?type=browser", {
            onSuccess: function(request) {
                if (request.responseText=="error_shift_report_required") {
                    $('clock_out_button').disabled = "";
                    Global.showStatusMessage("You must write a shift report before clocking out.", {
                        backgroundColor: "#ffb0b0",
                        color: "#c70000"
                    });
                } else {
                    Global.refresh_content();
                }
            }
        });
        Form.disable('timeclockForm');
	}
}

Page.timeclock.my_timesheet_browse = {
    init: function() {
        var yahoo_calendar = new YAHOO.widget.CalendarGroup("startDateCalendarID", "startDateCalendar", {navigator: true});
        new RangeSelector(yahoo_calendar, {
            onSelect: function(startDate, endDate) {
                $('searchResultsContainer').innerHTML = "Loading...";
                new Ajax.Request("php/actions/browse_my_timesheet.php", {
                    parameters: "startDate="+startDate+"&endDate="+endDate,
                    onSuccess: function(request){
                        $('searchResultsContainer').innerHTML = request.responseText;
                    }
                });
            }
        });
    }
}

Page.timeclock.edit_timesheet = {
    _init: function(startDate, endDate, pageDateIn, pageDateOut, inDateValue, outDateValue) {
        $('inDate').value = inDateValue;
        $('outDate').value = outDateValue;
        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();
        var calendarIn = new YAHOO.widget.Calendar("clockInCalendarID", "clockInCalendar", {
            pagedate: pageDateIn,
            selected: startDate,
            maxdate: todaysDate
        });
        calendarIn.selectEvent.subscribe(Page.timeclock.edit_timesheet.updateInDate);
	    calendarIn.render();

	    var calendarOut = new YAHOO.widget.Calendar("clockOutCalendarID", "clockOutCalendar", {
	        pagedate: pageDateOut,
	        selected: endDate,
	        maxdate: todaysDate
        });
        calendarOut.selectEvent.subscribe(Page.timeclock.edit_timesheet.updateOutDate);
	    calendarOut.render();

        new AjaxForm("editClockTimeForm", {
            handler: 'php/actions/time_edit_request_add.php',
            onSuccess: function(){Global.showSystemMessage("Edit request sent.");}
        });

        if ($('dontSetClockOut').checked) {
            Page.timeclock.edit_timesheet.disableClockOut();
        }
    },

    updateInDate: function(eventType, inDate) {
        $('inDate').value = inDate;
    },

    updateOutDate: function(eventType, outDate) {
        $('outDate').value = outDate;
    },

    disableClockOut: function() {
        if ($F('dontSetClockOut')) {
            $('outHour').disabled = "disabled";
            $('outMinute').disabled = "disabled";
            $('outAmPm').disabled = "disabled";
            Element.setOpacity('clockOutCalendar', 0.3);
        } else {
            $('outHour').disabled = "";
            $('outMinute').disabled = "";
            $('outAmPm').disabled = "";
            Element.setOpacity('clockOutCalendar', 1.0);
        }
    }
}

Page.timeclock.my_schedule = {
    init: function() {
        var c = new YAHOO.widget.Calendar("schedule_calendar_id", "schedule_calendar", {navigator: true});
        c.selectEvent.subscribe(function(eventType, theDate) {
            //Update schedule date range
            var sqlDate = Util.yui_to_sql_date(theDate);
            Schedule.changeDate(sqlDate);

            //Render calendar to show date range
            var start = Util.to_javascript_date(sqlDate);
            var end = Util.to_javascript_date(sqlDate);
            end.setDate(end.getDate()+6);

            var range = (start.getMonth()+1)+"/"+start.getDate()+"/"+start.getFullYear()+"-"+(end.getMonth()+1)+"/"+end.getDate()+"/"+end.getFullYear();
            c.cfg.setProperty("selected",range,false);
            c.render();
        });

        //Render the calendar
        var start = new Date();
        var end = new Date();
        end.setDate(end.getDate()+6);
        var range = (start.getMonth()+1)+"/"+start.getDate()+"/"+start.getFullYear()+"-"+(end.getMonth()+1)+"/"+end.getDate()+"/"+end.getFullYear();
        c.cfg.setProperty("selected",range,false);
        c.render();
    }
}

Page.timeclock.absence_request = {
    _calendar: null,

    init: function() {
        var d = new Date();
        var todaysDate = (d.getMonth()+1)+"/"+d.getDate()+"/"+d.getFullYear();

        Page.timeclock.absence_request._calendar = new YAHOO.widget.CalendarGroup("offDateCalendarID", "offDateCalendar", {
            MULTI_SELECT: true
        });
        Page.timeclock.absence_request._calendar.selectEvent.subscribe(Page.timeclock.absence_request.updateSelected);
        Page.timeclock.absence_request._calendar.deselectEvent.subscribe(Page.timeclock.absence_request.updateSelected);
	    Page.timeclock.absence_request._calendar.render();


	    new AjaxForm("absenceRequestForm", {
            handler: 'php/actions/absence_request_add.php',
            onSuccess: function() {
                Global.showSystemMessage("Absence request sent.");
                Form.reset("absenceRequestForm");
                Page.timeclock.absence_request._calendar.deselectAll();
                Page.timeclock.absence_request._calendar.render();
            }
        });
    },

    updateSelected: function() {
        var dates = Page.timeclock.absence_request._calendar.getSelectedDates();
        var dateList = [];
        for (var i = 0; i < dates.length; i++) {
            var date = dates[i];
            var dateString = date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate();
            dateList.push(dateString);
        }
        $('dateList').value = dateList.join(",");
    }
}

Page.support.search = {
    init: function() {
        $('token').focus();
        Event.observe('token', 'keydown', function(e) {
            if (e.keyCode==13) { //If pressed enter
                Event.stop(e);
                if ($('token').value) {
                    Page.support.search.submit_search();
                    $('token').value = "";
                }
            }
        }, false);
    },

    submit_search: function() {
        $('search_results').innerHTML = "<div class='searching_text'>Searching \""+$F('token')+"\"...</div>";

        new Ajax.Request("php/actions/support_search.php", {
            parameters: Form.serialize('search_customer_form'),
            onSuccess: function(request) {
                $('search_results').innerHTML = request.responseText;
            }
        });
    },

    save_note: function(businessID) {
        var note = $F('note_'+businessID);
        new Ajax.Request("php/actions/support_save_note.php", {
            parameters: "businessID="+businessID+"&note="+escape(note),
            onSuccess: function(request) {
                Global.showSystemMessage("Note saved.");
            }
        });
    }
}

Page.support.edit = {
    init: function() {
        new AjaxForm("edit_business_form", {
            handler: 'php/actions/support_edit.php',
            onSuccess: function(){
                Global.showSystemMessage("Business updated.");
            }
        });
    }
}

Page.support.tools = {
    email_all_customers: function() {
        $('email_all_submit').disabled = "disabled";
        $('email_all_submit').value = "Emailing...";
        new Ajax.Request("php/actions/support_tools.php?action=email_all_customers", {
            parameters: "subject="+escape($F('email_all_subject'))+"&body="+escape($F('email_all_body')),
            onSuccess: function(request){
                $('email_all_submit').value = "Sent!";
                alert(request.responseText);
            }
        });
    },

    edit_payment_method: function(paymentID, paymentMethod) {
        new Ajax.Request("php/actions/support_tools.php?action=edit_payment_method", {
            parameters: "paymentID="+paymentID+"&paymentMethod="+paymentMethod,
            onSuccess: function(request){
                Element.remove("subscriptionPayment"+paymentID+"_row");
            }
        });
    }
}/**
 * Creates a popup window with various display options.
 * For singleton windows, specify an elementID
 *
 * Author: Jason Ho
 */
var PopupWindow = Class.create();

PopupWindow.prototype = {
    element: null,
    selectedNode: null,

    initialize: function(content, options) {
        this.displayOptions = {};
        this.options = Object.extend({
            hideOnEsc: false,
            hideOnBlur: false,
            hideOnBlurContainer: null, //Lets you hide on blur if mouse clicked outside of container. Defaults to this.element.
            container: document.body
        }, options || {});
        this.options.container = $(this.options.container);

        //Initialize the element
        if (typeof content == "string") {
            this.element = document.createElement("div");
            this.element.innerHTML = content;
        } else {
            this.element = content;
        }
        if (this.options.elementID) {
            if ($(this.options.elementID)) Element.remove(this.options.elementID);
            this.element.id = this.options.elementID;
        }
        this.element.style.display = "none";
        this.element.style.position = "absolute";
        this.element.style.zIndex = "1000";
        this.options.container.appendChild(this.element);

        //Create a draggable handle
        if (this.options.handle) {
            new Draggable(this.element, { handle: this.options.handle });
        }

        //Attach event listeners
        if (this.options.hideOnEsc) {
            Event.observe(document, "keypress", this._hideOnEsc.bind(this), false);
        }
        if (this.options.hideOnBlur) {
            Event.observe(document.body, "mouseup", this._hideOnBlur.bind(this), false);
        }
    },

    show: function(displayOptions) {
        this.displayOptions = Object.extend({
            displayEffect: "none",
            dimSurroundings: false
        }, displayOptions || {});

        if (this.displayOptions.dimSurroundings) {
            this.dimSurroundings();
        }
        
        //Display the element
        if (this.displayOptions.displayEffect=="appear") {
            new Effect.Appear(this.element, {duration: 0.6});
        } else {
            Element.show(this.element);
        }

        //Trigger the event listener
        if (this.options.onShow) this.options.onShow();
    },

    hide: function() {
        if (this._dimElement) Element.remove(this._dimElement);
        if (this.displayOptions.selectedClass) {
            Element.removeClassName(this.selectedNode, this.displayOptions.selectedClass);
        }
        Element.hide(this.element);
    },

    showCentered: function(displayOptions) {
        this.center();
        this.show(displayOptions);
    },

    showBesideNode: function(selectedNode, displayOptions) {
        this.displayOptions = Object.extend({
            valign: "top",
            halign: "right",
            xoffset: 0,
            yoffset: 0
        }, displayOptions || {});

        selectedNode = $(selectedNode);
        if (this.displayOptions.selectedClass) {
            Element.removeClassName(this.selectedNode, this.displayOptions.selectedClass);
            Element.addClassName(selectedNode, this.displayOptions.selectedClass);
        }
        this.selectedNode = selectedNode;
        
        //Figure out left and top
        var selectedDim = Element.getDimensions(selectedNode);
        var selectedOffset = Position.cumulativeOffset(selectedNode);
        var left = (selectedOffset[0]+this.displayOptions.xoffset);
        if (this.displayOptions.halign=="right") left += selectedDim.width;
        var top = (selectedOffset[1]+this.displayOptions.yoffset);
        if (this.displayOptions.valign=="bottom") top += selectedDim.height;

        //Position window next to the selected node
        this.element.style.left = left+"px";
        this.element.style.top = top+"px";

        this.show(displayOptions);
    },

    center: function() {
        var dim = Element.getDimensions(this.element);
        var scrollX = window.pageXOffset ? window.pageXOffset : document.documentElement.scrollLeft;
        var scrollY = window.pageYOffset ? window.pageYOffset : document.documentElement.scrollTop;

        var top = scrollY+(document.documentElement.clientHeight-dim.height)/2;
        var left = scrollX+(document.documentElement.clientWidth-dim.width)/2;

        //Make sure coordinates don't go out of bounds
        if (top < 0) top = 0;

        this.element.style.top = top+"px";
        this.element.style.left = left+"px";
    },

    dimSurroundings: function() {
        var div = document.createElement("div");
        div.style.position = "fixed";
        div.style.top = "0px";
        div.style.left = "0px";
        div.style.width = "100%";
        div.style.height = "100%";
        div.style.backgroundColor = "black";
        Element.setOpacity(div, "0.6");
        document.body.appendChild(div);
        this._dimElement = div;
    },

    _hideOnEsc: function(e) {
        if (e.keyCode==Event.KEY_ESC) {
            this.hide();
        }
    },

    _hideOnBlur: function(e) {
        var container = this.options.hideOnBlurContainer ? $(this.options.hideOnBlurContainer) : this.element;
        Position.prepare();
        if (this.element && !Position.within(container, Event.pointerX(e), Event.pointerY(e))) {
            this.hide();
        }
    }
}/**
 * Author: Jason Ho
 */

var DropDownMenu = Class.create();

DropDownMenu.prototype = {
    element: null,
    options_element: null,
    _popup: null,
    current_value: null,
    
    initialize: function(element, options_element, options) {
        this.options = Object.extend({
            updateSelector: true,
            selectedClass: "",
            container: document.body
        }, options || {});
        this.element = $(element);
        this.options_element = $(options_element);

        this._popup = new PopupWindow(this.options_element, {
            hideOnEsc: true,
            hideOnBlur: true,
            container: this.options.container
        });

        Event.observe(this.element, "click", this._show_dropdown.bind(this), false);
        Event.observe(this.options_element, "click", this._select_option.bind(this), false);
    },

    _show_dropdown: function() {
        this._popup.showBesideNode(this.element, {
            halign: "left",
            valign: "bottom",
            selectedClass: this.options.selectedClass
        });
    },
    
    _select_option: function(e) {
        this._popup.hide();
        var option_element = Event.findElement(e, "a");
        if (this.options.updateSelector) {
            this.element.innerHTML = option_element.innerHTML;
        }
        this.current_value = option_element.getAttribute("value");
        if (this.current_value == null) this.current_value = "";
        if (this.options.onSelect) {
            this.options.onSelect.call(this, this.current_value);
        }
    }
}/**
 * Continuously polls the comet server for information. Once a response
 * is returned, dispatches the information to the appropriate handler.
 *
 * Author: Jason Ho
 */
var CometUpdater = {
	_serverURL: "http://www.clockspot.com:8080/",
	_handlers: [],

	startPoll: function() {
	 	CometUpdater._isPolling = true;
		CometUpdater._checkUpdates();
	},

	stopPoll: function() {
	 	CometUpdater._isPolling = false;
	},

	_checkUpdates: function() {
		//new Insertion.Top(document.body, 'poll...');
	 	CometUpdater.sendRequest("checkUpdates", {
	 		callback: "CometUpdater._dispatchUpdates",
	 		parameters: "userID="+Global.userID
		});
	},

	_dispatchUpdates: function(jsonData) {
		for (var channel in jsonData) {
			var handler = CometUpdater._handlers[channel];
			if (handler) {
				handler(jsonData[channel]);
			}
		}
		if (CometUpdater._isPolling) {
			CometUpdater._checkUpdates(); //Loop
		}
	},

	setHandler: function(channel, handler) {
		CometUpdater._handlers[channel] = handler;
	},

	//Options: parameters, callback
	sendRequest: function(command, options) {
		var params = "command="+command;
		if (options.callback) params += "&callback="+options.callback;
		if (options.parameters) params += "&"+options.parameters;
		JsonRequest.send(CometUpdater._serverURL, {
			parameters: params,
			onError: options.onError,
			onSuccess: options.onSuccess
		});
	}
}/**
 * Creates a draggable window frame with a close and minimize button.
 * Any DOM element can be added to the content area to be displayed
 * in the window.
 *
 * Author: Jason Ho
 */

var QaboomWindow = Class.create();

Object.extend(QaboomWindow, {
	_lastZIndex: 2
});

QaboomWindow.prototype = {

	//Options: windowID, title, onClose
	initialize: function(contentNode, options) {
	 	var windowID = options.windowID;
	 	var title = options.title;
	 	//Create the window
		var windowHTML = 
			"<table class='qaboomWindow' id='"+windowID+"'>" + 
			"<tr>" +
				"<td class='trimmed'>" +  
					"<table class='trimmed'>" + 
					"<tr class='qaboomTitleBar' id='"+windowID+"TitleBar'>" + 
					"<td class='qaboomTitleBarText' style='padding: 2px 0px;'>"+title+"</td>" + 
					"<td><a><img src='images/shadeChat.gif' alt='shade' id='"+windowID+"ShadeButton'/></a></td>" +
					"<td><a><img src='images/closeChat.gif' alt='close' id='"+windowID+"CloseButton'/></a></td>" + 
					"</tr>" + 
					"</table>" + 
				"</td>" + 
			"</tr>" + 
			"<tr>" + 
				"<td class='trimmed' id='"+windowID+"Content'>" + 
				"</td>" + 
			"</tr>" + 
			"</table>";
		new Insertion.Bottom($('qaboomWindows'), windowHTML);
		//Set references
		this.options = options;
		this.window = $(windowID);
		Event.observe(this.window, 'click', this._updateZIndex.bind(this), false);
		this.window.style.zIndex = 1; //Windows are rendered above the site
		this.content = $(windowID+'Content');
		//Add content to the window
		this.content.appendChild(contentNode);
		var dim = Element.getDimensions(this.window);
		Element.setStyle(this.window, {width: dim.width+"px"}); //Update window width
		//Load previous window state
		this._loadState();
		//Register events
		var titleBar = $(windowID+'TitleBar');
		var shadeButton = $(windowID+'ShadeButton');
		var closeButton = $(windowID+'CloseButton');
		Event.observe(shadeButton, 'click', this.shade.bind(this), false);
		Event.observe(closeButton, 'click', this.close.bind(this), false);
		new Draggable(this.window, {
			handle: titleBar,
			onEnd: this._saveState.bind(this)
		});
	},

	_updateZIndex: function() {
		this.window.style.zIndex = QaboomWindow._lastZIndex;
		QaboomWindow._lastZIndex++;
		if (QaboomWindow._lastZIndex >= 1000) {
			QaboomWindow._lastZIndex = 2;
		}
		Cookie.set("lastActiveWindow", this.window.id);
		//TODO: get rid of zIndex going to 1000. Remember every window order.
	},
	
	_saveState: function() {
		if (this.options.onEnd) this.options.onEnd();
		Cookie.set(this.window.id, this.window.style.top+","+this.window.style.left+","+this.shaded);
	},
	
	_loadState: function() {
		var windowState = Cookie.get(this.window.id);
		if (Cookie.get("lastActiveWindow") == this.window.id) {
			this.window.style.zIndex = 2;
			QaboomWindow._lastZIndex = 3;
		}
		if (windowState == null) {
			this._placeBottomRight();
			this._saveState();
		} else {
			var attributes = windowState.split(",");
			var top = attributes[0];
			var left = attributes[1];
			this.shaded = attributes[2]=="true";
			Element.setStyle(this.window, {top: top, left: left});
			if (this.shaded) {
				Element.hide(this.content);
			}
		}
	},
	
	shade: function() {
		if (Element.visible(this.content)) {
			new Effect.Fade(this.content, {duration: 0.3});
			this.shaded = true;
		} else {
			new Effect.Appear(this.content, {duration: 0.3});
			this.shaded = false;
		}
		this._saveState();
	},
	
	close: function() {
	 	if (this.options.onClose) {
	 		this.options.onClose.call();
	 	}
        Element.remove(this.window);
	 	Cookie.unset(this.window.id);
    },

	_placeBottomRight: function() {
		var dim = Element.getDimensions(this.window);
		var newLeft = document.documentElement.clientWidth-dim.width-Math.round(70*Math.random())-300;
		var newTop = document.documentElement.clientHeight-dim.height-Math.round(70*Math.random());
		Element.setStyle(this.window, {left: newLeft+"px", top: newTop+"px"});
	}
}/**
 * Handles saving chat window state, opening existing chat windows,
 * and dispatching any messages from the comet server.
 *
 * Author: Jason Ho
 */

var ChatManager = {
    windows: [],
	
	openChat: function(userID, title) {
		if (!$(userID+'Chat')) {
			ChatManager.windows[userID] = new ChatWindow(userID, title);
			ChatManager._saveChat(userID, title);
			return true;
		} else {
			return false;
		}
	},

	loadChatWindows: function() {
		var openChats = Cookie.get('openChats');
		var openChatTitles = Cookie.get('openChatTitles');
		if (openChats != null && openChats != '') {
			openChats = openChats.split(',');
			openChatTitles = openChatTitles.split(",");
			for (var i = 0; i < openChats.length; i++) {
				ChatManager.windows[openChats[i]] = new ChatWindow(openChats[i], openChatTitles[i]);
			}
		}
		//Get chat history
		CometUpdater.sendRequest("getChatHistory", {
			callback: "ChatManager._loadHistory",
			parameters: "userID="+Global.userID
		});
	},

	_saveChat: function(chatee, title) {
		var openChats = Cookie.get('openChats');
		var openChatTitles = Cookie.get('openChatTitles');
		if (openChats == null || openChats == '') {
			Cookie.set('openChats', chatee);
			Cookie.set('openChatTitles', title);
        } else {
			Cookie.set('openChats', openChats+","+chatee);
			Cookie.set('openChatTitles', openChatTitles+","+title);
        }
    },

	 _deleteChat: function(chatee) {
		var openChats = Cookie.get('openChats');
		var openChatTitles = Cookie.get('openChatTitles');

		if (openChats==chatee) {
			Cookie.set('openChats', '');
			Cookie.set('openChatTitles', '');
		} else if (openChats.indexOf(","+chatee+",") != -1) {
			var arrayPos = openChats.split(",").indexOf(chatee);
			var titleArray = openChatTitles.split(",");
			titleArray.splice(arrayPos, 1);
			Cookie.set('openChatTitles', titleArray.join(","));
			Cookie.set('openChats', openChats.replace(","+chatee+",", ","));
		} else if (openChats.indexOf(chatee+",") == 0) {
			Cookie.set('openChats', openChats.substr(1+openChats.indexOf(",")));
			Cookie.set('openChatTitles', openChatTitles.substr(1+openChatTitles.indexOf(",")));
		} else if (openChats.lastIndexOf(","+chatee) == openChats.length-chatee.length-1) {
			Cookie.set('openChats', openChats.substr(0, openChats.lastIndexOf(",")));
			Cookie.set('openChatTitles', openChatTitles.substr(0, openChatTitles.lastIndexOf(",")));
		}
	},

	_loadHistory: function(jsonData) {
		ChatManager.loadMessages(jsonData.chatHistory, true);
	},

	loadMessages: function(messages, isHistory) {
        //Play sound
        if (ChatWindow.enableChatSounds && !isHistory) {
            soundManager.play('imReceive');
        }

        //Load messages
        messages.each(function(rawMessage) {
			var chatee, talker;
			chatee = rawMessage[0];
			var name = rawMessage[1];
			var message = rawMessage[2];
			var isSystem = rawMessage[3];
			var className = "chatee";
			if (chatee.charAt(0) == "_") {
				className = "chatter";
				chatee = chatee.substring(1);
				name = "me";
				//talker = Global.userID;
			}
            ChatManager.openChat(chatee, name)
			
            var chatArea = $(chatee+"ChatArea");
			var formattedMessage;
			if (isSystem) {
				if (message.indexOf("sketch:") == 0) {
					Element.show(chatee+"SketchpadContainer");
					var attributes = message.split(":");
					var color = attributes[1];
					var points = attributes[2];
					ChatManager.windows[chatee].sketchpad.drawPolyLine(points, color);
					return;
				} else if (message == "clearSketch") {
					ChatManager.windows[chatee].sketchpad.clearSketchOnly();
					message = name == "me" ? "You cleared the sketchpad." : name+" cleared the sketchpad.";
				}
				formattedMessage = document.createElement("div");
				formattedMessage.className = "systemMessage";
				formattedMessage.innerHTML = message;
			} else {
				formattedMessage = document.createElement("div");
				formattedMessage.innerHTML = "<span class='"+className+"'>" + name + ": </span>" + ChatWindow.formatMessage(message);

				//TEMPORARY
				if (message == "ping" && chatee==1) {
					CometUpdater.sendRequest("sendSketch", {
						parameters: "from="+Global.userID+"&to=1"+
							"&color=rgb(255,0,0)&points=10,10,100,100"
					});
				}

			}
			chatArea.appendChild(formattedMessage);
			chatArea.scrollTop = chatArea.scrollHeight-chatArea.clientHeight;

			//document.focusNode.innerHTML
			if (!Global.hasFocus && !TitleOscillator.isRunning && Global.userID != chatee) {
				TitleOscillator.start(document.title, name+" says...", 2500);
				Event.observe(window, "focus", function(){TitleOscillator.stop();}, false);				
			}

		});
	}
}/**
 * Creates a chat form wrapped in a QaboomWindow.
 * Handles sending messages to the server.
 *
 * Author: Jason Ho
 */

var ChatWindow = Class.create();

Object.extend(ChatWindow, {
    enableChatSounds: false,
        
    formatMessage: function(message) {
		message = message.replace(/:\)/g, "<img style='vertical-align: top;' src='images/emoticons/smile.gif'/>");
		message = message.replace(/:-\)/g, "<img style='vertical-align: top;' src='images/emoticons/smile.gif'/>");

		message = message.replace(/:\(/g, "<img style='vertical-align: top;' src='images/emoticons/frown.gif'/>");
		message = message.replace(/:-\(/g, "<img style='vertical-align: top;' src='images/emoticons/frown.gif'/>");

		message = message.replace(/:D/g, "<img style='vertical-align: top;' src='images/emoticons/laugh.gif'/>");
		message = message.replace(/:-D/g, "<img style='vertical-align: top;' src='images/emoticons/laugh.gif'/>");

		message = message.replace(/:p/gi, "<img style='vertical-align: top;' src='images/emoticons/tongue.gif'/>");
		message = message.replace(/:-p/gi, "<img style='vertical-align: top;' src='images/emoticons/tongue.gif'/>");

		message = message.replace(/;-\)/g, "<img style='vertical-align: top;' src='images/emoticons/wink.gif'/>");
		message = message.replace(/;\)/g, "<img style='vertical-align: top;' src='images/emoticons/wink.gif'/>");
		
		message = message.replace(/:@/g, "<img style='vertical-align: top;' src='images/emoticons/angry.gif'/>");
		message = message.replace(/:-@/g, "<img style='vertical-align: top;' src='images/emoticons/angry.gif'/>");

		message = message.replace(/:\|/g, "<img style='vertical-align: top;' src='images/emoticons/footInMouth.gif'/>");
		message = message.replace(/:-\|/g, "<img style='vertical-align: top;' src='images/emoticons/footInMouth.gif'/>");

		message = message.replace(/:\$/g, "<img style='vertical-align: top;' src='images/emoticons/embarassed.gif'/>");
		message = message.replace(/:-\$/g, "<img style='vertical-align: top;' src='images/emoticons/embarassed.gif'/>");

		message = message.replace(/:o/gi, "<img style='vertical-align: top;' src='images/emoticons/surprised.gif'/>");
		message = message.replace(/:-o/gi, "<img style='vertical-align: top;' src='images/emoticons/surprised.gif'/>");

		message = message.replace(/B\)/g, "<img style='vertical-align: top;' src='images/emoticons/cool.gif'/>");
		message = message.replace(/B-\)/g, "<img style='vertical-align: top;' src='images/emoticons/cool.gif'/>");
		message = message.replace(/8\)/g, "<img style='vertical-align: top;' src='images/emoticons/cool.gif'/>");
		message = message.replace(/8-\)/g, "<img style='vertical-align: top;' src='images/emoticons/cool.gif'/>");

        message = message.replace(/<3/g, "<img style='vertical-align: top;' src='images/emoticons/heart.gif'/>");
        message = message.replace(/&lt;3/g, "<img style='vertical-align: top;' src='images/emoticons/heart.gif'/>");
		message = message.replace(/&#60;3/g, "<img style='vertical-align: top;' src='images/emoticons/heart.gif'/>");

		message = message.replace(/(^|[\n ])([\w]+?:\/\/[\w#$%&~\/.\-;:=,?@\[\]+]*)/i, "$1<a href=\"$2\" target=\"_blank\">$2</a>");

		return message;
	}
});


ChatWindow.prototype = {
	initialize: function(chateeID, title) {
		//Create the chat form
		var formNode = document.createElement("form");
        Element.setStyle(formNode, {display: "inline"});
        formNode.setAttribute('id', chateeID+'ChatForm');
		formNode.innerHTML =
			"<div class='chatArea' id='"+chateeID+"ChatArea'></div>" +
			"<input type='hidden' name='to' value='"+chateeID+"'/>" +
			"<input autocomplete='off' type='text' size='45' name='message' id='"+chateeID+"Message'/>"+
			"<a><img onclick=\"Element.toggle('"+chateeID+"SketchpadContainer');\" style='position: absolute; margin-left: -20px; margin-top: 3px;' src='images/sketchpadMedium.gif'/></a>";			

		var table = document.createElement("table");
		var row = table.insertRow(-1);
		var td1 = row.insertCell(-1);
		var td2 = row.insertCell(-1);
		table.className = "trimmed";
		td1.className = "trimmed";
		td2.className = "trimmed";
		td2.id = chateeID+"SketchpadContainer";
		td2.style.borderLeft = "1px solid #9C859C";
		td2.style.display = "none";

		this.sketchpad =
			new Sketchpad(chateeID+"Sketchpad", {
				width: 300,
				height: 204,
				onEndStroke: this._sendSketch.bind(this),
				onClearSketch: this._clearSketch.bind(this)
			});


		td1.appendChild(formNode);
		td2.appendChild(this.sketchpad.elementNode);
		
		//Wrap chat form in a window
		new QaboomWindow(table, {
			windowID: chateeID+"Chat",
			title: title,
			onClose: this._onClose.bind(this),
			onEnd: this._adjustScroll.bind(this)
		});

		//Set references
		this.chateeID = chateeID;
		this.chatForm = $(chateeID+'ChatForm');
	 	this.chatArea = $(chateeID+"ChatArea");
	 	this.messageField = $(chateeID+"Message");
	 	Field.focus(this.messageField);
	 	//Register Events
		Droppables.add(this.chatForm, {
			accept: 'questionHandle',
			hoverclass: 'hoverChatWindow',
			onDrop: this._attachQuestion.bind(this)
		});
		//Event.observe(this.messageField, 'focus', function(){TitleOscillator.stop(); alert("HI");}, false);
		Event.observe(this.chatForm, 'submit', this.submitMessage.bind(this), false);
	},

	_sendSketch: function() {
		CometUpdater.sendRequest("sendSketch", {
			parameters: "from="+Global.userID+"&to="+this.chateeID+
				"&color="+escape(this.sketchpad.color)+"&points="+this.sketchpad.points.toString()
		});
    },

    _clearSketch: function() {
    	CometUpdater.sendRequest("clearSketch", {
			parameters: "from="+Global.userID+"&to="+this.chateeID
		});
		var div = document.createElement("div");
		Element.addClassName(div, "systemMessage");
		div.innerHTML = "You cleared the sketchpad.";
		this.chatArea.appendChild(div);
		//this.chatArea.innerHTML += "<div class='systemMessage'>You cleared the sketchpad.</div>";
		this._adjustScroll();
    },

	_adjustScroll: function() {
		if (this.chatArea && this.messageField) {
			this.chatArea.scrollTop = this.chatArea.scrollHeight-this.chatArea.clientHeight;
			Field.focus(this.messageField);
		}
	},

	_attachQuestion: function(question) {
		new Effect.Highlight(this.chatArea);
		var href = question.getAttribute("href");
		var message = Global.userName+" has linked a question:<br><a href='"+href+"'>"+question.innerHTML+"</a>";
		this.chatArea.innerHTML += "<div class='systemMessage'>"+message+"</div>";
		this._adjustScroll();
	 	CometUpdater.sendRequest("sendSystem", {
	 		parameters: "from="+Global.userID+"&to="+this.chateeID+"&message="+message
		});
	},
	
	submitMessage: function(event) {
		Event.stop(event);
	 	var message = $F(this.messageField);
	 	if (message == "") return;
	 	message = message.escapeHTML();
	 	//Append the chatter's message to the window
		var div = document.createElement("div");
		div.innerHTML = "<span class='chatter'>me:</span> "+ChatWindow.formatMessage(message);
		this.chatArea.appendChild(div);

        //Play sound
        if (ChatWindow.enableChatSounds) {
            soundManager.play('imSend');
        }

        //Send the message
	 	CometUpdater.sendRequest("sendChat", {
	 		parameters: "from="+Global.userID+"&"+Form.serialize(this.chatForm)
		});
		//Reset
		Field.clear(this.messageField);
		this._adjustScroll();
    },
	
	_onClose: function() {
	 	CometUpdater.sendRequest("clearChatHistory", {
	 		parameters: "userID="+Global.userID+"&chateeID="+this.chateeID
		});
		ChatManager._deleteChat(this.chateeID);
	}
}
var TitleOscillator = {
	isRunning: false,
	_interval: 3000,

	start: function(originalTitle, newTitle, interval) {
		this._originalTitle = originalTitle;
		this._newTitle = newTitle;
		if (interval) this._interval = interval;
		this.isRunning = true;
		this._changeTitle();
	},

	stop: function() {
		this.isRunning = false;
		document.title = this._originalTitle;
	},

	_changeTitle: function() {
		if (this.isRunning == true) {
			document.title = document.title==this._originalTitle ? this._newTitle : this._originalTitle;
			setTimeout("TitleOscillator._changeTitle()", this._interval); //Loop
		}
	}
}

/**
 * Creates a popup box
 *
 * Author: Jason Ho
 */

var PopupBox = Class.create();

Object.extend(PopupBox, {
	removeLast: function() {
		var popup = document.getElementsByClassName("qaboomPopupBox")[0];
		if (popup) {
			var dim = Element.getDimensions(popup);
			new Effect.MoveBy(popup, dim.height+10, 0, {
				duration: 0.5,
				afterFinish: function(){Element.remove(popup)}
			});
		}
	},

	load: function(json) {
        var span = document.createElement("span");
		span.innerHTML = json;
		new PopupBox(span);

        //Update contacts
        var jsonString = json[0];
		if (jsonString.indexOf(" has logged in.") != -1 || jsonString.indexOf(" has logged out.") != -1) {
			new Ajax.Request('php/actions/users_online_get.php', {
				onSuccess: MenuManager.updateMyContacts
			});
		}
	}
});

PopupBox.prototype = {

	initialize: function(contentNode) {
		this.elementNode = document.createElement("div");
		this.elementNode.className = "qaboomPopupBox";
		//this.elementNode.style.display = "none";
		this.elementNode.appendChild(contentNode);
		this.elementNode.innerHTML += "<div style='text-align: right;'><a class='popupClose' onclick='Element.remove(this.parentNode.parentNode)'>Close this.</a></div>";
		document.body.appendChild(this.elementNode);
		this._placeBottomRight();
		var dim = Element.getDimensions(this.elementNode);
		new Effect.MoveBy(this.elementNode, -dim.height-10, 0, {duration: 0.5});
		setTimeout("PopupBox.removeLast()", 7000);
	},

	_placeBottomRight: function() {
		var dim = Element.getDimensions(this.elementNode);
		var newLeft = document.documentElement.clientWidth-dim.width-10;
		var newTop = document.documentElement.clientHeight;
		Element.setStyle(this.elementNode, {left: newLeft+"px", top: newTop+"px"});
	}
}
/** 
 * JsonRequest -- makes a GET request to the server via script tag.
 * Usually a callback function is specified, in which the server returns
 * the function with a JSON object as an argument - Ex. openChat({chatee: "bravo"});
 *
 * Author: Jason Ho
 */

var JsonRequest = {

	//Options: parameters
	send: function(serverURL, options) {
		var noCacheIE = 'noCacheIE=' + (new Date()).getTime(); // Keep IE from caching requests
		var requestURL = serverURL+"?"+noCacheIE;
		if (options.parameters) {
			requestURL += "&"+options.parameters;
		}
		//Insert the script tag
		var headLocation = document.getElementsByTagName("head").item(0); // Get the DOM location to put the script tag
		var scriptObj = document.createElement("script"); //Create the script tag

		//Set event handlers
		var onSuccessAction = "Element.remove(this);";
		if (options.onSuccess) onSuccessAction += options.onSuccess;
		scriptObj.setAttribute("onload", onSuccessAction);
		if (options.onError) scriptObj.setAttribute("onerror", options.onError);

		scriptObj.setAttribute("type", "text/javascript");
		scriptObj.setAttribute("charset", "utf-8");
		scriptObj.setAttribute("src", requestURL);
		headLocation.appendChild(scriptObj); //Add to the DOMs
	}
}
var AbstractRenderer = Class.create();

AbstractRenderer.prototype = {
	_types: [],
	_fillColor: "white",
	_strokeColor: "black",
	_strokeWeight: 1,

	initialize: function() {},

	setFillColor: function(color) {
		this._fillColor = color;
	},

	setStrokeColor: function(color) {
		this._strokeColor = color;
	},

	setStrokeWeight: function(weight) {
		this._strokeWeight = weight;
	},

	isShape: function(node) {
		return (this._types.indexOf(node.tagName) != -1);
	}
}

var Mode = {
	SELECT: -1,
	LINE: 0,
	POLYLINE: 1,
	RECT: 2,
	ROUNDRECT: 3,
	ELLIPSE: 4
}/**
 * SVG Implementation of AbstractRenderer
 *
 * Author: Jason Ho
 */

var SVGRenderer = Class.create();
SVGRenderer.prototype = new AbstractRenderer();

Object.extend(SVGRenderer.prototype, {
	namespace: "http://www.w3.org/2000/svg",
	_types: ["line", "polyline", "rect", "rect", "ellipse"],

	initialize: function(canvasNode) {
		canvasNode = $(canvasNode);
		this._svgNode = document.createElementNS(this.namespace, "svg");
		canvasNode.appendChild(this._svgNode);
		canvasNode.style.MozUserSelect = 'none';
	},

	addGroup: function() {
		var node = document.createElementNS(this.namespace, "g");
		node.setAttributeNS(null, "transform", "translate(0,0) rotate(0,0,0)");
		this._svgNode.appendChild(node);
		return node;
	},

	addShape: function(shape, x, y, width, height, group) {
		var node = document.createElementNS(this.namespace, this._types[shape]);
		node.setAttributeNS(null, "transform", "translate(0,0) rotate(0,0,0)");
		if (shape == Mode.ROUNDRECT) {
			node.setAttributeNS(null, "rx", 15+"px");
			node.setAttributeNS(null, "ry", 15+"px");
		}
		this.updateShape(node, x, y, width, height);
		node.style.position = "absolute";
		if (this._fillColor == "") this._fillColor = "none"; 
		node.setAttributeNS(null, 'fill', this._fillColor);
		node.setAttributeNS(null, 'stroke', this._strokeColor);
		node.setAttributeNS(null, 'stroke-width', this._strokeWeight);
		if (group) {
			group.appendChild(node);
		} else {
			this._svgNode.appendChild(node);
		}
		return node;
	},

	updateShape: function(node, x, y, width, height) {
		if (node.tagName == "line") {
			//Normalize line
			if ((width > 0 && height < 0) || (width < 0 && height < 0)) {
				x += width;
				y += height;
				width *= -1;
				height *= -1;
			}
			node.setAttributeNS(null, "x1", "0px");
			node.setAttributeNS(null, "y1", "0px");
			node.setAttributeNS(null, "x2", width+"px");
			node.setAttributeNS(null, "y2", height+"px");
			this.setPosition(node, x, y);
		} else {
			var offsetX = width < 0 ? width : 0; //Account for negative width/heights
			var offsetY = height < 0 ? height : 0;
			width = Math.abs(width);
			height = Math.abs(height);
			if (node.tagName == "rect") {
				node.setAttributeNS(null, "x", "0px");
				node.setAttributeNS(null, "y", "0px");
				node.setAttributeNS(null, "width", width+"px");
				node.setAttributeNS(null, "height", height+"px");
			} else if (node.tagName == "ellipse") {
				node.setAttributeNS(null, "cx", (width/2)+"px");
				node.setAttributeNS(null, "cy", (height/2)+"px");
				node.setAttributeNS(null, "rx", (width/2)+"px");
				node.setAttributeNS(null, "ry", (height/2)+"px");
			}
			this.setPosition(node, x+offsetX, y+offsetY);
		}
	},

	setRotation: function(node, angle, centerX, centerY) {
		var offset = this.getPosition(node);
		centerX -= offset.x;
		centerY -= offset.y;
		var rotateString = "rotate("+angle+","+centerX+","+centerY+")";
		var transform = node.getAttribute("transform")+""; //convert to string
		transform = transform.replace(/rotate\([^)]*\)/, rotateString);
		node.setAttributeNS(null, "transform", transform);
	},

	getRotation: function(node) {
		var transform = node.getAttribute("transform");
		var rotate = transform.match(/rotate\([^)]*\)/)+"";
		var attributes = rotate.substring("rotate(".length, rotate.length-1);
		if (attributes == "0") {
			return {angle: 0, x: 0, y: 0};
		} else {
			attributes = attributes.split(",");
			return {angle: parseInt(attributes[0]), x: parseInt(attributes[1]), y: parseInt(attributes[2])};
		}
	},
	
	getBounds: function(node) {
		var offset = this.getPosition(node);
		if (node.tagName == "line") {
			var x1= parseInt(node.getAttribute("x1"));
			var y1= parseInt(node.getAttribute("y1"));
			var x2= parseInt(node.getAttribute("x2"));
			var y2= parseInt(node.getAttribute("y2"));
			return {x: x1+offset.x, y: y1+offset.y, width: x2-x1, height: y2-y1};
		} else {
			var bounds = node.getBBox();
			bounds.x += offset.x;
			bounds.y += offset.y;
			return bounds;
		}
	},

	bringForward: function(node) {
		var parent = node.parentNode;
		Element.remove(node);
		parent.appendChild(node);
	},
	
	setPosition: function(node, x, y) {
		var translateString = "translate("+x+","+y+")";
		var transform = node.getAttribute("transform")+""; //convert to string
		transform = transform.replace(/translate\([^)]*\)/, translateString);
		node.setAttributeNS(null, "transform", transform);
	},

	getPosition: function(node) {
		var transform = node.getAttribute("transform");
		var translate = transform.match(/translate\([^)]*\)/)+"";
		var coorPair = translate.substring("translate(".length, translate.length-1);
		if (coorPair == "0") {
			return {x: 0, y: 0};
		} else {
			coorPair = coorPair.split(",");
			return {x: parseInt(coorPair[0]), y: parseInt(coorPair[1])};
		}
	}
});
/**
 * VML Implementation of AbstractRenderer
 *
 * Author: Jason Ho
 */

var VMLRenderer = Class.create();

Object.extend(VMLRenderer, {
	lastZIndex: 1
});

VMLRenderer.prototype = new AbstractRenderer();

Object.extend(VMLRenderer.prototype, {
	_types: ["line", "polyline", "rect", "roundrect", "oval"],

	initialize: function(canvasNode) {
		this._canvasNode = $(canvasNode);
		this._canvasNode.onselectstart = function() { return false; };
		this._canvasNode.style.overflow = "hidden";
		document.namespaces.add("v", "urn:schemas-microsoft-com:vml");
		document.createStyleSheet().addRule('v\\:*', "behavior: url(#default#VML);");
	},

	addGroup: function() {
		var node = document.createElement('v:group');
		//Set position
		node.style.position = "absolute";
		node.style.left = "0px";
		node.style.top = "0px";
		this.setPosition(node, 0, 0);
		//Set coordinate space
		var canvasWidth = parseInt(this._canvasNode.style.width);
		var canvasHeight = parseInt(this._canvasNode.style.height);
		var coordsize = Math.max(canvasWidth, canvasHeight);
		node.style.width = coordsize+"px";
		node.style.height = coordsize+"px";
		node.setAttribute("coordorigin", "0,0");
		node.setAttribute("coordsize", coordsize+","+coordsize);
		this._canvasNode.appendChild(node);
		return node;
	},

	addShape: function(shape, x, y, width, height, group) {
		var node = document.createElement('v:'+this._types[shape]);
		node.style.position = "absolute";
		this.updateShape(node, x, y, width, height);
		if (shape != Mode.LINE) {
			node.setAttribute('filled', (this._fillColor != ""));
			node.setAttribute('fillcolor', this._fillColor);
		}
		node.setAttribute('stroked', (this._strokeColor != ""));
		node.setAttribute('strokecolor', this._strokeColor);
		node.setAttribute('strokeWeight', this._strokeWeight+"px");
		if (group) {
			group.appendChild(node);
		} else {
			this._canvasNode.appendChild(node);
		}
		this.bringForward(node);
		return node;
	},

	updateShape: function(node, x, y, width, height) {
		if (node.tagName=="line") {
			//Normalize line
			if ((width > 0 && height < 0) || (width < 0 && height < 0)) {
				x += width;
				y += height;
				width *= -1;
				height *= -1;
			}
			var offsetX = (width < 0) ? Math.abs(width) : 0;
			this.setPosition(node, 0, 0); //For some reason IE needs this
			node.setAttribute('from', offsetX+",0");
    		node.setAttribute('to', (width+offsetX)+","+height);
    		this.setPosition(node, x-offsetX, y);
		} else {
			var offsetX = width < 0 ? width : 0; //Account for negative width/heights
			var offsetY = height < 0 ? height : 0;
			width = Math.abs(width);
			height = Math.abs(height);
			node.style.left = "0px";
			node.style.top = "0px";
			node.style.width = width+"px";
			node.style.height = height+"px";
			this.setPosition(node, x+offsetX, y+offsetY);
		}
	},

	setRotation: function(node, angle, centerX, centerY) {
		node.style.rotation = angle;
	},

	getRotation: function(node) {
		return {angle: node.style.rotation, x: 0, y: 0};
	},

	setPosition: function(node, x, y) {
		node.style.marginLeft = x+"px";
		node.style.marginTop = y+"px";
	},

	getPosition: function(node) {
		return {x: parseInt(node.style.marginLeft), y: parseInt(node.style.marginTop)};
	},

	bringForward: function(node) {
		node.style.zIndex = VMLRenderer.lastZIndex;
		VMLRenderer.lastZIndex++;
	},

	getBounds: function(node) {
		var offset = this.getPosition(node);
		if (node.tagName == "line") {
			var from = (node.getAttribute("from")+"").split(",");
			var to = (node.getAttribute("to")+"").split(",");
			var scaleFactor = screen.logicalXDPI/72;
			var x1 = scaleFactor*parseFloat(from[0]);
			var y1 = scaleFactor*parseFloat(from[1]);
			var x2 = scaleFactor*parseFloat(to[0]);
			var y2 = scaleFactor*parseFloat(to[1]);
			return {x: x1+offset.x, y: y1+offset.y, width: x2-x1, height: y2-y1};
		} else {
			return {x: offset.x, y: offset.y, width: parseInt(node.style.width), height: parseInt(node.style.height)};
		}
	}
});



/*
//linewidth
// VML always transforms the pixels to points, so we have to convert them back
return (parseFloat(shape.strokeweight) * (screen.logicalXDPI / 72)) + 'px';
*//**
 * Will create a sketchpad widget using Canvas (if the browser supports it),
 * or VML (if using IE). Depends on excanvas.js for the VML/Canvas abstraction.
 *
 * Author: Jason Ho
 */

var Sketchpad = Class.create();

Sketchpad.prototype = {
	elementNode: null,
	canvas: null,
	canvasContext: null,
	window: null,
	color: '#90778A',
	lineWidth: 1,
	points: [],

	initialize: function(canvasID, options) {
		this.canvasID = canvasID;
		this.options = options;
		this.width = options.width ? options.width : 100;
		this.height = options.height ? options.height : 100;
		//Create the sketchpad DOM
		this.elementNode = document.createElement("div");
		this.elementNode.innerHTML =
			"<table class='trimmed'>"+
				"<tr>"+
					"<td>"+
						"<canvas style='cursor: default' id='"+canvasID+"' width='"+this.width+"' height='"+this.height+"'></canvas>"+
					"</td>"+
				"</tr>"+
				"<tr>"+
					"<td id='colorChooserContainer'>"+
					"</td>"+
				"</tr>"+
			"</table>";
		//Find the colorChooser <td> and append a color chooser
		var container = this.elementNode;
		while ((container = container.lastChild).id != "colorChooserContainer");

		//var clearButton = document.createElement("img");
		//clearButton.src="images/answerButton.gif";
		//container.appendChild(clearButton);
		container.appendChild(this._createColorChooser());
		//Initialize for IE
		if (typeof G_vmlCanvasManager != "undefined") {
			G_vmlCanvasManager.initElement(this._findCanvasNode());
		}
		//Register Events
		var canvasNode = this._findCanvasNode();
		Event.observe(canvasNode, 'mousedown', this._startStroke.bind(this), false);
	},

	_initCanvas: function() {
		this.canvas = $(this.canvasID);
		this.canvasContext = this.canvas.getContext("2d");
		this.canvasContext.lineCap = "round";
		this.canvas.onselectstart = function() { return false; };
	},

	_findCanvasNode: function() {
		var currentNode = this.elementNode;
		while ((currentNode = currentNode.firstChild).id != this.canvasID);
		return currentNode;
	},

	_calculateOffset: function() {
		var scrollOffset = Position.realOffset(this.elementNode);
		var xOffset = scrollOffset[0];
		var yOffset = scrollOffset[1];
		var currentNode = this.elementNode;
		while (currentNode = currentNode.offsetParent) {
			xOffset += currentNode.offsetLeft;
			yOffset += currentNode.offsetTop;
		}
		return [xOffset, yOffset];
	},

	_startStroke: function(event) {
		if (!this.canvas) this._initCanvas();
		var offset = this._calculateOffset();
		this._xOffset = offset[0];
		this._yOffset = offset[1];
		var startX = Event.pointerX(event)-this._xOffset;
		var startY = Event.pointerY(event)-this._yOffset;
		this.points = [];
		this.points.push(startX, startY);
		this.canvasContext.lineWidth = this.lineWidth;
		this.canvasContext.strokeStyle = this.color;
		this._drawSegmentBinded = this._drawSegment.bind(this);
		this._endStrokeBinded = this._endStroke.bind(this);
		Event.observe(this.canvas, 'mousemove', this._drawSegmentBinded, false);
		Event.observe(document.body, 'mouseup', this._endStrokeBinded, false);
	},

	_endStroke: function() {
		Event.stopObserving(this.canvas, 'mousemove', this._drawSegmentBinded, false);
		Event.stopObserving(document.body, 'mouseup', this._endStrokeBinded, false);
		if (this.options.onEndStroke) {
			this.options.onEndStroke.call();
		}
	},

	_drawSegment: function(event) {
		var x = Event.pointerX(event)-this._xOffset;
		var y = Event.pointerY(event)-this._yOffset;
		var lastIndex = this.points.length-1;
		var previousX = this.points[lastIndex-1];
		var previousY = this.points[lastIndex];

        this.canvasContext.strokeStyle = this.color;
        this.canvasContext.beginPath();
		this.canvasContext.moveTo(previousX, previousY);
		this.canvasContext.lineTo(x, y);
		this.canvasContext.stroke();
		this.points.push(x,y);
	},

	drawPolyLine: function(points, color) {
		if (!this.canvas) this._initCanvas();
		var pointArray = points.split(",");
		this.canvasContext.strokeStyle = color;
		this.canvasContext.beginPath();
		this.canvasContext.moveTo(parseInt(pointArray[0]), parseInt(pointArray[1]));
		for (var i = 0; i < pointArray.length; i+=2) {
			this.canvasContext.lineTo(parseInt(pointArray[i]), parseInt(pointArray[i+1]));
		}
		this.canvasContext.stroke();
	},

	clearSketch: function() {
		this.clearSketchOnly();
		if (this.options.onClearSketch) {
			this.options.onClearSketch.call();
		}
	},

	clearSketchOnly: function() {
		this.canvasContext.clearRect(0,0,this.width,this.height);
	},

	_chooseColor: function(event) {
		event = event || window.event;
		var target = event.target || event.srcElement;
		this.color = Element.getStyle(target, "background-color");
	},

	_createColorChooser: function() {
		//Generate all the colors
		var colorBases = ["FF00","","","00FF","00","FF","00FF","","","FF00","FF","00"];
		var colorStep = 25;
		var prefix, suffix;
		var colors = [];
		for (var i = 0; i < 3; i++) {
			prefix = colorBases.shift();
			suffix = colorBases.shift();
			for (var color = 0; color < 255; color+=colorStep) {
				colors.push(prefix+this._toHex(color)+suffix);
			}
			prefix = colorBases.shift();
			suffix = colorBases.shift();
			for (var color = 255; color > 0; color-=colorStep) {
				colors.push(prefix+this._toHex(color)+suffix);
			}
		}
		colors.push("FF0000"); //Firefox fix?
		//Build the color chooser DOM
		var table = document.createElement("table");
		table.className = "colorChooser";
		Event.observe(table, 'click', this._chooseColor.bind(this), false);
		var row = table.insertRow(-1);
		for (var i = 0; i < colors.length; i++) {
			var td = row.insertCell(-1);
			td.style.backgroundColor = "#"+colors[i];
		}

		//Append color table
		var table2 = document.createElement("table");
		table2.style.borderCollapse = "collapse";
		table2.style.width = "100%";
		var row = table2.insertRow(-1);
		var td = row.insertCell(-1);
		td.style.width = "100%";
		td.style.padding = "0px";
		td.style.margin = "0px";
		td.appendChild(table);

		//Create clear button
		var clearButton = document.createElement("img");
		    clearButton.src="images/eraser.gif";
		Event.observe(clearButton, 'mouseup', this.clearSketch.bind(this), false);

		//Append clear button
		td = row.insertCell(-1);
		td.style.padding = "0px";
		td.style.margin = "0px";
		td.appendChild(clearButton);

		return table2;
	},

	_toHex: function(number) {
		var hexString = number.toString(16);
		if (hexString.length < 2) {
			hexString = "0"+hexString;
		}
		return hexString;
	}
}
var MenuManager = {
	updateMyContacts: function(request) {
        $('usersOnlineContainer').innerHTML = request.responseText;
	}
}





