/* 
 * Eileen Maccabe utility functions.
 * Written by Thomas Moran in 2008.
 */
function util() 
{
    var win             = window, 
        nav             = navigator, 
        doc             = document, 
        docElem         = doc.documentElement,
        body,
        dv              = doc.defaultView,
        domLoaded     = false,
        domLoadHandlers = [],
        ie, mozilla, webKit, opera, windows, mac,
        xmlHttpRequestFactories = [
            function() { return new XMLHttpRequest(); },
            function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
            function() { return new ActiveXObject("Microsoft.XMLHTTP"); }
        ],
        nodeData = {},
        nodeDataKeyCounter = 0,
        spinners       = [], 
        activeSpinners = 0;

    /* We can't proceed without various basic JavaScript and DOM methods. */
    if (!((win.addEventListener || win.attachEvent) && 
        win.top && 
        doc.createElement && 
        doc.appendChild && 
        doc.replaceChild && 
        doc.removeChild &&
        doc.cloneNode &&
        Array.prototype.push &&
        (docElem.getBoundingClientRect || dv && dv.getComputedStyle))) {
        return;
    }
    
    /* Create an object with __proto__ = obj. */
    this.augment = function(obj, augmentation)
    {
        var newObj, prop, C = function() {};
        C.prototype = obj;
        newObj = new C();
        if (augmentation !== undefined) {
            for (key in augmentation) {
                newObj[key] = augmentation[key];
            }
        }
        return newObj;
    }

    /* Browser detection. */
    var uas = nav.userAgent.toLowerCase(), ps = nav.platform.toLowerCase(), m;
    if (m = uas.match(/opera(?:\s*\/\s*| )(\d+(?:\.\d+)?)/)) {
        opera = +m[1];
    } else if (m = uas.match(/msie (\d+(?:\.\d+)?)/)) {
        ie = +m[1];
    } else if (m = uas.match(/webkit\s*\/\s*(\d+(?:\.\d+)?)/)) {
        webKit = +m[1];
    } else if ((m = uas.match(/mozilla\s*\/\s*(\d+(?:\.\d+)?)/)) && uas.indexOf("compatible") < 0) {
        mozilla = +m[1];
    }
    windows = ps.indexOf("win") >= 0;
    mac = ps.indexOf("mac") >= 0;
    
    this.caps = {
        ie: ie, 
        mozilla: mozilla, 
        webKit: webKit,
        opera: opera,
        windows: windows, 
        mac: mac
    };

    /* Arrange for domLoad to be called when the DOM can be modified. */
    if (win.addEventListener) {
        addEventListener("DOMContentLoaded", domLoad, false);
        addEventListener("load", domLoad, false);
    } else {
        attachEvent("onload", domLoad);
    }
    if ((ie || (webKit && webKit < 525)) && win == top) {            
        var scrollTestElem = ie && doc.createElement("scrolltest");
        var th = setInterval(pollDomReady, 50);
        
        function pollDomReady() 
        {
            if (!domLoaded) {
                if (scrollTestElem) {
                    try {
                        scrollTestElem.doScroll("left");
                    } catch (e) {
                        return;
                    }
                } else {
                    if (doc.readyState != "loaded" || doc.readyState != "complete")
                        return;
                }
                domLoad(null);
            }
            clearInterval(th);
        }
    }
    
    /* We cache a few tag name strings in the correct case (xhtml = lower) 
     * to avoid calling toLowerCase() when checking tag names. */ 
    if (doc.documentElement.tagName == "HTML") {
        this.A_TAG = "A";
        this.DIV_TAG = "DIV";
        this.P_TAG = "P";
        this.IMG_TAG = "IMG";
        this.LI_TAG = "LI";
        this.BUTTON_TAG = "BUTTON";
        this.FORM_TAG = "FORM";
    } else {
        this.A_TAG = "a";
        this.DIV_TAG = "div";
        this.P_TAG = "p";
        this.IMG_TAG = "img";
        this.LI_TAG = "li";
        this.BUTTON_TAG = "button";
        this.FORM_TAG = "form";
    }

    function domLoad()
    {
        if (domLoaded)
            return;
        domLoaded = true;
        body = doc.body;
        updateBodyChildOffsets();
        for (var i = 0, len = domLoadHandlers.length; i < len; ++i)
            domLoadHandlers[i]();
        domLoadHandlers = null;
    }

    var addDomLoadHandler = this.addDomLoadHandler = function(handler)
    {
        if (domLoaded)
            handler();
        else
            domLoadHandlers.push(handler);
    }
    
    /* 
     * Size and position functions.
     */
    var documentOffset = this.documentOffset = function(elem, result)
    {
        var e, op, x, y, cs, f, rect, childWasStatic, addPageScroll,
            borderAttrsSupported = elem.clientLeft !== undefined;
        
        if (elem.getBoundingClientRect) {
            rect = elem.getBoundingClientRect();
            x = rect.left - docElem.clientLeft;
            y = rect.top - docElem.clientTop;
            /* IE always returns (0,0) for HTML. */
            addPageScroll = !(ie && elem == docElem);
        } else {
            x = 0;
            y = 0;
            e = elem;
            cs = dv.getComputedStyle(e, null);
            for (;;) { 
                /* No browser (except IE, but that's handled by getBoundingClientRect)
                 * has helpful values of offsetLeft and offsetTop on the body, so we
                 * have to get the same information using a computed style. */
                if (e == body) {
                    /* The body's margin. */
                    x += parseInt(cs.marginLeft) || 0;
                    y += parseInt(cs.marginTop) || 0;
                    
                    if (cs.position != "static") {
                        /* The distance between the ICB's padding edge and the body's margin
                         * edge. */
                        x += parseInt(cs.left) || 0;
                        y += parseInt(cs.top) || 0;
                    }
                    break;
                } else {
                    x += e.offsetLeft;
                    y += e.offsetTop;
                }
                
                if (cs.position === "fixed") {
                    addPageScroll = true;
                    /* WebKit gives offsetLeft and offsetTop from the child's margin
                     * edge for fixed-position elements. */
                    if (webKit) {
                        x += e.clientLeft;
                        y += e.clientTop;
                    }
                    break;
                }
                
                if (!(op = e.offsetParent)) 
                    break;
                    
                childWasStatic = cs.position === "static";                
                do {
                    /* This test accounts for the case where op is not in the 
                     * parentNode chain, for instance if an element is a child of 
                     * the document element and the browser gives the body as its
                     * offsetParent. It also guards against the possibility of an
                     * element having a non-null offsetParent but a null parentNode,
                     * but that shouldn't occur in any reasonable implementation. */
                    if (!(e = e.parentNode)) {
                        e = op;
                        cs = dv.getComputedStyle(e, null);
                        break;
                    }
                    cs = dv.getComputedStyle(e, null);
                    if (e != body && e != docElem && (e.scrollLeft || e.scrollTop) && 
                        !(opera && (cs.display === "inline" || cs.display === "table"))) {
                        x -= e.scrollLeft;
                        y -= e.scrollTop; 
                    }
                    
                    /* Gecko < 1.9 forgets to include the border width of parents 
                     * with overflow != visible in a child's offsetLeft and 
                     * offsetTop, so we add them here. Gecko 1.9 has 
                     * getBoundingClientRect so it won't end up here. */
                    if (mozilla && cs.overflow !== "visible") {
                        x += parseInt(cs.borderLeftWidth);
                        y += parseInt(cs.borderTopWidth);
                    }
                } while (e != op);

                /* All browsers (except IE, but that always has getBoundingClientRect)
                 * report the body as an offsetParent when it's static. The offsetLeft
                 * and offsetTop properties of child elements in this situation are
                 * ICB relative, but don't include the body's border, except in Opera. 
                 * In Opera, offsetLeft and offsetTop are ICB relative even with a 
                 * positioned body. */
                if (e == body && (cs.position === "static" || opera)) {
                    /* Mozilla and WebKit give page-relative offsets when the
                     * body is static, but don't add in the body's border. */
                    if ((mozilla && childWasStatic) || webKit) {
                         x += parseInt(cs.borderLeftWidth) || 0;
                         y += parseInt(cs.borderTopWidth) || 0;
                    } 
                    e = e.offsetParent;
                    if (!e) break;
                }
                            
                /* Opera always gives border edge to border edge distances. Mozilla
                 * doesn't include the border, except on table, tr and td with 
                 * Gecko < 1.9. WebKit versions < 500 (Safari < 3) give border to 
                 * border distances, but newer versions omit the border. */
                if (!opera && !(mozilla && /^(?:table|tr|td)$/i.test(e.tagName)) && !(webKit && webKit < 500)) {
                    if (borderAttrsSupported) {
                        x += e.clientLeft;
                        y += e.clientTop;
                    } else {
                        x += parseInt(cs.borderLeftWidth);
                        y += parseInt(cs.borderTopWidth);
                    }
                }
            }
        }
        
        if (addPageScroll) {
            x += win.pageXOffset || docElem.scrollLeft || body.scrollLeft || 0;
            y += win.pageYOffset || docElem.scrollTop || body.scrollTop || 0;
        }
        
        if (!result) {
            return { x: x, y: y };
        } else {
            result.x = x;
            result.y = y;
            return result;
        }
    }
    
    /* We cache the (usually negative) offsets that must be added to a document offset to
     * give coordinates relative to the body. This is useful because we have lots of "global"
     * things that we need to position that are children of the body. */
    var updateBodyChildOffsets = this.updateBodyChildOffsets = function()
    {
        var cs = body.currentStyle || dv.getComputedStyle(body, null);
        /* cs is null in Firefox on iframe bodies. Not sure why. */
        if (cs && cs.position !== "static") {
            documentOffset(body, updateBodyChildOffsets);
            bodyChildOffsetX = -updateBodyChildOffsets.x;
            bodyChildOffsetY = -updateBodyChildOffsets.y;
        } else {
            bodyChildOffsetX = 0;
            bodyChildOffsetY = 0;
        } 
    }
    
    /* Returns the width and height of the content box of elem in pixels. */
    var contentSize = this.contentSize = function(elem)
    {
        var cs, width, height;
        
        if (dv && dv.getComputedStyle) {
            cs = dv.getComputedStyle(elem, null);
        } else {
            cs = elem.currentStyle;
        }
        width = parseFloat(cs.width);
        height = parseFloat(cs.height);
        
        return { width: width, height: height };
    }
    
    /* 
     * Colour Utilities
     */
    
    /* Parse a string in the form rgb(r, g, b) or #xxxxxx into a colour. */
    var parseColour = this.parseColour = function(s)
    {
        var match, n;
        
        match = /rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)/.exec(s);
        if (match) {
            return ((match[1] & 0xFF) << 0) |
                   ((match[2] & 0xFF) << 8) |
                   ((match[3] & 0xFF) << 16);
        }
        match = /(?:0x|#)?([\dA-F]{6})/i.exec(s);
        if (match) {
            n = parseInt(match[1], 16);
            n = (n >>> 16) + 
                (n & 0xFF00) +
                ((n << 16) & 0xFF0000);
            return n;
        }
        
        return null;
    }
    
    var colourToHex = this.colourToHex = function colourToHex(colour, prefix)
    {
        return this.rgbToHex(
            (colour & 0xFF),
            ((colour >> 8) & 0xFF),
            ((colour >> 16) & 0xFF), 
            prefix);
    }
    
    var rgbToHex = this.rgbToHex = function rgbToHex(r, g, b, prefix)
    {
        var PAD = rgbToHex.PAD;
        
        r = (r & 0xFF).toString(16);
        g = (g & 0xFF).toString(16);
        b = (b & 0xFF).toString(16);
        if (prefix === undefined)
            prefix = "#";
        return [ 
            prefix, 
            PAD[r.length], r, 
            PAD[g.length], g,
            PAD[b.length], b
        ].join("");
    }
    rgbToHex.PAD = { "1": "0", "2": "" };
    
    /*
     * Spinner management.
     */
    var showSpinner = this.showSpinner = function(parent, x, y, opacity)
    {
        var sl = spinners.length, 
            div, anim, style, filter, 
            useFilters = ie && ie < 7.0, 
            borderUrl, 
            skipFilterSetup,
            left, top;
        
        /* When using IE filters for PNG transparency we can't achieve true 
         * control over the opacity of the spinner, because the Alpha filter 
         * doesn't chain correctly with AlphaImageLoader (it overwrites the PNG's 
         * per-pixel alpha). As a work-around we have 10 versions of the border
         * image, each with its alpha channel attenuated to a multiple of 10%.
         * We round the requested opacity and use it to select one of these 
         * images.
         */
        if (useFilters) {
            opacity = "" + Math.round(opacity * 10) * 10;
            borderUrl = "/images/ui/spinner/spinner_border_a" + opacity + ".png"
        } else {
            opacity = "" + opacity;
            borderUrl = "/images/ui/spinner/spinner_border.png";
        }
        
        if (activeSpinners === sl) {
            if (sl === 0) {
                div = doc.createElement("div");
                setStyle(div, "position: absolute; display: block; width: 32px; height: 32px; z-index: 1000000;");
                anim = doc.createElement("img");
                setStyle(anim, "position: absolute; left: 4px; top: 4px;");
                anim.src = "/images/ui/spinner/spinner1.gif";
                div.appendChild(anim);
                
                if (useFilters) {
                    div.style.filter = [ 
                        'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="', borderUrl, '")'
                    ].join("");
                    anim.style.filter = [
                        'progid:DXImageTransform.Microsoft.Alpha(opacity=', opacity, ')'
                    ].join("");
                    skipFilterSetup = true;
                } else {
                    div.style.backgroundImage = [ 'url("', borderUrl, '")' ].join("");
                }
            } else {
                div = spinners[sl - 1].cloneNode(true);
            }
            spinners[sl] = div;
        } else {
            div = spinners[activeSpinners];
        }
      
        /* If the parent was not specified, assume x and y are in document coordinates. However,
         * the spinner must be a child of the body because children of the document element are
         * not shown in some browsers. */
        style = div.style;
        
        if (x == null) {
            style.left = "50%";
            style.marginLeft = "-16px";
        } else {
            if (parent == null)
                x += bodyChildOffsetX;
            style.left = (x - 16) + "px";
            style.marginLeft = "0";
        }
        
        if (y == null) {
            style.top = "50%";
            style.marginTop = "-16px";
        } else {
            if (parent == null)
                y += bodyChildOffsetY;
            style.left = (y - 16) + "px";
            style.marginTop = "0";
        }
        
        if (parent == null)
            parent = body;
;
        style.display = "block";
        if (useFilters && !skipFilterSetup) {
            /* The 'filters' collection is not accessible immediately after setting the filter string. IE throws an
             * exception if we try to access the filters property when it isn't defined (instead of returning 
             * unndefined), and the typeof operator gives the non-standard value "unknown". */
            anim = div.firstChild;
            filter = typeof div.filters === "object" && div.filters["DXImageTransform.Microsoft.AlphaImageLoader"];
            if (filter) filter.src = borderUrl;
            filter = typeof anim.filters === "object" && anim.filters["DXImageTransform.Microsoft.Alpha"];
            if (filter) filter.opacity = opacity;
        }
        style.opacity = opacity;
        parent.appendChild(div);
        
        ++activeSpinners;
        
        return div;
    }
    
    var hideSpinner = this.hideSpinner = function(div)
    {
        var i, t;
        for (i = 0; i < activeSpinners; ++i)
            if (spinners[i] === div)
                break;
        if (i === activeSpinners)
            throw new Error("spinner not found in hideSpinner()");
        if (i !== --activeSpinners) {
            t = spinners[activeSpinners];
            spinners[activeSpinners] = spinners[i];
            spinners[i] = t;
        }
        div.style.display = "none";
        if (div.parentNode != null)
            div.parentNode.removeChild(div);
    }
    
    this.createXmlHttpRequest = function()
    {
        var i, len, factory, obj;
        
        for (i = 0, len = xmlHttpRequestFactories.length; i < len; ++i) {
            try {
                factory = xmlHttpRequestFactories[i];
                obj = factory();
                if (obj !== null) {
                    this.createXmlHttpRequest = factory;
                    return obj;
                }
            } catch (e) {
            }
        }
        this.createXmlHttpRequest = function() { throw new Error("Cannot create XMLHttpRequest."); }
        this.createXmlHttpRequest();
    }
    
    if (doc.addEventListener !== undefined) {
        this.addListener = function(obj, name, f) { obj.addEventListener(name, f, false); };
        this.removeListener = function(obj, name, f) { obj.removeEventListener(name, f, false); }
    } else {
        this.addListener = function(obj, name, f) { obj.attachEvent("on" + name, f); };
        this.removeListener = function(obj, name, f) { obj.detachEvent("on" + name, f); };
    }
    
    /* Earlier versions of Safari apparently don't support cssText. */
    if (docElem.style.cssText !== undefined) {
        var setStyle = this.setStyle = function(elem, style) { elem.style.cssText = style; }
    } else {
        var setStyle = this.setStyle = function(elem, style) { elem.setAttribute("style", style); }
    }
           
    /* 
     * These functions associate arbitrary data with DOM nodes. 
     */
    this.getData = function(node, name)
    {
        var key, data;
        
        if ((key = node.utilDataKey) === undefined)
            key = node.utilDataKey = nodeDataKeyCounter++;
        if ((data = nodeData[key]) === undefined) {
            if (name === undefined) 
                data = nodeData[key] = {};
        } else {
            if (name !== undefined)
                data = data[name];
        }
        return data;
    }
   
    this.setData = function(node, name, value)
    {
        var key, data;
        
        if ((key = node.utilDataKey) === undefined)
            key = node.utilDataKey = nodeDataKeyCounter++;
        if ((data = nodeData[key]) === undefined)
            data = nodeData[key] = {};
        data[name] = value;
        return data;
    }
    
    this.deleteData = function(node)
    {
        var key;
        
        if ((key = node.utilDataKey) !== undefined) {
            if (key in nodeData)
                delete nodeData[key];
        }
    }
    
    /* Format a number into a size in bytes, Kilobytes, Megabytes etc. */
    this.prettyByteSize = function(bytes)
    {
        if (bytes < 1.024e3) {
            return bytes.toFixed(0) + " bytes";
        } else if (bytes < 1.04858e6) {
            return (bytes * 9.76563e-4).toFixed(1) + "KB";
        } else if (bytes  < 1.07374e9) {
            return (bytes * 9.53674e-7).toFixed(1) + "MB";
        } else {
            return (bytes * 9.31323e-10).toFixed(2) + "GB";
        }
    }
    
    /* 
     * A simple event utility. 
     */
    var createEvent = this.event = function()
    {
        function signal()
        {
            var self = signal, 
                end = self.count << 1, 
                rv, i;
            for (i = 0; i < end; i += 2) {
                if ((rv = self[i].apply(self[i + 1], arguments)) !== undefined)
                    return rv;
            }
            return undefined;
        }
        
        signal.count = 0;
        signal.add = createEvent.add;
        signal.remove = createEvent.remove;
        signal.clear = createEvent.clear;
        
        return signal;
    }

    createEvent.add = function(handler, thisObj)
    {
        var count = this.count,
            i= count << 1;
        this[i] = handler;
        this[i + 1] = thisObj;
        this.count = count + 1;
    }

    createEvent.remove = function(handler)
    {
        var i, last = (this.count - 1) << 1;
        
        for (i = last; i >= 0; i -= 2) {
            if (this[i] === handler) {
                if (i !== last) {
                    this[i] = this[last];
                    this[i + 1] = this[last + 1];
                }
                this[last] = null;
                this[last + 1] = null;
                this.count = last >> 1;
                break;
            }
        }
    }

    createEvent.clear = function()
    {
        var i, end = this.count << 1;
        
        for (i = 0; i < end; i += 2) {
            this[i] = null;
            this[i + 1] = null;
        }
        this.count = 0;
    }
    
    /* Replace various characters with their equavalent HTML escape sequence. */
    this.htmlEscape = function htmlEscape(s)
    {
        var ESCAPES = htmlEscape.ESCAPES;
        return s.replace(/[<>"'&]/g, function(ch) { return ESCAPES[ch]; } );
    }
    this.htmlEscape.ESCAPES = {
        "<": "&lt;",
        ">": "&gt;",
        "\"": "&#34;",
        "\'": "&#39;",
        "&": "&amp;"
    };
    
    /*
     * Misc DOM stuff.
     */

    this.getCheckedRadio = function(parent)
    {
        var i, len, elems, e;
        
        elems = parent.getElementsByTagName("input");
        for (i = 0, len = elems.length; i < len; ++i) {
            e = elems[i];
            if (e.type === "radio" && e.checked) {
                return e;
            }
        }
        
        return null;
    }

    /* Debugging. */
    
    this.dump = function(obj)
    {
        var message, propCount;
        
        message = [];
        propCount = 0;
        if (obj != null ) {
            for (key in obj) {
                message.push('"', key, '" = "', obj[key], '"\n');
                ++propCount;
            }
        } else {
            message.push("No proprties.\n");
        }
        message = [ 
            'Dump of object "', obj, '", typeof = "', typeof obj, '"\n',
            propCount, ' properties:\n'
        ].concat(message);
        
        alert(message.join(""));
    }
}
util.call(Util = util);

/*
 * Animation
 */
function animation()
{
    var startThread,
        endThread,
        handleTick,
        threads = [],
        threadsModified,
        timerHandle;
        
    /* Thread management. */
    
    startThread = this.startThread = function(handler, thisObj, duration)
    {
        var ta = threads,
            len = ta.length,
            now = (new Date()).getTime();
        ta.push(
            handler,
            thisObj,
            now,
            duration ? 1.0 / duration : NaN // Normalize +/-Inf, NaN to NaN.
        );
        if (len === 0)
            timerHandle = setInterval(handleTick, 30);
    }
    
    endThread = this.endThread = function(handler)
    {
        var i, ta = threads, last = ta.length - 4;
        
        if (last >= 0 && ta[last] === handler) {
            ta.length = last;
            threadsModified = true;
        } else {
            for (i = last - 4; i >= 0; i -= 4) {
                if (ta[i] === handler) {
                    ta[i] = ta[last];
                    ta[i + 1] = ta[last + 1];
                    ta[i + 2] = ta[last + 2];
                    ta[i + 3] = ta[last + 3];
                    ta.length = last;
                    threadsModified = true;
                    break;
                }
            }
        }
        
        if (ta.length === 0)
            clearInterval(timerHandle);
    }

    handleTick = this.handleTick = function()
    {
        var i, ta = threads, len = ta.length, startTime, elapsed;
        var now = (new Date()).getTime();
        threadsModified = false;
        for (i = 0; i < len && !threadsModified; i += 4) {
            startTime = ta[i + 2];
            elapsed = now - startTime;
            ta[i].call(ta[i + 1], startTime, elapsed, elapsed * ta[i + 3]);
        }
    }

    /* Element animation management. */
    
    function addElementAnimation(elem, obj)
    {
        var animations;
        
        animations = Util.getData(elem, "animations") || [];
        animations.push(obj);
        Util.setData(elem, "animations", animations);
    }
    
    function removeElementAnimation(elem, obj)
    {
        var animations, i, len;
        
        animations = Util.getData(elem, "animations");
        if (animations != null) {
            len = animations.length;
            for (i = len - 1; i >= 0; --i) {
                if (animations[i] === obj) {
                    animations[i] = animations[--len];
                    animations.length = len;
                    break;
                }
            }
            Util.setData(elem, "animations", animations);
        }        
    }
    
    function doCompleteAction(elem, action)
    {
        if (action != null) {
            if (typeof action === "function") {
                action(elem);
            } else {
                switch (action) {
                    case "display_none":
                        elem.style.display = "none";
                        break;
                    case "visibility_hidden":
                        elem.style.visibility = "hidden";
                        break;
                }
            }
        }
    }
    
    cancel = this.cancel = function(elem, name)
    {
        var i, len, animations, obj;
        
        animations = Util.getData(elem, "animations");
        if (animations != null) {
            len = animations.length;
            if (name == null) {
                for (i = 0; i < len; ++i) {
                    (obj = animations[i]).cancel ? obj.cancel() : endThread(obj);
                }
                len = 0;
            } else {
                for (i = 0; i < len; ++i) {
                    obj = animations[i];
                    if (obj.name == name) {
                        obj.cancel ? obj.cancel() : endThread(obj);
                        animations[i--] = animations[--len];
                    }
                }
            }
            animations.length = len;
        }
    }
    
    this.scale = function(elem, duration, targetWidth, targetHeight, completeAction)
    {
        function thread(startTime, elapsed, fraction)
        {
            var newWidth, newHeight;
            
            if (elapsed >= duration) {
                doCompleteAction(elem, completeAction);
                removeElementAnimation(elem, thread);
                endThread(thread);
            } else {
                if (targetWidth != null)
                    elem.style.width = (startWidth + dx * fraction) + "px";
                if (targetHeight != null)
                    elem.style.height = (startHeight + dy * fraction) + "px";
            }
        }
        
        var size,
            startWidth, 
            startHeight, 
            currentWidth, 
            currentHeight,
            dx, dy;
        
        size = Util.contentSize(elem);
        startWidth = currentWidth = size.width;
        startHeight = currentWidth = size.height;
        dx = targetWidth != null ? targetWidth - startWidth : 0;
        dy = targetHeight != null ? targetHeight - startHeight : 0;
        size = null;
        
        thread.name = "scale";

        addElementAnimation(elem, thread);
        startThread(thread, this, duration);
    }
    
    this.fade = function(elem, duration, from, to, completeAction)
    {
        function thread(startTime, elapsed, fraction)
        {
            var opacity;
            
            if (elapsed >= duration) {
                elem.style.opacity = to;
                doCompleteAction(elem, completeAction);
                removeElementAnimation(elem, thread);
                endThread(thread);
            } else {
                opacity = from + (to - from) * fraction;
                elem.style.opacity = opacity;
            }
        }
        
        if (from == null)
            from = elem.style.opacity;
        
        thread.name = "fade";

        cancel(elem, "fade");
        addElementAnimation(elem, thread);
        startThread(thread, this, duration);
    }
    
    this.flashBorder = function(elem, duration, colour, numFlashes, completeAction)
    {
        function thread(startTime, elapsed, fraction)
        {
            var r, g, b;
            
            if (elapsed >= duration) {
                elem.style.borderColor = Util.colourToHex(borderColour);
                if (--numFlashes <= 0) {
                    doCompleteAction(elem, completeAction);
                    removeElementAnimation(elem, thread);
                    endThread(thread);
                } else {
                    endThread(thread);
                    startThread(thread, this, duration);
                }
            } else {
                fraction = 1.0 - (fraction * fraction * fraction);
                r = origR + dR * fraction;
                g = origG + dG * fraction;
                b = origB + dB * fraction;
                elem.style.borderColor = Util.rgbToHex(r, g, b);
            }
        }
        
        thread.cancel = function()
        {
            elem.style.borderColor = Util.colourToHex(borderColour);
            endThread(thread);
        }
        
        var cs, dv = document.defaultView, borderColour,
            origR, origG, origB,
            hlR, hlG, hlB,
            dR, dG, dB;
            
        cancel(elem, "flashBorder");
        
        if (numFlashes == null)
            numFlashes = 1;
        
        if (dv && dv.getComputedStyle) {
            cs = dv.getComputedStyle(elem, null);
        } else {
            cs = elem.currentStyle;
        }
        borderColour = Util.parseColour(cs.borderTopColor);
        if (borderColour == null)
            return;
        origR = borderColour & 0xFF;
        origG = (borderColour >> 8) & 0xFF;
        origB = (borderColour >> 16) & 0xFF;
        
        if (typeof colour === "string")
            colour = Util.parseColour(colour);
        if (colour == null)
            return;
        hlR = colour & 0xFF;
        hlG = (colour >> 8) & 0xFF;
        hlB = (colour >> 16) & 0xFF;
        
        dR = hlR - origR;
        dG = hlG - origG;
        dB = hlB - origB;
        
        thread.name = "flashBorder";
        
        addElementAnimation(elem, thread);
        startThread(thread, this, duration);
    }
}
animation.call(Animation = animation);

/*
 * PopupBox
 */

function PopupBox(opts)
{
    var instance = this,    
        container,
        content,
        parent;
    
    function init()
    {
        var self = instance,
            doc = document,
            cbox1, cbox2;
            
        if (opts == null)
            opts = {};
        
        self.isOpen = false;
        self.onopen = Util.event();
        self.onclose = Util.event();
        self.container = container = doc.createElement("div");
        container.className = "popup_box";
        container.innerHTML = [
            '<div class="shadow_corner shadow_top_left_corner"></div>',
            '<div class="shadow_corner shadow_top_right_corner"></div>',
            '<div class="shadow_corner shadow_bottom_right_corner"></div>',
            '<div class="shadow_corner shadow_bottom_left_corner"></div>',
            '<div class="shadow_edge shadow_top_edge"></div>',
            '<div class="shadow_edge shadow_right_edge"></div>',
            '<div class="shadow_edge shadow_bottom_edge"></div>',
            '<div class="shadow_edge shadow_left_edge"></div>',
            '<a href="#" class="close">Close</a>',
            '<div class="content"></div>'
        ].join("");
        self.content = content = container.lastChild;
        
        if (opts.parent != null) {
            parent = opts.parent;
        } else {
            if (opts.useCentringBoxes) {
                cbox1 = doc.createElement("div");
                cbox1.className = "cbox1";
                cbox2 = doc.createElement("div");
                cbox2.className = "cbox2";
                cbox1.appendChild(cbox2);
                doc.body.appendChild(cbox1);
                parent = cbox2;
            } else {
                parent = document.body;
            }
        }
    }
    
    function handleClick(e)
    {
        if (!e) e = event;
        var self = instance,
            t = e.target || e.srcElement,
            handled = false,
            cls;
            
        if (t.nodeType === 1) {
            cls = t.className;
            if (cls.indexOf("close") >= 0) {
                self.close();
                handled = true;
            }
        }
        
        if (handled) {
            if (e.preventDefault)
                e.preventDefault();
            return false;
        }
        
        return true;
    }
    
    this.open = function()
    {
        var win = window, doc = document, de = document.documentElement, body = doc.body,
            style, scrollX, scrollY, clientWidth, clientHeight, width, height;
        
        if (!this.isOpen) {
            Util.addListener(container, "click", handleClick);
            Util.addListener(container, "dblclick", handleClick);
            
            parent.appendChild(container);
            
            style = container.style;
            scrollX = win.pageXOffset || de.scrollLeft  || 0;
            scrollY =  win.pageYOffset || de.scrollTop || 0;
            clientWidth = de.clientWidth;
            clientHeight = de.clientHeight;
            //alert(width);
            
            style.position =  "absolute";
            if (opts.width != null) {
                content.style.width = opts.width + "px";
            }
            if (opts.height != null) {
                content.style.height = opts.height + "px";
            }
            width = container.offsetWidth;
            height = container.offsetHeight;
            if (opts.x == "centre") {
                style.left = (scrollX + ((clientWidth - width) >> 1)) + "px";
            } else 
                if (opts.x != null) {
                    style.left = (scrollX + opts.x) + "px";
                }
            if (opts.y == "centre") {
                style.top = (scrollY + ((clientHeight - height) >> 1)) + "px";
            } else
                if (opts.y != null) {
                    style.top = (scrollY + opts.y) + "px";
                }
            
            this.isOpen = true;
            this.onopen(this);
        }
    }
        
    this.close = function()
    {
        if (this.isOpen) {
            Util.removeListener(container, "click", handleClick);
            Util.removeListener(container, "dblclick", handleClick);
            try {
                parent.removeChild(container);
            } catch (e) {
            }
            this.isOpen = false;
            this.onclose(this);
        }
    }
    
    Util.addDomLoadHandler(init);
}


