/* CONCAT of
/2static/script/fecru/ajax.js
/2static/script/fecru/browse.js
/2static/script/fecru/dialog.js
/2static/script/fecru/gwtutil.js
/2static/script/fecru/hover.js
/2static/script/fecru/profile.js
/2static/script/fecru/rss.js
/2static/script/fecru/ui.js
/2static/script/fecru/onReady.js
/2static/script/fecru/star.js
/2static/script/fecru/applinks.js
/2static/script/fecru/prefs.js
/2static/script/lib/graphael/g.raphael.js
/2static/script/lib/graphael/g.bar.js
/2static/script/lib/graphael/g.pie.js
/2static/script/lib/graphael/g.line.js
/2static/script/lib/graphael/g.dot.js
/2static/script/fecru/raphaelCharts.js
/script/common/global.js
/2static/script/lib/ajs/contentnamesearch.js
/2static/script/fe/fisheye-ui.js
/2static/script/fe/fisheye-changeset.js
/2static/script/fe/fisheye-history.js
/script/activitystream.js
/2static/script/lib/json2.min.js
/2static/script/cru/util.js
/2static/script/cru/dialog/dialog.js
/2static/script/cru/dialog/dialog-event.js
/2static/script/cru/review/email-comments.js
/2static/script/cru/review/review-history.js
*/
/* START /2static/script/fecru/ajax.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.AJAX = {};
(function($) {

    /**
     * Factory method that creates a new SequentialAjaxExecutor instance.
     */
    AJS.FECRU.AJAX.createSequentialExecutor = function() {
        return new SequentialAjaxExecutor();
    };

    /******************************************************************
     ******************** AJAX METHODS ********************************
     ******************************************************************/

    /**
     * This class represents a serial channel for ajax calls. An ajax call that
     * is submitted only gets run after the previous call has returned from the
     * server.
     * This is used by Crucible comments: when you click "post" while autosave
     * is waiting for the server to create and return the new commentId, the
     * Post action is queued and automatically gets evaluated after autosave
     * has returned (and populated the js comment model with the new commentId).
     */
    function SequentialAjaxExecutor() {

        var busy = false;
        var $jobs = $('<div></div>');   // private var to SequentialAjaxExecutor, never inserted into the document.

        /**
         * Makes an AJAX call, similar to AJS.FECRU.AJAX.ajaxDo(), but with the exception that
         * only one call can be active at any time. When this method is called
         * a second time, while the previous call has not yet finished, the new
         * call will be scheduled and executed automatically when the previous
         * call finishes.
         *
         * @param url
         * @param params
         * @param onCompleteFunc
         */
        this.executeAjax = function(url, params, onCompleteFunc, noDialog) {
            this.executeAjaxJob({
                url : url,
                params : params,
                callback : onCompleteFunc,
                noDialog : noDialog
            });
        };

        /**
         * Similar to SerializedAjaxDispatcher.executeAjax(), but takes a hash with
         * functions that allow callers to evaluate their url and query params
         * just-in-time.
         *
         * @param job
         */
        this.executeAjaxJob = function(job) {
            if (busy) {
                var dispatcher = this;
                $jobs.queue(function() {
                    dispatcher.executeAjaxJob(job);
                });
            } else {
                busy = true;
                AJS.FECRU.AJAX.ajaxDo(
                        $.isFunction(job.url) ? job.url() : job.url,
                        $.isFunction(job.params) ? job.params() : job.params,
                        function(resp) {
                            try {
                                if (job.callback) {
                                    job.callback(resp);
                                }
                            } finally {
                                busy = false;
                                $jobs.dequeue();
                            }
                        },
                        $.isFunction(job.noDialog) ? job.noDialog() : job.noDialog);
            }
        };

        this.isPending = function() {
            return busy;
        };
    }

    AJS.FECRU.AJAX.ajaxDo = function (url, params, onCompleteFunc, noDialog) {
        AJS.FECRU.AJAX.ajaxUpdate(url, params, null, onCompleteFunc, noDialog);
    };

    /**
     * Makes a Ajax request with the specified parameters and runs the onCompleteFunc
     * function when the call returns. Optionally, a DOM element can be passed in
     * which will be updated with the payload of the Ajax response (the JSON response
     * object must contain a "payload" member.
     * Also, the user can pass a second function (onPayloadEvalFunc) that will be
     * called after the payload content has been put in the specified elementToUpdate
     * element. If elementIdToUpdate is null, the onPayloadEvalFunc will not be called.
     *
     * noDialog is optional: if falsy, the error message dialog is shown on error.
     * If it is a callback, it is invoked on error instead of showing the dialog.
     * Otherwise, if it is truthy, the dialog is not shown on error and no action
     * is taken.
     */
    AJS.FECRU.AJAX.ajaxUpdate = function (url, params, elementIdToUpdate, onCompleteFunc, noDialog, feedbackRemove) {
        // Implemementation note:
        // jQuery does an eval(req.responseText) for us internally. We have an onSuccess callback to
        // intercept this, otherwise we end up eval'ing the response text twice (which is poor form).
        // The onComplete callback gives us access to the XmlHttpRequest and allows us to do extra
        // error handling if required.

        var data = false;
        var onSuccess = function(response) {
            data = response;
        };

        var onComplete = function(req) {
            var cleanUpOnFail = function () {
                if (onCompleteFunc) {
                    onCompleteFunc({ worked: false });  //allow function to clean up
                }
            };
            try {
                if (req.status == 0 ) {
                    return;
                }
                if (!data || !isECMA(req)) {
                    noDialog || displayUnexpectedResponse(req);
                    cleanUpOnFail();
                    return;
                }

                var $updateMe;
                if (elementIdToUpdate) {
                    $updateMe = $("#"+elementIdToUpdate);
                }

                if (!data.worked) {
                    if (!noDialog) {
                        appendErrorResponse(data.errorMsg);
                        appendCredentialsRequests(data.credentialsRequired);
                        if (data.userError) {
                            showUserErrorBox();
                        } else {
                            showErrorBox();
                        }
                    }
                } else if ($updateMe && $updateMe.length === 1) {
                    if ($updateMe.is(':input')) {
                        $updateMe.val(data.payload);
                    } else {
                        $updateMe.html(data.payload);
                    }

                    if (feedbackRemove) {
                        removeFeedback($updateMe,feedbackRemove);
                    }
                }
                if (onCompleteFunc) {
                    onCompleteFunc(data);
                }
            } catch (e) {
                console && console.error('ajaxUpdate failure: ' + e);
                displayErrorInResponse(e);
                cleanUpOnFail();
            }
        };

        $.ajax( {
            type: "post",
            url: url,
            data: params,
            dataType: "json",
            traditional: true, // serialize array params as rev=1&rev=2 rather than rev[]=1&rev[]=2
            success: onSuccess,
            complete: onComplete,
            error: noDialog ? ($.isFunction(noDialog) ? noDialog : NOOP) : ajaxFailure
        } );
        return false;
    };

    var NOOP = function () {};

    var ajaxFailure = function (req, textStatus, errorThrown) {
        try {
            if (!isECMA(req)) {
                displayUnexpectedResponse(req);
                return;
            }
            var resp = eval('(' + req.responseText + ')');
            var errorShown = false;
            if (!resp.worked || resp.errorMsg) {
                appendErrorResponse(resp.errorMsg);
                errorShown = true;
            }
            if (errorThrown) {
                appendErrorResponse(errorThrown);
                errorShown = true;
            }
            if (!errorShown) {
                appendErrorResponse("Unknown error");
            }
            showErrorBox();
        } catch (e) {
            console && console.error('ajaxFailure: ' + e);
            displayErrorInResponse(e);
        }
    };

    /******************************************************************
     ******************** HELPER METHODS ******************************
     ******************************************************************/

    var isECMA = function (req) {
        return isContentType(req, /^application\/(ecmascript|json)/);
    };

    var isHTML = function (req) {
        return isContentType(req, /^text\/html/);
    };

    var isContentType = function (req, targetType) {
        var respContentType = req.getResponseHeader("Content-Type");
        return respContentType != null && respContentType.match(targetType);
    };

    var isHTMLFrag = function (req) {
        var responseText = req.responseText;
        return responseText != null && !responseText.match(/<body/i);
    };

    var ourHtmlEscape = function (input) {
        try {
            return input.replace(/&/g, '&amp;').
                    replace(/>/g, '&gt;').
                    replace(/</g, '&lt;').
                    replace(/"/g, '&quot;');
        } catch(e) {
            console && console.error('escape failure: ' + e);
            return input.message || e.message;
        }
    };

    AJS.FECRU.AJAX.startSpin = function (id, className, replace) {
        className = className ? className + " spinner" : "spinner";

        var $el = typeof(id) == 'object' ? $(id) : $("#"+id);
        if ($el.length === 1) {
            var $spinner = $(document.createElement("span"));
            $spinner.addClass(className)
                    .html("<img src='" + fishEyePageContext + "/" + fishEyeSTATICDIR + "/2static/images/blank.gif'>");
            if (replace) {
                $el.replaceWith($spinner);
            } else {
                $spinner.insertAfter($el);
            }
            return true;
        }
        return false;
    };

    AJS.FECRU.AJAX.stopSpin = function (id) {
        var $el = typeof(id) == 'object' ? $(id) : $("#"+id);

        if ($el.length === 1) {
            $el.nextAll(".spinner").remove();
        }
    };

    /******************************************************************
     ******************** ERROR HANDLING ******************************
     ******************************************************************/

    AJS.FECRU.AJAX.checkError = function (resp) {
        return resp.error;
    };

    var appendErrorResponse = function (error, noEscape) {
        if (error) {
            appendErrorMessage(error + (error.stack ? ":\n" + error.stack : ""), noEscape);
        }
    };

    var appendCredentialsRequests = function (credentialsRequired) {
        if (credentialsRequired) {
            for (var i=0;i<credentialsRequired.length;i++) {
                var creds = credentialsRequired[i];
                var msg = "Click <a href='" + creds.authUrl + "' target='_new'>here</a> to authenticate with " + creds.name;
                appendErrorMessage(msg, true);
            }
        }
    };

    AJS.FECRU.AJAX.appendErrorResponse = appendErrorResponse;

    var appendErrorMessage = function (errorMsg, noEscape) {
        if (errorMsg) {
            var $err = $('#errorResponses'),
                    errorCount = $err.find('.errorResponse').length,
                    escaped = (noEscape) ? errorMsg : ourHtmlEscape(errorMsg)
            ;

            $err.show()
                .append($('<div class="errorResponse-count">Error' + (errorCount ? (' ' + ++errorCount) : '') + '</div>'))
                .append($('<div class="errorResponse"></div>').html( escaped ))
            ;
        }
    };

    AJS.FECRU.AJAX.appendErrorMessage = appendErrorMessage;

    var dialog = 0;

    var showErrorBox = function () {
        showNotificationBox('An error has occurred');
        AJS.$('#errorDetails').show();
    };

    AJS.FECRU.AJAX.showErrorBox = showErrorBox;
    AJS.FECRU.AJAX.showNotificationBox = showNotificationBox;

    var showUserErrorBox = function () {
        showNotificationBox('Cannot perform requested action');
        $('#errorDetails').show();
    };

    var showNotificationBox = function (title) {
        var util = AJS.CRU.UTIL;

        if (dialog == 0) {
            dialog = AJS.FECRU.DIALOG.create(440, 330, "error-dialog");
            dialog.addHeader(title)
                .addPanel("Information", $('#errorBox'))
                .addButton("Reload", function() { window.location.reload(); })
                .addButton("Close", function() {
                    $('#errorResponses .errorResponse').remove();
                    $('#errorResponses .errorResponse-count').remove();
                    dialog.hide();
                }, "close-button");
        }

        if (!util.isAnyDialogShowing()) {
            if (util.isAjaxDialogSpinning()) {
                util.stopAjaxDialogSpin();
            }
            dialog.show();
        }
        return false;
    };

    var displayUnexpectedResponse = function (req) {
        if (isHTML(req) && isHTMLFrag(req)) {
            appendErrorResponse(req.responseText);
            showErrorBox();
        }
        else if (!req.responseText) {
            //no-op here: no responseText means most likely server is gone offline.
        }
        else {
            var resp = eval('(' + req.responseText + ')');

            if (!resp.errorMessages[0]) {
                resp.errorDetails = "No further details were provided.";
            }
            else {
                resp.errorDetails = resp.errorMessages.join("<br>");
            }

            var aOrAn = resp.errorName && "aeiou".indexOf(resp.errorName.charAt(0));

            var currentError = [
                    (resp.errorName ? ((aOrAn) ? "A " : "An ") + resp.errorName : "An error"),
                    " was encountered<br><br>",
                    resp.errorDetails
                ].join(""),
                noEscape = true
            ;

            appendErrorResponse(currentError, noEscape);
            showErrorBox();
        }
    };

    var displayErrorInResponse = function (err) {
        appendErrorResponse(err);
        showErrorBox();
    };
})(AJS.$);
;
/* END /2static/script/fecru/ajax.js */
/* START /2static/script/fecru/browse.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.BROWSE = {};

(function() {
    /**
     * Open a folder, given the node of its span, and then run a post function. Opening the node may mean loading a
     * subtree via Ajax -- if this happens, the post function will be run after the subtree has loaded.
     *
     * @param $node -- a jQuery wrapper around the span tag containing the folder name (which is a sibling of the ul tag containing its children)
     * @param postFn -- a function of no arguments to run after opening the node
     * @param actionName -- an optional string naming the action to call to load a subtree
     * @param ajaxArgsFn -- an optional function to call to provide extra parameters to the subtree load fn
     */
    var toggleFolder = function($node, postFn, pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        if ($node.hasClass("unfilled")) {
            $node.removeClass("unfilled");
            var liId = $node.closest("li.tree-li").attr('id');
            var treeData = AJS.$("#tree-root").data("extraAttrs")[liId];
            var ajaxParameters = {
                path: treeData.path,
                repName: treeData.repname,
                baseUrl: treeData.baseurl,
                noFiles: treeData.nofiles
            };
            var selectedPath = AJS.$("#selectedDirTreeNode").children("input[name='selectedPath']").val();
            if (selectedPath) {
                ajaxParameters.selectedPath = selectedPath;
            }
            if (ajaxArgsFn) {
                var args = ajaxArgsFn($node);
                for (k in args) {
                    ajaxParameters[k] = args[k];
                }
            }
            $node.after(AJS.$("<span class='dirlistSpinner'>&nbsp;</span>").show());
            $node.removeClass("closed").addClass("open");
            var params = AJS.$("#queryStrSuffix").val();
            if (params) {
                ajaxParameters.queryStrSuffix = params;
            }

            if (!actionName) {
                actionName = "loadSubTree";
            }
            if (AJS.$('#includeBranchHeadParam').length) {
                ajaxParameters["includeBranchHeadParam"] = true;
            }
            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/fe/" + actionName + ".do", ajaxParameters, function(resp) {
                if (resp.worked) {
                    // use 'clean' to avoid the regex that jQuery uses to distinguish HTML from ids
                    var replacement = AJS.$.clean([resp.payload], document);
                    $node.parent().replaceWith(replacement);
                    postFn();
                }
            }, false);
        } else {
            var oldClass = "closed";
            var newClass = "open";
            if ($node.hasClass("open")) {
                oldClass = "open";
                newClass = "closed";
            }
            $node.removeClass(oldClass).addClass(newClass);
            $node.siblings("ul." + oldClass).removeClass(oldClass).addClass(newClass);
            postFn();
        }
    };

    var moveSelectedDirTreeIds = function($newLink) {
        AJS.$("#selectedDirTreeNode").attr("id", "");
        AJS.$("#selectedDirTreeLink").attr("id", "");
        $newLink.closest("span").attr("id", "selectedDirTreeNode");
        $newLink.attr("id", "selectedDirTreeLink");
    };

    /**
     * Open the parents of this link and set the selected ids
     * @param $linkNode the link in the directory tree which is selected.
     */
    AJS.FECRU.BROWSE.selectLink = function($linkNode, folderToggledFn, pathLinkFn) {
        var $span = $linkNode.parent();
        var done = function() {
            folderToggledFn($linkNode);
        };
        if ($span.hasClass("closed")) {
            AJS.FECRU.AJAX.startSpin(AJS.$("#filebox"), "", true); // Get's stopped when #fileResults has html() called
            toggleFolder($span, done, pathLinkFn);
        } else {
            done();
        }
        moveSelectedDirTreeIds($linkNode);
    };

    AJS.FECRU.BROWSE.setupDirectoryTree = function(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        AJS.$(document).delegate("span.tree", "click", function(event) {
            if (AJS.$(event.target).is("a")) {
                return true; // let the link do its job
            }
            toggleFolder(AJS.$(this), function() {}, pathLinkFn, fileLinkFn, actionName, ajaxArgsFn);
            event.stopPropagation();
            return false;
        });
        if (pathLinkFn) {
            AJS.$(document).delegate("#navigation-tree a.pathLink", "click", function(event) {
                return pathLinkFn(event);
            });
        }
        if (fileLinkFn) {
            AJS.$(document).delegate("#navigation-tree a.fileLink", "click", function(event) {
                return fileLinkFn(event);
            });
        }
    };

    AJS.FECRU.BROWSE.initDirectoryTree = function(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn) {
        AJS.FECRU.BROWSE.setupDirectoryTree(pathLinkFn, fileLinkFn, actionName, ajaxArgsFn);
        if (AJS.FE) {
            AJS.FE.setupPanes();
        }
    };
})();
;
/* END /2static/script/fecru/browse.js */
/* START /2static/script/fecru/dialog.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.DIALOG = {};

(function () {
    var safeDimensions = function (maxWidth, maxHeight, margin) {
        margin = margin || 50;
        return {
            height: Math.min(AJS.$(window).height() - margin, maxHeight),
            width: Math.min(AJS.$(window).width() - margin, maxWidth)
        };
    };

    AJS.FECRU.DIALOG.create = function (maxWidth, maxHeight, id) {
        var dimensions = safeDimensions(maxWidth, maxHeight);
        var dialog = new AJS.Dialog(dimensions.width, dimensions.height, id);
        // Add the height and width of the dialog as properties of the object
        AJS.$.extend(dialog, {width: dimensions.width, height: dimensions.height});
        return dialog;
    };

    AJS.FECRU.DIALOG.triggerAjaxDialogLoaded = function () {
        AJS.$(document).trigger('ajax-dialog-loaded');
    };
})();
;
/* END /2static/script/fecru/dialog.js */
/* START /2static/script/fecru/gwtutil.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}

AJS.FECRU.GWT = {
    /**
     * Scroll to the element identified by selector
     * in the current window.
     * 
     * @param selector selector to get the element
     */
    scrollToId: function(selector) {
        AJS.$(window).scrollTo(AJS.$(selector));
    },
    /**
     * Scroll to the element identified by selector, scrolling
     * the element selected by container.
     *
     * @param selector selector to get the element
     * @param container scrollable container
     */
    scrollToIdInContainer: function(selector, container) {
        AJS.$(container).scrollTo(AJS.$(selector));
    }
};
;
/* END /2static/script/fecru/gwtutil.js */
/* START /2static/script/fecru/hover.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.HOVER = (function() {
    var opts = {
        onHover: true,
        fadeTime: 200,
        hideDelay: 500,
        showDelay: 220,
        width: 300,
        offsetX: -4,
        offsetY: 10,
        container: "body",
        cacheContent:false,
        useLiveEvents : true,
        initCallback: function() {
            this.popup.refresh();
        }
    };
    var respCache = {};
    var CACHE_FOREVER = -1;
    var displayHandlerDefaultOpts = {
        createUrl:function() {
            throw new Error("must provide createUrl");
        },
        createCacheKey:function() {
            throw new Error("must provide createCacheKey");

        },
        createParams:function() {
            throw new Error("must provide createParams");
        },
        minutesToCache:CACHE_FOREVER,
        cacheType:"__all",
        createTemplateContent:function() {
            return "";
        }
    };
    /**
     * createDisplayHandler returns a function that handles the display of the hover popup, and is used as the display handler
     * to be passed into AJS.InlineDialog as the url parameter (which is overloaded to take a function like this one).
     * @param params The named parameter needs the following properties:
     * @property createUrl : a function that accepts a jquery elem of the trigger of the hover, and returns the url to get
     * the content of the hover.
     * @property createCacheKey : a function that accepts a jquery elem of the trigger, and returns a unique string key for
     * this hover. Used to cache the result of the call created by createUrl
     * @property createParams : a function that accepts a key (created from createCacheKey) and a jquery elem of the trigger,
     * and returns a hash of the parameters to be passed along in the ajax request to the url created by creatUrl.
     * @optionalproperty minutesToCache : number of minutes to cache the result of the call to the url created by createUrl.
     * Defaults to CACHE_FOREVER
     * @optionalproperty cachetype : a unique string that identifies the type of hover being shown. Used by invalidateCache() to
     * clear the cache of this type. can be null.
     * @optionalproperty createExtraParams : a function that accepts the trigger and returns a map of parameters to be passed to the server
     * @optionalproperty createTemplateContent : a function that accepts a key, and optionally the trigger and returns a string to be
     * placed in the spinner template.
     */
    var createDisplayHandler = function(params) {
        var options = AJS.$.extend({}, displayHandlerDefaultOpts, params);
        var minutesToCache = options.createUrl,
                cachetype = options.minutesToCache,
                createTemplateContent = options.createTemplateContent;

        return function ($contentDiv, mouseOverTrigger, showPopup) {
            var $trigger = AJS.$(mouseOverTrigger);
            var url = options.createUrl($trigger);
            var key = options.createCacheKey($trigger);
            var cache = respCache[cachetype] || {};
            var cacheHit = cache[key] || {isHit:false};
            var lastFetchTime = cacheHit.lastFetchTime || 0;
            var cacheTimedOut = (minutesToCache !== CACHE_FOREVER) &&
                                ((new Date().getTime() - lastFetchTime) > (minutesToCache * 1000 * 60));
            if (key) {
                $contentDiv.data("targetToDisplay", {key:key});
                var error = function(xmlHttpRequest, textStatus, errorThrown) {
                    $contentDiv.html(escape(textStatus) + "<br>" + escape(errorThrown ? errorThrown : ""));
                };
                var delayedShowSpinner;
                var done = function(resp) {
                    if (resp.worked) {
                        if (delayedShowSpinner) {
                            clearTimeout(delayedShowSpinner);
                        }
                        if ($contentDiv.data("targetToDisplay").key == key) {
                            $contentDiv.html(resp.html);
                            // show the popup if the spinner wasn't shown (either canceled or not supplied)
                            if (!createTemplateContent || delayedShowSpinner) {
                                showPopup();
                            }
                        }
                        // save result to cache
                        cacheHit.resp = resp;
                        cacheHit.lastFetchTime = new Date().getTime();
                        cacheHit.isHit = true;
                        cache[key] = cacheHit;
                        respCache[cachetype] = cache;
                    } else {
                        $contentDiv.html('<div class="hoverpopup">' + resp.errorMsg + '</div>');
                    }
                };
                if ((!cacheHit.isHit) || cacheTimedOut) {
                    AJS.$.ajax({url: url,
                        data:options.createParams(key, $trigger),
                        type:"GET",
                        dataType:"json",
                        success: done,
                        error: error
                    });
                    // show the spinner if the template is provided, delaying it for a bit
                    if (createTemplateContent) {
                        delayedShowSpinner = setTimeout(function () {
                            if ($contentDiv.data("targetToDisplay").key == key) {
                                $contentDiv.html(getSpinnerTemplate(createTemplateContent(key, $trigger)));
                                showPopup();
                            }
                        }, 150);
                    }
                } else {
                    $contentDiv.html(cacheHit.resp.html);
                    showPopup();
                }
            }
        };
    };

    var invalidateCache = function(type, key) {
        if (!key) {
            respCache[type] = undefined;
        } else if (respCache[type]) {
            respCache[type][key] = undefined;
        }
    };

    var addAllLinkPopups = function() {
        addJiraLinkPopups();
        addCruLinkPopups();
        addCsLinkPopups();
        addUserLinkPopups();
        addDeletedUserLinkPopups();
    };

    var addJiraLinkPopups = function() {
        var jiraShowDelay = 300;
        addLinkPopups({
            linkSpanClass:"jiralinkspan",
            url:fishEyePageContext + '/json/action/issue-tooltip.do',
            showDelay:jiraShowDelay
        });
    };

    var addCruLinkPopups = function() {
        addLinkPopups({
            linkSpanClass:"crulinkspan",
            url:fishEyePageContext + '/json/cru/tooltipdata',
            keyParser: function($trigger) {
                var $permaId = $trigger.find('input.permaId');
                if ($permaId.length == 1) {
                    return $permaId.val();
                }
                return $trigger.find('a').text();
            }
        });
    };

    var addCsLinkPopups = function() {
        var changesetKeyPattern = /^(\/\/([^/]+)\/)(.+)$/;
        addLinkPopups({
            linkSpanClass: "cslinkspan",
            url: fishEyePageContext + '/json/action/cstooltipdata.do',
            keyParser: function ($trigger) {
                return '//' + $trigger.find('input.repname').val() + '/' + $trigger.find('input.csid').val();
            },
            createTemplateContent: function(key) {
                var matches = changesetKeyPattern.exec(key);
                if (matches) {
                    var changesetId = matches[3];
                    var repository = matches[2];
                    //abbrev. the id if too long
                    changesetId = changesetId.length > 10 ? changesetId.substring(0, 6) + "..." : changesetId;
                    return "changest " + changesetId + " in " + repository;
                } else {
                    return "changeset";
                }
            },
            createExtraParams: function ($trigger) {
                return {
                    "repname": $trigger.find('input.repname').val(),
                    "csid": $trigger.find('input.csid').val()
                };
            }
        });
    };

    var addLinkPopups = function(params) {
        var defaults = {
            //the css class to bind the hover trigger to
            linkSpanClass:'',
            //the url to request the content of the trigger
            url:'',
            //how much to delay showing of the hover when triggered
            showDelay: opts.showDelay,
            //given the trigger jquery elem, return a unique string usable as a key for the cache
            keyParser:function ($trigger) {
                return $trigger.text();
            },
            //given the key and trigger, return something to display in the spinner message. can be an empty string.
            createTemplateContent : function(key, $trigger) {
                return key;
            }
        };
        var options = AJS.$.extend({}, defaults, params);
        var linkSpanClass = options.linkSpanClass;
        var displayHandler = createDisplayHandler({
            createUrl:function() {
                return options.url;
            },
            createCacheKey:function($trigger) {
                var $keylink = AJS.$("a", $trigger);
                var key = options.keyParser($keylink);
                key = AJS.$.trim(key);
                return key;
            },
            createParams:function(key, $trigger) {
                var extraParams = (options.createExtraParams && options.createExtraParams($trigger)) || {};
                return AJS.$.extend(false, extraParams, {key:key});
            },
            cacheType:linkSpanClass + "cache",
            createTemplateContent:options.createTemplateContent
        });
        var hoverOpts = AJS.$.extend(false, opts, {showDelay: options.showDelay});
        AJS.InlineDialog("." + linkSpanClass, linkSpanClass + "-popup", displayHandler, hoverOpts);
    };

    var addUserLinkPopups = function() {
        var getHref = function($trigger) {
            return $trigger.attr('href') || $trigger.parent().attr('href'); //sometimes the trigger is the child
        };
        var displayHandler = createDisplayHandler({
            createUrl: function($trigger) {
                return getHref($trigger);
            },
            createCacheKey:function($trigger) {
                return getHref($trigger);
            },
            createParams:function() {
                return {ajax :"true"};
            },
            cacheType:'userlinks',
            createTemplateContent:function(key, $trigger) {
                var $linkText = $trigger.hasClass('linkText') ? $trigger : $trigger.find(".linkText");
                if ($linkText.length === 0) {
                    //if the linkText doesnt exist, which does happen for avartars with no displayed name
                    return key.replace(/^.*\//, "");//remove all but the username
                } else {
                    return $linkText.text();
                }
            }
        });
        AJS.InlineDialog("a.userorcommitter,a.userorcommitter-parent .linkText", "user-hover-inline-dialog", displayHandler, opts);
    };

    var addDeletedUserLinkPopups = function() {
        var hoverContent = function($contents, trigger, showPopup) {
            var $trigger = AJS.$(trigger);
            $contents.html(
                "<div class='user-hover-info'>"+
                    "<div class='user-hover-avatar'>"+
                        "<img height='48' width='48' alt='Deleted User' src='" + fishEyePageContext + "/avatar/deleted' />"+
                    "</div>"+
                    "<div class='user-hover-details'>"+
                    "<h4><span class='linkText'></span></h4>"+
                    "<em>(deleted user)</em>"+
                "</div>"
            );
            var usernameSpan = $trigger.find(".hidden-username");
            if (usernameSpan) {
                $contents.find("span.linkText").text(usernameSpan.text());
            }
            showPopup();
        };
        AJS.InlineDialog("a.deleteduser,a.deleteduser-parent .linkText", "deleted-user-hover-inline-dialog", hoverContent, opts);
    };

    var getSpinnerTemplate = function(content) {
        // Don't add the content directly into the spinner, as it may contain unescaped HTML content.
        // The workaround is to create a jQuery object and call .text(), and then return the html of the constructed object.
        var $elem = AJS.$('<div class="hoverpopup-throb jirahoverpopup-throb">' +
                '<img src="' + fishEyePageContext + '/' + fishEyeSTATICDIR +
                '/2static/images/spinner_003366.gif" alt="Retrieving details">' +
                '<em class="hoverpopup-throbber-text">Retrieving ' +
                '<span class="hoverpopup-throbber-text-content">' +
                '</span>...</em></div>');
        $elem.find(".hoverpopup-throbber-text-content").text(content);
        return $elem.html();
    };

    return {
        addAllLinkPopups: addAllLinkPopups,
        invalidateCache : invalidateCache,
        CACHE_FOREVER: CACHE_FOREVER
    };
})();
;
/* END /2static/script/fecru/hover.js */
/* START /2static/script/fecru/profile.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
if (!AJS.FECRU.PROFILE) {
    AJS.FECRU.PROFILE = (function() {
        var makeDialogFor = function($profileSettingsLink) {
            var windowWidth = AJS.$(window).width();
            var windowHeight = AJS.$(window).height();
            var width = (windowWidth < 1000) ? windowWidth - 120 : 800;
            var height = (windowHeight < 700) ? windowHeight - 100 : 700;

            var HEADER_HEIGHT = 43; //height of the dialog header, in px
            var BUTTON_HEIGHT = 44; //height of the bottons at bottom of dialog, in px.
            var BODY_PADDING = 20; // needs to be the same as .aui-dialog .dialog-panel-body declaration in dialog.css
            var iframeHeight = height - HEADER_HEIGHT - BUTTON_HEIGHT - BODY_PADDING;
            var settingsDialog = AJS.FECRU.DIALOG.create(width, height, 'fecru-profile-settings-dialog');

            var deepProfileSettingsLink = $profileSettingsLink.attr("href") || fishEyePageContext + "/profile";

            // hack: we're adding a random number to the iframe id to work
            // around webkit bug: 24078 (http://lists.macosforge.org/pipermail/webkit-unassigned/2009-February/100941.html)
            var $iframe = AJS.$("<iframe id='fecru-iframe-" + (Math.ceil(Math.random() * 1000)) + "' frameborder='0' src='" + deepProfileSettingsLink + "' style='width:100%;height:" + (iframeHeight) + "px' ></iframe>");

            settingsDialog.addHeader("Settings");
            settingsDialog.addPanel("Display", $iframe);
            settingsDialog.addButton("Close", function (dialog) {
                dialog.hide();

                //todo: fix up reloading?
                if (getDialogURL()) {
                    // Remove "dialog" parameter before reloading
                    var topURL = window.location.href;
                    topURL = topURL.replace(/\?dialog=[^&]*/, "?");
                    topURL = topURL.replace(/&dialog=[^&]*/, "");
                    topURL = topURL.replace(/\?$/, "");
                    window.location.replace(topURL);
                } else {
                    window.location.reload();
                }
            });
            return settingsDialog;
        };

        AJS.$(document).ready(function () {

            var toggleMappingSubmitButton = function() {
                var disable = (AJS.$("#repositoryDropdown").attr("selectedIndex") == 0);
                AJS.$("#addMappingButton").attr("disabled", disable);
            };
            AJS.$("#repositoryDropdown").change(toggleMappingSubmitButton);
            toggleMappingSubmitButton();    // set initial state

            var settingsDialog;
            //todo may be use live events?
            var $profileSettingsLink = AJS.$("a.dialog-settings").click(function(e) {
                e.preventDefault();
                if (!settingsDialog) {
                    settingsDialog = makeDialogFor($profileSettingsLink);
                }
                settingsDialog.show();
            });

            var $form = AJS.$('form.autosubmit');
            $form.find('input,select').change(function () {
                var params = $form.serialize();
                var action = $form.attr('action');
                var $spinner = $form.find('.edit-settings-spinner').show();
                var saved = function () {
                    setTimeout(function() {
                        $spinner.hide();
                    }, 500); //show the spinner for slightly longer, so that the feedback is visible for longer
                };

                AJS.FECRU.AJAX.ajaxDo(action, params, saved);
            });

            $form.find('#clear-ignored-applinks').click(function () {
                var $spinner = AJS.$(".clear-ignored-applinks-spinner").show();
                AJS.FECRU.UAL.clearIgnoredAppLinks(function() {
                    setTimeout(function() {
                        $spinner.hide();
                    }, 500); //show the spinner for slightly longer, so that the feedback is visible for longer
                });
            });

            // If there's a dialog argument on our URL, open that dialog.
            var dialogURL = getDialogURL();
            if (dialogURL) {
                settingsDialog = makeDialogFor(AJS.$("<a href='" + fishEyePageContext + dialogURL + "' />"));
                settingsDialog.show();
            }

        });

        function getDialogURL() {
            // Don't match URLs which are login redirects
            if (location.search.match(/origUrl/)) {
                return null;
            }
            var matches = /[?&]dialog=([^&]*)/.exec(location.search);
            if (matches) {
                return decodeURIComponent(matches[1]);
            }
            return null;
        }

        return true; //flag to stop multiple calls which adds multiple dialog boxes.
    })();
}
;
/* END /2static/script/fecru/profile.js */
/* START /2static/script/fecru/rss.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}

AJS.FECRU.RSS = (function() {
    var makeDialogFor = function($Link) {
        var deepRSSLink = $Link.attr("href");
        var dlg = AJS.FECRU.DIALOG.create(800, 500, 'rss-settings-dialog');

        var $iframe = AJS.$("<iframe id='fecru-iframe' frameborder='0' src='" + deepRSSLink + "' style='width:100%;height:"+dlg.height+"px'></iframe>");

        dlg.addHeader("Notification Configuration")
           .addPanel("Display", $iframe)
           .addButton("Close", function (dialog) {
                dialog.hide();
                $iframe.attr("src", deepRSSLink);
            });
        return dlg;
    };
    var setupRSSDialog = function () {
        var rssDialog;
        var $rssLink = AJS.$("#dialog-rss").click(function (e) {
            e.preventDefault();
            if (!rssDialog) {
                rssDialog = makeDialogFor($rssLink);
            }
            rssDialog.show();
        });
    };

    AJS.toInit(function() {
        var $context = AJS.$("#rss-form");
        AJS.$("input, select", $context).change(function() {
            $context.submit();
        });
    });

    return {
        setupRSSDialog : setupRSSDialog
    };

})();
;
/* END /2static/script/fecru/rss.js */
/* START /2static/script/fecru/ui.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
    AJS.FECRU.UI = {};
} else if (!AJS.FECRU.UI) {
    AJS.FECRU.UI = {};
}

(function() {

    var hasSetDropdowns = false;

    AJS.FECRU.UI.setDropdowns = function (useroptions) {
        if (hasSetDropdowns) {
            AJS.log("WARNING: About to set duplicated dropdown live events");
        } else {
            hasSetDropdowns = true;
        }
        var options = {
            selectionHandler: function () {
                // Prevent AUI from reimplementing default browser handling of <a> elements.
                // May need to revisit depending on AUI changes (see CR-FE-1617).
            },
            useLiveEvents : true, //using live meant that hovers will no longer need to perform a dropdown bind on load
            trigger: ".aui-dd-link" // so we dont get AUI's screwed up css
        };
        if (useroptions) {
            AJS.$.extend(options, useroptions);
        }
        AJS.dropDown.Standard(options);
    };

    var setupFilterInlineDialog = function(trigger, box, unique, offsetX) {
        var PADDING = 8;
        var $filterBox = AJS.$(box);
        if ($filterBox.length) {
            var options = {
                onHover: false,
                showArrow: true,
                fadeTime: 200,
                hideDelay: null,
                showDelay: 200,
                width: $filterBox.width() + PADDING,
                offsetX: offsetX || 0,
                offsetY: PADDING,
                container: "body",
                cacheContent:true,
                ignoredElements:['.ui-datepicker'] //dont want to hide the filter box when selecting dates
            };
            var contentHandler = function ($contentDiv, mouseOverTrigger, showPopup) {
                $filterBox.appendTo($contentDiv).show();
                showPopup();
            };
            return AJS.InlineDialog(trigger, unique, contentHandler, options);
        }
        return null;
    };

    AJS.FECRU.UI.filterToggle = function(filters) {
        return setupFilterInlineDialog(".filter-toggle", filters, "filter-inline-dialog", 6);
    };

    var linkAugment = function () {
        AJS.$("a[rel='help']").attr("target", "_help");

        // details switch in stream
        AJS.$(".details-switch").unbind().click(function () {
            AJS.$(this).toggleClass("details-expanded");

            var $verbose = AJS.$(this).siblings(".details-verbose");
            if ( $verbose.css("display") == "none" ) {//toggle() is broken in IE8, so checking display
                $verbose.show();
            }
            else {
                $verbose.hide();
            }
        });

        // look for all jira inline activity items, and load the content ajax, chained one by one.
        var $jiraActivities = AJS.$("div.details-body div.jira-issue-key");
        // cache the ajax results to prevent multiple ajax calls for the same key
        var cache = {};
        var ajaxLoadJiraActivity = function($target, next) {
            var issueKey = AJS.$("input.jira-issue-key", $target[0]).attr("value");
            if (issueKey) {
                //                AJS.log("issueKey="+issueKey);
                var cacheHit = cache[issueKey];
                if (cacheHit) {
                    $target.html(cacheHit.resp.html);
                    if (next) {
                        next();
                    }
                } else {
                    // this doesn't use AJS.FECRU.ajaxDo because we don't want the standard error handling
                    AJS.$.ajax({
                        url: fishEyePageContext + '/json/action/issue-inline.do',
                        data: {'key':issueKey},
                        type: "GET",
                        dataType: "json",
                        success: function(resp) {
                            $target.html(resp.html);
                            if (next) {
                                next();
                            }
                            //cache using the issueKey - prevent multiple calls for the same keys
                            cache[issueKey] = {resp:resp};
                        },
                        error: function(resp) {
                            $target.html("Error retrieving issue: " + issueKey);
                        }
                    });
                }
            }
        };
        var nextInChain = function(i, max) {
            return function() {
                if (i < max) {
                    ajaxLoadJiraActivity(AJS.$($jiraActivities[i]), nextInChain(i + 1, max));
                }
            };
        };
        var max = $jiraActivities.length;
        var i = 0;
        if (max > 0) {
            ajaxLoadJiraActivity(AJS.$($jiraActivities[i]), nextInChain(i + 1, max));
        }

        // inline hover in stream
        AJS.$(".hover").mouseover(function (event) {
            AJS.$("#hover").addClass("mouseover");
            inlineHover(AJS.$(this), event);
        });

        AJS.$("#hover").mouseout(function() {
            var which = AJS.$(this);
            which.bind("mouseleave", function() {
                AJS.$(which)
                        .removeClass("mouseover")
                        .hide();
            });
        });
        AJS.$("hover-close").click(function() {
            AJS.$(this).hide();
        });

        contentToggle();
    };

    var formAugment = function (which) {
        if (AJS.$.browser.safari) {//overides type=search
            var searchBox = AJS.$("#quick-search-input");
            if (searchBox.length === 1) {
                searchBox[0].type = "text";
            }
        }
    };

    var $focusedTableRow;
    AJS.FECRU.UI.tableRowClick = function (prefix, rowClickFn) {
        AJS.$(document).delegate("#" + prefix + "-table > tbody > tr", "click", function () {
            var clearFocus = function() {
                if ($focusedTableRow) {
                    $focusedTableRow.removeClass(prefix + "-focus");
                }
            };

            var $this = AJS.$(this);
            if ($this.hasClass(prefix + "-focus")) {
                clearFocus();
                $focusedTableRow = null;
            } else {
                clearFocus();
                $this.addClass(prefix + "-focus");
                $focusedTableRow = $this;
                if (rowClickFn) {
                    rowClickFn(this);
                }
            }
        });
    };

    AJS.FECRU.UI.tableSort = function (prefix, extractionFn) {
        var params = {};
        if (extractionFn) {
            params['textExtraction'] = extractionFn;
        }
        AJS.$("#" + prefix + "-table").tablesorter(params);
    };

    AJS.FECRU.UI.accordion = function () {
        AJS.$(document).delegate('.sidebar-collapse', "click", function() {
            AJS.$('#content').toggleClass('collapsed-sidebar');
            return false;
        }).delegate(".accordion-head", "click", function() {
            var $next = AJS.$(this).next();
            var $parent = AJS.$(this).parent();
            if ($next.is(":hidden")) {
                $parent.addClass("active");
                if (!AJS.$.browser.msie) {//fork required as IE doesn't play nicely with jQuery slideDown animation
                    $next.slideDown("fast");
                }
                else {
                    $next.show();
                }
            }
            else {
                if (!AJS.$.browser.msie) {
                    $next.slideUp("fast", function () {
                        $parent.removeClass("active");
                    });
                }
                else {
                    $next.hide();
                    $parent.removeClass("active");
                }
            }
            return false;
        }).delegate("#accordion-toggle", "click", function() {
            var $accordionToggle = AJS.$(this);
            var $accordionContent = $accordionToggle.find(".accordion-content");
            var $parent = AJS.$(this).parent();
            var isExpanded = $accordionToggle.html() === 'expand';
            if (isExpanded) {
                $parent.addClass("active");
                if (!AJS.$.browser.msie) {//fork required as IE doesn't play nicely with jQuery slideDown animation
                    $accordionContent.slideDown("fast");
                } else {
                    $accordionContent.show();
                }
            } else {
                 if (!AJS.$.browser.msie) {
                    $accordionContent.slideUp("fast", function () {
                        $parent.removeClass("active");
                    });
                } else {
                    $accordionContent.hide();
                    $parent.removeClass("active");
                }
            }
            $accordionToggle.html(isExpanded ? 'expand' : 'collapse'); // reverse (it's now "wasExpanded")
            return false;
        });
    };

    var contentToggle = function () {
        AJS.$(document).delegate("#content-toggle", "click", function() {
            var $contentToggle = AJS.$("#content-toggle");
            if ($contentToggle.html() == 'expand') {
                AJS.$(".details-verbose").show();
                $contentToggle.html("collapse").addClass("expanded");
            }
            else {
                AJS.$(".details-verbose").hide();
                $contentToggle.html("expand").removeClass("expanded");
            }
            return false;
        }).delegate(".show-changeset-revisions:not(.disabled)", "click", function() {
            var $this = AJS.$(this);
            var shouldShow = !$this.hasClass("active");
            var $panel = AJS.$("#panel-target ");
            if (shouldShow) {
                $panel.find(".details-verbose").show();
                $panel.find("div.details-switch").addClass("details-expanded");
            } else {
                $panel.find(".details-verbose").hide();
                $panel.find("div.details-switch").removeClass("details-expanded");
            }
            $this.toggleClass("active");
            AJS.FECRU.PREFS.setPreference("aec", shouldShow ? "Y" : "N");
        });
    };

    var inlineHover = function (which, event) {
        if (!AJS.$("#hover").hasClass("mouseover")) {
            return false;//don't fire if the link is no longer hovered
        }

        var source = which.attr("name").split("-")[0];
        var subject = which.attr("name").split("-")[1];
        var content;
        var offset = which.offset();
        var height = which.height();

        if (source === "user") {
            content = hovers.users[subject];
        }
        else if (source === "item") {
            content = hovers.items[subject];
        }

        AJS.$("#hover-content").html(content);
        AJS.$("#hover").css({
            display:"block",
            top: offset.top + height,
            left: offset.left
        });
    };

    AJS.FECRU.UI.changesetToggle = function (postOpenFn) {
        return function() {
            AJS.$(document).delegate(".stream-delta dt.hasDiff", "click", function (e) {
                var target = e.target.tagName.toLowerCase();
                if (target == "a" || target == "span") {
                    return;
                }

                var $node = AJS.$(this);
                $node.next().toggle();
                $node.toggleClass("open");
                if (postOpenFn) {
                    postOpenFn(AJS.$(this));
                }
            });
        };
    };
    AJS.FECRU.UI.changesetToggleAll = function () {
        AJS.$("#expand-all").click(function() {
            AJS.$(".stream-delta dd").show();
            AJS.$(".delta-toggle").html("collapse all").addClass("expanded");
            AJS.$(".stream-delta dt.hasDiff").addClass("open");
        });
        AJS.$("#collapse-all").click(function() {
            AJS.$(".stream-delta dd").hide();
            AJS.$(".delta-toggle").html("expand all").removeClass("expanded");
            AJS.$(".stream-delta dt.hasDiff").removeClass("open");
        });
    };

    AJS.FECRU.UI.initStream = function () {
        linkAugment();
        formAugment();
    };

    AJS.FECRU.UI.contentPadBottom = function () {
        var contentPadding = AJS.$("#content").css("padding-bottom");
        var messageHeight = AJS.$("#footer-bar .system-message").height();
        var messagePadding = parseInt(contentPadding, 10) + messageHeight;
        AJS.$("#content").css("padding-bottom", messagePadding);
    };

    /**
     * checks if the dates in the elements ".calendar-date-end" and ".calendar-date-start" are in the correct order
     * and swap them around if they are found to be reversed.
     *
     * @param extractDateStringFn a function to extract the date string into the format 'yy-mm-dd' from the format
     * obtained in the value attribute of the input element
     */
    AJS.FECRU.UI.swapDatesIfReversed = function(extractDateStringFn, context) {
        context = context || "body";
        var endDateInput = AJS.$("input.calendar-date-end", context);
        var startDateInput = AJS.$("input.calendar-date-start", context);
        if (endDateInput.length > 0 && startDateInput.length > 0) {
            var startDateStr = startDateInput.val(),
                endDateStr = endDateInput.val(),
                extractedStartDate = extractDateStringFn(startDateStr),
                extractedEndDate = extractDateStringFn(endDateStr),
                startDate,
                endDate,
                errorThrown = false;

            try {
                startDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractedStartDate);
            } catch (e) {
                AJS.FECRU.AJAX.appendErrorMessage("Could not parse start date " + extractedStartDate);
                errorThrown = true;
            }

            try {
                endDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractedEndDate);
            } catch (e) {
                AJS.FECRU.AJAX.appendErrorMessage("Could not parse end date " + extractedEndDate);
                errorThrown = true;
            }

            if (errorThrown) {
                AJS.FECRU.AJAX.showErrorBox();
                return false;
            }

            if (startDateStr && endDateStr && (startDate > endDate)) {
                //do a swap of the date values before submitting if the dates are found to be reversed
                endDateInput.val(startDateStr);
                startDateInput.val(endDateStr);
            }
        }
        return true;
    };

    AJS.FECRU.UI.setupCalendar = function(addTime, constrainInput) {
        if (addTime === undefined || addTime === null) {
            addTime = true;
        }
        if (constrainInput === undefined || constrainInput === null) {
            constrainInput = true;
        }
        var calDateStart = AJS.$("input.calendar-date-start");
        calDateStart.attr("autocomplete", "off");
        calDateStart.datepicker({
            dateFormat: 'yy-mm-dd',
            constrainInput: constrainInput,
            onClose: function(dateText) {
                //only do this if the text is the length of a date
                //this will FAIL if anyone changes the date format

                //NOTE: yy-mm-dd in jquery is a 10 character format
                // ie 1987-12-23
                if (addTime && dateText.length === 10) {
                    AJS.$(this).attr("value", dateText + "T00:00:00");
                }
            }
        });
        var calDateEnd = AJS.$("input.calendar-date-end");
        calDateEnd.attr("autocomplete", "off");
        calDateEnd.datepicker({
            dateFormat: 'yy-mm-dd',
            constrainInput: constrainInput,
            onClose: function(dateText) {
                if (addTime && dateText.length === 10) {
                    AJS.$(this).attr("value", dateText + "T23:59:59");
                }
            }
        });
    };

    AJS.FECRU.UI.warnAboutFirebug = function (onClose) {
        var cookieName = 'hide_fecru_fb_warn';
        var suppressWarning = AJS.$.cookie(cookieName) === 'Y';
        if (!suppressWarning && window.console && window.console.firebug) {
            var product = AJS.$("#product-name").text() || "FishEye + Crucible";
            var $warning = AJS.$("<div id='firebug-warning'><p>Firebug is known to cause performance problems with " +
                                 product + ". Why not disable it?</p><a class='close'>X</a></div>");
            AJS.$(document).delegate("#firebug-warning .close", "click", function () {
                $warning.slideUp('fast', function() {
                    AJS.$.cookie(cookieName, 'Y', { expires: 365 });
                    if (onClose) {
                        onClose();
                    }
                });
            });
            $warning.prependTo(AJS.$("#masthead"));
        }
    };

    AJS.FECRU.UI.toggleSearch = function () {
        AJS.$(document).delegate("h5:[rel='toggle']", "click", function() {
            if (AJS.$(this).hasClass("show")) {
                AJS.$("#search-more").show();
                AJS.$(this).removeClass("show").addClass("hide");
                AJS.$(this).find("em").text("hide");
            }
            else {
                AJS.$("#search-more").hide();
                AJS.$(this).addClass("show").removeClass("hide");
                AJS.$(this).find("em").text("show");
            }
            return false;
        });
    };

    AJS.FECRU.UI.setCompletedResizeTimeout = function (selector, callback, completionDelay) {
        completionDelay = completionDelay || 100;
        var timeout;

        AJS.$(selector).resize(function () {
            if (completionDelay > 0 && timeout) {
                clearTimeout(timeout);
            }
            timeout = setTimeout(callback, completionDelay);
        });
    };
    
    AJS.FECRU.UI.setupRevisionPopups = function (selector) {
        if (!AJS.$.browser.msie) {
            AJS.$(document).delegate(selector + " tr .revision", "mouseover", function () {
                var $trigger = AJS.$(this).find(".revision-popup:first").css("visibility", "visible");
                // Hide any old drop downs if they aren't our own. Moving between rows doesn't close the old dropdown,
                // it only hides it.
                var current = AJS.dropDown.current;
                if (current && current.$.closest(".revision-popup")[0] !== $trigger[0]) {
                    current.hide();
                }
            }).delegate(selector + " tr .revision", "mouseout", function () {
                AJS.$(this).find(".revision-popup")
                        .css("visibility", "hidden");
            });
        } else {
            AJS.$(document).delegate(selector + " tr .revision", "mouseover", function () {
                if (!AJS.dropDown.current) {
                    AJS.$(this).find(".revision-popup:first").css("visibility", "visible");
                }
            }).delegate(selector + " tr .revision", "mouseout", function () {
                if (!AJS.dropDown.current) {
                    AJS.$(this).find(".revision-popup:first").css("visibility", "hidden");
                }
            });
        }
    };
})();
;
/* END /2static/script/fecru/ui.js */
/* START /2static/script/fecru/onReady.js */
//init
AJS.toInit(function(){

    var fecru = AJS.FECRU;
    var ui = fecru.UI;

    ui.initStream();

    ui.setDropdowns();

    fecru.HOVER.addAllLinkPopups();

    fecru.RSS.setupRSSDialog();

    ui.accordion();
});
;
/* END /2static/script/fecru/onReady.js */
/* START /2static/script/fecru/star.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}

(function() {

    /**
     * html for the star edit dialog, to be inserted on demand.
     */
    var DIALOG_DEFAULT = "Describe your favourite";
    var DIALOG_HTML = ["<div id='astronomy'>",
                          "<div id='astronomy-label'>",
                              "<h4><span>Update favourite</span></h4>",
                              "<span class='close'><a href='#' class='close-astronomy' id='close-astronomy'>X</a></span>",
                              "<form action='#'>",
                                  "<fieldset class='input'>",
                                      "<label for='star-labels'>Name</label>",
                                      "<input type='text' id='star-labels' value='Describe your favourite'>",
                                  "</fieldset>",
                                  "<fieldset class='button'>",
                                      "<ul>",
                                          "<li><button class='remove' id='remove-astronomy'>Remove</button></li>",
                                          "<li class='odd'><input type='submit' id='save-star-label' value='Save label'></li>",
                                      "</ul>",
                                  "</fieldset>",
                              "</form>",
                          "</div>",
                      "</div>"
                    ].join("");

    ////// private functions

    var ON_ONLY = true;
    var OFF_ONLY = false;

    function starOnOffClassName(on) {
        return on ? "star-on" : "star-off";
    }

    function starAddRemoveText($star, on) {
        return $star.find(on ? "input.star-textRemove" : "input.star-textAdd").val();
    }

    /**
     * todo: see if this can be swapped with the throbber jquery plugin.
     */
    function makeThrobberControl($link, throbberSetting) {
        /**
         * Starts the throbber, which activates after noLatencyThreshold milliseconds have passed.
         * @return a function that will stop the throbber after minThrobberDisplay milliseconds have passed, ensuring no flicker.
         */
        var startThrob = function () {
            $link.data("throbbing", true);
            var timeout = setTimeout(function () {
                $link.addClass("star-throb");
            }, throbberSetting.noLatencyThreshold);

            return function() {
                $link.data("throbbing", false);
                clearTimeout(timeout);
                setTimeout(function () {
                    $link.removeClass("star-throb");
                }, throbberSetting.minThrobberDisplay);
            };
        };

        /**
         * stores a function to stop the throbber for the given $link
         */
        var stopThrob = undefined;
        return {
            start: function() {
                if (throbberSetting.showThrob && !stopThrob) {
                    stopThrob = startThrob();
                }
            },
            stop: function() {
                if (stopThrob) {
                    stopThrob();
                    stopThrob = undefined;
                }
            },
            isThrobbing: function() {
                return $link.data("throbbing");
            }
        };
    }

    /**
     *
     * @param link the jquery element of the link that was clicked
     * @param dialogControl an object to control the dialog. Its members should be:
     * - showDialog: a function with no arguments, which when invoked will display the dialog
     * - hideDialog: a function with no arguments, which when invoked will hide the dialog
     * - getDialog: a function with no arguments, which returns a jquery object that represents the dialog
     * @param options the list of options. Available options are:
     * - throbber : options for throbbing
     *
     */
    function starClicked(link, dialogControl, options) {
        var starId = getStarId(link);
        var starKeys = {};
        var $link = AJS.$(link);
        var throbberControl = makeThrobberControl($link, options.throbberSetting);
        //if throbbing already, then don't do anything.
        if (throbberControl.isThrobbing()) {
            return;
        }

        $link.children("span.inputs").children("input.starKey").each(function() {
            var key = AJS.$(this);
            starKeys[key.attr("name")] = key.val();
        });
        if (starId != null) {
            if (isDialogShown($link)) {
                // we are unstarring, pop up the edit box
                editStar(starId, dialogControl, throbberControl);
            } else {
                //...or remove directly without anymore interaction
                doRemoveStar(starId, throbberControl);
            }
        } else {
            // we are adding a star
            addStar(starKeys, dialogControl, throbberControl);
        }

        // todo: fix this hack to make hoverpopups refresh after starring
        // just always invalidate all popups
        var fecruHover = AJS.FECRU.HOVER;
        fecruHover.invalidateCache(fecruHover.CACHE_FOREVER);

    }

    function updateAccordian() {
        var $starList = AJS.$("#accordionStarList");
        if ($starList.length > 0) {
            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/profile/starAccordionAjaxBody.do", {},
                    function(resp) {
                        if (resp.worked) {
                            $starList.replaceWith(resp.html);
                        }
                    },
                    false
                    );
        }
        var $numStars = AJS.$("#accordionNumStars");
        if ($numStars.length > 0) {
            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/profile/starAccordionAjaxNumber.do", {},
                    function(resp) {
                        if (resp.worked) {
                            $numStars.replaceWith(resp.html);
                        }
                    },
                    false
                    );
        }
    }

    function getIdElement(link) {
        return AJS.$(link).children("span.inputs").children("input[name='id']");
    }

    function getStarId(link) {
        return getIdElement(link).val();
    }

    function allStars(on) {
        return AJS.$("a." + starOnOffClassName(on));
    }

    function setStarAttributes(on, $node) {
        var newClass = starOnOffClassName(on);
        var oldClass = starOnOffClassName(!on);
        $node.removeClass(oldClass)
             .addClass(newClass)
             .children("span.starText").text(starAddRemoveText($node,on));
    }

    /**
     * Turn on the Star with the given keys
     * @param keys
     */
    function addStar(keys, dialogControl, throbberControl) {
        var paramMap = {};
        for (var key in keys) {
            paramMap["key." + key] = keys[key];
        }
        throbberControl.start();
        doStar(fishEyePageContext + "/json/profile/addStarAjax.do", paramMap, function(newId) {
            allStars(OFF_ONLY).each(function() {
                var $node = AJS.$(this);
                var $inputs = $node.children("span.inputs");
                for (var key in keys) {
                    var input = $inputs.children("input.starKey[name='" + key + "']");
                    if (input.length === 0 || input.val() != keys[key]) {
                        return;
                    }
                }
                setStarAttributes(true, $node);
                $inputs.append("<input type='hidden' name='id' value='" + newId + "'>");
            });
            var itemType = keys["itemType"];
            if (itemType == "atlassian-chart" || itemType == "atlassian-search" ||
                itemType == "atlassian-quicksearch") {
                editStar(newId, dialogControl, throbberControl);
            } else {
                // WARNING: epic hack
                // This is to work around an incorrect usage of InlineDialog. We are leveraging IDs triggers to load
                // star ajax dialog, but in some cases we are not actually showing the dialog. This leaves the internal
                // state of InlineDialog in tatters and further calls to show the dialogs won't work. This workaround
                // simply shows and immediately hides the dialog ensuring the internal state is correct. The hidden
                // visibility is to ensure that the browser doesn't render the dialog on the screen.
                var $dialog = AJS.$("#inline-dialog-star-inline-dialog");
                $dialog.css("visibility", "hidden");
                dialogControl.showDialog();
                dialogControl.hideDialog();
                $dialog.css("visibility", "visible");
            }
            throbberControl.stop();
        });
    }

    /**
     * Pop up a dialog box for a Star which is in the on state, allowing the user to set its label or to remove it.
     * @param starId the id of the Star to change.
     * @param dialogControl
     */
    function editStar(starId, dialogControl, throbberControl) {
        // pop up our star edit dialog
        var onSuccess = function(resp) {
            if (resp.worked) {
                var __updateLabel = function(label) {
                    doSaveLabel(starId, label, throbberControl);
                };
                var __removeStar = function() {
                    doRemoveStar(starId, throbberControl);
                };
                showStarDialog(dialogControl, __updateLabel, __removeStar, resp.payload);
            } else {
                //resp.worked is false, reset the star to an off star.
                turnOffStar(starId);
            }
            throbberControl.stop();
        };
        throbberControl.start();
        // get the label for this Star, to supply it to the dialog
        AJS.$.post(fishEyePageContext + "/json/profile/getStarLabelAjax.do", {id: starId}, onSuccess, 'json');
    }

    function turnOffStar(starId) {
        allStars(ON_ONLY).each(function() {
            var id = getStarId(this);
            if (starId == id) {
                // represents the same star
                getIdElement(this).remove();
                var $node = AJS.$(this);
                setStarAttributes(false, $node);
            }
        });
    }

    /**
     * Send a new label value to the server.
     * @param id the id of the Star who's label has been changed
     * @param label a String holding the new label text
     */
    function doSaveLabel(id, label, throbberControl) {
        var onDone = function() {
            refreshDropDown();
            throbberControl.stop();
        };
        throbberControl.start();
        AJS.$.post(fishEyePageContext + "/json/profile/setStarLabelAjax.do", {id:id, label:label}, onDone, 'json');
    }

    /**
     * Unstar the star with the given id.
     *
     * @param id the id of the Star to unstar.
     */
    function doRemoveStar(id, throbberControl) {
        throbberControl.start();
        doStar(fishEyePageContext + "/json/profile/removeStarAjax.do", {id: id}, function() {
            turnOffStar(id);
            throbberControl.stop();
        });
    }

    /**
     * Change the state of a set of stars, and refresh the 'my stars' dropdown.
     * @param url the URL to inform the server about the change
     * @param updateStars the function to call to change the state of the stars in the browser
     */
    function doStar(url, params, updateStars) {
        var done = function(resp) {
            if (resp.worked) {
                updateStars(resp.id);
                refreshDropDown();
                updateAccordian();
                return true;
            }
        };
        AJS.FECRU.AJAX.ajaxDo(url, params, done, false);
    }

    function refreshDropDown() {
        var $dropDown = AJS.$("#starDropDownList");
        if ($dropDown.length > 0) {
            var done = function(resp) {
                if (resp.worked) {
                    $dropDown.replaceWith(resp.html);
                }
            };
            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/profile/starDropDownAjax.do", {}, done, false);
        }
    }

    //todo: use AUI inline dialog
    function createStarDialog(dialogHider) {
        var $dialog = AJS.$(DIALOG_HTML);
        //intialize the dialog buttons
        AJS.$("#close-astronomy", $dialog).click(function(e) {
            dialogHider && dialogHider();
            e.preventDefault();
        });

        var $starLabel = AJS.$("#star-labels", $dialog);

        $starLabel.keypress(function(e) {
            if ((e.which || e.keyCode) === AJS.$.ui.keyCode.ENTER) {// if return is clicked
                e.preventDefault();                
                AJS.$("#save-star-label", $dialog).trigger('click');
            }
        }).attr("autocomplete", "off");

        return $dialog;
    }

    function showStarDialog(dialogController, updateLabel, removeStar, currentLabel) {
        var $dialog = dialogController.getDialog();
        var $starLabel = AJS.$("#star-labels", $dialog);

        if(currentLabel) {
            $starLabel.attr("value", currentLabel).data("defaultValue", currentLabel).addClass("focussed");
        } else {
            $starLabel.data("defaultValue", $starLabel.val());
        }

        AJS.$("#remove-astronomy", $dialog).unbind().click(function(e) {
            e.preventDefault();
            removeStar();
            dialogController.hideDialog();
            $starLabel.val(DIALOG_DEFAULT).removeClass("focussed");            
        });

        AJS.$("#save-star-label", $dialog).unbind().click(function(e) {
            e.preventDefault();
            var newLabel = $starLabel.val();
            if (newLabel !== $starLabel.data("defaultValue")) {
                $starLabel.data("defaultValue", newLabel);
                updateLabel(newLabel);
            }
            dialogController.hideDialog();
        });
        dialogController.showDialog();
    }

    function isDialogShown(starLink) {
        return starLink.hasClass("showDialog");
    }

    /**
     * bind a live click event to the stars.
     */
    function bindStars() {
        var $dialog = undefined; //stores a reference to the jquery elmem
        var dialogHider = undefined;
        var PADDING = 5;
        var hoverOptions = {
            onHover: false,
            showArrow: true,
            fadeTime: 200,
            hideDelay: null,
            showDelay: 0,
            width: 240 + PADDING,
            offsetX: -8, //to get the arrow centred at the star's vertical asix
            offsetY: 4,
            container: "body",
            useLiveEvents: true,
            cacheContent:false,
            initCallback: function() {
                //implementation note: we need a way to hide the dialog on demand - this callback has a hook into
                //the hide function that inline-dialog uses internally, so this is saving a reference to that function
                //for use later.
                var that = this;
                dialogHider = function() {
                    that.hide();
                };
            }
        };
        var onStarClickedHandler = function ($contentDiv, trigger, showPopup) {
            var $link = AJS.$(trigger);
            var dialogControl = {
                showDialog: function() {
                    if (isDialogShown($link)) {
                        if ($dialog === undefined) {
                            $dialog = createStarDialog(dialogHider);
                        }
                        $dialog.appendTo($contentDiv).show();
                        var $starLabels = AJS.$("#star-labels");
                        $starLabels.placeholder($starLabels.val());
                        showPopup();
                    }
                },
                hideDialog: function() {
                    dialogHider && dialogHider();
                },
                getDialog: function () {
                    if ($dialog === undefined) {
                        $dialog = createStarDialog(this.hideDialog);
                    }
                    return $dialog;
                }
            };
            var opts = {
                throbberSetting: {
                    showThrob: true,         //todo: customizable throbbing needed?
                    noLatencyThreshold: 150, //if the ajax call takes less than this milliseconds, then no throb shown
                    minThrobberDisplay: 200  //otherwise, show for at least this milliseconds.
                }
            };
            starClicked(trigger, dialogControl, opts);
        };
        AJS.InlineDialog(".starrable.showDialog", "star-inline-dialog", onStarClickedHandler, hoverOptions);
        AJS.$(document).delegate('.starrable:not(.showDialog)', 'click', function() {
            onStarClickedHandler(null, this, null);
        });
    }

    ////// onload events for stars
    AJS.toInit(function() {
        bindStars();
    });
})();
;
/* END /2static/script/fecru/star.js */
/* START /2static/script/fecru/applinks.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
    AJS.FECRU.UAL = {};
} else if (!AJS.FECRU.UAL) {
    AJS.FECRU.UAL = {};
}

(function() {

    var ignoreCookieName = "ignoredAppLinks";

    AJS.FECRU.UAL.ignoreAppLink = function(applicationId) {
        var cookieValue = AJS.$.cookie(ignoreCookieName);
        if (!cookieValue) {
            cookieValue = "";
        }

        // set cookie so that anonymous users can at least have error messages suppressed
        cookieValue += (cookieValue.length > 0 ? "," : "") + applicationId;
        AJS.$.cookie(ignoreCookieName, cookieValue, { expires: 365, path: fishEyePageContext });

        // also set server ignore preference for this applink
        var url = AJS.CRU.UTIL.jsonUrlBase() + "/ignoreLinkedApp.do";
        AJS.FECRU.AJAX.ajaxDo(url, {appLinkId : applicationId});
    };

    AJS.FECRU.UAL.clearIgnoredAppLinks = function(onComplete) {
        // clear cookie
        AJS.$.cookie("ignoredAppLinks", null, { path: fishEyePageContext });

        var url = AJS.CRU.UTIL.jsonUrlBase() + "/clearIgnoreFlags.do";
        AJS.FECRU.AJAX.ajaxDo(url, {}, function() {
            if (AJS.$.isFunction(onComplete)) {
                onComplete();
            }
        });
    };

    /**
     * Remote exception notifier - display an error message, or prompt the user for authentication with a linked application
     *
     * @method warnRemoteActivityStream
     * @namespace AJS
     * @for AJS
     * @param applicationId {string} the ApplicationId of the remote application
     * @param message {string} a message to display
     * @param authUrl {string} the authorisation url to redirect the user to, if unspecified this method will display the
     * supplied message as an error message
     */
    AJS.FECRU.UAL.warnRemoteActivityStream = function (applicationId, applicationName, message, authUrl) {


        var cookieValue = AJS.$.cookie(ignoreCookieName);
        var suppressWarning = false;
        if (cookieValue) {
            suppressWarning = cookieValue.indexOf(applicationId) > -1;
        }

        if (authUrl) {
            if (!suppressWarning) {
                var linkId = "provide-credentials-" + applicationId;
                var $warning = AJS.$("<div class='remote-credentials-required-warning stream-notice'><p>" + message + "</p>" +
                                     "<a id='" + linkId + "'>Authenticate</a> <a class='ual-ignore-applink'>Ignore authentication requests from " + applicationName + "</a></div>");
                AJS.$(".ual-ignore-applink", $warning).click(function () {
                    $warning.slideUp("fast");
                    AJS.FECRU.UAL.ignoreAppLink(applicationId);
                });
                AJS.$("#" + linkId, $warning).click(function () {
                    oauthCallback = {
                        success: function() {
                            aouthWindow.close();
                            window.location.reload();
                        },
                        failure: function() {
                            aouthWindow.close();
                        }
                    };

                    aouthWindow = window.open(authUrl);
                });
                $warning.prependTo(AJS.$("#stream-notice-box"));
            }
        } else {
            if (!suppressWarning) {
                var $error = AJS.$("<div class='remote-error-warning stream-error'><p>" + message + "</p>" +
                                         "<a class='ual-ignore-applink'>Ignore errors from " + applicationName + "</a></div>");
                AJS.$(".ual-ignore-applink", $error).click(function () {
                    $error.slideUp("fast");
                    AJS.FECRU.UAL.ignoreAppLink(applicationId);
                });
                $error.prependTo(AJS.$("#stream-notice-box"));
            }
        }
    };

    var invalidateCache = function(issueKey) {
        AJS.FECRU.HOVER.invalidateCache(AJS.FECRU.HOVER.CACHE_FOREVER, issueKey);
    };

    AJS.$(".issue-hover-ignore-applink").live("click", function() {
        var $this = AJS.$(this);
        var appLinkId = $this.attr('data-id');
        AJS.FECRU.UAL.ignoreAppLink(appLinkId);
        var issueKey = $this.parents('.jirahover-details').attr('data-key');
        invalidateCache(issueKey);
        AJS.$("#inline-dialog-jiralinkspan-popup").hide();
    });

    AJS.$(".ual-authenticate").live("click", function() {
        var $this = AJS.$(this);
        var issueKey = $this.parents('.jirahover-details').attr('data-key');
        invalidateCache(issueKey);
        AJS.$("#inline-dialog-jiralinkspan-popup").hide();
    });



})();;
/* END /2static/script/fecru/applinks.js */
/* START /2static/script/fecru/prefs.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
    AJS.FECRU.PREFS = {};
} else if (!AJS.FECRU.PREFS) {
    AJS.FECRU.PREFS = {};
}


AJS.FECRU.PREFS.setPreference = function(name, value, callback) {
    var params = {};
    params[name] = value;
    AJS.FECRU.PREFS.setPreferences(params, callback);
};

AJS.FECRU.PREFS.setPreferences = function(prefs, callback) {
    var params = {};
    for (i in prefs) {
        params["@" + i] = prefs[i];
    }
    AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/fe/setPreference.do", params, callback, false);
};

AJS.FECRU.PREFS.setupBinaryPrefLinks = function(name, cookieName, initialState, fns, onStateString, offStateString, forceInitialCall, reload, optionsSelector) {
    var onSelector = "." + name + onStateString;
    var offSelector = "." + name + offStateString;

    var setPref = function (value, hideSelector, showSelector, callback) {
        AJS.FECRU.PREFS.setPreference(cookieName, value, callback);
        setVisibility(value, hideSelector, showSelector, true);
    };

    var setVisibility = function (value, hideSelector, showSelector, callfunction) {
        var $optionsSelector = AJS.$(optionsSelector);
        $optionsSelector.find(hideSelector).css("display","none");
        $optionsSelector.find(showSelector).css("display","block");
        if(callfunction || forceInitialCall) {
           fns[value].call();
        }
    };

    var reloadPage = function () {
        if(reload) {
           window.location.reload();
        }
    };

    // make sure we extend the selector to every match, by splitting on ",
    var createSelector = function (base, extra) {
        var arr = base.split(",");
        for (var i = 0; i < arr.length; i++) {
            arr[i] = arr[i] + " " + extra;
        }
        return arr.join(",");
    };

    AJS.$(document).delegate(createSelector(optionsSelector, onSelector), "click", function() {
        setPref(onStateString, onSelector, offSelector, reloadPage);
        return false;
    }).delegate(createSelector(optionsSelector, offSelector), "click", function() {
        setPref(offStateString, offSelector, onSelector, reloadPage);
        return false;
    });
    if (initialState) {
        setVisibility(onStateString, onSelector, offSelector, false);
    } else {
        setVisibility(offStateString, offSelector, onSelector, false);
    }
};
;
/* END /2static/script/fecru/prefs.js */
/* START /2static/script/lib/graphael/g.raphael.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */


(function () {
    Raphael.fn.g = Raphael.fn.g || {};
    Raphael.fn.g.markers = {
        disc: "disc",
        o: "disc",
        square: "square",
        s: "square",
        triangle: "triangle",
        t: "triangle",
        star: "star",
        "*": "star",
        cross: "cross",
        x: "cross",
        plus: "plus",
        "+": "plus",
        arrow: "arrow",
        "->": "arrow"
    };
    Raphael.fn.g.txtattr = {font: "12px Arial, sans-serif"};
    Raphael.fn.g.colors = [];
    var hues = [.6, .2, .05, .1333, .75, 0];
    for (var i = 0; i < 10; i++) {
        if (i < hues.length) {
            Raphael.fn.g.colors.push("hsb(" + hues[i] + ", .75, .75)");
        } else {
            Raphael.fn.g.colors.push("hsb(" + hues[i - hues.length] + ", 1, .5)");
        }
    }
    Raphael.fn.g.text = function (x, y, text) {
        return this.text(x, y, text).attr(this.g.txtattr);
    };
    Raphael.fn.g.labelise = function (label, val, total) {
        if (label) {
            return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) {
                if (value) {
                    return (+val).toFixed(value.replace(/^#+\.?/g, "").length);
                }
                if (percent) {
                    return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%";
                }
            });
        } else {
            return (+val).toFixed(0);
        }
    };

    Raphael.fn.g.finger = function (x, y, width, height, dir, ending, isPath) {
        // dir 0 for horisontal and 1 for vertical
        if ((dir && !height) || (!dir && !width)) {
            return isPath ? "" : this.path({});
        }
        ending = {square: "square", sharp: "sharp", soft: "soft"}[ending] || "round";
        var path;
        height = Math.round(height);
        width = Math.round(width);
        x = Math.round(x);
        y = Math.round(y);
        switch (ending) {
            case "round":
                if (!dir) {
                    var r = Math.floor(height / 2);
                    if (width < r) {
                        r = width;
                        path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", 0, 0, "a", r, Math.floor(height / 2), 0, 0, 1, 0, height, "l", 0, 0, "z"];
                    } else {
                        path = ["M", x + .5, y + .5 - r, "l", width - r, 0, "a", r, r, 0, 1, 1, 0, height, "l", r - width, 0, "z"];
                    }
                } else {
                    var r = Math.floor(width / 2);
                    if (height < r) {
                        r = height;
                        path = ["M", x - Math.floor(width / 2), y, "l", 0, 0, "a", Math.floor(width / 2), r, 0, 0, 1, width, 0, "l", 0, 0, "z"];
                    } else {
                        path = ["M", x - r, y, "l", 0, r - height, "a", r, r, 0, 1, 1, width, 0, "l", 0, height - r, "z"];
                    }
                }
                break;
            case "sharp":
                if (!dir) {
                    var half = Math.floor(height / 2);
                    path = ["M", x + .5, y + .5 + half, "l", 0, -height, Math.max(width - half, 0), 0, Math.min(half, width), half, -Math.min(half, width), half + (half * 2 < height), "z"];
                } else {
                    var half = Math.floor(width / 2);
                    path = ["M", x + half, y, "l", -width, 0, 0, -Math.max(height - half, 0), half, -Math.min(half, height), half + (half * 2 < height), Math.min(half, height), half, "z"];
                }
                break;
            case "square":
                if (!dir) {
                    path = ["M", x + .5, y + .5 + Math.floor(height / 2), "l", 0, -height, width, 0, 0, height, "z"];
                } else {
                    path = ["M", x + Math.floor(width / 2), y, "l", -width, 0, 0, -height, width, 0, "z"];
                }
                break;
            case "soft":
                var r;
                if (!dir) {
                    r = Math.min(width, Math.round(height / 5));
                    path = ["M", x + .5, y + .5 - Math.floor(height / 2), "l", width - r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r * 2, "a", r, r, 0, 0, 1, -r, r, "l", r - width, 0, "z"];
                } else {
                    r = Math.min(Math.round(width / 5), height);
                    path = ["M", x - Math.floor(width / 2), y, "l", 0, r - height, "a", r, r, 0, 0, 1, r, -r, "l", width - 2 * r, 0, "a", r, r, 0, 0, 1, r, r, "l", 0, height - r, "z"];
                }
        }
        if (isPath) {
            return path.join(",");
        } else {
            return this.path({}, path);
        }
    };

    // Symbols
    Raphael.fn.g.disc = function (cx, cy, r) {
        return this.circle(cx, cy, r);
    };
    Raphael.fn.g.line = function (cx, cy, r) {
        return this.rect(cx - r, cy - r / 5, 2 * r, 2 * r / 5);
    };
    Raphael.fn.g.square = function (cx, cy, r) {
        r = r * .7;
        return this.rect(cx - r, cy - r, 2 * r, 2 * r);
    };
    Raphael.fn.g.triangle = function (cx, cy, r) {
        r *= 1.75;
        return this.path({}, "M".concat(cx, ",", cy, "m0-", r * .58, "l", r * .5, ",", r * .87, "-", r, ",0z"));
    };
    Raphael.fn.g.star = function (cx, cy, r, r2) {
        r2 = r2 || r * .5;
        var points = ["M", cx, cy + r2, "L"],
                R;
        for (var i = 1; i < 10; i++) {
            R = i % 2 ? r : r2;
            points = points.concat([(cx + R * Math.sin(i * Math.PI * .2)).toFixed(3), (cy + R * Math.cos(i * Math.PI * .2)).toFixed(3)]);
        }
        points.push("z");
        return this.path({}, points);
    };
    Raphael.fn.g.cross = function (cx, cy, r) {
        r = r / 2.5;
        return this.path({}, "M".concat(cx - r, ",", cy, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"]));
    };
    Raphael.fn.g.plus = function (cx, cy, r) {
        r = r / 2;
        return this.path({}, "M".concat(cx - r / 2, ",", cy - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"]));
    };
    Raphael.fn.g.arrow = function (cx, cy, r) {
        return this.path({}, "M".concat(cx - r * .7, ",", cy - r * .4, "l", [r * .6, 0, 0, -r * .4, r, r * .8, -r, r * .8, 0, -r * .4, -r * .6, 0], "z"));
    };

    // Tooltips
    Raphael.fn.g.tag = function (x, y, text, angle, r) {
        angle = angle || 0;
        r = r == null ? 5 : r;
        text = text == null ? "$9.99" : text;
        var R = .5522 * r,
                res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function () {
            this.rotate(0, x, y);
            var bb = this[1].getBBox();
            if (bb.height >= r * 2) {
                this[0].attr({path: ["M", x, y + r, "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, "m", 0, -r * 2 - d, "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, "L", x + r + d, y + bb.height / 2 + d, "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, "L", x, y - r - d].join(",")});
            } else {
                var dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2));
                // ["c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r]
                // "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2,
                this[0].attr({path: ["M", x, y + r, "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, "M", x + dx, y - bb.height / 2 - d, "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, "L", x + dx, y - bb.height / 2 - d].join(",")});
            }
            this[1].attr({x: x + r + d + bb.width / 2, y: y});
            angle = (360 - angle) % 360;
            this.rotate(angle, x, y);
            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
            return this;
        };
        res.update();
        return res;
    };
    Raphael.fn.g.popupit = function (x, y, set, dir, size) {
        dir = dir == null ? 2 : dir;
        size = size || 5;
        x = Math.round(x) + .5;
        y = Math.round(y) + .5;
        var bb = set.getBBox(),
                w = Math.round(bb.width / 2),
                h = Math.round(bb.height / 2),
                dx = [0, w + size * 2, 0, -w - size * 2],
                dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
                p = ["M", x - dx[dir], y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
                    "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
                    "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
                    "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
                    "l", -Math.max(w - size, 0), 0, "z"].join(","),
                xy = [
                    {
                        x: x,
                        y: y + size * 2 + h
                    },
                    {
                        x: x - size * 2 - w,
                        y: y
                    },
                    {
                        x: x,
                        y: y - size * 2 - h
                    },
                    {
                        x: x + size * 2 + w,
                        y: y
                    }
                ][dir];
        set.translate(xy.x - w - bb.x, xy.y - h - bb.y);
        return this.path({fill: "#000", stroke: "none"}, p).insertBefore(set.node ? set : set[0]);
    };
    Raphael.fn.g.popup = function (x, y, text, dir, size) {
        dir = dir == null ? 2 : dir;
        size = size || 5;
        text = text || "$9.99";
        var res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function (X, Y, withAnimation) {
            X = X || x;
            Y = Y || y;
            var bb = this[1].getBBox(),
                    w = bb.width / 2,
                    h = bb.height / 2,
                    dx = [0, w + size * 2, 0, -w - size * 2],
                    dy = [-h * 2 - size * 3, -h - size, 0, -h - size],
                    p = ["M", X - dx[dir], Y - dy[dir], "l", -size, (dir == 2) * -size, -Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, -size, -size,
                        "l", 0, -Math.max(h - size, 0), (dir == 3) * -size, -size, (dir == 3) * size, -size, 0, -Math.max(h - size, 0), "a", size, size, 0, 0, 1, size, -size,
                        "l", Math.max(w - size, 0), 0, size, !dir * -size, size, !dir * size, Math.max(w - size, 0), 0, "a", size, size, 0, 0, 1, size, size,
                        "l", 0, Math.max(h - size, 0), (dir == 1) * size, size, (dir == 1) * -size, size, 0, Math.max(h - size, 0), "a", size, size, 0, 0, 1, -size, size,
                        "l", -Math.max(w - size, 0), 0, "z"].join(","),
                    xy = [
                        {
                            x: X,
                            y: Y + size * 2 + h
                        },
                        {
                            x: X - size * 2 - w,
                            y: Y
                        },
                        {
                            x: X,
                            y: Y - size * 2 - h
                        },
                        {
                            x: X + size * 2 + w,
                            y: Y
                        }
                    ][dir];
            if (withAnimation) {
                this[0].animate({path: p}, 500, ">");
                this[1].animate(xy, 500, ">");
            } else {
                this[0].attr({path: p});
                this[1].attr(xy);
            }
            return this;
        };
        return res.update(x, y);
    };
    Raphael.fn.g.flag = function (x, y, text, angle) {
        angle = angle || 0;
        text = text || "$9.99";
        var res = this.set(),
                d = 3;
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function (x, y) {
            this.rotate(0, x, y);
            var bb = this[1].getBBox(),
                    h = bb.height / 2;
            this[0].attr({path: ["M", x, y, "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, "z"].join(",")});
            this[1].attr({x: x + h + d + bb.width / 2, y: y});
            angle = 360 - angle;
            this.rotate(angle, x, y);
            angle > 90 && angle < 270 && this[1].attr({x: x - r - d - bb.width / 2, y: y, rotation: [180 + angle, x, y]});
            return this;
        };
        return res.update(x, y);
    };
    Raphael.fn.g.label = function (x, y, text) {
        var res = this.set();
        res.push(this.rect(x, y, 10, 10).attr({stroke: "none", fill: "#000"}));
        res.push(this.text(x, y, text).attr(this.g.txtattr).attr({fill: "#fff"}));
        res.update = function () {
            var bb = this[1].getBBox(),
                    r = Math.min(bb.width + 10, bb.height + 10) / 2;
            this[0].attr({x: bb.x - r / 2, y: bb.y - r / 2, width: bb.width + r, height: bb.height + r, r: r});
        };
        res.update();
        return res;
    };
    Raphael.fn.g.labelit = function (set) {
        var bb = set.getBBox(),
                r = Math.min(20, bb.width + 10, bb.height + 10) / 2;
        return this.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({stroke: "none", fill: "#000"}).insertBefore(set[0]);
    };
    Raphael.fn.g.drop = function (x, y, text, size, angle) {
        size = size || 30;
        angle = angle || 0;
        var res = this.set();
        res.push(this.path({}, ["M", x, y, "l", size, 0, "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, "z"]).attr({fill: "#000", stroke: "none", rotation: [22.5 - angle, x, y]}));
        angle = (angle + 90) * Math.PI / 180;
        res.push(this.text(x + size * Math.sin(angle), y + size * Math.cos(angle), text).attr(this.g.txtattr).attr({"font-size": size * 12 / 30, fill: "#fff"}));
        res.drop = res[0];
        res.text = res[1];
        return res;
    };
    Raphael.fn.g.blob = function (x, y, text, angle) {
        var angle = (+angle + 1 ? angle : 45) + 90,
                size = 12,
                rad = Math.PI / 180,
                fontSize = size * 12 / 12;
        var res = this.set();
        res.push(this.path({fill: "#000", stroke: "none"}));
        res.push(this.text(x + size * Math.sin((angle) * rad), y + size * Math.cos((angle) * rad) - fontSize / 2, text).attr(this.g.txtattr).attr({"font-size": fontSize, fill: "#fff"}));
        res.update = function (X, Y, withAnimation) {
            X = X || x;
            Y = Y || y;
            var bb = this[1].getBBox(),
                    w = Math.max(bb.width + fontSize, size * 25 / 12),
                    h = Math.max(bb.height + fontSize, size * 25 / 12),
                    x2 = X + size * Math.sin((angle - 22.5) * rad),
                    y2 = Y + size * Math.cos((angle - 22.5) * rad),
                    x1 = X + size * Math.sin((angle + 22.5) * rad),
                    y1 = Y + size * Math.cos((angle + 22.5) * rad),
                    dx = (x1 - x2) / 2,
                    dy = (y1 - y2) / 2,
                    rx = w / 2,
                    ry = h / 2,
                    k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)),
                    cx = k * rx * dy / ry + (x1 + x2) / 2,
                    cy = k * -ry * dx / rx + (y1 + y2) / 2;
            if (withAnimation) {
                this.animate({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")}, 500, ">");
            } else {
                this.attr({x: cx, y: cy, path: ["M", x, y, "L", x1, y1, "A", rx, ry, 0, 1, 1, x2, y2, "z"].join(",")});
            }
            return this;
        };
        res.update(x, y);
        return res;
    };

    Raphael.fn.g.colorValue = function (value, total, s, b) {
        return "hsb(" + [Math.min((1 - value / total) * .4, 1), s || .75, b || .75] + ")";
    };

    Raphael.fn.g.snapEnds = function (from, to, steps) {
        var f = from,
                t = to;
        if (f == t) {
            return {from: f, to: t, power: 0};
        }
        function round(a) {
            return Math.abs(a - .5) < .25 ? Math.floor(a) + .5 : Math.round(a);
        }

        var d = (t - f) / steps,
                r = Math.floor(d),
                R = r,
                i = 0;
        if (r) {
            while (R) {
                i--;
                R = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
            }
            i ++;
        } else {
            while (!r) {
                i = i || 1;
                r = Math.floor(d * Math.pow(10, i)) / Math.pow(10, i);
                i++;
            }
            i && i--;
        }
        var t = round(to * Math.pow(10, i)) / Math.pow(10, i);
        if (t < to) {
            t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i);
        }
        var f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i);
        return {from: f, to: t, power: i};
    };
    Raphael.fn.g.axis = function (x, y, length, from, to, steps, orientation, labels, type, dashsize) {
        dashsize = dashsize == null ? 2 : dashsize;
        type = type || "t";
        steps = steps || 10;
        var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0],
                ends = this.g.snapEnds(from, to, steps),
                f = ends.from,
                t = ends.to,
                i = ends.power,
                j = 0,
                text = this.set();
        d = (t - f) / steps;
        var label = f,
                rnd = i > 0 ? i : 0;
        dx = length / steps;
        if (+orientation == 1 || +orientation == 3) {
            var Y = y,
                    addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1));
            while (Y >= y - length) {
                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0]));
                text.push(this.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
                label += d;
                Y -= dx;
            }
            if (Math.round(Y + dx - (y - length))) {
                type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0]));
                text.push(this.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr).attr({"text-anchor": orientation - 1 ? "start" : "end"}));
            }
        } else {
            var X = x,
                    label = f,
                    rnd = i > 0 ? i : 0,
                    addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation),
                    dx = length / steps,
                    txt = 0,
                    prev = 0;
            while (X <= x + length) {
                type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
                text.push(txt = this.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
                var bb = txt.getBBox();
                if (prev >= bb.x - 5) {
                    text.pop(text.length - 1).remove();
                } else {
                    prev = bb.x + bb.width;
                }
                label += d;
                X += dx;
            }
            if (Math.round(X - dx - x - length)) {
                type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1]));
                text.push(this.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(this.g.txtattr));
            }
        }
        var res = this.path({}, path);
        res.text = text;
        res.all = this.set([res, text]);
        res.remove = function () {
            this.text.remove();
            this.constructor.prototype.remove.call(this);
        };
        return res;
    };

    Raphael.el.lighter = function (times) {
        times = times || 2;
        var fs = [this.attrs.fill, this.attrs.stroke];
        this.fs = this.fs || [fs[0], fs[1]];
        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
        fs[0].b = Math.min(fs[0].b * times, 1);
        fs[0].s = fs[0].s / times;
        fs[1].b = Math.min(fs[1].b * times, 1);
        fs[1].s = fs[1].s / times;
        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
    };
    Raphael.el.darker = function (times) {
        times = times || 2;
        var fs = [this.attrs.fill, this.attrs.stroke];
        this.fs = this.fs || [fs[0], fs[1]];
        fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex);
        fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex);
        fs[0].s = Math.min(fs[0].s * times, 1);
        fs[0].b = fs[0].b / times;
        fs[1].s = Math.min(fs[1].s * times, 1);
        fs[1].b = fs[1].b / times;
        this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"});
    };
    Raphael.el.original = function () {
        if (this.fs) {
            this.attr({fill: this.fs[0], stroke: this.fs[1]});
            delete this.fs;
        }
    };
})();
;
/* END /2static/script/lib/graphael/g.raphael.js */
/* START /2static/script/lib/graphael/g.bar.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.barchart = function (x, y, width, height, values, isVertical, opts) {
    opts = opts || {};
    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
            gutter = parseFloat(opts.gutter || "20%"),
            chart = this.set(),
            bars = this.set(),
            covers = this.set(),
            covers2 = this.set(),
            total = Math.max.apply(Math, values),
            stacktotal = [],
            paper = this,
            multi = 0,
            colors = opts.colors || this.g.colors,
            len = values.length;
    if (this.raphael.isArray(values[0])) {
        total = [];
        multi = len;
        len = 0;
        for (var i = values.length; i--;) {
            total.push(Math.max.apply(Math, values[i]));
            len = Math.max(len, values[i].length);
        }
        if (opts.stacked) {
            for (var i = len; i--;) {
                var tot = 0;
                for (var j = values.length; j--;) {
                    tot += + values[j][i] || 0;
                }
                stacktotal.push(tot);
            }
        }
        for (var i = values.length; i--;) {
            if (values[i].length < len) {
                for (var j = len; j--;) {
                    values[i].push(0);
                }
            }
        }
        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
    }

    total = (opts.to) || total;
    if (!isVertical) {
        var barwidth = width / (len * (100 + gutter) + gutter) * 100,
                barhgutter = barwidth * gutter / 100,
                barvgutter = typeof opts.vgutter == "undefined" ? 20 : opts.vgutter,
                stack = [],
                X = x + barhgutter,
                Y = (height - 2 * barvgutter) / total;
        if (!opts.stretch) {
            barhgutter = Math.round(barhgutter);
            barwidth = Math.floor(barwidth);
        }
        !opts.stacked && (barwidth /= multi || 1);
        for (var i = 0; i < len; i++) {
            stack = [];
            for (var j = 0; j < multi; j++) {
                var h = Math.round((multi ? values[j][i] : values[i]) * Y),
                        top = y + height - barvgutter - h,
                        bar;
                bars.push(bar = this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, opts.init ? 0 : h, true, type).attr({stroke: "none", fill: colors[multi ? j : i]}));
                bar.y = top;
                bar.x = Math.round(X + barwidth / 2);
                bar.w = barwidth;
                bar.h = h;
                bar.value = multi ? values[j][i] : values[i];
                opts.init && bar.animate({path: this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, 1)}, (+opts.init - 1) || 1000, ">");
                if (!opts.stacked) {
                    X += barwidth;
                } else {
                    stack.push(bar);
                }
            }
            if (opts.stacked) {
                var cvr;
                covers2.push(cvr = this.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr({stroke: "none", fill: "#000", opacity: 0}));
                cvr.bars = [];
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    cvr.bars.push(stack[s]);
                }
                stack.sort(function (a, b) {
                    return a.value - b.value;
                });
                var size = 0;
                for (var s = stack.length; s--;) {
                    stack[s].toFront();
                }
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    var bar = stack[s],
                            cover,
                            h = (size + bar.value) * Y,
                            path = this.g.finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1);
                    size && opts.init && bar.animate({path: path}, (+opts.init - 1) || 1000, ">");
                    size && !opts.init && bar.attr({path: path});
                    bar.h = h;
                    bar.y = y + height - barvgutter - !!size * .5 - h;
                    covers.push(cover = this.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bar;
                    size += bar.value;
                }

                X += barwidth;
            }
            X += barhgutter;
        }
        covers2.toFront();
        X = x + barhgutter;
        if (!opts.stacked) {
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    var cover;
                    covers.push(cover = this.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bars[i * (multi || 1) + j];
                    X += barwidth;
                }
                X += barhgutter;
            }
        }
        chart.label = function (labels, isBottom) {
            labels = labels || [];
            this.labels = paper.set();
            var L, l = -Infinity;
            if (opts.stacked) {
                for (var i = 0; i < len; i++) {
                    var tot = 0;
                    for (var j = 0; j < multi; j++) {
                        tot += multi ? values[j][i] : values[i];
                        if (j == multi - 1) {
                            var label = paper.g.labelise(labels[i], tot, total);
                            L = paper.g.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).insertBefore(covers[i * (multi || 1) + j]);
                            var bb = L.getBBox();
                            if (bb.x - 7 < l) {
                                L.remove();
                            } else {
                                this.labels.push(L);
                                l = bb.x + bb.width;
                            }
                        }
                    }
                }
            } else {
                for (var i = 0; i < len; i++) {
                    for (var j = 0; j < multi; j++) {
                        var label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
                        L = paper.g.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).insertBefore(covers[i * (multi || 1) + j]);
                        var bb = L.getBBox();
                        if (bb.x - 7 < l) {
                            L.remove();
                        } else {
                            this.labels.push(L);
                            l = bb.x + bb.width;
                        }
                    }
                }
            }
            return this;
        };
    } else {
        var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
                bargutter = Math.floor(barheight * gutter / 100),
                stack = [],
                Y = y + bargutter,
                X = (width - 1) / total;
        !opts.stacked && (barheight /= multi || 1);
        for (var i = 0; i < len; i++) {
            stack = [];
            for (var j = 0; j < multi; j++) {
                var val = multi ? values[j][i] : values[i],
                        bar;
                bars.push(bar = this.g.finger(x, Y + barheight / 2, opts.init ? 0 : Math.round(val * X), barheight - 1, false, type).attr({stroke: colors[multi > 1 ? j : i], fill: colors[multi > 1 ? j : i]}));
                bar.x = x + Math.round(val * X);
                bar.y = Y + barheight / 2;
                bar.w = Math.round(val * X);
                bar.h = barheight;
                bar.value = +val;
                opts.init && bar.animate({path: this.g.finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, 1)}, (+opts.init - 1) || 500, ">");
                if (!opts.stacked) {
                    Y += barheight;
                } else {
                    stack.push(bar);
                }
            }
            if (opts.stacked) {
                stack.sort(function (a, b) {
                    return a.value - b.value;
                });
                var size = 0;
                for (var s = stack.length; s--;) {
                    stack[s].toFront();
                }
                for (var s = 0, ss = stack.length; s < ss; s++) {
                    var bar = stack[s],
                            cover,
                            val = Math.round((size + bar.value) * X),
                            path = this.g.finger(x, bar.y, val, barheight - 1, false, type, 1);
                    size && opts.init && bar.animate({path: path}, (+opts.init - 1) || 1000, ">");
                    size && !opts.init && bar.attr({path: path});
                    bar.w = val;
                    bar.x = x + val;
                    covers.push(cover = this.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr({stroke: "none", fill: "#000", opacity: 0}));
                    cover.bar = bar;
                    size += bar.value;
                }
                Y += barheight;
            }
            Y += bargutter;
        }
        Y = y + bargutter;
        if (!opts.stacked) {
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    covers.push(this.rect(x, Y, width, barheight).attr({stroke: "none", fill: "#000", opacity: 0}));
                    Y += barheight;
                }
                Y += bargutter;
            }
        }
        chart.label = function (labels, isRight) {
            labels = labels || [];
            this.labels = paper.set();
            for (var i = 0; i < len; i++) {
                for (var j = 0; j < multi; j++) {
                    var label = paper.g.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total);
                    var X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5,
                            A = isRight ? "end" : "start",
                            L;
                    this.labels.push(L = paper.g.text(X, bars[i * (multi || 1) + j].y, label).attr({"text-anchor": A}).insertBefore(covers[0]));
                    if (L.getBBox().x < x + 5) {
                        L.attr({x: x + 5, "text-anchor": "start"});
                    } else {
                        bars[i * (multi || 1) + j].label = L;
                    }
                }
            }
            return this;
        };
    }
    chart.hover = function (fin, fout) {
        covers2.hide();
        covers.show();
        fout = fout || function () {
        };
        covers.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.hoverColumn = function (fin, fout) {
        covers.hide();
        covers2.show();
        fout = fout || function () {
        };
        covers2.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.click = function (f) {
        covers2.hide();
        covers.show();
        covers.click(f);
        return this;
    };
    chart.clickColumn = function (f) {
        covers.hide();
        covers2.show();
        covers2.click(f);
        return this;
    };
    chart.push(bars, covers, covers2);
    chart.bars = bars;
    chart.covers = covers;
    return chart;
};
;
/* END /2static/script/lib/graphael/g.bar.js */
/* START /2static/script/lib/graphael/g.pie.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.piechart = function (cx, cy, r, values, opts) {
    opts = opts || {};
    var paper = this,
            sectors = [],
            covers = this.set(),
            chart = this.set(),
            series = this.set(),
            order = [],
            len = values.length,
            angle = 0,
            total = 0,
            others = 0,
            cut = 9,
            defcut = true;
    chart.covers = covers;
    if (len == 1) {
        series.push(this.circle(cx, cy, r).attr({fill: this.g.colors[0], stroke: opt.stroke || "#fff", "stroke-width": opts.strokewidth == null ? 1 : opts.strokewidth}));
        covers.push(this.circle(cx, cy, r).attr({fill: "#000", opacity: 0, "stroke-width": 3}));
        total = values[0];
        values[0] = {value: values[0], order: 0, valueOf: function () {
            return this.value;
        }};
        series[0].middle = {x: cx, y: cy};
        series[0].mangle = 180;
    } else {
        function sector(cx, cy, r, startAngle, endAngle, fill) {
            var rad = Math.PI / 180,
                    x1 = cx + r * Math.cos(-startAngle * rad),
                    x2 = cx + r * Math.cos(-endAngle * rad),
                    xm = cx + r / 2 * Math.cos(-(startAngle + (endAngle - startAngle) / 2) * rad),
                    y1 = cy + r * Math.sin(-startAngle * rad),
                    y2 = cy + r * Math.sin(-endAngle * rad),
                    ym = cy + r / 2 * Math.sin(-(startAngle + (endAngle - startAngle) / 2) * rad),
                    res = ["M", cx, cy, "L", x1, y1, "A", r, r, 0, +(Math.abs(endAngle - startAngle) > 180), 1, x2, y2, "z"];
            res.middle = {x: xm, y: ym};
            return res;
        }

        for (var i = 0; i < len; i++) {
            total += values[i];
            values[i] = {value: values[i], order: i, valueOf: function () {
                return this.value;
            }};
        }
        values.sort(function (a, b) {
            return b.value - a.value;
        });
        for (var i = 0; i < len; i++) {
            if (defcut && values[i] * 360 / total <= 1.5) {
                cut = i;
                defcut = false;
            }
            if (i > cut) {
                defcut = false;
                values[cut].value += values[i];
                values[cut].others = true;
                others = values[cut].value;
            }
        }
        len = Math.min(cut + 1, values.length);
        others && values.splice(len) && (values[cut].others = true);
        for (var i = 0; i < len; i++) {
            var mangle = angle - 360 * values[i] / total / 2;
            if (!i) {
                angle = 90 - mangle;
                mangle = angle - 360 * values[i] / total / 2;
            }
            if (opts.init) {
                var ipath = sector(cx, cy, 1, angle, angle - 360 * values[i] / total).join(",");
            }
            var path = sector(cx, cy, r, angle, angle -= 360 * values[i] / total);
            var p = this.path({fill: opts.colors && opts.colors[i] || this.g.colors[i] || "#666", stroke: opts.stroke || "#fff", "stroke-width": opts.strokewidth == null ? 1 : opts.strokewidth, "stroke-linejoin": "round"}, opts.init ? ipath : path.join(","));
            p.value = values[i];
            p.middle = path.middle;
            p.mangle = mangle;
            sectors.push(p);
            series.push(p);
            opts.init && p.animate({path: path.join(",")}, (+opts.init - 1) || 1000, ">");
        }
        for (var i = 0; i < len; i++) {
            var p = paper.path({fill: "#000", opacity: 0, "stroke-width": 3}, sectors[i].attr("path"));
            opts.href && opts.href[i] && p.attr({href: opts.href[i]});
            p.attr = function () {
            };
            covers.push(p);
            series.push(p);
        }
    }

    chart.hover = function (fin, fout) {
        fout = fout || function () {
        };
        var that = this;
        for (var i = 0; i < len; i++) {
            (function (sector, cover, j) {
                var o = {
                    sector: sector,
                    cover: cover,
                    cx: cx,
                    cy: cy,
                    mx: sector.middle.x,
                    my: sector.middle.y,
                    mangle: sector.mangle,
                    r: r,
                    value: values[j],
                    total: total,
                    label: that.labels && that.labels[j]
                };
                cover.mouseover(function () {
                    fin.call(o);
                }).mouseout(function () {
                    fout.call(o);
                });
            })(series[i], covers[i], i);
        }
        return this;
    };
    chart.click = function (f) {
        var that = this;
        for (var i = 0; i < len; i++) {
            (function (sector, cover, j) {
                var o = {
                    sector: sector,
                    cover: cover,
                    cx: cx,
                    cy: cy,
                    mx: sector.middle.x,
                    my: sector.middle.y,
                    mangle: sector.mangle,
                    r: r,
                    value: values[j],
                    total: total,
                    label: that.labels && that.labels[j]
                };
                cover.click(function () {
                    f.call(o);
                });
            })(series[i], covers[i], i);
        }
        return this;
    };
    chart.inject = function (element) {
        element.insertBefore(covers[0]);
    };
    var legend = function (labels, otherslabel, mark, dir) {
        var x = cx + r + r / 5,
                y = cy,
                h = y + 10;
        labels = labels || [];
        dir = (dir && dir.toLowerCase && dir.toLowerCase()) || "east";
        mark = paper.g.markers[mark && mark.toLowerCase()] || "disc";
        chart.labels = paper.set();
        for (var i = 0; i < len; i++) {
            var clr = series[i].attr("fill"),
                    j = values[i].order,
                    txt;
            values[i].others && (labels[j] = otherslabel || "Others");
            labels[j] = paper.g.labelise(labels[j], values[i], total);
            chart.labels.push(paper.set());
            chart.labels[i].push(paper.g[mark](x + 5, h, 5).attr({fill: clr, stroke: "none"}));
            chart.labels[i].push(txt = paper.text(x + 20, h, labels[j] || values[j]).attr(paper.g.txtattr).attr({fill: opts.legendcolor || "#000", "text-anchor": "start"}));
            covers[i].label = chart.labels[i];
            h += txt.getBBox().height * 1.2;
        }
        var bb = chart.labels.getBBox(),
                tr = {
                    east: [0, -bb.height / 2],
                    west: [-bb.width - 2 * r - 20, -bb.height / 2],
                    north: [-r - bb.width / 2, -r - bb.height - 10],
                    south: [-r - bb.width / 2, r + 10]
                }[dir];
        chart.labels.translate.apply(chart.labels, tr);
        chart.push(chart.labels);
    };
    if (opts.legend) {
        legend(opts.legend, opts.legendothers, opts.legendmark, opts.legendpos);
    }
    chart.push(series, covers);
    chart.series = series;
    chart.covers = covers;
    return chart;
};
;
/* END /2static/script/lib/graphael/g.pie.js */
/* START /2static/script/lib/graphael/g.line.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.linechart = function (x, y, width, height, valuesx, valuesy, opts) {
    function shrink(values, dim) {
        var k = values.length / dim,
                j = 0,
                l = k,
                sum = 0,
                res = [];
        while (j < values.length) {
            l--;
            if (l < 0) {
                sum += values[j] * (1 + l);
                res.push(sum / k);
                sum = values[j++] * -l;
                l += k;
            } else {
                sum += values[j++];
            }
        }
        return res;
    }

    opts = opts || {};
    if (!this.raphael.isArray(valuesx[0])) {
        valuesx = [valuesx];
    }
    if (!this.raphael.isArray(valuesy[0])) {
        valuesy = [valuesy];
    }
    var allx = Array.prototype.concat.apply([], valuesx),
            ally = Array.prototype.concat.apply([], valuesy),
            xdim = this.g.snapEnds(Math.min.apply(Math, allx), Math.max.apply(Math, allx), valuesx[0].length - 1),
            minx = xdim.from,
            maxx = xdim.to,
            gutter = opts.gutter || 10,
            kx = (width - gutter * 2) / (maxx - minx),
            ydim = this.g.snapEnds(Math.min.apply(Math, ally), Math.max.apply(Math, ally), valuesy[0].length - 1),
            miny = ydim.from,
            maxy = ydim.to,
            ky = (height - gutter * 2) / (maxy - miny),
            len = Math.max(valuesx[0].length, valuesy[0].length),
            symbol = opts.symbol || "",
            colors = opts.colors || Raphael.fn.g.colors,
            that = this,
            columns = null,
            dots = null,
            chart = this.set(),
            path = [];

    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        len = Math.max(len, valuesy[i].length);
    }
    var shades = this.set();
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        if (opts.shade) {
            shades.push(this.path({stroke: "none", fill: colors[i], opacity: opts.nostroke ? 1 : .3}));
        }
        if (valuesy[i].length > width - 2 * gutter) {
            valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
            len = width - 2 * gutter;
        }
        if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
            valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
        }
    }
    var axis = this.set();
    if (opts.axis) {
        var ax = (opts.axis + "").split(/[,\s]+/);
        +ax[0] && axis.push(this.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2));
        +ax[1] && axis.push(this.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3));
        +ax[2] && axis.push(this.g.axis(x + gutter, y + height - gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0));
        +ax[3] && axis.push(this.g.axis(x + gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1));
    }
    var lines = this.set(),
            symbols = this.set(),
            line;
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        if (!opts.nostroke) {
            lines.push(line = this.path({
                stroke: colors[i],
                "stroke-width": opts.width || 2,
                "stroke-linejoin": "round",
                "stroke-linecap": "round",
                "stroke-dasharray": opts.dash || ""
            }));
        }
        var sym = this.raphael.isArray(symbol) ? symbol[i] : symbol;
        path = [];
        for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
            var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx;
            var Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
            (Raphael.isArray(sym) ? sym[j] : sym) && symbols.push(this.g[Raphael.fn.g.markers[this.raphael.isArray(sym) ? sym[j] : sym]](X, Y, (opts.width || 2) * 3).attr({fill: colors[i], stroke: "none"}));
            path = path.concat([j ? "L" : "M", X, Y]);
        }
        if (opts.shade) {
            shades[i].attr({path: path.concat(["L", X, y + height - gutter, "L",  x + gutter + ((valuesx[i] || valuesx[0])[0] - minx) * kx, y + height - gutter, "z"]).join(",")});
        }
        !opts.nostroke && line.attr({path: path.join(",")});
    }
    function createColumns() {
        // unite Xs together
        var Xs = [];
        for (var i = 0, ii = valuesx.length; i < ii; i++) {
            Xs = Xs.concat(valuesx[i]);
        }
        Xs.sort();
        // remove duplicates
        var Xs2 = [],
                xs = [];
        for (var i = 0, ii = Xs.length; i < ii; i++) {
            Xs[i] != Xs[i - 1] && Xs2.push(Xs[i]) && xs.push(x + gutter + (Xs[i] - minx) * kx);
        }
        Xs = Xs2;
        ii = Xs.length;
        var cvrs = that.set();
        for (var i = 0; i < ii; i++) {
            var X = xs[i] - (xs[i] - (xs[i - 1] || x)) / 2,
                    w = ((xs[i + 1] || x + width) - xs[i]) / 2 + (xs[i] - (xs[i - 1] || x)) / 2,
                    C;
            cvrs.push(C = that.rect(X - 1, y, w + 1, height).attr({stroke: "none", fill: "#000", opacity: 0}));
            C.values = [];
            C.y = [];
            C.x = xs[i];
            C.axis = Xs[i];
            C.xIndex = i;
            for (var j = 0, jj = valuesy.length; j < jj; j++) {
                Xs2 = valuesx[j] || valuesx[0];
                for (var k = 0, kk = Xs2.length; k < kk; k++) {
                    if (Xs2[k] == Xs[i]) {
                        C.values.push(valuesy[j][k]);
                        C.y.push(y + height - gutter - (valuesy[j][k] - miny) * ky);
                    }
                }
            }
        }
        columns = cvrs;
    }

    function createDots() {
        var cvrs = that.set(),
                C;
        for (var i = 0, ii = valuesy.length; i < ii; i++) {
            for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
                var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx,
                        nearX = x + gutter + ((valuesx[i] || valuesx[0])[j ? j - 1 : 1] - minx) * kx,
                        Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
                cvrs.push(C = that.circle(X, Y, Math.abs(nearX - X) / 2).attr({stroke: "none", fill: "#000", opacity: 0}));
                C.x = X;
                C.y = Y;
                C.value = valuesy[i][j];
                C.axis = (valuesx[i] || valuesx[0])[j];
            }
        }
        dots = cvrs;
    }

    chart.push(axis, columns, dots, lines, shades, symbols);
    chart.hoverColumn = function (fin, fout) {
        !columns && createColumns();
        columns.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.clickColumn = function (f) {
        !columns && createColumns();
        columns.click(f);
        return this;
    };
    chart.hrefColumn = function (cols) {
        var hrefs = that.raphael.isArray(arguments[0]) ? arguments[0] : arguments;
        if (!(arguments.length - 1) && typeof cols == "object") {
            for (var x in cols) {
                for (var i = 0, ii = columns.length; i < ii; i++) if (columns[i].axis == x) {
                    columns[i].attr("href", cols[x]);
                }
            }
        }
        !columns && createColumns();
        for (var i = 0, ii = hrefs.length; i < ii; i++) {
            columns[i] && columns[i].attr("href", hrefs[i]);
        }
        return this;
    };
    chart.hoverDot = function (fin, fout) {
        !dots && createDots();
        dots.mouseover(fin).mouseout(fout);
        return this;
    };
    chart.clickDot = function (f) {
        !dots && createDots();
        dots.click(f);
        return this;
    };
    return chart;
};
;
/* END /2static/script/lib/graphael/g.line.js */
/* START /2static/script/lib/graphael/g.dot.js */
/*
 * g.Raphael 0.2 - Charting library, based on Raphaël
 *
 * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
 * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
 */

Raphael.fn.g.dotchart = function (x, y, width, height, valuesx, valuesy, size, opts) {
    function drawAxis(ax) {
        +ax[0] && (ax[0] = paper.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2, opts.axisxlabels || null, opts.axisxtype || "t"));
        +ax[1] && (ax[1] = paper.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3, opts.axisylabels || null, opts.axisytype || "t"));
        +ax[2] && (ax[2] = paper.g.axis(x + gutter, y + height - gutter + maxR, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0, opts.axisxlabels || null, opts.axisxtype || "t"));
        +ax[3] && (ax[3] = paper.g.axis(x + gutter - maxR, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1, opts.axisylabels || null, opts.axisytype || "t"));
    }

    opts = opts || {};
    var xdim = this.g.snapEnds(Math.min.apply(Math, valuesx), Math.max.apply(Math, valuesx), valuesx.length - 1),
            minx = xdim.from,
            maxx = xdim.to,
            gutter = opts.gutter || 10,
            ydim = this.g.snapEnds(Math.min.apply(Math, valuesy), Math.max.apply(Math, valuesy), valuesy.length - 1),
            miny = ydim.from,
            maxy = ydim.to,
            len = Math.max(valuesx.length, valuesy.length, size.length),
            symbol = this.g.markers[opts.symbol] || "disc",
            res = this.set(),
            series = this.set(),
            max = opts.max || 100,
            top = Math.max.apply(Math, size),
            R = [],
            paper = this,
            k = Math.sqrt(top / Math.PI) * 2 / max;

    for (var i = 0; i < len; i++) {
        R[i] = Math.min(Math.sqrt(size[i] / Math.PI) * 2 / k, max);
    }
    gutter = Math.max.apply(Math, R.concat(gutter));
    var axis = this.set(),
            maxR = Math.max.apply(Math, R);
    if (opts.axis) {
        var ax = (opts.axis + "").split(/[,\s]+/);
        drawAxis(ax);
        var g = [], b = [];
        for (var i = 0, ii = ax.length; i < ii; i++) {
            var bb = ax[i].all ? ax[i].all.getBBox()[["height", "width"][i % 2]] : 0;
            g[i] = bb + gutter;
            b[i] = bb;
        }
        gutter = Math.max.apply(Math, g.concat(gutter));
        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
            ax[i].remove();
            ax[i] = 1;
        }
        drawAxis(ax);
        for (var i = 0, ii = ax.length; i < ii; i++) if (ax[i].all) {
            axis.push(ax[i].all);
        }
        res.axis = axis;
    }
    var kx = (width - gutter * 2) / ((maxx - minx) || 1),
            ky = (height - gutter * 2) / ((maxy - miny) || 1);
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        var sym = this.raphael.isArray(symbol) ? symbol[i] : symbol,
                X = x + gutter + (valuesx[i] - minx) * kx,
                Y = y + height - gutter - (valuesy[i] - miny) * ky;
        sym && R[i] && series.push(this.g[sym](X, Y, R[i]).attr({fill: opts.heat ? this.g.colorValue(R[i], maxR) : Raphael.fn.g.colors[0], "fill-opacity": opts.opacity ? R[i] / max : 1, stroke: "none"}));
    }
    var covers = this.set();
    for (var i = 0, ii = valuesy.length; i < ii; i++) {
        var X = x + gutter + (valuesx[i] - minx) * kx,
                Y = y + height - gutter - (valuesy[i] - miny) * ky;
        covers.push(this.circle(X, Y, maxR).attr({fill: "#000", stroke: "none", opacity: 0}));
        opts.href && opts.href[i] && covers[i].attr({href: opts.href[i]});
        covers[i].r = +R[i].toFixed(3);
        covers[i].x = +X.toFixed(3);
        covers[i].y = +Y.toFixed(3);
        covers[i].X = valuesx[i];
        covers[i].Y = valuesy[i];
        covers[i].value = size[i] || 0;
        covers[i].dot = series[i];
    }
    res.covers = covers;
    res.series = series;
    res.push(series, axis, covers);
    res.hover = function (fin, fout) {
        covers.mouseover(fin).mouseout(fout);
        return this;
    };
    res.click = function (f) {
        covers.click(f);
        return this;
    };
    res.href = function (map) {
        var cover;
        for (var i = covers.length; i--;) {
            cover = covers[i];
            if (cover.X == map.x && cover.Y == map.y && cover.value == map.value) {
                cover.attr({href: map.href});
            }
        }
    };
    return res;
};
;
/* END /2static/script/lib/graphael/g.dot.js */
/* START /2static/script/fecru/raphaelCharts.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.RAPHAELCHARTS = {};

(function () {

    var LINE_COLOURS_PIE = ["#478ec7","#769810","#d8561f","#d7e52e","#0c4383","#5fbe41","#f5832b","#edef00","#0c87c9","#ad2a15"];
    var LINE_COLOURS_XY = ["#d7561f","#478ec7","#769810","#dee439","#5fbe41","#0c4383","#f5832b","#edef00","#0c87c9","#ad2a15","#aebf47"];
    var REPOSITORY_COLOURS = ["#d7561f","#769810","#dee439","#5fbe41","#0c4383","#f5832b","#edef00","#0c87c9","#ad2a15","#aebf47"];

    var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

    var getClr = function(i) {
        return LINE_COLOURS_XY[i % LINE_COLOURS_XY.length];
    };

    function getMinY(bars) {
        var minY = bars[0].y;
        for (var i = 1, ii = bars.length; i < ii; i++) {
            minY = Math.min(bars[i].y, minY);
        }
        return minY;
    }

    function getMaxY(bars) {
        var maxY = 0;
        for (var i = 0, ii = bars.length; i < ii; i++) {
            maxY = Math.max(bars[i].y + bars[i].h, maxY);
        }
        return maxY;
    }

    function toComp(num) {
        var prefix = ["",,, "K",,, "M",,, "G",,, "T",,, "P",,, "E",,, "Z",,, "Y"];
        for (var i = 0, ii = prefix.length; i < ii; i++) {
            if (typeof prefix[i] == "string") {
                if (num < Math.pow(10, i + 3)) {
                    return Math.round(num / Math.pow(10, i)) + prefix[i];
                }
            }
        }
        return Math.round(num / Math.pow(10, i - 1)) + prefix[prefix.length - 1];
    }

    function initDiv(divName) {
        AJS.$("body").append('<div id="' + divName + '-child" style="display:block;position:absolute; left:-4000px;">&nbsp;</div>');
        return divName + '-child';
    }

    function finaliseDiv(divName) {
        var div = AJS.$('#' + divName + '-child');
        div.removeAttr("style");

        //div.insertAfter(AJS.$('#' + divName));
        AJS.$('#' + divName).replaceWith(div);

    }

    function drawLocCharts(xValuesLoc, yValuesLoc, seriesLabelsShort, seriesLabelsLong, pageUrl) {

        var txt = {"text-anchor": "start", "font-size": 11, fill: "#666"};
        var popup;
        var dateText;

        var formattedDates = [];
        for (var i = 0, ii = xValuesLoc.length; i < ii; i++) {
            formattedDates[i] = formatDate(xValuesLoc[i]);
        }

        function hin2() {
            axisSet.hide();
            if (popup) {
                popup.remove();
                tooltipSet.hide();
                popup = null;
            }
            if (dateText) {
                dateText.remove();
                dateText = null;
            }
            for (var i = 0, ii = this.values.length; i < ii; i++) {
                tooltipSet[i].attr({text: seriesLabelsShort[i] + ": " + toComp(this.values[i])});
            }
            tooltipSet.show();
            if (this.line) {
                this.line.show();
            } else {
                this.line = rMain.set();
                this.line.push(rMain.path({opacity: .3}, "M" + [Math.round(this.x) + .5, 10, "l", 0, 130]).insertBefore(this));
                for (var j = 0, jj = this.values.length; j < jj; j++) {
                    this.line.push(rMain.circle(Math.round(this.x) + .5, this.y[j], 3).insertBefore(this).attr({fill: getClr(j), stroke: "#fff"}));
                }
            }
            popup = rMain.set();
            var maxy = Math.max.apply(0, this.y);
            var miny = Math.min.apply(0, this.y);
            popup.push(rMain.g.popupit(this.x, miny + (maxy - miny) / 2, tooltipSet, (this.x < 200) * 2 + 1).insertBefore(this).attr({fill: "#fff", stroke: "#666"}));
            tooltipSet.insertBefore(this);
            dateText = rMain.text(this.x, 150, (formattedDates[this.xIndex]));
        }

        function hout() {
            if (popup) {
                popup.remove();
                tooltipSet.hide();
                axisSet.show();
                popup = null;
                this.line.hide();
            }
            if (dateText) {
                dateText.remove();
                dateText = null;
            }
        }

        function fclick() {
            if (pageUrl) {
                var oldMaxDate = pageUrl.match(/maxDate=.*?(?=&|$)/);
                var newMaxDate = 'maxDate=' + xValuesLoc[this.xIndex];
                if (oldMaxDate) {
                    pageUrl = pageUrl.replace(oldMaxDate, newMaxDate);
                } else {
                    pageUrl += (pageUrl.indexOf("?") == -1) ? "?" : "&";
                    pageUrl += newMaxDate;
                }
                window.location = pageUrl;
            }
        }

        // draw sparkline

        var divSparkline = "locChartSparkline";
        var divSparklineChild = initDiv(divSparkline);

        var rSparkline = Raphael(divSparklineChild, 280, 37);
        rSparkline.g.linechart(0, 0, 280, 32, xValuesLoc, yValuesLoc, {colors: LINE_COLOURS_XY, width: 2});


        // draw main chart

        var divMain = "locChartMain";
        var divMainChild = initDiv(divMain);

        var bump = 20;
        var mainHeight = 150 + 10 + 20 * seriesLabelsLong.length;

        var rMain = Raphael(divMainChild, 280, mainHeight);

        // set up tooltip text
        var tooltipSet = rMain.set();
        for (var l = 0, ll = seriesLabelsLong.length; l < ll; l++) {
            tooltipSet.push(rMain.text(100, 30 + l * 15, "").attr({fill: getClr(l), "font-weight": 800}));
        }
        tooltipSet.hide();

        // draw legend
        for (var i = 0, ii = seriesLabelsLong.length; i < ii; i++) {
            rMain.g.disc(20, 150 + 20 * i + bump, 7).attr({stroke: "none", fill: getClr(i)});
            rMain.text(30, 150 + 20 * i + bump, seriesLabelsLong[i]).attr({"text-anchor": "start"});
        }

        rMain.path({opacity: .3}, "M" + [0, 130, "l", 276, 0]);

        // draw date axis
        var axisSet = rMain.set();

        var step = (276 - 9) / xValuesLoc.length;

        var dateWidth = 60;
        var lastX = - dateWidth / 2;

        for (var i = 0, ii = xValuesLoc.length; i < ii; i++) {

            var xVal = 12 + i * step;
            if (xVal - lastX > dateWidth) {
                lastX = xVal;
                //                axisSet.push(rMain.path({opacity: .3}, "M" + [xVal, 140, "l", 0, 5]));
                axisSet.push(rMain.text(xVal, 150, (formattedDates[i])));
            }
        }

        rMain.g.linechart(0, 10, 276, 130, xValuesLoc, yValuesLoc, {colors: LINE_COLOURS_XY, width: 2}).hoverColumn(hin2, hout).clickColumn(fclick);

        finaliseDiv(divSparkline);
        finaliseDiv(divMain);

    }

    function formatDate(ms) {

        var d = new Date(ms);

        var s = "";
        if (d.getDate() < 10) {
            s += '0';
        }
        s += (d.getDate()) + " ";

        s += months[d.getMonth()] + " ";

        var year = d.getYear() - 100;
        if (year < 10) {
            s += '0';
        }
        s += year;

        return s;
    }

    function drawCommitCharts(seriesLabels, sparklineLabels, sparklineData, commitsByDay, commitsByHour,
                              activityCalendarYears, activityCalendarData) {

        var dayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        var hourLabels = ["00","01","02","03","04","05","06","07","08","09","10","11","12","13","14","15","16","17","18","19","20","21","22","23"];
        var monthLabels = ["J","F","M","A","M","J","J","A","S","O","N","D"];

        var divSparklineName = "sparklineSmall";
        var divSparklineChild = initDiv(divSparklineName);

        var rSmall = Raphael(divSparklineChild, 276, 32);
        rSmall.g.barchart(0, 0, 276, 32, sparklineData, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
        rSmall.path({opacity: .3}, "M" + [0, 32, "l", 276, 0]);

        var divMainName = "mainCanvas";
        var divMainChild = initDiv(divMainName);
        var calendarHeight = 80 + 13 * activityCalendarYears.length;
        var mainHeight = 310 + calendarHeight;

        var r = Raphael(divMainChild, 300, mainHeight);

        // intialise tooltips
        var tooltipSet = r.set();
        for (var l = 0; l < seriesLabels.length; l++) {
            tooltipSet.push(r.text(100, 30 + l * 15, "").attr({fill: getClr(l), "font-weight": 800}));
        }
        tooltipSet.hide();

        var txt = {"text-anchor": "start", "font-size": 11, fill: "#666"};
        r.text(10, 10, "52 week commits volume").attr(txt);
        r.text(10, 110, "Commits by day").attr(txt);
        r.text(10, 200, "Commits by hour").attr(txt);
        r.text(10, 290, "Commit calendar").attr(txt);

        var hin = function() {
            for (var i = 0, ii = this.bars.length; i < ii; i++) {
                tooltipSet[i].attr({text: seriesLabels[i] + ": " + this.bars[i].value});
            }
            tooltipSet.show();
            this.popup = r.g.popupit(this.bars[0].x, getMinY(this.bars), tooltipSet, 2).insertBefore(this).attr({fill: "#fff", stroke: "#666"});
            tooltipSet.insertBefore(this);
        };

        var hin_below = function() {
            for (var i = 0, ii = this.bars.length; i < ii; i++) {
                tooltipSet[i].attr({text:  seriesLabels[i] + ": " + this.bars[i].value});
            }
            tooltipSet.show();
            this.popup = r.g.popupit(this.bars[0].x, getMaxY(this.bars), tooltipSet, 0).insertBefore(this).attr({fill: "#fff", stroke: "#666"});
            tooltipSet.insertBefore(this);
        };

        var hout = function() {
            if (this.popup) {
                this.popup.remove();
                tooltipSet.hide();
                delete this.popup;
            }
        };


        r.g.barchart(0, 20, 276, 70, sparklineData, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
        r.path({opacity: .3}, "M" + [0, 90, "l", 276, 0]);

        r.g.barchart(20, 110, 235, 70, commitsByDay, false, {stacked: 1, vgutter: 15, colors: LINE_COLOURS_XY, "font-size": 11, fill: "#666"}).label(dayLabels).hoverColumn(hin, hout);

        r.g.barchart(25, 200, 230, 70, commitsByHour, false, {stacked: 1, vgutter: 15, colors: LINE_COLOURS_XY, "font-size": 11, fill: "#666"}).label(hourLabels).hoverColumn(hin, hout);


        var data = [],
                xs = [],
                ys = [];

        for (var i = 0, ii = activityCalendarData.length; i < ii; i++) {
            for (var j = 0, jj = activityCalendarData[j].length; j < jj; j++) {
                xs.push(j);
                ys.push(ii - i);
                data.push(activityCalendarData[i][j]);
            }
        }

        r.g.dotchart(0, 300, 270, calendarHeight, xs, ys, data, {symbol: "o", max: 6, heat: true, axis: "0 0 1 1", axisxstep: monthLabels.length - 1, axisystep: activityCalendarYears.length - 1, axisxlabels: monthLabels, axisxtype: " ", axisytype: " ", axisylabels: activityCalendarYears.reverse()}).hover(function () {
            this.tag = this.tag || r.g.tag(this.x, this.y, this.value, 0, this.r + 2).insertBefore(this);
            this.tag.show();
        }, function () {
            this.tag && this.tag.hide();
        }).axis.attr({"font-size": 10, fill: "#666"});

        finaliseDiv(divMainName);
        finaliseDiv(divSparklineName);
    }

    AJS.FECRU.RAPHAELCHARTS.commitSparkline = function(divName, width, height, queryParams, contextPath) {

        AJS.$.getJSON(contextPath + '/fe/commitSparkline.do', queryParams,
                function(_52) {
                    var r = Raphael(divName, width, height);

                    r.g.barchart(0, 10, width, height - 10, _52.data, false, {stacked: 1, vgutter: 0, colors: LINE_COLOURS_XY});
                });

    };

    AJS.FECRU.RAPHAELCHARTS.sidebarCharts = function(data, url) {
        drawLocCharts(data.locDataX, data.locDataY, data.seriesLabelsShort, data.seriesLabelsLong, url);
        drawCommitCharts(data.seriesLabelsShort, data.sparklineLabels, data.sparklineData, data.commitsByDay, data.commitsByHour, data.activityCalendarYears, data.activityCalendarData);
    };

    AJS.FECRU.RAPHAELCHARTS.sidebarChartsAjax = function(queryParams, contextPath, url) {

        AJS.$.getJSON(contextPath + '/fe/sidebarChartsJson.do', queryParams,
                function(data) {
                    AJS.FECRU.RAPHAELCHARTS.sidebarCharts(data, url);
                });
    };


})();

;
/* END /2static/script/fecru/raphaelCharts.js */
/* START /script/common/global.js */
//crucible global.js
var $dropdown	= 0;
var dropdownTimer = 0;

function openDropDown(id) {
    clearTimeout(dropdownTimer);

    // Close the old menu if it's not the same menu as is already open
    if ($dropdown && (id instanceof AJS.$ ? ($dropdown.get(0) !== id.get(0)) : ($dropdown.attr("id") !== id))) {
        $dropdown.fadeOut(100);
    }

    // get new menu and show it
    var $menu = (id instanceof AJS.$) ? id : AJS.$("#" + id);
    $dropdown = $menu.show();
}

function closeDropDown() {
    if ($dropdown && $dropdown.length === 1) {
        dropdownTimer = setTimeout(function(){
            $dropdown.fadeOut(100);
        }, 200);
    }
}

function simpleSwap(toHide, toShow) {
    if (typeof(toHide) == 'object') {
        AJS.$(toHide).hide();
    } else {
        AJS.$("#"+toHide).hide();
    }
    if (typeof(toShow) == 'object') {
        AJS.$(toShow).show();
    } else {
        AJS.$("#"+toShow).show();
    }
}
function expandAll(ids, prefix) {
    toggleAll(ids, true, false, prefix);
}
function collapseAll(ids, prefix) {
    toggleAll(ids, false, true, prefix);
}
function expandSelected(allIds, selectedIds, prefix) {
    //first collapse all
    toggleAll(allIds, false, true, prefix);
    toggleAll(selectedIds, true, false, prefix);
}
function toggleType(ids, prefix) {
    toggleAll(ids, false, false, prefix);
}
function toggleBasic(nodeName) {
    toggleNodeAndImage(nodeName, false, false);
}
function collapseBasic(nodeName) {
    toggleNodeAndImage(nodeName, false, true);
}
function expandBasic(nodeName) {
    toggleNodeAndImage(nodeName, true, false);
}
function toggleAll(ids, forceOpen, forceClose, prefix) {
    prefix = prefix || '';
    for (var i = 0; i < ids.length; i++) {
        var theNode = prefix + ids[i];
        toggleNodeAndImage(theNode, forceOpen, forceClose);
    }
    return false;
}

/**
 * jQuery uses [,] and : as special characters for selectors. These characters are still valid in HTML ids.
 * Use this method to return a sanitized version of the id for use in jQuery selectors.
 * @param id id to sanitize
 */
function sanitizeId(id) {
    return id.replace(/:/g,"\\:").replace(/\./g,"\\.");
}

function toggleNodeAndImage(nodeName, forceOpen, forceClose) {
    if (!nodeName) {
        return;
    }
    nodeName = sanitizeId(nodeName);
    var $node = AJS.$("#"+nodeName);
    if ($node.length === 0) {
        return;
    }

    var img = AJS.$("#"+nodeName+'img');
    if (img.length === 1) {
        var swapImage = true;
    }

    // Don't use is(":hidden") here, because we need to force things closed even if their parents are closed
    var shouldOpen = $node.css("display") === 'none';
    shouldOpen = (!forceClose) && (forceOpen || shouldOpen);
    if (shouldOpen) {
        $node.show();
        if (swapImage) {
            img.attr("src", fishEyePageContext + '/' + fishEyeSTATICDIR + '/images/arrow_open.gif' );
        }
    } else {
        $node.hide();
        if (swapImage) {
            img.attr("src", fishEyePageContext + '/' + fishEyeSTATICDIR + '/images/arrow_closed.gif' );
        }
    }
}

function rollover(obj) {
    if (obj.tagName == 'IMG') {
        var imgsrc = obj.src.replace(/\.gif$/, '');
        obj.src = imgsrc + '_over.gif';
    }
    return false;
}

function rollout(obj) {
    if (obj.tagName == 'IMG') {
        obj.src = obj.src.replace(/\_over\.gif$/, '.gif');
    }
    return false;
}

function toggleOverflow(handle, element) {
    var $el = AJS.$(element);
    var $hd = AJS.$(handle);
    if ($el.css("overflow") == 'hidden' || $el.css("overflow") == '') {
        $el.css({'overflow':'visible', 'height':'auto'});
        $hd.attr("src", $hd.attr("src").replace(/expand\.gif$/, 'collapse.gif') );
    } else {
        $el.css({'overflow':'hidden', 'height':'1.3em'});
        $hd.attr( "src", $hd.attr("src").replace(/collapse\.gif$/, 'expand.gif') );
    }
}


function submitDefaultForm(command) {
    document.defaultForm.command.value = command;
    document.defaultForm.submit();
}

function toggleWording(handle) {
    var $handleEl = AJS.$(handle);
    var handleText = $handleEl.html().substring(0, 4);
    switch (handleText) {
        case 'Show':
            $handleEl.html($handleEl.html().replace(/^Show/, 'Hide'));
            return true;
        case 'Hide':
            $handleEl.html($handleEl.html().replace(/^Hide/, 'Show'));
            return true;
        case 'Expa':
            $handleEl.html($handleEl.html().replace(/^Expand/, 'Collapse'));
            return true;
        case 'Coll':
            $handleEl.html($handleEl.html().replace(/^Collapse/, 'Expand'));
            return true;
        case 'More':
            $handleEl.html($handleEl.html().replace(/^More/, 'Less'));
            return true;
        case 'Less':
            $handleEl.html($handleEl.html().replace(/^Less/, 'More'));
            return true;
    }
    return false;
}

function show(toShow, bool) {
    AJS.$(toShow).toggle(bool);
}

function submitCruSearch(inputEl) {
    window.location = fishEyePageContext + "/cru/search?query=" + encodeURIComponent(inputEl.value);
}

function searchReviews() {
    var searchVal = document.forms.searchForm["search.text"].value;
    if (searchVal) {
        window.location = fishEyePageContext + "/cru/search?query=" + encodeURIComponent(searchVal);
    } else {
        window.location = fishEyePageContext + "/cru/search";
    }
}
function searchComments() {
    var searchVal = document.forms.searchForm["query"].value;
    if (searchVal) {
        window.location = fishEyePageContext + "/cru/commentSearch?search.text=" + encodeURIComponent(searchVal);
    } else {
        window.location = fishEyePageContext + "/cru/commentSearch";
    }
}

var ovAnk = false; //tracks whether the mouse is over a "real" anchor
function toggleSensitively(id) {
    if (!ovAnk) {
        toggleBasic(id);
    }
}
;
/* END /script/common/global.js */
/* START /2static/script/lib/ajs/contentnamesearch.js */
if(!(/jwebunit/).test(navigator.userAgent.toLowerCase())){
    AJS.FECRU.UI.setupQuicksearch = function () {
        AJS.$("#quick-search-form").submit(function () {
            return AJS.$.trim(AJS.$("#quick-search-input").val()).length > 0;
        });

        var $quickInput = AJS.$("#quick-search-input");
        $quickInput.placeholder($quickInput.val());

        var attr = {
            cache_size: 30,
            max_length: 1,
            effect: "appear" // TODO: add "fade" effect
        };
        var dd,
            cache = {},
            cache_stack = [],
            timer;

        function regexEscape(text) {
            if (!arguments.callee.sRE) {
                var specials = [
                    '/', '.', '*', '+', '?', '|',
                    '(', ')', '[', ']', '{', '}', '\\'
                    ];
                arguments.callee.sRE = new RegExp(
                    '(\\' + specials.join('|\\') + ')', 'g'
                    );
            }
            return text.replace(arguments.callee.sRE, '\\$1');
        }

        var hider = function (list) {
             AJS.$("a span", list).each(function () {
                var $a = AJS.$(this),
                    elpss = AJS("var", "&#8230;"),
                    elwidth = elpss[0].offsetWidth,
                    width = this.parentNode.parentNode.parentNode.parentNode.offsetWidth,
                    isLong = false,
                    rightPadding = 20; // add some padding so the ellipsis doesn't run over the edge of the box

                    // get the hidden space name property from the span
                    var spaceName = AJS.dropDown.getAdditionalPropertyValue($a, "spaceName");
                    if (spaceName != null) {
                        spaceName = "(" + spaceName + ") ";
                    } else {
                        spaceName = "";
                    }
                    AJS.dropDown.removeAllAdditionalProperties($a);

                this.realhtml = this.realhtml || $a.html();
                $a.html("<em>" + this.realhtml + "</em>");
                $a.append(elpss);
                //$a.attr("title", spaceName + this.realhtml.replace(/<\/?strong>/gi, ""));
                this.elpss = elpss;

                AJS.$("em", $a).each(function () {
                    var $label = AJS.$(this);

                    $label.show();
                    if (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding) {

                        var childNodes = this.childNodes;
                        var success = false;

                        for (var j = childNodes.length - 1; j >= 0; j--) {
                            var childNode = childNodes[j];
                            var truncatedChars = 1;

                            var valueAttr = (childNode.nodeType == 3) ? "nodeValue" : "innerHTML";
                            var nodeText = childNode[valueAttr];

                            do {
                                if (truncatedChars <= nodeText.length) {
                                    childNode[valueAttr] = nodeText.substr(0, nodeText.length - truncatedChars++);
                                } else { // if we cannot fit even one character of the next word, then try truncating the node just previous to this
                                    break;
                                }
                            } while (this.offsetLeft + this.offsetWidth + elwidth > width - rightPadding);

                            if (truncatedChars <= nodeText.length) {
                                // we've managed truncate part of the word and fit it in
                                success = true;
                                break;
                            }
                        }

                        if (success) {
                            isLong = true;
                        } else {
                            $label.hide();
                        }
                    }
                });
                elpss[isLong ? "show" : "hide"]();
            });
        };

        var searchBox = AJS.$("#quick-search-input");
        var $ddContainer = AJS.$("#quick-search-dropdown-holder");
        if (AJS.$.browser.safari && searchBox.length === 1) {
        //    searchBox[0].type = "search";
            searchBox[0].setAttribute("results", 10);
            searchBox[0].setAttribute("placeholder", "search");
        }
        var jsonparser = function (json, resultStatus) {
            var hasErrors = json.statusMessage ? true : false; // right now, we are overloading the existance of a status message to imply an error
            var matches = hasErrors ? [[{html: json.statusMessage, className: "error"}]] : json.matches;

            if (!hasErrors) {
                var query = json.query;
                if (!cache[query + AJS.FECRU.quickSearchUrl] && resultStatus == "success") {
                    cache[query + AJS.FECRU.quickSearchUrl] = json;
                    cache_stack.push(query + AJS.FECRU.quickSearchUrl);
                    if (cache_stack.length > attr.cache_size) {
                        delete cache[cache_stack.shift() + AJS.FECRU.quickSearchUrl];
                    }
                }
            }

            // do not update drop down for earlier requests. We are only interested in displaying the results for the most current search
            if (json.query != searchBox.val() && json.query.charAt(json.query.length-1) != "*") {
                return;
            }

            var old_dd = dd;
            dd = AJS.dropDown(matches, {selectionHandler: function (e, selected) {
                if (selected) {
                    var setHref = function(selected, href) {
                        if (href == "#" || !href) {
                            // if this menu item doesn't simply send us to a new url, trigger a custom event on it.
                            AJS.$(selected).trigger("menuItemSelected");
                        } else {
                            window.location = href;
                        }
                    };

                    var link = selected;
                    if (link.get(0).nodeName.toLowerCase() !== "a") {
                        link = selected.find("a");
                    }
                    setHref(link, link.attr("href"));
                    e.preventDefault();
                    return false;
                }
            }})[0];
            dd.$.attr("id", "quick-nav-drop-down");
            dd.$.appendTo($ddContainer);

            dd.onhide = function (causer) {
                if (causer == "escape") {
                    searchBox.focus();
                }
            };
            if (json.queryTokens) {
                var spans = AJS.$("span", dd.$);
                for (var i = 0, ii = spans.length - 1; i < ii; i++) {
                    (function () {
                        var $this = AJS.$(this),
                                html = $this.html();
                        // highlight matching tokens
                        var match = "(";
                        for (var j = 0, jj = json.queryTokens.length - 1; j <= jj; j++) {
                            match += regexEscape(json.queryTokens[j]);
                            if (j < jj) {
                                match += "|";
                            }
                        }
                        match += ")";
                        html = html.replace(new RegExp(match, "gi"), "<strong>$1</strong>");
                        $this.html(html);
                    }).call(spans[i]);
                }
            }
            hider(dd.$);
            if(typeof onShow == "function") {
                onShow.apply(dd);
            }
            dd.hider = function () {
                hider(dd.$);
            };
            AJS.onTextResize(dd.hider);
            if (old_dd) {
                dd.show();
                dd.method = attr.effect;
                AJS.unbindTextResize(old_dd.hider);
                old_dd.$.remove();
            } else {
                dd.show(attr.effect);
            }
            AJS.$("#busyQnav").hide();
        };
        searchBox.data("oldval", searchBox.val());
        var keyUpHandler = function () {
            var val = searchBox.val();

            var oldVal = searchBox.data("oldval");

            if (val != oldVal) {
                searchBox.data("oldval", val);
                if (!searchBox.hasClass("placeholded")) {
                    clearTimeout(timer);

                    if ((/[\S]{2,}/).test(val)) {
                        if (cache[val + AJS.FECRU.quickSearchUrl]) {
                            jsonparser(cache[val + AJS.FECRU.quickSearchUrl]);
                        } else {
                            var url = AJS.FECRU.quickSearchUrl + "?q=" + AJS.escape(val) + "&type=ajax&searchAllDirs=false";
                            timer = setTimeout(function () { // delay sending a request to give the user a chance to finish typing their search term(s)'
                                AJS.$("#busyQnav").show();
                                return AJS.$.ajax({
                                    type: "GET",
                                    url: url,
                                    data: null,
                                    success: jsonparser,
                                    dataType: "json",
                                    timeout: 20000,
                                    error: function ( xml, status, e ) { // ajax error handler
                                        if (status == "timeout") {
                                            jsonparser({statusMessage: "Query has timed out", query: val}, status);
                                        } else if (status == "error") {
                                            jsonparser({statusMessage: "A server-side error has occurred.", query: val}, status);
                                        }
                                    }
                                });
                            }, 600);
                        }
                    } else {
                        AJS.$("#busyQnav").hide();
                        dd && dd.hide();
                    }
                }
            }
        };
        searchBox.keyup(keyUpHandler);
    };
}
;
/* END /2static/script/lib/ajs/contentnamesearch.js */
/* START /2static/script/fe/fisheye-ui.js */
if (!AJS.FE) {
    AJS.FE = {};
}

(function() {
    /**
     * Converts an id to a jquery object.
     *
     * @param el an element id, a jquery id selector (starting with '#'), or a jquery elem
     * @return an object
     */
    var ensureObject = function (el) {
        if (typeof el == "string") {
            if (el.charAt(0) != "#") {
                el = '#' + el;
            }
            el = AJS.$(el);
        }

        return el;
    };

    /**
     * Returns the minimum height of an element (el) so that it is always at least at
     * the height of its parent element, and optionally takes into account the height/s of
     * any siblings when el is not specified as onlyChild.
     *
     * onlyChild should be used either where el has no sibling element, or where you want
     * el to not consider its siblings - where el and siblings might be floated, for example.
     *
     */
    var elementGetMinHeight = function (el, onlyChild) {
        var element = ensureObject(el);
        if (element.length === 0) {// If element doesn't exist on the page a length of 0 will be returned
            return 0
        }

        var parentHeight = AJS.$(window).height(),
            siblingsHeight = 0
        ;

        if (!onlyChild) {
            var siblings = element.siblings(),
                siblingsCount = siblings.size()
            ;

            for (var i = 0; i < siblingsCount; i += 1) {
                var sibling = siblings.eq(i);
                if (sibling.is(":visible")) {
                    siblingsHeight += sibling.outerHeight(true);
                }
            }
        }

        return parseInt(parentHeight, 10) - parseInt(siblingsHeight, 10);
    };
    AJS.FE.elementGetMinHeight = elementGetMinHeight;

    /**
     * Sets the minimum height of an element (el) to "height" and works hand in hand with
     * elementGetMinHeight.
     *
     * Rather than being part of a larger function which might set _and_ get, this usage
     * allows other functions to utilize the method, without having to determine el's parent and/or
     * sibling elements.
     *
     */
    var elementSetMinHeight = function (el, height) {
        var element = ensureObject(el);

        element.css({
            minHeight: outerHeightProperties(el, height)
        })
    };

    /**
     * Sets the height of an element (el) to "height" and works hand in hand with
     * elementGetMinHeight.
     *
     * Rather than being part of a larger function which might set _and_ get, this usage
     * allows other functions to utilize the method, without having to determine el's parent and/or
     * sibling elements.
     *
     */
    var elementSetHeight = function (el, height) {
        var element = ensureObject(el);

        element.height(outerHeightProperties(el, height))
    };

    /**
     * Determines whether an element (el) has style proprties that affect its outerHeight(true) value
     *
     * You can't set outerHeight so we need to take outerHeight effecting properties in to account, so we
     * accept a height parameter, test for any "outer" properties, and subtract any we find from the height given
     *
     */

    var outerHeightProperties = function (el, height) {
        var element = ensureObject(el),
            outerProperties = ["borderTopWidth", "borderBottomWidth", "marginTop", "marginBottom", "paddingTop", "paddingBottom"],
            properties = 0
        ;

        for (var i = 0, l = outerProperties.length; i < l; i += 1) {
            var property = parseInt(element.css(outerProperties[i]), 10);

            if (property) {
                properties += property;
            }
        }

        return height - properties;
    };
    AJS.FE.outerHeightProperties = outerHeightProperties;

    var panelsSetMinHeight = function () {
        if (AJS.$("#atlas").length > 0) {//makes that we only try to force the element sizing on pages that can benefit from it
            if (AJS.$("#columns").length > 0) {//threePanelPAgeContent.tag used
                elementSetHeight("columns", elementGetMinHeight("#atlas", false)
                       - AJS.$('#content-fixed').outerHeight(true)
                );

                
                var columnHeight = parseInt(AJS.$("#columns").css('height'), 10) // not .height()
                        - AJS.$("#content-sidebar-head").outerHeight() // Less the branch selector
                        - 2; // Less 2 pixels (from a border i think. Its cheaper hard coding this

                // If we are on the annotation/diff view, restrict, otherwise let it expand
                if (AJS.FECRU.restrictToWindowHeight) {
                    elementSetHeight("content-navigation-panel", columnHeight);
                } else {
                    elementSetMinHeight("content-navigation-panel", columnHeight);
                }
            } else if (AJS.$("#content-single").length > 0) {
                elementSetMinHeight("content-single", AJS.$(window).height()
                       - AJS.$('#header').outerHeight(true)
                       - AJS.$('#footer').outerHeight(true)
                );
            }
        }
    };

    /**
     * Bind a callback that fires when resizing is completed on matching elements.
     *
     * A resize is considered complete after completionDelay ms have passed since
     * the last resize event.
     *
     * This prevents firing the resize event handler multiple times before the resize
     * is actually complete.
     *
     * This should only be being utilised by Snippets pages and TODO deprecate in favour
     * of elementSetMinHeight
    */

    var hasSetupPageFillHeight = false;
    AJS.FE.setupPageFillHeight = function ($changeHeight, useMinHeight) {
        if (hasSetupPageFillHeight) {
            return;
        }

        var offset = ($changeHeight) ? $changeHeight.offset().top : 34;

        $changeHeight = $changeHeight || AJS.$("#atlas");
        hasSetupPageFillHeight = true;
        var onResize = function() {
            var windowHeight = AJS.$(window).height(),
                footerHeight = AJS.$("#footer").outerHeight(),
                borderAndPadding = $changeHeight.outerHeight() - $changeHeight.height();
            $changeHeight.css(useMinHeight ? 'min-height' : 'height', windowHeight - footerHeight - offset - borderAndPadding);
        };
        onResize();
        AJS.FECRU.UI.setCompletedResizeTimeout(window, onResize);
    };

    var columnResize = function (min) {
        AJS.$("#content-resizable").resizable({
            ghost: true,
            handles: "e",
            maxWidth: 600,
            minWidth: min || 310,
            start: function() {
                AJS.$(".tearout-tabs").append(AJS.$(document.createElement("div")).addClass("ui-resizable-helper"));
            },
            stop: function() {
                AJS.$(".tearout-tabs").children(".ui-resizable-helper").remove();
                AJS.$('#content-sidebar').css('width', AJS.$(this).css('width'));
            }
        });

        var selector = "#content-resizable .ui-resizable-handle, #content-shield, .shielded";
        AJS.$(document).delegate(selector, "mousedown", function() {
            AJS.$("html").addClass("shielded");
        }).delegate(selector, "mouseup", function() {
            AJS.$("html").removeClass("shielded");
        });
    };

    var setupWatch = function() {
        var $watchform = AJS.$("#watchform");
        var eventName = "click.watchform";
        if ($watchform.length > 0) {
            var $input = $watchform.children("input");
            AJS.$(".watch-on", $watchform).unbind(eventName).bind(eventName, function(evnt) {
                evnt.preventDefault();
                $input.val('off');
                $watchform.submit();
            });
            AJS.$(".watch-off", $watchform).unbind(eventName).bind(eventName, function(evnt) {
                evnt.preventDefault();
                $input.val('on');
                $watchform.submit();
            });
        }
    };

    var reloadFilePaneAndHeader = function(href) {
        var origUrl = AJS.$("#origUrl").val();
        var $spinner = AJS.$("#file-table-spinner");
        $spinner.show();
        AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/fe/loadFilePane.do", {href: href, origUrl: origUrl}, function(resp) {
            if (resp.worked) {
                var replacement = AJS.$.clean([resp.fileTable], document);
                AJS.$("#browse-table").replaceWith(replacement);
                AJS.$("#masthead").replaceWith(resp.mastheadDiv);
                AJS.$("#dirlist-toolbar-bar").replaceWith(resp.taskBarBar);
                AJS.$(".content-view").replaceWith(resp.contentFixed);
                AJS.$("#content-fixed > .header").replaceWith(resp.contentTitle);

                var encodedLinkUrl = resp.encodedBranchSelectorUrl;
                var link = AJS.$("#select-branch-all");
                var href = link.attr('href');
                var queryString = href.substring(href.indexOf('?'));
                link.attr('href', encodedLinkUrl + queryString);


                link = AJS.$("#select-branch-default");
                href = link.attr('href');
                queryString = href.substring(href.indexOf('?'));
                link.attr('href', encodedLinkUrl + queryString);

                link = AJS.$("#select-branch-last");
                if (link.length) {
                    href = link.attr('href');
                    queryString = href.substring(href.indexOf('?'));
                    link.attr('href', encodedLinkUrl + queryString);
                }

                AJS.$('#branch-constraint-form').attr('action', encodedLinkUrl);

                setupWatch();
                AJS.FECRU.RSS.setupRSSDialog();
                AJS.FECRU.UI.setupQuicksearch();
            }
            $spinner.hide();
        }, false);
    };

    /**
     * Find a link in the directory tree by its href
     */
    var $findLink = function(href) {
        return AJS.$("#navigation-tree").find("a.pathLink[href='" + href + "']");
    };

    AJS.FE.browseDirectoryPathLinkFunction = function(event) {
        var $node = AJS.$(event.target);
        var href = $node.attr("href");
        var toggled = function () { };
        var self = AJS.FE.browseDirectoryPathLinkFunction;
        if ($node.hasClass("browse-directory")) {
            // find the node in the tree with the same href as us
            var $selectedLink = $findLink(href);
            AJS.FECRU.BROWSE.selectLink($selectedLink, toggled, self);
        } else {
            AJS.FECRU.BROWSE.selectLink($node, toggled, self);
        }
        reloadFilePaneAndHeader(href);
        return false;
    };

    AJS.FE.extractRevisionDetailsSortKeys = function(cell) {
        var $node = AJS.$(cell);
        var $input = $node.children("input");
        if ($input.size() > 0) {
            return $input.val();
        } else {
            var result = AJS.$.trim($node.text());
            if ($node.hasClass("browse-comment")) {
                // sort directories before files
                if ($node.children("a").hasClass("browse-directory")) {
                    result = "directory-" + result;
                } else {
                    result = "file-" + result;
                }
            }
            return result;
        }
    };

    AJS.FE.setupTable = function(prefix, rowClickFn, extractionFn) {
        AJS.FE.resetupTable(prefix, extractionFn);
        AJS.FECRU.UI.tableRowClick(prefix, rowClickFn);
    };

    AJS.FE.resetupTable = function(prefix, extractionFn) {
        columnResize();
        AJS.FECRU.UI.tableSort(prefix, extractionFn);
    };

    AJS.FE.toggleTabs = function () {
        AJS.$(document).delegate(".tearout-tabs li", "click", function () {
            var active = AJS.$(this).hasClass("tearout-active") ? true : null;
            var tab = AJS.$(this).attr("class").split("-")[1];
            var panel = "#panel-" + tab;

            AJS.$(".tearout-tabs li").each(function () {
                AJS.$(this).removeClass("tearout-active");
                AJS.$(this).children("a").unbind();
            });

            AJS.$(".panel-tearout", "#content-navigation").each(function () {
                AJS.$(this).addClass("hidden");
            });
            // preference will be null if we are ignoring preferences
            var preference = AJS.$(this).children("input").val();
            if (active) {
                AJS.$('#content').addClass('collapsed-sidebar');
                // hiding everything
                if (preference) {
                    AJS.FECRU.PREFS.setPreference("shp", "N");
                }
            } else {
                AJS.$(this).addClass("tearout-active");
                AJS.$('#content').removeClass('collapsed-sidebar');
                AJS.$(panel).removeClass("hidden");
                if (preference) {
                    AJS.FECRU.PREFS.setPreferences({slp: preference, shp: "Y"});
                }
            }
            // resize the pane when in changeset view
            if (AJS.$('#section-changeset-view').length > 0) {
                AJS.$(window).resize();
            }
        });
    };

    var hasSetupPanes = false;

    AJS.FE.setupPanes = function() {
        if (!hasSetupPanes) {
            columnResize(310);
            hasSetupPanes = true;
        }
        var $bottomToolbar = AJS.$("#toolbar-bottom");
        if ($bottomToolbar) {
            //TODO sharkie
        }
    };

    AJS.FE.streamMoreFocus = function () {
        AJS.$(document).delegate("#stream a.more", "click", function(){
            AJS.$(this).closest(".stream").addClass("stream-focus");
            AJS.$("body").one("click",function(){
                AJS.$("#stream").find(".stream").removeClass("stream-focus");
            });
        });
    };

    AJS.FECRU.UI.refreshSearch = function(url) {
        var repoQuicksearchURL = AJS.FECRU.quickSearchUrl;
        AJS.FECRU.quickSearchUrl = url;
        var searchBox = AJS.$("#quick-search-input");
        searchBox.removeData("oldval");
        searchBox.trigger("keyup");
        AJS.FECRU.quickSearchUrl = repoQuicksearchURL;
        return false;
    };

    AJS.$(function () {
        // Give a warning if firebug is running
        AJS.FECRU.UI.warnAboutFirebug(function() {
            AJS.$(window).resize();
        });

        panelsSetMinHeight();
        AJS.FECRU.UI.setCompletedResizeTimeout(window, panelsSetMinHeight);

        //setup the watch links
        setupWatch();

        AJS.FE.setupPanes();

        if (!AJS.$.support.opacity) {
            AJS.FE.streamMoreFocus();
        }

        /* branch selector stuff */
        var showInputError = function() {
            AJS.FECRU.AJAX.appendErrorResponse("FishEye could not find a branch matching '" + $input.val() + "'");
            AJS.FECRU.AJAX.showErrorBox();
            $input.trigger("reset.autocomplete");
        };

        var triggerKeyDownOnInput = function(keyCode) {
            var keydown = jQuery.Event('keydown');
            keydown.which = keyCode;
            keydown.keyCode = keydown.which;
            $input.trigger(keydown);
        };

        var $input = AJS.$('#branchConstraint-input'),
            $branchSelector = AJS.$('#branch-selector'),
            $closeGroup = $branchSelector.find('.close-group'),
            $selectLastBranch = AJS.$('#select-branch'),
            $editLastBranch = $branchSelector.find('.edit-last-branch'),
            $closeLastBranch = $branchSelector.find('.close-last-branch'),
            $constraintForm = AJS.$('#branch-constraint-form'),
            $branchConstraint = $branchSelector.find('.branchConstraint'),
            $tagConstraint = $branchSelector.find('.tagConstraint'),
            timeout
        ;

        // if there is no last branch, we dont want the edit box to go away
        if ($editLastBranch.length === 1) {
            // if you click on the edit button, hide the edit button and show the autocomplete
            $editLastBranch.click(function() {
                $branchSelector.addClass('edit');
                $input.focus();
                if (AJS.$.browser.msie) {
                    $constraintForm.closest(".toolbar-group").siblings().attr("style", "");
                }
            });

            // hide and clear the autocomplete field and show the
            var hideAutoComplete = function() {
                $input.trigger("reset.autocomplete"); // clears it and handles internal ajax calls etc
                $branchSelector.removeClass('edit');
                AJS.FECRU.AJAX.stopSpin('branchConstraint-input'); // if it was mid-ajax call
                $input.trigger("placeholder.reset"); // so it shows the placeholder text
            };

            $input
                .keydown(function(e) {
                    if (e.keyCode === AJS.$.ui.keyCode.ESCAPE) {
                        hideAutoComplete();
                    }
                });
            $closeLastBranch.click(hideAutoComplete);

            // if we are already on the branch, clicking the branch button edits it
            if ($selectLastBranch.hasClass('active')) {
                $selectLastBranch.children('a').click(function(e) {
                    $editLastBranch.click();
                    e.preventDefault();
                    e.stopPropagation();
                });
            }

        }

        AJS.FECRU.UI.processSelectedBranch = function(data) {
            if (!data) {
                return;
            }
            triggerKeyDownOnInput(AJS.$.ui.keyCode.ESC); // hide the select box
            var type;

            if (data.isBranch) {
                $branchConstraint.val(data.id);
                $tagConstraint.val('');
                type = 'b';
            } else if (data.isTag) {
                $tagConstraint.val(data.id);
                $branchConstraint.val('');
                type = 't';
            } else {
                return;
            }

            AJS.FECRU.AJAX.startSpin(AJS.$('.close-last-branch'), 'selected-spinner', true);
            AJS.$('body').children('.ac_results').hide();
            AJS.CRU.UTIL.makeCssRule('.ac_results', 'display: none !important;'); // since the results may not exist, and
                                                                              // this page is getting reloaded anyway,
                                                                              // we create a css rule to prevent it
                                                                              // from being show again unnecessarily

            $input.val(data.id)
                .addClass('disabled')
                .siblings('.fecru-autocomplete-dropdown-icon')
                    .removeClass('visible');

            var cookieJson = {};
            var branchJson = {};
            branchJson['t'] = type;
            branchJson['n'] = data.id;
            cookieJson[AJS.FE.branchSelectorRepName] = branchJson;

            $branchSelector.find('#lastSelected').val(JSON.stringify(cookieJson)); // set the preference
            $constraintForm.submit();
        };

        var autocompleteEnterPressed = false; // enter was pressed before results came back

        $input
            .keydown(function(e) {
                if (e.keyCode === AJS.$.ui.keyCode.ENTER) {
                    e.preventDefault();
                    e.stopPropagation();

                    autocompleteEnterPressed = true;
                }
            })
            .change(function() {
                autocompleteEnterPressed = false;
            })
            .bind("autocomplete-data-received", function(event, result) {
                if (autocompleteEnterPressed) {
                    var inputValue = $input.val();
                    var dataList = result.data;
                    autocompleteEnterPressed = false;
                    if (dataList && dataList.length) {
                        if (dataList.length === 1) {
                            // select the one value that is there
                            triggerKeyDownOnInput(AJS.$.ui.keyCode.ENTER);
                        } else {
                            for(var i = 0, len = dataList.length; i < len; i++) {
                                var data = dataList[i].data;
                                if (data.id.toLowerCase() === inputValue.toLowerCase()) {
                                    // select the exact match
                                    AJS.FECRU.UI.processSelectedBranch(data);
                                }
                            }
                        }
                    } else {
                        if ($input.val()) {
                            showInputError();
                        }
                    }
                }
            });
    });
})();
;
/* END /2static/script/fe/fisheye-ui.js */
/* START /2static/script/fe/fisheye-changeset.js */
if (!AJS.FE.CHANGESET) {
    AJS.FE.CHANGESET = {};
}
(function() {
    /////////////////////
    // private functions
    /////////////////////

    /**
     *from http://www.quirksmode.org/js/cookies.html
     */
    function readCookie(name) {
        var nameEQ = name + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) == 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    }

    /*
     * usefull for appending to urls (esp ajax urls) when the content of the
     * url depends on the user's cookie preferences
     */
    function getCookiePrefToken() {
        try {
            var val = readCookie("crucibleprefs1");
            if (val) {
                val = val.replace(/^D=[0-9]+/, "");
                return encodeURIComponent(val);
            }
        } catch(ex) {
            AJS.log("error getting cookie preference:" +
                            ex.message +
                            "\nin:" + ex.fileName +
                            "\nat:" + ex.lineNumber);
        }
        return "unknown";
    }

    function csDisplaySpinner(revId) {
        var divId = csGetCsDivId(revId);
        AJS.$("#" + divId).html("<div class='loading'><span class='loading'>Loading content</span></div>");
        return divId;
    }

    function csGetCsDivId(revId) {
        return 'showfilesDiv' + revId;
    }

    function scrollToSelectedItem() {
        var index = AJS.$("#selectedIndex").val();
        AJS.$("#content-column-panel").scrollTo(AJS.$("#item" + index));
    }

    /////////////////////
    // exported functions
    /////////////////////

    AJS.FE.CHANGESET.loadDiff = function (revId, url, onCompleteFunc) {
        var $revDiv = AJS.$("#" + revId);
        if ($revDiv.hasClass("unloaded-diff")) {
            var divId = csDisplaySpinner(revId);
            $revDiv.removeClass("unloaded-diff");
            AJS.FECRU.AJAX.ajaxDo(url, {revid: revId, rndtmp: getCookiePrefToken()}, function(resp) {
                if (resp.worked) {
                    // use 'clean' to avoid the regex that jQuery uses to distinguish HTML from ids
                    var replacement = AJS.$.clean([resp.payload], document);
                    AJS.$("#" + divId).html(replacement);
                    scrollToSelectedItem();
                    if (onCompleteFunc) {
                        onCompleteFunc();
                    }
                }
            }, false);
        } else {
            if (onCompleteFunc) {
                onCompleteFunc();
            }
        }
    };

    /**
     * loads a diff asynchronously.
     * @param i
     * @param url
     * @param revIds
     */
    AJS.FE.CHANGESET.loadDiffAsync = function (i, url, revIds) {
        var revIdLength = revIds.length;
        var onComp = function(origRequest) {
            // do the chaining.
            AJS.FE.CHANGESET.loadDiffAsync(i + 1, url, revIds);
        };
        var MAX_TO_LOAD = 100; //so that if viewing changeset of a branch operation then the page wont overload, set a hard limit.
        if ((i < revIdLength) && (i < MAX_TO_LOAD)) {
            var revId = revIds[i];
            AJS.FE.CHANGESET.loadDiff(revId, url, onComp);
        }
    };

    AJS.FE.CHANGESET.setupChangesetDiffLoading = function() {
        AJS.$(document).delegate("a.unloaded-diff", "click", function (e) {
            e.preventDefault();
            var $node = AJS.$(this);
            var id = $node.closest(".diffPaneChangset").attr("id");
            var baseClUrl = AJS.$("#baseJSONClUrl").val();
            AJS.FE.CHANGESET.loadDiff(id, baseClUrl, null);
        });
    };

    AJS.FE.CHANGESET.loadDiffForDt = function($dt) {
        var $revIdDiv = $dt.next().children("div.unloaded-diff");
        // if it is already loaded we won't find that div
        if ($revIdDiv) {
            var baseClUrl = AJS.$("#baseJSONClUrl").val();
            AJS.FE.CHANGESET.loadDiff($revIdDiv.attr("id"), baseClUrl, null);
        }
    };

    var isInitialised = false;

    /**
     * Set a property to indicate that the live events etc for the changeset page have been set up
     * @param newValue
     */
    AJS.FE.CHANGESET.initialised = function(newValue) {
        if (newValue === undefined) {
            return isInitialised;
        }
        isInitialised = newValue;
        return newValue;
    };

    var reloadChangesetPage = function(page, index) {
        if (page < 1 || page > getNoOfPages()) {
            return;
        }
        var csid = AJS.$("#csid").val();
        var currentPage = getCurrentPageNo();
        if (currentPage == page) {
            AJS.$("#selectedIndex").val(index);
            scrollToSelectedItem();
        } else {
            var $spinner = AJS.$("#file-table-spinner");
            $spinner.show();
            var params = {href: AJS.$("#baseClUrl").val(), csid: csid, pageNum: page, index: index};

            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/fe/loadChangesetPage.do", params, function(resp) {
                if (resp.worked) {
                    var replacement = AJS.$.clean([resp.changesetPage], document);
                    AJS.$("#panel-target").replaceWith(replacement);
                    setCurrentPageNo(parseInt(resp.currentPageNo, 10));
                    AJS.$("#selectedIndex").val(resp.selectedIndex);
                    scrollToSelectedItem();
                }
                $spinner.hide();
            }, false);
        }
    };

    function getCurrentPageNo() {
        return parseInt(AJS.$("#currentPageNo").val(), 10);
    }

    function getNoOfPages() {
        return parseInt(AJS.$("#noOfPages").val(), 10);
    }

    function setNavClass(selector, enabled) {
        var links = AJS.$("span.cs-page a").filter(selector);
        if (enabled) {
            links.removeClass("disabled");
        } else {
            links.addClass("disabled");
        }
    }

    function setCurrentPageNo(newPageNo) {
        AJS.$("#currentPageNo").val(newPageNo);
        AJS.$("#currentPageNoDisplay").text(newPageNo);
        setNavClass(".pagination-back, .pagination-first", newPageNo != 1);
        setNavClass(".pagination-next, .pagination-last", newPageNo != getNoOfPages());
    }

    function getAttrValue(node, name) {
        return node.children("input[name='" + name + "']").val();
    }

    AJS.FE.CHANGESET.changesetFileLinkFn = function(event) {
        document.location.hash = getAttrValue(AJS.$(event.target), "anchor");
        var $node = AJS.$(event.target);
        reloadChangesetPage(getAttrValue($node, "page"), getAttrValue($node, "index"));
        return false;
    };

    AJS.FE.CHANGESET.changesetPathLinkFn = AJS.FE.CHANGESET.changesetFileLinkFn;

    AJS.FE.CHANGESET.changesetSubTreeArgs = function($node) {
        return {csid: AJS.$("#csid").val()}
    };

    AJS.FE.CHANGESET.setupPaging = function () {
        // We don't put ":not(.disabled)" in the selector otherwise we can't preventDefault for disabled links
        // (as these handlers won't get triggered)
        var handler = function (event, method) {
            event.preventDefault();
            if (!AJS.$(this).is(".disabled")) {
                var args = arguments.length > 2 ? AJS.$.makeArray(arguments).slice(2) : [];
                method && method.apply(this, args);
            }
        };

        var $document = AJS.$(document);

        $document.delegate("span.cs-page a.pagination-back", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getCurrentPageNo() - 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-next", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getCurrentPageNo() + 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-first", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, 1, 1);
        });
        $document.delegate("span.cs-page a.pagination-last", "click", function (e) {
            handler.call(this, e, reloadChangesetPage, getNoOfPages(), 1);
        });
    };

    /**
     * Sets up the two detail blocks in the Changeset page and establishes the click responses and stickiness
     */
    AJS.FE.CHANGESET.augmentDetails = function () {
        var $details = AJS.$("#changeset-details"),
            $terse = $details.children("h1.details-terse").children("span"),
            $verbose = $details.children("div.details-verbose"),
            $links = $details.find("a"),
            expandCsHeaderPref = 'xcsh'
        ;

        /**
         * Targets the single line "heading" and adds a click response to reveal the full details.
         * The !e.altKey checks the event properties to ensure that the alt/option key isn't being pressed. This
         * facilitates copy + paste actions
         */
        $terse.click(function(e){
            if (!e.altKey && e.target.tagName.toLowerCase() !== "a") {
               $details.addClass('expand');
               AJS.FECRU.PREFS.setPreference(expandCsHeaderPref, 'Y');
            }
        });

        /**
         * Targets the multi line "details" and adds a click response to return to heading only.
         * The !e.altKey checks the event properties to ensure that the alt/option key isn't being pressed. This
         * facilitates copy + paste actions
         * The e.target checks ensure that only the text of the first line of the details will trigger the toggle
         */
        $verbose.click(function(e){
            var target = e.target.tagName.toLowerCase();
            if (!e.altKey && target !== "a" && target !== "div") {
                if (target !== "p" || AJS.$(e.target).is(":first-child")){
                    $details.removeClass('expand');
                    AJS.FECRU.PREFS.setPreference(expandCsHeaderPref, 'N');
                }
            }
        });

        /**
         * stops the link from firing if the user is trying to copy the link and also
         * stops the changelog commit message toggling so it won't bump if you're trying
         * to leave the page.
         */
        $links.click(function(e){
            if (e.altKey) {
                e.preventDefault();
                return true;
            }

            e.stopPropagation();
        });
    };

    /**
     * Maintains the UI show/hide principles by checking to see whether this element has any non-whitespace content
     * and displaying if any is found.
     */

    AJS.FE.CHANGESET.showCustomDetails = function(){
        var $custom = AJS.$("div.changeset-header-custom"),
            $customDetails = $custom.children(".custom"),
            details = false
        ;
        details = $customDetails.html().replace(/\s/g,"");

        if (details.length > 0) {
            $custom.show();
        }
    };
})();
;
/* END /2static/script/fe/fisheye-changeset.js */
/* START /2static/script/fe/fisheye-history.js */
(function($) {
    var revisionForRow = function($e) {
        return $e.children("input.revision-check").val();
    };

    var revisionIdForRow = function($e) {
        return $e.children("input.revision-check").attr("id").replace(/^\D+/,"");
    };

    var rowForRevisionId = function(revisionId) {
        return $("#revision-" + revisionId).closest(".revision");
    };

    // Cache the focused row as it can be too slow to do $("tr.history-focus");
    var $focusedRow = undefined;
    var setRowFocus = function(revisionData) {
        if ($focusedRow) {
            $focusedRow.removeClass("history-focus");
        }
        $focusedRow = rowForRevisionId(revisionData.id);
        $focusedRow.addClass("history-focus");
        revisionData.focussed = true;
    };

    var revisionDataForId = function (revisionId) {
        var arrayIndex = historyTableData.revisionKeyValueIndexMap[revisionId];
        return historyTableData.revisions[arrayIndex];
    };

    var hasSetupDiffCheckboxes = false;
    /**
     * Binds a live event to validate that only two diff checkboxes can be selected at a time.
     *
     * This method will short circuit if it is called more than once (since we don't want to bind the same live event
     * multiple times).
     */
    var setupDiffCheckboxes = function() {
        if (hasSetupDiffCheckboxes) {
            return;
        }
        hasSetupDiffCheckboxes = true;

        var queue = []; // Queue of checkbox ids in the order that they were checked (oldest -> newest)

        $(document).delegate("#history-revisions input.revision-check", "click", function(event) {
            var $r1 = $("#rev1");
            var $r2 = $("#rev2");

            var id = this.id;
            var value = this.value;

            var revisionId = historyTableData.revisionToRevisionIdMap[value];
            var revisionData = revisionDataForId(revisionId);
            revisionData.checked = !!this.checked;

            var queueLength = queue.length;

            // Nothing is checked, push the id onto the back
            if (queueLength == 0) {
                queue.push(id);
                $r1.val(value);
            }
            // Otherwise, we need to bump the last stack value and shuffle down
            else {
                $r1.val("");
                $r2.val("");

                var removed;
                var queuePosition = $.inArray(id, queue);
                if (queuePosition >= 0) {
                    removed = queue.splice(queuePosition, 1);
                } else {
                    // add the new id to the end of the queue
                    queue.push(id);

                    // if the queue is more than two elements, take off the first n-2 elements
                    if (queue.length > 2) {
                        removed = queue.splice(0, queue.length - 2);
                    }
                }

                if (removed) {
                    for (var i = 0, l = removed.length; i < l; i++) {
                        var rem = removed[i];
                        var removedRevId = rem.replace(/^\D+/, "");
                        revisionData = revisionDataForId(removedRevId);
                        revisionData.checked = false;
                        $("#"+rem).removeAttr("checked");
                    }
                }

                if (queue.length == 2) {
                    $r1.val(revisionDataForId(queue[0].replace(/^\D+/, "")).revision);
                    $r2.val(revisionDataForId(queue[1].replace(/^\D+/, "")).revision);
                }
            }

            var $diffButton = $("#diff-selected-button");
            if ($r1.val() && $r2.val()) {
                $diffButton
                    .removeAttr("disabled")
                    .attr("title", "View the diff between " + $r1.val() + " and " + $r2.val())
                    .closest(".toolbar-item")
                        .removeClass("disabled");
            } else {
                $diffButton
                    .attr("disabled", "disabled")
                    .attr("title", "Select two revisions to view their diff")
                    .closest(".toolbar-item")
                        .addClass("disabled");
            }

            event.stopPropagation();
        });
    };

    var setupFocusedRevision = function() {
        var location = document.location.hash;
        if (!location) {
            return null;
        }
        // remove the '#r' from the anchor
        var revision = location.substring(2);
        var revisionId = historyTableData.revisionToRevisionIdMap[revision];
        if (!revisionId) {
            return null;
        }

        var revisionData = revisionDataForId(revisionId);
        setRowFocus(revisionData);
        $(window).scrollTo(rowForRevisionId(revisionId), {
            duration: 500,
            axis: 'y',
            offset: -50
        });
        return revisionId;
    };

    var CURRENT_PAGE = 1;
    var REVISIONS_PER_PAGE = 30;

   var getTotalPageCount = function () {
        return Math.ceil(historyTableData.revisions.length / REVISIONS_PER_PAGE);
    };

    /**
     * Ensures that the navigation buttons (next/previous revisions) are synchronised -- ie, that they are enabled/disabled
     * when required.
     */
    var syncPaginationButtons = function() {
        var $firstLink = $("a.pagination-first");
        var $prevLink = $("a.pagination-back");
        var $nextLink = $("a.pagination-next");
        var $lastLink = $("a.pagination-last");

        var totalPageCount = getTotalPageCount();
        var show = true;

        if (totalPageCount <= 1) {
            $firstLink.addClass("disabled");
            $prevLink.addClass("disabled");
            $nextLink.addClass("disabled");
            $lastLink.addClass("disabled");
            show = false;
        } else if (CURRENT_PAGE <= 1) {
            $firstLink.removeClass("disabled");
            $prevLink.removeClass("disabled");
            $nextLink.addClass("disabled");
            $lastLink.addClass("disabled");
        } else if (CURRENT_PAGE >= totalPageCount) {
            $firstLink.addClass("disabled");
            $prevLink.addClass("disabled");
            $nextLink.removeClass("disabled");
            $lastLink.removeClass("disabled");
        } else {
            $firstLink.removeClass("disabled");
            $prevLink.removeClass("disabled");
            $nextLink.removeClass("disabled");
            $lastLink.removeClass("disabled");
        }
        var topPagination = $("span.taskbar-pagination");
        var bottomPagination = $("div.toolbar-bottom");
        if (show) {
            topPagination.show();
            bottomPagination.show();
        } else {
            topPagination.hide();
            bottomPagination.hide();
        }

        $(".pagination-text").text(CURRENT_PAGE+"/"+totalPageCount);
    };

    /**
     * Loads revisions into the history table for display. This method will lazily load extra information from the server
     * if it isn't available locally (such as rendered commit messages).
     * @param pageNumber page to render 
     */
    var loadRevisionsPage = function(pageNumber) {
        pageNumber = pageNumber || 1;
        CURRENT_PAGE = pageNumber;

        var totalPageCount = getTotalPageCount();
        if (pageNumber > totalPageCount) {
            pageNumber = totalPageCount;
        }

        var allRevisions = historyTableData.revisions;
        var $historyTable = $("#history-revisions");
        $historyTable
                .removeClass("data-loaded")
                .addClass("data-loading")
                .empty();
        var startPoint = (pageNumber-1) * REVISIONS_PER_PAGE;
        if (startPoint < 0) {
            startPoint = 0;
        }
        var endPoint = startPoint + REVISIONS_PER_PAGE;

        var revIdsToLoad = [];

        var $revisionElementsToInsert = $([]);

        for (var c = 0, i = startPoint, revCount = allRevisions.length;
             i < endPoint && i < revCount;
             i++, c++) {
            var rev = allRevisions[i];
            var $revElem = $("<div class='revision'>" + rev.htmlContent + "</div>");

            var expandableComment = true;
            var $comment = $revElem.find(".revision-message");
            // this means the comment should not be expandable
            if ($comment.children("p").length == 1) {
                $revElem.addClass("one-line-comment");
                expandableComment = false;
            }

            if (rev.collapsed) {
                $revElem.addClass("revision-collapsed");
                if (expandableComment) {
                    collapseCommentText($revElem);
                }
            } else if (expandableComment) {
                expandCommentText($revElem);
            }
            if (rev.focussed) {
                $revElem.addClass("history-focus");
            }

            $revisionElementsToInsert.push($revElem.get(0));
            if (!rev.loaded) {
                revIdsToLoad.push(rev.id);
            }
        }

        $historyTable.append($revisionElementsToInsert);

        var onDone = function () {
            $historyTable
                .addClass("data-loaded")
                .removeClass("data-loading");

            if (!$focusedRow || $focusedRow.length == 0) {
                setupFocusedRevision();
            }
        };

        syncPaginationButtons();

        loadExtraRevisionData(revIdsToLoad, onDone);
    };

    /**
     * Sends a list of revisions to the server for extra data which is required for displaying complete data for each
     * revision to the user. Upon completion of the ajax call, retrieved data is stored locally for later reuse.
     * @param revisions array of revisions to complete
     * @param onDone callback function 
     */
    var loadExtraRevisionData = function (revisions, onDone) {
        if (!revisions || revisions.length == 0) {
            onDone && onDone();
            return;
        }
        var done = function (resp) {
            var revs = resp.revisions;
            if (!revs) {
                return;
            }
            for (var i = 0, len = revs.length; i < len; i++) {
                var rev = revs[i];

                // Update the data structure
                var revisionData = revisionDataForId(rev.id);
                revisionData.loaded = true;

                // Update the row
                var $row = rowForRevisionId(rev.id);
                var $comment = $row.find(".revision-message").html(rev.comment);
                if ($comment.children("p").length > 1) {
                    $row.removeClass("one-line-comment");
                } else {
                    $row.addClass("one-line-comment");
                }
                $row.append(rev.fullDetails);

                revisionData.htmlContent = $row.html();                
            }
            onDone && onDone();
        };

        var params = {
            content: true,
            rev: revisions
        };

        var pathName = window.location.pathname;
        pathName = pathName.substring(fishEyePageContext.length);
        var url = fishEyePageContext + "/json" + pathName;

        AJS.FECRU.AJAX.ajaxDo(url, params, done);
    };

    /**
     * Create a map between each revision id and it's index in the {{historyTableData.revisions}} array. This method
     * should be called whenever the order of the revisions data is changed, such as on sorting.
     *
     * This data structure is used so that we can efficiently look up revisionData objects and update them as we lazily
     * retrieve expensive data from the server.
     */
    var createRevisionKeyIndexMap = function() {
        var allRevisions = historyTableData.revisions;
        var revisionKeyValueIndexMap = {};
        var revisionToRevisionIdMap = {};
        if (allRevisions) {
            for (var i = 0, len = allRevisions.length; i < len; i++) {
                var rev = allRevisions[i];
                revisionKeyValueIndexMap[rev.id] = i;
                revisionToRevisionIdMap[rev.revision] = rev.id;
            }
        }
        historyTableData.revisionKeyValueIndexMap = revisionKeyValueIndexMap;
        historyTableData.revisionToRevisionIdMap = revisionToRevisionIdMap;
    };

    var expandCommentText = function ($container) {
        if (!$container.is(".revision-board")) {
            $container = $container.find(".revision-board");
        }
        $container
            .removeClass("revision-terse")
            .addClass("revision-verbose");
    };

    var toggleRevisionDetails = function ($twixie, setOpened) {
        var collapsedClass = "revision-collapsed";
        var $revBlock = $twixie.closest(".revision");
        var revData = revisionDataForId(revisionIdForRow($revBlock));
        if (setOpened === undefined) {
            setOpened = $revBlock.hasClass(collapsedClass);
        }
        if (setOpened) {
            $revBlock.removeClass(collapsedClass);
            $twixie.text("less");
            expandCommentText($revBlock);
        } else {
            $revBlock.addClass(collapsedClass);
            $twixie.text("more");
            collapseCommentText($revBlock);
        }

        revData.collapsed = !setOpened;
    };


    var collapseCommentText = function ($container) {
        if (!$container.is(".revision-board")) {
            $container = $container.find(".revision-board");
        }
        $container
            .addClass("revision-terse")
            .removeClass("revision-verbose");
    };

    AJS.FE.setupFileHistoryPage = function () {
        createRevisionKeyIndexMap();

        setupDiffCheckboxes();

        var pageNumberToLoad = 1; // We start at 1
        var revisionIdToLoad = setupFocusedRevision();
        if (revisionIdToLoad) {
            var arrayIndexToLoad = historyTableData.revisionKeyValueIndexMap[revisionIdToLoad];
            pageNumberToLoad = Math.floor(arrayIndexToLoad / REVISIONS_PER_PAGE) + 1; // pageNumber starts at 1
        }

        loadRevisionsPage(pageNumberToLoad);

        $("a.pagination-back").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(CURRENT_PAGE+1); // next page
            return false;
        });
        $("a.pagination-next").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(CURRENT_PAGE-1); // previous page
            return false;
        });
        $("a.pagination-last").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            loadRevisionsPage(1); // last page
            return false;
        });
        $("a.pagination-first").click(function () {
            if ($(this).hasClass("disabled")) {
                return false;
            }
            var totalPageCount = getTotalPageCount();
            loadRevisionsPage(totalPageCount); // first page
            return false;
        });

        var $historyRevisions = $("#history-revisions");

        $historyRevisions.delegate(":not(.one-line-comment) .revision-toggle", "click", function () {
            var $commentWrapper = $(this).parent();
            if ($commentWrapper.hasClass("revision-terse")) {
                expandCommentText($commentWrapper);
            } else {
                collapseCommentText($commentWrapper);
            }
        });

        $("#expand-all-revisions").click(function () {
            var allRevisions = historyTableData.revisions;
            if (!allRevisions || allRevisions.length === 0) {
                return;
            }

            var $this = $(this).parent();
            var shouldExpand = !$this.hasClass("active");
            $historyRevisions.find(".revision-twixie").each(function () {
                toggleRevisionDetails($(this), shouldExpand);
            });

            if (shouldExpand) {
                $this.addClass("active");
            } else {
                $this.removeClass("active");
            }

            for (var i = 0, len = allRevisions.length; i < len; i++) {
                var rev = allRevisions[i];
                rev.collapsed = !shouldExpand;
            }
        });

        $("#history-revisions").delegate(".revision-twixie", "click", function () {
            toggleRevisionDetails($(this));
        }).delegate(".revision-plus a", "click", function () {
            var $p = $(this).parent();
            $p.siblings(".hidden").removeClass("hidden");
            $p.hide();
        });

        // handle IE7 z-index bug for positioned elements
        if ($.browser.msie && $.browser.version < 8) {
            $(".aui-dd-link").live("click", function() {
                var $this = $(this);

                // restore the default z-index for all the revisions
                $(".revision.ie-zindex-fixer").removeClass("ie-zindex-fixer");

                // make the parent revision above the subsequent one(s) and the dropdown above the subsequent "more" button
                var $revision = $this.closest(".revision").addClass("ie-zindex-fixer");

                // bind a IE-specific event handler to know when the dropdown becomes hidden
                // (unbind first to prevent duplicate bindings if the user clicks several times)
                var dropdown = $(this).siblings(".aui-dropdown");
                dropdown.unbind("propertychange").bind("propertychange", function() { // does not work with .live()
                    var $dropdown = $(this);
                    if ($dropdown.is(":not(:visible)")) {
                        $revision.removeClass("ie-zindex-fixer");
                        $dropdown.unbind("propertychange");
                    }
                });
            });
        }

        // setup local-link links
        $(document).delegate("a.local-link", 'click', function() {
            var href = $(this).attr("href");
            var revision = href.substring(href.lastIndexOf("#") + 2);
            var revisionId = historyTableData.revisionToRevisionIdMap[revision];
            var revisionData = revisionDataForId(revisionId);
            if (revisionId) {
                setRowFocus(revisionData);
            }
        });
    };

    // Following methods not used for the file history - move them elsewhere
    AJS.FE.fileHistoryPathLinkFn = function(event) {
        event.stopPropagation();
        return true;
    };
})(AJS.$);
;
/* END /2static/script/fe/fisheye-history.js */
/* START /script/activitystream.js */
//todo: refactor all these string manips into using real js objects with explicit property names.

function loadActivityStream(viewParam, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam
    };
    updateActivityStreamImpl(mergeMaps([paramsMap,pagingMap]), "activity", callbackParams);
}

function loadActivityStreamForProject(id, viewParam, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam,
        project:id
     };
    updateActivityStream(mergeMaps([paramsMap,pagingMap]), callbackParams);
}

function loadFileActivityStream(id, path, repname, viewParam, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam,
        p:path,
        repname:repname
    };
    updateActivityStreamImpl(mergeMaps([paramsMap,pagingMap]), id, callbackParams);
}

//todo: add max item constraint for changelog view
function loadDirActivityStream(id, path, repname, viewPara, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewPara,
        p:path,
        repname:repname
    };
    updateActivityStreamImpl(mergeMaps([paramsMap,pagingMap]), id, callbackParams);
}

function loadDownStreamStarActivity(viewParam, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam,
        star:"star",
        home:"home"
    };
    updateActivityStream(mergeMaps([paramsMap,pagingMap]), callbackParams);
}

function loadActivityStreamForUser(username, viewParam, callbackParams) {
    var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam,
        username:username
    };
    updateActivityStream(mergeMaps([paramsMap,pagingMap]), callbackParams);
}

function loadActivityStreamForCommitter(committer, repname, viewParam, callbackParams) {
var pagingMap = constructPagingParamsMap();
    var paramsMap = {
        type:"ajax",
        view:viewParam,
        committer:committer,
        repname:repname
    };
    updateActivityStream(mergeMaps([paramsMap,pagingMap]), callbackParams);
}

function updateActivityStream(paramMap, callbackParams) {
    updateActivityStreamImpl(paramMap, 'activity', callbackParams);
}

function pageActivityStream(forwards, id, callbackParams) {
    updateActivityStreamImpl(toParamMap(forwards ? nextPageParams : prevPageParams), id, callbackParams);
    var $elem = AJS.$("#"+id + " #stream");
    var spinnerUrl = fishEyePageContext + "/" + fishEyeSTATICDIR + "/2static/images/spinner_003366.gif";
    $elem.html('<div><img src="' +spinnerUrl+ '" alt="loading" width="13" height="13">loading...</div>');
}


function updateActivityStreamImpl(params, id, callbackParams) {
    var url = fishEyePageContext + '/json/changelog';
    var done = function(resp) {
        var $elem = AJS.$('#' + id);
        try {
            if (resp.worked) {
//                $elem.html(resp.html);
                // the replaceWith call is a hack to get around an IE7 bug, which makes the .html() call fail.
                $elem.replaceWith("<div id='activity'>" + resp.html + "</div>");
                var $toolbar = AJS.$("#activitystream-toolbar");
                $toolbar.replaceWith("<div id='activitystream-toolbar'>" + resp.activityStreamToolbar + "</div>");
                prevPageParams = resp.prevPageParams;
                nextPageParams = resp.nextPageParams;
                thisPageParams = resp.thisPageParams;
                view = resp.view ? resp.view : "all";
                if (resp.onFinishedLoad) {
                    resp.onFinishedLoad(callbackParams);
                }
                AJS.FECRU.UI.initStream();
                var $newElem = AJS.$('#' + id);
                AJS.FECRU.HOVER.addAllLinkPopups($newElem);
            } else {
                handleError(resp, $elem, null);
            }
        } catch (error) {
            handleError(resp, $elem, error);
        }
    };
    AJS.$.getJSON(url, params, done);
}

function handleError(resp, $elem, error) {
    if (error) {
        $elem.html("<div style='cursor:pointer;' onclick=\"AJS.$('#activityStreamErrorBox').toggle();\">" +
                   "Error, cannot complete request. <br>" +
                   "<div id='activityStreamErrorBox' class='ajaxError' style='display:none;cursor:pointer;'>" +
                   "details:" + error.message + "<br>" +
                   "url:" + error.fileName + "<br>" +
                   "line:" + error.lineNumber + "<br>" +
                   "error:<br>" +
                   (function() {
                               var str;
                               for (var k in error) {
                                   str += "error[" + k + "]=" + error[k] + "<br>";
                               }
                               return str;
                   }()) + "<br>" +
                   "</div>" +
                   "</div>");
    } else {
        $elem.html("An activity stream error has occurred.<br>");
    }
}

//implementation of the prototype toQueryParam() method.
//does NOT support queryString of the type key1=val1&key1=val2&key1=val3 -> {key1:[val1, val2, val3]}
function toParamMap(paramString) {
    var KEY = 0;
    var VAL = 1;
    var paramMap = {};
    var cleanedParamString = paramString.charAt(0) == '?' ? paramString.substring(1, paramString.length)
                                                          : paramString;
    var paramPairs = cleanedParamString.split('&');
    for (var i = 0; i < paramPairs.length; i++) {
        if (paramPairs[i].length > 0) {
            var subpair = paramPairs[i].split('=', 2);
            paramMap[subpair[KEY]] = subpair[VAL];
        }
    }
    return paramMap;
}

function constructPagingParamsMap() {
    var originalParams = toParamMap(thisPageParams);
    var keys = ["maxDate","minDate","direction","prevAnchor","nextAnchor"];
    var cleanedParams = {};
    for (var i = 0; i < keys.length; i++) {
        if (originalParams[keys[i]]) {
            cleanedParams[keys[i]] = originalParams[keys[i]];
        }
    }
    return cleanedParams;
}

/**
 *
 * @param maps an array of javascript map to merge together.
 * Be wary that if two maps have the same key, only one of the value is preserved,
 * and it is not specified which one will be preserved.
 */
function mergeMaps(maps) {
    var merged = {};
    for (var i = 0 ; i < maps.length ; i++) {
        var m = maps[i];
        for (var key in m) {
            if (m.hasOwnProperty(key)) {
                merged[key] = m[key];
            }
        }
    }
    return merged;
}

var prevPageParams = "";
var nextPageParams = "";
var thisPageParams = "";
var view = "";
;
/* END /script/activitystream.js */
/* START /2static/script/lib/json2.min.js */
/* json2.js
 * 2008-01-17
 * Public Domain
 * No warranty expressed or implied. Use at your own risk.
 * See http://www.JSON.org/js.html
*/
if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;}
Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;}
c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+
(c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
if(typeof value.toJSON==='function'){return stringify(value.toJSON());}
a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;i<l;i+=1){a.push(stringify(value[i],whitelist)||'null');}
return'['+a.join(',')+']';}
if(whitelist){l=whitelist.length;for(i=0;i<l;i+=1){k=whitelist[i];if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}else{for(k in value){if(typeof k==='string'){v=stringify(value[k],whitelist);if(v){a.push(stringify(k)+':'+v);}}}}
return'{'+a.join(',')+'}';}}
return{stringify:stringify,parse:function(text,filter){var j;function walk(k,v){var i,n;if(v&&typeof v==='object'){for(i in v){if(Object.prototype.hasOwnProperty.apply(v,[i])){n=walk(i,v[i]);if(n!==undefined){v[i]=n;}}}}
return filter(k,v);}
if(/^[\],:{}\s]*$/.test(text.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,''))){j=eval('('+text+')');return typeof filter==='function'?walk('',j):j;}
throw new SyntaxError('parseJSON');}};}();};
/* END /2static/script/lib/json2.min.js */
/* START /2static/script/cru/util.js */
/**
 * Crucible utility functions that can be used from any crucible page.
 */

if (AJS.CRU === undefined) {
    AJS.CRU = {};
}
AJS.CRU.UTIL = {};

(function ($) {
    var cruUtil = AJS.CRU.UTIL;
    /**
     * Crucible base url (without trailing '/').
     *
     * @param permaId optional review id
     */
    cruUtil.urlBase = function (permaId) {
        if (permaId) {
            return fishEyePageContext + '/cru/' + permaId;
        } else {
            return fishEyePageContext + '/cru';
        }
    };

    /**
     * Crucible JSON base url (without trailing '/').
     *
     * @param permaId optional review id
     */
    cruUtil.jsonUrlBase = function (permaId) {
        if (permaId) {
            return fishEyePageContext + '/json/cru/' + permaId;
        } else {
            return fishEyePageContext + '/json/cru';
        }
    };
    
    cruUtil.isReviewPage = function () {
        return typeof review !== 'undefined';
    };


    cruUtil.startAjaxDialogSpin = function () {
        $('body').addClass('ajax-dialog');
        AJS.dim();
    };

    cruUtil.stopAjaxDialogSpin = function () {
        AJS.undim();
        $('body').removeClass('ajax-dialog');
    };

    cruUtil.isAjaxDialogSpinning = function () {
        return cruUtil.isDimmed() && $('body').hasClass('ajax-dialog');
    };

    cruUtil.ajaxDialog = function (url, params) {
        cruUtil.startAjaxDialogSpin();
        AJS.FECRU.AJAX.ajaxDo(url, params || {}, function (resp) {
            if (resp.worked) {
                if (resp.showDialog) {
                    cruUtil.stopAjaxDialogSpin();
                    try {
                        AJS.CRU.DIALOG.$CONTAINER.html(resp.payload);
                        AJS.FECRU.DIALOG.triggerAjaxDialogLoaded();
                    } catch (e) {
                        alert(e);
                    }
                } else if (resp.redirect) {
                    // no need to undim
                    window.location = resp.payload;
                }
            }
            // !resp.worked handled by ajaxDo
        });
        return false;
    };

    cruUtil.stateTransition = function (transition, permaId, params) {
        var util = cruUtil;
        var url = util.jsonUrlBase(permaId) + '/changeStateAjax';
        var unsaved = AJS.CRU.UNSAVED;

        // Make sure there aren't any unsaved inputs on the page, and if there are, then provide a warning
        // and give the user the ability to cancel and save their inputs.
        if (unsaved) {
            if (!unsaved.confirmUnsubmittedInputs()) {
                return;
            }
            unsaved.clearWatchForUnsavedChanges();
        }

        params = params || {};

        $.extend(params, {
            command: transition
        });

        if (util.isReviewPage() && $.inArray(transition, ['action:completeReview', 'action:summarizeReview']) >= 0) {
            // If completing or summarizing we need to warn if the review has been updated.

            util.startAjaxDialogSpin();
            AJS.CRU.REVIEW.UTIL.reviewUpdatedAjax({
                done: function () {
                    var reviewUpdated = $('body').hasClass('review-updated');
                    if (reviewUpdated) {
                        AJS.CRU.REVIEW.UTIL.warnAboutReviewUpdates({ reshowWarning: true });
                    }
                    util.stopAjaxDialogSpin();
                    return util.ajaxDialog(url, $.extend(params, { reviewUpdated: reviewUpdated }));
                }
            });
            return false;

        } else {
            return util.ajaxDialog(url, params);
        }
    };

    cruUtil.editDetailsFormChange = false;
    cruUtil.checkEditForm = function (done) {
        if (cruUtil.editDetailsFormChange) {
            AJS.CRU.REVIEW.UTIL.postEditDetailsForm(done);
        } else {
            if (done) {
                done();
            }
        }
        return false;//do a link action if called from <a>
    };

    cruUtil.command = function (cmd, pid, button) {
        var perma = pid || permaId;
        if (button) {
            button.disabled = true;
        }
        var donext = function() {
            var url = cruUtil.urlBase(perma);
            window.location = url + '/' + cmd;
        };
        //check and post the editDetailsForm if it has changed
        cruUtil.checkEditForm(donext);
    };


    cruUtil.createBlankReview = function (params) {
        var url = cruUtil.jsonUrlBase() + '/createReviewDialog';
        return cruUtil.ajaxDialog(url, params);
    };

    cruUtil.addToReview = function (params) {
        var url = cruUtil.jsonUrlBase() + '/createDialog';
        return cruUtil.ajaxDialog(url, params);
    };

    cruUtil.isAnyDialogShowing = function () {
        return $('body').children('div.dialog:visible').length > 0;
    };

    cruUtil.isDimmed = function () {
        return !!AJS.dim.dim;
    };

    cruUtil.makeCssRule = function (selector, ruleBody) {
        var styleSheet = document.styleSheets[0];
        var index = 0;
        if (styleSheet.insertRule) {
            styleSheet.insertRule(selector + '{' + ruleBody + '}', index);
            return styleSheet.cssRules[index].style;
        } else {
            // Internet Explorer's version.
            styleSheet.addRule(selector, ruleBody, index);
            return styleSheet.rules[index].style;
        }
    };

    cruUtil.createIdeSrc = function(linkUrl, frxId) {
        var src = linkUrl + '&id=' + Math.floor(Math.random()*1000);
        if (frxId) {
            src += '&line=' + AJS.CRU.REVIEW.UTIL.getTopVisibleLineNumber(frxId);
        }
        return src;
    };

})(AJS.$);
;
/* END /2static/script/cru/util.js */
/* START /2static/script/cru/dialog/dialog.js */
if (!AJS.CRU) {
    AJS.CRU = {};
}
AJS.CRU.DIALOG = {};

(function ($) {

    AJS.CRU.DIALOG.$CONTAINER = $('<div id="ajax-dialog-container"></div>');
    $(document).ready(function () {
        $('body').append(AJS.CRU.DIALOG.$CONTAINER);
    });

    AJS.CRU.DIALOG.ajaxDialog = function (maxWidth, maxHeight, data, id) {
        var dialog = AJS.FECRU.DIALOG.create(maxWidth, maxHeight, id);
        $.each(
            $.extend({ dialog: dialog }, data),
            function (k, v) {
                AJS.CRU.DIALOG.$CONTAINER.data(k, v);
            }
        );
        return dialog;
    };

})(AJS.$);
;
/* END /2static/script/cru/dialog/dialog.js */
/* START /2static/script/cru/dialog/dialog-event.js */
(function () {

    var $container = AJS.CRU.DIALOG.$CONTAINER;

    var dialogHandler = function (callbacks) {
        return function (event) {
            var dialog = $container.data('dialog');
            var permaId = $container.data('permaId');

            if (AJS.CRU.UTIL.isReviewPage() && callbacks.review) {
                return callbacks.review(dialog, permaId, event);
            } else if (!AJS.CRU.UTIL.isReviewPage() && callbacks.external) {
                return callbacks.external(dialog, permaId, event);
            }
        };
    };

    var viewFiltersHandler = function (filters) {
        return dialogHandler({
            review: function (dialog) {
                if (AJS.$('body').hasClass('review-updated')) {
                    window.location.hash = 'f-' + filters.join(',');
                    window.location.reload(true);
                } else {
                    AJS.CRU.REVIEW.UTIL.filterAndExpandFrxs(filters);
                    dialog.remove();
                }
            },
            external: function (dialog, permaId) {
                window.location = AJS.CRU.UTIL.urlBase(permaId) + '#f-' + filters.join(',');
            }
        });
    };

    var batchProcessDraftComments = function (action, permaId, onComplete) {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/draftCommentsAjax';
        var params = {
            action: action
        };
        var done = function (resp) {
            if (onComplete) {
                onComplete(resp);
            }
        };
        AJS.FECRU.AJAX.ajaxDo(url, params, done, true);
    };

    var draftCommentsHandler = function (action) {
        return dialogHandler({
            review:  function (dialog) {
                AJS.$.each(review.draftComments(), function (i, comment) {
                    commentator[action + 'Comment'](comment.id(), review.id());
                });
                if (action == 'delete') {
                    AJS.$("#dialog-drafts-message").html("Your draft comments have been deleted.");
                } else if (action == 'publish') {
                    AJS.$("#dialog-drafts-message").html("Your draft comments have been posted.");
                }
                AJS.$('#dialog-drafts-links').hide();
            },
            external: function (dialog, permaId) {
                batchProcessDraftComments(action, permaId, function (resp) {
                    if (resp.worked) {
                        AJS.$('#dialog-drafts-links').hide();
                    } else {
                        // TODO Show error message within dialog panel.
                    }
                });
            }
        });
    };

    var resolveUnresolvedJiras = function () {
        var doIt = function(dialog, permaId) {
            AJS.$('#dialog-unresolved-jiras-controls').hide();
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/resolveAllSubtasksAjax';
            var params = {};
            AJS.$('#dialog-unresolved-jiras-spinner').show();
            var done = function (resp) {
                AJS.$('#dialog-unresolved-jiras-spinner').hide();
                if (resp.worked) {
                    AJS.$('#dialog-unresolved-jiras-title').html("There are no unresolved subtasks.").show();
                } else {
                    AJS.$('#dialog-unresolved-jiras-results').addClass("jira-error").html(resp.errorMsg).show();
                }
            };
            AJS.FECRU.AJAX.ajaxDo(url, params, done, true);
        };
        return dialogHandler({review: doIt, external: doIt});
    };

    AJS.$(document).ready(function () {
        var $document = AJS.$(document);
        $document.delegate('#dialog-view-drafts', 'click', viewFiltersHandler(['draftcomments']));
        $document.delegate('#dialog-view-unread-comments', 'click', viewFiltersHandler(['unreadcomments']));
        $document.delegate('#dialog-view-incomplete-frxs', 'click', viewFiltersHandler(['incomplete']));
        $document.delegate('#dialog-view-unresolved-jiras', 'click', viewFiltersHandler(['unresolvedsubtasks']));

        $document.delegate('#dialog-delete-drafts', 'click', draftCommentsHandler('delete'));
        $document.delegate('#dialog-post-drafts', 'click', draftCommentsHandler('publish'));
        $document.delegate('#dialog-resolve-unresolved-jiras', 'click', resolveUnresolvedJiras());
    });

})();
;
/* END /2static/script/cru/dialog/dialog-event.js */
/* START /2static/script/cru/review/email-comments.js */
if (!AJS.CRU) {
    AJS.CRU = {};
}
if (!AJS.CRU.REVIEW) {
    AJS.CRU.REVIEW = {};
}
if (!AJS.CRU.REVIEW.EMAILCOMMENTS) {
    AJS.CRU.REVIEW.EMAILCOMMENTS = {};
}

(function() {
    var dialog;
    var self = AJS.CRU.REVIEW.EMAILCOMMENTS;

    var nextButton = function () {};
    var previousButton = function () {};

    self.showPage = function (pageId) {

        AJS.$(".email-wizard", "#emailWizardDialog").each(function() {
            if (AJS.$(this).is('#' + pageId)) {
                AJS.$(this).show();
            } else {
                AJS.$(this).hide();
            }
        });
        AJS.$("#emailWizardDialog").find(".previousButton, .nextButton, .cancelButton").each(function() {
            AJS.$(this)
                .attr('disabled', false)
                .show();
        });

        if (pageId === 'recipients') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Next");
            nextButton = function () {
                self.showPage('message');
            };
            AJS.$(".previousButton", "#emailWizardDialog")
                    .hide();
            previousButton = function () {};

        } else if (pageId === 'message') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Send");
            nextButton = function () {
                var $form = AJS.$("#emailWizardDialog form#emailCommentsForm");
                var url = $form.attr('action');
                AJS.$(".nextButton", "#emailWizardDialog").attr("disabled", true);
                AJS.FECRU.AJAX.ajaxUpdate(url, $form.serialize(), "dialogPanel");
            };
            previousButton = function () {
                self.showPage('recipients');
            };

        } else if (pageId === 'sent') {
            AJS.$(".nextButton", "#emailWizardDialog").html("Close");
            nextButton = function () {
                dialog.remove();
            };
            previousButton = function () {
                self.showPage('message');
            };
        }
    };

    self.start = function (permaId) {

        dialog = AJS.CRU.DIALOG.ajaxDialog(900, 600, {}, "emailWizardDialog")
            .addHeader("Email Review Comments")
            .addPanel("Recipients", "<div id='dialogPanel'>Loading...</div>")
            .addButton("Cancel", function(dialog) {
                dialog.remove();
            }, "cancelButton")
            .addButton("Previous", function() {
                previousButton();
            }, "previousButton")
            .addButton("Next", function() {
                nextButton();
            }, "nextButton")
            .show();

        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/allcommentsemail";

        var ajax = AJS.FECRU.AJAX;
        ajax.startSpin("dialogPanel", "email-spinner");
        ajax.ajaxUpdate(url, {}, "dialogPanel", function () {
            ajax.stopSpin(AJS.$('#dialogPanel'));
            AJS.$("form#emailCommentsForm").find("#to").focus();
        });
    };
} )();
;
/* END /2static/script/cru/review/email-comments.js */
/* START /2static/script/cru/review/review-history.js */
if (!AJS.CRU) {
    AJS.CRU = {};
}
if (!AJS.CRU.REVIEW) {
    AJS.CRU.REVIEW = {};
}
if (!AJS.CRU.REVIEW.HISTORY) {
    AJS.CRU.REVIEW.HISTORY = {};
}

AJS.CRU.REVIEW.HISTORY.showPage = function (permaId) {
    AJS.dropDown.current.hide();

    if (!permaId) {
        AJS.log("I don't know which review to give you history for.");
        return;
    }

    var dialog = AJS.FECRU.DIALOG.create(1200, 700, "cru-review-history-dialog");

    var iframeStyle = "style='width:100%;height:" + (dialog.height - 43 - 44 - 23) + "px'";
    var cs = "<iframe frameborder='0' id='reviewHistoryIframe' name='reviewHistoryIframe' src='" + AJS.CRU.UTIL.urlBase(permaId) + "/reviewHistoryWrapper" + "' " + iframeStyle + "></iframe>";

    var header = "History of Review " + permaId;
    dialog.addHeader(header)
            .addPanel("History", cs);

    dialog.addButton("Done", function(dialog) {
        dialog.hide();
    }).show();

    AJS.$("#cru-review-history-dialog").data("dialog", dialog);//stores the object so we can access it from its contents
};

AJS.CRU.REVIEW.HISTORY.setupLinks = function() {
    var $document = AJS.$(document);
    $document.delegate("a.action-link", "click", function () {
        parent.top.AJS.$("#cru-review-history-dialog").data("dialog").hide();
    });
    $document.delegate("a.comment-link", "click", function () {
        var comment_nav = parent.top.AJS.CRU.COMMENT.NAV;
        var comment_id = AJS.$(this).attr('hash').replace('#c', '');

        var scrollToMap = comment_nav.navigateFindComment({ commentId: comment_id });
        comment_nav.navigateDirectlyToElement({ commentId: comment_id }, scrollToMap);
    });
    $document.delegate("a.frx-link", "click", function () {
        var frx_id = AJS.$(this).attr('hash').replace('#CFR-','');

        parent.top.AJS.CRU.FRX.NAV.gotoFrx({ frxId: frx_id , destination: '' });
    });

};
;
/* END /2static/script/cru/review/review-history.js */

