﻿(function($) {

    /* ---------- jQuery Element Extensions ---------- */

    $.fn.alignCenter = function(options) {
        options = $.extend({
            forceAbsolute: false,
            container: window,
            completeHandler: null
        }, options);

        return this.each(function(i) {
            var el = $(this);
            var jWin = $(options.container);
            var isWin = options.container == window;

            if (options.forceAbsolute) {
                if (isWin) {
                    el.remove().appendTo("body");
                } else {
                    el.remove().appendTo(jWin.get(0));
                }
            }

            el.css("position", "absolute");
            var heightFudge = isWin ? 2.0 : 1.8;
            var x = (isWin ? jWin.width() : jWin.outerWidth()) / 2 - el.outerWidth() / 2;
            var y = (isWin ? jWin.height() : jWin.outerHeight()) / heightFudge - el.outerHeight() / 2;
            el.css("left", x + jWin.scrollLeft()).css("top", y + jWin.scrollTop());

            if (options.completeHandler) options.completeHandler(this);
        });
    };

    $.fn.chooseDate = function(options, callback) {
        options = $.extend({
            format: "d MMMM, yyyy"
        }, options);

        $.dateChooser.show(this, options, callback);

        return this;
    };

    $.fn.enableIE6Transparency = function(options) {
        options = $.extend({
            blankImageUrl: "/i/_.gif"
        }, options);

        if (navigator.appName == "Microsoft Internet Explorer" && parseInt(navigator.appVersion) == 4 && (navigator.appVersion.indexOf("MSIE 5.5") != -1 || navigator.appVersion.indexOf("MSIE 6.0") != -1)) {
            return this.each(function(i) { $(this).css("filter", "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled='true', src='" + $(this).attr("src") + "')").attr("src", options.blankImageUrl); });
        } else {
            return this;
        }
    };

    $.fn.enableIE6BackgroundTransparency = function(options) {
        options = $.extend({
            blankImageUrl: "/i/_.gif",
            scale: false
        }, options);

        if (navigator.appName == "Microsoft Internet Explorer" && parseInt(navigator.appVersion) == 4 && (navigator.appVersion.indexOf("MSIE 5.5") != -1 || navigator.appVersion.indexOf("MSIE 6.0") != -1)) {
            return this.each(function(i) {
                var bgCss = $(this).css("background-image");
                $(this).css({ "background-image": "none", "filter": "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled='true', sizingMethod='" + (options.scale ? "scale" : "crop") + "', src='" + bgCss.substring(5, bgCss.length - 2) + "')" });
            });
        } else {
            return this;
        }
    };

    $.fn.limitHeight = function(options) {
        options = $.extend({
            value: 100
        }, options);

        return this.each(function(i) {
            if ($(this).height() > options.value) $(this).css({ "height": options.value + "px", "overflow": "hidden", "text-overflow": "ellipsis" });
        });
    };

    $.fn.overlay = function(options, callback) {
        options = $.extend({
            zIndex: 1000,
            color: "#303132",
            opacity: 0.87,
            click: null
        }, options);

        $.overlay.show(options);

        var iMax = this.length;
        return this.each(function(i) {
            $(this).addClass("Phizz_Overlay").css({ "z-index": (options.zIndex + $.overlay.zIndexStack.length + 1), "visibility": "visible", "opacity": 0 }).alignCenter().fadeTo(200, 1, i == iMax - 1 ? callback : null);
        });
    };

    $.fn.unoverlay = function(callback) {
        $.overlay.hide();

        var iMax = this.length;
        return this.each(function(i) {
            $(this).removeClass("Phizz_Overlay").fadeTo(200, 0, function() { $(this).css({ "top:": "-2000px", "visibility": "hidden" }); if (i == iMax - 1 && callback != null) callback(); });
        });
    };

    $.fn.rotateImages = function(options) {
        options = $.extend({
            urlArray: [],
            delay: 5000,
            speed: 750
        }, options);

        return this.each(function(i) {
            $.rotator.initialise($(this), options.urlArray, options.delay, options.speed);
        });
    }




    /* ---------- Backwards Compatibility ---------- */

    $.backwardsCompatibility = {
        hideIE6Foreground: function() {
            $("object, select").each(function() {
                $(this).css("visibility", "hidden").addClass("IE6Foreground");
            });
        },
        unhideIE6Foreground: function() {
            $(".IE6Foreground").css("visibility", "visible").removeClass("IE6Foreground");
        }
    }




    /* ---------- Browser Detection ---------- */

    $.browser = {
        isWindows: function() { return /Windows/i.test(navigator.userAgent); },
        isIE: function() { return /MSIE/i.test(navigator.userAgent); },
        isIE6: function() { return /MSIE 6/i.test(navigator.userAgent); },
        isIE7: function() { return /MSIE 7/i.test(navigator.userAgent); },
        isSafari: function() { return /Safari/i.test(navigator.userAgent); }
    }




    /* ---------- Cookies ---------- */

    $.cookie = {
        set: function(name, value, days) {
            var expires = "";
            if (days) {
                var date = new Date();
                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                var expires = "; expires=" + date.toGMTString();
            }
            document.cookie = name + "=" + value + expires + "; path=/";
        },

        get: function(name) {
            var nameEQ = name + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') c = c.substring(1, c.length);
                if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
            }
            return null;
        },

        remove: function(name) {
            $.cookie.set(name, "", -1);
        }
    }




    /* ---------- Date Chooser  ---------- */

    $.dateChooser = {
        jqTarget: null,
        format: null,
        selectedDate: null,

        show: function(jqTarget, options, callback) {
            if (document.getElementById("DateChooser") == null) {
                $(document.body).append("<div id=\"DateChooser\"><div class=\"content\"><table class=\"navigation\"><tbody><tr><td onclick=\"$.dateChooser.addYears(-1);\">&lt; Year</td><td onclick=\"$.dateChooser.addMonths(-1);\">&lt; Month</td><th> </th><td onclick=\"$.dateChooser.addMonths(1);\">Month &gt;</td><td onclick=\"$.dateChooser.addYears(1);\">Year &gt;</td></tr></tbody></table><div id=\"DateChooserCalendar\"> </div></div></div>");
                $("#DateChooser table.navigation td").mouseover(function() { $(this).addClass("focus"); }).mouseout(function() { $(this).removeClass("focus"); });
            }

            this.jqTarget = jqTarget;
            this.format = options.format;

            var date = $.dateTime.isValid(jqTarget.eq(0).val(), this.format) ? $.dateTime.parse(jqTarget.eq(0).val(), this.format) : new Date();
            this.selectedDate = new Date(date.getFullYear(), date.getMonth(), 1);
            this.refresh();

            $("#DateChooser").overlay({ click: function() { $.dateChooser.hide(); } }, callback);
        },

        hide: function() {
            $("#DateChooser").unoverlay();
        },

        refresh: function() {
            $("#DateChooser table.navigation th").html($.dateTime.monthNames[this.selectedDate.getMonth()].substr(0, 3).concat(" ", this.selectedDate.getFullYear()));

            var sBody = "";
            var i = 1;
            var firstDayOfWeek = this.selectedDate.getDay() - 1; if (firstDayOfWeek == -1) firstDayOfWeek = 6;
            var lastDayInMonth = $.dateTime.daysInMonth(this.selectedDate);
            var today = new Date();

            while (i <= lastDayInMonth) {
                sBody = sBody.concat("<tr>");
                for (var d = 0; d < 7; d++) {
                    if ((i == 1 && firstDayOfWeek > d) || i == lastDayInMonth + 1) {
                        sBody = sBody.concat("<td class=\"e\">&nbsp;</td>");
                    } else {
                        sBody = sBody.concat("<td", (this.selectedDate.getFullYear() == today.getFullYear() && this.selectedDate.getMonth() == today.getMonth() && i == today.getDate() ? " class=\"today\"" : ""), ">", i++, "</td>");
                    }
                }
                sBody = sBody.concat("</tr>");
            }

            $("#DateChooserCalendar").html("<table class=\"calendar\" cellpadding=\"0\" cellspacing=\"0\"><thead><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></thead><tbody>".concat(sBody, "</tbody></table>"));
            $("#DateChooser table.calendar td:not(.e)").click(function() { $.dateChooser.submit(parseInt($(this).html())); }).mouseover(function() { $(this).addClass("focus"); }).mouseout(function() { $(this).removeClass("focus"); });
        },

        submit: function(day) {
            this.selectedDate.setDate(day);
            this.jqTarget.val($.dateTime.toString(this.selectedDate, this.format));
            this.jqTarget.keyup();
            this.hide();
        },

        addMonths: function(n) {
            this.selectedDate.setMonth(this.selectedDate.getMonth() + n);
            this.refresh();
        },

        addYears: function(n) {
            this.selectedDate.setFullYear(this.selectedDate.getFullYear() + n);
            this.refresh();
        }
    };




    /* ---------- Date ---------- */

    $.dateTime = {

        monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
        dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],

        toString: function(dt, strFormat) {
            strFormat = strFormat.replace(/yyyy/g, dt.getFullYear()).replace(/yy/g, dt.getFullYear().toString().substr(2, 2));
            strFormat = strFormat.replace(/MMMM/g, "$1").replace(/MMM/g, "$2").replace(/MM/g, $.padStart((dt.getMonth() + 1).toString(), "0", 2)).replace(/M/g, dt.getMonth() + 1);
            strFormat = strFormat.replace(/dddd/g, "$3").replace(/ddd/g, "$4").replace(/dd/g, $.padStart(dt.getDate().toString(), "0", 2)).replace(/d/g, dt.getDate());
            strFormat = strFormat.replace(/HH/g, $.padStart(dt.getHours().toString(), "0", 2)).replace(/H/g, dt.getHours());
            strFormat = strFormat.replace(/hh/g, $.padStart((dt.getHours() > 12 ? dt.getHours() - 12 : dt.getHours()).toString(), "0", 2)).replace(/h/g, dt.getHours() > 12 ? dt.getHours() - 12 : dt.getHours());
            strFormat = strFormat.replace(/mm/g, $.padStart(dt.getMinutes().toString(), "0", 2)).replace(/m/g, dt.getMinutes());
            strFormat = strFormat.replace(/ss/g, $.padStart(dt.getSeconds().toString(), "0", 2)).replace(/s/g, dt.getSeconds());
            strFormat = strFormat.replace(/tt/g, dt.getHours() > 12 ? "PM" : "AM").replace(/t/g, dt.getHours() > 12 ? "P" : "A");
            strFormat = strFormat.replace(/\$1/g, this.monthNames[dt.getMonth()]).replace(/\$2/g, this.monthNames[dt.getMonth()].substr(0, 3)).replace(/\$3/g, this.dayNames[dt.getDay()]).replace(/\$4/g, this.dayNames[dt.getDay()].substr(0, 3));
            return strFormat;
        },

        isValid: function(strDate, strFormat) {
            var dt;
            try {
                dt = this.parse(strDate, strFormat);
                return true;
            } catch (err) { }
            return false;
        },

        parse: function(strDate, strFormat) {
            strDate = strDate.toLowerCase();

            var i;
            if (strFormat.indexOf("MMMM") > -1) for (i = 0; i < this.monthNames.length; i++) if (strDate.indexOf(this.monthNames[i].toLowerCase()) > -1) {
                strDate = strDate.replace(this.monthNames[i].toLowerCase(), i < 9 ? "0" + (i + 1) : i + 1);
                strFormat = strFormat.replace("MMMM", "MM");
                break;
            }
            if (strFormat.indexOf("MMM") > -1) for (i = 0; i < this.monthNames.length; i++) if (strDate.indexOf(this.monthNames[i].toLowerCase().substr(0, 3)) > -1) {
                strDate = strDate.replace(this.monthNames[i].toLowerCase().substr(0, 3), i < 9 ? "0" + (i + 1) : i + 1);
                strFormat = strFormat.replace("MMM", "MM");
                break;
            }

            var parsedDateElements = { year: null, month: null, day: null, hours: null, minutes: null, seconds: null, ampm: null };

            var tokens = new Array();
            while (strFormat.length > 0) {
                var obj = (function(s) {
                    var token = "";
                    if (s.search(/[yMdHhmst]/) == 0) {
                        do {
                            token = token.concat(s.charAt(0));
                            s = s.substr(1);
                        } while (s.length > 0 && s.charAt(0) == token.charAt(0));
                    } else {
                        token = "'";
                        do {
                            token = token.concat(s.charAt(0));
                            s = s.substr(1);
                        } while (s.length > 0 && s.search(/[yMdHhmst]/) != 0);
                        token = token.concat("'");
                    }
                    return { token: token, strFormat: s };
                })(strFormat);
                tokens.push(obj.token);
                strFormat = obj.strFormat;
            }

            for (i = 0; i < tokens.length; i++) {
                var token = tokens[i];
                if (/^'[^']+'$/.test(token)) {
                    token = token.substr(1, token.length - 2);
                    if (strDate.indexOf(token) == 0) {
                        strDate = strDate.substr(token.length);
                    } else {
                        throw new Error("Input string does not match format string.");
                    }
                } else if (token == "yyyy") {
                    if (parsedDateElements.year != null) throw new Error("Invalid format string. Year cannot be parsed more than once.");
                    parsedDateElements.year = parseInt(strDate.substr(0, 4));
                    strDate = strDate.substr(4);
                } else if (token == "yy") {
                    if (parsedDateElements.year != null) throw new Error("Invalid format string. Year cannot be parsed more than once.");
                    parsedDateElements.year = parseInt("20".concat(strDate.substr(0, 2)));
                    strDate = strDate.substr(2);
                } else if (token == "MM") {
                    if (parsedDateElements.month != null) throw new Error("Invalid format string. Month cannot be parsed more than once.");
                    parsedDateElements.month = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "M") {
                    if (parsedDateElements.month != null) throw new Error("Invalid format string. Month cannot be parsed more than once.");
                    if (/^1[012]/.test(strDate)) {
                        parsedDateElements.month = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.month = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "dd") {
                    if (parsedDateElements.day != null) throw new Error("Invalid format string. Day cannot be parsed more than once.");
                    if (!/^(0[1-9]|[12][0-9]|3[01])/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.day = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "d") {
                    if (parsedDateElements.day != null) throw new Error("Invalid format string. Day cannot be parsed more than once.");
                    if (!/^([1-9]|[12][0-9]|3[01])/.test(strDate)) throw new Error("Input string does not match format string.");
                    if (/^([12][0-9]|3[01])/.test(strDate)) {
                        parsedDateElements.day = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.day = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "HH") {
                    if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
                    if (!/^([0-1][0-9]|2[0-3])/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.hours = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "H") {
                    if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
                    if (!/^(0-9|1[0-9]|2[0-3])/.test(strDate)) throw new Error("Input string does not match format string.");
                    if (/^(1[0-9]|2[0-3])/.test(strDate)) {
                        parsedDateElements.hours = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.hours = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "hh") {
                    if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
                    if (!/^(0[1-9]|1[0-2])/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.hours = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "h") {
                    if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
                    if (!/^([1-9]|1[0-2])/.test(strDate)) throw new Error("Input string does not match format string.");
                    if (/^1[0-2]/.test(strDate)) {
                        parsedDateElements.hours = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.hours = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "mm") {
                    if (parsedDateElements.minutes != null) throw new Error("Invalid format string. Minutes cannot be parsed more than once.");
                    if (!/^[0-5]?[0-9]/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.minutes = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "m") {
                    if (parsedDateElements.minutes != null) throw new Error("Invalid format string. Minutes cannot be parsed more than once.");
                    if (/^[1-5][0-9]/.test(strDate)) {
                        parsedDateElements.minutes = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.minutes = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "ss") {
                    if (parsedDateElements.seconds != null) throw new Error("Invalid format string. Seconds cannot be parsed more than once.");
                    if (!/^[0-5]?[0-9]/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.seconds = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
                    strDate = strDate.substr(2);
                } else if (token == "s") {
                    if (parsedDateElements.seconds != null) throw new Error("Invalid format string. Seconds cannot be parsed more than once.");
                    if (/^[1-5][0-9]/.test(strDate)) {
                        parsedDateElements.seconds = parseInt(strDate.substr(0, 2));
                        strDate = strDate.substr(2);
                    } else {
                        parsedDateElements.seconds = parseInt(strDate.substr(0, 1));
                        strDate = strDate.substr(1);
                    }
                } else if (token == "tt") {
                    if (parsedDateElements.ampm != null) throw new Error("Invalid format string. AM/PM cannot be parsed more than once.");
                    if (!/^[ap]m/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.ampm = strDate.substr(0, 1);
                    strDate = strDate.substr(2);
                } else if (token == "t") {
                    if (parsedDateElements.ampm != null) throw new Error("Invalid format string. AM/PM cannot be parsed more than once.");
                    if (!/^[ap]/.test(strDate)) throw new Error("Input string does not match format string.");
                    parsedDateElements.ampm = strDate.substr(0, 1);
                    strDate = strDate.substr(1);
                }
            }

            if (strDate.length > 0) throw new Error("Input string does not match format string.");

            if (parsedDateElements.hours < 12 && parsedDateElements.ampm == "p") parsedDateElements.hours += 12;

            var dt = new Date();
            if (parsedDateElements.year != null) dt.setFullYear(parsedDateElements.year);
            dt.setMonth(parsedDateElements.month != null ? parsedDateElements.month - 1 : 0);
            dt.setDate(parsedDateElements.day != null ? parsedDateElements.day : 1);
            dt.setHours(parsedDateElements.hours != null ? parsedDateElements.hours : 0);
            dt.setMinutes(parsedDateElements.minutes != null ? parsedDateElements.minutes : 0);
            dt.setSeconds(parsedDateElements.seconds != null ? parsedDateElements.seconds : 0);

            return dt;
        },

        daysInMonth: function(date) {
            switch (date.getMonth() + 1) {
                case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; break;
                case 4: case 6: case 9: case 11: return 30; break;
                case 2: return this.isLeapYear(date.getFullYear()) ? 29 : 28; break;
            }
        },

        isLeapYear: function(year) {
            return year % 4 == 0 ? (year % 100 == 0 ? year % 400 == 0 : true) : false;
        }

    };




    /* ---------- Navigation  ---------- */

    $.navigate = function(url) {
        if (window.event) window.event.returnValue = false;
        window.location.href = url;
    };

    $.openWindow = function(url) {
        window.open(url);
    };

    $.queryString = {
        path: null,
        keys: null,
        values: null,

        parse: function() {
            if (this.keys != null) return;
            var url = window.location.href;
            this.keys = new Array();
            this.values = new Array();
            if (url.indexOf("?") > -1) {
                var n = url.indexOf("?");
                this.path = url.substr(0, n);
                var pairs = url.substr(n + 1).split("&");
                for (var i = 0; i < pairs.length; i++) {
                    n = pairs[i].indexOf("=");
                    this.append(pairs[i].substr(0, n), $.urlDecode(pairs[i].substr(n + 1)), true);
                }
            } else {
                this.path = url;
            }
        },

        commit: function() {
            var qs = "";
            for (var i = 0; i < this.keys.length; i++) qs = qs.concat(qs.length > 0 ? "&" : "", $.urlEncode(this.keys[i]), "=", $.urlEncode(this.values[this.keys[i]]));
            $.navigate(this.path + (qs.length > 0 ? "?" + qs : ""));
        },

        get: function(key) {
            this.parse();
            return this.values[key];
        },

        hasKey: function(key) {
            return this.get(key) != null;
        },

        append: function(key, value, cancelCommit) {
            this.parse();

            if (this.hasKey(key) && this.values[key].length > 0) {
                if (this.values[key] != value) this.values[key] = this.values[key].concat(",", value);
            } else {
                this.keys.push(key);
                this.values[key] = value;
            }

            if (!cancelCommit) this.commit();
        },

        appendMultiple: function(keysAndValues, cancelCommit) {
            for (var key in keysAndValues) this.append(key, keysAndValues[key], true);
            if (!cancelCommit) this.commit();
        },

        set: function(key, value, cancelCommit) {
            this.parse();

            if (!this.hasKey(key)) this.keys.push(key);
            this.values[key] = value.toString();

            if (!cancelCommit) this.commit();
        },

        setMultiple: function(keysAndValues, cancelCommit) {
            for (var key in keysAndValues) this.set(key, keysAndValues[key], true);
            if (!cancelCommit) this.commit();
        },

        remove: function(key, cancelCommit) {
            this.parse();

            if (this.hasKey(key)) {
                for (var i = 0; i < this.keys.length; i++) if (this.keys[i] == key) {
                    this.keys.splice(i, 1);
                    break;
                }
                delete this.values[key];
            }

            if (!cancelCommit) this.commit();
        },

        removeMultiple: function(keys, cancelCommit) {
            for (var i = 0; i < keys.length; i++) this.remove(keys[i], true);
            if (!cancelCommit) this.commit();
        },

        unappend: function(key, value, cancelCommit) {
            this.parse();

            if (this.hasKey(key) && this.values[key].length > 0) {
                if (this.values[key] == value) {
                    this.remove(key, true);
                } else {
                    this.values[key] = this.values[key].replace(new RegExp("(^|,)" + value + "(,|$)", "g"), "");
                }
            }

            if (!cancelCommit) this.commit();
        }
    };




    /* ---------- Overlay  ---------- */

    $.overlay = {
        zIndexStack: new Array(),

        show: function(options) {
            if (document.getElementById("Phizz_Overlay") == null) $(document.body).append("<div id=\"Phizz_Overlay\" style=\"position:absolute;visibility:hidden;width:100%;height:1px;top:-1px;left:0px;\">&nbsp;</div>");

            this.zIndexStack.push(options.zIndex);
            var jq = $("#Phizz_Overlay");
            jq.css("z-index", options.zIndex);

            if (this.zIndexStack.length == 1) {
                jq.css({ "visibility": "visible", "top": "0px", "height": $(document).height(), "opacity": 0, "background-color": options.color }).fadeTo(200, options.opacity);
                if ($.browser.isIE6()) $.backwardsCompatibility.hideIE6Foreground();
                $(window).bind("resize", $.overlay.onWindowResize).bind("scroll", $.overlay.onWindowScroll);
                //} else if (parseFloat(jq.css("opacity")) != options.opacity) {
            }

            if (options.click != null) jq.bind("click", options.click);
        },

        hide: function() {
            this.zIndexStack.pop();
            if (this.zIndexStack.length == 0) {
                $("#Phizz_Overlay").fadeTo(200, 0, function() { $(this).css({ "visibility": "hidden", "top": "-1px", "height": "1px" }); if ($.browser.isIE6()) $.backwardsCompatibility.unhideIE6Foreground(); }).unbind("click");
                $(window).unbind("resize", $.overlay.onWindowResize).unbind("scroll", $.overlay.onWindowScroll);
            } else {
                $("#Phizz_Overlay").css("z-index", this.zIndexStack[this.zIndexStack.length - 1]);
            }
        },

        onWindowResize: function() {
            $(".Phizz_Overlay").alignCenter();
        },

        onWindowScroll: function() {
            $(".Phizz_Overlay").alignCenter();
        }
    };




    /* ---------- Rotator  ---------- */

    //TODO: handle elements with no id, handle elements with no <img>, handle non-<img> / multiple <img> child elements
    $.rotator = {
        urlArrays: null,
        indices: null,
        delays: null,
        speeds: null,

        initialise: function(jq, urlArray, delay, speed) {
            if (this.urlArrays == null) this.urlArrays = new Array();
            if (this.indices == null) this.indices = new Array();
            if (this.delays == null) this.delays = new Array();
            if (this.speeds == null) this.speeds = new Array();

            var id = jq.attr("id");
            this.indices[id] = 0;
            this.delays[id] = delay;
            this.speeds[id] = speed;

            this.urlArrays[id] = new Array(urlArray.length);
            var initialSrc = jq.find("img:eq(0)").attr("src");
            for (var i = 0; i < this.urlArrays[id].length; i++) {
                this.urlArrays[id][i] = urlArray.splice(Math.floor(Math.random() * urlArray.length), 1)[0];
                if (this.urlArrays[id][i] == initialSrc) { this.indices[id] = i; this.indices[id]; }
            }
            setTimeout(function() { $.rotator.rotate(jq); }, this.delays[id]);
        },

        rotate: function(jq) {
            var id = jq.attr("id");
            this.indices[id] = (this.indices[id] == this.urlArrays[id].length - 1) ? 0 : this.indices[id] + 1;
            var img = new Image();
            $(img).load(function() {
                $(this).hide();
                jq.append(this);
                $(this).fadeIn($.rotator.speeds[id], function() {
                    jq.find("img:eq(0)").remove();
                    setTimeout(function() { $.rotator.rotate(jq); }, $.rotator.delays[id]);
                });
            }).attr("src", this.urlArrays[id][this.indices[id]]);
        }
    };




    /* ---------- Utilities ---------- */

    $.indexOfFirst = function(str, arrayofSubstrings) {
        var n = str.length;
        for (var i = 0; i < arrayofSubstrings.length; i++) {
            var m = str.indexOf(arrayofSubstrings[i]);
            if (m > -1) n = Math.min(n, m);
        }
        return n == str.length ? -1 : n;
    },

    $.padStart = function(strToPad, chrPadding, intLength) {
        while (strToPad.length < intLength) strToPad = chrPadding.concat(strToPad);
        return strToPad;
    };

    $.padEnd = function(strToPad, chrPadding, intLength) {
        while (strToPad.length < intLength) strToPad = strToPad.concat(chrPadding);
        return strToPad;
    };

    $.preloadImages = function() {
        for (var i = 0; i < arguments.length; i++) jQuery("<img>").attr("src", arguments[i]);
    };

    $.substringAfter = function(s1, s2) {
        return s1.substr(s1.indexOf(s2) + 1);
    };

    $.substringAfterLast = function(s1, s2) {
        return s1.substr(s1.lastIndexOf(s2) + 1);
    };

    $.substringBefore = function(s1, s2) {
        return s1.indexOf(s2) > -1 ? s1.substr(0, s1.indexOf(s2)) : s1;
    };

    $.substringBeforeLast = function(s1, s2) {
        return s1.indexOf(s2) > -1 ? s1.substr(0, s1.lastIndexOf(s2)) : s1;
    };

    $.toValidHtmlId = function(id) {
        if (/^[a-zA-Z]([a-zA-Z0-9\-_])*$/.test(id)) return id;
        id = id.replace(/[^a-zA-Z0-9\-_]+/g, "_");
        id = id.replace(/^_+/, "");
        return id;
    };

    $.urlEncode = function(value) {
        if (value == null) {
            return null;

        } else if (typeof value == "object") {
            var result = "";
            for (var key in value) result = result.concat(result.length > 0 ? "&" : "", key, "=", $.urlEncode(value[key]));
            return result;

        } else {
            var SAFECHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.!~*'()";
            var HEX = "0123456789abcdef";
            value = value.toString();

            var result = "";
            for (var i = 0; i < value.length; i++) {
                var c = value.charAt(i);
                if (c == " ") {
                    result += "+";
                } else if (SAFECHARS.indexOf(c) != -1) {
                    result += c;
                } else {
                    var charCode = c.charCodeAt(0);
                    if (charCode > 255) {
                        result += "+";
                    } else {
                        result += "%";
                        result += HEX.charAt((charCode >> 4) & 0xF);
                        result += HEX.charAt(charCode & 0xF);
                    }
                }
            }

            return result;
        }
    };

    $.urlDecode = function(value) {
        var HEXCHARS = "0123456789ABCDEFabcdef";

        var result = "";
        var i = 0;
        while (i < value.length) {
            var c = value.charAt(i);
            if (c == "+") {
                result += " ";
                i++;
            } else if (c == "%") {
                if (i < (value.length - 2) && HEXCHARS.indexOf(value.charAt(i + 1)) != -1 && HEXCHARS.indexOf(value.charAt(i + 2)) != -1) {
                    result += unescape(value.substr(i, 3));
                    i += 3;
                } else {
                    result += "%[ERROR]";
                    i++;
                }
            } else {
                result += c;
                i++;
            }
        }

        return result;
    };




    /* ---------- Validation  ---------- */

    $.validation = {

        alert: function(s) { window.alert(s); },
        autoFocus: true,
        element: null,

        setElement: function(el) {
            this.element = el;
            if (this.autoFocus && $(el).css("visibility") != "hidden") el.focus();
        },

        hasValue: function(id) {
            var el = document.getElementById(id);
            if ($.trim(el.value).length == 0) {
                this.alert("Please provide a value for the " + el.title + " field.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        matchesRegex: function(id, regExp, errorMessage, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!regExp.test(el.value)) {
                this.alert(errorMessage ? errorMessage : "Please enter a valid value.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        hasEquivalence: function(id1, id2) {
            var el1 = document.getElementById(id1);
            var el2 = document.getElementById(id2);
            if ($.trim(el1.value) != $.trim(el2.value)) {
                this.alert("The " + el1.title + " and " + el2.title + " field values must be equivalent.");
                this.setElement(el1);
                return false;
            }
            return true;
        },

        matchesEmail: function(id, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!this.isEmail(el.value)) {
                this.alert("Please enter a valid email address in the " + el.title + " field.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        isEmail: function(s) {
            return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(s);
        },

        matchesInteger: function(id, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!this.isInteger(el.value)) {
                this.alert("Please enter a valid integer value in the " + el.title + " field.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        isInteger: function(s) {
            return /^-?\d+$/.test(s);
        },

        matchesDecimal: function(id, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!this.isDecimal(el.value)) {
                this.alert("Please enter a valid numeric value in the " + el.title + " field.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        isDecimal: function(s) {
            return /^-?\d+(\.\d+)?$/.test(s);
        },

        matchesDate: function(id, formatString, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!$.dateTime.isValid(el.value, formatString)) {
                this.alert("Please enter a valid date value in the " + el.title + " field, in the format " + formatString + ".");
                this.setElement(el);
                return false;
            }
            return true;
        },

        matchesHexColor: function(id, allowEmpty) {
            var el = document.getElementById(id);
            if (allowEmpty && el.value.length == 0) return true;
            if (!this.isHexColor(el.value)) {
                this.alert("Please enter a valid hexadecimal colour value in the " + el.title + " field.");
                this.setElement(el);
                return false;
            }
            return true;
        },

        isHexColor: function(s) {
            return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s);
        },

        hasSelection: function(id, allowEmpty, errorMessage) {
            var el = document.getElementById(id);

            if (el != null && el.nodeName.toLowerCase() == "select") { // if this is a dropdown selection
                // if (el.selectedIndex == 0 || (!allowEmpty && el.options[el.selectedIndex].value.length == 0)) {
                if (el.selectedIndex == 0 && el.options[el.selectedIndex].value.length == 0) {
                    this.alert(errorMessage ? errorMessage : "A value must be selected for the " + el.title + " field.");
                    this.setElement(el);
                    return false;
                }
                return true;

            } else if (document.getElementsByName(id).length > 0) { // if this is a checkbox or radio button list
                var checked = false;
                var elArray = document.getElementsByName(id);
                for (var i = 0; i < elArray.length; i++) if (elArray[i].checked) {
                    checked = true;
                    break;
                }
                if (!checked) {
                    this.alert(errorMessage ? errorMessage : "A value must be selected for the " + elArray[0].title + " field.");
                    this.setElement(elArray[0]);
                    return false;
                }
                return true;
            }
        },

        hasSelectionCount: function(name, minimum, maximum, errorMessage) {
            var jq = $("*[name=" + name + "]");
            var elFirst = jq.get(0);
            var count = jq.filter(":checked").length;
            if (minimum == 0 && maximum > 0 && count > maximum) {
                this.alert(errorMessage ? errorMessage : "A maximum of " + maximum + " item" + (maximum == 1 ? "" : "s") + " can be selected for the " + elFirst.title + " field.");
                this.setElement(elFirst);
                return false;
            } else if (minimum > 0 && maximum == 0 && count < minimum) {
                this.alert(errorMessage ? errorMessage : "A minimum of " + minimum + " item" + (minimum == 1 ? "" : "s") + " must be selected for the " + elFirst.title + " field.");
                this.setElement(elFirst);
                return false;
            } else if (minimum > 0 && maximum > 0 && (count < minimum || count > maximum)) {
                this.alert(errorMessage ? errorMessage : "Between " + minimum + " and " + maximum + " items must be selected for the " + elFirst.title + " field.");
                this.setElement(elFirst);
                return false;
            }
            return true;
        }

    };




    /* ---------- XML ---------- */

    $.createXmlDocument = function(xml) {
        var objXmlDocument = null;
        if (window.ActiveXObject) {
            objXmlDocument = new ActiveXObject("MSXML2.FreeThreadedDOMDocument.3.0");
            if (xml != null && typeof (xml) == "string") {
                objXmlDocument.async = false;
                objXmlDocument.loadXML(xml);
            }
        } else if (window.DOMParser) {
            objXmlDocument = (xml != null && typeof (xml) == "string") ? (new DOMParser()).parseFromString(xml, "text/xml") : document.implementation.createDocument("ns", "root", null);
        }
        return objXmlDocument;
    },

	$.convertXmlDocumentToString = function(objXmlDocument) {
	    if (window.ActiveXObject) {
	        return objXmlDocument.documentElement.xml;
	    } else if (window.DOMParser) {
	        return (new XMLSerializer()).serializeToString(objXmlDocument);
	    }
	    return "";
	},

	$.xslt = function(objXmlDocument, objXslDocument, params) {
	    if (window.ActiveXObject) {
	        var objXSLTemplate = new ActiveXObject("MSXML2.XSLTemplate.3.0");
	        objXSLTemplate.stylesheet = objXslDocument;
	        var objProcessor = objXSLTemplate.createProcessor();
	        objProcessor.input = objXmlDocument;
	        if (params != null && typeof (params) == "object") for (var key in params) objProcessor.addParameter(key, params[key]);
	        objProcessor.transform();
	        return objProcessor.output;
	    } else if (window.XSLTProcessor) {
	        var objProcessor = new XSLTProcessor();
	        objProcessor.importStylesheet(objXslDocument);
	        if (params != null && typeof (params) == "object") for (var key in params) objProcessor.setParameter("", key, params[key]);
	        var objXmlTransformation = objProcessor.transformToDocument(objXmlDocument);
	        return (new XMLSerializer()).serializeToString(objXmlTransformation);
	    }
	    return "";
	}

})(jQuery);