﻿/// <reference path="/Content/scripts/jquery-1.4.4.js" />
/// <reference path="/Content/scripts/bloo-1.0.1.js" />

// jquery.blockUI
; (function ($) {
    if (/1\.(0|1|2)\.(0|1|2)/.test($.fn.jquery) || /^1.1/.test($.fn.jquery)) {
        alert('blockUI requires jQuery v1.2.3 or later!  You are using v' + $.fn.jquery);
        return;
    }

    $.fn._fadeIn = $.fn.fadeIn;

    var noOp = function () { };

    // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
    // retarded userAgent strings on Vista)
    var mode = document.documentMode || 0;
    var setExpr = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8);
    var ie6 = $.browser.msie && /MSIE 6.0/.test(navigator.userAgent) && !mode;

    // global $ methods for blocking/unblocking the entire page
    $.blockUI = function (opts) { install(window, opts); };
    $.unblockUI = function (opts) { remove(window, opts); };

    // convenience method for quick growl-like notifications  (http://www.google.com/search?q=growl)
    $.growlUI = function (title, message, timeout, onClose) {
        var $m = $('<div class="growlUI"></div>');
        if (title) $m.append('<h1>' + title + '</h1>');
        if (message) $m.append('<h2>' + message + '</h2>');
        if (timeout == undefined) timeout = 3000;
        $.blockUI({
            message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
            timeout: timeout, showOverlay: false,
            onUnblock: onClose,
            css: $.blockUI.defaults.growlCSS
        });
    };

    // plugin method for blocking element content
    $.fn.block = function (opts) {
        return this.unblock({ fadeOut: 0 }).each(function () {
            if ($.css(this, 'position') == 'static')
                this.style.position = 'relative';
            if ($.browser.msie)
                this.style.zoom = 1; // force 'hasLayout'
            install(this, opts);
        });
    };

    // plugin method for unblocking element content
    $.fn.unblock = function (opts) {
        return this.each(function () {
            remove(this, opts);
        });
    };

    $.blockUI.version = 2.33; // 2nd generation blocking at no extra cost!

    // override these in your code to change the default behavior and style
    $.blockUI.defaults = {
        // message displayed when blocking (use null for no message)
        message: '<h1>Please wait...</h1>',

        title: null,   // title string; only used when theme == true
        draggable: true,  // only used when theme == true (requires jquery-ui.js to be loaded)

        theme: false, // set to true to use with jQuery UI themes

        // styles for the message when blocking; if you wish to disable
        // these and use an external stylesheet then do this in your code:
        // $.blockUI.defaults.css = {};
        css: {
            padding: 0,
            margin: 0,
            width: '30%',
            top: '40%',
            left: '35%',
            textAlign: 'center',
            color: '#000',
            border: '3px solid #aaa',
            backgroundColor: '#fff',
            cursor: 'wait'
        },

        // minimal style set used when themes are used
        themedCSS: {
            width: '30%',
            top: '40%',
            left: '35%'
        },

        // styles for the overlay
        overlayCSS: {
            backgroundColor: '#000',
            opacity: 0.6,
            cursor: 'wait'
        },

        // styles applied when using $.growlUI
        growlCSS: {
            width: '350px',
            top: '10px',
            left: '',
            right: '10px',
            border: 'none',
            padding: '5px',
            opacity: 0.6,
            cursor: 'default',
            color: '#fff',
            backgroundColor: '#000',
            '-webkit-border-radius': '10px',
            '-moz-border-radius': '10px',
            'border-radius': '10px'
        },

        // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
        // (hat tip to Jorge H. N. de Vasconcelos)
        iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',

        // force usage of iframe in non-IE browsers (handy for blocking applets)
        forceIframe: false,

        // z-index for the blocking overlay
        baseZ: 1000,

        // set these to true to have the message automatically centered
        centerX: true, // <-- only effects element blocking (page block controlled via css above)
        centerY: true,

        // allow body element to be stetched in ie6; this makes blocking look better
        // on "short" pages.  disable if you wish to prevent changes to the body height
        allowBodyStretch: true,

        // enable if you want key and mouse events to be disabled for content that is blocked
        bindEvents: true,

        // be default blockUI will supress tab navigation from leaving blocking content
        // (if bindEvents is true)
        constrainTabKey: true,

        // fadeIn time in millis; set to 0 to disable fadeIn on block
        fadeIn: 200,

        // fadeOut time in millis; set to 0 to disable fadeOut on unblock
        fadeOut: 400,

        // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
        timeout: 0,

        // disable if you don't want to show the overlay
        showOverlay: true,

        // if true, focus will be placed in the first available input field when
        // page blocking
        focusInput: true,

        // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
        applyPlatformOpacityRules: true,

        // callback method invoked when fadeIn has completed and blocking message is visible
        onBlock: null,

        // callback method invoked when unblocking has completed; the callback is
        // passed the element that has been unblocked (which is the window object for page
        // blocks) and the options that were passed to the unblock call:
        //	 onUnblock(element, options)
        onUnblock: null,

        // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
        quirksmodeOffsetHack: 4
    };

    // private data and functions follow...

    var pageBlock = null;
    var pageBlockEls = [];

    function install(el, opts) {
        var full = (el == window);
        var msg = opts && opts.message !== undefined ? opts.message : undefined;
        opts = $.extend({}, $.blockUI.defaults, opts || {});
        opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
        var css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
        var themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
        msg = msg === undefined ? opts.message : msg;

        // remove the current block (if there is one)
        if (full && pageBlock)
            remove(window, { fadeOut: 0 });

        // if an existing element is being used as the blocking content then we capture
        // its current place in the DOM (and current display style) so we can restore
        // it when we unblock
        if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
            var node = msg.jquery ? msg[0] : msg;
            var data = {};
            $(el).data('blockUI.history', data);
            data.el = node;
            data.parent = node.parentNode;
            data.display = node.style.display;
            data.position = node.style.position;
            if (data.parent)
                data.parent.removeChild(node);
        }

        var z = opts.baseZ;

        // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
        // layer1 is the iframe layer which is used to supress bleed through of underlying content
        // layer2 is the overlay layer which has opacity and a wait cursor (by default)
        // layer3 is the message content that is displayed while blocking

        var lyr1 = ($.browser.msie || opts.forceIframe)
		? $('<iframe class="blockUI" style="z-index:' + (z++) + ';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="' + opts.iframeSrc + '"></iframe>')
		: $('<div class="blockUI" style="display:none"></div>');
        var lyr2 = $('<div class="blockUI blockOverlay" style="z-index:' + (z++) + ';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');

        var lyr3, s;
        if (opts.theme && full) {
            s = '<div class="blockUI blockMsg blockPage ui-dialog ui-widget ui-corner-all" style="z-index:' + z + ';display:none;position:fixed">' +
				'<div class="ui-widget-header ui-dialog-titlebar blockTitle">' + (opts.title || '&nbsp;') + '</div>' +
				'<div class="ui-widget-content ui-dialog-content"></div>' +
			'</div>';
        }
        else if (opts.theme) {
            s = '<div class="blockUI blockMsg blockElement ui-dialog ui-widget ui-corner-all" style="z-index:' + z + ';display:none;position:absolute">' +
				'<div class="ui-widget-header ui-dialog-titlebar blockTitle">' + (opts.title || '&nbsp;') + '</div>' +
				'<div class="ui-widget-content ui-dialog-content"></div>' +
			'</div>';
        }
        else if (full) {
            s = '<div class="blockUI blockMsg blockPage" style="z-index:' + z + ';display:none;position:fixed"></div>';
        }
        else {
            s = '<div class="blockUI blockMsg blockElement" style="z-index:' + z + ';display:none;position:absolute"></div>';
        }
        lyr3 = $(s);

        // if we have a message, style it
        if (msg) {
            if (opts.theme) {
                lyr3.css(themedCSS);
                lyr3.addClass('ui-widget-content');
            }
            else
                lyr3.css(css);
        }

        // style the overlay
        if (!opts.applyPlatformOpacityRules || !($.browser.mozilla && /Linux/.test(navigator.platform)))
            lyr2.css(opts.overlayCSS);
        lyr2.css('position', full ? 'fixed' : 'absolute');

        // make iframe layer transparent in IE
        if ($.browser.msie || opts.forceIframe)
            lyr1.css('opacity', 0.0);

        //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
        var layers = [lyr1, lyr2, lyr3], $par = full ? $('body') : $(el);
        $.each(layers, function () {
            this.appendTo($par);
        });

        if (opts.theme && opts.draggable && $.fn.draggable) {
            lyr3.draggable({
                handle: '.ui-dialog-titlebar',
                cancel: 'li'
            });
        }

        // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
        var expr = setExpr && (!$.boxModel || $('object,embed', full ? null : el).length > 0);
        if (ie6 || expr) {
            // give body 100% height
            if (full && opts.allowBodyStretch && $.boxModel)
                $('html,body').css('height', '100%');

            // fix ie6 issue when blocked element has a border width
            if ((ie6 || !$.boxModel) && !full) {
                var t = sz(el, 'borderTopWidth'), l = sz(el, 'borderLeftWidth');
                var fixT = t ? '(0 - ' + t + ')' : 0;
                var fixL = l ? '(0 - ' + l + ')' : 0;
            }

            // simulate fixed position
            $.each([lyr1, lyr2, lyr3], function (i, o) {
                var s = o[0].style;
                s.position = 'absolute';
                if (i < 2) {
                    full ? s.setExpression('height', 'Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.boxModel?0:' + opts.quirksmodeOffsetHack + ') + "px"')
					 : s.setExpression('height', 'this.parentNode.offsetHeight + "px"');
                    full ? s.setExpression('width', 'jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"')
					 : s.setExpression('width', 'this.parentNode.offsetWidth + "px"');
                    if (fixL) s.setExpression('left', fixL);
                    if (fixT) s.setExpression('top', fixT);
                }
                else if (opts.centerY) {
                    if (full) s.setExpression('top', '(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
                    s.marginTop = 0;
                }
                else if (!opts.centerY && full) {
                    var top = (opts.css && opts.css.top) ? parseInt(opts.css.top) : 0;
                    var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + ' + top + ') + "px"';
                    s.setExpression('top', expression);
                }
            });
        }

        // show the message
        if (msg) {
            if (opts.theme)
                lyr3.find('.ui-widget-content').append(msg);
            else
                lyr3.append(msg);
            if (msg.jquery || msg.nodeType)
                $(msg).show();
        }

        if (($.browser.msie || opts.forceIframe) && opts.showOverlay)
            lyr1.show(); // opacity is zero
        if (opts.fadeIn) {
            var cb = opts.onBlock ? opts.onBlock : noOp;
            var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
            var cb2 = msg ? cb : noOp;
            if (opts.showOverlay)
                lyr2._fadeIn(opts.fadeIn, cb1);
            if (msg)
                lyr3._fadeIn(opts.fadeIn, cb2);
        }
        else {
            if (opts.showOverlay)
                lyr2.show();
            if (msg)
                lyr3.show();
            if (opts.onBlock)
                opts.onBlock();
        }

        // bind key and mouse events
        bind(1, el, opts);

        if (full) {
            pageBlock = lyr3[0];
            pageBlockEls = $(':input:enabled:visible', pageBlock);
            if (opts.focusInput)
                setTimeout(focus, 20);
        }
        else
            center(lyr3[0], opts.centerX, opts.centerY);

        if (opts.timeout) {
            // auto-unblock
            var to = setTimeout(function () {
                full ? $.unblockUI(opts) : $(el).unblock(opts);
            }, opts.timeout);
            $(el).data('blockUI.timeout', to);
        }
    };

    // remove the block
    function remove(el, opts) {
        var full = (el == window);
        var $el = $(el);
        var data = $el.data('blockUI.history');
        var to = $el.data('blockUI.timeout');
        if (to) {
            clearTimeout(to);
            $el.removeData('blockUI.timeout');
        }
        opts = $.extend({}, $.blockUI.defaults, opts || {});
        bind(0, el, opts); // unbind events

        var els;
        if (full) // crazy selector to handle odd field errors in ie6/7
            els = $('body').children().filter('.blockUI').add('body > .blockUI');
        else
            els = $('.blockUI', el);

        if (full)
            pageBlock = pageBlockEls = null;

        if (opts.fadeOut) {
            els.fadeOut(opts.fadeOut);
            setTimeout(function () { reset(els, data, opts, el); }, opts.fadeOut);
        }
        else
            reset(els, data, opts, el);
    };

    // move blocking element back into the DOM where it started
    function reset(els, data, opts, el) {
        els.each(function (i, o) {
            // remove via DOM calls so we don't lose event handlers
            if (this.parentNode)
                this.parentNode.removeChild(this);
        });

        if (data && data.el) {
            data.el.style.display = data.display;
            data.el.style.position = data.position;
            if (data.parent)
                data.parent.appendChild(data.el);
            $(el).removeData('blockUI.history');
        }

        if (typeof opts.onUnblock == 'function')
            opts.onUnblock(el, opts);
    };

    // bind/unbind the handler
    function bind(b, el, opts) {
        var full = el == window, $el = $(el);

        // don't bother unbinding if there is nothing to unbind
        if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
            return;
        if (!full)
            $el.data('blockUI.isBlocked', b);

        // don't bind events when overlay is not in use or if bindEvents is false
        if (!opts.bindEvents || (b && !opts.showOverlay))
            return;

        // bind anchors and inputs for mouse and key events
        var events = 'mousedown mouseup keydown keypress';
        b ? $(document).bind(events, opts, handler) : $(document).unbind(events, handler);

        // former impl...
        //	   var $e = $('a,:input');
        //	   b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
    };

    // event handler to suppress keyboard/mouse events when blocking
    function handler(e) {
        // allow tab navigation (conditionally)
        if (e.keyCode && e.keyCode == 9) {
            if (pageBlock && e.data.constrainTabKey) {
                var els = pageBlockEls;
                var fwd = !e.shiftKey && e.target == els[els.length - 1];
                var back = e.shiftKey && e.target == els[0];
                if (fwd || back) {
                    setTimeout(function () { focus(back) }, 10);
                    return false;
                }
            }
        }
        // allow events within the message content
        if ($(e.target).parents('div.blockMsg').length > 0)
            return true;

        // allow events for content that is not being blocked
        return $(e.target).parents().children().filter('div.blockUI').length == 0;
    };

    function focus(back) {
        if (!pageBlockEls)
            return;
        var e = pageBlockEls[back === true ? pageBlockEls.length - 1 : 0];
        if (e)
            e.focus();
    };

    function center(el, x, y) {
        var p = el.parentNode, s = el.style;
        var l = ((p.offsetWidth - el.offsetWidth) / 2) - sz(p, 'borderLeftWidth');
        var t = ((p.offsetHeight - el.offsetHeight) / 2) - sz(p, 'borderTopWidth');
        if (x) s.left = l > 0 ? (l + 'px') : '0';
        if (y) s.top = t > 0 ? (t + 'px') : '0';
    };

    function sz(el, p) {
        return parseInt($.css(el, p)) || 0;
    };
})(jQuery);

// jquery.droppy
$.fn.droppy = function (options) {
    options = $.extend({ speed: 250 }, options || {});

    this.each(function () {
        var root = this;
        var zIndex = 1000;

        $(root).find('ul > li:has(> ul)').addClass('parent');
        $(root).find('li:has(> ul) > a').addClass('parent');

        function getSubnav(ele) {
            if (ele.nodeName.toLowerCase() == 'li') {
                var subnav = $('> ul', ele);
                return subnav.length ? subnav[0] : null;
            }
            else {
                return ele;
            }
        }

        function getActuator(ele) {
            if (ele.nodeName.toLowerCase() == 'ul') {
                return $(ele).parents('li')[0];
            }
            else {
                return ele;
            }
        }

        function hide() {
            var subnav = getSubnav(this);
            if (!subnav) return;
            $.data(subnav, 'cancelHide', false);
            setTimeout(function () {
                if (!$.data(subnav, 'cancelHide')) {
                    $(subnav).slideUp(options.speed);
                }
            }, 200);
        }

        function show() {
            var subnav = getSubnav(this);
            if (!subnav) return;
            $.data(subnav, 'cancelHide', true);
            $(subnav).css({ zIndex: zIndex++ }).slideDown(options.speed);
            if (this.nodeName.toLowerCase() == 'ul') {
                var li = getActuator(this);
                $(li).addClass('hover');
                $('> a', li).addClass('hover');
            }
        }

        if (typeof $.fn.hoverIntent == 'function') {
            $('ul, li', this).hoverIntent($.extend({
                sensitivity: 2, interval: 50, timeout: 100
            }, options.hoverIntent || {}, { over: show, out: hide }));
        } else {
            $('ul, li', this).hover(show, hide);
        }

        $('li', this).hover(
      function () { $(this).addClass('hover'); $('> a', this).addClass('hover'); },
      function () { $(this).removeClass('hover'); $('> a', this).removeClass('hover'); }
    );
    });
};

// jquery.form
; (function ($) {
    /*
    Usage Note:
    -----------
    Do not use both ajaxSubmit and ajaxForm on the same form.  These
    functions are intended to be exclusive.  Use ajaxSubmit if you want
    to bind your own submit handler to the form.  For example,

    $(document).ready(function() {
    $('#myForm').bind('submit', function() {
    $(this).ajaxSubmit({
    target: '#output'
    });
    return false; // <-- important!
    });
    });

    Use ajaxForm when you want the plugin to manage all the event binding
    for you.  For example,

    $(document).ready(function() {
    $('#myForm').ajaxForm({
    target: '#output'
    });
    });

    When using ajaxForm, the ajaxSubmit function will be invoked for you
    at the appropriate time.
    */

    /**
    * ajaxSubmit() provides a mechanism for immediately submitting
    * an HTML form using AJAX.
    */
    $.fn.ajaxSubmit = function (options) {
        // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
        if (!this.length) {
            log('ajaxSubmit: skipping submit process - no element selected');
            return this;
        }

        if (typeof options == 'function') {
            options = { success: options };
        }

        var url = $.trim(this.attr('action'));
        if (url) {
            // clean url (don't include hash vaue)
            url = (url.match(/^([^#]+)/) || [])[1];
        }
        url = url || window.location.href || '';

        options = $.extend(true, {
            url: url,
            type: this.attr('method') || 'GET',
            iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
        }, options);

        // hook for manipulating the form data before it is extracted;
        // convenient for use with rich editors like tinyMCE or FCKEditor
        var veto = {};
        this.trigger('form-pre-serialize', [this, options, veto]);
        if (veto.veto) {
            log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
            return this;
        }

        // provide opportunity to alter form data before it is serialized
        if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
            log('ajaxSubmit: submit aborted via beforeSerialize callback');
            return this;
        }

        var n, v, a = this.formToArray(options.semantic);
        if (options.data) {
            options.extraData = options.data;
            for (n in options.data) {
                if (options.data[n] instanceof Array) {
                    for (var k in options.data[n]) {
                        a.push({ name: n, value: options.data[n][k] });
                    }
                }
                else {
                    v = options.data[n];
                    v = $.isFunction(v) ? v() : v; // if value is fn, invoke it
                    a.push({ name: n, value: v });
                }
            }
        }

        // give pre-submit callback an opportunity to abort the submit
        if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
            log('ajaxSubmit: submit aborted via beforeSubmit callback');
            return this;
        }

        // fire vetoable 'validate' event
        this.trigger('form-submit-validate', [a, this, options, veto]);
        if (veto.veto) {
            log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
            return this;
        }

        var q = $.param(a);

        if (options.type.toUpperCase() == 'GET') {
            options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
            options.data = null;  // data is null for 'get'
        }
        else {
            options.data = q; // data is the query string for 'post'
        }

        var $form = this, callbacks = [];
        if (options.resetForm) {
            callbacks.push(function () { $form.resetForm(); });
        }
        if (options.clearForm) {
            callbacks.push(function () { $form.clearForm(); });
        }

        // perform a load on the target only if dataType is not provided
        if (!options.dataType && options.target) {
            var oldSuccess = options.success || function () { };
            callbacks.push(function (data) {
                var fn = options.replaceTarget ? 'replaceWith' : 'html';
                $(options.target)[fn](data).each(oldSuccess, arguments);
            });
        }
        else if (options.success) {
            callbacks.push(options.success);
        }

        options.success = function (data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
            var context = options.context || options;   // jQuery 1.4+ supports scope context
            for (var i = 0, max = callbacks.length; i < max; i++) {
                callbacks[i].apply(context, [data, status, xhr || $form, $form]);
            }
        };

        // are there files to upload?
        var fileInputs = $('input:file', this).length > 0;
        var mp = 'multipart/form-data';
        var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);

        // options.iframe allows user to force iframe mode
        // 06-NOV-09: now defaulting to iframe mode if file input is detected
        if (options.iframe !== false && (fileInputs || options.iframe || multipart)) {
            // hack to fix Safari hang (thanks to Tim Molendijk for this)
            // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
            if (options.closeKeepAlive) {
                $.get(options.closeKeepAlive, fileUpload);
            }
            else {
                fileUpload();
            }
        }
        else {
            $.ajax(options);
        }

        // fire 'notify' event
        this.trigger('form-submit-notify', [this, options]);
        return this;

        // private function for handling file uploads (hat tip to YAHOO!)
        function fileUpload() {
            var form = $form[0];

            if ($(':input[name=submit],:input[id=submit]', form).length) {
                // if there is an input with a name or id of 'submit' then we won't be
                // able to invoke the submit fn on the form (at least not x-browser)
                alert('Error: Form elements must not have name or id of "submit".');
                return;
            }

            var s = $.extend(true, {}, $.ajaxSettings, options);
            s.context = s.context || s;
            var id = 'jqFormIO' + (new Date().getTime()), fn = '_' + id;
            window[fn] = function () {
                var f = $io.data('form-plugin-onload');
                if (f) {
                    f();
                    window[fn] = undefined;
                    try { delete window[fn]; } catch (e) { }
                }
            }
            var $io = $('<iframe id="' + id + '" name="' + id + '" src="' + s.iframeSrc + '" onload="window[\'_\'+this.id]()" />');
            var io = $io[0];

            $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });

            var xhr = { // mock object
                aborted: 0,
                responseText: null,
                responseXML: null,
                status: 0,
                statusText: 'n/a',
                getAllResponseHeaders: function () { },
                getResponseHeader: function () { },
                setRequestHeader: function () { },
                abort: function () {
                    this.aborted = 1;
                    $io.attr('src', s.iframeSrc); // abort op in progress
                }
            };

            var g = s.global;
            // trigger ajax global events so that activity/block indicators work like normal
            if (g && !$.active++) {
                $.event.trigger("ajaxStart");
            }
            if (g) {
                $.event.trigger("ajaxSend", [xhr, s]);
            }

            if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
                if (s.global) {
                    $.active--;
                }
                return;
            }
            if (xhr.aborted) {
                return;
            }

            var cbInvoked = false;
            var timedOut = 0;

            // add submitting element to data if we know it
            var sub = form.clk;
            if (sub) {
                var n = sub.name;
                if (n && !sub.disabled) {
                    s.extraData = s.extraData || {};
                    s.extraData[n] = sub.value;
                    if (sub.type == "image") {
                        s.extraData[n + '.x'] = form.clk_x;
                        s.extraData[n + '.y'] = form.clk_y;
                    }
                }
            }

            // take a breath so that pending repaints get some cpu time before the upload starts
            function doSubmit() {
                // make sure form attrs are set
                var t = $form.attr('target'), a = $form.attr('action');

                // update form attrs in IE friendly way
                form.setAttribute('target', id);
                if (form.getAttribute('method') != 'POST') {
                    form.setAttribute('method', 'POST');
                }
                if (form.getAttribute('action') != s.url) {
                    form.setAttribute('action', s.url);
                }

                // ie borks in some cases when setting encoding
                if (!s.skipEncodingOverride) {
                    $form.attr({
                        encoding: 'multipart/form-data',
                        enctype: 'multipart/form-data'
                    });
                }

                // support timout
                if (s.timeout) {
                    setTimeout(function () { timedOut = true; cb(); }, s.timeout);
                }

                // add "extra" data to form if provided in options
                var extraInputs = [];
                try {
                    if (s.extraData) {
                        for (var n in s.extraData) {
                            extraInputs.push(
							$('<input type="hidden" name="' + n + '" value="' + s.extraData[n] + '" />')
								.appendTo(form)[0]);
                        }
                    }

                    // add iframe to doc and submit the form
                    $io.appendTo('body');
                    $io.data('form-plugin-onload', cb);
                    form.submit();
                }
                finally {
                    // reset attrs and remove "extra" input elements
                    form.setAttribute('action', a);
                    if (t) {
                        form.setAttribute('target', t);
                    } else {
                        $form.removeAttr('target');
                    }
                    $(extraInputs).remove();
                }
            }

            if (s.forceSync) {
                doSubmit();
            }
            else {
                setTimeout(doSubmit, 10); // this lets dom updates render
            }

            var data, doc, domCheckCount = 50;

            function cb() {
                if (cbInvoked) {
                    return;
                }

                $io.removeData('form-plugin-onload');

                var ok = true;
                try {
                    if (timedOut) {
                        throw 'timeout';
                    }
                    // extract the server response from the iframe
                    doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;

                    var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
                    log('isXml=' + isXml);
                    if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) {
                        if (--domCheckCount) {
                            // in some browsers (Opera) the iframe DOM is not always traversable when
                            // the onload callback fires, so we loop a bit to accommodate
                            log('requeing onLoad callback, DOM not available');
                            setTimeout(cb, 250);
                            return;
                        }
                        // let this fall through because server response could be an empty document
                        //log('Could not access iframe DOM after mutiple tries.');
                        //throw 'DOMException: not available';
                    }

                    //log('response detected');
                    cbInvoked = true;
                    xhr.responseText = doc.documentElement ? doc.documentElement.innerHTML : null;
                    xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
                    xhr.getResponseHeader = function (header) {
                        var headers = { 'content-type': s.dataType };
                        return headers[header];
                    };

                    var scr = /(json|script)/.test(s.dataType);
                    if (scr || s.textarea) {
                        // see if user embedded response in textarea
                        var ta = doc.getElementsByTagName('textarea')[0];
                        if (ta) {
                            xhr.responseText = ta.value;
                        }
                        else if (scr) {
                            // account for browsers injecting pre around json response
                            var pre = doc.getElementsByTagName('pre')[0];
                            if (pre) {
                                xhr.responseText = pre.innerHTML;
                            }
                        }
                    }
                    else if (s.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
                        xhr.responseXML = toXml(xhr.responseText);
                    }
                    data = $.httpData(xhr, s.dataType);
                }
                catch (e) {
                    log('error caught:', e);
                    ok = false;
                    xhr.error = e;
                    $.handleError(s, xhr, 'error', e);
                }

                // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
                if (ok) {
                    s.success.call(s.context, data, 'success', xhr);
                    if (g) {
                        $.event.trigger("ajaxSuccess", [xhr, s]);
                    }
                }
                if (g) {
                    $.event.trigger("ajaxComplete", [xhr, s]);
                }
                if (g && ! --$.active) {
                    $.event.trigger("ajaxStop");
                }
                if (s.complete) {
                    s.complete.call(s.context, xhr, ok ? 'success' : 'error');
                }

                // clean up
                setTimeout(function () {
                    $io.removeData('form-plugin-onload');
                    $io.remove();
                    xhr.responseXML = null;
                }, 100);
            }

            function toXml(s, doc) {
                if (window.ActiveXObject) {
                    doc = new ActiveXObject('Microsoft.XMLDOM');
                    doc.async = 'false';
                    doc.loadXML(s);
                }
                else {
                    doc = (new DOMParser()).parseFromString(s, 'text/xml');
                }
                return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
            }
        }
    };

    /**
    * ajaxForm() provides a mechanism for fully automating form submission.
    *
    * The advantages of using this method instead of ajaxSubmit() are:
    *
    * 1: This method will include coordinates for <input type="image" /> elements (if the element
    *	is used to submit the form).
    * 2. This method will include the submit element's name/value data (for the element that was
    *	used to submit the form).
    * 3. This method binds the submit() method to the form for you.
    *
    * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
    * passes the options argument along after properly binding events for submit elements and
    * the form itself.
    */
    $.fn.ajaxForm = function (options) {
        // in jQuery 1.3+ we can fix mistakes with the ready state
        if (this.length === 0) {
            var o = { s: this.selector, c: this.context };
            if (!$.isReady && o.s) {
                log('DOM not ready, queuing ajaxForm');
                $(function () {
                    $(o.s, o.c).ajaxForm(options);
                });
                return this;
            }
            // is your DOM ready?  http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
            log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
            return this;
        }

        return this.ajaxFormUnbind().bind('submit.form-plugin', function (e) {
            if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
                e.preventDefault();
                $(this).ajaxSubmit(options);
            }
        }).bind('click.form-plugin', function (e) {
            var target = e.target;
            var $el = $(target);
            if (!($el.is(":submit,input:image"))) {
                // is this a child element of the submit el?  (ex: a span within a button)
                var t = $el.closest(':submit');
                if (t.length == 0) {
                    return;
                }
                target = t[0];
            }
            var form = this;
            form.clk = target;
            if (target.type == 'image') {
                if (e.offsetX != undefined) {
                    form.clk_x = e.offsetX;
                    form.clk_y = e.offsetY;
                } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
                    var offset = $el.offset();
                    form.clk_x = e.pageX - offset.left;
                    form.clk_y = e.pageY - offset.top;
                } else {
                    form.clk_x = e.pageX - target.offsetLeft;
                    form.clk_y = e.pageY - target.offsetTop;
                }
            }
            // clear form vars
            setTimeout(function () { form.clk = form.clk_x = form.clk_y = null; }, 100);
        });
    };

    // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
    $.fn.ajaxFormUnbind = function () {
        return this.unbind('submit.form-plugin click.form-plugin');
    };

    /**
    * formToArray() gathers form element data into an array of objects that can
    * be passed to any of the following ajax functions: $.get, $.post, or load.
    * Each object in the array has both a 'name' and 'value' property.  An example of
    * an array for a simple login form might be:
    *
    * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
    *
    * It is this array that is passed to pre-submit callback functions provided to the
    * ajaxSubmit() and ajaxForm() methods.
    */
    $.fn.formToArray = function (semantic) {
        var a = [];
        if (this.length === 0) {
            return a;
        }

        var form = this[0];
        var els = semantic ? form.getElementsByTagName('*') : form.elements;
        if (!els) {
            return a;
        }

        var i, j, n, v, el;
        for (i = 0, max = els.length; i < max; i++) {
            el = els[i];
            n = el.name;
            if (!n) {
                continue;
            }

            if (semantic && form.clk && el.type == "image") {
                // handle image inputs on the fly when semantic == true
                if (!el.disabled && form.clk == el) {
                    a.push({ name: n, value: $(el).val() });
                    a.push({ name: n + '.x', value: form.clk_x }, { name: n + '.y', value: form.clk_y });
                }
                continue;
            }

            v = $.fieldValue(el, true);
            if (v && v.constructor == Array) {
                for (j = 0, jmax = v.length; j < jmax; j++) {
                    a.push({ name: n, value: v[j] });
                }
            }
            else if (v !== null && typeof v != 'undefined') {
                a.push({ name: n, value: v });
            }
        }

        if (!semantic && form.clk) {
            // input type=='image' are not found in elements array! handle it here
            var $input = $(form.clk), input = $input[0];
            n = input.name;
            if (n && !input.disabled && input.type == 'image') {
                a.push({ name: n, value: $input.val() });
                a.push({ name: n + '.x', value: form.clk_x }, { name: n + '.y', value: form.clk_y });
            }
        }
        return a;
    };

    /**
    * Serializes form data into a 'submittable' string. This method will return a string
    * in the format: name1=value1&amp;name2=value2
    */
    $.fn.formSerialize = function (semantic) {
        //hand off to jQuery.param for proper encoding
        return $.param(this.formToArray(semantic));
    };

    /**
    * Serializes all field elements in the jQuery object into a query string.
    * This method will return a string in the format: name1=value1&amp;name2=value2
    */
    $.fn.fieldSerialize = function (successful) {
        var a = [];
        this.each(function () {
            var n = this.name;
            if (!n) {
                return;
            }
            var v = $.fieldValue(this, successful);
            if (v && v.constructor == Array) {
                for (var i = 0, max = v.length; i < max; i++) {
                    a.push({ name: n, value: v[i] });
                }
            }
            else if (v !== null && typeof v != 'undefined') {
                a.push({ name: this.name, value: v });
            }
        });
        //hand off to jQuery.param for proper encoding
        return $.param(a);
    };

    /**
    * Returns the value(s) of the element in the matched set.  For example, consider the following form:
    *
    *  <form><fieldset>
    *	  <input name="A" type="text" />
    *	  <input name="A" type="text" />
    *	  <input name="B" type="checkbox" value="B1" />
    *	  <input name="B" type="checkbox" value="B2"/>
    *	  <input name="C" type="radio" value="C1" />
    *	  <input name="C" type="radio" value="C2" />
    *  </fieldset></form>
    *
    *  var v = $(':text').fieldValue();
    *  // if no values are entered into the text inputs
    *  v == ['','']
    *  // if values entered into the text inputs are 'foo' and 'bar'
    *  v == ['foo','bar']
    *
    *  var v = $(':checkbox').fieldValue();
    *  // if neither checkbox is checked
    *  v === undefined
    *  // if both checkboxes are checked
    *  v == ['B1', 'B2']
    *
    *  var v = $(':radio').fieldValue();
    *  // if neither radio is checked
    *  v === undefined
    *  // if first radio is checked
    *  v == ['C1']
    *
    * The successful argument controls whether or not the field element must be 'successful'
    * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
    * The default value of the successful argument is true.  If this value is false the value(s)
    * for each element is returned.
    *
    * Note: This method *always* returns an array.  If no valid value can be determined the
    *	   array will be empty, otherwise it will contain one or more values.
    */
    $.fn.fieldValue = function (successful) {
        for (var val = [], i = 0, max = this.length; i < max; i++) {
            var el = this[i];
            var v = $.fieldValue(el, successful);
            if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
                continue;
            }
            v.constructor == Array ? $.merge(val, v) : val.push(v);
        }
        return val;
    };

    /**
    * Returns the value of the field element.
    */
    $.fieldValue = function (el, successful) {
        var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
        if (successful === undefined) {
            successful = true;
        }

        if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
		(t == 'checkbox' || t == 'radio') && !el.checked ||
		(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
		tag == 'select' && el.selectedIndex == -1)) {
            return null;
        }

        if (tag == 'select') {
            var index = el.selectedIndex;
            if (index < 0) {
                return null;
            }
            var a = [], ops = el.options;
            var one = (t == 'select-one');
            var max = (one ? index + 1 : ops.length);
            for (var i = (one ? index : 0); i < max; i++) {
                var op = ops[i];
                if (op.selected) {
                    var v = op.value;
                    if (!v) { // extra pain for IE...
                        v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
                    }
                    if (one) {
                        return v;
                    }
                    a.push(v);
                }
            }
            return a;
        }
        return $(el).val();
    };

    /**
    * Clears the form data.  Takes the following actions on the form's input fields:
    *  - input text fields will have their 'value' property set to the empty string
    *  - select elements will have their 'selectedIndex' property set to -1
    *  - checkbox and radio inputs will have their 'checked' property set to false
    *  - inputs of type submit, button, reset, and hidden will *not* be effected
    *  - button elements will *not* be effected
    */
    $.fn.clearForm = function () {
        return this.each(function () {
            $('input,select,textarea', this).clearFields();
        });
    };

    /**
    * Clears the selected form elements.
    */
    $.fn.clearFields = $.fn.clearInputs = function () {
        return this.each(function () {
            var t = this.type, tag = this.tagName.toLowerCase();
            if (t == 'text' || t == 'password' || tag == 'textarea') {
                this.value = '';
            }
            else if (t == 'checkbox' || t == 'radio') {
                this.checked = false;
            }
            else if (tag == 'select') {
                this.selectedIndex = -1;
            }
        });
    };

    /**
    * Resets the form data.  Causes all form elements to be reset to their original value.
    */
    $.fn.resetForm = function () {
        return this.each(function () {
            // guard against an input with the name of 'reset'
            // note that IE reports the reset function as an 'object'
            if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
                this.reset();
            }
        });
    };

    /**
    * Enables or disables any matching elements.
    */
    $.fn.enable = function (b) {
        if (b === undefined) {
            b = true;
        }
        return this.each(function () {
            this.disabled = !b;
        });
    };

    /**
    * Checks/unchecks any matching checkboxes or radio buttons and
    * selects/deselects and matching option elements.
    */
    $.fn.selected = function (select) {
        if (select === undefined) {
            select = true;
        }
        return this.each(function () {
            var t = this.type;
            if (t == 'checkbox' || t == 'radio') {
                this.checked = select;
            }
            else if (this.tagName.toLowerCase() == 'option') {
                var $sel = $(this).parent('select');
                if (select && $sel[0] && $sel[0].type == 'select-one') {
                    // deselect all other options
                    $sel.find('option').selected(false);
                }
                this.selected = select;
            }
        });
    };

    // helper fn for console logging
    // set $.fn.ajaxSubmit.debug to true to enable debug logging
    function log() {
        if ($.fn.ajaxSubmit.debug) {
            var msg = '[jquery.form] ' + Array.prototype.join.call(arguments, '');
            if (window.console && window.console.log) {
                window.console.log(msg);
            }
            else if (window.opera && window.opera.postError) {
                window.opera.postError(msg);
            }
        }
    };
})(jQuery);

// jquery.metadata
(function ($) {
    $.extend({
        metadata: {
            defaults: {
                type: 'class',
                name: 'metadata',
                cre: /({.*})/,
                single: 'metadata'
            },
            setType: function (type, name) {
                this.defaults.type = type;
                this.defaults.name = name;
            },
            get: function (elem, opts) {
                var settings = $.extend({}, this.defaults, opts);
                // check for empty string in single property
                if (!settings.single.length) settings.single = 'metadata';

                var data = $.data(elem, settings.single);
                // returned cached data if it already exists
                if (data) return data;

                data = "{}";

                if (settings.type == "class") {
                    var m = settings.cre.exec(elem.className);
                    if (m)
                        data = m[1];
                } else if (settings.type == "elem") {
                    if (!elem.getElementsByTagName)
                        return undefined;
                    var e = elem.getElementsByTagName(settings.name);
                    if (e.length)
                        data = $.trim(e[0].innerHTML);
                } else if (elem.getAttribute != undefined) {
                    var attr = elem.getAttribute(settings.name);
                    if (attr)
                        data = attr;
                }

                if (data.indexOf('{') < 0)
                    data = "{" + data + "}";

                data = eval("(" + data + ")");

                $.data(elem, settings.single, data);
                return data;
            }
        }
    });

    /**
    * Returns the metadata object for the first member of the jQuery object.
    *
    * @name metadata
    * @descr Returns element's metadata object
    * @param Object opts An object contianing settings to override the defaults
    * @type jQuery
    * @cat Plugins/Metadata
    */
    $.fn.metadata = function (opts) {
        return $.metadata.get(this[0], opts);
    };
})(jQuery);

// jquery.tablesorter
(function ($) {
    $.extend({
        tablesorter: new function () {
            var parsers = [], widgets = [];

            this.defaults = {
                cssHeader: "header",
                cssAsc: "headerSortUp",
                cssDesc: "headerSortDown",
                sortInitialOrder: "asc",
                sortMultiSortKey: "shiftKey",
                sortForce: null,
                sortAppend: null,
                textExtraction: "simple",
                parsers: {},
                widgets: [],
                widgetZebra: { css: ["even", "odd"] },
                headers: {},
                widthFixed: false,
                cancelSelection: true,
                sortList: [],
                headerList: [],
                dateFormat: "us",
                decimal: '.',
                debug: false
            };

            /* debuging utils */
            function benchmark(s, d) {
                log(s + "," + (new Date().getTime() - d.getTime()) + "ms");
            }

            this.benchmark = benchmark;

            function log(s) {
                if (typeof console != "undefined" && typeof console.debug != "undefined") {
                    console.log(s);
                } else {
                    alert(s);
                }
            }

            /* parsers utils */
            function buildParserCache(table, $headers) {
                if (table.config.debug) { var parsersDebug = ""; }

                var rows = table.tBodies[0].rows;

                if (table.tBodies[0].rows[0]) {
                    var list = [], cells = rows[0].cells, l = cells.length;

                    for (var i = 0; i < l; i++) {
                        var p = false;

                        if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) {
                            p = getParserById($($headers[i]).metadata().sorter);
                        } else if ((table.config.headers[i] && table.config.headers[i].sorter)) {
                            p = getParserById(table.config.headers[i].sorter);
                        }
                        if (!p) {
                            p = detectParserForColumn(table, cells[i]);
                        }

                        if (table.config.debug) { parsersDebug += "column:" + i + " parser:" + p.id + "\n"; }

                        list.push(p);
                    }
                }

                if (table.config.debug) { log(parsersDebug); }

                return list;
            };

            function detectParserForColumn(table, node) {
                var l = parsers.length;
                for (var i = 1; i < l; i++) {
                    if (parsers[i].is($.trim(getElementText(table.config, node)), table, node)) {
                        return parsers[i];
                    }
                }
                // 0 is always the generic parser (text)
                return parsers[0];
            }

            function getParserById(name) {
                var l = parsers.length;
                for (var i = 0; i < l; i++) {
                    if (parsers[i].id.toLowerCase() == name.toLowerCase()) {
                        return parsers[i];
                    }
                }
                return false;
            }

            /* utils */
            function buildCache(table) {
                if (table.config.debug) { var cacheTime = new Date(); }

                var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0,
					totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0,
					parsers = table.config.parsers,
					cache = { row: [], normalized: [] };

                for (var i = 0; i < totalRows; ++i) {
                    /** Add the table data to main data array */
                    var c = table.tBodies[0].rows[i], cols = [];

                    cache.row.push($(c));

                    for (var j = 0; j < totalCells; ++j) {
                        cols.push(parsers[j].format(getElementText(table.config, c.cells[j]), table, c.cells[j]));
                    }

                    cols.push(i); // add position for rowCache
                    cache.normalized.push(cols);
                    cols = null;
                };

                if (table.config.debug) { benchmark("Building cache for " + totalRows + " rows:", cacheTime); }

                return cache;
            };

            function getElementText(config, node) {
                if (!node) return "";

                var t = "";

                if (config.textExtraction == "simple") {
                    if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) {
                        t = node.childNodes[0].innerHTML;
                    } else {
                        t = node.innerHTML;
                    }
                } else {
                    if (typeof (config.textExtraction) == "function") {
                        t = config.textExtraction(node);
                    } else {
                        t = $(node).text();
                    }
                }
                return t;
            }

            function appendToTable(table, cache) {
                if (table.config.debug) { var appendTime = new Date() }

                var c = cache,
					r = c.row,
					n = c.normalized,
					totalRows = n.length,
					checkCell = (n[0].length - 1),
					tableBody = $(table.tBodies[0]),
					rows = [];

                for (var i = 0; i < totalRows; i++) {
                    rows.push(r[n[i][checkCell]]);
                    if (!table.config.appender) {
                        var o = r[n[i][checkCell]];
                        var l = o.length;
                        for (var j = 0; j < l; j++) {
                            tableBody[0].appendChild(o[j]);
                        }

                        //tableBody.append(r[n[i][checkCell]]);
                    }
                }

                if (table.config.appender) {
                    table.config.appender(table, rows);
                }

                rows = null;

                if (table.config.debug) { benchmark("Rebuilt table:", appendTime); }

                //apply table widgets
                applyWidget(table);

                // trigger sortend
                setTimeout(function () {
                    $(table).trigger("sortEnd");
                }, 0);
            };

            function buildHeaders(table) {
                if (table.config.debug) { var time = new Date(); }

                var meta = ($.metadata) ? true : false, tableHeadersRows = [];

                for (var i = 0; i < table.tHead.rows.length; i++) { tableHeadersRows[i] = 0; };

                $tableHeaders = $("thead th", table);

                $tableHeaders.each(function (index) {
                    this.count = 0;
                    this.column = index;
                    this.order = formatSortingOrder(table.config.sortInitialOrder);

                    if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true;

                    if (!this.sortDisabled) {
                        $(this).addClass(table.config.cssHeader);
                    }

                    // add cell to headerList
                    table.config.headerList[index] = this;
                });

                if (table.config.debug) { benchmark("Built headers:", time); log($tableHeaders); }

                return $tableHeaders;
            };

            function checkCellColSpan(table, rows, row) {
                var arr = [], r = table.tHead.rows, c = r[row].cells;

                for (var i = 0; i < c.length; i++) {
                    var cell = c[i];

                    if (cell.colSpan > 1) {
                        arr = arr.concat(checkCellColSpan(table, headerArr, row++));
                    } else {
                        if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) {
                            arr.push(cell);
                        }
                        //headerArr[row] = (i+row);
                    }
                }
                return arr;
            };

            function checkHeaderMetadata(cell) {
                if (($.metadata) && ($(cell).metadata().sorter === false)) { return true; };
                return false;
            }

            function checkHeaderOptions(table, i) {
                if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { return true; };
                return false;
            }

            function applyWidget(table) {
                var c = table.config.widgets;
                var l = c.length;
                for (var i = 0; i < l; i++) {
                    getWidgetById(c[i]).format(table);
                }
            }

            function getWidgetById(name) {
                var l = widgets.length;
                for (var i = 0; i < l; i++) {
                    if (widgets[i].id.toLowerCase() == name.toLowerCase()) {
                        return widgets[i];
                    }
                }
            };

            function formatSortingOrder(v) {
                if (typeof (v) != "Number") {
                    i = (v.toLowerCase() == "desc") ? 1 : 0;
                } else {
                    i = (v == (0 || 1)) ? v : 0;
                }
                return i;
            }

            function isValueInArray(v, a) {
                var l = a.length;
                for (var i = 0; i < l; i++) {
                    if (a[i][0] == v) {
                        return true;
                    }
                }
                return false;
            }

            function setHeadersCss(table, $headers, list, css) {
                // remove all header information
                $headers.removeClass(css[0]).removeClass(css[1]);

                var h = [];
                $headers.each(function (offset) {
                    if (!this.sortDisabled) {
                        h[this.column] = $(this);
                    }
                });

                var l = list.length;
                for (var i = 0; i < l; i++) {
                    h[list[i][0]].addClass(css[list[i][1]]);
                }
            }

            function fixColumnWidth(table, $headers) {
                var c = table.config;
                if (c.widthFixed) {
                    var colgroup = $('<colgroup>');
                    $("tr:first td", table.tBodies[0]).each(function () {
                        colgroup.append($('<col>').css('width', $(this).width()));
                    });
                    $(table).prepend(colgroup);
                };
            }

            function updateHeaderSortCount(table, sortList) {
                var c = table.config, l = sortList.length;
                for (var i = 0; i < l; i++) {
                    var s = sortList[i], o = c.headerList[s[0]];
                    o.count = s[1];
                    o.count++;
                }
            }

            /* sorting methods */
            function multisort(table, sortList, cache) {
                if (table.config.debug) { var sortTime = new Date(); }

                var dynamicExp = "var sortWrapper = function(a,b) {", l = sortList.length;

                for (var i = 0; i < l; i++) {
                    var c = sortList[i][0];
                    var order = sortList[i][1];
                    var s = (getCachedSortType(table.config.parsers, c) == "text") ? ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? "sortNumeric" : "sortNumericDesc");

                    var e = "e" + i;

                    dynamicExp += "var " + e + " = " + s + "(a[" + c + "],b[" + c + "]); ";
                    dynamicExp += "if(" + e + ") { return " + e + "; } ";
                    dynamicExp += "else { ";
                }

                // if value is the same keep orignal order
                var orgOrderCol = cache.normalized[0].length - 1;
                dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];";

                for (var i = 0; i < l; i++) {
                    dynamicExp += "}; ";
                }

                dynamicExp += "return 0; ";
                dynamicExp += "}; ";

                eval(dynamicExp);

                cache.normalized.sort(sortWrapper);

                if (table.config.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); }

                return cache;
            };

            function sortText(a, b) {
                return ((a < b) ? -1 : ((a > b) ? 1 : 0));
            };

            function sortTextDesc(a, b) {
                return ((b < a) ? -1 : ((b > a) ? 1 : 0));
            };

            function sortNumeric(a, b) {
                return a - b;
            };

            function sortNumericDesc(a, b) {
                return b - a;
            };

            function getCachedSortType(parsers, i) {
                return parsers[i].type;
            };

            /* public methods */
            this.construct = function (settings) {
                return this.each(function () {
                    if (!this.tHead || !this.tBodies) return;

                    var $this, $document, $headers, cache, config, shiftDown = 0, sortOrder;

                    this.config = {};

                    config = $.extend(this.config, $.tablesorter.defaults, settings);

                    // store common expression for speed
                    $this = $(this);

                    // build headers
                    $headers = buildHeaders(this);

                    // try to auto detect column type, and store in tables config
                    this.config.parsers = buildParserCache(this, $headers);

                    // build the cache for the tbody cells
                    cache = buildCache(this);

                    // get the css class names, could be done else where.
                    var sortCSS = [config.cssDesc, config.cssAsc];

                    // fixate columns if the users supplies the fixedWidth option
                    fixColumnWidth(this);

                    // apply event handling to headers
                    // this is to big, perhaps break it out?
                    $headers.click(function (e) {
                        $this.trigger("sortStart");

                        var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0;

                        if (!this.sortDisabled && totalRows > 0) {
                            // store exp, for speed
                            var $cell = $(this);

                            // get current column index
                            var i = this.column;

                            // get current column sort order
                            this.order = this.count++ % 2;

                            // user only whants to sort on one column
                            if (!e[config.sortMultiSortKey]) {
                                // flush the sort list
                                config.sortList = [];

                                if (config.sortForce != null) {
                                    var a = config.sortForce;
                                    for (var j = 0; j < a.length; j++) {
                                        if (a[j][0] != i) {
                                            config.sortList.push(a[j]);
                                        }
                                    }
                                }

                                // add column to sort list
                                config.sortList.push([i, this.order]);

                                // multi column sorting
                            } else {
                                // the user has clicked on an all ready sortet column.
                                if (isValueInArray(i, config.sortList)) {
                                    // revers the sorting direction for all tables.
                                    for (var j = 0; j < config.sortList.length; j++) {
                                        var s = config.sortList[j], o = config.headerList[s[0]];
                                        if (s[0] == i) {
                                            o.count = s[1];
                                            o.count++;
                                            s[1] = o.count % 2;
                                        }
                                    }
                                } else {
                                    // add column to sort list array
                                    config.sortList.push([i, this.order]);
                                }
                            };
                            setTimeout(function () {
                                //set css for headers
                                setHeadersCss($this[0], $headers, config.sortList, sortCSS);
                                appendToTable($this[0], multisort($this[0], config.sortList, cache));
                            }, 1);
                            // stop normal event by returning false
                            return false;
                        }
                        // cancel selection
                    }).mousedown(function () {
                        if (config.cancelSelection) {
                            this.onselectstart = function () { return false };
                            return false;
                        }
                    });

                    // apply easy methods that trigger binded events
                    $this.bind("update", function () {
                        // rebuild parsers.
                        this.config.parsers = buildParserCache(this, $headers);

                        // rebuild the cache map
                        cache = buildCache(this);
                    }).bind("sorton", function (e, list) {
                        $(this).trigger("sortStart");

                        config.sortList = list;

                        // update and store the sortlist
                        var sortList = config.sortList;

                        // update header count index
                        updateHeaderSortCount(this, sortList);

                        //set css for headers
                        setHeadersCss(this, $headers, sortList, sortCSS);

                        // sort the table and append it to the dom
                        appendToTable(this, multisort(this, sortList, cache));
                    }).bind("appendCache", function () {
                        appendToTable(this, cache);
                    }).bind("applyWidgetId", function (e, id) {
                        getWidgetById(id).format(this);
                    }).bind("applyWidgets", function () {
                        // apply widgets
                        applyWidget(this);
                    });

                    if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) {
                        config.sortList = $(this).metadata().sortlist;
                    }
                    // if user has supplied a sort list to constructor.
                    if (config.sortList.length > 0) {
                        $this.trigger("sorton", [config.sortList]);
                    }

                    // apply widgets
                    applyWidget(this);
                });
            };

            this.addParser = function (parser) {
                var l = parsers.length, a = true;
                for (var i = 0; i < l; i++) {
                    if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) {
                        a = false;
                    }
                }
                if (a) { parsers.push(parser); };
            };

            this.addWidget = function (widget) {
                widgets.push(widget);
            };

            this.formatFloat = function (s) {
                var i = parseFloat(s);
                return (isNaN(i)) ? 0 : i;
            };
            this.formatInt = function (s) {
                var i = parseInt(s);
                return (isNaN(i)) ? 0 : i;
            };

            this.isDigit = function (s, config) {
                var DECIMAL = '\\' + config.decimal;
                var exp = '/(^[+]?0(' + DECIMAL + '0+)?$)|(^([-+]?[1-9][0-9]*)$)|(^([-+]?((0?|[1-9][0-9]*)' + DECIMAL + '(0*[1-9][0-9]*)))$)|(^[-+]?[1-9]+[0-9]*' + DECIMAL + '0+$)/';
                return RegExp(exp).test($.trim(s));
            };

            this.clearTableBody = function (table) {
                if ($.browser.msie) {
                    function empty() {
                        while (this.firstChild) this.removeChild(this.firstChild);
                    }
                    empty.apply(table.tBodies[0]);
                } else {
                    table.tBodies[0].innerHTML = "";
                }
            };
        }
    });

    // extend plugin scope
    $.fn.extend({
        tablesorter: $.tablesorter.construct
    });

    var ts = $.tablesorter;

    // add default parsers
    ts.addParser({
        id: "text",
        is: function (s) {
            return true;
        },
        format: function (s) {
            return $.trim(s.toLowerCase());
        },
        type: "text"
    });

    ts.addParser({
        id: "digit",
        is: function (s, table) {
            var c = table.config;
            return $.tablesorter.isDigit(s, c);
        },
        format: function (s) {
            return $.tablesorter.formatFloat(s);
        },
        type: "numeric"
    });

    ts.addParser({
        id: "currency",
        is: function (s) {
            return /^[£$€?.]/.test(s);
        },
        format: function (s) {
            return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g), ""));
        },
        type: "numeric"
    });

    ts.addParser({
        id: "ipAddress",
        is: function (s) {
            return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);
        },
        format: function (s) {
            var a = s.split("."), r = "", l = a.length;
            for (var i = 0; i < l; i++) {
                var item = a[i];
                if (item.length == 2) {
                    r += "0" + item;
                } else {
                    r += item;
                }
            }
            return $.tablesorter.formatFloat(r);
        },
        type: "numeric"
    });

    ts.addParser({
        id: "url",
        is: function (s) {
            return /^(https?|ftp|file):\/\/$/.test(s);
        },
        format: function (s) {
            return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), ''));
        },
        type: "text"
    });

    ts.addParser({
        id: "isoDate",
        is: function (s) {
            return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);
        },
        format: function (s) {
            return $.tablesorter.formatFloat((s != "") ? new Date(s.replace(new RegExp(/-/g), "/")).getTime() : "0");
        },
        type: "numeric"
    });

    ts.addParser({
        id: "percent",
        is: function (s) {
            return /\%$/.test($.trim(s));
        },
        format: function (s) {
            return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), ""));
        },
        type: "numeric"
    });

    ts.addParser({
        id: "usLongDate",
        is: function (s) {
            return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));
        },
        format: function (s) {
            return $.tablesorter.formatFloat(new Date(s).getTime());
        },
        type: "numeric"
    });

    ts.addParser({
        id: "shortDate",
        is: function (s) {
            return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);
        },
        format: function (s, table) {
            var c = table.config;
            s = s.replace(/\-/g, "/");
            if (c.dateFormat == "us") {
                // reformat the string in ISO format
                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2");
            } else if (c.dateFormat == "uk") {
                //reformat the string in ISO format
                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1");
            } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") {
                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3");
            }
            return $.tablesorter.formatFloat(new Date(s).getTime());
        },
        type: "numeric"
    });

    ts.addParser({
        id: "time",
        is: function (s) {
            return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);
        },
        format: function (s) {
            return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime());
        },
        type: "numeric"
    });

    ts.addParser({
        id: "metadata",
        is: function (s) {
            return false;
        },
        format: function (s, table, cell) {
            var c = table.config, p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
            return $(cell).metadata()[p];
        },
        type: "numeric"
    });

    // add default widgets
    ts.addWidget({
        id: "zebra",
        format: function (table) {
            if (table.config.debug) { var time = new Date(); }
            $("tr:visible", table.tBodies[0])
	        .filter(':even')
	        .removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0])
	        .end().filter(':odd')
	        .removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);
            if (table.config.debug) { $.tablesorter.benchmark("Applying Zebra widget", time); }
        }
    });
})(jQuery);

$.tablesorter.addParser({
    id: 'link',
    is: function (s) {
        if (s.indexOf("<") >= 0) {
            return $(s).is("a");
        }

        return false;
    },
    format: function (s) {
        return $(s).text();
    },
    type: 'text'
});

$.tablesorter.addParser({
    id: 'textbox',
    is: function (s) {
        if (s.indexOf("<") >= 0) {
            return $(s).is("input:text");
        }

        return false;
    },
    format: function (s) {
        return $(s).val();
    },
    type: 'text'
});

$.tablesorter.addParser({
    id: 'cost',
    is: function (s, table, cell) {
        return CompareColumnHeader("Cost", table, cell);
    },
    format: function (s) {
        return s.replace("X", 0).replace("??", -1);
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'rarity',
    is: function (s, table, cell) {
        return CompareColumnHeader("Rarity", table, cell);
    },
    format: function (s) {
        var rarity = $.trim(s);
        if (rarity == "C" || rarity == "Common") {
            return 1;
        }

        if (rarity == "UC" || rarity == "Uncommon") {
            return 2;
        }

        if (rarity == "R" || rarity == "Rare") {
            return 3;
        }

        if (rarity == "VR" || rarity == "Very Rare") {
            return 4;
        }

        return 5;
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'stat',
    is: function (s, table, cell) {
        var headers = ["HP", "Def", "Atk", "Dmg"];
        return CompareColumnHeaders(headers, table, cell);
    },
    format: function (s) {
        return s.replace("--", -1).valueOf();
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'price',
    is: function (s) {
        return false;
    },
    format: function (s) {
        var t = s.replace("$", "");
        return $(t).val();
    }
});

$.tablesorter.addParser({
    id: 'faction',
    is: function (s, table, cell) {
        return CompareColumnHeader("*", table, cell);
    },
    format: function (s) {
        var name = $(s).attr("title");

        if (name == "Old Republic") {
            return 1;
        }

        if (name == "Sith") {
            return 2;
        }

        if (name == "Mandalorians") {
            return 3;
        }

        if (name == "Republic") {
            return 4;
        }

        if (name == "Separatists") {
            return 5;
        }

        if (name == "Rebel") {
            return 6;
        }

        if (name == "Imperial") {
            return 7;
        }

        if (name == "New Republic") {
            return 8;
        }

        if (name == "Yuuzhan Vong") {
            return 9;
        }

        return 10;
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'collectorNumber',
    is: function (s, table, cell) {
        return CompareColumnHeader("#", table, cell);
    },
    format: function (s) {
        var str = $.trim(s);
        var arr = str.split(' ');
        var num = arr[1];

        if (num < 10) {
            str = str.replace(num, '0' + num);
        }

        return str
        .replace("RS ", "1")
        .replace("CS ", "2")
        .replace("RotS ", "3")
        .replace("UH ", "4")
        .replace("AT ", "5")
        .replace("AoE ", "6")
        .replace("CotF ", "7")
        .replace("BH ", "8")
        .replace("AE ", "9")
        .replace("RI ", "10")
        .replace("BoH ", "12")
        .replace("TFU ", "13")
        .replace("LotF ", "14")
        .replace("KotOR ", "15")
        .replace("CWSS ", "16")
        .replace("SS ", "11")
        .replace("CW ", "17")
        .replace("CWB ", "18")
        .replace("IE ", "19")
        .replace("AoT ", "20")
        .replace("STP ", "21")
        .replace("JA ", "22")
        .replace("CCI ", "23")
        .replace("GaW ", "24")
        .replace("DT ", "25")
        .replace("MotF ", "26")
        .replace("DotF ", "27")
        .replace("RaR ", "28")
        .replace("BoT ", "29")
        .replace("Epic ", "30").valueOf();
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'collectorSet',
    is: function (s, table, cell) {
        return CompareColumnHeader("Set", table, cell);
    },
    format: function (s) {
        return s
        .replace("RS ", "1")
        .replace("CS ", "2")
        .replace("RotS ", "3")
        .replace("UH ", "4")
        .replace("AT ", "5")
        .replace("AoE ", "6")
        .replace("CotF ", "7")
        .replace("BH ", "8")
        .replace("AE ", "9")
        .replace("RI ", "10")
        .replace("SS ", "11")
        .replace("BoH ", "12")
        .replace("TFU ", "13")
        .replace("LotF ", "14")
        .replace("KotOR ", "15")
        .replace("CWSS ", "16")
        .replace("CW ", "17")
        .replace("CWB ", "18")
        .replace("IE ", "19")
        .replace("AoT ", "20")
        .replace("STP ", "21")
        .replace("JA ", "22")
        .replace("CCI ", "23")
        .replace("GaW ", "24")
        .replace("DT ", "25")
        .replace("MotF ", "26").valueOf();
    },
    type: 'numeric'
});

$.tablesorter.addParser({
    id: 'myRating',
    is: function (s, table, cell) {
        return CompareColumnHeader("My Rating", table, cell);
    },
    format: function (s) {
        return s.replace('--', '0').replace('/10', '').valueOf();
    },
    type: 'numeric'
});

function CompareColumnHeader(text, table, cell) {
    var i = cell.cellIndex;
    var columnHeader = table.tHead.rows[0].cells[i].innerHTML;
    return $.trim(columnHeader) == text;
}

function CompareColumnHeaders(array, table, cell) {
    var i = cell.cellIndex;
    var columnHeader = table.tHead.rows[0].cells[i].innerHTML;
    return $.inArray($.trim(columnHeader), array) != -1;
}

function TryAppend(queryString, key, value) {
    if (value) {
        if (queryString) {
            queryString = queryString + '&' + key + '=' + value;
        }
        else {
            queryString = key + '=' + value;
        }
    }

    return queryString;
}

var Ability = function () {
    var Selectors = {
        CharactersDiv: '#characters',
        CustomCharactersDiv: '#custom-characters'
    };

    var Urls = {
        GetCharacters: '',
        GetCustomCharacters: ''
    };

    var GetCharacters = function (sort, page) {
        var data = {};

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.GetCharacters, data, function (results) {
            var $results = $(results).fixTable();
            $(Selectors.CharactersDiv).html($results);
        });
    };

    var GetCustomCharacters = function (sort, page) {
        var data = {};

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.GetCustomCharacters, data, function (results) {
            var $results = $(results).fixTable();
            $(Selectors.CustomCharactersDiv).html($results);
        });
    };

    var Init = function () {
        GetCharacters();
        GetCustomCharacters();
    };

    return {
        GetCharacters: GetCharacters,
        GetCustomCharacters: GetCustomCharacters,
        Init: Init,
        Urls: Urls
    };
} ();

var BlooMailInbox = function () {
    var Selectors =
    {
        BlooMailTable: '#blooMails',
        ClearButton: '#clear-button',
        DeleteButton: '#delete-button',
        DeleteImage: 'td.bloo-mail-delete > img.button',
        FilterButton: '#filter-button',
        FromDropDown: '#From',
        MarkAsReadButton: '#mark-as-read-button',
        MarkOneReadImage: 'td.bloo-mail-mark-read > img.button',
        MarkUnreadImage: 'td.bloo-mail-mark-unread > img.button',
        QueryTextBox: '#Query',
        ResultsDiv: '#results',
        SelectedForm: '#selectedForm',
        StatusDropDown: '#Read',
        SubjectLink: 'a.subject',
        UndeleteLink: 'a.bloo-mail-undelete'
    };

    var Urls =
    {
        DeleteOne: function (blooMailId) { return ''; },
        DeleteSelected: '',
        Filter: '',
        GetRow: function (blooMailId) { return ''; },
        MarkAsRead: '',
        MarkRead: function (blooMailId) { return ''; },
        MarkUnread: function (blooMailId) { return ''; },
        UndeleteOne: function (blooMailId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.FromDropDown).val('');
        $(Selectors.QueryTextBox).val('');
        $(Selectors.StatusDropDown).val('');

        $.getNoCache(Urls.Filter, function (results) {
            var $results = $(results).fixDates();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Delete = function (event) {
        var $this = $(event.target),
            $row = $this.parents('tr'),
            blooMailId = $row.attr('data-id'),
            subject = $row.find(Selectors.SubjectLink).text();

        $.post(Urls.DeleteOne(blooMailId), { sender: false }, function () {
            $row.html(GetDeletedRow(subject));
        });
    };

    var DeleteSelected = function () {
        var data = $(Selectors.SelectedForm).serialize();

        $.post(Urls.DeleteSelected, data, function () {
            $(Selectors.BlooMailTable).find('tr:has(input:checked)').each(function () {
                var $row = $(this),
                    subject = $row.find(Selectors.SubjectLink).text(),
                    blooMailId = $row.attr('data-id');

                $row.html(GetDeletedRow(subject));
            });
        });
    };

    var Filter = function (sort, page) {
        var data =
        {
            from: $(Selectors.FromDropDown).val(),
            query: $(Selectors.QueryTextBox).val(),
            read: $(Selectors.StatusDropDown).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Filter, data, function (results) {
            var $results = $(results).fixDates();
            $(Selectors.ResultsDiv).html($results);
        });
    }

    var GetDeletedRow = function (subject) {
        return '<td colspan="4" style="text-align: center"><strong>'
            + subject
            + '</strong> has been deleted</td><td><a class="bloo-mail-undelete">Undo</a></td>';
    };

    var GetRow = function (element, blooMailId) {
        $.ajax({
            type: 'GET',
            cache: false,
            url: Urls.GetRow(blooMailId),
            success: function (results) {
                var $results = $(results).fixDates();
                $(element).parents('tr').replaceWith($results);
            }
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).live('click', ClearFilter);
        $(Selectors.DeleteButton).live('click', DeleteSelected);
        $(Selectors.DeleteImage).live('click', Delete);
        $(Selectors.FilterButton).live('click', function () { Filter(); });
        $(Selectors.MarkAsReadButton).live('click', MarkAsRead);
        $(Selectors.MarkOneReadImage).live('click', MarkRead);
        $(Selectors.MarkUnreadImage).live('click', MarkUnread);
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var MarkAsRead = function () {
        var data = $(Selectors.SelectedForm).serialize();

        $.post(Urls.MarkAsRead, data, function () {
            $(Selectors.BlooMailTable).find('tr:has(input:checked)').each(function () {
                var $row = $(this),
                    blooMailId = $row.find('input:checked').attr('value');

                $.ajax({
                    type: 'GET',
                    cache: false,
                    url: Urls.GetRow(blooMailId),
                    success: function (results) {
                        var $results = $(results).fixDates();
                        $row.replaceWith($results);
                    }
                });
            });
        });
    };

    var MarkRead = function (event) {
        var $this = $(event.target),
            blooMailId = $this.parents('tr').attr('data-id');

        $.post(Urls.MarkRead(blooMailId), function () { GetRow($this, blooMailId); });
    };

    var MarkUnread = function (event) {
        var $this = $(event.target),
            blooMailId = $this.parents('tr').attr('data-id');

        $.post(Urls.MarkUnread(blooMailId), function () { GetRow($this, blooMailId); });
    };

    var Undelete = function (event) {
        var $this = $(event.target),
            blooMailId = $this.parents('tr').attr('data-id');

        $.post(Urls.UndeleteOne(blooMailId), function () { GetRow($this, blooMailId); });
    };

    return {
        GetList: Filter,
        Init: Init,
        Urls: Urls
    }
} ();

var BlooMailSent = function () {
    var Selectors =
    {
        BlooMailTable: '#blooMails',
        ClearButton: '#clear-button',
        DeleteButton: '#delete-button',
        DeleteImage: 'td.bloo-mail-delete > img.button',
        FilterButton: '#filter-button',
        QueryTextBox: '#Query',
        ResultsDiv: '#results',
        SelectedForm: '#selectedForm',
        SubjectLink: 'a.subject',
        ToDropDown: '#To',
        UndeleteLink: 'a.bloo-mail-undelete'
    };

    var Urls =
    {
        DeleteOne: function (blooMailId) { return ''; },
        DeleteSelected: '',
        Filter: '',
        GetRow: function (blooMailId) { return ''; },
        UndeleteOne: function (blooMailId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.QueryTextBox).val('');
        $(Selectors.ToDropDown).val('');

        $.getNoCache(Urls.Filter, function (results) {
            var $results = $(results).fixDates();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Delete = function (event) {
        var $this = $(event.target),
            $row = $this.parents('tr'),
            blooMailId = $row.attr('data-id'),
            subject = $row.find(Selectors.SubjectLink).text();

        $.post(Urls.DeleteOne(blooMailId), { sender: false }, function () {
            $row.html(GetDeletedRow(subject));
        });
    };

    var DeleteSelected = function () {
        var data = $(Selectors.SelectedForm).serialize();

        $.post(Urls.DeleteSelected, data, function () {
            $(Selectors.BlooMailTable).find('tr:has(input:checked)').each(function () {
                var $row = $(this),
                    subject = $row.find(Selectors.SubjectLink).text(),
                    blooMailId = $row.attr('data-id');

                $row.html(GetDeletedRow(subject));
            });
        });
    };

    var Filter = function (sort, page) {
        var data =
        {
            query: $(Selectors.QueryTextBox).val(),
            to: $(Selectors.ToDropDown).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Filter, data, function (results) {
            var $results = $(results).fixDates();
            $(Selectors.ResultsDiv).html($results);
        });
    }

    var GetDeletedRow = function (subject) {
        return '<td colspan="4" style="text-align: center"><strong>'
            + subject
            + '</strong> has been deleted</td><td><a class="bloo-mail-undelete">Undo</a></td>';
    };

    var GetRow = function (element, blooMailId) {
        $.ajax({
            type: 'GET',
            cache: false,
            url: Urls.GetRow(blooMailId),
            success: function (results) {
                var $results = $(results).fixDates();
                $(element).parents('tr').replaceWith($results);
            }
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).live('click', ClearFilter);
        $(Selectors.DeleteButton).live('click', DeleteSelected);
        $(Selectors.DeleteImage).live('click', Delete);
        $(Selectors.FilterButton).live('click', function () { Filter(); });
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var Undelete = function (event) {
        var $this = $(event.target),
            blooMailId = $this.parents('tr').attr('data-id');

        $.post(Urls.UndeleteOne(blooMailId), function () { GetRow($this, blooMailId); });
    };

    return {
        GetList: Filter,
        Init: Init,
        Urls: Urls
    }
} ();

var CharacterCompare = function () {
    var Selectors = {
        Character1Div: '#char1',
        Character1DropDown: '#CharacterId1',
        Character2Div: '#char2',
        Character2DropDown: '#CharacterId2',
        CommanderEffectDiv: 'div.compare-ce',
        Faction1DropDown: '#FactionId1',
        Faction1Link: '#faction-link-1',
        Faction2DropDown: '#FactionId2',
        Faction2Link: '#faction-link-2',
        ForcePowerDiv: 'div.compare-fp',
        NameLink: 'a.character-name',
        RatingDiv: 'div.compare-rating',
        RatingDropDown: '#myRating',
        Set1DropDown: '#sets-1',
        Set1Link: '#set-link-1',
        Set2DropDown: '#sets-2',
        Set2Link: '#set-link-2',
        SpecialAbilityDiv: 'div.compare-sa'
    };

    var Urls = {
        GetCharactersByFaction: function (factionId) { return ''; },
        GetCharactersBySet: function (setId) { return ''; },
        GetComparison: function (characterId) { return ''; },
        GetFactionList: function (characterId) { return ''; },
        GetRating: function (characterId) { return ''; },
        GetSetList: function (characterId) { return ''; },
        MergeRating: function (characterId) { return ''; }
    };

    var AlignComparisons = function () {
        var $character1Div = $(Selectors.Character1Div),
            $character2Div = $(Selectors.Character2Div),
            $specialAbility1Div = $character1Div.find(Selectors.SpecialAbilityDiv),
            $specialAbility2Div = $character2Div.find(Selectors.SpecialAbilityDiv);

        $(Selectors.SpecialAbilityDiv).css('height', 'auto');

        var height1 = $specialAbility1Div.height(),
            height2 = $specialAbility2Div.height();

        if (height1 > height2) {
            $specialAbility2Div.height(height1);
        }
        else if (height2 > height1) {
            $specialAbility1Div.height(height2);
        }

        var $forcePower1Div = $character1Div.find(Selectors.ForcePowerDiv),
            $forcePower2Div = $character2Div.find(Selectors.ForcePowerDiv);

        $(Selectors.ForcePowerDiv).css('height', 'auto');

        height1 = $forcePower1Div.height();
        height2 = $forcePower2Div.height();

        if (height1 > height2) {
            $forcePower2Div.height(height1);
        }
        else if (height2 > height1) {
            $forcePower1Div.height(height2);
        }

        var $commanderEffect1Div = $character1Div.find(Selectors.CommanderEffectDiv),
            $commanderEffect2Div = $character2Div.find(Selectors.CommanderEffectDiv);

        $(Selectors.CommanderEffectDiv).css('height', 'auto');

        height1 = $commanderEffect1Div.height();
        height2 = $commanderEffect2Div.height();

        if (height1 > height2) {
            $commanderEffect2Div.height(height1);
        }
        else if (height2 > height1) {
            $commanderEffect1Div.height(height2);
        }
    };

    var ChangeCharacter1 = function () {
        var characterId = $(Selectors.Character1DropDown).val();
        $(Selectors.Character1Div).load(Urls.GetComparison(characterId), CharacterChanged);
    };

    var ChangeCharacter2 = function () {
        var characterId = $(Selectors.Character2DropDown).val();
        $(Selectors.Character2Div).load(Urls.GetComparison(characterId), CharacterChanged);
    };

    var ChangeFaction1 = function (characterId) {
        var factionId = $(Selectors.Faction1DropDown).val(),
            $characterDropDown = $(Selectors.Character1DropDown);

        $characterDropDown.load(Urls.GetCharactersByFaction(factionId), function () {
            if (characterId) {
                $characterDropDown.val(characterId);
            }
            else {
                ChangeCharacter1();
            }
        });
    };

    var ChangeFaction2 = function (characterId) {
        var factionId = $(Selectors.Faction2DropDown).val(),
            $characterDropDown = $(Selectors.Character2DropDown);

        $characterDropDown.load(Urls.GetCharactersByFaction(factionId), function () {
            if (characterId) {
                $characterDropDown.val(characterId);
            }
            else {
                ChangeCharacter2();
            }
        });
    };

    var ChangeRating1 = function () {
        var value = $(Selectors.Character1Div).find(Selectors.RatingDropDown).val(),
            characterId = $(Selectors.Character1DropDown).val();

        $.post(Urls.MergeRating(characterId), { value: value }, LoadRating1);
    };

    var ChangeRating2 = function () {
        var value = $(Selectors.Character2Div).find(Selectors.RatingDropDown).val(),
            characterId = $(Selectors.Character2DropDown).val();

        $.post(Urls.MergeRating(characterId), { value: value }, LoadRating2);
    };

    var ChangeSet1 = function (characterId) {
        var setId = $(Selectors.Set1DropDown).val(),
            $characterDropDown = $(Selectors.Character1DropDown);

        $characterDropDown.load(Urls.GetCharactersBySet(setId), function () {
            if (characterId) {
                $characterDropDown.val(characterId);
            }
            else {
                ChangeCharacter1();
            }
        });
    };

    var ChangeSet2 = function (characterId) {
        var setId = $(Selectors.Set2DropDown).val(),
            $characterDropDown = $(Selectors.Character2DropDown);

        $characterDropDown.load(Urls.GetCharactersBySet(setId), function () {
            if (characterId) {
                $characterDropDown.val(characterId);
            }
            else {
                ChangeCharacter2();
            }
        });
    };

    var CharacterChanged = function () {
        ShowComparisons();
        AlignComparisons();

        var name1 = $(Selectors.Character1Div).find(Selectors.NameLink).text(),
            name2 = $(Selectors.Character2Div).find(Selectors.NameLink).text();

        document.title = name1 + ' vs. ' + name2 + ' - Bloo Milk';
    };

    var CompareStat = function (stat, reversed) {
        $('.' + stat).remove();

        var $stat1 = $(Selectors.Character1Div).find('.compare-' + stat),
            $stat2 = $(Selectors.Character2Div).find('.compare-' + stat),
            value1 = $stat1.text(),
            value2 = $stat2.text();

        if (!value1 || value == '' || !value2 || value2 == '') {
            return;
        }

        if (value1 == '??' || value1 == '--' || value1 == 'X') {
            value1 = 0;
        }

        if (value2 == '??' || value2 == '--' || value2 == 'X') {
            value2 = 0;
        }

        var value = value1 - value2;
        if (value == 0) {
            return;
        }

        if (value > 0) {
            if (!reversed) {
                $('<span/>', {
                    'class': stat + ' better',
                    text: ' (+' + value + ')'
                }).appendTo($stat1);

                $('<span/>', {
                    'class': stat + ' worse',
                    text: ' (-' + value + ')'
                }).appendTo($stat2);
            }
            else {
                $('<span/>', {
                    'class': stat + ' worse',
                    text: ' (+' + value + ')'
                }).appendTo($stat1);

                $('<span/>', {
                    'class': stat + ' better',
                    text: ' (-' + value + ')'
                }).appendTo($stat2);
            }

            return;
        }

        value = value * -1;

        if (!reversed) {
            $('<span/>', {
                'class': stat + ' better',
                text: ' (+' + value + ')'
            }).appendTo($stat2);

            $('<span/>', {
                'class': stat + ' worse',
                text: ' (-' + value + ')'
            }).appendTo($stat1);
        }
        else {
            $('<span/>', {
                'class': stat + ' worse',
                text: ' (+' + value + ')'
            }).appendTo($stat2);

            $('<span/>', {
                'class': stat + ' better',
                text: ' (-' + value + ')'
            }).appendTo($stat1);
        }
    };

    var Init = function () {
        $(Selectors.Character1DropDown).change(ChangeCharacter1);
        $(Selectors.Character2DropDown).change(ChangeCharacter2);
        $(Selectors.Faction1DropDown).change(function () { ChangeFaction1(); });
        $(Selectors.Faction1Link).click(LoadFactions1);
        $(Selectors.Faction2DropDown).change(function () { ChangeFaction2(); });
        $(Selectors.Faction2Link).click(LoadFactions2);
        $(Selectors.Set1DropDown).change(function () { ChangeSet1(); });
        $(Selectors.Set1Link).click(LoadSets1);
        $(Selectors.Set2DropDown).change(function () { ChangeSet2(); });
        $(Selectors.Set2Link).click(LoadSets2);

        $(Selectors.Character1Div).find(Selectors.RatingDropDown).live('change', ChangeRating1);
        $(Selectors.Character2Div).find(Selectors.RatingDropDown).live('change', ChangeRating2);

        ShowComparisons();
        AlignComparisons();
    };

    var LoadFactions1 = function () {
        var characterId = $(Selectors.Character1DropDown).val(),
            $factions = $(Selectors.Faction1DropDown);

        $factions.load(Urls.GetFactionList(characterId), function () {
            $factions.show();
            $(Selectors.Set1DropDown).hide();

            ChangeFaction1(characterId);
        });
    };

    var LoadFactions2 = function () {
        var characterId = $(Selectors.Character2DropDown).val(),
            $factions = $(Selectors.Faction2DropDown);

        $factions.load(Urls.GetFactionList(characterId), function () {
            $factions.show();
            $(Selectors.Set2DropDown).hide();

            ChangeFaction2(characterId);
        });
    };

    var LoadRating1 = function () {
        var characterId = $(Selectors.Character1DropDown).val();

        $.getNoCache(Urls.GetRating(characterId), function (results) {
            $(Selectors.Character1Div).find(Selectors.RatingDiv).html(results);
        });
    };

    var LoadRating2 = function () {
        var characterId = $(Selectors.Character2DropDown).val();

        $.getNoCache(Urls.GetRating(characterId), function (results) {
            $(Selectors.Character2Div).find(Selectors.RatingDiv).html(results);
        });
    };

    var LoadSets1 = function () {
        var characterId = $(Selectors.Character1DropDown).val(),
            $sets = $(Selectors.Set1DropDown);

        $sets.load(Urls.GetSetList(characterId), function () {
            $sets.show();
            $(Selectors.Faction1DropDown).hide();

            ChangeSet1(characterId);
        });
    };

    var LoadSets2 = function () {
        var characterId = $(Selectors.Character2DropDown).val(),
            $sets = $(Selectors.Set2DropDown);

        $sets.load(Urls.GetSetList(characterId), function () {
            $sets.show();
            $(Selectors.Faction2DropDown).hide();

            ChangeSet2(characterId);
        });
    };

    var ShowComparisons = function () {
        CompareStat('cost', true);
        CompareStat('hp');
        CompareStat('def');
        CompareStat('atk');
        CompareStat('dmg');
    };

    return {
        Init: Init,
        Urls: Urls
    };
} ();

var CharacterSearch = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        SearchButton: '#search-button',
        SearchResultsDiv: '#search-results'
    };

    var Urls = {
        GetSearchResults: ''
    };

    var ClearSearch = function () {
        $('form')[0].reset();
    };

    var Init = function () {
        $(Selectors.ClearButton).click(ClearSearch);
        $(Selectors.SearchButton).click(function () { Search(); });

        $('fieldset').defaultButton('button[type=button]');
    };

    var Search = function (sort, page) {
        var data = $('form').serialize();

        data = TryAppend(data, 'sort', sort);
        data = TryAppend(data, 'page', page);

        $.getNoCache(Urls.GetSearchResults, data, function (results) {
            var $results = $(results).fixTable();
            $(Selectors.SearchResultsDiv).html($results).show();
        });

        return false;
    };

    return {
        GetList: Search,
        Init: Init,
        Urls: Urls
    };
} ();

var CharacterUsage = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        FilterButton: '#filter-button',
        FilterForm: '#filter-form',
        ResultsDiv: '#results'
    };

    var Urls = {
        UsageGrid: ''
    };

    var Clear = function () {
        $(Selectors.FilterForm)[0].reset();
        Filter();
    };

    var Filter = function (sort, page) {
        var data = $(Selectors.FilterForm).serialize();

        data = TryAppend(data, 'sort', sort);
        data = TryAppend(data, 'page', page);

        $.getNoCache(Urls.UsageGrid, data, function (results) {
            var $results = $(results).fixTable();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).click(Clear);
        $(Selectors.FilterButton).click(function () { Filter(); });

        $('fieldset').defaultButton();
    };

    return {
        GetList: Filter,
        Init: Init,
        Urls: Urls
    };
} ();

var Comment = function () {
    var Selectors =
    {
        AddCommentButton: '#add-comment-button',
        CancelCommentImage: 'img.button.comment-cancel',
        CommentDiv: 'div.Comment',
        Comments: '#comments',
        DeleteCommentImage: 'img.button.comment-delete',
        EditCommentImage: 'img.button.comment-edit',
        ModCommentImage: 'img.button.comment-mod',
        ModNotesTextBox: '#ModNotes',
        NewComment: '#newComment',
        TextTextArea: 'textarea#Text',
        UpdateCommentImage: 'img.button.comment-update'
    };

    var Urls =
    {
        AddComment: '',
        DeleteComment: function (commentId) { return ''; },
        GetComments: '',
        ModComment: function (commentId) { return ''; },
        UpdateComment: function (commentId) { return ''; }
    };

    var Add = function () {
        var text = $.trim($(Selectors.NewComment).val());

        if (!text) {
            Load();
            return;
        }

        $.post(Urls.AddComment, { comment: text }, function () { Load(); });
    };

    var Delete = function (event) {
        if (confirm('Are you sure you want to delete this comment?')) {
            var commentId = $(event.target).parents(Selectors.CommentDiv).attr('data-id');
            $.post(Urls.DeleteComment(commentId), function () { Load(); });
        }
    };

    var Edit = function (event) {
        var commentId = $(event.target).parents(Selectors.CommentDiv).attr('data-id');
        Load(commentId);
    };

    var Init = function () {
        $(Selectors.AddCommentButton).live('click', Add);
        $(Selectors.CancelCommentImage).live('click', function () { Load(); });
        $(Selectors.DeleteCommentImage).live('click', Delete);
        $(Selectors.EditCommentImage).live('click', Edit);
        $(Selectors.ModCommentImage).live('click', Mod);
        $(Selectors.UpdateCommentImage).live('click', Update);

        Load();
    };

    var Load = function (editCommentId) {
        $.ajax({
            type: 'GET',
            cache: false,
            url: Urls.GetComments,
            data: { editCommentId: editCommentId },
            success: function (results) {
                var $results = $(results).fixDates();
                $(Selectors.Comments).html($results).show();
            }
        });
    };

    var Mod = function (event) {
        var $commentDiv = $(event.target).parents(Selectors.CommentDiv),
            commentId = $commentDiv.attr('data-id'),
            text = $.trim($commentDiv.find(Selectors.TextTextArea).val()),
            modNotes = $.trim($commentDiv.find(Selectors.ModNotesTextBox).val());

        if (!text || !modNotes) {
            Load(commentId);
            return;
        }

        $.post(Urls.ModComment(commentId), { modNotes: modNotes, text: text }, function () { Load(); });
    };

    var Update = function (event) {
        var $commentDiv = $(event.target).parents(Selectors.CommentDiv),
            commentId = $commentDiv.attr('data-id'),
            text = $.trim($commentDiv.find(Selectors.TextTextArea).val());

        if (!text) {
            Load(commentId);
            return;
        }

        $.post(Urls.UpdateComment(commentId), { text: text }, function () { Load(); });
    };

    return {
        Init: Init,
        Urls: Urls
    };
} ();

var CommentReview = function () {
    var Selectors = {
        CancelCommentImage: 'img.button.comment-cancel',
        ClearButton: '#clear-button',
        CommentReviewDiv: 'div.CommentReview',
        CommentsDiv: '#comments',
        DeleteCommentImage: 'img.button.comment-delete',
        EditCommentImage: 'img.button.comment-edit',
        FilterButton: '#filter-button',
        FilterDiv: '#filter-div',
        FilterForm: '#filter-form',
        FilterId: '#FilterId',
        FilterLink: 'a.comment-filter',
        FilterName: '#filter-name',
        ModCommentImage: 'img.button.comment-mod',
        ModNotesTextBox: '#ModNotes',
        NewCommentTextArea: 'textarea.NewCommentTextBox',
        UnfilterLink: 'a.comment-unfilter',
        UpdateCommentImage: 'img.button.comment-update'
    };

    var Urls = {
        DeleteComment: function (commentId) { return ''; },
        GetComments: '',
        ModComment: function (commentId) { return ''; },
        UpdateComment: function (commentId) { return ''; }
    };

    var Clear = function () {
        $(Selectors.FilterForm)[0].reset();
        Unfilter();
    };

    var Delete = function (event) {
        if (confirm("Are you sure you want to delete this comment?")) {
            $.post(Urls.DeleteComment(commentId), function () { Load(); });
        }
    };

    var Edit = function (event) {
        var commentId = $(event.target).parents(Selectors.CommentReviewDiv).attr('data-id');
        Load(commentId);
    };

    var Filter = function (event) {
        var id = $(event.target).attr('data-id');
        $(Selectors.FilterId).val(id);
        $(Selectors.FilterName).text(id);
        $(Selectors.FilterDiv).show();
        Load();
    };

    var Init = function () {
        $(Selectors.CancelCommentImage).live('click', function () { Load(); });
        $(Selectors.ClearButton).click(Clear);
        $(Selectors.DeleteCommentImage).live('click', Delete);
        $(Selectors.EditCommentImage).live('click', Edit);
        $(Selectors.FilterButton).click(function () { Load(); });
        $(Selectors.FilterLink).live('click', Filter);
        $(Selectors.UnfilterLink).live('click', Unfilter);
        $(Selectors.UpdateCommentImage).live('click', Update);
    };

    var Load = function (editCommentId, page) {
        var data = $(Selectors.FilterForm).serialize();

        data = TryAppend(data, 'editCommentId', editCommentId);
        data = TryAppend(data, 'page', page);

        $.getNoCache(Urls.GetComments, data, function (results) {
            var $results = $(results).fixDates();
            $(Selectors.CommentsDiv).html($results);
        });
    };

    var Mod = function (event) {
        var $commentReviewDiv = $(event.target).parents(Selectors.CommentReviewDiv),
            commentId = $commentReviewDiv.attr('data-id'),
            text = $.trim($commentReviewDiv.find(Selectors.NewCommentTextArea).val()),
            modNotes = $.trim($commentReviewDiv.find(Selectors.ModNotesTextBox).val());

        if (!text || !modNotes) {
            Load(commentId);
            return;
        }

        $.post(Urls.ModComment(commentId), { modNotes: modNotes, text: text }, function () { Load(); });
    };

    var Unfilter = function () {
        $(Selectors.FilterDiv).hide();
        $(Selectors.FilterId).val('');
        $(Selectors.FilterName).text('');
        Load();
    };

    var Update = function (event) {
        var $commentReviewDiv = $(event.target).parents(Selectors.CommentReviewDiv),
            commentId = $commentReviewDiv.attr('data-id'),
            text = $.trim($commentReviewDiv.find(Selectors.NewCommentTextArea).val());

        if (!text) {
            Load(commentId);
            return;
        }

        $.post(Urls.UpdateComment(commentId), { text: text }, function () { Load(); });
    };

    return {
        GetList: Load,
        Init: Init,
        Urls: Urls
    };
} ();

var CustomCharacterList = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        DeleteLink: 'span.custom-character-delete > a',
        NameLink: 'a.custom-character',
        QueryTextBox: '#query',
        ResultsDiv: '#results',
        SearchButton: '#search-button',
        UndeleteLink: 'a.custom-character-undelete'
    };

    var Urls = {
        Delete: function (customCharacterId) { return ''; },
        GetRow: function (customCharacterId) { return ''; },
        Grid: '',
        Undelete: function (customCharacterId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.QueryTextBox).val('');

        GetList();
    };

    var Delete = function (event) {
        var $row = $(event.target).parents('tr'),
            customCharacterId = $row.attr('data-id'),
            name = $row.find(Selectors.NameLink).text();

        $.post(Urls.Delete(customCharacterId), function () {
            $row.html(GetDeletedRow(name));
        });
    };

    var GetDeletedRow = function (name) {
        return '<td colspan="14" style="text-align: center"><strong>'
            + name
            + '</strong> has been deleted</td><td><a class="custom-character-undelete">Undo</a></td>';
    };

    var GetList = function (sort, page) {
        var data = {
            query: $(Selectors.QueryTextBox).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Grid, data, function (results) {
            var $results = $(results).fixDates().fixTable();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).click(ClearFilter);
        $(Selectors.DeleteLink).live('click', Delete);
        $(Selectors.SearchButton).click(function () { GetList(); });
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var Undelete = function (event) {
        var $row = $(event.target).parents('tr'),
            customCharacterId = $row.attr('data-id');

        $.post(Urls.Undelete(customCharacterId), function () {
            $.getNoCache(Urls.GetRow(customCharacterId), function (results) {
                var $results = $(results).fixDates();
                $row.replaceWith($results);
            });
        });
    };

    return {
        GetList: GetList,
        Init: Init,
        Urls: Urls
    };
} ();

var CustomForcePowerList = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        DeleteLink: 'span.custom-force-power-delete > a',
        NameLink: 'a.custom-force-power',
        QueryTextBox: '#query',
        ResultsDiv: '#results',
        SearchButton: '#search-button',
        UndeleteLink: 'a.custom-force-power-undelete'
    };

    var Urls = {
        Delete: function (customForcePowerId) { return ''; },
        GetRow: function (customForcePowerId) { return ''; },
        Grid: '',
        Undelete: function (customForcePowerId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.QueryTextBox).val('');

        GetList();
    };

    var Delete = function (event) {
        var $row = $(event.target).parents('tr'),
            customForcePowerId = $row.attr('data-id'),
            name = $row.find(Selectors.NameLink).text();

        $.post(Urls.Delete(customForcePowerId), function () {
            $row.html(GetDeletedRow(name));
        });
    };

    var GetDeletedRow = function (name) {
        return '<td colspan="5" style="text-align: center"><strong>'
            + name
            + '</strong> has been deleted</td><td><a class="custom-force-power-undelete">Undo</a></td>';
    };

    var GetList = function (sort, page) {
        var data = {
            query: $(Selectors.QueryTextBox).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Grid, data, function (results) {
            var $results = $(results).fixDates().fixTable();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).click(ClearFilter);
        $(Selectors.DeleteLink).live('click', Delete);
        $(Selectors.SearchButton).click(function () { GetList(); });
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var Undelete = function (event) {
        var $row = $(event.target).parents('tr'),
            customForcePowerId = $row.attr('data-id');

        $.post(Urls.Undelete(customForcePowerId), function () {
            $.getNoCache(Urls.GetRow(customForcePowerId), function (results) {
                var $results = $(results).fixDates();
                $row.replaceWith($results);
            });
        });
    };

    return {
        GetList: GetList,
        Init: Init,
        Urls: Urls
    };
} ();

var CustomSetList = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        DeleteLink: 'span.custom-set-delete > a',
        NameLink: 'a.custom-set',
        QueryTextBox: '#query',
        ResultsDiv: '#results',
        SearchButton: '#search-button',
        UndeleteLink: 'a.custom-set-undelete'
    };

    var Urls = {
        Delete: function (customSetId) { return ''; },
        GetRow: function (customSetId) { return ''; },
        Grid: '',
        Undelete: function (customSetId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.QueryTextBox).val('');

        GetList();
    };

    var Delete = function (event) {
        var $row = $(event.target).parents('tr'),
            customSetId = $row.attr('data-id'),
            name = $row.find(Selectors.NameLink).text();

        $.post(Urls.Delete(customSetId), function () {
            $row.html(GetDeletedRow(name));
        });
    };

    var GetDeletedRow = function (name) {
        return '<td colspan="6" style="text-align: center"><strong>'
            + name
            + '</strong> has been deleted</td><td><a class="custom-special-ability-undelete">Undo</a></td>';
    };

    var GetList = function (sort, page) {
        var data = {
            query: $(Selectors.QueryTextBox).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Grid, data, function (results) {
            var $results = $(results).fixDates().fixTable();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).click(ClearFilter);
        $(Selectors.DeleteLink).live('click', Delete);
        $(Selectors.SearchButton).click(function () { GetList(); });
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var Undelete = function (event) {
        var $row = $(event.target).parents('tr'),
            customSetId = $row.attr('data-id');

        $.post(Urls.Undelete(customSetId), function () {
            $.getNoCache(Urls.GetRow(customSetId), function (results) {
                var $results = $(results).fixDates();
                $row.replaceWith($results);
            });
        });
    };

    return {
        GetList: GetList,
        Init: Init,
        Urls: Urls
    };
} ();

var CustomSpecialAbilityList = function () {
    var Selectors = {
        ClearButton: '#clear-button',
        DeleteLink: 'span.custom-special-ability-delete > a',
        NameLink: 'a.custom-special-ability',
        QueryTextBox: '#query',
        ResultsDiv: '#results',
        SearchButton: '#search-button',
        UndeleteLink: 'a.custom-special-ability-undelete'
    };

    var Urls = {
        Delete: function (customSpecialAbilityId) { return ''; },
        GetRow: function (customSpecialAbilityId) { return ''; },
        Grid: '',
        Undelete: function (customSpecialAbilityId) { return ''; }
    };

    var ClearFilter = function () {
        $(Selectors.QueryTextBox).val('');

        GetList();
    };

    var Delete = function (event) {
        var $row = $(event.target).parents('tr'),
            customSpecialAbilityId = $row.attr('data-id'),
            name = $row.find(Selectors.NameLink).text();

        $.post(Urls.Delete(customSpecialAbilityId), function () {
            $row.html(GetDeletedRow(name));
        });
    };

    var GetDeletedRow = function (name) {
        return '<td colspan="5" style="text-align: center"><strong>'
            + name
            + '</strong> has been deleted</td><td><a class="custom-special-ability-undelete">Undo</a></td>';
    };

    var GetList = function (sort, page) {
        var data = {
            query: $(Selectors.QueryTextBox).val()
        };

        if (sort) {
            data.sort = sort;
        }

        if (page) {
            data.page = page;
        }

        $.getNoCache(Urls.Grid, data, function (results) {
            var $results = $(results).fixDates().fixTable();
            $(Selectors.ResultsDiv).html($results);
        });
    };

    var Init = function () {
        $(Selectors.ClearButton).click(ClearFilter);
        $(Selectors.DeleteLink).live('click', Delete);
        $(Selectors.SearchButton).click(function () { GetList(); });
        $(Selectors.UndeleteLink).live('click', Undelete);

        $('fieldset').defaultButton();
    };

    var Undelete = function (event) {
        var $row = $(event.target).parents('tr'),
            customSpecialAbilityId = $row.attr('data-id');

        $.post(Urls.Undelete(customSpecialAbilityId), function () {
            $.getNoCache(Urls.GetRow(customSpecialAbilityId), function (results) {
                var $results = $(results).fixDates();
                $row.replaceWith($results);
            });
        });
    };

    return {
        GetList: GetList,
        Init: Init,
        Urls: Urls
    };
} ();

var DynamicDuoList = function () {
    var Selectors = {
        RatingDropDown: 'select.dynamic-duo-rating'
    };

    var Urls = {
        GetRow: function (dynamicDuoId) { return ''; },
        MergeRating: function (dynamicDuoId) { return ''; }
    };

    var ChangeRating = function (event) {
        var $this = $(event.target),
            value = $this.val(),
            $row = $this.parents('tr'),
            dynamicDuoId = $row.attr('data-id');

        $.post(Urls.MergeRating(dynamicDuoId), { value: value }, function () {
            $.getNoCache(Urls.GetRow(dynamicDuoId), function (results) {
                $row.replaceWith(results);
            });
        });
    };

    var Init = function () {
        $(Selectors.RatingDropDown).live('change', ChangeRating);
    };

    return {
        Init: Init,
        Urls: Urls
    };
} ();
