/* CONCAT of
/script/jquery/jquery.viewport.js
/2static/script/lib/jquery/plugins/jquery.hotkeys-0.7.8.js
/2static/script/lib/jquery/plugins/jquery.scrollTo.js
/script/jquery/jquery.autocomplete.pack.js
/2static/script/lib/jquery/plugins/jquery.tablesorter-2.0.3.min.js
/2static/script/lib/jquery/plugins/jquery.cookie.js
/2static/script/lib/jquery/plugins/jquery.editable-select.js
/2static/script/lib/jquery/plugins/jquery.getscrollbarwidth.js
/script/common/global.js
/2static/script/fecru/star.js
/2static/script/fecru/browse.js
/2static/script/fecru/dialog.js
/2static/script/fecru/ajax.js
/2static/script/fecru/hover.js
/2static/script/fecru/profile.js
/2static/script/fecru/rss.js
/2static/script/fecru/ui.js
/2static/script/fecru/prefs.js
/2static/script/lib/ajs/contentnamesearch.js
/2static/script/cru/crucible-ui.js
/2static/script/cru/util.js
/2static/script/cru/review/email-comments.js
/2static/script/cru/review/review-history.js
/2static/script/cru/review/widgets/sliders.js
/2static/script/cru/review/widgets/scroll-tracker.js
/2static/script/cru/review/wiki/wiki-preview.js
/2static/script/cru/review/review.js
/2static/script/cru/review/review-model.js
/2static/script/cru/review/review-event.js
/2static/script/cru/review/comment/comment-ajax.js
/2static/script/cru/review/comment/comment-event.js
/2static/script/cru/review/comment/comment-issue.js
/2static/script/cru/review/comment/comment-form.js
/2static/script/cru/review/comment/commentator.js
/2static/script/cru/review/comment/comment-nav.js
/2static/script/cru/review/comment/comment-tetris.js
/2static/script/cru/review/comment/comment-thread.js
/2static/script/cru/review/frx/frx.js
/2static/script/cru/review/frx/frx-event.js
/2static/script/cru/review/frx/frx-ajax.js
/2static/script/cru/review/frx/frx-nav.js
/2static/script/cru/create/create.js
/2static/script/cru/create/create-browse.js
/2static/script/cru/create/create-event.js
/2static/script/cru/dialog/dialog.js
/2static/script/cru/dialog/dialog-event.js
/2static/script/fecru/onReady.js
*/
/* START /script/jquery/jquery.viewport.js */
/*
 * Viewport - jQuery selectors for finding elements in viewport
 *
 * Copyright (c) 2008-2009 Mika Tuupola
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Project home:
 *  http://www.appelsiini.net/projects/viewport
 *
 * Modifications:
 * - monitor the #frx-pane instead of the window
 * - threshold is an optional param, e.g., ':in-viewport(100)'
 * 
 * Optimizations:
 * - avoid redundant lookups
 * - avoid horizontal calculation (via ':in-viewport-vert')
 */
(function($) {

    //map of container id to {$container, containerOffset, containerScrollTop, containerScrollLeft}
    var containers = {};
    var $element;
    var elementOffset;

    function init(element, containerId) {
        var containerSet = containers[containerId];
        var $container;
        var containerOffset;
        if (typeof containerSet == 'undefined') {
            containerSet = {};
            $container = $('#' + containerId);
            containerOffset = $container.offset();
            containerSet.container = $container;
            containerSet.containerOffset = containerOffset;
        } else {
            $container = containerSet.container;
            containerOffset = containerSet.containerOffset;
        }
        var containerScrollTop = containerSet.containerScrollTop = $container.scrollTop();
        var containerScrollLeft = containerSet.containerScrollLeft = $container.scrollLeft();

        $element = $(element);

        var rawOffset = $element.offset();
        elementOffset = {
            top: rawOffset.top - containerOffset.top + containerScrollTop,
            left: rawOffset.left - containerOffset.left + containerScrollLeft
        };

        containers[containerId] = containerSet;
        return containerSet;
    }

    function isBelowTheFold(settings, containerSet) {
        var fold = containerSet.container.height() + containerSet.containerScrollTop;
        return fold - settings.threshold <= elementOffset.top;
    }

    $.belowthefold = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return isBelowTheFold(settings, containerSet);
    };

    function isAboveTheTop(settings, containerSet) {
        var top = containerSet.containerScrollTop;
        return top + settings.threshold >= elementOffset.top + $element.height();
    };

    $.abovethetop = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return isAboveTheTop(settings, containerSet);
    };

    function isRightOfScreen(settings, containerSet) {
        var fold = containerSet.container.width() + containerSet.containerScrollLeft;
        return fold - settings.threshold <= elementOffset.left;
    }

    $.rightofscreen = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return isRightOfScreen(settings, containerSet);
    };

    function isLeftOfScreen(settings, containerSet) {
        var left = containerSet.containerScrollLeft;
        return left + settings.threshold >= elementOffset.left + $element.width();
    }

    $.leftofscreen = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return isLeftOfScreen(settings, containerSet);
    };

    $.inviewport = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return !isLeftOfScreen(settings, containerSet) && !isRightOfScreen(settings, containerSet) && !isBelowTheFold(settings, containerSet) && !isAboveTheTop(settings, containerSet);
    };

    $.inviewportvert = function(element, settings, containerId) {
        var containerSet = init(element, containerId);
        return !isBelowTheFold(settings, containerSet) && !isAboveTheTop(settings, containerSet);
    };

    $.extend($.expr[':'], {
        "below-the-fold": function(a, i, m) {
            return $.belowthefold(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        },
        "above-the-top": function(a, i, m) {
            return $.abovethetop(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        },
        "left-of-screen": function(a, i, m) {
            return $.leftofscreen(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        },
        "right-of-screen": function(a, i, m) {
            return $.rightofscreen(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        },
        "in-viewport": function(a, i, m) {
            return $.inviewport(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        },
        "in-viewport-vert": function(a, i, m) {
            return $.inviewportvert(a, {threshold : parseInt(m[3], 10) || 0}, m[3].split(' ')[1]);
        }
    });

})(jQuery);
;
/* END /script/jquery/jquery.viewport.js */
/* START /2static/script/lib/jquery/plugins/jquery.hotkeys-0.7.8.js */
/*
(c) Copyrights 2007 - 2008

Original idea by by Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
 
jQuery Plugin by Tzury Bar Yochay 
tzury.by@gmail.com
http://evalinux.wordpress.com
http://facebook.com/profile.php?id=513676303

Project's sites: 
http://code.google.com/p/js-hotkeys/
http://github.com/tzuryby/hotkeys/tree/master

License: same as jQuery license. 

USAGE:
    // simple usage
    $(document).bind('keydown', 'Ctrl+c', function(){ alert('copy anyone?');});
    
    // special options such as disableInIput
    $(document).bind('keydown', {combi:'Ctrl+x', disableInInput: true} , function() {});
    
Note:
    This plugin wraps the following jQuery methods: $.fn.find, $.fn.bind and $.fn.unbind

*/


(function (jQuery){
    // keep reference to the original $.fn.bind and $.fn.unbind
    jQuery.fn.__bind__ = jQuery.fn.bind;
    jQuery.fn.__unbind__ = jQuery.fn.unbind;
    jQuery.fn.__find__ = jQuery.fn.find;
    
    var hotkeys = {
        version: '0.7.8',
        override: /keydown|keypress|keyup/g,
        triggersMap: {},
        
        specialKeys: { 27: 'esc', 9: 'tab', 32:'space', 13: 'return', 8:'backspace', 145: 'scroll', 
            20: 'capslock', 144: 'numlock', 19:'pause', 45:'insert', 36:'home', 
            35:'end', 33: 'pageup', 34:'pagedown', 37:'left', 38:'up', 39:'right',40:'down', 
            112:'f1',113:'f2', 114:'f3', 115:'f4', 116:'f5', 117:'f6', 118:'f7', 119:'f8', 
            120:'f9', 121:'f10', 122:'f11', 123:'f12' },

        shiftNums: { "`":"~", "1":"!", "2":"@", "3":"#", "4":"$", "5":"%", "6":"^", "7":"&",
            "8":"*", "9":"(", "0":")", "_":"_", "+":"+", ":":":", "\"":"\"", "<":"<",
            ">":">",  "?":"?",  "|":"|" },
        
        newTrigger: function (type, combi, callback) { 
            // i.e. {'keyup': {'ctrl': {cb: callback, disableInInput: false}}}
            var result = {};
            result[type] = {};
            result[type][combi] = {cb: callback, disableInInput: false};
            return result;
        }
    };
    // add firefox num pad char codes
    if (jQuery.browser.mozilla){
        hotkeys.specialKeys = jQuery.extend(hotkeys.specialKeys, { 96: '0', 97:'1', 98: '2', 99: 
            '3', 100: '4', 101: '5', 102: '6', 103: '7', 104: '8', 105: '9' });
    }
    
    // a wrapper around of $.fn.find 
    // see more at: http://groups.google.com/group/jquery-en/browse_thread/thread/18f9825e8d22f18d
    jQuery.fn.find = function( selector ) {
        this.query=selector;
        return jQuery.fn.__find__.apply(this, arguments);
	};
    
    jQuery.fn.unbind = function (type, combi, fn){
        if (jQuery.isFunction(combi)){
            fn = combi;
            combi = null;
        }
        if (combi && typeof combi === 'string'){
            var selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();
            var hkTypes = type.split(' ');
            for (var x=0; x<hkTypes.length; x++){
                delete hotkeys.triggersMap[selectorId][hkTypes[x]][combi];
            }
        }
        // call jQuery original unbind
        return  this.__unbind__(type, fn);
    };
    
    jQuery.fn.bind = function(type, data, fn){
        // grab keyup,keydown,keypress
        var handle = type.match(hotkeys.override);
        
        if (jQuery.isFunction(data) || !handle){
            // call jQuery.bind only
            return this.__bind__(type, data, fn);
        }
        else{
            // split the job
            var result = null,            
            // pass the rest to the original $.fn.bind
            pass2jq = jQuery.trim(type.replace(hotkeys.override, ''));
            
            // see if there are other types, pass them to the original $.fn.bind
            if (pass2jq){
                // call original jQuery.bind()
                result = this.__bind__(pass2jq, data, fn);
            }            
            
            if (typeof data === "string"){
                data = {'combi': data};
            }
            if(data.combi){
                for (var x=0; x < handle.length; x++){
                    var eventType = handle[x];
                    var combi = data.combi.toLowerCase(),
                        trigger = hotkeys.newTrigger(eventType, combi, fn),
                        selectorId = ((this.prevObject && this.prevObject.query) || (this[0].id && this[0].id) || this[0]).toString();
                        
                    //trigger[eventType][combi].propagate = data.propagate;
                    trigger[eventType][combi].disableInInput = data.disableInInput;
                    
                    // first time selector is bounded
                    if (!hotkeys.triggersMap[selectorId]) {
                        hotkeys.triggersMap[selectorId] = trigger;
                    }
                    // first time selector is bounded with this type
                    else if (!hotkeys.triggersMap[selectorId][eventType]) {
                        hotkeys.triggersMap[selectorId][eventType] = trigger[eventType];
                    }
                    // make trigger point as array so more than one handler can be bound
                    var mapPoint = hotkeys.triggersMap[selectorId][eventType][combi];
                    if (!mapPoint){
                        hotkeys.triggersMap[selectorId][eventType][combi] = [trigger[eventType][combi]];
                    }
                    else if (mapPoint.constructor !== Array){
                        hotkeys.triggersMap[selectorId][eventType][combi] = [mapPoint];
                    }
                    else {
                        hotkeys.triggersMap[selectorId][eventType][combi][mapPoint.length] = trigger[eventType][combi];
                    }
                    
                    // add attribute and call $.event.add per matched element
                    this.each(function(){
                        // jQuery wrapper for the current element
                        var jqElem = jQuery(this);
                        
                        // element already associated with another collection
                        if (jqElem.attr('hkId') && jqElem.attr('hkId') !== selectorId){
                            selectorId = jqElem.attr('hkId') + ";" + selectorId;
                        }
                        jqElem.attr('hkId', selectorId);
                    });
                    result = this.__bind__(handle.join(' '), data, hotkeys.handler)
                }
            }
            return result;
        }
    };
    // work-around for opera and safari where (sometimes) the target is the element which was last 
    // clicked with the mouse and not the document event it would make sense to get the document
    hotkeys.findElement = function (elem){
        if (!jQuery(elem).attr('hkId')){
            if (jQuery.browser.opera || jQuery.browser.safari){
                while (!jQuery(elem).attr('hkId') && elem.parentNode){
                    elem = elem.parentNode;
                }
            }
        }
        return elem;
    };
    // the event handler
    hotkeys.handler = function(event) {
        var target = hotkeys.findElement(event.currentTarget), 
            jTarget = jQuery(target),
            ids = jTarget.attr('hkId');
        
        if(ids){
            ids = ids.split(';');
            var code = event.which,
                type = event.type,
                special = hotkeys.specialKeys[code],
                // prevent f5 overlapping with 't' (or f4 with 's', etc.)
                character = !special && String.fromCharCode(code).toLowerCase(),
                shift = event.shiftKey,
                ctrl = event.ctrlKey,
                // patch for jquery 1.2.5 && 1.2.6 see more at:
                // http://groups.google.com/group/jquery-en/browse_thread/thread/83e10b3bb1f1c32b
                alt = event.altKey || event.originalEvent.altKey,
                meta = event.metaKey,
                mapPoint = null;

            for (var x=0; x < ids.length; x++){
                if (hotkeys.triggersMap[ids[x]][type]){
                    mapPoint = hotkeys.triggersMap[ids[x]][type];
                    break;
                }
            }
            
            //find by: id.type.combi.options            
            if (mapPoint){ 
                var trigger;
                // event type is associated with the hkId
                if(!shift && !ctrl && !alt && !meta) { // No Modifiers
                    trigger = mapPoint[special] ||  (character && mapPoint[character]);
                }
                else{
                    // check combinations (alt|ctrl|meta|shift+anything)
                    var modif = '';
                    if(alt) modif +='alt+';
                    if(ctrl) modif+= 'ctrl+';
                    if(meta) modif += 'meta+';
                    if(shift) modif += 'shift+';
                    
                    // modifiers + special keys or modifiers + character or modifiers + shift character or just shift character
                    trigger = mapPoint[modif+special];
                    if (!trigger){
                        if (character){
                            trigger = mapPoint[modif+character] 
                                || mapPoint[modif+hotkeys.shiftNums[character]]
                                // '$' can be triggered as 'Shift+4' or 'Shift+$' or just '$'
                                || (modif === 'shift+' && mapPoint[hotkeys.shiftNums[character]]);
                        }
                    }
                }
                if (trigger){
                    var result = false;
                    for (var x=0; x < trigger.length; x++){
                        if(trigger[x].disableInInput){
                            // double check event.currentTarget and event.target
                            var elem = jQuery(event.target);
                            if (jTarget.is("input") || jTarget.is("textarea") || jTarget.is("select")
                                || elem.is("input") || elem.is("textarea") || elem.is("select")) {
                                return true;
                            }
                        }
                        // call the registered callback function
                        result = result || trigger[x].cb.apply(this, [event]);
                    }
                    return result;
                }
            }
        }
    };
    // place it under window so it can be extended and overridden by others
    window.hotkeys = hotkeys;
    return jQuery;
})(jQuery);
;
/* END /2static/script/lib/jquery/plugins/jquery.hotkeys-0.7.8.js */
/* START /2static/script/lib/jquery/plugins/jquery.scrollTo.js */
/**
 * jQuery.ScrollTo
 * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
 * Dual licensed under MIT and GPL.
 * Date: 3/9/2009
 *
 * @projectDescription Easy element scrolling using jQuery.
 * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
 * Works with jQuery +1.2.6. Tested on FF 2/3, IE 6/7, Opera 9.5/6, Safari 3, Chrome 1 on WinXP.
 *
 * @author Ariel Flesler
 * @version 1.4.1
 *
 * @id jQuery.scrollTo
 * @id jQuery.fn.scrollTo
 * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements.
 *	  The different options for target are:
 *		- A number position (will be applied to all axes).
 *		- A string position ('44', '100px', '+=90', etc ) will be applied to all axes
 *		- A jQuery/DOM element ( logically, child of the element to scroll )
 *		- A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc )
 *		- A hash { top:x, left:y }, x and y can be any kind of number/string like above.
 * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead.
 * @param {Object,Function} settings Optional set of settings or the onAfter callback.
 *	 @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'.
 *	 @option {Number} duration The OVERALL length of the animation.
 *	 @option {String} easing The easing method for the animation.
 *	 @option {Boolean} margin If true, the margin of the target element will be deducted from the final position.
 *	 @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }.
 *	 @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes.
 *	 @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends.
 *	 @option {Function} onAfter Function to be called after the scrolling ends. 
 *	 @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends.
 * @return {jQuery} Returns the same jQuery object, for chaining.
 *
 * @desc Scroll to a fixed position
 * @example $('div').scrollTo( 340 );
 *
 * @desc Scroll relatively to the actual position
 * @example $('div').scrollTo( '+=340px', { axis:'y' } );
 *
 * @dec Scroll using a selector (relative to the scrolled element)
 * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } );
 *
 * @ Scroll to a DOM element (same for jQuery object)
 * @example var second_child = document.getElementById('container').firstChild.nextSibling;
 *			$('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){
 *				alert('scrolled!!');																   
 *			}});
 *
 * @desc Scroll on both axes, to different values
 * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } );
 */
;(function( $ ){
	
	var $scrollTo = $.scrollTo = function( target, duration, settings ){
		$(window).scrollTo( target, duration, settings );
	};

	$scrollTo.defaults = {
		axis:'xy',
		duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1
	};

	// Returns the element that needs to be animated to scroll the window.
	// Kept for backwards compatibility (specially for localScroll & serialScroll)
	$scrollTo.window = function( scope ){
		return $(window).scrollable();
	};

	// Hack, hack, hack... stay away!
	// Returns the real elements to scroll (supports window/iframes, documents and regular nodes)
	$.fn.scrollable = function(){
		return this.map(function(){
			var elem = this,
				isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1;

				if( !isWin )
					return elem;

			var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem;
			
			return $.browser.safari || doc.compatMode == 'BackCompat' ?
				doc.body : 
				doc.documentElement;
		});
	};

	$.fn.scrollTo = function( target, duration, settings ){
		if( typeof duration == 'object' ){
			settings = duration;
			duration = 0;
		}
		if( typeof settings == 'function' )
			settings = { onAfter:settings };
			
		if( target == 'max' )
			target = 9e9;
			
		settings = $.extend( {}, $scrollTo.defaults, settings );
		// Speed is still recognized for backwards compatibility
		duration = duration || settings.speed || settings.duration;
		// Make sure the settings are given right
		settings.queue = settings.queue && settings.axis.length > 1;
		
		if( settings.queue )
			// Let's keep the overall duration
			duration /= 2;
		settings.offset = both( settings.offset );
		settings.over = both( settings.over );

		return this.scrollable().each(function(){
			var elem = this,
				$elem = $(elem),
				targ = target, toff, attr = {},
				win = $elem.is('html,body');

			switch( typeof targ ){
				// A number will pass the regex
				case 'number':
				case 'string':
					if( /^([+-]=)?\d+(\.\d+)?(px)?$/.test(targ) ){
						targ = both( targ );
						// We are done
						break;
					}
					// Relative selector, no break!
					targ = $(targ,this);
				case 'object':
					// DOMElement / jQuery
					if( targ.is || targ.style )
						// Get the real position of the target 
						toff = (targ = $(targ)).offset();
			}
			$.each( settings.axis.split(''), function( i, axis ){
				var Pos	= axis == 'x' ? 'Left' : 'Top',
					pos = Pos.toLowerCase(),
					key = 'scroll' + Pos,
					old = elem[key],
					Dim = axis == 'x' ? 'Width' : 'Height';

				if( toff ){// jQuery / DOMElement
					attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] );

					// If it's a dom element, reduce the margin
					if( settings.margin ){
						attr[key] -= parseInt(targ.css('margin'+Pos)) || 0;
						attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0;
					}
					
					attr[key] += settings.offset[pos] || 0;
					
					if( settings.over[pos] )
						// Scroll to a fraction of its width/height
						attr[key] += targ[Dim.toLowerCase()]() * settings.over[pos];
				}else
					attr[key] = targ[pos];

				// Number or 'number'
				if( /^\d+$/.test(attr[key]) )
					// Check the limits
					attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max(Dim) );

				// Queueing axes
				if( !i && settings.queue ){
					// Don't waste time animating, if there's no need.
					if( old != attr[key] )
						// Intermediate animation
						animate( settings.onAfterFirst );
					// Don't animate this axis again in the next iteration.
					delete attr[key];
				}
			});

			animate( settings.onAfter );			

			function animate( callback ){
				$elem.animate( attr, duration, settings.easing, callback && function(){
					callback.call(this, target, settings);
				});
			};

			// Max scrolling position, works on quirks mode
			// It only fails (not too badly) on IE, quirks mode.
			function max( Dim ){
				var scroll = 'scroll'+Dim;
				
				if( !win )
					return elem[scroll];
				
				var size = 'client' + Dim,
					html = elem.ownerDocument.documentElement,
					body = elem.ownerDocument.body;

				return Math.max( html[scroll], body[scroll] ) 
					 - Math.min( html[size]  , body[size]   );
					
			};

		}).end();
	};

	function both( val ){
		return typeof val == 'object' ? val : { top:val, left:val };
	};

})( jQuery );;
/* END /2static/script/lib/jquery/plugins/jquery.scrollTo.js */
/* START /script/jquery/jquery.autocomplete.pack.js */
/*
 * Autocomplete - jQuery plugin 1.0.2
 *
 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
 *
 */
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(3($){$.31.1o({12:3(b,d){5 c=Y b=="1w";d=$.1o({},$.D.1L,{11:c?b:14,w:c?14:b,1D:c?$.D.1L.1D:10,Z:d&&!d.1x?10:3U},d);d.1t=d.1t||3(a){6 a};d.1q=d.1q||d.1K;6 I.K(3(){1E $.D(I,d)})},M:3(a){6 I.X("M",a)},1y:3(a){6 I.15("1y",[a])},20:3(){6 I.15("20")},1Y:3(a){6 I.15("1Y",[a])},1X:3(){6 I.15("1X")}});$.D=3(o,r){5 t={2N:38,2I:40,2D:46,2x:9,2v:13,2q:27,2d:3x,2j:33,2o:34,2e:8};5 u=$(o).3f("12","3c").P(r.24);5 p;5 m="";5 n=$.D.2W(r);5 s=0;5 k;5 h={1z:B};5 l=$.D.2Q(r,o,1U,h);5 j;$.1T.2L&&$(o.2K).X("3S.12",3(){4(j){j=B;6 B}});u.X(($.1T.2L?"3Q":"3N")+".12",3(a){k=a.2F;3L(a.2F){Q t.2N:a.1d();4(l.L()){l.2y()}A{W(0,C)}N;Q t.2I:a.1d();4(l.L()){l.2u()}A{W(0,C)}N;Q t.2j:a.1d();4(l.L()){l.2t()}A{W(0,C)}N;Q t.2o:a.1d();4(l.L()){l.2s()}A{W(0,C)}N;Q r.19&&$.1p(r.R)==","&&t.2d:Q t.2x:Q t.2v:4(1U()){a.1d();j=C;6 B}N;Q t.2q:l.U();N;3A:1I(p);p=1H(W,r.1D);N}}).1G(3(){s++}).3v(3(){s=0;4(!h.1z){2k()}}).2i(3(){4(s++>1&&!l.L()){W(0,C)}}).X("1y",3(){5 c=(1n.7>1)?1n[1]:14;3 23(q,a){5 b;4(a&&a.7){16(5 i=0;i<a.7;i++){4(a[i].M.O()==q.O()){b=a[i];N}}}4(Y c=="3")c(b);A u.15("M",b&&[b.w,b.H])}$.K(1g(u.J()),3(i,a){1R(a,23,23)})}).X("20",3(){n.18()}).X("1Y",3(){$.1o(r,1n[1]);4("w"2G 1n[1])n.1f()}).X("1X",3(){l.1u();u.1u();$(o.2K).1u(".12")});3 1U(){5 b=l.26();4(!b)6 B;5 v=b.M;m=v;4(r.19){5 a=1g(u.J());4(a.7>1){v=a.17(0,a.7-1).2Z(r.R)+r.R+v}v+=r.R}u.J(v);1l();u.15("M",[b.w,b.H]);6 C}3 W(b,c){4(k==t.2D){l.U();6}5 a=u.J();4(!c&&a==m)6;m=a;a=1k(a);4(a.7>=r.22){u.P(r.21);4(!r.1C)a=a.O();1R(a,2V,1l)}A{1B();l.U()}};3 1g(b){4(!b){6[""]}5 d=b.1Z(r.R);5 c=[];$.K(d,3(i,a){4($.1p(a))c[i]=$.1p(a)});6 c}3 1k(a){4(!r.19)6 a;5 b=1g(a);6 b[b.7-1]}3 1A(q,a){4(r.1A&&(1k(u.J()).O()==q.O())&&k!=t.2e){u.J(u.J()+a.48(1k(m).7));$.D.1N(o,m.7,m.7+a.7)}};3 2k(){1I(p);p=1H(1l,47)};3 1l(){5 c=l.L();l.U();1I(p);1B();4(r.2U){u.1y(3(a){4(!a){4(r.19){5 b=1g(u.J()).17(0,-1);u.J(b.2Z(r.R)+(b.7?r.R:""))}A u.J("")}})}4(c)$.D.1N(o,o.H.7,o.H.7)};3 2V(q,a){4(a&&a.7&&s){1B();l.2T(a,q);1A(q,a[0].H);l.1W()}A{1l()}};3 1R(f,d,g){4(!r.1C)f=f.O();5 e=n.2S(f);4(e&&e.7){d(f,e)}A 4((Y r.11=="1w")&&(r.11.7>0)){5 c={45:+1E 44()};$.K(r.2R,3(a,b){c[a]=Y b=="3"?b():b});$.43({42:"41",3Z:"12"+o.3Y,2M:r.2M,11:r.11,w:$.1o({q:1k(f),3X:r.Z},c),3W:3(a){5 b=r.1r&&r.1r(a)||1r(a);n.1h(f,b);d(f,b)}})}A{l.2J();g(f)}};3 1r(c){5 d=[];5 b=c.1Z("\\n");16(5 i=0;i<b.7;i++){5 a=$.1p(b[i]);4(a){a=a.1Z("|");d[d.7]={w:a,H:a[0],M:r.1v&&r.1v(a,a[0])||a[0]}}}6 d};3 1B(){u.1e(r.21)}};$.D.1L={24:"3R",2H:"3P",21:"3O",22:1,1D:3M,1C:B,1a:C,1V:B,1j:10,Z:3K,2U:B,2R:{},1S:C,1K:3(a){6 a[0]},1q:14,1A:B,E:0,19:B,R:", ",1t:3(b,a){6 b.2C(1E 3J("(?![^&;]+;)(?!<[^<>]*)("+a.2C(/([\\^\\$\\(\\)\\[\\]\\{\\}\\*\\.\\+\\?\\|\\\\])/2A,"\\\\$1")+")(?![^<>]*>)(?![^&;]+;)","2A"),"<2z>$1</2z>")},1x:C,1s:3I};$.D.2W=3(g){5 h={};5 j=0;3 1a(s,a){4(!g.1C)s=s.O();5 i=s.3H(a);4(i==-1)6 B;6 i==0||g.1V};3 1h(q,a){4(j>g.1j){18()}4(!h[q]){j++}h[q]=a}3 1f(){4(!g.w)6 B;5 f={},2w=0;4(!g.11)g.1j=1;f[""]=[];16(5 i=0,30=g.w.7;i<30;i++){5 c=g.w[i];c=(Y c=="1w")?[c]:c;5 d=g.1q(c,i+1,g.w.7);4(d===B)1P;5 e=d.3G(0).O();4(!f[e])f[e]=[];5 b={H:d,w:c,M:g.1v&&g.1v(c)||d};f[e].1O(b);4(2w++<g.Z){f[""].1O(b)}};$.K(f,3(i,a){g.1j++;1h(i,a)})}1H(1f,25);3 18(){h={};j=0}6{18:18,1h:1h,1f:1f,2S:3(q){4(!g.1j||!j)6 14;4(!g.11&&g.1V){5 a=[];16(5 k 2G h){4(k.7>0){5 c=h[k];$.K(c,3(i,x){4(1a(x.H,q)){a.1O(x)}})}}6 a}A 4(h[q]){6 h[q]}A 4(g.1a){16(5 i=q.7-1;i>=g.22;i--){5 c=h[q.3F(0,i)];4(c){5 a=[];$.K(c,3(i,x){4(1a(x.H,q)){a[a.7]=x}});6 a}}}6 14}}};$.D.2Q=3(e,g,f,k){5 h={G:"3E"};5 j,y=-1,w,1m="",1M=C,F,z;3 2r(){4(!1M)6;F=$("<3D/>").U().P(e.2H).T("3C","3B").1J(2p.2n);z=$("<3z/>").1J(F).3y(3(a){4(V(a).2m&&V(a).2m.3w()==\'2l\'){y=$("1F",z).1e(h.G).3u(V(a));$(V(a)).P(h.G)}}).2i(3(a){$(V(a)).P(h.G);f();g.1G();6 B}).3t(3(){k.1z=C}).3s(3(){k.1z=B});4(e.E>0)F.T("E",e.E);1M=B}3 V(a){5 b=a.V;3r(b&&b.3q!="2l")b=b.3p;4(!b)6[];6 b}3 S(b){j.17(y,y+1).1e(h.G);2h(b);5 a=j.17(y,y+1).P(h.G);4(e.1x){5 c=0;j.17(0,y).K(3(){c+=I.1i});4((c+a[0].1i-z.1c())>z[0].3o){z.1c(c+a[0].1i-z.3n())}A 4(c<z.1c()){z.1c(c)}}};3 2h(a){y+=a;4(y<0){y=j.1b()-1}A 4(y>=j.1b()){y=0}}3 2g(a){6 e.Z&&e.Z<a?e.Z:a}3 2f(){z.2B();5 b=2g(w.7);16(5 i=0;i<b;i++){4(!w[i])1P;5 a=e.1K(w[i].w,i+1,b,w[i].H,1m);4(a===B)1P;5 c=$("<1F/>").3m(e.1t(a,1m)).P(i%2==0?"3l":"3k").1J(z)[0];$.w(c,"2c",w[i])}j=z.3j("1F");4(e.1S){j.17(0,1).P(h.G);y=0}4($.31.2b)z.2b()}6{2T:3(d,q){2r();w=d;1m=q;2f()},2u:3(){S(1)},2y:3(){S(-1)},2t:3(){4(y!=0&&y-8<0){S(-y)}A{S(-8)}},2s:3(){4(y!=j.1b()-1&&y+8>j.1b()){S(j.1b()-1-y)}A{S(8)}},U:3(){F&&F.U();j&&j.1e(h.G);y=-1},L:3(){6 F&&F.3i(":L")},3h:3(){6 I.L()&&(j.2a("."+h.G)[0]||e.1S&&j[0])},1W:3(){5 a=$(g).3g();F.T({E:Y e.E=="1w"||e.E>0?e.E:$(g).E(),2E:a.2E+g.1i,1Q:a.1Q}).1W();4(e.1x){z.1c(0);z.T({29:e.1s,3e:\'3d\'});4($.1T.3b&&Y 2p.2n.3T.29==="3a"){5 c=0;j.K(3(){c+=I.1i});5 b=c>e.1s;z.T(\'3V\',b?e.1s:c);4(!b){j.E(z.E()-28(j.T("32-1Q"))-28(j.T("32-39")))}}}},26:3(){5 a=j&&j.2a("."+h.G).1e(h.G);6 a&&a.7&&$.w(a[0],"2c")},2J:3(){z&&z.2B()},1u:3(){F&&F.37()}}};$.D.1N=3(b,a,c){4(b.2O){5 d=b.2O();d.36(C);d.35("2P",a);d.4c("2P",c);d.4b()}A 4(b.2Y){b.2Y(a,c)}A{4(b.2X){b.2X=a;b.4a=c}}b.1G()}})(49);',62,261,'|||function|if|var|return|length|||||||||||||||||||||||||data||active|list|else|false|true|Autocompleter|width|element|ACTIVE|value|this|val|each|visible|result|break|toLowerCase|addClass|case|multipleSeparator|moveSelect|css|hide|target|onChange|bind|typeof|max||url|autocomplete||null|trigger|for|slice|flush|multiple|matchSubset|size|scrollTop|preventDefault|removeClass|populate|trimWords|add|offsetHeight|cacheLength|lastWord|hideResultsNow|term|arguments|extend|trim|formatMatch|parse|scrollHeight|highlight|unbind|formatResult|string|scroll|search|mouseDownOnSelect|autoFill|stopLoading|matchCase|delay|new|li|focus|setTimeout|clearTimeout|appendTo|formatItem|defaults|needsInit|Selection|push|continue|left|request|selectFirst|browser|selectCurrent|matchContains|show|unautocomplete|setOptions|split|flushCache|loadingClass|minChars|findValueCallback|inputClass||selected||parseInt|maxHeight|filter|bgiframe|ac_data|COMMA|BACKSPACE|fillList|limitNumberOfItems|movePosition|click|PAGEUP|hideResults|LI|nodeName|body|PAGEDOWN|document|ESC|init|pageDown|pageUp|next|RETURN|nullData|TAB|prev|strong|gi|empty|replace|DEL|top|keyCode|in|resultsClass|DOWN|emptyList|form|opera|dataType|UP|createTextRange|character|Select|extraParams|load|display|mustMatch|receiveData|Cache|selectionStart|setSelectionRange|join|ol|fn|padding|||moveStart|collapse|remove||right|undefined|msie|off|auto|overflow|attr|offset|current|is|find|ac_odd|ac_even|html|innerHeight|clientHeight|parentNode|tagName|while|mouseup|mousedown|index|blur|toUpperCase|188|mouseover|ul|default|absolute|position|div|ac_over|substr|charAt|indexOf|180|RegExp|100|switch|400|keydown|ac_loading|ac_results|keypress|ac_input|submit|style|150|height|success|limit|name|port||abort|mode|ajax|Date|timestamp||200|substring|jQuery|selectionEnd|select|moveEnd'.split('|'),0,{}));
/* END /script/jquery/jquery.autocomplete.pack.js */
/* START /2static/script/lib/jquery/plugins/jquery.tablesorter-2.0.3.min.js */
(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,cells[i]);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,node){var l=parsers.length;for(var i=1;i<l;i++){if(parsers[i].is($.trim(getElementText(table.config,node)),table,node)){return parsers[i];}}return parsers[0];}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=table.tBodies[0].rows[i],cols=[];cache.row.push($(c));for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c.cells[j]),table,c.cells[j]));}cols.push(i);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){if(!node)return"";var t="";if(config.textExtraction=="simple"){if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){t=node.childNodes[0].innerHTML;}else{t=node.innerHTML;}}else{if(typeof(config.textExtraction)=="function"){t=config.textExtraction(node);}else{t=$(node).text();}}return t;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){rows.push(r[n[i][checkCell]]);if(!table.config.appender){var o=r[n[i][checkCell]];var l=o.length;for(var j=0;j<l;j++){tableBody[0].appendChild(o[j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false,tableHeadersRows=[];for(var i=0;i<table.tHead.rows.length;i++){tableHeadersRows[i]=0;};$tableHeaders=$("thead th",table);$tableHeaders.each(function(index){this.count=0;this.column=index;this.order=formatSortingOrder(table.config.sortInitialOrder);if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(!this.sortDisabled){$(this).addClass(table.config.cssHeader);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){i=(v.toLowerCase()=="desc")?1:0;}else{i=(v==(0||1))?v:0;}return i;}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(getCachedSortType(table.config.parsers,c)=="text")?((order==0)?"sortText":"sortTextDesc"):((order==0)?"sortNumeric":"sortNumericDesc");var e="e"+i;dynamicExp+="var "+e+" = "+s+"(a["+c+"],b["+c+"]); ";dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function sortText(a,b){return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){var DECIMAL='\\'+config.decimal;var exp='/(^[+]?0('+DECIMAL+'0+)?$)|(^([-+]?[1-9][0-9]*)$)|(^([-+]?((0?|[1-9][0-9]*)'+DECIMAL+'(0*[1-9][0-9]*)))$)|(^[-+]?[1-9]+[0-9]*'+DECIMAL+'0+$)/';return RegExp(exp).test($.trim(s));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}$("tr:visible",table.tBodies[0]).filter(':even').removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0]).end().filter(':odd').removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);;
/* END /2static/script/lib/jquery/plugins/jquery.tablesorter-2.0.3.min.js */
/* START /2static/script/lib/jquery/plugins/jquery.cookie.js */
/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

/**
 * Create a cookie with the given name and value and other optional parameters.
 *
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Set the value of a cookie.
 * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
 * @desc Create a cookie with all available options.
 * @example $.cookie('the_cookie', 'the_value');
 * @desc Create a session cookie.
 * @example $.cookie('the_cookie', null);
 * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
 *       used when the cookie was set.
 *
 * @param String name The name of the cookie.
 * @param String value The value of the cookie.
 * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
 * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
 *                             If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
 *                             If set to null or omitted, the cookie will be a session cookie and will not be retained
 *                             when the the browser exits.
 * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
 * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
 * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
 *                        require a secure protocol (like HTTPS).
 * @type undefined
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */

/**
 * Get the value of a cookie with the given name.
 *
 * @example $.cookie('the_cookie');
 * @desc Get the value of a cookie.
 *
 * @param String name The name of the cookie.
 * @return The value of the cookie.
 * @type String
 *
 * @name $.cookie
 * @cat Plugins/Cookie
 * @author Klaus Hartl/klaus.hartl@stilbuero.de
 */
jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        if (value === null) {
            value = '';
            options.expires = -1;
        }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        // CAUTION: Needed to parenthesize options.path and options.domain
        // in the following expressions, otherwise they evaluate to undefined
        // in the packed version for some reason...
        var path = options.path ? '; path=' + (options.path) : '';
        var domain = options.domain ? '; domain=' + (options.domain) : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
};;
/* END /2static/script/lib/jquery/plugins/jquery.cookie.js */
/* START /2static/script/lib/jquery/plugins/jquery.editable-select.js */
/**
 * Copyright (c) 2009 Anders Ekdahl (http://coffeescripter.com/)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 *
 * Version: 1.3.1
 *
 * Demo and documentation: http://coffeescripter.com/code/editable-select/
 */
(function($) {
  var instances = [];
  $.fn.editableSelect = function(options) {
    var defaults = { bg_iframe: false,
                     onSelect: false,
                     items_then_scroll: 10,
                     case_sensitive: false,
                     selectStrategy: false
    };
    var settings = $.extend(defaults, options);
    // Only do bg_iframe for browsers that need it
    if(settings.bg_iframe && !$.browser.msie) {
      settings.bg_iframe = false;
    };
    var instance = false;
    $(this).each(function() {
      var i = instances.length;
      if(typeof $(this).data('editable-selecter') == 'undefined') {
        instances[i] = new EditableSelect(this, settings);
        $(this).data('editable-selecter', i);
      };
    });
    return $(this);
  };
  $.fn.editableSelectInstances = function() {
    var ret = [];
    $(this).each(function() {
      if(typeof $(this).data('editable-selecter') != 'undefined') {
        ret[ret.length] = instances[$(this).data('editable-selecter')];
      };
    });
    return ret;
  };

  var EditableSelect = function(select, settings) {
    this.init(select, settings);
  };
  EditableSelect.prototype = {
    settings: false,
    text: false,
    select: false,
    wrapper: false,
    list_item_height: 20,
    list_height: 0,
    list_is_visible: false,
    hide_on_blur_timeout: false,
    bg_iframe: false,
    current_value: '',
    init: function(select, settings) {
      this.settings = settings;
      this.select = $(select);
      this.text = $('<input type="text">');
      this.text.attr('name', this.select.attr('name'));
      this.text.data('editable-selecter', this.select.data('editable-selecter'));
      // Because we don't want the value of the select when the form
      // is submitted
      this.select.attr('disabled', 'disabled');
      var id = this.select.attr('id');
      if(!id) {
        id = 'editable-select'+ instances.length;
      };
      this.text.attr('id', id);
      this.text.attr('autocomplete', 'off');
      this.text.addClass('editable-select');
      this.select.attr('id', id +'_hidden_select');
      this.initInputEvents(this.text);
      this.duplicateOptions();
      this.positionElements();
      this.setWidths();

      if(this.settings.bg_iframe) {
        this.createBackgroundIframe();
      };
    },
    duplicateOptions: function() {
      var context = this;
      var wrapper = $(document.createElement('div'));
      wrapper.addClass('editable-select-options');
      var option_list = $(document.createElement('ul'));
      wrapper.append(option_list);
      var options = this.select.find('option');
      options.each(function() {
          var $this = $(this);
          if ($this.attr('selected')) {
              context.text.val($.trim($this.text()));
              context.current_value = $this.val();
          }
          if (!($this.attr("disabled") === "disabled" || $this.attr("disabled") === "true")) {//
              var li = $('<li>' + $this.text() + '<input value="' + $this.val() + '" type="hidden"></li>');
              context.initListItemEvents(li);
              option_list.append(li);
          }
      });
      this.wrapper = wrapper;
      this.checkScroll();
    },
    checkScroll: function() {
      var options = this.wrapper.find('li');
      if(options.length > this.settings.items_then_scroll) {
        this.list_height = this.list_item_height * this.settings.items_then_scroll;
        this.wrapper.css('height', this.list_height +'px');
        this.wrapper.css('overflow', 'auto');
      } else {
        this.wrapper.css('height', 'auto');
        this.wrapper.css('overflow', 'visible');
      };
    },
    addOption: function(value) {
      var li = $('<li>'+ value +'</li>');
      var option = $('<option>'+ value +'</option>');
      this.select.append(option);
      this.initListItemEvents(li);
      this.wrapper.find('ul').append(li);
      this.setWidths();
      this.checkScroll();
    },
    initInputEvents: function(text) {
      var context = this;
      var timer = false;
      $(document.body).click(
        function() {
//          context.clearSelectedListItem();
          context.hideList();
        }
      );
      text.focus(
        function() {
          // Can't use the blur event to hide the list, because the blur event
          // is fired in some browsers when you scroll the list
          context.showList();
          context.highlightSelected();
        }
      ).click(
        function(e) {
          e.stopPropagation();
          context.showList();
          context.highlightSelected();
        }
      ).keydown(
        // Capture key events so the user can navigate through the list
        function(e) {
          switch(e.keyCode) {
            // Down
            case 40:
              if(!context.listIsVisible()) {
                context.showList();
                context.highlightSelected();
              } else {
                e.preventDefault();
                context.selectNewListItem('down');
              };
              break;
            // Up
            case 38:
              e.preventDefault();
              context.selectNewListItem('up');
              break;
            // Tab
            case 9:
              context.pickListItem(context.selectedListItem());
              break;
            // Esc
            case 27:
              e.preventDefault();
              context.hideList();
              return false;
              break;
            // Enter, prevent form submission
            case 13:
              e.preventDefault();
              if (context.listIsVisible()) {
                context.pickListItem(context.selectedListItem());
              }
              return false;
            default:
              context.hideList();
              break;
          };
        }
      ).keyup(
        function(e) {
          // Prevent lots of calls if it's a fast typer
          if(timer !== false) {
            clearTimeout(timer);
            timer = false;
          };
          timer = setTimeout(
            function() {
              // If the user types in a value, select it if it's in the list
              if(context.text.val() != context.current_value) {
                context.current_value = context.text.val();
                context.highlightSelected();
              };
            },
            200
          );
        }
      ).keypress(
        function(e) {
          if(e.keyCode == 13) {
            // Enter, prevent form submission
            e.preventDefault();
            return false;
          };
        }
      );
    },
    initListItemEvents: function(list_item) {
      var context = this;
      list_item.mouseover(
        function() {
          context.clearSelectedListItem();
          context.selectListItem(list_item);
        }
      ).mousedown(
        // Needs to be mousedown and not click, since the inputs blur events
        // fires before the list items click event
        function(e) {
          e.stopPropagation();
          context.pickListItem(context.selectedListItem());
        }
      );
    },
    selectNewListItem: function(direction) {
      var li = this.selectedListItem();
      if(!li.length) {
        li = this.selectFirstListItem();
      };
      if(direction == 'down') {
        var sib = li.next();
      } else {
        var sib = li.prev();
      };
      if(sib.length) {
        this.selectListItem(sib);
        this.scrollToListItem(sib);
        this.unselectListItem(li);
      };
    },
    selectListItem: function(list_item) {
      this.clearSelectedListItem();
      list_item.addClass('selected');
    },
    selectFirstListItem: function() {
      this.clearSelectedListItem();
      var first = this.wrapper.find('li:first');
      first.addClass('selected');
      return first;
    },
    unselectListItem: function(list_item) {
      list_item.removeClass('selected');
    },
    selectedListItem: function() {
      return this.wrapper.find('li.selected');
    },
    clearSelectedListItem: function() {
      this.wrapper.find('li.selected').removeClass('selected');
    },
    pickListItem: function(list_item) {
      if(list_item.length) {
        this.text.val(list_item.text());
        this.current_value = this.text.val();
      };
      if(typeof this.settings.onSelect == 'function') {
        this.settings.onSelect.call(this, list_item);
      };
      this.hideList();
    },
    listIsVisible: function() {
      return this.list_is_visible;
    },
    showList: function() {
      this.refreshOffset();
      this.wrapper.show();
      this.hideOtherLists();
      this.list_is_visible = true;
      if(this.settings.bg_iframe) {
        this.bg_iframe.show();
      };
    },
    highlightSelected: function() {
        if (this.selectedListItem().length === 0) {
            this.highlightValue(this.text.val());
        } else {
            this.scrollToListItem(this.selectedListItem());
        }
    },
    highlightValue:function(val) {
      var context = this;
      var current_value = val;
      if(current_value.length < 0) {
        if(highlight_first) {
          this.selectFirstListItem();
        };
        return;
      };
      if(!context.settings.case_sensitive) {
        current_value = current_value.toLowerCase();
      };
      var best_candiate = false;
      var value_found = false;
      var list_items = this.wrapper.find('li');
      list_items.each(
        function() {
          if(!value_found) {
            var text = $(this).text();
            if(!context.settings.case_sensitive) {
              text = text.toLowerCase();
            };
            if(text == current_value) {
              value_found = true;
              context.clearSelectedListItem();
              context.selectListItem($(this));
              context.scrollToListItem($(this));
              return false;
            } else if(text.indexOf(current_value) === 0 && !best_candiate) {
              // Can't do return false here, since we still need to iterate over
              // all list items to see if there is an exact match
              best_candiate = $(this);
            };
          };
        }
      );
      if(best_candiate && !value_found) {
        context.clearSelectedListItem();
        context.selectListItem(best_candiate);
        context.scrollToListItem(best_candiate);
      } else if(!best_candiate && !value_found) {
          if (context.settings.selectStrategy) {
              context.settings.selectStrategy(this);
          } else {
              this.selectFirstListItem();
          }
      };
    },
    scrollToListItem: function(list_item) {
      if(this.list_height) {
        this.wrapper.scrollTop(list_item[0].offsetTop - (this.list_height / 2));
      };
    },
    hideList: function() {
      this.wrapper.hide();
      this.list_is_visible = false;
      if(this.settings.bg_iframe) {
        this.bg_iframe.hide();
      };
    },
    hideOtherLists: function() {
      for(var i = 0; i < instances.length; i++) {
        if(i != this.select.data('editable-selecter')) {
          instances[i].hideList();
        };
      };
    },
    refreshOffset: function() {
        var elem = this.select;
        if (!elem.is(':visible')) {
            elem = this.text;
        }
        var offset = elem.offset();
        offset.top += elem[0].offsetHeight;
        this.wrapper.css({top: offset.top +'px', left: offset.left +'px'});
    },
    positionElements: function() {
      this.refreshOffset();
      this.select.after(this.text);
      this.select.hide();
      this.select.parent().append(this.wrapper);
      // Need to do this in order to get the list item height
      this.wrapper.css('visibility', 'hidden');
      this.wrapper.show();
      this.list_item_height = this.wrapper.find('li')[0].offsetHeight;
      this.wrapper.css('visibility', 'visible');
      this.wrapper.hide();
    },
    setWidths: function() {
      // The text input has a right margin because of the background arrow image
      // so we need to remove that from the width
      var width = this.select.width() + 2;
      var padding_right = parseInt(this.text.css('padding-right').replace(/px/, ''), 10);
      this.text.width(width - padding_right);
      this.wrapper.width(width + 2);
      if(this.bg_iframe) {
        this.bg_iframe.width(width + 4);
      };
    },
    createBackgroundIframe: function() {
      var bg_iframe = $('<iframe frameborder="0" class="editable-select-iframe" src="about:blank;"></iframe>');
      this.select.parent().append(bg_iframe);
      bg_iframe.width(this.select.width() + 2);
      bg_iframe.height(this.wrapper.height());
      bg_iframe.css({top: this.wrapper.css('top'), left: this.wrapper.css('left')});
      this.bg_iframe = bg_iframe;
    }
  };
})(jQuery);;
/* END /2static/script/lib/jquery/plugins/jquery.editable-select.js */
/* START /2static/script/lib/jquery/plugins/jquery.getscrollbarwidth.js */
/*! Copyright (c) 2008 Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 */

/**
 * Gets the width of the OS scrollbar
 */
(function($) {
	var scrollbarWidth = 0;
	$.getScrollbarWidth = function() {
		if ( !scrollbarWidth ) {
			if ( $.browser.msie ) {
				var $textarea1 = $('<textarea cols="10" rows="2"></textarea>')
						.css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body'),
					$textarea2 = $('<textarea cols="10" rows="2" style="overflow: hidden;"></textarea>')
						.css({ position: 'absolute', top: -1000, left: -1000 }).appendTo('body');
				scrollbarWidth = $textarea1.width() - $textarea2.width();
				$textarea1.add($textarea2).remove();
			} else {
				var $div = $('<div />')
					.css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: -1000 })
					.prependTo('body').append('<div />').find('div')
						.css({ width: '100%', height: 200 });
				scrollbarWidth = 100 - $div.width();
				$div.parent().remove();
			}
		}
		return scrollbarWidth;
	};
})(jQuery);
;
/* END /2static/script/lib/jquery/plugins/jquery.getscrollbarwidth.js */
/* START /script/common/global.js */
//crucible global.js
var $dropdown	= 0;
var dropdownTimer = 0;

function openDropDown(id) {
    clearTimeout(dropdownTimer);

    // close old menu
    if( $dropdown && $dropdown.attr("id") != id) {
        $dropdown.fadeOut(100);
    }

    // get new menu and show it
    $dropdown = AJS.$("#"+id).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' );
        }
    }
}

var currentTab;
var currentBucket;
// rewrote so we don't set styles we don't have to.
function selectTab(tab) {
    if (currentTab) {
        currentTab.removeClass();
        currentBucket.hide();
    }
    currentTab = AJS.$("#" + tab + 'Tab')
            .attr('class','active');

    currentBucket = AJS.$("#" + tab + 'Bucket')
            .show();
}

function clearField(obj, defaultValue) {
    if (obj.value == defaultValue) {
        obj.value = '';
    }
}

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 showDiffForm(frxId) {

    simpleSwap('updateFrxFromFormToggle' + frxId, 'updateFrxFromFormSpan' + frxId);
    toggleNodeAndImage('frxinner' + frxId, true, false);

    var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/changeDiffDropdownAjax/";
    var params = {'frxId' : frxId, 'fromManagedFile':true};
    var spanName = "updateFrxFromFormSpan" + frxId;

    AJS.FECRU.AJAX.startSpin(spanName, false, "span", "");

    AJS.FECRU.AJAX.ajaxDo(url, params, function(resp) {
        var $span = AJS.$("#" + spanName);
        if ($span.length > 0) {
            AJS.FECRU.AJAX.stopSpin($span[0]);
        }
        if (resp.worked) {
            AJS.$("#" + spanName).html(resp.msgHtml);
            AJS.CRU.FRX.AJAX.updateFrxEditRevisionsDropdown(frxId);
        }
    });
}

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/fecru/star.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}

(function() {

    /**
     * html for the star edit dialog, to be inserted on demand.
     */
    var DIALOG_HTML = "<div id='astronomy'>" +
                          "<div id='astronomy-scape'>&nbsp;</div>" +
                          "<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>";

    ////// 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
        var isUserStar = starKeys["itemType"] === 'atlassian-user';
        var isCommitterStar = starKeys["itemType"] === 'atlassian-committer';
        if (isUserStar || isCommitterStar) {
            //todo: problem is that some committer names displays user hovers, and so the trigger for the hover
            //todo: is no longer available for use here, so cannot work out what the cache key is from the star.
            //todo: refactor this into a better caching mechanism, may be use the DOM to cache instead.
           AJS.FECRU.HOVER.invalidateCache('userlinks');
        }

    }

    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 + "'>");
            });
            // AJS.log("id = " + newId);
            if (keys["itemType"] == "atlassian-chart" || keys["itemType"] == "atlassian-search" ||
                keys["itemType"] == "atlassian-quicksearch") {
                editStar(newId, dialogControl, throbberControl);
            }
            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(dialogControl) {
        var $dialog = AJS.$(DIALOG_HTML);
        //intialize the dialog buttons
        AJS.$("#close-astronomy", $dialog).click(function() {
            dialogControl.hideDialog();
            return false;
        });
        //make the input dialog change style when user start inputting...
        //...and reset to the default if user did not input anything.
        var $starLabel = AJS.$("#star-labels", $dialog);
        $starLabel.focus(function() {
            if ($starLabel.attr("value") === $starLabel.attr("defaultValue")) {
                $starLabel.attr("value", "").addClass("generated");
            }
        }).blur(function() {
            //use the default if input is empty string or space
            if ($starLabel.attr("value").match(/^\s*$/)) {
                $starLabel.attr("value", $starLabel.attr("defaultValue")).removeClass("generated");
            }
        }).keypress(function(evnt) {
            //if user presses enter
            if (evnt.which == 13) {
                AJS.$("#save-star-label", $dialog).trigger('click');
                evnt.preventDefault();
            }
        }).attr("autocomplete", "off");

        return $dialog;
    }

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

        labels = (labels !== "") ? labels : LABEL_TIP;

        $starLabel.attr("value", labels);
        if ($starLabel.attr("value") !== LABEL_TIP) {
            $starLabel.addClass("generated");
        }
        else {
            $starLabel.removeClass("generated");
        }
        AJS.$("#remove-astronomy", $dialog).unbind().click(function(evnt) {
            evnt.preventDefault();
            removeStar();
            dialogController.hideDialog();
        });

        AJS.$("#save-star-label", $dialog).unbind().click(function(evnt) {
            evnt.preventDefault();
            var newLabel = $starLabel.val();
            if (newLabel !== LABEL_TIP) {
                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 = {};
        var PADDING = 5;
        var hoverOptions = {
            onHover: false,
            showArrow: true,
            fadeTime: 200,
            hideDelay: 200,
            showDelay: 0,
            width: 240 + PADDING,
            offsetX: -8, //to get the arrow centred at the star's vertical asix
            offsetY: 0,
            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.hideDialog = 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();
                        showPopup();
                    }
                },
                hideDialog: function() {
                    dialogHider.hideDialog();
                },
                getDialog: function () {
                    if ($dialog === undefined) {
                        $dialog = createStarDialog(dialogHider);
                    }
                    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", "star-inline-dialog", onStarClickedHandler, hoverOptions);
    }

    ////// onload events for stars
    AJS.toInit(function() {
        bindStars();
    });
})();
;
/* END /2static/script/fecru/star.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.
     */
    var toggleFolder = function($node, postFn, pathLinkFn, fileLinkFn) {
        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;
            }
            $node.after(AJS.$("<span class='dirlistSpinner'>&nbsp;</span>").show());
            $node.removeClass("closed").addClass("open");
            var params = AJS.$("#queryStrSuffix").val();
            if (params) {
                ajaxParameters.queryStrSuffix = params;
            }
            AJS.FECRU.AJAX.ajaxDo(fishEyePageContext + "/json/fe/loadSubTree.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) {
        AJS.$("span.tree").live("click", function(event) {
            if (AJS.$(event.target).is("a")) {
                return true; // let the link do its job
            }
            toggleFolder(AJS.$(this), function() {}, pathLinkFn, fileLinkFn);
            event.stopPropagation();
            return false;
        });
        if (pathLinkFn) {
            AJS.$("#navigation-tree a.pathLink").live("click", function(event) {
                return pathLinkFn(event);
            });
        }
        if (fileLinkFn) {
            AJS.$("#navigation-tree a.fileLink").live("click", function(event) {
                return fileLinkFn(event);
            });
        }
    };

    AJS.FECRU.BROWSE.initDirectoryTree = function(pathLinkFn, fileLinkFn) {
        AJS.FECRU.BROWSE.setupDirectoryTree(pathLinkFn, fileLinkFn);
        if (AJS.FE) {
            AJS.FE.setupPanes();
        }
        AJS.$("div.panel-directory","#content-navigation").scrollTo(AJS.$("#selectedDirTreeNode"));
    };
})();
;
/* 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)
        };
    };

    /**
     * @param optionalClass a string containing a css class that should be applied to the dialog
     */
    AJS.FECRU.DIALOG.create = function (maxWidth, maxHeight, id, optionalClass) {
        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});
        if (optionalClass) {
            dialog.addClass(optionalClass)
        }
        return dialog;
    };
})();
;
/* END /2static/script/fecru/dialog.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 = AJS.$('<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(
                        AJS.$.isFunction(job.url) ? job.url() : job.url,
                        AJS.$.isFunction(job.params) ? job.params() : job.params,
                        function(resp) {
                            try {
                                if (job.callback) {
                                    job.callback(resp);
                                }
                            } finally {
                                busy = false;
                                $jobs.dequeue();
                            }
                        },
                        AJS.$.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) {
        // 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 = AJS.$("#"+elementIdToUpdate);
                }

                if (!data.worked) {
                    AJS.$('#errorDesc').html("<span class='icon-error'>Error</span><p>" + ourHtmlEscape(data.errorMsg));
                    if (!noDialog) {
                        if (data.userError) {
                            showUserErrorBox();
                        } else {
                            showErrorBox();
                        }
                    }
                } else if ($updateMe && $updateMe.length === 1) {
                    $updateMe.html(data.payload);
                }
                if (onCompleteFunc) {
                    onCompleteFunc(data);
                }
            } catch (e) {
                displayErrorInResponse(e);
                cleanUpOnFail()    ;
            }
        };
        AJS.$.ajax( {
            type: "post",
            url: url,
            data: params,
            dataType: "json",
            success: onSuccess,
            complete: onComplete,
            error: noDialog ? (AJS.$.isFunction(noDialog) ? noDialog : NOOP) : ajaxFailure
        } );
        return false;
    };

    var NOOP = function () {};

    var ajaxFailure = function (req) {
        try {
            if (!isECMA(req)) {
                displayUnexpectedResponse(req);
                return;
            }
            var resp = eval('(' + req.responseText + ')');
            AJS.$('#errorDesc').html("<span class='icon-error'>Error</span><p>There was an error in the response from the server.<br>" +
                                     "Refresh the page before posting more comments.<br>" +
                                     "Click 'show details' for more information." );
            AJS.$('#showErrorDetails').show();
            appendErrorResponse(resp.errorMsg);
            showErrorBox();
        } catch (e) {
            AJS.log('ajax failure: ' + e);
            displayErrorInResponse(e);
        }
    };

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

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

    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) {
            return input.message || e.message;
        }
    };

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

        var $el = typeof(id) == 'object' ? AJS.$(id) : AJS.$("#"+id);
        var tag = tagType || 'div';
        if ($el.length === 1) {
            var $spinner = AJS.$(document.createElement(tag));
            $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 (el, tagType) {
        if (el) {
            var emptyDiv = document.createElement(tagType || "div");
            el.parentNode.replaceChild(emptyDiv, el.nextSibling);
        }
    };

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

    AJS.FECRU.AJAX.checkError = function (resp) {
        if (resp.error) {
            AJS.$('#errorDesc').html('<span class="icon-error">Error</span><p>' + resp.errorHtml);
            return true;
        }
        return false;
    };

    var appendErrorResponse = function (error) {
        if (error) {
            AJS.$('#errorResponses').show();
            AJS.$('#errorResponses').append(AJS.$('<div class="errorResponse-count">Error ' + (AJS.$('#errorResponses').find('.errorResponse').length + 1) + '</div>'));
            var errorMsg = error + (error.stack ? ":\n" + error.stack : "");
            AJS.$('#errorResponses').append(AJS.$('<div class="errorResponse"></div>').html( ourHtmlEscape(errorMsg) ));
        }
    };

    var dialog = 0;

    var showErrorBox = function () {
        showNotificationBox('An error has occurred');
    };

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

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

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

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

    var displayUnexpectedResponse = function (req) {
        if (isHTML(req)) {
            if (isHTMLFrag(req)) {
                AJS.$('#errorDesc').html("<span class='icon-error'>Error</span><p>There was an error in the response from the server.<br>" +
                                         "Refresh the page before posting more comments.<br>" +
                                         "Click 'show details' for more information." );
                appendErrorResponse(req.responseText);
                showErrorBox();
            } else {
                AJS.$('#errorDesc').html(AJS.$(req.responseText).filter('title').html());
                appendErrorResponse(AJS.$(req.responseText).filter('body').html());
                showErrorBox();
            }
        } else if (!req.responseText) {
            //no-op here: no responseText means most likely server is gone offline.
        }
    };

    var displayErrorInResponse = function (err) {
        AJS.$('#errorDesc').html("<span class='icon-error'>Error</span><p>There was an error in the response from the server.<br>" +
                                 "Refresh the page before posting more comments.<br>" +
                                 "Click 'show details' for more information." );
        appendErrorResponse(err);
        showErrorBox();
    };
})();
;
/* END /2static/script/fecru/ajax.js */
/* START /2static/script/fecru/hover.js */
if (!AJS.FECRU) {
    AJS.FECRU = {};
}
AJS.FECRU.HOVER = (function() {
    var opts = {
        onHover: true,
        showArrow: false,
        fadeTime: 200,
        hideDelay: 500,
        showDelay: 220,
        width: 300,
        offsetX: 10,
        offsetY: 2,
        container: "body",
        cacheContent:false,
        useLiveEvents : true
    };
    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 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) {
        respCache[type] = 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'
        });
    };

    var addCsLinkPopups = function() {
        var changesetKeyPattern = /^(\/\/([^/]+)\/)(.+)$/;
        addLinkPopups({
            linkSpanClass: "cslinkspan",
            url: fishEyePageContext + '/json/action/cstooltipdata.do',
            keyParser: function ($trigger) {
                return '//' + $trigger.find('input.repname').val() + '/' + $trigger.text();
            },
            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";
                }
            }
        });
    };

    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) {
                return {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 displayHandler = createDisplayHandler({
            createUrl: function($trigger) {
                return $trigger.attr('href');
            },
            createCacheKey:function($trigger) {
                return $trigger.attr('href');
            },
            createParams:function() {
                return {ajax :"true"};
            },
            cacheType:'userlinks',
            createTemplateContent:function(key, $trigger) {
                var $linkText = $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", "user-hover-inline-dialog", displayHandler, opts);
    };

    var addDeletedUserLinkPopups = function() {
        var hoverContent = function($contents, $trigger, showPopup) {
            $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", "deleted-user-hover-inline-dialog", hoverContent, opts);
    };

    var getSpinnerTemplate = function(content) {
        return ('<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">' +
                content +
                '</span>...</em></div>');
    };

    return {
        addAllLinkPopups: addAllLinkPopups,
        invalidateCache : invalidateCache
    };
})();;
/* 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 iframeHeight = height - HEADER_HEIGHT - BUTTON_HEIGHT;
            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 () {
                    $spinner.hide();
                };

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

            // 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() {
            var matches = /[?&]dialog=([^&]*)/.exec(location.search);
            if (matches) {
                return 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
        };
        if (useroptions) {
            AJS.$.extend(options, useroptions);
        }
        AJS.dropDown.Standard(options);
    };

    var setupFilterInlineDialog = function(trigger) {
        var PADDING = 2;
        var $filterBox = AJS.$("#header-constraint");
        if ($filterBox.length > 0) {
            var options = {
                onHover: false,
                showArrow: true,
                fadeTime: 200,
                hideDelay: 200,
                showDelay: 200,
                width: $filterBox.width() + PADDING,
                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();
            };
            AJS.InlineDialog(trigger, "header-constraint-inline-dialog", contentHandler, options);
        }
    };

    AJS.FECRU.UI.constraintToggle = function() {
        setupFilterInlineDialog(".constraint-toggle");
    };

    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");
            AJS.$(this).parents(".details").children(".details-verbose").toggle();
            return false;
        });

        // 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).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");
                AJS.$(which).hide();
            });
        });
        AJS.$("hover-close").click(function() {
            AJS.$(this).hide();
        });
        // filter bar in stream
        //	AJS.$(".stream-sift a").click(function () {
        //		AJS.$(".stream-sift li").removeClass("active");
        //		AJS.$(this).parent().addClass("active");
        //TODO: could be useful at a later date?
        //	});

        contentToggle();
        starDialog();
    };

    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.$("#" + prefix + "-table > tbody > tr").live("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.$('.sidebar-collapse').live("click", function() {
            AJS.$('#content').toggleClass('collapsed-sidebar');
            return false;
        });

        AJS.$(".accordion-head").live("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;
        });

        AJS.$("#accordion-toggle").live("click", function() {
            var $accordionToggle = AJS.$(this);
            var $accordionContent = AJS.$(".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.$("#content-toggle").live("click", function() {
            if (AJS.$("#content-toggle").html() == 'expand') {
                AJS.$(".details-verbose").show();
                AJS.$("#content-toggle").html("collapse").addClass("expanded");
            }
            else {
                AJS.$(".details-verbose").hide();
                AJS.$("#content-toggle").html("expand").removeClass("expanded");
            }
            return false;
        });

        AJS.$(".alwaysExpandChangesetsY").live("click", function() {
            AJS.$(".details-verbose").show();
            AJS.$("div.details-switch").addClass("details-expanded");
        });
        AJS.$(".alwaysExpandChangesetsN").live("click", function() {
            AJS.$(".details-verbose").hide();
            AJS.$("div.details-switch").removeClass("details-expanded");
        });
    };

    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
        });
    };

    var starDialog = function () {
        AJS.$(".close-astronomy").click(function() {
            AJS.$("#astronomy").hide();
            return false;
        });
        AJS.$("#star-labels").attr("autocomplete", "off");
        AJS.FECRU.UI.augmentFormFields("#star-labels");
    };

    AJS.FECRU.UI.augmentFormFields = function (which) {
        AJS.$(which).focus(function() {
            if (AJS.$(which).attr("value") == "Describe your favourite" ||
                AJS.$(which).attr("value") == "scroll to changeset" ||
                AJS.$(which).attr("value") == "Enter user name" ||
                AJS.$(which).attr("value") == "Search") {
                AJS.$(which).attr("value", "").addClass("generated");
            }
        });

        AJS.$(which).blur(function() {
            if (AJS.$(which).attr("value").match(/^\s*$/)) { //use the default if only have empty string or space
                AJS.$(which).attr("value", AJS.$(which).attr("defaultValue")).removeClass("generated");
            }
        });
    };

    AJS.FECRU.UI.changesetToggle = function (postOpenFn) {
        return function() {
            AJS.$(".stream-delta dt.hasDiff").click(function () {
                var $node = AJS.$(this);
                $node.next().toggle();
                $node.toggleClass("open");
                if (postOpenFn) {
                    postOpenFn(AJS.$(this));
                }
            });
            AJS.$(".stream-delta dt *").click(function (e) {
                e.stopPropagation();
            });
        };
    };
    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();
            var endDateStr = endDateInput.val();
            var startDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractDateStringFn(startDateStr));
            var endDate = AJS.$.datepicker.parseDate('yy-mm-dd', extractDateStringFn(endDateStr));

            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);
            }
        }
    };

    AJS.FECRU.UI.setupCalendar = function(addTime) {
        addTime = (typeof addTime == 'undefined') ? true : addTime;
        var calDateStart = AJS.$("input.calendar-date-start");
        calDateStart.attr("autocomplete", "off");
        calDateStart.datepicker({
            dateFormat: 'yy-mm-dd',
            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',
            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.$("#firebug-warning .close").live("click", function () {
                $warning.slideUp('fast', function() {
                    AJS.$.cookie(cookieName, 'Y', { expires: 365 });
                    if (onClose) {
                        onClose();
                    }
                });
            });
            $warning.prependTo(AJS.$("#cone"));
        }
    };

    AJS.FECRU.UI.toggleSearch = function () {
        AJS.$("h5:[rel='toggle']").live("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);
        });
    };

})();
;
/* END /2static/script/fecru/ui.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) {
    var onSelector = "." + name + onStateString;
    var offSelector = "." + name + offStateString;

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

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

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

    AJS.$(onSelector).live("click", function() {
        setPref(onStateString, onSelector, offSelector, reloadPage);
        return false;
    });
    AJS.$(offSelector).live("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/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;
            });
            AJS.FECRU.UI.augmentFormFields("#quick-search-input");

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

            if (typeof path == "function") {
	            var getPath = path;
            } else {
            	var getPath = function () {
	                return path;
	            };
            }

        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");
        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.contentNameMatches;

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

            // 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) {
                    if (selected.get(0).nodeName.toLowerCase() !== "a") {
                        var enclosingLinkHref = selected.find("a").attr("href");
                        if (enclosingLinkHref) {
                            window.location = enclosingLinkHref;
                        }
                    } else {
                        var selectedHref = selected.attr("href");
                        if (selectedHref) {
                            window.location = selectedHref;
                        }
                    }
                }
            }})[0];
            dd.$.attr("id", "quick-nav-drop-down");
            dd.$.insertAfter(searchBox);

            dd.onhide = function (causer) {
                if (causer == "escape") {
                    searchBox.focus();
                }
            };
            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.oldval = searchBox.val();
        searchBox.keyup(function () {
            var val = searchBox.val();

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

                    if ((/[\S]{2,}/).test(val)) {
                            if (cache[val]) {
                                jsonparser(cache[val]);
                            } else {
                                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: quicksearchURL + "?q=" + AJS.escape(val) + "&type=ajax&searchAllDirs=false",
                                        data: null,
                                        success: jsonparser,
                                        dataType: "json",
                                        timeout: 20000,
                                        error: function ( xml, status, e ) { // ajax error handler
                                            if (status == "timeout") {
                                                jsonparser({statusMessage: "Timeout", query: val}, status);
                                            }
                                        }
                                    });

                                }, 600);
                            }
                    } else {
                        dd && dd.hide();
                    }
                }
        };
    });
};
}
;
/* END /2static/script/lib/ajs/contentnamesearch.js */
/* START /2static/script/cru/crucible-ui.js */
if (!AJS.CRU) {
    AJS.CRU = {};
    AJS.CRU.UI = {};
} else if (!AJS.CRU.UI) {
    AJS.CRU.UI = {};
}

(function() {
    AJS.CRU.UI.contentHeight = 0;

    AJS.CRU.UI.columnFillHeight = function () {
        var head = AJS.$("#content").offset();
        var foot = AJS.$('#footer').outerHeight();
        var windowHeight = AJS.$(window).height() - foot - 1.3;//window - masthead - roundingerrorhack
        var contentHeight = windowHeight - head.top + 1; // TODO where does this +1 come from
        AJS.CRU.UI.contentHeight = contentHeight;

        AJS.$("#content").css({
            paddingBottom: 0,
            height: contentHeight
        });

        AJS.$("#content-resizable, #content-shield").css({
            height: contentHeight
        });

        AJS.$("#content-navigation-panel").css({
            height: windowHeight - AJS.$('#content-navigation-panel').offset().top - 1
        });

        AJS.$("#content-column").css({
            height: contentHeight,
            marginRight: "auto",
            marginLeft: "auto"
        });

        AJS.$("#frx-pane").css({
            height: windowHeight - AJS.$("#frx-pane").offset().top
        });

        AJS.CRU.UI.frxFillPane(AJS.$("#content-navigation-panel").height());
    };

    AJS.CRU.UI.columnResize = function () {
        AJS.$("#content-resizable").resizable({
            distance: 20,
            handles: "e"
        });
    };

    var singleFrxViewOn = false; // gets really initialised in document.ready below

    AJS.CRU.UI.isSingleFrxView = function () {
        return singleFrxViewOn;
    };

    AJS.CRU.UI.singleFrxView = function () {
        var cruFrxNav = AJS.CRU.FRX.NAV;
        var previouslyViewingFrxId = cruFrxNav.getCurrentFrxId();
        singleFrxViewOn = true;
        AJS.$("#single-frx-view-toggle").addClass("active");
        AJS.$("#all-frx-view-toggle").removeClass("active");
        AJS.$("body").addClass("single-frx-view").removeClass("all-frx-view");
        cruFrxNav.disableFrxScrollTracking();
        cruFrxNav.visibleFrxsChanged();
        cruFrxNav.setCurrentFrxAndScroll(previouslyViewingFrxId);
    };

    AJS.CRU.UI.allFrxView = function () {
        var cruFrxNav = AJS.CRU.FRX.NAV;
        var previouslyViewingFrxId = cruFrxNav.getCurrentFrxId();
        singleFrxViewOn = false;
        AJS.$("#single-frx-view-toggle").removeClass("active");
        AJS.$("#all-frx-view-toggle").addClass("active");
        AJS.$("body").addClass("all-frx-view").removeClass("single-frx-view");
        cruFrxNav.enableFrxScrollTracking();
        cruFrxNav.visibleFrxsChanged();
        cruFrxNav.setCurrentFrxAndScroll(previouslyViewingFrxId);
    };

    var fullscreen = false;

    AJS.CRU.UI.isFullscreen = function () {
        return fullscreen;
    };

    AJS.CRU.UI.toggleFullscreen = function () {
        fullscreen = !fullscreen;
        if(fullscreen) {
            AJS.$("#fullscreen-on").show();
            AJS.$("#fullscreen-off").hide();            
        } else {
            AJS.$("#fullscreen-on").hide();
            AJS.$("#fullscreen-off").show();
        }

        AJS.$("body").toggleClass("fullscreen", fullscreen);
        AJS.$(window).trigger('panes-resized');
        AJS.CRU.UI.columnFillHeight(); // Makes sure the content panes are the correct height
    };

    AJS.CRU.UI.highlightElements = function ($elem) {
        $elem.effect("highlight", { color: "#B0CCEA" }, 800);
    };

    AJS.CRU.UI.frxFillPane = function (pane) {
        var isCompliant = !!AJS.$.support.opacity;// w3c or IE
        var lastChild = AJS.$("#frxs .frxouter:last-child").attr("id");//just so IE gets some love
        var sheet;
        var full = (isCompliant ? pane : (pane - 1)) + "px";
        var half = (pane * 0.5) + "px !important";
        var cascade = "body.single-frx-view #generalCommentsInner {min-height:" + full + "}\n";
            cascade += "body.single-frx-view #frxs .frx-table-cloth {margin-bottom:" + half + "}\n";
            cascade += "#frxs #" + lastChild + " {padding-bottom:" + half + ";margin-bottom:0;}\n";

        if( isCompliant ){
            if (AJS.$("#dynamicStyle").length == 1) {//does the stylesheet exist?
                AJS.$("#dynamicStyle").text(cascade);
            }
            else {
                sheet = document.createElement("style");
                sheet.setAttribute("type", "text/css");
                sheet.setAttribute("id", "dynamicStyle");
                sheet.appendChild(document.createTextNode(cascade));

                AJS.$("head").append(sheet);
            }
        }
        else {//IE is happy to add the stylesheet, but not update it...
            AJS.$("#dynamicStyle").remove();//so we rip it out of the DOM, and give it a new one.
            sheet = document.createElement("style");
            sheet.setAttribute("type", "text/css");
            sheet.setAttribute("id", "dynamicStyle");
            sheet.styleSheet.cssText = cascade;

            AJS.$("head").append(sheet);
        }
    };

    AJS.$(document).ready(function(){
        // Give a warning if firebug is running
        // NB: do this before columnFillHeight() because this function may insert content into the page
        AJS.FECRU.UI.warnAboutFirebug(function() {
            AJS.CRU.UI.columnFillHeight();
        });

        // When resizing the window, resize the panes
        var resizeDelay = 1000;
        AJS.FECRU.UI.setCompletedResizeTimeout(window, function () {
            AJS.CRU.UI.columnFillHeight();
            AJS.$(window).trigger('panes-resized');
        },resizeDelay);

        AJS.$("#content-resizable").bind('resizestop', function () {
            AJS.$(window).trigger('panes-resized');
        });

        singleFrxViewOn = AJS.$("body").hasClass("single-frx-view");

        // And make sure the page is setup appropriately on first load
        AJS.CRU.UI.columnResize();
        AJS.CRU.UI.columnFillHeight();
        AJS.$("#fullscreen-off").show();
        AJS.$("#fullscreen-on").hide();
    });
})();
;
/* END /2static/script/cru/crucible-ui.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 () {

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

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


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

    AJS.CRU.UTIL.stopAjaxDialogSpin = function () {
        AJS.dim();
        AJS.$('body').removeClass('ajax-dialog');
    };

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

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

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

        params = params || {};

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

        if (util.isReviewPage() && AJS.$.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 = AJS.$('body').hasClass('review-updated');
                    if (reviewUpdated) {
                        AJS.CRU.REVIEW.UTIL.warnAboutReviewUpdates({ reshowWarning: true });
                    }
                    util.stopAjaxDialogSpin();
                    return util.ajaxDialog(url, AJS.$.extend(params, { reviewUpdated: reviewUpdated }));
                }
            });
            return false;

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

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

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


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

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

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

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

})();
;
/* END /2static/script/cru/util.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", "hover-over")
            .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";

        AJS.FECRU.AJAX.startSpin("dialogPanel", false, "span", "email-spinner");
        AJS.FECRU.AJAX.ajaxUpdate(url, {}, "dialogPanel", function () {
            AJS.FECRU.AJAX.stopSpin(AJS.$('#dialogPanel').get(0), "span");
            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 () {

    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 " + review.id();
    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() {
    AJS.$("a.action-link").live("click", function () {
        parent.top.AJS.$("#cru-review-history-dialog").data("dialog").hide();
    });
    AJS.$("a.comment-link").live("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.navigateDirectlyToComment({ commentId: comment_id }, scrollToMap);
    });
    AJS.$("a.frx-link").live("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 */
/* START /2static/script/cru/review/widgets/sliders.js */
/**
 * Range slider where the handles meet at a point (but don't overlap).
 */

AJS.CRU.WIDGETS = {};
(function() {
    function draggableRangeSlider(options) {

        var $slider = AJS.$(options.elem);
        var activeNub;
        var leftNub;
        var rightNub;

        var prevLeftValue;
        var prevRightValue;
        var prevLeftSlideValue;
        var prevRightSlideValue;

        function focused(nub) {
            return activeNub === nub;
        }

        /*
         * When mutating the slider values, don't cache the values of the slider
         * since mutating causes slider change events to be triggered.
         */
        function val(nub, value) {
            if (value !== undefined) {
                return $slider.slider('values', nub === leftNub ? 0 : 1, value);
            } else {
                return $slider.slider('values', nub === leftNub ? 0 : 1);
            }
        }

        $slider.slider({
            orientation: 'horizontal',
            range: true,
            min: options.min,
            max: options.max,
            values: options.values,

            start: function (event, ui) {
                activeNub = event.originalTarget || event.srcElement;
                prevLeftValue = prevLeftSlideValue = val(leftNub);
                prevRightValue = prevRightSlideValue = val(rightNub);
            },

            slide: function (event, ui) {
                // Prevent the nubs from overlapping, effectively bumping them along.
                if (val(leftNub) === val(rightNub)) {
                    if (focused(leftNub)) {
                        val(rightNub, val(rightNub) + 1);
                    }
                    if (focused(rightNub)) {
                        val(leftNub, val(leftNub) - 1);
                    }
                }
            },

            change: function (event, ui) {
                // Prevent the nubs from overlapping at each end.
                if (focused(leftNub) && val(leftNub) === options.max ) {
                    val(leftNub, val(leftNub) - 1);
                } else if (focused(rightNub) && val(rightNub) === options.min) {
                    val(rightNub, val(rightNub) + 1);
                }

                if (!(val(leftNub) === prevLeftSlideValue && val(rightNub) === prevRightSlideValue)) {
                    prevLeftSlideValue = val(leftNub);
                    prevRightSlideValue = val(rightNub);
                    return options.slide(val(leftNub), val(rightNub));
                }
            },

            stop: function (event, ui) {
                if (!(val(leftNub) === prevLeftValue && val(rightNub) === prevRightValue)) {
                    return options.change(val(leftNub), val(rightNub));
                }
            }
        });

        leftNub = $slider.find('.ui-slider-handle:first')[0];
        rightNub = $slider.find('.ui-slider-handle:last')[0];
    }

    /**
     * Slider for a given set of datapoints.
     */
    AJS.CRU.WIDGETS.datapointSlider = (function () {

        // Must be odd so that there is no single median tickmark between adjacent datapoints.
        var STEP = 6 + 1;

        function tickmarks(datapoints) {
            var result = [];
            for (var i = 0, len = datapoints.length; i < len; i += 1) {
                result.push(STEP * i + 1);
            }
            return result;
        }

        function quantizeTick(tick) {
            return Math.round((tick - 1) / STEP) * STEP + 1;
        }

        function leftTick(val) { return val + 1; }
        function leftVal(tick) { return tick - 1; }
        function rightTick(val) { return val; }
        function rightVal(tick) { return tick; }

        function createSet() {
            var result = {};
            var i, len;
            for (i = 0, len = arguments.length; i < len; i += 1) {
                if (arguments[i]) {
                    result[arguments[i]] = true;
                }
            }
            return result;
        }

        return function (settings) {
            var datapoints = settings.datapoints;
            var tick2dp = {};
            var dp2tick = {};
            (function () {
                var ticks = tickmarks(datapoints);
                for (var i = 0, len = ticks.length; i < len; i++) {
                    tick2dp[ticks[i]] = datapoints[i];
                    dp2tick[datapoints[i]] = ticks[i];
                }
            })();

            var selectedDPs = {
                start: settings.start || datapoints[0],
                end: settings.end || datapoints[datapoints.length - 1]
            };
            var activeDPs = createSet(selectedDPs.start, selectedDPs.end);

            draggableRangeSlider({
                elem: settings.elem,
                min: leftVal(1),
                max: rightVal((datapoints.length - 1) * STEP + 1),
                values: [leftVal(dp2tick[selectedDPs.start]), rightVal(dp2tick[selectedDPs.end])],

                slide: function (leftVal, rightVal) {
                    var newDPs = createSet(tick2dp[quantizeTick(leftTick(leftVal))], tick2dp[quantizeTick(rightTick(rightVal))]);
                    var dp;

                    for (dp in activeDPs) {
                        if (!newDPs[dp]) {
                            settings.inactive(dp);
                        }
                    }
                    for (dp in newDPs) {
                        if (!activeDPs[dp]) {
                            settings.active(dp);
                        }
                    }

                    activeDPs = newDPs;
                },

                change: function (left, right) {
                    var start = tick2dp[quantizeTick(leftTick(left))];
                    var end = tick2dp[quantizeTick(rightTick(right))];

                    // Snap nubs to the exact corresponding slider value for each datapoint.
                    AJS.$(settings.elem).slider('values', 0, leftVal(dp2tick[start]));
                    AJS.$(settings.elem).slider('values', 1, rightVal(dp2tick[end]));

                    if (start && end && !(start === selectedDPs.start && end === selectedDPs.end)) {
                        selectedDPs.start = start;
                        selectedDPs.end = end;
                        return settings.change(selectedDPs.start, selectedDPs.end);
                    }
                }
            });

            (function () {
                for (var dp in activeDPs) {
                    settings.active(dp);
                }

                // Need to set dimensions in terms of nubWidth so that the left and right
                // nub perfectly touch when next to eachother (i.e., no overlap or gap).
                // And we can't ask for the width at runtime because the slider is
                // initially hidden, so it's width is 0.
                var nubWidth = 16;
                AJS.$(settings.elem)
                    .width(nubWidth * (datapoints.length - 1) * STEP)
                    .closest('td')
                        .css({
                            'padding-left': STEP / 2 * nubWidth,
                            'padding-right': STEP / 2 * nubWidth
                        })
                    .end();
            })();

        };

    })();
})();
;
/* END /2static/script/cru/review/widgets/sliders.js */
/* START /2static/script/cru/review/widgets/scroll-tracker.js */
(function() {
    /**
     * Bind a callback that fires when scrolling is completed on matching elements.
     *
     * A scroll is considered complete after completionDelay ms have passed since
     * the last scroll event.
     */
    var setCompletedScrollTimeout = function (selector, callback, completionDelay) {
        completionDelay = completionDelay || 100;
        var timeout;

        AJS.$(selector).scroll(function () {
            if (completionDelay > 0 && timeout) {
                clearTimeout(timeout);
            }
            timeout = setTimeout(callback, completionDelay);
        });
    };

    var frxPaneId = 'frx-pane'

    /**
     * Bind a scroll tracker that monitors a scrollable container for matching
     * elements.
     *
     * After each completed scroll, if the currently active element is still in
     * the viewport/container, it remains active; if no longer in the viewport,
     * the top most element becomes active. If no tracked elements are in
     * the container, the active element prior to the completed scroll (if any)
     * remains active.
     *
     * The options.active and options.deactive callbacks are fired when the newly
     * calculated active element differs from the previously active element. The
     * options.outofview callback is called the first time the active element
     * becomes out of view (even partially).
     *
     * @param {jQuery selector|callback} options.selector expression/callback for elements to track
     * @param {jQuery selector} options.container scrollable container where the tracked elements live
     * @param {Number} options.threshold maximum number of px from the top/bottom of the container to start tracking elements
     * @param {Function} options.active callback where <tt>this</tt> is bound to the active element
     * @param {Function} options.deactive optional callback where <tt>this</tt> is bound to the previously active element
     * @param {Function} options.outofview optional callback where <tt>this</tt> is bound to the active element
     * @param {Number} options.delay optional delay (in ms) before a scroll is considered completed
     * @return {object} handle to the scroll tracker with methods to manipulate it's behaviour
     */
    AJS.CRU.WIDGETS.makeScrollTracker = function (options) {

        var trackedElements;
        var currentElement;
        var hasBeenOutOfView = false;
        var ignoreNextScroll = false;
        var sticky = false;

        options.container = options.container || window;
        options.threshold = options.threshold || 0;

        function lookupElements() {
            trackedElements = AJS.$.isFunction(options.selector) ? options.selector() : AJS.$(options.selector);
        }

        function filterElements(threshold) {
            threshold = typeof threshold === 'number' ? threshold : calcThreshold();
            var result;
            trackedElements.each(function (i) {
                if (AJS.$(this).filter(':in-viewport-vert(' + threshold + ', '+ frxPaneId + ')').length === 1) {
                    result = this;
                    return false;  // break
                }
            });
            return result;
        }

        function calcThreshold() {
            return Math.min(AJS.$(options.container).scrollTop(), options.threshold);
        }

        function topMostElement() {
            if (!trackedElements) {
                lookupElements();
            }
            return filterElements();
        }

        function updateCurrentElementAndFireEvents(newCurrentElement, shouldFireActivation) {
            if (currentElement && options.deactive) {
                options.deactive.call(currentElement);
            }
            currentElement = newCurrentElement;
            hasBeenOutOfView = false;
            if (currentElement && shouldFireActivation) {
                options.active.call(currentElement);
            }
        }

        function fireEventsIfOutOfView() {
            if (options.outofview && isOutOfView(currentElement)) {
                options.outofview.call(currentElement);
                hasBeenOutOfView = true;
            }
        }

        function isOutOfView(element) {
            var $container = AJS.$(options.container);
            var $element = AJS.$(element);
            var offset = $element.offset();

            return offset.top < $container.scrollTop() || $container.scrollTop() + $container.height() < offset.top + $element.height();
        }

        function mainloop() {
            if (ignoreNextScroll) {
                ignoreNextScroll = false;
                return;
            }

            var topElement;
            if (!(currentElement && sticky && AJS.$(currentElement).is(':in-viewport-vert(' + calcThreshold() + ', '+ frxPaneId + ')'))) {
                topElement = topMostElement();
                if (topElement && topElement !== currentElement) {
                    sticky = false;
                    updateCurrentElementAndFireEvents(topElement, true);
                }
            }
            if (currentElement && !hasBeenOutOfView) {
                fireEventsIfOutOfView();
            }
        }

        setCompletedScrollTimeout(
            options.container,
            mainloop,
            options.delay
        );

        AJS.$(document).ready(function () {
            lookupElements();
        });

        return {

            /**
             * Ignore the next scroll event.
             *
             * Useful for when you're programatically scrolling the page and don't want
             * the scroll tracker to recalculate the active element after that scroll.
             *
             * A typical calling pattern might be:
             * <pre>
             *     scrollTracker.ignoreNextScroll();
             *     // programatically scroll to `elem`
             *     scrollTracker.setCurrentElement(elem);
             * </pre>
             *
             * @see setCurrentElement
             */
            ignoreNextScroll: function () {
                ignoreNextScroll = true;
            },

            /**
             * Set the currently active element.
             *
             * Useful after calling ignoreNextScroll to programatically scroll
             * to a specific element.
             *
             * If element is not equal to getCurrentElement() this:
             * - will fire options.deactive,
             * - depending on fireActivation, may or may not fire options.active,
             * - depending on whether the element is out of view, may fire
             *   options.outofview
             *
             * @param {DOMElement} element the element to set as the active one
             * @param {Boolean} fireActivation whether to fire options.active (if applicable) for element (defaults to false)
             *
             * @see ignoreNextScroll
             */
            setCurrentElement: function (element, fireActivation) {
                fireActivation = fireActivation || false;

                if (element !== currentElement) {
                    updateCurrentElementAndFireEvents(element, fireActivation);
                }
                fireEventsIfOutOfView();
            },

            /**
             * Return the currently active element or undefined if there is none.
             */
            getCurrentElement: function () {
                return currentElement;
            },

            /**
             * Reevaluate the set of tracked elements, using the originally
             * provided selector expression/callback.
             *
             * Useful when tracked elements are removed from- or added to the DOM,
             * or their visiblity changes.
             */
            rescan: function () {
                lookupElements();
                // Calculate the new current element.
                mainloop();
            },

            /**
             * Returns the top most element visible in the actual container.
             *
             * @param threshold optional margin for the 'viewport'
             */
            getAbsoluteTopElement: function (threshold) {
                return filterElements(threshold || 0);
            },

            /**
             * Make the current element sticky, so that as long as it is in the viewport
             * it remains the current element.
             */
            makeCurrentElementSticky: function () {
                sticky = true;
            }

        };
    };
})();
;
/* END /2static/script/cru/review/widgets/scroll-tracker.js */
/* START /2static/script/cru/review/wiki/wiki-preview.js */
(function() {
    AJS.CRU = AJS.CRU || {};
    AJS.CRU.REVIEW = AJS.CRU.REVIEW || {};
    var getWikiPreviewStruct = function($container, filter) {
        var $previewButton = $container.find(".wiki-preview-button"),
                $textarea = $container.siblings("textarea" + (filter || "")),
                $editOnly = $container.parent().find(".edit-only"),
                $previewPane = $container.siblings(".wiki-preview-pane" + (filter || "")),
                $previewSpinner = $container.find(".wiki-preview-button-spinner"),
                wikiText = $textarea.val();
        return {
            container: $container,
            previewButton: $previewButton,
            textarea: $textarea,
            editOnly: $editOnly,
            previewPane: $previewPane,
            previewSpinner: $previewSpinner,
            wikiText:wikiText
        };
    };

    AJS.CRU.REVIEW.WIKI = AJS.CRU.REVIEW.WIKI || {
        resetPreview : function(parentId) {
            var struct = getWikiPreviewStruct(AJS.$((parentId ? ("#" + parentId + " ") : "") + "div a.wiki-preview-button").closest(".wiki-preview-button-holder"));
            var $previewButton = struct.previewButton,
                    $textarea = struct.textarea,
                    $editOnly = struct.editOnly,
                    $previewPane = struct.previewPane,
                    $previewSpinner = struct.previewSpinner;
            $previewPane.hide();
            $previewPane.text("");
            $previewButton.removeClass("wiki-preview-pane-selected");
            $previewPane.data("isInPreview", false);
            $previewSpinner.removeClass("wiki-preview-pane-spin");
            $textarea.show();
            $editOnly.removeClass('hidden');
            $previewButton.show();
        }
    };

    AJS.$(document).ready(function() {
        AJS.$("div a.wiki-preview-button").live("click", function() {
            var struct = getWikiPreviewStruct(AJS.$(this).closest(".wiki-preview-button-holder"), ":first");
            var $previewButton = struct.previewButton,
                    $textarea = struct.textarea,
                    $editOnly = struct.editOnly,
                    $previewPane = struct.previewPane,
                    $previewSpinner = struct.previewSpinner,
                    wikiText = struct.wikiText;

            var previewController = {
                toggleOn: function(html) {
                    $textarea.hide();
                    $editOnly.addClass('hidden');
                    $previewPane.html(html);
                    $previewPane.show();
                    $previewButton.addClass("wiki-preview-pane-selected");
                    $previewPane.data("isInPreview", true);
                },
                toggleOff: function() {
                    $previewPane.hide();
                    $previewPane.text("");
                    $textarea.show();
                    $editOnly.removeClass('hidden');
                    $previewButton.removeClass("wiki-preview-pane-selected");
                    $previewPane.data("isInPreview", false);
                }
            };

            if ($previewPane.data("isInPreview") === true) {
                previewController.toggleOff();
            } else {
                if (wikiText) {
                    var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/wikiPreviewJson";
                    var onComplete = function (resp) {
                        $previewSpinner.removeClass("wiki-preview-pane-spin");
                        $previewButton.show();
                        previewController.toggleOn(resp.html);
                    };
                    previewController.toggleOff();
                    $previewButton.hide();
                    $previewSpinner.addClass("wiki-preview-pane-spin");
                    AJS.FECRU.AJAX.ajaxDo(url, {wikiText:wikiText}, onComplete, true);
                }
            }
        });

        // live event for the comment post/cancel buttons
        AJS.$("form fieldset.comment-toolbar-holder button").live("click.previewReset", function() {
            AJS.CRU.REVIEW.WIKI.resetPreview();
        });
    });
})();;
/* END /2static/script/cru/review/wiki/wiki-preview.js */
/* START /2static/script/cru/review/review.js */
if (!AJS.CRU.REVIEW) {
    AJS.CRU.REVIEW = {};
}
if (!AJS.CRU.REVIEW.UTIL) {
    AJS.CRU.REVIEW.UTIL = {};
}
if (!AJS.CRU.REVIEW.TIMER) {
    AJS.CRU.REVIEW.TIMER = {};
}

(function() {

    AJS.CRU.REVIEW.UTIL.findJiraIssue = function () {
        var jiraIssueKey = AJS.$.trim(AJS.$("#jiraIssueKeyField").val());
        if (!jiraIssueKey) {
            return;
        }
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/findJiraIssueAjax";
        var params = {jiraIssueKey : jiraIssueKey, autoLink : true};
        var $button = AJS.$("#jiraFindButton").attr("disabled", "disabled");

        AJS.FECRU.AJAX.startSpin("jiraFindButton", false, "span", "");

        var done = function(resp) {
            if (resp.worked) {
                AJS.$("#linked-issue-container").html(resp.jiraEditHtml);
                AJS.$("#jiraDisplayDiv").replaceWith(resp.jiraDisplayHtml);
                AJS.$("#frx-filter-subtasks,#frx-filter-unresolvedsubtasks").removeClass("disabled");
            } else {
                AJS.$("#linked-issue-error").html(resp.errorMsg);
                $button.removeAttr("disabled");
                AJS.$("#frx-filter-subtasks,#frx-filter-unresolvedsubtasks").addClass("disabled");
                AJS.FECRU.AJAX.stopSpin($button.get(0), "span");
            }
        };

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

    AJS.CRU.REVIEW.UTIL.unlinkJiraIssue = function () {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/findJiraIssueAjax';
        var params = {unlinkJira : true};

        var $button = AJS.$("#jiraClearButton").attr('disabled', 'disabled');

        var done = function (resp) {
            if (resp.worked) {
                AJS.$("#linked-issue-container").html(resp.jiraEditHtml);
                AJS.$("#jiraDisplayDiv").replaceWith(resp.jiraDisplayHtml);
                AJS.$("#frx-filter-subtasks,#frx-filter-unresolvedsubtasks").removeClass("disabled");
            } else {
                $button.removeAttr("disabled");
            }
        };

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

    /**
     * This function is used from review details dialog to quickly link the review
     * to a jira issue that was suggested by Crucible.
     *
     * @param jiraIssueKey
     */
    AJS.CRU.REVIEW.UTIL.findAndLinkJiraIssue = function (jiraIssueKey) {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/findJiraIssueAjax/";
        var params = {jiraIssueKey : jiraIssueKey, autoLink : true};

        AJS.$("#jiraFindButton").attr("disabled", "disabled");

        var ajax = AJS.FECRU.AJAX;

        ajax.startSpin("jiraDisplaySpinner", false, "span", "");

        ajax.ajaxDo(url, params, function(resp) {
            ajax.stopSpin(AJS.$("#jiraDisplaySpinner")[0]);
            if (resp.worked) {
                AJS.$("#linked-issue-container").html(resp.jiraEditHtml);
                AJS.$("#jiraDisplayDiv").html(resp.jiraDisplayHtml);
                AJS.$("#jiraIssueQuickLink").hide();
                AJS.$("#frx-filter-subtasks,#frx-filter-unresolvedsubtasks").removeClass("disabled");
            } else {
                AJS.$("#linked-issue-container").append(AJS.$('<span>' + resp.errorMsg + '</span>'));
                AJS.$("#frx-filter-subtasks,#frx-filter-unresolvedsubtasks").addClass("disabled");
            }
        });
    };

    AJS.CRU.REVIEW.UTIL.addSuggestedReviewer = function (userid) {
        AJS.$("#rc_" + userid)
            .addClass('selected new')
            .find("input:first").attr("checked", "checked");
            AJS.CRU.REVIEW.UTIL.reorderReviewers(userid);

        AJS.$("#sug_" + userid)
                .hide();
    };

    AJS.CRU.REVIEW.UTIL.$editDetailsForm = function() {
        var $form = AJS.$('#editDetailsForm');
        if ($form.length === 0) {
            $form = AJS.$('#editReviewIframe').contents().find('#editDetailsForm');
        }
        return $form;
    };

    AJS.CRU.REVIEW.UTIL.reorderReviewers = function (addedId) {
        var minReviewersInColumn = 2;
        AJS.$('.reviewers-chosen .reviewer-span-holder').remove();
        var $reviewerSpanHolders = AJS.$('.reviewers-unchosen .reviewer-row.selected .reviewer-span-holder');
        var i, len = $reviewerSpanHolders.length;

        if (len <= minReviewersInColumn) {
            $reviewerSpanHolders.clone(true).appendTo('.left-reviewers-column');
        } else {
            var maxInColumn = minReviewersInColumn;
            if (len > 3 * minReviewersInColumn) {
                maxInColumn = Math.ceil(len / 3);
            }
            for (i = 0; i < len; i++) {
                if (i < maxInColumn) {
                    AJS.$($reviewerSpanHolders.get(i)).clone(true).appendTo('.left-reviewers-column');
                } else if (i < maxInColumn * 2) {
                    AJS.$($reviewerSpanHolders.get(i)).clone(true).appendTo('.middle-reviewers-column');
                } else {
                    AJS.$($reviewerSpanHolders.get(i)).clone(true).appendTo('.right-reviewers-column');
                }
            }
        }
        if (addedId) {
            AJS.CRU.UI.highlightElements(AJS.$('.reviewer-span' + addedId).parent());
        }

    };

    AJS.CRU.REVIEW.UTIL.postEditDetailsForm = function (onCompleteFunc) {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/postDetails';
        var $form = AJS.CRU.REVIEW.UTIL.$editDetailsForm();
        if ($form.length === 0) {
            if (onCompleteFunc) {
                onCompleteFunc();
            }
            return false;
        }

        var params = $form.serialize() + '&command=norender';
        AJS.FECRU.AJAX.ajaxDo(url, params, onCompleteFunc);
        return false;
    };

    AJS.CRU.REVIEW.UTIL.closeReviewAjax = function () {
        var params = {
            summary: AJS.$("#reviewSummaryInput").val()
        };

        AJS.CRU.UTIL.stateTransition('action:closeReview', permaId, params);
        return false;
    };

    AJS.CRU.REVIEW.UTIL.postLinkedReview = function () {
        var newParentId = AJS.$("#parentReviewId").val();
        if (!newParentId) {
            return;
        }
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/linkReview';
        var params = '&parentReviewId=' + newParentId;
        var $button = AJS.$("#linkReviewSaveButton").attr('disabled', 'disabled');

        var ajax = AJS.FECRU.AJAX;

        ajax.startSpin("linkedReviewSpinner", false, "span", "");

        var done = function (resp) {
            if (resp.worked) {
                AJS.$("#unlinkReview").html(resp.unlinkHtml).show();
                AJS.$("#linkReviewForm").hide();
            }
            ajax.stopSpin(AJS.$('#linkedReviewSpinner').get(0), "span");
            $button.removeAttr('disabled');
        };

        ajax.ajaxDo(url, params, done);
    };

    AJS.CRU.REVIEW.UTIL.postUnlinkReview = function () {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/linkReview';
        var params = {unlinkParent : true };

        var ajax = AJS.FECRU.AJAX;

        ajax.startSpin("linkedReviewSpinner", false, "span", "");
        var $button = AJS.$("#linkReviewUnlinkButton").attr('disabled', 'disabled');

        var done = function (resp) {
            if (resp.worked) {
                AJS.$("#unlinkReview").hide().html("");
                AJS.$("#linkReviewForm").show();
            }
            ajax.stopSpin(AJS.$('#linkedReviewSpinner').get(0), "span");
            $button.removeAttr('disabled');
        };

        ajax.ajaxDo(url, params, done);
    };

    AJS.CRU.REVIEW.UTIL.filterAndExpandFrxs = function (filters) {
        var $filterOptions = AJS.$('#frxFilterOptions');
        AJS.$.each(filters, function (i, filter) {
            $filterOptions.find('li[id=frx-filter-' + filter + ']').addClass('selected');
        });
        AJS.CRU.FRX.filterFrxs();
        AJS.CRU.FRX.expandFrxs(review.frxIds());
    };

    /**
     * @param opts.done called when the request was successful
     * @param opts.error see noDialog param of AJS.FECRU.AJAX.ajaxUpdate
     */
    AJS.CRU.REVIEW.UTIL.warnAboutReviewUpdates = function (opts) {
        opts = opts || {};
        var notShownYet = !AJS.$('body').hasClass('review-updated');
        AJS.CRU.REVIEW.UTIL.reviewUpdatedAjax({
            done: function () {
                if (AJS.$('body').hasClass('review-updated')) {
                    if (notShownYet || opts.reshowWarning) {
                        AJS.$('#review-updated-warning').slideDown('fast');
                    }
                }
                opts.done && opts.done();
            },
            error: opts.error
        });
    };

    AJS.CRU.REVIEW.UTIL.reorderParticipants = function() {

        var parent = AJS.$("#participant-list-runner");
        var children = parent.children();

        var sortedReviewers = review.getReviewersSortedByCompletedness();

        for (var i = 0, len = children.length; i < len; i++) {
            var idExpected = sortedReviewers[i].getId();
            var child = children[i];
            if (child.id != ('review-participant-' + idExpected)) {
                AJS.$(child).before(AJS.$('#review-participant-' + idExpected, parent));
                children = parent.children();
            }
        }
    };

    AJS.CRU.REVIEW.UTIL.redrawStatePercentComplete = function(complete) {
        var $statusBar = AJS.$('.status-open');
        $statusBar.closest('ul').attr('title', complete + '% complete');
        //12 is the minimum, 180 is the width including the first 12, so we need to * by 180 - 12 = 168
        var completeChunk = (12 + (complete/100 * 168)) ;
        $statusBar.css({
            "background-position": completeChunk + "px 0px"
        });
    };

    /**
     * If on the review page, checks if the review has been updated since last refresh.
     *
     * If the review has been updated, the body has the class `review-updated` added.
     * Silently fails if the Ajax request fails.
     *
     * @param opts.done no arg callback called regardless of whether the request is made.
     * @param opts.error see noDialog param of AJS.FECRU.AJAX.ajaxUpdate
     */
    AJS.CRU.REVIEW.UTIL.reviewUpdatedAjax = function (opts) {
        opts = opts || {};

        var util = AJS.CRU.UTIL;
        if (!util.isReviewPage() || !review.isLoaded()) {
            opts.done && opts.done();
            return;
        }

        var url = util.jsonUrlBase(review.id()) + '/reviewUpdatedAjax';

        // TODO Invert the logic so that server sends the last modified timestamps, FRXs, and revisions.
        // Serialize render times and visible frxs.
        var params = {
            reviewRenderTime: review.getRenderTime().getTime()
        };
        var frxIds = [];
        var frxRenderTimes = [];
        var frxSliderRevsLengths = [];
        var frxSliderRevsArrays = [];
        AJS.$.each(review.frxs(), function (i, frx) {
            frxIds.push(frx.id());
            frxRenderTimes.push(frx.getRenderTime().getTime());
            var sliderRevs = frx.getSliderFrxRevisions();
            frxSliderRevsLengths.push(sliderRevs.length);
            frxSliderRevsArrays.push(sliderRevs);
        });
        params.frxIds = frxIds;
        params.frxRenderTimes = frxRenderTimes;
        params.frxSliderRevsLengths = frxSliderRevsLengths;
        params.frxSliderRevs = Array.prototype.concat.apply([], frxSliderRevsArrays);
        params.reviewStateName = review.getStateName();
        params.backingOff = backingOff;
        var timeSpent = AJS.CRU.REVIEW.TIMER.getAndReset();
        params.timeSpent = timeSpent;

        AJS.FECRU.AJAX.ajaxDo(url, params, function (resp) {
            if (resp.worked) {
                var noticedChanged = review.getUpdatedReviewCommentIds().length !== resp.updatedReviewComments.length ||
                                     review.getUpdatedFileCommentIds().length !== resp.updatedFileComments.length ||
                                     review.getUpdatedInlineCommentIds().length !== resp.updatedInlineComments.length ||
                                     review.toBeRemovedFrxIds().length !== resp.frxsRemoved.length ||
                                     review.isOutOfDate() != resp.reviewUpdated ||
                                     review.outOfDateFrxIds().length !== resp.frxsUpdated.length ||
                                     review.getPendingFrxIds().length !== resp.frxsAdded.length ||
                                     review.getPendingStateName() !== resp.stateName ||
                                     review.hasBeenUncompleted() != resp.hasBeenUncompleted;

                review.setLastCheckedUpdateTime(new Date());
                review.setUpdatedReviewCommentIds(resp.updatedReviewComments);
                review.setUpdatedFileCommentIds(resp.updatedFileComments);
                review.setUpdatedInlineCommentIds(resp.updatedInlineComments);

                var i, len;

                var removedFrxIds = resp.frxsRemoved;
                for (i = 0,len = removedFrxIds.length; i < len; i++) {
                    var removedFrxId = removedFrxIds[i];
                    var removedFrx = review.frx(removedFrxId);
                    if (removedFrx) {
                        removedFrx.setToBeRemoved(true);
                    }
                }
                review.setOutOfDate(resp.reviewUpdated);
                review.setFrxsOutOfDate(resp.frxsUpdated);
                review.setPendingFrxIds(resp.frxsAdded);
                review.setPendingUnreadFrxIds(resp.unreadFrxs);
                review.setPendingStateName(resp.stateName);
                review.setPendingMetaStateName(resp.metaStateName);
                review.setHasBeenUncompleted(resp.hasBeenUncompleted);

                if (resp.alsoViewingHtml) {
                    AJS.CRU.REVIEW.UTIL.displayAlso(resp.alsoViewingHtml);
                } else {
                    AJS.$('#also-viewing').hide();
                }

                var $timeSpentInput = AJS.$('#time-spent-input');
                if (resp.timeSpent && !AJS.CRU.REVIEW.TIMER.editing) {
                    var minutes = Math.round(resp.timeSpent / 60000);
                    $timeSpentInput.val(minutes + (minutes == 1 ? " minute" : " minutes")).show();
                }

                if (review.isPendingClosed()) {
                    AJS.$('#time-spent').addClass('disabled')
                            .removeClass('enabled');
                    $timeSpentInput.attr('disabled', 'disabled')
                            .addClass('disabled')
                            .removeClass('enabled');
                } else {
                    AJS.$('#time-spent').removeClass('disabled')
                            .addClass('enabled');
                    $timeSpentInput.removeAttr('disabled')
                            .removeClass('disabled')
                            .addClass('enabled');
                }

                AJS.$('#review-updated-warning div.more-info').html(resp.updateMessageHtml);

                var completednesses = resp.participantCompletedCounts;
                if (completednesses) {
                    var completednessChanged = false;
                    for (i = 0,len = completednesses.length; i < len; i++) {
                        var values = completednesses[i];
                        var user = values.user;
                        var percentage = values.percentage;
                        var isComplete = values.isComplete;
                        var chunk = 100 - (Math.floor(percentage / 20) * 20);//divides the percentage into chunks of 20

                        var changed = review.updateReviewerCompleteness(user, percentage, isComplete);

                        var $detailsParticipantRow = AJS.$("#details-participant-" + user);
                        if (changed) {
                            var $headerParticipant = AJS.$("#review-participant-" + user);
                            $detailsParticipantRow.find('.reviewer').toggleClass("completedReviewer", isComplete);
                            $headerParticipant.toggleClass("completedReviewer", isComplete);
                            var $progress = $headerParticipant.children(".reviewer-progressbar");
                            var $detailsProgress = $detailsParticipantRow.find('.reviewer-progressbar');
                            if ($progress.length > 0) {
                                $progress.css({"background-position": chunk + "% 0%"});
                                $progress.attr('title', percentage + '% complete');
                                $detailsProgress.css({"background-position": chunk + "% 0%"});
                                $detailsProgress.attr('title', percentage + '% complete');
                                $detailsParticipantRow.find('.role-progress').text(percentage);
                            }
                            completednessChanged = true;
                        }
                        if (values.timeSpent) {
                            $detailsParticipantRow.find('.timeSpent').text(values.timeSpent);
                        }
                    }
                    AJS.$('#details-participants-total .timeSpent').text(resp.totalTimeSpent);
                    if (completednessChanged) {
                        AJS.CRU.REVIEW.UTIL.reorderParticipants(completednesses);
                    }
                }

                if (resp.reviewStatePercentComplete != review.getStatePercentComplete()) {
                    review.setStatePercentComplete(resp.reviewStatePercentComplete);
                    AJS.CRU.REVIEW.UTIL.redrawStatePercentComplete(review.getStatePercentComplete());
                }

                if (resp.showMessage) {
                    if (!opts.recheck) {
                        AJS.$('body').addClass('review-updated');
                    }
                } else {
                    AJS.$('body').removeClass('review-updated');
                }

                if (noticedChanged) {
                    AJS.$('#review-updated-warning').removeClass('collapsed');
                    AJS.$('#review-updated-warning a.collapse').text('Collapse');
                }
            } else {
                //on error, roll time that should have been logged back into the timer for the next update
                AJS.CRU.REVIEW.TIMER.elapsedTime += timeSpent;
            }
            opts.done && opts.done();
        }, opts.error);
    };


    var lastActivityMs = new Date().getTime();
    var backingOff = false;
    var pollingDelay;

    /**
     * For the first INACTIVITY_BEFORE_BACKOFF ms (e.g., 5 min) since the last activity, poll
     * every DELAY_MIN ms (e.g., 20 sec). After the INACTIVITY_BEFORE_BACKOFF, start backing
     * off the polling period to a maximum of DELAY_MAX ms (e.g., 1 hr).
     *
     * The backoff is required, e.g., if you leave your browser open overnight.
     */
    function calcTimeout() {
        var DELAY_MIN = 1000 * 20;
        var DELAY_MAX = 1000 * 60 * 60;
        var DELAY_MULTIPLIER = 1.25;
        var INACTIVITY_BEFORE_BACKOFF = 1000 * 60 * 5;

        var t = new Date().getTime() - lastActivityMs;
        if (t < INACTIVITY_BEFORE_BACKOFF) {
            backingOff = false;
            pollingDelay = DELAY_MIN;
        } else {
            backingOff = true;
            pollingDelay *= DELAY_MULTIPLIER;
            //also stop time-tracking if backing off
            AJS.CRU.REVIEW.TIMER.stop();
        }
        pollingDelay = pollingDelay > DELAY_MAX ? DELAY_MAX : pollingDelay;
        return pollingDelay;
    }

    var pollingTimer;
    var pollRequestActive = false;


    function doPollingLoop() {
        var rutil = AJS.CRU.REVIEW.UTIL;
        if (!pollRequestActive) {
            pollRequestActive = true;
            rutil.warnAboutReviewUpdates({
                done: chainPollingLoop,
                // Continue requesting when the server is down in case it comes back up.
                error: chainPollingLoop
            });
        }
    }

    function chainPollingLoop() {
        pollingTimer = setTimeout(doPollingLoop, calcTimeout());
        pollRequestActive = false;
    }

    AJS.CRU.REVIEW.UTIL.resetInactivityTimer = function () {
        lastActivityMs = new Date().getTime();
    };

    AJS.CRU.REVIEW.UTIL.startPollingForReviewUpdates = function () {
        AJS.CRU.REVIEW.UTIL.stopPollingForReviewUpdates();
        doPollingLoop();
    };

    AJS.CRU.REVIEW.UTIL.stopPollingForReviewUpdates = function () {
        if (pollingTimer) {
            clearTimeout(pollingTimer);
            pollingTimer = undefined;
        }
    };

    // also-viewing
    AJS.CRU.REVIEW.UTIL.displayAlso = function (viewing) {
        var scrolling = {};
        if (!AJS.$.browser.msie) {
            AJS.$("#frxs .frxouter:visible").each(function(){
                if (AJS.$(this).outerHeight() >= AJS.$("#content-column").height()) {scrolling.up = true;}
                if (AJS.$(this).outerWidth() >= AJS.$("#content-column").width()) {scrolling.side = true;}
            });

            AJS.$("#bottom-status-notifications").css({
                "right": scrolling.up ? "15px" : "0px",
                "bottom": scrolling.side ? "15px" : "0px"
            });
        }
        else {
            AJS.$("#bottom-status-notifications").css({
                "right": "17px",
                "bottom": "17px"
            });
        }

        AJS.$('#also-viewing').html(viewing).show();
    };

    // time-tracking
    AJS.CRU.REVIEW.TIMER.startTime = new Date().getTime();
    AJS.CRU.REVIEW.TIMER.elapsedTime = 0;
    AJS.CRU.REVIEW.TIMER.running = false;
    AJS.CRU.REVIEW.TIMER.editing = false;

    /* Start the timer (no-effect if already running) */
    AJS.CRU.REVIEW.TIMER.start = function () {
        var timer = AJS.CRU.REVIEW.TIMER;
        if (!timer.running) {
            timer.startTime = new Date().getTime();
            timer.running = true;
        }
    };

    /* Stop the timer (no-effect if not running) */
    AJS.CRU.REVIEW.TIMER.stop = function () {
        var timer = AJS.CRU.REVIEW.TIMER;
        if (timer.running) {
            timer.elapsedTime += new Date().getTime() - timer.startTime;
            timer.running = false;
        }
    };

    /* Get the counted ms and reset the timer */
    AJS.CRU.REVIEW.TIMER.getAndReset = function () {
        var timer = AJS.CRU.REVIEW.TIMER;
        if (timer.editing) {
            return 0;
        }
        timer.stop();
        var count = timer.elapsedTime;
        timer.elapsedTime = 0;
        timer.start();
        return count;
    };

    /* User is currently editing time spent - time is accrued, but not logged during this time */
    AJS.CRU.REVIEW.TIMER.startEditing = function () {
        AJS.CRU.REVIEW.TIMER.editing = true;
    };

    /* User has finished editing - ditch the accrued time if update was successful */
    AJS.CRU.REVIEW.TIMER.stopEditing = function (updated) {
        var timer = AJS.CRU.REVIEW.TIMER;
        timer.editing = false;
        if (updated) {
            timer.getAndReset();
        }
    };

    /* debug - I suspect we'll need this again
     AJS.CRU.REVIEW.TIMER.log = function (msg) {
     console.log(msg + " count=" + AJS.CRU.REVIEW.TIMER.count + " running=" + AJS.CRU.REVIEW.TIMER.running +
     " startTime=" + AJS.CRU.REVIEW.TIMER.startTime);
     };
     */

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

        // Poll immediately on focus and start the polling loop.
        AJS.$(window).focus(function () {
            if (review.isLoaded()) {
                // Restart polling for updates.
                var rutil = AJS.CRU.REVIEW.UTIL;
                rutil.resetInactivityTimer();
                rutil.startPollingForReviewUpdates();
                AJS.CRU.REVIEW.TIMER.start();
            } else {
                // Once all FRXs are loaded, frx.js calls startPollingForReviewUpdates.
            }
        });

        // Resetting on keypress (of shortcuts) is handled by keynav.js since it prevents propagation.
        AJS.$('body').click(function () {
            AJS.CRU.REVIEW.UTIL.resetInactivityTimer();
            AJS.CRU.REVIEW.TIMER.start();
        });

        // Don't poll at all when the window doesn't have focus.
        AJS.$(window).blur(function () {
            AJS.CRU.REVIEW.UTIL.stopPollingForReviewUpdates();
            AJS.CRU.REVIEW.TIMER.stop();
        });

        /* because $(window).blur() is inneffective when switching tabs in FF */
        AJS.$(document).blur(function () {
            //todo should we add AJS.CRU.REVIEW.UTIL.stopPollingForReviewUpdates(); here?
            AJS.CRU.REVIEW.TIMER.stop();
        });

        var removeAndReloadFrxs = function() {
            var toBeRemovedFrxIds = review.toBeRemovedFrxIds();
            var removeFrxFromPage = AJS.CRU.CREATE.removeFrxFromPage;
            for (var i = 0, len = toBeRemovedFrxIds.length; i < len; i++) {
                removeFrxFromPage(toBeRemovedFrxIds[i]);
            }

            var pendingUnreadFrxIds = review.getPendingUnreadFrxIds();
            for (i = 0,len = pendingUnreadFrxIds.length; i < len; i++) {
                AJS.CRU.FRX.AJAX.changeFileReadStatusClass(pendingUnreadFrxIds[i], false, 'unread');
            }

            AJS.$('#details .review-meta-links .file-count').text(review.frxs().length);

            review.setPendingFrxIds([]);
            AJS.CRU.FRX.reloadFrxs({}, review.outOfDateFrxIds());
        };

        var processUpdatedComments = function(commentsToRetrieve, updatedComments, commentsToRemove, type, commentContainer) {
            for (var i = 0, len = updatedComments.length; i < len; i++) {
                var commentResp = updatedComments[i];
                delete commentsToRemove[commentResp.id];
                commentator.replaceOrInsertComment(type,
                        commentResp.id,
                        commentResp.commentHtml,
                        commentContainer + (commentResp.frxId || ''));
                commentator.createOrUpdateComment(commentResp);
                commentsToRetrieve.pop(commentResp.id);
            }
        };

        AJS.$('#review-updated-warning a.reload').live('click', function () {
            var frxId = AJS.CRU.FRX.NAV.getCurrentFrxId();
            var commentForm = commentator.getDisplayingCommentForm();
            if (commentForm) {
                var frxOuterId = commentator.getDisplayingCommentForm().getTextBox().closest('.frxouter').attr('id');
                var scrollToFrxId = frxOuterId !== 'generalComments' ? frxOuterId.replace(/^frxouter/, '') : frxOuterId;
                AJS.CRU.FRX.NAV.gotoFrx({ frxId: scrollToFrxId , destination: '' });
                commentForm.getTextBox().focus();
                AJS.$('#review-updated-warning .comment-warning').show();

                return false;
            } else {
                AJS.$('#review-updated-warning .comment-warning').hide();
            }
            if (review.frx(frxId)) {
                window.location.hash = 'CFR-' + frxId;
            }
            if (review.isOutOfDate()) {
                window.location.reload();
            } else {
                AJS.CRU.REVIEW.UTIL.reviewUpdatedAjax({
                    done: function () {
                        // review is now up to date - update render times as though
                        // entire page was loaded at the last check time
                        review.setRenderTime(review.getLastCheckedUpdateTime());

                        if (review.hasBeenUncompleted()) {
                            var $uncompleteButton = AJS.$('li.stateAction.uncomplete');
                            $uncompleteButton.unbind('click');
                            $uncompleteButton.attr('onclick', '');
                            $uncompleteButton.click(function() {
                                AJS.CRU.UTIL.stateTransition('action:completeReview', permaId);
                            })
                                    .removeClass('uncomplete')
                                    .addClass('complete');
                            $uncompleteButton.find('span').text('Complete');
                            review.setHasBeenUncompleted(false);
                        }

                        var i, len;
                        var frxs = review.frxs();
                        for (i = 0,len = review.frxs().length; i < len; i++) {
                            frxs[i].setRenderTime(review.getLastCheckedUpdateTime());
                        }

                        if (review.getUpdatedReviewCommentIds().length > 0 ||
                            review.getUpdatedFileCommentIds().length > 0 ||
                            review.getUpdatedInlineCommentIds().length > 0) {
                            removeAndReloadComments();
                        }

                        if (review.getPendingFrxIds().length > 0) {
                            AJS.CRU.CREATE.retrieveNewFrxs(function () {
                                review.setPendingFrxIds([]);
                                removeAndReloadFrxs();
                            });
                        } else {
                            removeAndReloadFrxs();
                        }
                    },
                    recheck: true
                });
            }
            AJS.$('body').removeClass('review-updated');
            return false;
        });

        var removeAndReloadComments = function() {
            var commentator = AJS.CRU.COMMENT;
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/retrieveCommentsAjax';

            var reviewCommentsToRetrieve = review.getUpdatedReviewCommentIds();
            var fileCommentsToRetrieve = review.getUpdatedFileCommentIds();
            var inlineCommentsToRetrieve = review.getUpdatedInlineCommentIds();

            var params = {};
            params.reviewCommentsToRetrieve = reviewCommentsToRetrieve;
            params.fileCommentsToRetrieve = fileCommentsToRetrieve;
            params.inlineCommentsToRetrieve = inlineCommentsToRetrieve;

            if (params.inlineCommentsToRetrieve.length > 0) {
                var frxIds = [];
                var frxFromRevs = [];
                var frxToRevs = [];
                AJS.$.each(review.frxs(), function (i, frx) {
                    frxIds.push(frx.id());
                    frxFromRevs.push(frx.visibleFromRevision() || frx.visibleToRevision());
                    frxToRevs.push(frx.visibleToRevision());
                });
                params.frxIds = frxIds;
                params.frxFromRevs = frxFromRevs;
                params.frxToRevs = frxToRevs;
            }

            AJS.FECRU.AJAX.ajaxDo(url, params, function (resp) {
                if (resp.worked) {
                    var commentsToRemove = {}; //add all comments to retrieve, then remove each as we retrieve them. remove any comments still in the list
                    var i, len;

                    for (i = 0,len = reviewCommentsToRetrieve.length; i < len; i++) {
                        commentsToRemove[reviewCommentsToRetrieve[i]] = true;
                    }
                    for (i = 0,len = fileCommentsToRetrieve.length; i < len; i++) {
                        commentsToRemove[fileCommentsToRetrieve[i]] = true;
                    }
                    for (i = 0,len = inlineCommentsToRetrieve.length; i < len; i++) {
                        commentsToRemove[inlineCommentsToRetrieve[i]] = true;
                    }

                    processUpdatedComments(reviewCommentsToRetrieve, resp.updatedReviewComments, commentsToRemove, 'general', 'general-comments-container');
                    review.clearUpdatedReviewCommentIds();

                    processUpdatedComments(fileCommentsToRetrieve, resp.updatedFileComments, commentsToRemove, 'revision', 'revision_comments_frxinner');
                    review.clearUpdatedFileCommentIds();

                    var updatedInlineComments;
                    updatedInlineComments = resp.updatedInlineComments;
                    for (i = 0,len = updatedInlineComments.length; i < len; i++) {
                        var commentResp;
                        commentResp = updatedInlineComments[i];

                        //remove the old comment if it exists
                        if (review.comment(commentResp.id)) {
                            commentator.removeCommentHtml(review.comment(commentResp.id));
                        }

                        var frx = review.frx(commentResp.frxId);

                        //insert 'above' comment
                        AJS.$("#inline_comments_frxinner" + frx.id()).append(commentResp.aboveCommentHtml);

                        var isFromOnSkippedLines = false;
                        var isToOnSkippedLines = false;
                        var isOnSkippedLines = false;
                        if (!commentResp.hidden) {
                            var lastTo = commentResp.lastTo;
                            var lastFrom = commentResp.lastFrom;
                            var $sourceFrxInner = AJS.$("#sourcefrxinner" + frx.id());
                            var lastToLine = $sourceFrxInner.find("tr.to" + lastTo + '.sourceLine');
                            var lastFromLine = $sourceFrxInner.find("tr.from" + lastFrom + '.sourceLine');


                            //if lastFromLine or lastToLine is not found, look in skipped sections
                            if (lastToLine.length === 0 || lastFromLine.length === 0) {
                                var $startOfDiff = $sourceFrxInner.find("#diffStart" + frx.id());
                                var $firstFromLine = $sourceFrxInner.find(".sourceLine.commentableLine.fromLine:first");
                                var $firstToLine = $sourceFrxInner.find(".sourceLine.commentableLine.toLine:first");
                                if ($firstFromLine.length === 1) {
                                    var firstFromLine;
                                    if ($firstFromLine.hasClass("toLine")) {
                                        var firstFromLineId = $firstFromLine.attr('id');
                                        firstFromLineId = firstFromLineId.replace('Both', '');
                                        firstFromLine = parseInt((firstFromLineId.split('line')[1]).split(',')[0], 10);
                                    } else {
                                        firstFromLine = parseInt($firstFromLine.attr('id').split('lineFrom')[1], 10);
                                    }
                                    if (lastFrom <= firstFromLine) {
                                        lastFromLine = $startOfDiff;
                                        isFromOnSkippedLines = true;
                                    }
                                }
                                if ($firstToLine.length === 1) {
                                    var firstToLine;
                                    if ($firstToLine.hasClass("fromLine")) {
                                        var firstToLineId = $firstToLine.attr('id');
                                        firstToLineId = firstToLineId.replace('Both', '');
                                        firstToLine = parseInt(($firstToLine.attr('id').split('line')[1]).split(',')[1], 10);
                                    } else {
                                        firstToLine = parseInt($firstToLine.attr('id').split('lineTo')[1], 10);
                                    }
                                    if (lastTo <= firstToLine) {
                                        lastToLine = $startOfDiff;
                                        isToOnSkippedLines = true;
                                    }
                                }

                                var $skippedSections = $sourceFrxInner.find('tr.diffSkipped');
                                for (var sectionIndex = 0, sectionLength = $skippedSections.length; sectionIndex < sectionLength; sectionIndex++) {
                                    if (lastToLine.length > 0 && lastFromLine.length > 0) {
                                        break;
                                    }
                                    var $skippedSection = AJS.$($skippedSections[sectionIndex]);
                                    var skippedSectionIdBits = $skippedSection.attr('id').split('_');
                                    if (lastFrom > 0 && lastFromLine.length === 0) {
                                        var nextFrom = parseInt(skippedSectionIdBits[1], 10);
                                        if (lastFrom <= nextFrom) {
                                            lastFromLine = $skippedSection;
                                            isFromOnSkippedLines = true;
                                        }
                                    }
                                    if (lastTo > 0 && lastToLine.length === 0) {
                                        var nextTo = parseInt(skippedSectionIdBits[2], 10);
                                        if (lastTo <= nextTo) {
                                            lastToLine = $skippedSection;
                                            isToOnSkippedLines = true;
                                        }
                                    }
                                }

                                if (lastToLine.length === 0) {
                                    lastToLine = $sourceFrxInner.find("tr:last");
                                    isFromOnSkippedLines = true;
                                }
                                if (lastFromLine.length === 0) {
                                    lastFromLine = $sourceFrxInner.find("tr:last");
                                    isToOnSkippedLines = true;
                                }
                            }

                            //get the last of lastFromLine and lastToLine
                            var lastSelectedLine;
                            if (lastToLine.length === 0) {
                                lastSelectedLine = lastFromLine;
                                isOnSkippedLines = isFromOnSkippedLines;
                            } else if (lastFromLine.length === 0) {
                                lastSelectedLine = lastToLine;
                                isOnSkippedLines = isToOnSkippedLines;
                            } else if (lastToLine.nextAll('#' + lastFromLine.attr('id')).length === 0) {
                                lastSelectedLine = lastToLine;
                                isOnSkippedLines = isToOnSkippedLines;
                            } else {
                                lastSelectedLine = lastFromLine;
                                isOnSkippedLines = isFromOnSkippedLines;
                            }

                            //insert the comment in line
                            if (lastSelectedLine) {
                                var colSpan = frx.colspan();
                                var $cell = AJS.$("<td colspan='" + colSpan + "'></td>")
                                        .html(commentResp.inlineCommentHtml);
                                AJS.$("<tr class='comment-row'></tr>")
                                        .append($cell)
                                        .insertAfter(lastSelectedLine);
                                commentator.setCommentWidths('inlinecomment' + commentResp.id, true);
                            }
                        }

                        //update review-model and arrays
                        commentator.createOrUpdateComment(commentResp);
                        inlineCommentsToRetrieve.pop(commentResp.id);
                        delete commentsToRemove[commentResp.id];

                        tetrisCommentController.renderTetrisCommentMarkersForComment(commentResp.id);

                        //set 'skipped line' message
                        if (lastSelectedLine && isOnSkippedLines) {
                            AJS.$('#' + review.comment(commentResp.id).domId() + ' .comment-actions-primary').append('<span class="moreComments">' +
                                                                                                                     '<a class="comment-skipped" id="comment-skipped' + commentResp.id + '" ' +
                                                                                                                     'title="click to show full context">comment on skipped line ' +
                                                                                                                     (lastTo === 0 ? lastFrom : lastTo) + (lastTo === 0 ? '(revision ' +
                                                                                                                                                                          commentResp.fromRevString + ')' : '') + '</a>' +
                                                                                                                     '</span>');
                        }
                    }
                    review.clearUpdatedInlineCommentIds();

                    // delete any comments not returned
                    for (var commentId in commentsToRemove) {
                        var comment = review.comment(commentId);
                        if (comment) {
                            commentator.removeCommentHtml(review.comment(commentId));
                            review.removeComment(comment);

                            if (comment.frx()) {
                                commentator.checkGeneralCommentsWarning(comment.frx().id());
                            } else {
                                commentator.checkGeneralCommentsWarning();
                            }
                        }
                    }
                    commentator.updateCommentCount();

                    //update participant comment details
                    var participantCommentDetails = resp.participantCommentDetails;
                    for (i = 0, len = participantCommentDetails.length; i < len; i++) {
                        var participant = participantCommentDetails[i];
                        var userId = participant.user;
                        var detailsParticipant = AJS.$('#details-participant-' + userId);
                        if (participant.latestComment) {
                            detailsParticipant.find('.latest-comment').html(participant.latestComment);
                        }
                        if (participant.authoredComments) {
                            detailsParticipant.find('.authored-comments').text(participant.authoredComments);
                        }
                    }

                }

            });
        };

        //start time-tracking
        AJS.CRU.REVIEW.TIMER.start();
    });

})();
;
/* END /2static/script/cru/review/review.js */
/* START /2static/script/cru/review/review-model.js */

function Review() {
    // Member variables
    this.m_permaId = undefined;
    this.m_frxs = {};
    this.m_firstFrx = undefined;
    this.m_lastFrx = undefined;
    this.m_frxCacheDirty = true;
    this.m_frxArrayCache = []; // Optimisation array, should never be accessed directly but through this.frxs()
    this.m_comments = {};
    this.m_commentCacheDirty = true;
    this.m_commentArrayCache = []; // Optimisation array, should never be accessed directly but through this.comments()
    this.m_loaded = false;
    this.m_writable = false;
    this.m_stateName = undefined;
    this.m_stateVerbage = undefined;
    this.m_pendingStateName = undefined;
    this.m_pendingMetaStateName = undefined;
    this.m_metaStateName = undefined;
    this.m_hasBeenUncompleted = false;
    this.m_name = undefined;
    this.m_transitioningState = undefined;
    this.m_summarize = undefined;
    this.m_renderTime = undefined;
    this.m_lastCheckedUpdateTime = undefined;
    this.m_commentFrxId = {};
    this.m_outOfDate = false;
    this.m_pendingFrxIds = [];
    this.m_pendingUnreadFrxIds = [];
    this.m_updatedReviewCommentIds = []; // ids
    this.m_updatedFileCommentIds = []; // ids
    this.m_updatedInlineCommentIds = []; // ids
    this.m_reviewerCompleteness = {};
    this.m_statePercentComplete = 0;
    this.m_loggedInUsername = undefined;
}

// Function which maps an array of [FRX|Comment] -> array of object ids
Review.prototype.idArray = function(obj) {
    return AJS.$.map(obj, function (obj) {
        return obj.id();
    });
};

// Convert to a plain array instead of associative
Review.prototype.map2array = function(map) {
    var array = [];
    AJS.$.each(map, function() {
        array.push(this);
    });

    return array;
};

Review.prototype.id = function() {
    return this.m_permaId;
};


Review.prototype.setId = function(id) {
    this.m_permaId = id;
    return this;
};

Review.prototype.isLoaded = function () {
    return this.m_loaded;
};

Review.prototype.setLoaded = function (loaded) {
    this.m_loaded = loaded;
    return this;
};

Review.prototype.writable = function() {
    return this.m_writable;
};

Review.prototype.setWritable = function(writable) {
    this.m_writable = writable;
    return this;
};

Review.prototype.isOpen = function () {
    return this.getMetaStateName() === 'OPEN';
};

Review.prototype.isClosed = function () {
    return this.getMetaStateName() === 'CLOSED';
};

Review.prototype.isDraft = function () {
    return this.getMetaStateName() === 'DRAFT';
};

Review.prototype.isPendingOpen = function () {
    return this.getPendingMetaStateName() === 'OPEN';
};

Review.prototype.isPendingClosed = function () {
    return this.getPendingMetaStateName() === 'CLOSED';
};

Review.prototype.isPendingDraft = function () {
    return this.getPendingMetaStateName() === 'DRAFT';
};

Review.prototype.getStateName = function () {
    return this.m_stateName;
};

Review.prototype.setStateName = function (stateName) {
    this.m_stateName = stateName;
    return this;
};

Review.prototype.getStateVerbage = function () {
    return this.m_stateVerbage;
};

Review.prototype.setStateVerbage = function (stateVerbage) {
    this.m_stateVerbage = stateVerbage;
    return this;
};

Review.prototype.getPendingStateName = function () {
    return this.m_pendingStateName;
};

Review.prototype.setPendingStateName = function (stateName) {
    this.m_pendingStateName = stateName;
    return this;
};

Review.prototype.getPendingMetaStateName = function () {
    return this.m_pendingMetaStateName;
};

Review.prototype.setPendingMetaStateName = function (stateName) {
    this.m_pendingMetaStateName = stateName;
    return this;
};

Review.prototype.getMetaStateName = function () {
    return this.m_metaStateName;
};

Review.prototype.setMetaStateName = function (metaStateName) {
    this.m_metaStateName = metaStateName;
    return this;
};

Review.prototype.hasBeenUncompleted = function () {
    return this.m_hasBeenUncompleted;
};

Review.prototype.setHasBeenUncompleted = function (uncompleted) {
    this.m_hasBeenUncompleted = uncompleted;
    return this;
};

Review.prototype.isEmpty = function() {
    return this.frxs().length === 0;
};

Review.prototype.setName = function(name) {
    this.m_name = name;
    return this;
};

Review.prototype.setLoggedInUsername = function(username) {
    this.m_loggedInUsername = username;
    return this;
};

Review.prototype.getLoggedInUsername = function() {
    return this.m_loggedInUsername;
};

Review.prototype.isTransitioning = function() {
    return !!this.m_transitioningState;
};

Review.prototype.setTransitioningState = function(state) {
    this.m_transitioningState = state;
    return this;
};

Review.prototype.isSummarize = function() {
    return this.m_summarize;
};

Review.prototype.setSummarize = function(summarize) {
    this.m_summarize = summarize;
    return this;
};

Review.prototype.setRenderTime = function (date) {
    this.m_renderTime = new Date(date.getTime());
    return this;
};

Review.prototype.getRenderTime = function () {
    return new Date(this.m_renderTime.getTime());
};

Review.prototype.setLastCheckedUpdateTime = function (date) {
    this.m_lastCheckedUpdateTime = new Date(date.getTime());
    return this;
};

Review.prototype.getLastCheckedUpdateTime = function () {
    return new Date(this.m_lastCheckedUpdateTime.getTime());
};

Review.prototype.isOutOfDate = function () {
    return this.m_outOfDate;
};

Review.prototype.setOutOfDate = function (outOfDate) {
    this.m_outOfDate = outOfDate;
    return this;
};

/******************
 ****   FRXS   ****
 ******************/

Review.prototype.frxs = function() {
    if (this.m_frxCacheDirty) {
        this.m_frxArrayCache = this.frxArray();
        this.m_frxCacheDirty = false;
    }
    return this.m_frxArrayCache;
};

Review.prototype.frxArray = function() {
    var array = [];
    var nextFrx = this.m_firstFrx;
    while (nextFrx) {
        array.push(nextFrx);
        nextFrx = nextFrx.getNextFrx();
    }
    return array;
};



Review.prototype.frxMap = function() {
    return this.m_frxs;
};

Review.prototype.frx = function(frxId) {
    return this.m_frxs[frxId];
};

Review.prototype.frxIds = function() {
    return this.idArray(this.frxs());
};

Review.prototype.addFrx = function(frx, prevFrxId) {
    var existingFrx = this.frx(frx.id());
    var prevFrx = this.frx(prevFrxId);
    //first remove any existing frx with the same id
    if (existingFrx) {
        prevFrx = existingFrx.getPrevFrx();
        prevFrxId = prevFrx ? prevFrx.id() : 'generalComments';
        this.removeFrx(existingFrx);
    }

    if (prevFrxId === 'generalComments') {
        if (this.m_firstFrx) {
            frx.setNextFrx(this.m_firstFrx);
            this.m_firstFrx.setPrevFrx(frx);
            this.m_firstFrx = frx;
        } else {
            //the only frx
            this.m_firstFrx = frx;
            this.m_lastFrx = frx;
        }
    } else {
        if (!prevFrx) {
            //if no prev - append to the end
            prevFrx = this.m_lastFrx;
        }
        if (!prevFrx) {
            //if still no prevFrx - this is the only frx
            this.m_firstFrx = frx;
            this.m_lastFrx = frx;
        } else {
            //insert frx between prevFrx and prevFrx.next
            var nextFrx = prevFrx.getNextFrx();
            prevFrx.setNextFrx(frx);
            frx.setPrevFrx(prevFrx);
            frx.setNextFrx(nextFrx);
            if (!nextFrx) {
                //there was no next frx - frx is the new last
                this.m_lastFrx = frx;
            } else {
                nextFrx.setPrevFrx(frx);
            }
        }
    }
    this.m_frxs[frx.id()] = frx;
    this.m_frxCacheDirty = true;
    return this;
};

Review.prototype.removeFrx = function(frx) {
    var that = this;
    AJS.$.each(frx.comments(), function() {
        that.removeComment(this);
    });
    var prevFrx = frx.getPrevFrx();
    var nextFrx = frx.getNextFrx();
    if (prevFrx) {
        prevFrx.setNextFrx(frx.getNextFrx());
    } else {
        this.m_firstFrx = frx.getNextFrx();
    }
    if (nextFrx) {
        nextFrx.setPrevFrx(frx.getPrevFrx());
    } else {
        this.m_lastFrx = frx.getPrevFrx();
    }

    delete this.m_frxs[frx.id()];
    this.m_frxCacheDirty = true;
    return this;
};

Review.prototype.completeFrxs = function() {
    return AJS.$.grep(this.frxs(), function(frx) {
        return frx.isComplete();
    });
};

Review.prototype.completeFrxIds = function() {
    return this.idArray(this.completeFrxs());
};

Review.prototype.incompleteFrxs = function() {
    return AJS.$.grep(this.frxs(), function(frx) {
        return !frx.isComplete();
    });
};

Review.prototype.incompleteFrxIds = function() {
    return this.idArray(this.incompleteFrxs());
};

Review.prototype.visibleFrxIdsWithComments = function() {
    return this.idArray(AJS.$.grep(this.frxs(), function(frx) {
        return frx.hasComments() && !frx.isFiltered();
    }));
};

Review.prototype.outOfDateFrxIds = function() {
    return this.idArray(AJS.$.grep(this.frxs(), function(frx) {
        return frx.isOutOfDate();
    }));
};

Review.prototype.toBeRemovedFrxIds = function() {
    return this.idArray(AJS.$.grep(this.frxs(), function(frx) {
        return frx.isToBeRemoved();
    }));
};

Review.prototype.getPendingFrxIds = function() {
    return this.m_pendingFrxIds;
};

Review.prototype.setPendingFrxIds = function(frxIds) {
    this.m_pendingFrxIds = frxIds;
    return this;
};

Review.prototype.getPendingUnreadFrxIds = function() {
    return this.m_pendingUnreadFrxIds;
};

Review.prototype.setPendingUnreadFrxIds = function(frxIds) {
    this.m_pendingUnreadFrxIds = frxIds;
    return this;
};

Review.prototype.setFrxsOutOfDate = function(frxIds) {
    var frxs = this.frxs();
    var i, len;
    for (i = 0,len = frxs.length; i < len; i++) {
        frxs[i].setOutOfDate(false);
    }
    for (i = 0,len = frxIds.length; i < len; i++) {
        var frx = this.frx(frxIds[i]);
        if (frx) {
            frx.setOutOfDate(true);
        }
    }
    return this;
};

Review.prototype.hasUnloadedFrxs = function() {
    var frxs = this.frxs();
    for (var i = 0, len = frxs.length; i < len; i++) {
        if (!frxs[i].isLoaded()) {
            return true;
        }
    }
    return false;
};

Review.prototype.setFrxsUnloaded = function() {
    AJS.$.each(this.m_frxs, function() {
        this.setLoaded(false);
    });
    return this;
};

/******************
 **** COMMENTS ****
 ******************/
Review.prototype.addComment = function(comment) {
    this.m_comments[comment.id()] = comment;
    this.m_commentCacheDirty = true;
    return this;
};

Review.prototype.removeComment = function(comment) {
    comment.clearReplies();
    if (comment.isReply()) {
        // remove the dead comment from the parent's children list
        var siblings = comment.replyTo().getReplies();
        for (var i = 0, len = siblings.length; i < len; i++) {
            if (siblings[i] === comment) {
                siblings.splice(i, 1);
                break;
            }
        }
    }
    delete this.m_comments[comment.id()];
    if (comment.frx()) {
        comment.frx().removeComment(comment);
    }
    this.m_commentCacheDirty = true;
    return this;
};

Review.prototype.comment = function(id) {
    return this.m_comments[id];
};

Review.prototype.comments = function() {
    if (this.m_commentCacheDirty) {
        this.m_commentArrayCache = this.map2array(this.m_comments);
        this.m_commentCacheDirty = false;
    }
    return this.m_commentArrayCache;
};

Review.prototype.commentIds = function() {
    return this.idArray(this.comments());
};

Review.prototype.generalComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return /^general/.test(comment.type());
    });
};

Review.prototype.unreadGeneralComments = function() {
    return AJS.$.grep(this.generalComments(), function(comment) {
        return comment.status() === 'unread' || comment.status() === 'leaveUnread';
    });
};

Review.prototype.generalCommentIds = function() {
    return this.idArray(this.generalComments());
};

Review.prototype.fileComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return /^revision/.test(comment.type());
    });
};

Review.prototype.fileCommentIds = function() {
    return this.idArray(this.fileComments());
};

Review.prototype.inlineComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return /^inline/.test(comment.type());
    });
};

Review.prototype.inlineCommentIds = function() {
    return this.idArray(this.inlineComments());
};

Review.prototype.unreadComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return comment.status() === 'unread' || comment.status() === 'leaveUnread';
    });
};

Review.prototype.draftComments = function () {
    return AJS.$.grep(this.comments(), function (comment) {
        return comment.draft();
    });
};

/** Should only be used when generating the initial response. */
Review.prototype.setCommentFrxId = function (commentId, frxId) {
    this.m_commentFrxId[commentId] = frxId;
    return this;
};

/** @return undefined if it is not an frx or inline comment. */
Review.prototype.getCommentFrxId = function (commentId) {
    var comment = review.comment(commentId);
    if (comment) {
        var frx = comment.frx();
        return frx ? frx.id() : undefined;
    } else {
        return this.m_commentFrxId[commentId];
    }
};

Review.prototype.getUpdatedReviewCommentIds = function() {
    return this.m_updatedReviewCommentIds;
};

Review.prototype.getUpdatedFileCommentIds = function() {
    return this.m_updatedFileCommentIds;
};

Review.prototype.getUpdatedInlineCommentIds = function() {
    return this.m_updatedInlineCommentIds;
};

Review.prototype.setUpdatedReviewCommentIds = function(comments) {
    this.m_updatedReviewCommentIds = comments;
    return this;
};

Review.prototype.setUpdatedFileCommentIds = function(comments) {
    this.m_updatedFileCommentIds = comments;
    return this;
};

Review.prototype.setUpdatedInlineCommentIds = function(commentIds) {
    this.m_updatedInlineCommentIds = commentIds;
    return this;
};

Review.prototype.clearUpdatedReviewCommentIds = function() {
    return this.setUpdatedReviewCommentIds([]);
};

Review.prototype.clearUpdatedFileCommentIds = function() {
    return this.setUpdatedFileCommentIds([]);
};

Review.prototype.clearUpdatedInlineCommentIds = function() {
    return this.setUpdatedInlineCommentIds([]);
};

Review.prototype.setReviewer = function(id, username, percentComplete, isCompleted) {
    this.m_reviewerCompleteness[id] = new Reviewer(id, username, percentComplete, isCompleted);
};

// returns true if the values have changed
Review.prototype.updateReviewerCompleteness = function(id, percentComplete, isCompleted) {
    var reviewer = this.m_reviewerCompleteness[id];
    if (!reviewer) {
        // this is a reviewer that has been added to the review, we're not displaying new reviewers so we should ignore it
        return false;
    } else {
        return reviewer.updateReviewer(percentComplete, isCompleted);
    }
};

Review.prototype.setStatePercentComplete = function(percent) {
    this.m_statePercentComplete = percent;
};

Review.prototype.getStatePercentComplete = function() {
    return this.m_statePercentComplete;
};

Review.prototype.getReviewersSortedByCompletedness = function() {
    var reviewerList = [];
    for (var reviewerId in this.m_reviewerCompleteness) {
        var reviewer = this.m_reviewerCompleteness[reviewerId];
        if (reviewer != undefined) {
            reviewerList.push(reviewer);
        }
    }
    reviewerList.sort(function(reviewerA, reviewerB) {
        //if a reviewer has completed, their weighting should be more than 100%
        var aCompleted = reviewerA.getHasCompleted() ? 101 : reviewerA.getPercentageComplete();
        var bCompleted = reviewerB.getHasCompleted() ? 101 : reviewerB.getPercentageComplete();

        var diff = aCompleted - bCompleted;
        if (diff != 0) {
            return diff;
        }
        if (reviewerA.getSortName() == reviewerB.getSortName()) {
            return 0;
        } else if (reviewerA.getSortName() < reviewerB.getSortName()) {
            return -1;
        } else {
            return 1;
        }
    });
    return reviewerList;
};

function Reviewer(id, username, percentageComplete, hasCompleted) {
    this.m_id = id;
    this.m_username = username;
    this.m_percentageComplete = Math.floor(percentageComplete / 20) * 20;
    this.m_hasCompleted = hasCompleted;
}

// returns true if the values have changed
Reviewer.prototype.updateReviewer = function(percentageComplete, hasCompleted) {
    var oldPercent = this.m_percentageComplete;
    var oldCompleted = this.m_hasCompleted;
    this.m_percentageComplete = Math.floor(percentageComplete / 20) * 20;
    this.m_hasCompleted = hasCompleted;
    return oldPercent != this.m_percentageComplete || oldCompleted != this.m_hasCompleted;
};

Reviewer.prototype.getId = function() {
    return this.m_id;
};
Reviewer.prototype.getSortName = function() {
    return this.m_username;
};
Reviewer.prototype.getPercentageComplete = function() {
    return this.m_percentageComplete;
};
Reviewer.prototype.getHasCompleted = function() {
    return this.m_hasCompleted;
};

function Frx(id, review) {
    this.m_id = id;
    this.m_review = review;

    this.m_prevFrx = undefined;
    this.m_nextFrx = undefined;

    this.m_isComplete = false;
    this.m_readStatus = undefined;
    this.m_readStatusLocked = false;
    this.m_colspan = 4;
    this.m_isLoaded = false;
    this.m_isLoading = false;
    this.m_isExpanded = false;
    this.m_isFiltered = false;
    this.m_isBinary = false;
    this.m_isDirectory = false;
    this.m_isNewSinceComplete = false;
    this.m_renderTime = undefined;
    this.m_outOfDate = false;
    this.m_toBeRemoved = false;

    // revision stuff
    this.m_frxRevisions = [];
    this.m_sliderFrxRevisions = undefined;
    this.m_visibleFromRevision = undefined;
    this.m_visibleToRevision = undefined;
    this.m_visibleFromSCMRevision = undefined;
    this.m_visibleToSCMRevision = undefined;
    this.m_frxRevisionToCruRevisionMap = {};

    this.m_commentCacheDirty = true;
    this.m_commentArrayCache = []; // Optimisation array, should never be accessed directly but through this.comments()
    this.m_comments = {};

    // diff opts
    this.m_context = undefined;
    this.m_whitespace = undefined;
    this.m_diffLayout = undefined;
    this.m_ignoreBlankLines = undefined;
}

Frx.prototype.getContext = function() {
    return this.m_context;
};

Frx.prototype.getWhitespace = function() {
    return this.m_whitespace;
};

Frx.prototype.getDiffLayout = function() {
    return this.m_diffLayout;
};

Frx.prototype.setContext = function(context) {
    this.m_context = context;
    return this;
};

Frx.prototype.setWhitespace = function(whitespace) {
    this.m_whitespace = whitespace;
    return this;
};

Frx.prototype.setDiffLayout = function(diffLayout) {
    this.m_diffLayout = diffLayout;
    return this;
};

Frx.prototype.getIgnoreBlankLines = function() {
    return this.m_ignoreBlankLines;
};

Frx.prototype.setIgnoreBlankLines = function(ignoreBlankLines) {
    this.m_ignoreBlankLines = ignoreBlankLines;
    return this;
};

// Methods
Frx.prototype.id = function() {
    return this.m_id;
};

Frx.prototype.review = function() {
    return this.m_review;
};

Frx.prototype.getPrevFrx = function() {
    return this.m_prevFrx;
};

Frx.prototype.setPrevFrx = function(frx) {
    this.m_prevFrx = frx;
    return this;
};

Frx.prototype.getNextFrx = function() {
    return this.m_nextFrx;
};

Frx.prototype.setNextFrx = function(frx) {
    this.m_nextFrx = frx;
    return this;
};

Frx.prototype.isComplete = function() {
    return this.m_isComplete;
};

Frx.prototype.setComplete = function(isComplete) {
    this.m_isComplete = isComplete;
    return this;
};

Frx.prototype.colspan = function() {
    return this.m_colspan;
};

Frx.prototype.setColspan = function(colspan) {
    this.m_colspan = colspan;
    return this;
};

Frx.prototype.isLoaded = function() {
    return this.m_isLoaded;
};

Frx.prototype.setLoaded = function(loaded) {
    this.m_isLoaded = loaded;
    return this;
};

Frx.prototype.isLoading = function() {
    return this.m_isLoading;
};

Frx.prototype.setLoading = function(loading) {
    this.m_isLoading = loading;
    return this;
};

Frx.prototype.isExpanded = function () {
    return this.m_isExpanded;
};

Frx.prototype.setExpanded = function (expanded) {
    this.m_isExpanded = expanded;
    return this;
};

Frx.prototype.isFiltered = function () {
    return this.m_isFiltered;
};

Frx.prototype.setFiltered = function (filtered) {
    this.m_isFiltered = filtered;
    return this;
};

Frx.prototype.isBinary = function() {
    return this.m_isBinary;
};

Frx.prototype.setBinary = function (binary) {
    this.m_isBinary = binary;
    return this;
};

Frx.prototype.isDirectory = function() {
    return this.m_isDirectory;
};

Frx.prototype.setDirectory = function (directory) {
    this.m_isDirectory = directory;
    return this;
};

Frx.prototype.isNewSinceComplete = function() {
    return this.m_isNewSinceComplete;
};

Frx.prototype.setNewSinceComplete = function (newSinceComplete) {
    this.m_isNewSinceComplete = newSinceComplete;
    return this;
};

Frx.prototype.setRenderTime = function (date) {
    this.m_renderTime = new Date(date.getTime());
    return this;
};

Frx.prototype.getRenderTime = function () {
    return new Date(this.m_renderTime.getTime());
};

// FRX Revision Stuff
Frx.prototype.addFrxRevision = function(frxRev) {
    this.m_frxRevisions.push(frxRev);
    return this;
};

Frx.prototype.frxRevisions = function() {
    return this.m_frxRevisions;
};

Frx.prototype.setSliderFrxRevisions = function (revs) {
    this.m_sliderFrxRevisions = AJS.$.makeArray(revs);
    return this;
};

Frx.prototype.getSliderFrxRevisions = function () {
    return this.m_sliderFrxRevisions;
};

Frx.prototype.setVisibleFromRevision = function(frxRev) {
    this.m_visibleFromRevision = frxRev;
    return this;
};

Frx.prototype.setVisibleToRevision = function(frxRev) {
    this.m_visibleToRevision = frxRev;
    return this;
};

Frx.prototype.setVisibleFromSCMRevision = function (rev) {
    this.m_visibleFromSCMRevision = rev;
    return this;
};

Frx.prototype.setVisibleToSCMRevision = function (rev) {
    this.m_visibleToSCMRevision = rev;
    return this;
};

Frx.prototype.visibleFromRevision = function() {
    return this.m_visibleFromRevision;
};

Frx.prototype.visibleToRevision = function() {
    return this.m_visibleToRevision;
};

Frx.prototype.visibleFromSCMRevision = function() {
    return this.m_visibleFromSCMRevision;
};

Frx.prototype.visibleToSCMRevision = function() {
    return this.m_visibleToSCMRevision;
};

Frx.prototype.setFrxRevisionToCruRevisionMap = function (map) {
    this.m_frxRevisionToCruRevisionMap = map;
    return this;
};

Frx.prototype.frxRevisionToCruRevisionMap = function () {
    return this.m_frxRevisionToCruRevisionMap;
};

Frx.prototype.hasComments = function() {
    return this.comments().length > 0;
};

Frx.prototype.hasDefects = function() {
    return this.defects().length > 0;
};

Frx.prototype.hasUnreadComments = function() {
    return this.unreadComments().length > 0;
};

Frx.prototype.defects = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return comment.defect();
    });
};

Frx.prototype.addComment = function(comment) {
    this.m_comments[comment.id()] = comment;
    this.m_commentCacheDirty = true;
    return this;
};

Frx.prototype.comments = function() {
    if (this.m_commentCacheDirty) {
        this.m_commentArrayCache = [];
        var that = this;
        AJS.$.each(this.m_comments, function() {
            that.m_commentArrayCache.push(this);
        });
        this.m_commentCacheDirty = false;
    }
    return this.m_commentArrayCache;
};

Frx.prototype.unreadComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return comment.status() === 'unread' || comment.status() === 'leaveUnread';
    });
};

Frx.prototype.fileComments = function() {
    return AJS.$.grep(this.comments(), function(comment) {
        return /^revision/.test(comment.type());
    });
};

Frx.prototype.removeComment = function(comment) {
    delete this.m_comments[comment.id()];
    this.m_commentCacheDirty = true;
    return this;
};

Frx.prototype.isOutOfDate = function () {
    return this.m_outOfDate;
};

Frx.prototype.setOutOfDate = function (outOfDate) {
    this.m_outOfDate = outOfDate;
    return this;
};

Frx.prototype.isToBeRemoved = function () {
    return this.m_toBeRemoved;
};

Frx.prototype.setToBeRemoved = function (toBeRemoved) {
    this.m_toBeRemoved = toBeRemoved;
    return this;
};

Frx.prototype.readStatus = function() {
    return this.m_readStatus;
};

Frx.prototype.setReadStatus = function(status) {
    this.m_readStatus = status;
    return this;
};

Frx.prototype.isReadStatusLocked = function() {
    return this.m_readStatusLocked;
};

Frx.prototype.lockReadStatus = function() {
    this.m_readStatusLocked = true;
    return this;
};

Frx.prototype.unlockReadStatus = function() {
    this.m_readStatusLocked = false;
    return this;
};

function Comment(id, type) {
    this.m_id = id;
    this.m_type = type;
    this.m_domId = this.m_type + this.m_id;
    this.m_contentDomId = this.m_type.replace('reply', 'comment') + 'Content' + this.m_id;
    this.m_frx = undefined;
    this.m_review = undefined;
    this.m_replyTo = undefined;
    this.m_replies = [];
    this.m_status = undefined;
    this.m_message = undefined;
    this.m_defect = false;
    this.m_fromLineRange = undefined;
    this.m_toLineRange = undefined;
    this.m_draft = false;
    this.m_commentColor = undefined;
    this.m_gutterLine = undefined; // int
    this.m_fromRevId = undefined;
    this.m_toRevId = undefined;
    this.m_metrics = [];
    this.m_issueKey = undefined;
    this.m_issueStatus = undefined;
}

Comment.prototype.id = function() {
    return this.m_id;
};

/**
 * Returns the DOM ID of the comment's content and replies container.
 *
 * <p>
 * For source comments, this returns the inline version of the DOM ID.
 * You will need to regexp replace <code>inline</code> with <code>above</code>
 * to get the DOM ID for the above version of the comment.
 * </p>
 */
Comment.prototype.domId = function() {
    return this.m_domId; // if you update the HTML structure of comments, update visiblePosition()
};

/**
 * Returns the DOM ID of the comment's content container.
 *
 * <p>
 * For source comments, this returns the inline version of the DOM ID.
 * You will need to regexp replace <code>inline</code> with <code>above</code>
 * to get the DOM ID for the above version of the comment.
 * </p>
 *
 * <p>
 * Use this in favour of, e.g., <code>jQuery(comment.domId()).children('div.comment')</code>.
 * </p>
 */
Comment.prototype.contentDomId = function () {
    return this.m_contentDomId;
};

// It is possible for the comment object to exist, but not be in the dom - eg when we autosave comments.
Comment.prototype.domIdExists = function () {
    return AJS.$("#" + this.domId()).length > 0;
};

Comment.prototype.frx = function() {
    return this.m_frx;
};

Comment.prototype.setFrx = function(frx) {
    this.m_frx = frx;
    this.m_review = frx.review();
    return this;
};

Comment.prototype.review = function() {
    return this.m_review;
};

Comment.prototype.setReview = function(review) {
    this.m_review = review;
    return this;
};

Comment.prototype.type = function() {
    return this.m_type;
};

/**
 * the location of the comment: either general, revision or inline.
 */
Comment.prototype.position = function() {
    return this.isReply() ? this.m_type.replace(/reply$/, "") : this.m_type.replace(/comment$/, "");
};

/**
 * Returns the position of the comment which is visible.
 * @return either 'general', 'revision', 'inline' or 'above' depending on which comment is visible. If the comment
 * is hidden, an empty string is returned. If both inline and above comments are shown, the result is indeterminant.
 */
Comment.prototype.visiblePosition = function () {
    var position = this.position();
    var place = this.isReply() ? 'reply' : 'comment';
    if (AJS.$("#" + position + place + this.m_id).is(":visible")) {
        return position;
    } else if (this.isInline()) {
        position = 'above';
        if (AJS.$('#' + position + place + this.m_id).is(":visible")) {
            return position;
        }
    }
    return '';
};

// todo rename to readStatus
Comment.prototype.status = function() {
    return this.m_status;
};

Comment.prototype.setStatus = function(status) {
    this.m_status = status;
    return this;
};

Comment.prototype.isReply = function() {
    return !!this.m_replyTo;
};

Comment.prototype.replyTo = function() {
    return this.m_replyTo;
};

Comment.prototype.setReplyTo = function(replyTo) {
    this.m_replyTo = replyTo;
    return this;
};

Comment.prototype.hasReplies = function() {
    return this.m_replies.length > 0;
};

Comment.prototype.addReply = function(comment) {
    this.m_replies.push(comment);
    return this;
};

Comment.prototype.getReplies = function() {
    return this.m_replies;
};

Comment.prototype.clearReplies = function() { //recursive
    var replies_copy = this.m_replies.slice(); // use a copy because review.removeComment() modifies this.m_replies
    for (var i = 0, len = replies_copy.length; i < len; i++) {
        var reply = replies_copy[i];
        if (reply) {
            reply.clearReplies();
            review.removeComment(reply);
        }
    }
    this.m_replies = [];
};

Comment.prototype.isInline = function() {
    return /^inline/.test(this.m_type);
};

Comment.prototype.isHidden = function () {
    return this.isInline() && !this.fromLineRange() && !this.toLineRange();
};

Comment.prototype.message = function() {
    return this.m_message;
};

Comment.prototype.setMessage = function(message) {
    this.m_message = message;
    return this;
};

Comment.prototype.defect = function() {
    return this.m_defect;
};

Comment.prototype.setDefect = function(defect) {
    this.m_defect = defect;
    return this;
};

Comment.prototype.draft = function() {
    return this.m_draft;
};

Comment.prototype.setDraft = function(draft) {
    this.m_draft = draft;
    return this;
};

Comment.prototype.fromLineRange = function() {
    return this.m_replyTo ? this.m_replyTo.fromLineRange() : this.m_fromLineRange;
};

Comment.prototype.setFromLineRange = function(lr) {
    this.m_fromLineRange = lr;
    return this;
};

Comment.prototype.toLineRange = function() {
    return this.m_replyTo ? this.m_replyTo.toLineRange() : this.m_toLineRange;
};

Comment.prototype.setToLineRange = function(lr) {
    this.m_toLineRange = lr;
    return this;
};

Comment.prototype.fromRevId = function() {
    return this.isReply() ? this.m_replyTo.fromRevId() : this.m_fromRevId;
};

Comment.prototype.setFromRevId = function(id) {
    this.m_fromRevId = id;
    return this;
};

Comment.prototype.toRevId = function() {
    return this.m_replyTo ? this.m_replyTo.toRevId() : this.m_toRevId;
};

Comment.prototype.setToRevId = function(id) {
    this.m_toRevId = id;
    return this;
};

Comment.prototype.commentColor = function() {
    return this.m_commentColor;
};

Comment.prototype.setCommentColor = function(color) {
    this.m_commentColor = color;
    return this;
};

Comment.prototype.gutterLine = function() {
    return this.m_replyTo ? this.m_replyTo.gutterLine() : this.m_gutterLine;
};

Comment.prototype.setGutterLine = function(gutterLine) {
    this.m_gutterLine = gutterLine;
    return this;
};

Comment.prototype.metrics = function() {
    return this.m_metrics;
};

Comment.prototype.setMetrics = function(metrics) {
    this.m_metrics = metrics || [];
    return this;
};

Comment.prototype.issueKey = function() {
    return this.m_issueKey;
};

Comment.prototype.setIssueKey = function(issueKey) {
    this.m_issueKey = issueKey;
    return this;
};

Comment.prototype.issueStatus = function() {
    return this.m_issueStatus;
};

Comment.prototype.setIssueStatus = function(issueStatus) {
    this.m_issueStatus = issueStatus;
    return this;
};

// Both variables are populated in elementIds.jspf
var review = new Review();
var permaId;
;
/* END /2static/script/cru/review/review-model.js */
/* START /2static/script/cru/review/review-event.js */
if (!AJS.CRU.REVIEW.EVENTS) {
    AJS.CRU.REVIEW.EVENTS = {};
}

(function() {
    AJS.$(document).ready(function() {
        AJS.$('.frxouter:not(.activeFrx)').live('mousedown', function (e) {
            var $target = AJS.$(e.target || e.srcElement);
            if ($target.is('#generalComments, .frx-actions')
                    || $target.parents('.frx-actions').length === 1) {
                return true;
            }
            AJS.CRU.FRX.NAV.setCurrentFrx(this, { sticky: true });
            var $parentRow = $target.closest('tr');
            if ($parentRow.length > 0 && !e.altKey) {
                return commentator.selectLine_down($parentRow, true);
            }
            return true;
        });

        var mouseDown = false;
        // Event delegation for frx commentable lines.
        // Huge performance boost for reviews with thousands of lines of frx lines
        AJS.$('.activeFrx .commentableLine')
                .live("mouseover", function() {
            if (mouseDown) {
                return commentator.selectLine_over(AJS.$(this));
            }
            return true;
        });

        AJS.$('.activeFrx .inlineSource .commentableLine td:not(.tetrisColumn, .diffNav)')
                .live("mousedown", function(e) {
            if (e.altKey) {
                return true;
            }
            mouseDown = true;
            return commentator.selectLine_down(AJS.$(this).closest('tr.commentableLine'));
        });

        AJS.$('.activeFrx')
                .live("mouseup", function() {
            if (mouseDown) {
                mouseDown = false;
                return commentator.selectLine_up(AJS.$(this).find('.inlineSource:first'));
            }
            return true;
        });

        AJS.$("#parentReviewId, #linkReviewSaveButton").keypress(function(e) {
            var key = e.which || e.keyCode;
            var el = e.target || e.srcElement;
            if ((key === AJS.$.ui.keyCode.ENTER || key === AJS.$.ui.keyCode.TAB) && el.type != 'textarea') {
                if (AJS.CRU.REVIEW.UTIL.postLinkedReview) {
                    AJS.CRU.REVIEW.UTIL.postLinkedReview();
                }
                return false;
                //TODO: Port to jQuery events and e.stopPropagation();
            }
        });

        AJS.$("#linkReviewSaveButton").live("click", function() {
            AJS.CRU.REVIEW.UTIL.postLinkedReview();
            return false;
        });

        AJS.$("#linkReviewUnlinkButton").live("click", function() {
            AJS.CRU.REVIEW.UTIL.postUnlinkReview();
            return false;
        });

        AJS.$("#addInviteeButton").click(function () {
            AJS.CRU.CREATE.addInvitee();
        });

        // Jira links get loaded by Ajax
        AJS.$("#jiraIssueQuickLink").live("click", function() {
            var suggestedIssueKey = AJS.$(this).siblings("input[name=suggestedIssueKey]").val();
            AJS.CRU.REVIEW.UTIL.findAndLinkJiraIssue(suggestedIssueKey);
            return false;
        });

        AJS.$("#jiraFindButton").live("click", function() {
            AJS.CRU.REVIEW.UTIL.findJiraIssue();
            return false;
        });

        AJS.$("#jiraClearButton").live("click", function() {
            AJS.CRU.REVIEW.UTIL.unlinkJiraIssue();
            return false;
        });

        var toggleAndResize = function ($elem) {
            $elem.toggle();
            AJS.CRU.UI.columnFillHeight();
        };

        AJS.$("#patchList").click(function() {
            toggleAndResize(AJS.$(this).siblings(".expandable"));
        });

        AJS.$('#clear-filter-link').click(function () {
            AJS.$('#frxFilterOptions li.selected').removeClass("selected");
            AJS.CRU.FRX.filterFrxs();
        });

        AJS.$('#expandAllFrxs').click(function () {
            AJS.CRU.FRX.expandFrxs(review.frxIds());
            return false;
        });

        AJS.$('#collapseAllFrxs').click(function () {
            AJS.CRU.FRX.collapseFrxs(review.frxIds());
        });

        AJS.$('#frxFilterOptions li.frx-filter-option').live('click', function (e) {
            AJS.$(this).toggleClass("selected");
            AJS.CRU.FRX.filterFrxs();
            e.stopPropagation(); // Don't let the menu be closed when selecting options
        });

        AJS.$('#element-navigation').live('click', function (e) {
            if (e.target.id === 'element-navigation') {
                AJS.$('#element-link').click();
            }
        });

        AJS.$('#element-navigation li.search-option').live('click', function (e) {
            AJS.$(this).toggleClass("selected");
            e.stopPropagation();
        });

        AJS.$('#next-element-link').click(function () {
            AJS.CRU.FRX.goToNextElement(true);
        });

        AJS.$('#prev-element-link').click(function () {
            AJS.CRU.FRX.goToNextElement(false);
        });

        AJS.$("#set-inline-comments a").click(function () {
            commentator.toggleComments('inline');
            AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
        });

        AJS.$("#set-above-comments a").click(function () {
            commentator.toggleComments('top');
            AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
        });

        AJS.$("#set-hidden-comments a").click(function () {
            commentator.toggleComments('none');
            AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
        });

        AJS.$("#set-side-by-side-diffs a").click(function () {
            AJS.$("#set-unified-diffs").removeClass("selected");
            AJS.$("#set-side-by-side-diffs").addClass("selected");
            AJS.CRU.FRX.toggleAllFrxsDiffMode('s');
        });

        AJS.$("#set-unified-diffs a").click(function () {
            AJS.$("#set-side-by-side-diffs").removeClass("selected");
            AJS.$("#set-unified-diffs").addClass("selected");
            AJS.CRU.FRX.toggleAllFrxsDiffMode('u');
        });

        AJS.$("#set-soft-wrapping-on a").click(function () {
            AJS.$("#set-soft-wrapping-off").removeClass("selected");
            AJS.$("#set-soft-wrapping-on").addClass("selected");
            AJS.CRU.FRX.toggleAllFrxsSoftWrapping(true);
        });

        AJS.$("#set-soft-wrapping-off a").click(function () {
            AJS.$("#set-soft-wrapping-on").removeClass("selected");
            AJS.$("#set-soft-wrapping-off").addClass("selected");
            AJS.CRU.FRX.toggleAllFrxsSoftWrapping(false);
        });

        AJS.$("#single-frx-view-toggle").click(function () {
            AJS.CRU.UI.singleFrxView();
        });

        AJS.$("#all-frx-view-toggle").click(function () {
            if (!AJS.$(this).hasClass("disabled")) {
                AJS.CRU.UI.allFrxView();
            }
        });

        AJS.$("#fullscreen-toggle-on > a").click(function () {
            AJS.CRU.UI.toggleFullscreen();
        });

        AJS.$("#fullscreen-toggle-off > a").click(function () {
            AJS.CRU.UI.toggleFullscreen();
        });

        AJS.$("#mark-comments-read-button:not(.disabled)").live("click", function () {
            AJS.CRU.COMMENT.markAllCommentsRead();
        });

        AJS.$("#addReviewCommentLink:not(.disabled)").live("click", function () {
            AJS.CRU.REVIEW.WIKI.resetPreview();
            commentator.displaySimpleCommentForm(null, 'generalCommentForm');
            AJS.$(this).hide();
        });

        //TODO @seb - needs to be a single element with an id
        AJS.$(".addFileCommentLink:not(.disabled)").live("click", function () {
            var frxId = this.id.replace('addFileCommentLink', '');
            AJS.CRU.REVIEW.WIKI.resetPreview();
            commentator.displayFileCommentForm(null, 'fileCommentForm' + frxId, frxId);
            AJS.$(this).parent().siblings(".revision_comments_frxinner").removeClass("revision-comments-empty");
        });

        AJS.$("#review-updated-warning .close").live("click", function () {
            AJS.$('#review-updated-warning').slideUp('fast');
            AJS.$('body').removeClass('review-updated');
        });

        AJS.$("#review-updated-warning .collapse").live("click", function () {
            var warning = AJS.$('#review-updated-warning');
            if (warning.hasClass('collapsed')) {
                warning.removeClass('collapsed');
                AJS.$('#review-updated-warning a.collapse').text('Collapse');
            } else {
                warning.addClass('collapsed');
                AJS.$('#review-updated-warning a.collapse').text('Expand');
            }
        });

        /** previous value of time-tracking field, in case of invalid update **/
        var prevMins = 0;
        /** has current value of time-tracking field value been posted to server **/
        var savedMins = false;

        AJS.$("#time-spent-input")
                .live("click", function() {
            prepTimeSpentForInput();
        })
                .bind("blur", function() {
            saveUpdatedTimeSpent();
        })
                .keypress(function(e) {
            if (e.which == 13) { // user presses enter
                saveUpdatedTimeSpent();
            }
        });

        /** truncate 'minutes' from editable time field **/
        var prepTimeSpentForInput = function () {
            if (!AJS.CRU.REVIEW.TIMER.editing) {
                AJS.CRU.REVIEW.TIMER.startEditing();
                var $in = AJS.$("#time-spent-input");
                prevMins = $in.val().substring(0, $in.val().indexOf(' '));
                $in.val(prevMins);
                $in.parent().addClass("edit");
                savedMins = false;
            }
        };

        /** dispatch updated time spent to server (if valid) **/
        var saveUpdatedTimeSpent = function () {
            if (savedMins) {
                return; //don't dispatch twice
            }
            savedMins = true;
            var $in = AJS.$("#time-spent-input");
            $in.attr("disabled", "disabled");
            var minsStr = $in.val();
            var postToServer = true;
            var mins = parseInt(minsStr, 10);
            if (isNaN(mins) || mins < 0) {
                mins = prevMins;
                postToServer = false;
            }

            /*
             The in-line editable field is a fixed, calculated width (based on time spent at page render) so if
             the user updates the 'minutes' field to a figure with more digits than can fit nicely, the 'minutes' is
             collapsed to 'mins' to make room, until the next page refresh. The lengths we go to for looks :)
             */
            var impliedNumberOfDigits = (parseInt($in.css("width"), 10) - 5) / .5; // width:(5 + number_of_digits/2)em;
            var actualNumberOfDigits = (mins + "").length;
            var unitStr;
            if (actualNumberOfDigits > impliedNumberOfDigits + 1) {
                unitStr = " mins";
            } else {
                unitStr = mins == 1 ? " minute" : " minutes";
            }
            $in.val(mins + unitStr);

            if (postToServer) {
                var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/updateTimeTrackingAjax";
                var params = {timeSpent : mins * 60000};
                AJS.FECRU.AJAX.ajaxDo(url, params);
            }

            AJS.CRU.REVIEW.TIMER.stopEditing(postToServer);
            $in.removeAttr("disabled");
            $in.parent().removeClass("edit");
            //ensure focus is removed (e.g. when switching tabs/windows blur is triggered, but the input is still selected)
            $in.blur();
        };

        /**
         * Whether the edit review dialog has been shown during this browser session.
         *
         * Used to make sure a user is presented with this at least once for draft reviews.
         */
        var hasEditReviewBeenShown = function () {
            return AJS.$.inArray(review.id(), reviewIdsEditReviewShownFor()) >= 0;
        };

        var reviewIdsEditReviewShownFor = function () {
            var editReviewShown = AJS.$.cookie('editReviewShown');
            return editReviewShown ? editReviewShown.split(',') : [];
        };

        var hasEditReviewDetailBeenShown = function () {
            return AJS.$.inArray(review.id(), reviewIdsEditReviewDetailShownFor()) >= 0;
        };

        var reviewIdsEditReviewDetailShownFor = function () {
            var editReviewDetailShown = AJS.$.cookie('editReviewDetailShown');
            return editReviewDetailShown ? editReviewDetailShown.split(',') : [];
        };

        var manageFiles = function (editPage) {
            var reviewId = review.id();
            var reviewsShown = reviewIdsEditReviewShownFor();
            if (AJS.$.inArray(reviewId, reviewsShown) < 0) {
                reviewsShown.push(reviewId);
                // IE7 won't respect the review path, e.g., `/foo/cru/CR-1`, unless the
                // browser URL ends with a slash, so store the cookie in a parent path.
                // Consequently, store under a single key since IE7 has a limit per domain.
                AJS.$.cookie('editReviewShown', reviewsShown.join(','), { path: fishEyePageContext + '/cru/' });
            }

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

            var iframeStyle = "style='width:100%;height:" + (dialog.height - 43 - 44) + "px'";
            editPage = editPage || (review.isEmpty() ? "/edit-changelog" : "/edit-content");
            var cs = "<iframe frameborder='0' id='editReviewIframe' name='editReviewIframe' src='" + AJS.CRU.UTIL.urlBase(permaId) + editPage + "' " + iframeStyle + "></iframe>";

            var header = "Edit Review " + review.id();
            dialog.addHeader(header)
                    .addPanel("Manage", cs);

            if (review.isEmpty() || review.isDraft()) {
                dialog.addButton("Abandon Review", function(dialog) {
                    AJS.CRU.UTIL.stateTransition('action:abandonReview', permaId);
                    dialog.remove();
                });
            }
            if (review.isDraft()) {
                var $approveButton = AJS.$("#context-navigation .tools-drop-down .action-approveReview");
                if ($approveButton.length > 0) {
                    dialog.addButton($approveButton.text(), function(dialog) {
                        AJS.CRU.CREATE.submitDetailsFormAndStart(function() {
                            dialog.remove();
                        });
                    });
                }
            }
            dialog.addButton("Done", function(dialog) {
                AJS.CRU.CREATE.submitDetailsForm();
            }).show();
        };

        // Live event: button is replaced by ajax content load after saving an edited review
        AJS.$("#edit-review-link:not(.disabled)").live("click", function() {
            editReview();
            return false;
        });

        var editReview = function () {
            var reviewId = review.id();
            var reviewDetailsShown = reviewIdsEditReviewDetailShownFor();
            if (AJS.$.inArray(reviewId, reviewDetailsShown) < 0) {
                reviewDetailsShown.push(reviewId);
                AJS.$.cookie('editReviewDetailShown', reviewDetailsShown.join(','), { path: fishEyePageContext + '/cru/' });
            }

            manageFiles('/edit-details');
        };

        var summarizeDialog = 0;
        AJS.CRU.REVIEW.createSummarizeDialog = function () {
            summarizeDialog = AJS.CRU.DIALOG.ajaxDialog(640, 480);
            summarizeDialog.addHeader("Summarize Review")
                    .addPanel("Summarize", AJS.$("#closingComment"))
                    .addButton("Continue Without Closing", function(summarizeDialog) {
                summarizeDialog.hide();
            })
                    .addButton("Close Review", function(summarizeDialog) {
                AJS.CRU.REVIEW.UTIL.closeReviewAjax();
                summarizeDialog.hide();
            });
        };

        AJS.CRU.REVIEW.openSummarizeDialog = function () {
            if (summarizeDialog === 0) {
                AJS.CRU.REVIEW.createSummarizeDialog();
            }
            summarizeDialog.show();
        };


        if (AJS.$('#reviewpage').length === 1 && review.writable()) {
            if (review.isDraft()) {
                if (review.isEmpty() && !hasEditReviewBeenShown()) {
                    manageFiles();
                } else if (!hasEditReviewDetailBeenShown()) {
                    editReview();
                }
            } else if (review.isSummarize()) {
                AJS.CRU.REVIEW.openSummarizeDialog();
            }
        }

        AJS.CRU.REVIEW.UTIL.reorderParticipants();
    });
})();
;
/* END /2static/script/cru/review/review-event.js */
/* START /2static/script/cru/review/comment/comment-ajax.js */
if (!AJS.CRU) {
    AJS.CRU = {};
}
AJS.CRU.COMMENT = {};

var commentAjaxController = (function() {
    /*****************
     * PRIVATE STUFF *
     *****************/
    function isEmptyComment(form, nameprefix) {
        var empty = false;
        var newText = form['newText'];

        if (newText && /^\s*$/.test(newText.value)) {
            empty = true;
            AJS.$("#"+nameprefix + "AutosaveMsg").html("Cannot save or post an empty comment.");
        }

        return empty;
    }

    function progressIssueLinkClass(commentId, from, to) {
        getCommentElements(commentId, 'commentIssueKey_')
                .removeClass('commentIssueKey'+from)
                .addClass('commentIssueKey'+to);
        getCommentElements(commentId, 'commentIssueStatus_')
                .removeClass('commentIssueStatus'+from)
                .addClass('commentIssueStatus'+to);
        if (to == "Linking") {
            getCommentElements(commentId, 'busyCommentIssue').show();
        } else {
            getCommentElements(commentId, 'busyCommentIssue').hide();
        }
    }

    function getCommentElements(commentId, elementSuffix) {
        var comment = review.comment(commentId);
        if (comment.isInline()) {
            return AJS.$('#above' + elementSuffix + commentId +
                         ',#inline' + elementSuffix + commentId);
        } else if (comment.frx()) {
            return AJS.$('#revision' + elementSuffix + commentId);
        } else {
            return AJS.$('#general' + elementSuffix + commentId);
        }
    }

    function commentButtonSyncWrapper(callback) {
        return function (resp) {
            if (callback) {
                callback(resp);
            }
            commentator.syncCommentButtons();
        };
    }

    /*****************
     * PUBLIC  STUFF *
     *****************/
    var res = {
        // General comment methods
        publishComment : function (commentId, permaId, done) {
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/publishCommentAjax/';
            var params = { "commentId" : commentId };

            var onDone = function(resp) {
                if(done) {
                    done(commentId, resp);
                    // Re-enable child draft post links
                    var children = review.comment(commentId).getReplies();
                    for (var i = 0, len = children.length; i < len; i++) {
                        var child = children[i];
                        if (child.draft()) {
                            AJS.$(  "#generaldraftPublish" + child.id() + "," +
                                    "#revisiondraftPublish" + child.id() + "," +
                                    "#inlinedraftPublish" + child.id()).removeClass("disabled");
                        }
                    }
                }
            };
            var onComplete = commentButtonSyncWrapper(onDone);
            AJS.FECRU.AJAX.ajaxDo(url, params, onComplete);
            return false;
        },

        addReply : function (permaId, form, draft) {
            var $form = AJS.$(form);
            var $fieldset = $form.children("fieldset");
            var $decal = $fieldset.children(".decal");
            var $buttons = $fieldset.find('button');
            if (!isEmptyComment(form,"reply")) {
                $decal.addClass("spinner");
                $buttons.attr('disabled', true);
                var done = function(resp) {
                    $decal.removeClass("spinner");
                    $buttons.removeAttr('disabled');
                    commentator.insertAjaxReply(resp); //checks worked
                    commentator.updateCommentCount(resp);
                };
                var onComplete = commentButtonSyncWrapper(done);
                AJS.CRU.COMMENT.commentAjaxExecutor.executeAjaxJob({
                    url : AJS.CRU.UTIL.jsonUrlBase(permaId) + '/replyAjax/',
                    params : function() { return $form.serialize() + "&draft=" + (draft ? draft : false); },
                    callback : onComplete
                });
            }
            return false;
        },

        addGeneralComment : function (permaId, form, draft) {
            var $form = AJS.$(form);
            var $fieldset = $form.children("fieldset");
            var $decal = $fieldset.children(".decal");
            var $buttons = $fieldset.find('button');
            if (!isEmptyComment(form,"general")) {
                $decal.addClass("spinner");
                $buttons.attr('disabled', true);
                var done = function(resp) {
                    $decal.removeClass("spinner");
                    $buttons.removeAttr('disabled');
                    commentator.insertAjaxGeneralComment(resp); //checks worked
                };
                var onComplete = commentButtonSyncWrapper(done);
                AJS.CRU.COMMENT.commentAjaxExecutor.executeAjaxJob({
                    url : AJS.CRU.UTIL.jsonUrlBase(permaId) + '/generalCommentAjax/',
                    params : function() { return $form.serialize() + "&draft=" + (draft ? draft : false); },
                    callback : onComplete
                });
            }
            return false;
        },

        addRevisionComment : function (permaId, form, draft, namePrefix) {
            var $form = AJS.$(form);
            var $fieldset = $form.children("fieldset");
            var $decal = $fieldset.children(".decal");
            var $buttons = $fieldset.find('button');
            if (!isEmptyComment(form, namePrefix)) {
                $decal.addClass("spinner");
                $buttons.attr('disabled', true);
                var done = function(resp) {
                    $decal.removeClass("spinner");
                    $buttons.removeAttr('disabled');
                    commentator.insertRevisionComment(resp); //checks worked
                    if (resp.worked) {
                        tetrisCommentController.renderTetrisCommentMarkersForComment(resp.comment.id);
                    }
                };
                var onComplete = commentButtonSyncWrapper(done);
                AJS.CRU.COMMENT.commentAjaxExecutor.executeAjaxJob({
                    url : AJS.CRU.UTIL.jsonUrlBase(permaId) + '/revisionCommentAjax/',
                    params : function() { return $form.serialize() + "&draft=" + (draft ? draft : false); },
                    callback : onComplete
                });
            }
            return false;
        },

        autoSaveReply : function (permaId, form, done) {
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/replyAjax/';
            var params = AJS.$(form).serialize() + "&draft=true";
            AJS.CRU.COMMENT.commentAjaxExecutor.executeAjax(url, params, done, true);
        },

        autoSaveGeneralComment : function (permaId, form, done) {
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/generalCommentAjax/';
            var params = AJS.$(form).serialize() + "&draft=true";
            AJS.CRU.COMMENT.commentAjaxExecutor.executeAjax(url, params, done, true);
        },

        autoSaveRevisionComment : function (permaId, form, done) {
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/revisionCommentAjax/';
            var params = AJS.$(form).serialize() + "&draft=true";
            AJS.CRU.COMMENT.commentAjaxExecutor.executeAjax(url, params, done, true);
        },

        deleteComment : function (commentId, permaId, done) {
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/deleteCommentAjax/';
            var params = { "commentId" : commentId };
            var onComplete = commentButtonSyncWrapper(done);
            AJS.FECRU.AJAX.ajaxDo(url, params, onComplete);
            return false;
        },

        // Comment read status
        updateCommentReadStatus : function(commentId, permaId, markAsRead, done) {
            // Prevent simultaneous requests.

            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/commentReadStatusAjax/';
            var params = { "commentId" : commentId, "markAsRead" : markAsRead };

            var onComplete = commentButtonSyncWrapper(done);
            AJS.FECRU.AJAX.ajaxDo(url, params, onComplete);
            return false;
        },

        markAllCommentsRead : function (updatedComments, permaId, done) {
            if (updatedComments.length > 0) {
                var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/commentReadStatusAjax/';
                var params = {
                    markAsRead: true,
                    cid: AJS.$.map(updatedComments, function (comment) { return comment.id(); })
                };

                var onComplete = commentButtonSyncWrapper(done);
                AJS.FECRU.AJAX.ajaxDo(url, params, onComplete);
            }
            return false;
        },

        loadCommentIssueStatuses : function (frxId) {
            var commentIdsWithLinkedIssues = [];
            var comments;
            if (frxId) {
                comments = review.frx(frxId).comments();
            } else {
                comments = review.generalComments();
            }
            AJS.$.each(comments, function() {
                var comment = review.comment(this.id());
                if (comment.issueKey() && comment.issueKey() != "") {
                    commentIdsWithLinkedIssues.push(comment.id());
                }
            });
            if (commentIdsWithLinkedIssues.length === 0) {
                return;
            }
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/getCommentIssueStatusAjax/';
            var params = { "commentIds" : commentIdsWithLinkedIssues };
            var done = function(resp) {
                if (resp.worked) {
                    var issues = resp.issues;
                    AJS.$.each(issues, function() {
                        var commentId = this.commentId;
                        var $statusElems = getCommentElements(commentId, 'commentIssueStatus_');
                        $statusElems.text(this.issueStatus);
                        review.comment(commentId).setIssueStatus(this.issueStatus);
                        var statusElemText = $statusElems.filter(':first').text().toLowerCase();
                        if (statusElemText === 'resolved' || statusElemText === 'closed' || statusElemText === 'error') {
                            getCommentElements(commentId, 'commentIssueResolve_').hide();
                        } else {
                            getCommentElements(commentId, 'commentIssueResolve_').show();
                        }
                    });
                }
            };
            AJS.FECRU.AJAX.ajaxDo(url, params, done);
        },

        resolveIssue : function (commentId) {
            getCommentElements(commentId, 'busyCommentIssue_').show();
            getCommentElements(commentId, 'commentIssueResolve_').hide();
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/resolveCommentIssueAjax/';
            var params = { "commentId" : commentId };
            var done = function(resp) {
                var $statusElems = getCommentElements(commentId, 'commentIssueStatus_');
                if (resp.worked) {
                    $statusElems.text(resp.issueStatus);
                    review.comment(commentId).setIssueStatus(resp.issueStatus);
                }
                var $statusElem = $statusElems.filter(':first');
                if ($statusElem.text() == 'Resolved' || $statusElem.text() == 'Error') {
                    getCommentElements(commentId, 'commentIssueResolve_').hide();
                } else {
                    getCommentElements(commentId, 'commentIssueResolve_').show();
                }
                getCommentElements(commentId, 'busyCommentIssue_').hide();
            };
            AJS.FECRU.AJAX.ajaxDo(url, params, done);
        },

        linkIssue : function (issueDialog) {
            var summary = issueDialog.find(".create-issue-summary").val();
            var commentId = issueDialog.find(".create-issue-comment-id").val();
            var assignee = issueDialog.find(".create-issue-assignee").val();
            getCommentElements(commentId, 'busyCommentIssue_').show();
            progressIssueLinkClass(commentId, 'Unlinked', 'Linking');
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/linkCommentIssueAjax/';
            var params = { "commentId" : commentId , "summary" : summary, "assignee" : assignee };
            var done = function(resp) {
                if (resp.worked) {
                    var comment = review.comment(commentId);
                    getCommentElements(commentId, 'commentIssueKey_').html(resp.issueHoverHtml);
                    comment.setIssueKey(resp.issueKey);
                    comment.setIssueStatus(resp.issueStatus);
                    progressIssueLinkClass(commentId, 'Linking', 'Linked');

                    var $statusElems = getCommentElements(commentId, 'commentIssueStatus_');
                    $statusElems.text(resp.issueStatus);
                    if ($statusElems.text().indexOf('Resolved') == -1 && $statusElems.text().indexOf('Error') == -1) {
                        getCommentElements(commentId, 'commentIssueResolve_').show();
                    } else {
                        getCommentElements(commentId, 'commentIssueResolve_').hide();
                    }

                } else {
                    progressIssueLinkClass(commentId, 'Linking', 'Unlinked');
                }
                getCommentElements(commentId, 'busyCommentIssue_').hide();
            };
            AJS.FECRU.AJAX.ajaxDo(url, params, done);
        }

    };
    return res;
})();
;
/* END /2static/script/cru/review/comment/comment-ajax.js */
/* START /2static/script/cru/review/comment/comment-event.js */
(function() {

   // On load
    AJS.$(document).ready( function() {

        AJS.$(".commentForm .copyCode").live('click', function() {
            var $lines = AJS.$(this).closest('.inlineSource').find('.lineHighlighted .lineContent');

            var text = "{code}";
            for (var i = 0, len = $lines.length; i < len; i++) {
                if (i != 0) {
                    text += '\n';
                }
                text += AJS.$($lines[i]).text();
            }
            text += '{code}';

            var $textarea = AJS.$(this).closest('.commentForm').find('.commentTextarea');
            $textarea.val($textarea.val() + '\n' + text);
        });

        //comment collapse live event
        AJS.$(".comment:not(.hover-comment) .author, .comment:not(.hover-comment) .reply-count").live("click.collapse_comment", function() {
            var comment = AJS.$(this).closest(".comment");
            var commentContainer = comment.parent();
            if (comment.data("isAnimating")) {
                //prevent multiple invocations during the animation
                return;
            }
            comment.data("isAnimating", true);

            //check whether the edit comment form is being displayed on a comment that is a child of the comment being
            //collapsed, and if it is, stop the collapse - lest we lose the edit
            var commentForm = commentator.getDisplayingReplyForm();
            if (commentForm) {
                var $commentForm = AJS.$(commentForm.getForm());
                var containsOpenEditForm = $commentForm.closest("#" + commentContainer.attr("id")).length > 0;
                if (containsOpenEditForm) {
                    $commentForm.find(".autosavemessage").text("please save or cancel before collapsing a comment");
                    commentForm.getTextBox().focus();
                    comment.data("isAnimating", false);
                    return;
                }
            }

            var commentBody = comment.find(".commentBody");
            var logo = comment.find(".userlogo");
            var avatar = logo.find(".user");
            var avatarSmall = logo.find(".smalluserlogo");
            var arrow = logo.find(".commentNav");
            var excerpt = comment.find(".excerpt");
            var replyContainer = commentContainer.children(".reply-container");

            var delay = "normal";
            var size = 24;
            var opacity = 0.4;
            var type = "swing";

            if (commentContainer.hasClass("comment-collapsed")) {
                //restore the saved height info
                var commentOriginalHeight = comment.data("commentOriginalHeight");
                var originalReplyHeight = replyContainer.data("replyThreadHeight");
                var avatarSize = avatar.data("avatarSize");

                avatarSmall.animate({width:avatarSize + "px", height:avatarSize + "px"}, delay, type, function() {
                    //show the arrow last to avoid a move/flicker
                    avatarSmall.hide();
                    avatar.show();
                    arrow.show();
                });
                excerpt.hide();
                commentBody.show();
                commentBody.animate({opacity:1}, delay, type);
                replyContainer.show();
                replyContainer.animate({opacity:1, height:originalReplyHeight + "px"}, delay, type, function() {
                    //reset the height to fluid after animating
                    replyContainer.css("height", "");
                });
                comment.animate({opacity:1, height:commentOriginalHeight + "px"}, delay, type, function() {
                    commentContainer.removeClass("comment-collapsed");
                    comment.data("isAnimating", false);
                });
            } else {
                //save the heights of comment and replies so as to restore it when expanding
                comment.data("commentOriginalHeight", comment.height());
                replyContainer.data("replyThreadHeight", replyContainer.height());
                avatar.data("avatarSize", avatar.height());

                //hide the arrow first to avoid it doing a flicker move up when hiding the avatar
                arrow.hide();
                avatar.hide();
                avatarSmall.show();
                avatarSmall.animate({width:size + "px", height:size + "px"}, delay, type);
                commentBody.animate({opacity:0}, delay, "swing", function() {
                    commentBody.hide();
                });
                replyContainer.animate({opacity:0, height:"0px"}, delay, type, function() {
                    replyContainer.hide();
                });
                comment.animate({opacity:opacity, height: (6 + size) + "px"}, delay, type, function() {
                    var count = replyContainer.find(".comment").length;
                    if (count > 0) {
                        var replyCountContainer = excerpt.find(".reply-count");
                        replyCountContainer.text(" ... " + count + " " + (count == 1 ? "reply" : "replies") + " hidden");
                    }
                    excerpt.show();
                    commentContainer.addClass("comment-collapsed");
                    comment.data("isAnimating", false);
                });
            }
        });

        AJS.$('.hover-comment .commentPermaLink').live('click', function() {
            var comment_nav = AJS.CRU.COMMENT.NAV;
            var commentId = AJS.$(this).attr('href').replace(/[^0-9]+/g, '');
            var scrollToMap = comment_nav.navigateFindComment({ commentId: commentId });
            comment_nav.navigateDirectlyToComment({ commentId: commentId }, scrollToMap);
        });

        // Click to focus comments and mark as read.
        AJS.$('#frxs .frxouter .comment').live('click', function () {
            var commentId = this.id.replace(/(general|revision|inline|above)commentContent/, '');
            if (!AJS.$(this).is('.hover-comment')) {
                AJS.CRU.COMMENT.NAV.setCurrentComment(commentId, { sticky: true });
            }
        });

        AJS.$(".comment a.publishComment:not(.disabled)").live("click", function() {
            var comment = AJS.CRU.COMMENT.commentFromInnerElement(this);
            commentator.publishComment(comment.id(), permaId);
        });

        AJS.$(".comment a.replyToComment").live("click", function() {
            var cru = AJS.CRU;
            var comment = cru.COMMENT.commentFromInnerElement(this);
            var visiblePos = comment.visiblePosition();
            var id = comment.id();
            var handleId = visiblePos + "replyFormDiv" + id + "Handle";
            var parentId = visiblePos + "replyFormDiv" + id;
            commentator.displayReplyCommentForm(handleId, parentId, comment.id());
            cru.REVIEW.WIKI.resetPreview();
        });

        AJS.$(".comment a.editComment").live("click", function() {
            var cru = AJS.CRU;
            var comment = cru.COMMENT.commentFromInnerElement(this);
            var commentId = comment.id();
            var position = comment.visiblePosition();
            var handleId = position + "commentContent" + commentId;
            var parentId = position + "commentEdit" + commentId;

            var isReply = comment.isReply();
            var $p = AJS.$("#" + parentId);
            $p.toggleClass("replyCommentForm", isReply);
            cru.REVIEW.WIKI.resetPreview();

            if(isReply) {
                commentator.displayReplyCommentForm(handleId, parentId, comment.replyTo().id(), commentId);
            } else {
                var frx = comment.frx();
                if(frx) {
                    var frxId = frx.id();
                    if (comment.isInline()) {
                        commentator.displayRevisionCommentForm(handleId, parentId, frxId, commentId);
                    } else {
                        commentator.displayFileCommentForm(handleId, parentId, frxId, commentId);
                    }
                } else {
                    commentator.displaySimpleCommentForm(handleId, parentId, commentId);
                }
            }
        });

        AJS.$(".comment a.deleteComment:not(.disabled)").live("click", function() {
            var comment = AJS.CRU.COMMENT.commentFromInnerElement(this);
            commentator.deleteComment(comment.id(), permaId);
        });

        AJS.$(".comment a.toggleCommentRead").live("click", function() {
            var comment = AJS.CRU.COMMENT.commentFromInnerElement(this);
             commentator.toggleCommentRead(comment.id(), true);
        });

        AJS.$(".comment img.nextCommentThread").live("click", function() {
            var CRU_COMMENT = AJS.CRU.COMMENT;
            var comment = CRU_COMMENT.commentFromInnerElement(this);
            CRU_COMMENT.NAV.nextCommentThread(comment.domId());
            return false;
        });

        AJS.$(".comment img.prevCommentThread").live("click", function() {
            var CRU_COMMENT = AJS.CRU.COMMENT;
            var comment = CRU_COMMENT.commentFromInnerElement(this);
            CRU_COMMENT.NAV.previousCommentThread(comment.domId());
            return false;

        });

        AJS.$(".create-issue-dialog input.create-issue-button").live("click", function() {
            var dialog = AJS.$(this).closest(".contents");
            commentAjaxController.linkIssue(dialog);
            dialog.data("dialogHider").hideDialog();
        });

        AJS.$(".create-issue-dialog span.issue-assign-to-me").live("click", function() {
            AJS.$(this).closest(".contents").find(".create-issue-assignee").val(review.getLoggedInUsername());
        });

        AJS.$(".create-issue-dialog span.close").live("click", function() {
            AJS.$(this).closest(".contents").data("dialogHider").hideDialog();
        });

        AJS.$(".comment a.commentIssueResolveButton").live("click", function() {
            var comment = AJS.CRU.COMMENT.commentFromInnerElement(this);
            commentAjaxController.resolveIssue(comment.id());
        });

        AJS.$("a.comment-skipped").live("click", function() {
            var commentId = this.id.replace('comment-skipped', '');
            var comment = review.comment(commentId);

            // Presumably, this should never happen - you can't have a skipped comment which isn't inline
            if (!comment || !comment.frx()) {
                return;
            }

            var done = function () {
                AJS.CRU.COMMENT.NAV.scrollDirectlyToComment(comment.domId());
            };

             // show full context for the frx
            AJS.CRU.FRX.toggleDiffModeAjax(review.id(), comment.frx().id(), '-1', done);
        });
    });

})();
;
/* END /2static/script/cru/review/comment/comment-event.js */
/* START /2static/script/cru/review/comment/comment-issue.js */
(function() {

    AJS.$(document).ready(function() {
        var dialogHider = {};

        var contentHandler = function ($contentDiv, $trigger, showPopup) {
            var MAX_SUMMARY_LENGTH = 255;
            AJS.$(".create-issue-template").removeClass("create-issue-template").appendTo($contentDiv).show();
            var comment = AJS.CRU.COMMENT.commentFromInnerElement($trigger);
            $contentDiv.find(".create-issue-comment-id").val(comment.id());
            var summary = comment.message();
            if (summary && summary.length > MAX_SUMMARY_LENGTH) {
                summary = summary.substr(0, MAX_SUMMARY_LENGTH - 3) + "...";
            }
            $contentDiv.find(".create-issue-summary").val(summary);

            var url = AJS.CRU.UTIL.jsonUrlBase(review.id()) + '/assignableUsersForSubtaskAjax/';
            var params = { "commentId" : comment.id() };

            var done = function(resp) {
                if (resp.worked) {
                    var assigneeSelect = $contentDiv.find(".create-issue-assignee");
                    assigneeSelect.children(".from-server").remove();
                    var assignees = resp.users;
                    for (var i = 0; i < assignees.length; i++) {
                        var assignee = assignees[i];
                        assigneeSelect.append(AJS.$("<option class='from-server' />")
                                .val(assignee.username)
                                .text(assignee.display));
                    }
                    var assignToMe = $contentDiv.find(".issue-assign-to-me");
                    if (resp.trustedApps && review.getLoggedInUsername()) {
                        assignToMe.show();
                    } else {
                        assignToMe.hide();
                    }
                }
                $contentDiv.find(".issue-assignee-spinner").hide();
            };

            $contentDiv.find(".issue-assignee-spinner").show();
            AJS.FECRU.AJAX.ajaxDo(url, params, done, false);

            $contentDiv.data("dialogHider", dialogHider);
            showPopup();
        };

        var options = {
            onHover: false,
            showArrow: true,
            fadeTime: 200,
            hideDelay: 200,
            showDelay: 200,
            width: 400,
            offsetX: 0,
            offsetY: 2,
            container: "body",
            useLiveEvents:true,
            cacheContent:false,
            upfrontCallback: function() {
                //provide a hook into the hide function
                var that = this;
                dialogHider.hideDialog = function() {
                    that.hide();
                };
            }
        };

        AJS.InlineDialog(".commentIssueKeyUnlinked", "create-issue-inline-dialog", contentHandler, options);
    });

})();;
/* END /2static/script/cru/review/comment/comment-issue.js */
/* START /2static/script/cru/review/comment/comment-form.js */
AJS.CRU.COMMENT.FORMS = {};

(function() {

    /**
     * CommentForm class
     * Base commentForm class extend for specific forms
     */
    var CommentForm = function (id, source) {
        var name = id;
        var $form;
        var $textBox;
        var inited = false;
        var sourceForm = source;

        this.con = function(id, source) {
            name = id;
            sourceForm = source;
        };

        this.init = function() {
            if(!inited){
                var $formSource = AJS.$("#"+sourceForm);
                if ($formSource.length === 1) {
                    $form = $formSource.clone();
                    $form.attr('id',name)
                         .attr('name',name);
                    // setAttribute does not work for name in IE! so manually replace the name
                    $form.html( $form.html().replace(/source/,name) )
                         .html( $form.html().replace(/DefectFields/g,'DF') );
                    inited = true;
                }
            }
        };

        this.getForm = function() {
            this.init();
            return $form;
        };

        this.getFormElement = function(name) {
            this.init();
            return $form.find('[name='+name+']');
        };

        this.getTextBox = function() {
            if( !$textBox || $textBox.length !== 1) {
                $textBox = this.getFormElement('newText');
            }
            return $textBox;
        };

        this.setTextChangeListener = function(l) {
            this.getTextBox().bind("onkeyup", l );
        };

        this.getCommentText = function() {
            return this.getTextBox().val();
        };

        this.setDefaultFocus = function() {
            (this.getTextBox()[0]).focus();
        };

        this.clearForm = function() {
            this.resetButtons();
            var clear = function(element) {
                try {
                    var $element = AJS.$(element);
                    var type = $element.attr("type");
                    if(type == "checkbox") {
                        $element.removeAttr("checked");
                        $element.val(false);
                    } else if (type == "button" || type == "submit") {
                        // don't do anything to button's values
                        // (in IE6/7, clearing .value clears the button text)
                        $element.removeAttr("disabled"); // but make sure they are enabled
                    } else {
                        $element.val("");
                    }
                } catch(e) {
                    //ignore. happens in safari/konqueror when comment form is cleared when not displayed
                }
            };

            // this.getForm().find('input, select, textarea') fails under
            // Safari 3.2 and jQuery 1.3.2 when the form is not yet in the DOM.
            // See http://dev.jquery.com/ticket/4554
            var $form = this.getForm();

            var i, len, $inputs;

            $inputs = $form.find('input')
                .add($form.find('textarea'))
                .add($form.find('select'));

            for (i = 0, len = $inputs.length; i < len; i++) {
                clear($inputs[i]);
            }
        };

        this.resetButtons = function() {
            this.toggleButtons(true);
            this.getFormElement("saveAsDraft").text("Save as Draft");
            this.getFormElement("cancelComment").show();
            this.getFormElement("discardComment").hide();
        };

        this.activateAutosaveButtons = function() {
            this.getFormElement("saveAsDraft").text("Keep as Draft");
            this.getFormElement("cancelComment").hide();
            this.getFormElement("discardComment").show();
        };

        this.toggleButtons = function(enable) {
            this.getForm().children("button").each( function() {
                try{
                    this.disabled = !enable;
                } catch(e) {
                    //ignore. happens in safari/konqueror when comment form is cleared when not displayed
                }
            } );
        };

        this.hasCommentId = function() {
            var cid = this.getCommentId();
            return cid != "" && cid != "-1";
        };

        this.getCommentId = function () {
            return this.getFormElement("commentId").val();
        };

        this.setFormVal = function(name, val) {
            this.init();
            try{
                var $formEl = this.getFormElement(name);
                if( $formEl.attr("type") == "checkbox") {
                    $formEl.attr("checked", val ? "checked" : "");
                }
                $formEl.val(val);
            } catch(e) {
                //ignore. happens in safari/konqueror when comment form is cleared when not displayed
            }
        };

        this.setFormElementVis = function(name, val) {
            this.init();
            try {
                this.getFormElement(name).css("display", val);
            } catch(e) {
                //ignore. happens in safari/konqueror when comment form is cleared when not displayed
            }
        };

        this.showMetrics = function(prefix, show) {
            var nodeName = prefix + "DF";
            toggleNodeAndImage(nodeName, show, !show, true);
        };
    };

    /**
     * CommentForm class
     * This holds a single revision comment form and controls access to the form as well as providing
     * useful methods
     */
    var RevisionCommentForm = function (n, s) {
        this.con(n,s);

        this.setFromLines = function(fromLines) {
            this.setFormVal("fromLineRange",fromLines);
        };

        this.setToLines = function(toLines) {
            this.setFormVal("toLineRange",toLines);
        };

        this.setFrxId = function(id) {
            this.setFormVal("frxId",id);
        };

        this.setFromFrxRevision = function(id) {
            this.setFormVal("fromFrxRevisionId",id);
        };

        this.setToFrxRevision = function(id) {
            this.setFormVal("toFrxRevisionId",id);
        };

        this.clearComment = function() {
            this.clearForm();
            this.showMetrics("revision", false);
            this.setFormElementVis("saveAsDraft", "");
        };

        this.populateFromComment = function(comment) {
            this.setFormVal("newText",comment.message());
            this.setFormVal("commentId",comment.id());
            this.setFormVal("toLineRange",comment.toLineRange());
            this.setFormVal("fromLineRange",comment.fromLineRange());
            this.setFormVal("defect",comment.defect());
            var rcf = this;
            AJS.$.each(comment.metrics(), function() {
                rcf.setFormVal(this.label, this.value);
            });
            this.showMetrics("revision", comment.defect());
            if (!comment.draft()) {
                this.setFormElementVis("saveAsDraft", "none");
            }
        };
    };
    RevisionCommentForm.prototype = new CommentForm();

    AJS.CRU.COMMENT.FORMS.RevisionCommentForm = RevisionCommentForm;


    var FileCommentForm = function (n, s) {
        this.con(n,s);

        this.setFrxId = function(id) {
            this.setFormVal("frxId",id);
        };

        this.clearComment = function() {
            this.clearForm();
            this.showMetrics("file", false);
            this.setFormElementVis("saveAsDraft", "");
        };

        this.populateFromComment = function(comment) {
            this.setFormVal("newText",comment.message());
            this.setFormVal("commentId",comment.id());
            this.setFormVal("defect",comment.defect());
            var rcf = this;
            var metrics = comment.metrics();
            for (var i = 0, len = metrics.length; i < len; i++) {
                var m = metrics[i];
                rcf.setFormVal(m.label, m.value);
            }
            this.showMetrics("file", comment.defect());
            if (!comment.draft()) {
                this.setFormElementVis("saveAsDraft", "none");
            }
        };
    };
    FileCommentForm.prototype = new CommentForm();

    AJS.CRU.COMMENT.FORMS.FileCommentForm = FileCommentForm;

    var GeneralCommentForm = function (n, s) {
        this.con(n,s);
        this.clearComment = function(){
            this.clearForm();
            this.showMetrics("general", false);
            this.setFormElementVis("saveAsDraft", "");
        };

        this.populateFromComment = function(comment){
            this.setFormVal("newText",comment.message());
            this.setFormVal("commentId",comment.id());
            this.setFormVal("defect",comment.defect());
            var gcf = this;
            var metrics = comment.metrics();
            for (var i = 0, len = metrics.length; i < len; i++) {
                var m = metrics[i];
                gcf.setFormVal(m.label, m.value);
            }
            this.showMetrics("general", comment.defect());
            if (!comment.draft()) {
                this.setFormElementVis("saveAsDraft", "none");
            }
        };
    };
    GeneralCommentForm.prototype = new CommentForm();

    AJS.CRU.COMMENT.FORMS.GeneralCommentForm = GeneralCommentForm;

    var ReplyCommentForm = function (n, s) {
        this.con(n,s);

        this.clearComment = function(){
            this.clearForm();
            this.setFormElementVis("saveAsDraft", "");
        };

        this.setReplyToId = function(replyToId){
            this.setFormVal("replyToId", replyToId);
        };

        this.populateFromComment = function(comment){
            this.setFormVal("newText",comment.message());
            this.setFormVal("commentId",comment.id());
            if (!comment.draft()) {
                this.setFormElementVis("saveAsDraft", "none");
            }
        };
    };
    ReplyCommentForm.prototype = new CommentForm();

    AJS.CRU.COMMENT.FORMS.ReplyCommentForm = ReplyCommentForm;

    /**
     * This is a CommentForm handler class that looks after inserting/removing a comment form inside a parent element
     * It also manages other elements you wish to hide or unhide while displaying the element
     */
    var CommentFormWrangler = function (commentform, namePrefix, autosaveFuncName) {
        var that = this;
        var $switchElement;
        var cForm = commentform;
        var $insertParent;
        var $insertPoint;
        var insertRow = false;
        var displaying = false;
        var $wrapperDiv;
        var wrapperRow;
        var divWidth;
        var inited = false;
        var changeParentDisplay = false;
        var autosaveChecksEnabled = false;
        var lastAutosaveDate = new Date();
        var lastTextChangeDate = new Date();
        var lastAutosaveText = "";

        var TableRow = function (id) {
            var $cell = AJS.$(document.createElement('td'));
            var $row  = AJS.$(document.createElement('tr'))
                            .attr('id',id)
                            .append($cell);

            this.setSpan = function(colSpan){
                $cell.attr('colSpan',colSpan);
            };

            this.getCell = function() {
                return $cell;
            };

            this.getRow = function() {
                return $row;
            };
        };

        function init() {
            if(!inited) {
                $wrapperDiv = AJS.$(document.createElement('div'));
                wrapperRow = new TableRow('commentTR');
                $wrapperDiv.addClass('commentForm');
                $wrapperDiv.append(cForm.getForm());

                cForm.setTextChangeListener(that.autosaveTextChanged);

                inited = true;
            }
        }

        this.getCommentForm = function(){
            init();
            return cForm;
        };

        this.setWidth = function(width){
            $wrapperDiv.width(width);
            divWidth = width;
        };

        this.unsetWidth = function() {
            $wrapperDiv.width('');
            divWidth = null;
        };

        this.setInsertPoint = function(parent, insertAfter) {
            init();
            $insertParent = AJS.$(parent);
            $insertPoint = AJS.$(insertAfter);
            insertRow = $insertParent.is('TABLE') || ($insertParent.is('TBODY') );
        };

        this.setSwitchElement = function(elementId){
            init();
            $switchElement = AJS.$("#"+elementId);
        };

        this.isDisplaying = function(){
            init();
            return displaying;
        };

        this.getParent = function() {
            return $insertParent;
        };

        this.isDisplayingRow = function(){
            init();
            return insertRow;
        };

        function switchParentOn(){
            if($insertParent.is(":hidden")) {
                changeParentDisplay = true;
                $insertParent.show();
            } else {
                changeParentDisplay = false;
            }
            $switchElement.hide();
        }

        function switchParentOff(){
            if(changeParentDisplay) {
                $insertParent.hide();
            }
            changeParentDisplay = false;
            $switchElement.show();
        }

        /*
         insert/append the element to the parent and hide all the switch elements
        */
        this.insertAndSwitch = function(){
            init();
            if(!displaying) {
                if(insertRow) { //if Row the insertpoint MUST be a ROW
                    // HACK! any colspan which is larger than the actual colspan makes the row fit nice and snug.
                    // We could easily calculate the true colspan of a table row (tr[cols].length is incorrect though)
                    // but for performance reasons it's just as easy to let the browser do the work. Fuck browsers.
                    wrapperRow.setSpan('99');
                    wrapperRow.getCell().append($wrapperDiv);
                    wrapperRow.getRow().show().insertAfter($insertPoint);
                } else {
                    $wrapperDiv.appendTo($insertParent);
                }
                $wrapperDiv.show();
                switchParentOn();
                displaying = true;
            }
        };

        this.removeAndSwitch = function(){
            init();
            if(displaying) {
                if(insertRow) {
                    $wrapperDiv.hide();
                    wrapperRow.getRow().hide();
                } else {
                    $wrapperDiv.hide();
                }
                switchParentOff();
                displaying = false;
            }
        };

        /* remove the element from current insertpoint restoring all current switch elements and
           put it in the new insert point with the new switch and display
        */
        this.exchange = function(parent, insertBefore, newSwitchElementId){
            init();
            if(displaying){
                this.removeAndSwitch();
            }
            this.setSwitchElement(newSwitchElementId);
            this.setInsertPoint(parent, insertBefore);
            this.insertAndSwitch();
        };

        this.move = function (insertBefore) {
            this.setInsertPoint($insertParent, insertBefore);
            this.removeAndSwitch();
            this.insertAndSwitch();
        };

        this.formOpened = function(isdraft) {
            cForm.resetButtons();
            autosaveChecksEnabled = isdraft;
            AJS.$("#"+namePrefix + "AutosaveMsg").html("");
            if (autosaveChecksEnabled) {
                if (cForm.hasCommentId()) {
                    cForm.activateAutosaveButtons();
                }
                lastAutosaveDate = new Date();
                lastTextChangeDate = new Date();
                lastAutosaveText = cForm.getCommentText();
            }
        };

        this.formClosed = function() {
            autosaveChecksEnabled = false;
        };

        /**
         * Converts a date object to a string in the format of HH:MM [AM|PM]
         * @param d date object to stringify
         */
        var date2TimeOfDay = function (d) {
            var s = "AM";
            var h = d.getHours();
            var m = d.getMinutes();

            if (h == 12) {
                s = "PM";
            } else if (h > 12) {
                s = "PM";
                h = h - 12;
            }
            m = "" + m;
            if (m.length < 2) {
                m = "0" + m;
            }
            return h + ":" + m + " " + s;
        };

        this.autosaveTimerPing = function() {
            if (!autosaveChecksEnabled || AJS.CRU.COMMENT.commentAjaxExecutor.isPending() || !that.isDisplaying()) {
                return;
            }

            var now = new Date();
            var sinceLastSave = now.getTime() - lastAutosaveDate.getTime();
            var sinceLastEdit = now.getTime() - lastTextChangeDate.getTime();

            if ((sinceLastSave < 30000) && (sinceLastEdit < 2000)) {
                // don't bother saving if they have edited in the last 2s
                // until 30s passes
                return;
            }

            var currentText = cForm.getCommentText();
            if (currentText == lastAutosaveText) {
                return;
            }

            if (/^\s*$/.test(currentText)) {
                return; // you can't save empty comments at the moment
            }

            lastAutosaveText = currentText;
            lastAutosaveDate = now;

            cForm.toggleButtons(false);

            var done = function(resp) {
                cForm.toggleButtons(true);

                if (resp.worked) {
                    AJS.$("#"+namePrefix + "AutosaveMsg").text("Autosaved at " + date2TimeOfDay(new Date()));
                    cForm.setFormVal("commentId", resp.comment.id);
                    commentator.createOrUpdateComment(resp);
                    commentator.updateCommentCount(resp);

                    cForm.activateAutosaveButtons();
                } else {
                    AJS.$("#"+namePrefix + "AutosaveMsg").text("problem autosaving");
                }
            };

            var autosaveFunc = eval(autosaveFuncName);
            autosaveFunc(permaId, cForm.getForm(), done); // NB: permaId is a global
            AJS.$("#"+namePrefix + "AutosaveMsg").text("Autosaving...");
        };

        this.autosaveTextChanged = function() {
            if (!autosaveChecksEnabled) {
                return;
            }
            var currentText = cForm.getCommentText();
            if (currentText == lastAutosaveText) {
                return;
            }

            lastTextChangeDate = new Date();

            var $autosaveMsg = AJS.$("#"+namePrefix + "AutosaveMsg");
            var msg = $autosaveMsg.text();
            if (msg != "" && !/\*$/.test(msg)) {
                // put an astrix at the end when it is unsaved
                $autosaveMsg.text(msg + " *");
            }
            msg = "changed, not saved";
        };
    };

    AJS.CRU.COMMENT.FORMS.CommentFormWrangler = CommentFormWrangler;
})();
;
/* END /2static/script/cru/review/comment/comment-form.js */
/* START /2static/script/cru/review/comment/commentator.js */
AJS.CRU.COMMENT = (function() {
    var g_mousedown = false;
    var g_modeSelecting = false;
    var g_currentTable = null;
    var g_mouseDownFirstRow = null;
    var lastSelectedLine = null;
    var firstTimeDisplayingInlineForm = true;
    var frxPaneWidth = null;
    var commentDivPrefixes = ['general','revision','inline','above','defect'];

    var commentFormNS = AJS.CRU.COMMENT.FORMS;
    var scFormWrangler = new commentFormNS.CommentFormWrangler(new commentFormNS.GeneralCommentForm('simpleCommentForm','commentFormTemplate'), 'general', 'commentAjaxController.autoSaveGeneralComment');
    var rcFormWrangler = new commentFormNS.CommentFormWrangler(new commentFormNS.RevisionCommentForm('revisionCommentForm','revisionCommentFormTemplate'), 'revision', 'commentAjaxController.autoSaveRevisionComment');
    var fcFormWrangler = new commentFormNS.CommentFormWrangler(new commentFormNS.FileCommentForm('fileCommentForm','fileCommentFormTemplate'), 'file', 'commentAjaxController.autoSaveRevisionComment');
    var replyFormWrangler = new commentFormNS.CommentFormWrangler(new commentFormNS.ReplyCommentForm('replyCommentForm','replyFormTemplate'), 'reply', 'commentAjaxController.autoSaveReply');

    var tetrisCellVisibleCommentCount = {};
    var tetrisCellInvisibleCommentCount = {};
    var tetrisCellVisibleFirstCount = {};

    var highlightedLines = [];
    var editingRCHandleId;

    var selectTR = function (tr, select) {
        var $tr = AJS.$(tr);
        if ($tr.hasClass('commentableLine')) {
            $tr.toggleClass('lineHighlighted', select)
               .toggleClass('sourceLine', !select);
            if (!select) {
                $tr.removeClass('hoveringComment');
            }
        }
    };

    var clearSelectedRows = function () {
        g_modeSelecting = false;
        if(g_currentTable) {
            AJS.$(g_currentTable).find("tr.lineHighlighted").each(function() {
                 selectTR(this, false);
            });
        } else {
            AJS.$("tr.lineHighlighted").each(function() {
                 selectTR(this, false);
            });
        }
        rcFormWrangler.getCommentForm().clearComment();
        g_modeSelecting = false;
        g_currentTable = null;
        g_mouseDownFirstRow = null;
        firstTimeDisplayingInlineForm = true;
    };

    var convertLinesToRange = function (lines) {
        var lineStr = "";
        var count = 0;
        for (var i=0, len = lines.length; i < len; i++) {
            var line = lines[i];
            if (count != 0 && line == (parseInt(lines[i-1],10)+1)) {
                if (line != (parseInt(lines[i+1], 10) - 1)) {
                    lineStr += "-" + line;
                    count = 0;
                } else {
                    count++;
                }
            } else {
                if (i>0) { lineStr += ", "; }
                lineStr += line;
                count++;
            }
        }
        return lineStr;
    };

    var convertRangeToLines = function (ranges) {
        var lines = [];
        if( !ranges ) {
            return lines;
        }
        var range = ranges.split(",");
        for (var i=0, len = range.length; i < len; i++){
            var s = range[i].split("-");
            var s0 = s[0];
            if (s.length === 1) {
                lines.push(parseInt(s0,10));
            } else {
                for (var b=0; b <= (s[1]-s0); b++) {
                    lines.push(parseInt(s0, 10)+b);
                }
            }
        }
        return lines;
    };

    var displayCommentForm = function (handleId, parentId, commentId, wrangler, exchanger) {
        var parent = AJS.$("#"+parentId);
        //display before setting the new values because safari/konqueror don't let you change the values
        //if not attached to the document unlike IE and FF
        if (exchanger) {
            exchanger();
        } else {
            wrangler.exchange(parent, null, handleId);
        }
        var isDraft = true;
        if(commentId) {
            var comment = review.comment(commentId);
            wrangler.getCommentForm().populateFromComment(comment);
            isDraft = comment.draft();
        } else {
            wrangler.getCommentForm().clearComment();
        }
        wrangler.formOpened(isDraft);
        wrangler.getCommentForm().setDefaultFocus();
    };

    var clearForm = function (wrangler) {
        wrangler.removeAndSwitch();
        wrangler.getCommentForm().clearComment();
    };

    /******************************************************************
     ******************** INLINE COMMENTS *****************************
     ******************************************************************/

    var checkInlineCommentBox = function ($tableElement, forceFormExchange) {
        if(res.commentLock) {
            return false;
        }
        var toLinesSelected = [];
        var fromLinesSelected = [];
        var lastSelected = null;

        $tableElement.children('tbody').children("tr.lineHighlighted:not(.hoveringComment)").each( function() {
            var num = this.id.split("_line")[1];
            if (num.charAt(0) === 'B') { // Both
                var lines = num.split(",");
                fromLinesSelected.push(lines[0].replace(/^Both/, ""));
                toLinesSelected.push(lines[1]);
            } else if (num.charAt(0) === 'F') { // From
                fromLinesSelected.push(num.replace(/^From/, ""));
            } else { // To
                toLinesSelected.push(num);
            }
            lastSelected = this;
        } );

        var commentForm = rcFormWrangler.getCommentForm();
        if (lastSelected != null) {
            var frxId = lastSelected.id.split('_')[0];
            //must display first before setting values or safari/konqueror bork
            var formHidden = commentForm.getForm().is(":hidden");
            if (forceFormExchange || formHidden) {
                if (formHidden) { // show it
                    rcFormWrangler.exchange(AJS.$(lastSelected).closest("table"), lastSelected, null);
                } else { // move it
                    rcFormWrangler.move(lastSelected);
                }
            }
            //only if displaying for the first time do we want to clear the comment text
            //so that if they click a line after entering text it doesn't clear
            if (firstTimeDisplayingInlineForm) {
                commentForm.clearComment();
                commentForm.setDefaultFocus();
                rcFormWrangler.formOpened(true);
                firstTimeDisplayingInlineForm = false;
            }
            commentForm.setFromLines(convertLinesToRange(fromLinesSelected));
            commentForm.setToLines(convertLinesToRange(toLinesSelected));
            commentForm.setFrxId(frxId);
            var frx = review.frx(frxId);
            commentForm.setFromFrxRevision(frx.visibleFromRevision());
            commentForm.setToFrxRevision(frx.visibleToRevision());
            if(rcFormWrangler.isDisplayingRow()){
                rcFormWrangler.setWidth(getInlineCommentFormWidth());
            } else {
                rcFormWrangler.unsetWidth();
            }
            lastSelectedLine = lastSelected;
        } else {
            //clean up
            if(commentForm.getTextBox().val() !== '') {
                var popupDeleteResponse = window.confirm("You've deselected all lines\n\n" +
                                                         "Delete the comment?\n");
                if (popupDeleteResponse) {
                    if (commentForm.hasCommentId()) {
                        commentator.deleteComment(commentForm.getCommentId(), permaId);
                    }
                    res.clearRevisionCommentBox();
                }
            } else {
                res.clearRevisionCommentBox();
            }
        }
        return false;
    };

    var insertAjaxRevisionComment = function (resp) {
        fcFormWrangler.removeAndSwitch();
        AJS.$('#addFileCommentLink' + fcFormWrangler.getCommentForm().getFormElement('frxId').val()).show();
        if (resp.worked) {
            var commentId = resp.comment.id;
            commentator.replaceOrInsertComment('revision', commentId, resp.commentHtml, 'revision_comments_frxinner' + resp.frxId);
        }
    };

    var insertAjaxInlineComment = function(resp) {
        if (resp.worked) {
            res.clearRevisionCommentBox();
            var commentId = resp.comment.id;
            var $oldComment = AJS.$('#inlinecomment' + commentId);
            var isEdit = $oldComment.length === 1;

            var createNewRow = true;

            // comment exists, let's update it
            if (isEdit) {
                if (lastSelectedLine) {
                    var $row = $oldComment.closest("tr");
                    // check to see if the updated comment is on a different last line.
                    var $oldLastLine = $row.prevAll(".sourceLine:first");
                    if ($oldLastLine.length === 1) {
                        var sameLastLine = $oldLastLine.attr('id') === lastSelectedLine.id;
                        if (sameLastLine) {
                            $oldComment.parent().html(resp.inlineCommentHtml);
                            createNewRow = false;
                        } else {
                            $row.remove();
                        }
                    }
                    editingRCHandleId = null;
                } else {
                    // the parent is the td which holds the contents of the inline comment
                    $oldComment.parent().html(resp.inlineCommentHtml);
                    AJS.log("TODO: Check how this gets called");
                }
            }

            if (createNewRow) {
                if (lastSelectedLine) {
                    var frx = review.frx(resp.frxId);
                    var colSpan = frx.colspan();
                    var $cell = AJS.$("<td colspan='"+colSpan+"'></td>")
                                    .html(resp.inlineCommentHtml);
                    AJS.$("<tr class='comment-row'></tr>")
                        .append($cell)
                        .insertAfter(lastSelectedLine);
                }
            }
            if (isEdit) {
                // aboveCommentHtml contains the edit div too, so just remove it
                AJS.$("#abovecommentEdit" + commentId).remove();
                AJS.$("#abovecomment" + commentId).replaceWith(resp.aboveCommentHtml);
            } else {
                AJS.$("#inline_comments_frxinner" + resp.frxId).append(resp.aboveCommentHtml);
            }

            res.setCommentWidths('inlinecomment' + commentId, true);
        }
    };

    var leaveCommentUnread = function (commentId) {
        if (canChangeCommentStatus(commentId, 'leaveUnread')) {
            res.updateCommentReadStatus(commentId, permaId, false);
        }
    };

    var canChangeCommentStatus = function (commentId, status) {
        var comment = review.comment(commentId);
        var $comment = AJS.$(res.commentContainerDivs(commentId)).children('.comment');
        return review.writable() && !($comment.hasClass('readStatusLocked') || comment.status() === status);
    };

    var autosaveTimer = function () {
        try {
            rcFormWrangler.autosaveTimerPing();
            scFormWrangler.autosaveTimerPing();
            fcFormWrangler.autosaveTimerPing();
            replyFormWrangler.autosaveTimerPing();
        } finally {
            setTimeout(autosaveTimer, 10000); // every 10s
        }
    };

    var calculateFrxPaneWidth = function () {
        // Leave room for the scrollbar.
        return AJS.$('#frx-pane').width() - 8;
    };

    var getInlineCommentFormWidth = function () {
        var commentFormIndent = 30;
        var wikiPreviewIconWidth = 22;
        if (review.frxs()) {
            var currentWidth = calculateFrxPaneWidth();
            if (frxPaneWidth != currentWidth) {
                frxPaneWidth = currentWidth;
            }
        }
        return frxPaneWidth - commentFormIndent - 21 - wikiPreviewIconWidth;
    };

    var deleteComments = function (midFix, commentId) {
        for (var i = 0, len = commentDivPrefixes.length; i < len; i++) {
            var prefix = commentDivPrefixes[i];
            AJS.$("#"+prefix+midFix+commentId).remove();
        }
        res.clearCommentedLines();
    };

    var updateRevisionSliderCounts = function (comment, totalDelta, unreadDelta) {
        if (comment.isInline()) {
            if (comment.fromRevId()) {
                updateCommentCountImpl('frxRev', comment.fromRevId(), totalDelta, true);
                updateUnreadCommentCountImpl('frxRev', comment.fromRevId(), unreadDelta, true);
            }
            if (comment.toRevId() && comment.fromRevId() !== comment.toRevId()) {
                updateCommentCountImpl('frxRev', comment.toRevId(), totalDelta, true);
                updateUnreadCommentCountImpl('frxRev', comment.toRevId(), unreadDelta, true);
            }
        }
    };

    // Comment counts
    var updateUnreadCommentCountImpl = function (idPrefix, idSuffix, unreadCommentCount, isDelta) {
        var $unread = AJS.$('#'+idPrefix+'UnreadCommentCount' + idSuffix);
        var unreadCount = isDelta ? (parseInt($unread.text(), 10) + unreadCommentCount) : unreadCommentCount;
        $unread.text(unreadCount);

        var $unreadWrapper = AJS.$('#'+idPrefix+'UnreadCommentCountWrapper'+idSuffix);
        if (unreadCount > 0) {
            $unreadWrapper.show();
        } else {
            $unreadWrapper.hide();
        }
    };

    var updateFRXCommentCountImpl = function (frxId, commentCount, isDelta) {
        updateCommentCountImpl('frx', frxId, commentCount, isDelta);
        updateCommentCountImpl('frxpath', frxId, commentCount, isDelta);
    };

    var updateCommentCountImpl = function (idPrefix, idSuffix, commentCount, isDelta) {
        var $total = AJS.$('#'+idPrefix+'CommentCount' + idSuffix);
        var currentTotal = parseInt($total.text(), 10);
        var newCommentCount = isDelta ? currentTotal + commentCount : commentCount;
        $total.text(newCommentCount);

        var $unreadSpan = AJS.$('#'+idPrefix+'UnreadComment' + idSuffix);
        if (newCommentCount > 0) {
            $total.show();
            $unreadSpan.show();
        } else {
            $total.hide();
            $unreadSpan.hide();
        }
    };

    var updateGeneralCommentCount = function (resp) {
        if (resp.worked) {
            var commentCountDelta = resp.generalCommentCountDelta;
            var unreadCommentCountDelta = resp.generalUnreadCommentCountDelta;
            updateCommentCountImpl('general', '', commentCountDelta, true );
            updateUnreadCommentCountImpl('general', '', unreadCommentCountDelta, true );
        }
    };

    var modifyIndicatorDisplay = function (midFix, commentId, value) {
        AJS.$.each( commentator.commentDivPrefixes, function() {
            AJS.$("#"+this+midFix+commentId).css('display',value);
        });
    };

    var res = {

        tetrisCellVisibleCommentCount : tetrisCellVisibleCommentCount,
        tetrisCellVisibleFirstCount : tetrisCellVisibleFirstCount,
        tetrisCellInvisibleCommentCount : tetrisCellInvisibleCommentCount,
        showCommentsInline : function () { return AJS.$("body").hasClass("show-inline-comments"); },
        showCommentsAbove : function () { return AJS.$("body").hasClass("show-above-comments"); },
        hideSourceComments : function () { return AJS.$("body").hasClass("hide-comments"); },
        showSource : true,
        commentDivPrefixes : commentDivPrefixes,
        g_pageCompletelyLoaded : false,
        commentLock : false,

        replaceOrInsertComment : function (commentType, commentId, commentHtml, appendTo) {
            var $oldComment = AJS.$('#' + commentType + 'comment' + commentId);
            if ($oldComment.length > 0) {
                // commentHtml contains the edit div too, so just remove it
                AJS.$('#' + commentType + 'commentEdit' + commentId).remove();
                $oldComment.replaceWith(commentHtml);
            } else {
                AJS.$('#' + appendTo).append(commentHtml);
            }
        },

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

        showCommentedLines : function (frxId, fromLineRange, toLineRange){
            highlightedLines = res.getCommentedLines(frxId, fromLineRange, toLineRange);
            AJS.$(highlightedLines).addClass('lineHighlighted hoveringComment');
        },

        clearCommentedLines : function () {
            AJS.$(highlightedLines).removeClass('lineHighlighted hoveringComment');
            highlightedLines = [];
        },

        getHighlightedLines : function () {
            return highlightedLines;
        },

        /******************************************************************
         ******************** COMMON METHODS ******************************
         ******************************************************************/

        checkGeneralCommentsWarning : function (frxId) {
            if (!frxId) {
                if (review.generalCommentIds().length === 0) {
                    AJS.$('#no-general-comments').show();
                    AJS.$('#addReviewCommentLink').hide();
                } else {
                    AJS.$('#no-general-comments').hide();
                    AJS.$('#addReviewCommentLink').show();
                }
            } else {
                if (review.frx(frxId).fileComments().length === 0) {
                    AJS.$('#addFileCommentLink' + frxId).hide();
                } else {
                    AJS.$('#addFileCommentLink' + frxId).show();
                }
            }
        },

        updateFRXUnreadCommentCount : function (frxId, unreadCommentCount, isDelta) {
            updateUnreadCommentCountImpl('frx', frxId, unreadCommentCount, isDelta);
            updateUnreadCommentCountImpl('frxpath', frxId, unreadCommentCount, isDelta);
        },

        changeCommentStatus : function (commentId, newStatus, oldStatus) {
            AJS.$(res.commentContainerDivs(commentId))
                .children('.comment')
                .removeClass(oldStatus)
                .addClass(newStatus);
            review.comment(commentId).setStatus(newStatus);
        },

        displaySimpleCommentForm : function (handleId, parentId, commentId) {
            displayCommentForm(handleId, parentId, commentId, scFormWrangler);
        },

        createOrUpdateComment : function (resp) {
            if (resp.worked) {
                var commentResp = resp.comment;

                var comment = review.comment(commentResp.id);
                if (!comment) {
                    var position = 'general';
                    if (resp.replyToId) {
                        position = review.comment(resp.replyToId).position();
                    } else if (resp.frxId) {
                        if (resp.whole) {
                            position = 'revision';
                        } else {
                            position = 'inline';
                        }
                    }
                    var type = resp.replyToId ? 'reply' : 'comment';
                    var commentType = position + type;

                    comment = new Comment(commentResp.id, commentType);

                    if (resp.replyToId) {
                        var parent = review.comment(resp.replyToId);
                        comment.setReplyTo( parent );
                        parent.addReply(comment);
                    }

                    comment.setReview(review);
                    review.addComment(comment);
                    if (resp.frxId) {
                        var frx = review.frx(resp.frxId);
                        comment.setFrx(frx);
                        frx.addComment(comment);
                    } else {
                        res.checkGeneralCommentsWarning();
                    }

                }
                comment.setMessage(commentResp.message)
                       .setToLineRange(commentResp.toLineRange)
                       .setFromLineRange(commentResp.fromLineRange)
                       .setGutterLine(commentResp.gutterLine)
                       .setToRevId(commentResp.toRevId)
                       .setFromRevId(commentResp.fromRevId)
                       .setDefect(commentResp.defect)
                       .setDraft(commentResp.draft)
                       .setMetrics(commentResp.metrics)
                       .setCommentColor(commentResp.commentColor)
                       .setIssueKey(commentResp.issueKey)
                       .setStatus(commentResp.status);

                var replyComments = resp.replyComments;
                if (replyComments) {
                    comment.clearReplies();
                    for (var i = 0, len = replyComments.length; i < len; i++) {
                        var replyComment = replyComments[i];
                        var replyParent = review.comment(replyComment.replyToId);
                        if (replyParent) {
                            replyParent.addReply(commentator.createOrUpdateComment(replyComment));
                        }
                    }
                }

                return comment;
            }
        },

        updateCommentCount : function (resp) {
            if (resp && resp.worked) {
                res.updateTotalCommentCount(resp.totalCommentCountDelta, true);
                res.updateTotalUnreadCommentCount(resp.totalUnreadCommentCountDelta, true);
                if (resp.frxId) {
                    res.updateFRXCommentCount(resp);
                } else {
                    updateGeneralCommentCount(resp);
                }
            } else if (!resp) {
                res.updateTotalCommentCount(review.comments().length, false);
                res.updateTotalUnreadCommentCount(review.unreadComments().length, false);
                updateCommentCountImpl('general', '', review.generalComments().length, false);
                updateUnreadCommentCountImpl('general', '', review.unreadGeneralComments().length, false);
                var frxs = review.frxs();
                for (var i = 0, len = frxs.length; i < len; i++) {
                    var frx = frxs[i];
                    var comments = frx.comments();
                    updateFRXCommentCountImpl(frx.id(), comments.length, false);
                    res.updateFRXUnreadCommentCount(frx.id(), frx.unreadComments().length, false);
                    var frxRevCommentCounts = {};
                    var frxRevUnreadCommentCounts = {};
                    var j, innerLen;
                    for (j = 0, innerLen = comments.length; j < innerLen; j++) {
                        var comment = comments[j];
                        if (comment.isInline()) {
                            var toRevId = comment.toRevId();
                            var toRevCount = frxRevCommentCounts[toRevId];
                            var toRevUnreadCount = frxRevUnreadCommentCounts[toRevId];
                            if (!toRevCount) {
                                toRevCount = 0;
                            }
                            if (!toRevUnreadCount) {
                                toRevUnreadCount = 0;
                            }
                            toRevCount++;
                            if (comment.status() === 'unread' || comment.status() === 'leaveUnread') {
                                toRevUnreadCount++;
                            }
                            frxRevCommentCounts[toRevId] = toRevCount;
                            frxRevUnreadCommentCounts[toRevId] = toRevUnreadCount;

                            var fromRevId = comment.fromRevId();
                            if (fromRevId !== toRevId) {
                                var fromRevCount = frxRevCommentCounts[fromRevId];
                                var fromRevUnreadCount = frxRevUnreadCommentCounts[fromRevId];
                                if (!fromRevCount) {
                                    fromRevCount = 0;
                                }
                                if (!fromRevUnreadCount) {
                                    fromRevUnreadCount = 0;
                                }
                                fromRevCount++;
                                if (comment.status() === 'unread' || comment.status() === 'leaveUnread') {
                                    fromRevUnreadCount++;
                                }
                                frxRevCommentCounts[fromRevId] = fromRevCount;
                                frxRevUnreadCommentCounts[fromRevId] = fromRevUnreadCount;
                            }
                        }
                    }
                    var frxRevs = frx.frxRevisions();
                    for (j = 0, innerLen = frxRevs.length; j < innerLen; j++) {
                        var revId = frxRevs[j];
                        updateCommentCountImpl('frxRev', revId, frxRevCommentCounts[revId] || 0, false);
                        updateUnreadCommentCountImpl('frxRev', revId, frxRevUnreadCommentCounts[revId] || 0, false);
                    }
                }

            }
        },

        updateTotalCommentCount : function (count, isDelta) {
            var $total = AJS.$("#totalCommentCount");
            var total;
            if (isDelta) {
                total = parseInt($total.text(), 10);
                total += count;
            } else {
                total = count;
            }

            $total.text(total);
        },

        updateTotalUnreadCommentCount : function (count, isDelta) {
            var $unread = AJS.$("#totalUnreadCommentCount");
            var unread;
            if (isDelta) {
                unread = parseInt($unread.text(), 10);
                unread += count;
            } else {
                unread = count;
            }

            AJS.$('#totalUnreadCommentCountWrapper')[unread > 0 ? 'show' : 'hide']();
            $unread.text(unread);
        },

        discardFileCommentBox : function (commentId, permaId) {
            res.clearFileCommentBox();
            var done = function () {
                review.removeComment(review.comment(commentId));
            };
            commentAjaxController.deleteComment(commentId, permaId, done);
        },

        discardRevisionCommentBox : function (commentId, permaId) {
            res.clearRevisionCommentBox();
            var done = function () {
                review.removeComment(review.comment(commentId));
            };
            commentAjaxController.deleteComment(commentId, permaId, done);
        },

        clearFileCommentBox : function () {
            AJS.$('#addFileCommentLink' + fcFormWrangler.getCommentForm().getFormElement('frxId').val()).show();
            clearForm(fcFormWrangler);
            fcFormWrangler.formClosed();
        },

        clearRevisionCommentBox : function () {
            if (rcFormWrangler.isDisplaying()) {
                //a little inefficient, but effective.
                rcFormWrangler.removeAndSwitch();
                clearSelectedRows();
                if(editingRCHandleId){
                    AJS.$("#"+editingRCHandleId).show();
                    editingRCHandleId = null;
                }
                rcFormWrangler.formClosed();
            }
        },

        /** Returns all occurences of the container divs. Needed due to inline/above. */
        commentContainerDivs : function (commentId) {
            var comment = review.comment(commentId);
            var domId = comment.domId();
            var domIds = ['#' + domId];
            if (/^inline/.test(comment.type())) {
                domIds.push('#' + domId.replace('inline', 'above') );
                domIds.push('.comment' + commentId);
            }
            return AJS.$(domIds.join(',')).get();
        },

        replyToComment : function (commentId) {
            var comment = review.comment(commentId);
            if (!comment) {
                return;
            }
            if (comment.isReply()) {
                comment = comment.replyTo(); // get the thread parent
            }
            var $commentElem = AJS.$("#" + comment.domId());
            $commentElem.find("div.comment:first div.comment-actions .replyToComment:first").click();
        },

        /******************************************************************
         ******************** COMMENT EDITING *****************************
         ******************************************************************/

        publishComment: function (commentId, permaId) {
            var done = function (commentId, resp) {
                if (resp.worked) {
                    modifyIndicatorDisplay("draftIndicator", commentId, "none");
                    modifyIndicatorDisplay("draftPublish", commentId, "none");
                    modifyIndicatorDisplay("starComment", commentId, "inline");
                    review.comment(commentId).setDraft(false);
                    AJS.$('.comment' + commentId + ' .comment').removeClass('draft');
                    commentator.updateCommentCount(resp);
                }
            };
            commentAjaxController.publishComment(commentId, permaId, done);
            return false;
        },

        /******************************************************************
         ******************** COMMENT DELETING ****************************
         ******************************************************************/

        deleteComment : function (commentId, permaId) {
            var done = function(resp) {
                if (resp.worked) {
                    var comment = review.comment(commentId);
                    res.removeCommentHtml(comment);
                    if (comment.isInline()) {
                        updateRevisionSliderCounts(comment, -1, 0);
                    }
                    res.updateCommentCount(resp);
                    var parent = comment.replyTo(); // will be undefined for root comments
                    review.removeComment(comment);

                    res.checkGeneralCommentsWarning();

                    if (parent && !parent.hasReplies()) {
                        // Enable the delete link in the parent
                        AJS.$("#" + parent.domId() + " a.deleteComment:first")
                                .removeClass("disabled")
                                .removeAttr("title");
                    }

                    AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
                }
            };
            commentAjaxController.deleteComment(commentId, permaId, done);
            return false;
        },

        removeCommentHtml : function (comment) {
            var commentId = comment.id();
            //turn off all displayed comments with comment ID
            deleteComments("comment", commentId); // TODO optimise
            deleteComments("reply", commentId);   // TODO optimise

            if (comment.isInline()) {
                tetrisCommentController.deleteTetrisCommentMarkers(commentId);
            }
        },

        /******************************************************************
         ******************** REPLY COMMENTS ******************************
         ******************************************************************/

        insertAjaxReply : function (resp) {
            replyFormWrangler.removeAndSwitch();
            if (resp.worked) {

                var replyId = resp.comment.id;
                var idPrefix = resp.type;
                var isInline = idPrefix === 'inline';

                var oldComment = review.comment(replyId);

                // if we are editing, we want to replace the existing replies
                if (oldComment && oldComment.domIdExists()) {
                    // html contains the edit div too, so just remove it
                    AJS.$("#" + idPrefix + "commentEdit" + replyId).remove();
                    AJS.$("#" + idPrefix + "reply" + replyId).replaceWith(resp.html[idPrefix]);

                    if (isInline) {
                        AJS.$("#abovecommentEdit" + replyId).remove();
                        AJS.$("#abovereply" + replyId).replaceWith(resp.html['above']);
                    }
                } else {
                    var parentId = resp.replyToId;
                    AJS.$("#" + idPrefix + "replys" + parentId ).append(resp.html[idPrefix]);

                    if (isInline) {
                        AJS.$("#abovereplys" + parentId ).append(resp.html['above']);
                    }
                }

                res.createOrUpdateComment(resp);

                // Disable the delete link in the parent
                var parent = review.comment(resp.replyToId);
                if (parent.hasReplies()) {
                    AJS.$("#" + parent.domId() + " > div.comment a.deleteComment")
                            .addClass("disabled")
                            .attr("title", "You cannot delete this comment as long as it has replies.");
                }

                AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
            }
        },

        displayReplyCommentForm : function (handleId, parentId, replyToId, commentId) {
            if (replyFormWrangler.isDisplaying() && AJS.$("#"+parentId)[0] === replyFormWrangler.getParent()[0]) {
                return;//already displaying here so do nothing
            }
            displayCommentForm(handleId, parentId, commentId, replyFormWrangler);
            //replyToId must be set or it ain't a reply
            replyFormWrangler.getCommentForm().setReplyToId(replyToId);

            // Disable the "Post" button for repies on drafts:
            AJS.$("#replyCommentForm .postButton")
                    .attr('disabled', review.comment(replyToId).draft());
        },

        discardReplyCommentForm : function (commentId, permaId) {
            res.clearReplyCommentForm();
            var done = function () {
                review.removeComment(review.comment(commentId));
            };
            commentAjaxController.deleteComment(commentId, permaId, done);
        },

        clearReplyCommentForm : function () {
            clearForm(replyFormWrangler);
            replyFormWrangler.formClosed();
        },

        /******************************************************************
         ******************** GENERAL COMMENTS ****************************
         ******************************************************************/

        insertAjaxGeneralComment : function (resp) {
            scFormWrangler.removeAndSwitch();
            if (resp.worked) {

                var commentId = resp.comment.id;
                var oldComment = review.comment(commentId);

                res.replaceOrInsertComment('general', commentId, resp.commentHtml, 'general-comments-container');

                res.createOrUpdateComment(resp);
                res.updateCommentCount(resp);
                AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
            }
        },

        discardGeneralCommentForm : function (commentId, permaId){
            res.clearGeneralCommentForm();
            var done = function () {
                review.removeComment(review.comment(commentId));

                res.checkGeneralCommentsWarning();
            };
            AJS.CRU.COMMENT.commentAjaxController.deleteComment(commentId, permaId, done);
        },

        clearGeneralCommentForm : function () {
            clearForm(scFormWrangler);
            scFormWrangler.formClosed();
            res.checkGeneralCommentsWarning();
        },

        clearFileCommentForm : function () {
            var frxId = fcFormWrangler.getCommentForm().getFormElement('frxId').val();
            AJS.$('#addFileCommentLink' + frxId).show();
            clearForm(fcFormWrangler);
            fcFormWrangler.formClosed();
        },

        /******************************************************************
         ******************** REVISION COMMENTS ***************************
         ******************************************************************/

        displayFileCommentForm : function (handleId, parentId, frxId, commentId) {
            AJS.$('#addFileCommentLink' + frxId).hide();
            displayCommentForm(handleId, parentId, commentId, fcFormWrangler);
            fcFormWrangler.getCommentForm().setFrxId(frxId);

            fcFormWrangler.unsetWidth();
        },

        /**
         * display a comment form as a child of parent with the details in the comment object
         *
         * @param handleId the switch you want to hide while displaying the form
         * @param parentId div/containing block id to display form in
         * @param commentId comment dom id
        */
        displayRevisionCommentForm : function (handleId, parentId, frxId, commentId) {
            clearSelectedRows();

            var comment = review.comment(commentId);
            if (comment) {
                editingRCHandleId = handleId;
                var lines = res.getCommentedLines(frxId, comment.fromLineRange(), comment.toLineRange());
                AJS.$.each(lines, function(i) {
                    var $line = AJS.$(this);
                    if (i === 0) {
                        g_currentTable = $line.parent()[0];
                    }
                    selectTR(this, true);
                    lastSelectedLine = this;
                });
            }

            var exchanger = function () {
                rcFormWrangler.exchange(AJS.$(lastSelectedLine).closest("table"), lastSelectedLine, handleId);
            };
            displayCommentForm(handleId, parentId, commentId, rcFormWrangler, exchanger);

            var commentForm = rcFormWrangler.getCommentForm();
            commentForm.setFrxId(frxId);

            if (/^inline/.test(parentId)){
                rcFormWrangler.setWidth(getInlineCommentFormWidth());
                var frx = review.frx(frxId);
                commentForm.setFromFrxRevision(frx.visibleFromRevision());
                commentForm.setToFrxRevision(frx.visibleToRevision());
                firstTimeDisplayingInlineForm = false;
            } else {
                rcFormWrangler.unsetWidth();
                firstTimeDisplayingInlineForm = true;
            }
        },

        insertRevisionComment : function (resp) {
            if (resp.worked) {
                if (resp.whole) {
                    insertAjaxRevisionComment(resp);
                } else {
                    insertAjaxInlineComment(resp);
                }
                res.createOrUpdateComment(resp);
                res.updateCommentCount(resp);
                AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
            }
        },

        /******************************************************************
         ******************** COMMENT READ STATUS *************************
         ******************************************************************/

        toggleCommentRead : function (commentId, force) {
            var comment = review.comment(commentId);
            if (!comment) {
                return;
            }
            var oldStatus = comment.status();
            if (oldStatus === 'read' || oldStatus === 'unread') {
                leaveCommentUnread(commentId);
            } else {
                res.markCommentRead(commentId, force);
            }
        },

        markCommentRead : function (commentId, force) {
            var comment = review.comment(commentId);
            if (!comment || comment.draft()) {
                return;
            }
            var leaveUnread = comment.status() === 'leaveUnread';
            if ( (!leaveUnread || force) && canChangeCommentStatus(commentId, 'read') ) {
                res.updateCommentReadStatus(commentId, permaId, true);
            }
        },

        leaveCommentUnread : function (commentId) {
            if (canChangeCommentStatus(commentId, 'leaveUnread')) {
                res.updateCommentReadStatus(commentId, permaId, false);
            }
        },

        markAllCommentsRead : function (frxId) {
            var updatedComments = [];
            var i, len, comment;
            var comments;
            if (!frxId) {
                comments = review.comments();
            } else if (review.frx(frxId)) {
                comments = review.frx(frxId).comments();
            } else {
                comments = review.generalComments();
            }

            for (i = 0, len = comments.length; i < len; i++) {
                comment = comments[i];
                if (canChangeCommentStatus(comment.id(), 'read')) {
                    updatedComments.push(comment);
                }
            }

            var done = function(resp) {
                if (resp.worked) {
                    var _res = res; // store a local variable of the outer scope instance

                    for (i = 0, len = updatedComments.length; i < len; i++) {
                        comment = updatedComments[i];
                        _res.changeCommentStatus(comment.id(), 'read', comment.status() );
                    };

                    _res.updateCommentCount();
                }
            };

            commentAjaxController.markAllCommentsRead(updatedComments, permaId, done);
            return false;
        },

        // Comment read status
        updateCommentReadStatus : function(commentId, permaId, markAsRead) {
            // Prevent simultaneous requests.
            var $comments = AJS.$(res.commentContainerDivs(commentId)).children('.comment')
                .addClass('readStatusLocked');
            var comment = review.comment(commentId);

            var oldStatus = comment.status();
            var newStatus = markAsRead ? 'read' : 'leaveUnread';

            var done = function(resp) {
                if (resp.worked) {
                    res.changeCommentStatus(commentId, newStatus, oldStatus);
                    commentator.updateCommentCount(resp);
                    if (comment.isInline()) {
                        commentator.updateFrxRevisionUnreadCommentCount(comment, resp.frxUnreadCommentCountDelta);
                    }
                }
                $comments.removeClass('readStatusLocked');
            };
            commentAjaxController.updateCommentReadStatus(commentId, permaId, markAsRead, done);
            return false;
        },

        /******************************************************************
         ******************** MISC METHODS    *****************************
         ******************************************************************/

        toggleComments : function (view) {
            AJS.$("#set-inline-comments, #set-above-comments, #set-hidden-comments").removeClass("selected");
            var $reviewpage = AJS.$('#reviewpage');
            AJS.$('#reviewpage').removeClass("hide-comments").removeClass("show-inline-comments").removeClass("show-above-comments");

            if (view === 'none') {
                $reviewpage.addClass("hide-comments");
                AJS.$("#set-hidden-comments").addClass("selected");
            } else if (view === 'inline') {
                $reviewpage.addClass("show-inline-comments");
                AJS.$("#set-inline-comments").addClass("selected");
                if ($reviewpage.hasClass("hide-source")) {
                    AJS.$("#show_source_button").children("a").click();
                }
            } else {
                $reviewpage.addClass("show-above-comments");
                AJS.$("#set-above-comments").addClass("selected");
            }

        },

        convertRangeToLines: convertRangeToLines,

        /**
         * Returns a list of row objects which are to commented
         * TODO: optimise by not iterating over _every_ line of the frx, and by checking ids
         */
        getCommentedLines : function (frxId, fromLineRange, toLineRange){
            var commentedLines = [];

            if (!g_modeSelecting && (fromLineRange || toLineRange ) ) {
                highlightedLines = [];

                var $rows = AJS.$('#sourceTable'+frxId).find("tr.sourceLine");

                var toLines = convertRangeToLines(toLineRange);
                var fromLines = convertRangeToLines(fromLineRange);
                var toLinesLen = toLines.length;
                var fromLinesLen = fromLines.length;
                var toLine = 0;
                var fromLine = 0;

                $rows.each( function() {
                    var a = 0;
                    var lineId = this.id;
                    if( lineId ) {
                        var num = lineId.split("_line")[1];
                        if (num.charAt(0) === 'B') {
                            var lines = num.split(",");
                            fromLine = lines[0].replace(/^Both/, "");
                            toLine = lines[1];
                            var pushed = false;
                            for (a=0; a < toLinesLen; a++){
                                if (toLine == toLines[a]) {
                                    commentedLines.push(this);
                                    pushed = true;
                                    break;
                                }
                            }
                            for (a=0; !pushed && a < fromLinesLen; a++){
                                if (fromLine == fromLines[a]) {
                                    commentedLines.push(this);
                                    break;
                                }
                            }
                        } else if (num.charAt(0) === 'F') {
                            fromLine = num.replace(/^From/, "");
                            for (a=0 ; a < fromLinesLen; a++){
                                if (fromLine == fromLines[a]) {
                                    commentedLines.push(this);
                                    break;
                                }
                            }
                        } else {
                            toLine = num;
                            for (a=0 ; a < toLinesLen; a++){
                                if (toLine == toLines[a]) {
                                    commentedLines.push(this);
                                    break;
                                }
                            }
                        }
                    }
                } );
            }
            return commentedLines;
        },


        /**
         * set comment widths to the browser window width instead of the width of the overflowing div
         * it is contained in
         * @param commentId (optional) the id of the individual comment you wish to set the width of as used in ajax updated comments
         * @param forceUpdate (optional) force the comment widths to be set even if the width hasn't changed as used in ajax updates of frx's
         */
        setCommentWidths : function (commentId, forceUpdate) {
            var $commentToUpdate;
            if( commentId ) {
                $commentToUpdate = AJS.$("#"+commentId);
            }
            var commentIndent = 27;
            var replyIndent = 57;
            if (review.frxs()) {
                var currentWidth = calculateFrxPaneWidth();
                if ($commentToUpdate) {
                    $commentToUpdate.width(frxPaneWidth - commentIndent);
                } else if (forceUpdate || frxPaneWidth != currentWidth || !res.g_pageCompletelyLoaded) {
                    frxPaneWidth = currentWidth;
                    AJS.$.each(review.inlineComments(), function (i, comment) {
                        AJS.$('#' + comment.domId()).css("max-width",(frxPaneWidth - (comment.isReply() ? replyIndent : commentIndent)));
                    });
                    if (rcFormWrangler.isDisplaying() && /^inline/.test(rcFormWrangler.getParent().id)) {
                        rcFormWrangler.setWidth(getInlineCommentFormWidth());
                    }
                }
            }
            return true;
        },

        updateFRXCommentCount : function (resp) {
            if (resp.worked) {
                var frxId = resp.frxId;
                var frxCommentCountDelta = resp.frxCommentCountDelta;
                var frxUnreadCommentCountDelta = resp.frxUnreadCommentCountDelta;
                updateFRXCommentCountImpl(frxId, frxCommentCountDelta, true );
                res.updateFRXUnreadCommentCount(frxId, frxUnreadCommentCountDelta, true );
                if (resp.comment) {
                    var comment = review.comment(resp.comment.id);
                    updateRevisionSliderCounts(comment, frxCommentCountDelta, frxUnreadCommentCountDelta);
                }
            }
        },

        updateFrxRevisionUnreadCommentCount: function (comment, unreadDelta) {
            updateRevisionSliderCounts(comment, 0, unreadDelta);
        },

        selectLine_down : function ($row, dontActuallySelectThisRow) {
            var $parent = $row.parents(":first");
            var parent = $parent[0];
            g_mousedown = true;
            if (g_currentTable !== parent) {
                if (g_currentTable) {
                    clearSelectedRows();
                }
                g_currentTable = parent;
            }
            g_modeSelecting = !$row.hasClass("lineHighlighted");
            var row = $row.get(0);
            g_mouseDownFirstRow = row;
            if (!dontActuallySelectThisRow) {
                selectTR(row, g_modeSelecting );
                return false;
            } else {
                return true;
            }
        },

        selectLine_over : function ($obj){
            if (g_mouseDownFirstRow != null) {
                selectTR(g_mouseDownFirstRow, g_modeSelecting);
            }
            if (g_mousedown) {
                selectTR($obj.get(0), g_modeSelecting);
                return false;
            }
            return true;
        },

        selectLine_up : function ($tableElement) {
            checkInlineCommentBox($tableElement, true);
            g_mouseDownFirstRow = null;
            return false;
        },

        commentFromInnerElement : function (button) {
            var id = AJS.$(button).closest(".comment-container")
                        .attr("id")
                        .replace(/^\D*/,"");
            return review.comment(id);
        },

        syncCommentButtons : function () {
            // Sync the comment nav buttons
            var hasUnread = review.unreadComments().length > 0;
            AJS.$("#mark-comments-read-button").toggleClass("disabled", !hasUnread);
            // If we have unread comments, then we obviously have comments...
            var $toolbars = AJS.$("#frxs").find("div.toolbar");
            if (hasUnread) {
                $toolbars.find(".prev-comment-button, .next-comment-button").removeClass("disabled");
            } else {
                // otherwise check if we actually have comments
                var hasComments = review.comments().length > 0;
                $toolbars.find(".prev-comment-button, .next-comment-button").toggleClass("disabled", !hasComments);
            }
        },

        getDisplayingCommentForm : function () {
            var forms = res.getDisplayingForms();
            if (forms.length > 0) {
                return forms[0];
            } else {
                return null;
            }
        },
        
        getDisplayingForms : function () {
            var forms = [];
            if (scFormWrangler.isDisplaying()) {
                forms.push(scFormWrangler.getCommentForm());
            }
            if (fcFormWrangler.isDisplaying()) {
                forms.push(fcFormWrangler.getCommentForm());
            }
            if (rcFormWrangler.isDisplaying()) {
                forms.push(rcFormWrangler.getCommentForm());
            }
            if (replyFormWrangler.isDisplaying()) {
                forms.push(replyFormWrangler.getCommentForm());
            }
            return forms;
        },

        getDisplayingReplyForm: function() {
            if (replyFormWrangler.isDisplaying()) {
                return replyFormWrangler.getCommentForm();
            } else {
                return null;
            }
        }
    };

    AJS.$(document).ready( function() {
        AJS.$("body.crucible").mouseup(function() {
            g_mousedown = false;
        });

        setTimeout(autosaveTimer, 5000); // 5s after page load

        res.syncCommentButtons();
    });

    return res;
})();

AJS.$(document).ready(function () {
    if (!AJS.CRU.disableScrollTracking) {
        AJS.CRU.COMMENT.commentScrollTracker = AJS.CRU.WIDGETS.makeScrollTracker({
            threshold: 70,  // height of frx header
            container: AJS.$("#frx-pane")[0],
            selector: function () {
                var ids = [];
                var comments, comment, domId, i, len;
                var frxs = review.frxs();
                var $body = AJS.$("body");
                var isMultiFrxView = $body.hasClass("multi-frx-view");
                var hideSourceComments = $body.hasClass("hide-comments");
                var aboveComments = $body.hasClass("show-above-comments");

                // Go over general comments when general comments are visible
                // Since general comments are always at the top, we should use this exclusively if we can
                if (AJS.$("#generalComments").hasClass("activeFrx")) {
                    comments = review.generalComments();
                    for (i = 0, len = comments.length; i < len; i++) {
                        comment = comments[i];
                        domId = comment.contentDomId();
                        ids.push("#" + domId);
                    }
                }
                // Go over frx comments when the frx is visible
                else {
                    for (i = 0, len = frxs.length; i < len; i++) {
                        var frx = frxs[i];
                        if (!frx.isLoaded() || frx.isFiltered()) {
                            continue;
                        }

                        if (isMultiFrxView && !frx.isExpanded()) {
                            continue;
                        }

                        var $frxElement = AJS.$("#frxouter"+frx.id());
                        if (!$frxElement.hasClass("activeFrx")) {
                            continue;
                        }
                        comments = hideSourceComments ? frx.fileComments() : frx.comments();
                        for (var j = 0, clen = comments.length; j < clen; j++) {
                            comment = comments[j];
                            domId = comment.contentDomId();
                            if (comment.isInline() && aboveComments) {
                                ids.push("#" + domId.replace('inline', 'above'));
                            } else {
                                ids.push("#" + domId);
                            }
                        }
                    }
                }
                return AJS.$(ids.length > 0 ? ids.join(",") : []);
            },
            active: function () {
                AJS.CRU.COMMENT.NAV.setCurrentComment(this.parentNode);
            }
        });
    }
});

if (!commentator) {
    var commentator = AJS.CRU.COMMENT;
    commentator.commentAjaxExecutor = AJS.FECRU.AJAX.createSequentialExecutor();
}
;
/* END /2static/script/cru/review/comment/commentator.js */
/* START /2static/script/cru/review/comment/comment-nav.js */
AJS.CRU.COMMENT.NAV = (function() {

    var currentCommentId = null;

    function asCommentId(comment) {
        if (comment instanceof AJS.$) {
            comment = comment[0];
        }

        if (!comment) {
            return null;
        } else if (comment.nodeType) {
            return comment.id.replace(/(general|revision|inline|above)(comment|reply)(Content)?/, '');
        } else if (comment instanceof Comment) {
            return comment.id();
        } else if (typeof comment === 'string') {
            return comment;
        }
        return null;
    }

    var getCommentDomElements = function(opts, frxId) {
        var inlineComment = /^inlinecomment/;
        var inlineReply = /^inlinereply/;
        if (commentator.showCommentsAbove()) {
            inlineComment = /^abovecomment/;
            inlineReply = /^abovereply/;
        }

        // cache opts values into local variables for faster lookups
        var nextThread = opts.nextThread;

        var comments = frxId ?
                        (
                            commentator.hideSourceComments() ?
                                review.frx(frxId).fileComments() :
                                review.frx(frxId).comments()
                        ) :
                        review.generalComments();
        var commentDomIds = []; // List of comment ids to traverse over
        AJS.$.each(comments, function() {
            if (this.frx() && this.frx().isFiltered()) {
                return true;
            }
            var commentDomId = this.domId();
            if (commentator.showCommentsAbove() && /^inline/.test(this.type())) { // if it's an inline comment and they're shown above, change the id
                commentDomId = commentDomId.replace('inline','above');
            }

            if (nextThread) {
                if (inlineComment.test(commentDomId) ||
                    /^revisioncomment[0-9]/.test(commentDomId) ||
                    /^generalcomment[0-9]/.test(commentDomId)
                    ) {
                    commentDomIds.push("#"+commentDomId);
                }
                // we need to make sure the current reply is also added to the comments
                // so we can find the next comment thread from the reply.
                else if(opts.commentId && opts.commentId == this.id()) {
                    commentDomIds.push("#"+commentDomId);
                }
            }
            else if (inlineComment.test(commentDomId) ||
                inlineReply.test(commentDomId) ||
                /^revisioncomment[0-9]/.test(commentDomId) ||
                /^generalcomment[0-9]/.test(commentDomId) ||
                /^generalreply[0-9]/.test(commentDomId) ||
                /^revisionreply[0-9]/.test(commentDomId)
                ) {
                commentDomIds.push("#"+commentDomId);
            }
        } );
        return AJS.$(commentDomIds.length > 0 ? commentDomIds.join(",") : []);
    };

    var getScrollToCommentElement = function(opts, frxId) {
        var nextFrx = review.frx(frxId);
        var $commentDomElems = getCommentDomElements(opts, frxId);
        var sourceCommentDomId = res.getCurrentCommentDomId();
        do {
            var destinationComment = null;

            // cache opts values into local variables for faster lookups
            var destination = opts.destination;
            var skipReadComments = opts.skipReadComments;
            var skipNonDefects = opts.skipNonDefects;

            if (destination === 'first' && !skipReadComments && !skipNonDefects) {
                destinationComment = $commentDomElems[0];
            }
            else if(destination === 'last' && !skipReadComments&& !skipNonDefects) {
                destinationComment = $commentDomElems[$commentDomElems.length-1];
            }
            else {
                // Find the source comment id in the list of comments
                var index = -1;
                var len = $commentDomElems.length;

                if (destination === 'last' || destination === 'previous' && !sourceCommentDomId) {
                    index = len;
                } else if (destination === 'first' || destination === 'next' && !sourceCommentDomId) {
                    index = -1;
                } else if (destination !== 'first') {
                    for( var j= 0; j < len; j++ ) {
                        if( sourceCommentDomId == $commentDomElems[j].id ) {
                            index = j;
                            break;
                        }
                    }
                }

                // If we're iterating over unread comments, we want to keep jumping comments
                // when the comment is marked as 'read'.
                do {
                    destinationComment = null;
                    if (destination == 'previous' || destination == 'last') {
                        if ($commentDomElems[index-1]) {
                            destinationComment = $commentDomElems[index-1];
                            index--;
                        } else {
                            break;
                        }
                    } else if (destination == 'next' || destination == 'first') {
                        if ($commentDomElems[index+1]) {
                            destinationComment = $commentDomElems[index+1];
                            index++;
                        } else {
                            break;
                        }
                    } else {
                        destinationComment = $commentDomElems[index];
                        break;
                    }
                    if (destinationComment && destinationComment.id) {
                        var commentId = res.commentIdFromDomId(destinationComment.id);
                        var commentStatus = review.comment(commentId).status();
                        var isDefect = review.comment(commentId).defect();
                    } else {
                        break;
                    }
                } while( skipReadComments && !(commentStatus == 'unread' || commentStatus == 'leaveUnread') ||
                         skipNonDefects   && !isDefect);
            }

            if (!destinationComment || !destinationComment.id) {
                sourceCommentDomId = null;
                if (opts.destination === 'last' || opts.destination === 'previous') {
                    if (!nextFrx) {
                        $commentDomElems = null;
                    } else if (nextFrx.getPrevFrx()) {
                        nextFrx = nextFrx.getPrevFrx();
                        $commentDomElems = getCommentDomElements(opts, nextFrx.id());
                    } else {
                        nextFrx = null;
                        $commentDomElems = getCommentDomElements(opts);
                    }
                } else {
                    if (!nextFrx && review.frxs().length > 0) {
                        nextFrx = review.frxs()[0];
                        $commentDomElems = getCommentDomElements(opts, nextFrx.id());
                    } else if (nextFrx && nextFrx.getNextFrx()) {
                        nextFrx = nextFrx.getNextFrx();
                        $commentDomElems = getCommentDomElements(opts, nextFrx.id());
                    } else {
                        nextFrx = null;
                        $commentDomElems = null;
                    }
                }
            }

        } while ((!destinationComment || !destinationComment.id) && $commentDomElems);

        if (!destinationComment || !destinationComment.id) {
            return null;
        }

        return destinationComment;
    };

    var scrollDirectlyToCommentImpl = function(destinationCommentId) {
        // ensure destination comment is visible
        var destId = destinationCommentId.replace(/[0-9]+/,'');
        var scrollToCommentId = destinationCommentId.replace(destId,'');
        var destinationComment;
        var $destinationComment;
        if (destId === 'unknownfrx' || destId === 'unknownfrxreply') {
            $destinationComment = AJS.$('#' + review.comment(scrollToCommentId).domId());
            destinationComment = $destinationComment[0];
        } else if (!AJS.CRU.UI.isSingleFrxView() && destId != 'generalcomment' && destId != 'generalreply') {
            $destinationComment = AJS.$('#' + destinationCommentId);
            destinationComment = $destinationComment[0];
            var $innerFrx = AJS.$(destinationComment).parents(".frxinner:first");
            // remove the leading 'frxinner' from the id
            AJS.CRU.FRX.expandFrx($innerFrx.attr('id').substring(8));
        } else {
            $destinationComment = AJS.$('#' + destinationCommentId);
            destinationComment = $destinationComment[0];
        }

        //expand comment / comment's parent if it is collapsed, up the tree. Then do the scroll as callback
        AJS.CRU.COMMENT.THREAD.expandCommentThread($destinationComment.children("div.comment"), function() {
            var cruFrxNav = AJS.CRU.FRX.NAV;
            // scroll to the comment's frx
            var frx;
            if (scrollToCommentId && (frx = review.comment(scrollToCommentId).frx())) {
                cruFrxNav.setCurrentFrx(frx.id());
            } else {
                cruFrxNav.setCurrentFrx('generalComments');
            }
            res.scrollToComment(destinationComment);
        });
    };

    var res = {
        scrollDirectlyToComment : function(destinationCommentId) {
            if (!destinationCommentId) {
                return;
            }
            // ensure destination comment is visible
            var destId = destinationCommentId.replace(/[0-9]+/,'');
            var scrollToCommentId = destinationCommentId.replace(destId,'');

            var comment = review.comment(scrollToCommentId);
            if (comment && comment.isHidden()) {
                var frx = comment.frx();
                var loadOpts;
                loadOpts = {};
                loadOpts.fromRev = comment.fromRevId();
                loadOpts.toRev = frx.visibleToRevision();
                loadOpts.isForcedReload = true;
                loadOpts.diffMode = AJS.CRU.FRX.diffParamsMap(comment.frx().id(), {});

                AJS.CRU.FRX.AJAX.prioritisedFrxLoad(frx.id(), function() {scrollDirectlyToCommentImpl(destinationCommentId);}, loadOpts);
            } else {
                scrollDirectlyToCommentImpl(destinationCommentId);
            }
        },

        getCurrentCommentDomId: function () {
            if (currentCommentId) {
                var comment = review.comment(currentCommentId);
                if (comment.isInline() && commentator.showCommentsAbove()) {
                    return comment.domId().replace('inline', 'above');
                } else {
                    return comment.domId();
                }
            } else {
                return null;
            }
        },

        getCurrentCommentContentDomId: function () {
            if (currentCommentId) {
                var comment = review.comment(currentCommentId);
                // The current comment can be deleted
                if (!comment) {
                    return null;
                } else if (comment.isInline() && commentator.showCommentsAbove()) {
                    return comment.contentDomId().replace('inline', 'above');
                } else {
                    return comment.contentDomId();
                }
            } else {
                return null;
            }
        },

        setCurrentComment : function (comment, options) {
            options = AJS.$.extend({ sticky: false }, options);
            var scrollTracker = AJS.CRU.COMMENT.commentScrollTracker;
            var commentId = asCommentId(comment);
            if (currentCommentId !== commentId) {
                // Lookup the previous current comment in case it was deleted.
                if (review.comment(currentCommentId)) {
                    AJS.$('#' + res.getCurrentCommentContentDomId()).removeClass("current_comment");
                }
                currentCommentId = commentId;
                if (commentId) {
                    var $commentContent = AJS.$('#' + res.getCurrentCommentContentDomId())
                        .addClass("current_comment");
                    commentator.markCommentRead(commentId);
                    if (scrollTracker) {
                        scrollTracker.setCurrentElement($commentContent[0]);
                        if (options.sticky) {
                            scrollTracker.makeCurrentElementSticky();
                        }
                    }
                }
            }
        },

        visibleCommentsChanged: function () {
            var scrollTracker = AJS.CRU.COMMENT.commentScrollTracker;
            if (scrollTracker) {
                scrollTracker.rescan();
            } else {
                var frxId = AJS.CRU.FRX.NAV.getCurrentFrxId();
                var $commentElems = getCommentDomElements({}, frxId === 'generalComments' ? null : frxId);
                if ($commentElems.length > 0) {
                    // Lookup the previous current comment in case it was deleted.
                    if (review.comment(currentCommentId)) {
                        var $currentComment = AJS.$('#' + res.getCurrentCommentContentDomId());
                        if (AJS.$.inArray($currentComment[0], $commentElems.get()) < 0) {
                            res.setCurrentComment($commentElems[0]);
                        }
                    } else {
                        res.setCurrentComment($commentElems[0]);
                    }
                } else {
                    res.setCurrentComment(null);
                }
            }
        },

        scrollToComment: function (comment) {
            var commentId = asCommentId(comment);

            var scrollTracker = AJS.CRU.COMMENT.commentScrollTracker;
            scrollTracker && scrollTracker.ignoreNextScroll();
            var $commentContent = AJS.$('#' + review.comment(commentId).contentDomId());
            AJS.$('#frx-pane').scrollTo($commentContent[0], {
                axis: 'y',
                offset: -100    // scroll past the sticky FRX header
            });
            res.setCurrentComment(commentId);
        },

        checkCommentAnchor : function () {
            if (/^#c/.test(window.location.hash)) {
                var commentId = res.commentIdFromAnchor( window.location.hash.replace(/^#/,'') );
                var comment = review.comment(commentId);
                var frxId = review.getCommentFrxId(commentId);

                var isInlineButNotLoaded = !comment && frxId && !review.frx(frxId).isLoaded();
                if (comment || isInlineButNotLoaded) {
                    var scrollToMap = res.navigateFindComment({ commentId: commentId });
                    res.navigateDirectlyToComment({ commentId: commentId }, scrollToMap);
                    return scrollToMap.frxId;
                } else {
                    window.location.hash = '';
                    AJS.CRU.FRX.NAV.setCurrentFrx('generalComments');
                }
            }
            return false;
        },

        nextComment : function ( current ) {
            current = current || res.getCurrentCommentDomId();
            var opts = { destination: 'next' };
            res.navigateToComment(opts);
        },

        prevComment : function ( current ) {
            current = current || res.getCurrentCommentDomId();
            var opts = { destination: 'previous' };
            res.navigateToComment(opts);
        },

        nextCommentThread : function ( current ) {
            current = current || res.getCurrentCommentDomId();
            var opts = { destination: 'next', nextThread: true };
            res.navigateToComment(opts);
        },

        previousCommentThread : function (current) {
            current = current || res.getCurrentCommentDomId();
            var opts = { destination: 'previous', nextThread: true };
            res.navigateToComment(opts);
        },

        /**
         * @param {String} opts.destination location to scroll to - may be <tt>first</tt>, <tt>last</tt>, <tt>previous</tt> or <tt>next</tt>.
         * @param {String} opts.commentId the id of the comment to scroll to. This value overrides the <tt>opts.destination</tt> is <tt>first</tt> and <tt>last</tt>.
         * @param {Boolean} opts.skipReadComments if true, comments which have been marked as read will be passed over and ignored.
         * @param {Boolean} opts.nextThread if true, comment replies will be passed over and ignored.
         * @return the frxId of the frx of the comment if it has one
         */

        navigateToComment : function(opts) {
            var $elementWithSpinner;
            if (opts.destination === 'first' || opts.destination === 'next') {
                $elementWithSpinner = AJS.$('#next-element-link');
            } else {
                $elementWithSpinner = AJS.$('#prev-element-link');
            }
            res.navigateDirectlyToComment(opts, res.navigateFindComment(opts), $elementWithSpinner);
        },

        navigateDirectlyToComment : function(opts, scrollToMap, $elementWithSpinner) {
            if (!scrollToMap.frxId) {
                res.scrollDirectlyToComment(scrollToMap.scrollToDomId);
            } else {
                if (scrollToMap.frxId) {
                    var frx = review.frx(scrollToMap.frxId);
                    if (frx.isLoaded()) {
                        if (scrollToMap.scrollToDomId) {
                            res.scrollDirectlyToComment(scrollToMap.scrollToDomId);
                        } else {
                            var destinationComment = getScrollToCommentElement(opts, scrollToMap.frxId);
                            if (!destinationComment || !destinationComment.id) {
                                AJS.log('no comment found in frx ' + scrollToMap.frxId + ' with ' + opts);
                                return;
                            }
                            res.scrollDirectlyToComment(destinationComment.id);
                        }
                    } else {
                        //load frx here
                        if ($elementWithSpinner) {
                            $elementWithSpinner.find('span').addClass('spinner');
                        }
                        var onDone = function () {
                            if (scrollToMap.scrollToDomId) {
                                res.scrollDirectlyToComment(scrollToMap.scrollToDomId);
                            } else {
                                var destinationComment = getScrollToCommentElement(opts, scrollToMap.frxId);
                                if (!destinationComment || !destinationComment.id) {
                                    AJS.log('no comment found in frx ' + scrollToMap.frxId + ' with ' + opts);
                                    return;
                                }
                                res.scrollDirectlyToComment(destinationComment.id);
                            }
                            if ($elementWithSpinner) {
                                $elementWithSpinner.find('span').removeClass('spinner');
                            }
                        };
                        AJS.CRU.FRX.AJAX.prioritisedFrxLoad(frx.id(), onDone);
                    }
                }
            }
        },


        navigateFindComment : function(opts) {
            if (opts.commentId) {
                var comment = review.comment(opts.commentId);
                return {
                    scrollToDomId: comment.domId(),
                    frxId: comment.frx() ? comment.frx().id() : null
                };
            }
            var currentComment = res.getCurrentCommentDomId() ? review.comment(res.getCurrentCommentDomId().replace( /^\D*/, "" )) : null;
            if (!currentComment) {
                if (opts.destination === 'next') {
                    opts.destination = 'first';
                }
                if (opts.destination === 'previous') {
                    opts.destination = 'last';
                }
            }

            var currentElements;
            var scrollToFrxId = null;
            var scrollToDomId = null;
            var destinationComment;
            if (opts.destination === 'first') {
                scrollToDomId = AJS.$('#general-comments-container > .comment-container:first').attr('id');
            } else if (opts.destination !== 'last') {
                scrollToFrxId = currentComment.frx() ? currentComment.frx().id() : null;
                destinationComment = getScrollToCommentElement(opts, currentComment.frx() ? currentComment.frx().id() : null);
                scrollToDomId = destinationComment ? destinationComment.id : null;
            }
            if (scrollToDomId) {
                var scrollToComment = review.comment(scrollToDomId.replace( /^\D*/, "" ));
                return {
                    scrollToDomId: scrollToDomId,
                    frxId: scrollToComment.frx() ? scrollToComment.frx().id() : null
                };
            }

            var forwards = opts.destination === 'next' || opts.destination === 'first';
            var frxs = review.frxs().slice();
            if (!forwards) {
                frxs = frxs.reverse();
            }
            var currentFrxId = AJS.CRU.FRX.NAV.getCurrentFrxId();
            var i, len = frxs.length;
            if (opts.destination === 'first' ||
                opts.destination === 'last' ||
                currentFrxId === 'generalComments') {
                i = -1;
            } else {
                for (i = 0, len; i < len; i++) {
                    if (currentFrxId === frxs[i].id()) {
                        break;
                    }
                }
            }
            for (i = i + 1; i < len; i++) {
                var frx = frxs[i];
                if (opts.skipNonDefects && !frx.hasDefects()) {
                    continue;
                }
                if (opts.skipReadComments && !frx.hasUnreadComments()) {
                    continue;
                }
                if (frx.hasComments()) {
                    return {
                        frxId: frx.id(),
                        forwards: forwards
                    };
                }
            }
            if (!scrollToDomId) {
                if (!forwards) {
                    //have to search general comments
                    destinationComment = getScrollToCommentElement(opts, null);
                    scrollToDomId = destinationComment ? destinationComment.id : null;
                    return {
                        scrollToDomId: scrollToDomId
                    };
                }
            }
            //didnt find a next
            return {};

        },

        // TODO Deprecate, everything should be using the Comment Object
        commentIdFromAnchor : function ( commentId ) {
            return commentId.replace( /^\D*/, "" );
        },

        commentIdFromDomId: function (commentDomId) {
            return commentDomId.replace(/^\D*(\d+)$/, '$1');
        }
    };

    return res;
})();
;
/* END /2static/script/cru/review/comment/comment-nav.js */
/* START /2static/script/cru/review/comment/comment-tetris.js */
//TODO Arguments should be review-model objects, not ids.
var tetrisCommentController = (function() {

    var createAndShowHoverComment = function ($cell, cellId) {
        if (!cellId) {
            AJS.log('couldnt render tetris comment - couldnt find cell');
            return;
        }
        var cellIdCleaned = cellId.replace(',', '\\,');
        var $hovercommentWrapper = AJS.$('#hovercommentWrapper' + cellIdCleaned);
        if ($hovercommentWrapper.length === 0) {
            var onMouseover = function() {
                openDropDown('hovercommentWrapper' + cellId);
            };
            var onMouseout = function() {
                closeDropDown();
            };
            $hovercommentWrapper = AJS.$('<div></div>').addClass('hovercommentWrapper').hide()
                    .attr('id', 'hovercommentWrapper' + cellId)
                    .hover(onMouseover, onMouseout);
        }

        //only show the hover for comments where this is its first line
        var cellCommentArray = commentator.tetrisCellVisibleFirstCount[cellId];
        if (!cellCommentArray) {
            cellCommentArray = commentator.tetrisCellInvisibleCommentCount[cellId];
        }

        AJS.$.each(cellCommentArray, function() {
            //copy the above comment and make it a hover comment
            var $comment = AJS.$('#hovercomment' + this);
            if ($comment.length === 0) {
                $comment = AJS.$('#abovecomment' + this).clone().attr('id', 'hovercomment' + this);
                //remove comment buttons (edit, delete, etc)
                $comment.find('td.commentButton').replaceWith('<td>&nbsp;</td>');
                $comment.find('td.commentIssueKeyUnlinked').replaceWith('<td>&nbsp;</td>');
                //remove replyFormDivs
                $comment.find('#abovereplyFormDiv' + this).remove();
            }
            var $replyContainers = $comment.children(".reply-container").children(".comment-container");
            for (var i = 0, len = $replyContainers.length; i < len; i++) {
                var replyContainer = AJS.$($replyContainers[i]);
                var countReplies = replyContainer.find(".reply-container .comment").length;
                if (countReplies > 0) {
                    var excerpt = replyContainer.children('.comment').find(".excerpt");
                    var replyCountContainer = excerpt.find(".reply-count");
                    replyCountContainer.text(" ... " + countReplies + " " + (countReplies == 1 ? "reply" : "replies") + " hidden");
                }
            }

            $hovercommentWrapper.append($comment);
            $comment.find('.comment').addClass('hover-comment');
        });

        $cell.append($hovercommentWrapper);
        openDropDown('hovercommentWrapper' + cellIdCleaned);
    };

    var putValueInSet = function (array, element) {
        if (!array) {
            array = [];
        }
        if (AJS.$.inArray(element, array) == -1) {
            array.push(element);
        }
        return array;
    };

    var removeValueFromSet = function(array, element) {
        if (array) {
            var index = AJS.$.inArray(element, array);
            if (index >= 0) {
                array.splice(index, 1);
            }
        }
        return array;
    };

    var getAllCommentLines = function (comment, frxId) {
        var fromLines = [];
        var toLines = [];
        if (comment.fromRevId()) {
            var fromLines = commentator.getCommentedLines(frxId, comment.fromLineRange(), "");
        }
        if (comment.toRevId()) {
            var toLines = commentator.getCommentedLines(frxId, "", comment.toLineRange());
        }
        return fromLines.concat(toLines);
    };

    var cleanupEmptyTetrisCell = function(array, cellId, className, comment) {
        if (!cellId) {
            AJS.log('couldnt render tetris comment - couldnt find cell');
            return;
        }
        var cellIdCleaned = cellId.replace(',', '\\,');
        //if there are no more comments in this cell
        if (array.length === 0) {
            var $cell = AJS.$("#" + cellIdCleaned + " .tetrisColumn");
            $cell.removeClass(className).removeClass(comment.commentColor()).unbind();
        }
        AJS.$('#hovercommentWrapper' + cellIdCleaned).remove();
        if (className == 'tetrisCommentHidden') {
            AJS.$('#' + cellIdCleaned).find('span').remove();
        }
    };

    var res = {
        /**
         * Renders the comment markers for all comments in the specified FRX.
         */
        renderTetrisCommentMarkersForFrx : function (frxId) {
            AJS.$.each(review.inlineComments(), function() {
                if (this.frx().id() == frxId) {
                    res.renderTetrisCommentMarkersForComment(this.id());
                }
            });
        },

        /**
         * given the comment id, return a jquery object of the first row that the comment is on.
         * @param commentId
         */
        getFirstCommentedLine : function (commentId) {
            var comment = review.comment(commentId);
            if (!comment) {
                return;
            }
            var frxId = comment.frx().id();
            var toLineNum = comment.toLineRange().split(',')[0].split('-')[0];
            var fromLineNum = comment.fromLineRange().split(',')[0].split('-')[0];
            var $srcTable = AJS.$('#sourceTable' + frxId);
            var $toRow = $srcTable.find('.to' + toLineNum);
            var $fromRow = $srcTable.find('.from' + fromLineNum);
            var $firstLine;
            if ($fromRow.length === 0) { //no from, use to
                $firstLine = $toRow;
            } else if ($toRow.length === 0) {//no to, use from
                $firstLine = $fromRow;
            } else if ($toRow.prev($fromRow).length !== 0) {// use the eariler one
                $firstLine = $fromRow;
            } else {
                $firstLine = $toRow;
            }
            return $firstLine;
        },

        /**
         * Renders the comment markers for the given comment.
         * TODO: commentator.getCommentedLines method runs in O(n) where n == frx line count. We _MUST_ improve this.
         */
        renderTetrisCommentMarkersForComment : function (commentId) {
            var comment = review.comment(commentId);
            if (!comment) {
                return;
            }
            var frxId = comment.frx().id();
            var fromLines = [];
            var toLines = [];
            var allLines;
            var className = "tetrisCommentVisible";
            var cellCounter = commentator.tetrisCellVisibleCommentCount;
            var firstCellCounter = commentator.tetrisCellVisibleFirstCount;
            var $firstLine;
            if (comment.isHidden()) {
                toLines = commentator.getCommentedLines(frxId, "", comment.gutterLine());
                if (toLines.length === 0) {
                    toLines.push(AJS.$('#' + frxId + '_last_line').attr('id', frxId + '_last_line')[0]);
                }
                className = "tetrisCommentHidden";
                cellCounter = commentator.tetrisCellInvisibleCommentCount;
                allLines = fromLines.concat(toLines);
            } else {
                //allLines = getAllCommentLines(comment, frxId);
                $firstLine = res.getFirstCommentedLine(commentId);
                $firstLine.find('.tetrisColumn').addClass('firstTetrisBlock');
                allLines = $firstLine;
                var firstCellId = $firstLine.attr('id');
                firstCellCounter[firstCellId] = putValueInSet(firstCellCounter[firstCellId], commentId);
            }
            var updateCellCounters = function(lines) {
                AJS.$(lines).each(function() {
                    var cellId = this.id;
                    if (!cellId) {
                        AJS.log('couldnt render tetris comment - couldnt find cell');
                        return;
                    }
                    var cellIdCleaned = cellId.replace(',', '\\,');
                    cellCounter[cellId] = putValueInSet(cellCounter[cellId], commentId);
                    var $cell = AJS.$("#" + cellIdCleaned + " .tetrisColumn");
                    $cell.addClass(className).unbind();
                    //if it is a 'first' cell or it is a hidden comment, show hover
                    if ($cell.hasClass('firstTetrisBlock') || className == 'tetrisCommentHidden') {
                        var onMouseover = function() {
                            createAndShowHoverComment($cell, cellId);
                        };
                        var onMouseout = function() {
                            closeDropDown();
                        };
                        $cell.hover(onMouseover, onMouseout);
                        if (className == 'tetrisCommentHidden') {
                            if ($cell.find('span').length != 1) {
                                AJS.$('<span>').html('&nbsp;').addClass(className).appendTo($cell);
                            }
                        } else {
                            $cell.addClass(comment.commentColor());
                        }
                    }
                });
            }
            updateCellCounters(allLines);
        },

        deleteTetrisCommentMarkers : function (commentId) {
            var comment = review.comment(commentId);
            if (!comment) {
                return;
            }
            var frxId = comment.frx().id();
            var fromLines = [];
            var toLines = [];
            var allLines;
            var className = "tetrisCommentVisible";
            var cellCounter = commentator.tetrisCellVisibleCommentCount;
            var firstCellCounter = commentator.tetrisCellVisibleFirstCount;
            if (comment.isHidden()) {
                toLines = commentator.getCommentedLines(frxId, "", comment.gutterLine());
                if (toLines.length === 0) {
                    toLines.push(AJS.$('#' + frxId + '_last_line').attr('id', frxId + '_last_line')[0]);
                }
                className = "tetrisCommentHidden";
                cellCounter = commentator.tetrisCellInvisibleCommentCount;
                allLines = fromLines.concat(toLines);
            } else {
                //allLines = getAllCommentLines(comment, frxId);

                var $firstLine = res.getFirstCommentedLine(commentId);
                var firstCellId = $firstLine.attr('id');
                allLines = $firstLine;
                if (firstCellCounter[firstCellId]) {
                    firstCellCounter[firstCellId] = removeValueFromSet(firstCellCounter[firstCellId], commentId);
                    cleanupEmptyTetrisCell(firstCellCounter[firstCellId], firstCellId, className, comment);
                }
            }
            var updateCellCounters = function (lines) {
                AJS.$.each(lines, function() {
                    var cellId = this.id;
                    if (!cellId) {
                        AJS.log('couldnt render tetris comment - couldnt find cell');
                        return;
                    }
                    cellCounter[cellId] = removeValueFromSet(cellCounter[cellId], commentId);
                    cleanupEmptyTetrisCell(cellCounter[cellId], cellId, className, comment);
                });
            }
            updateCellCounters(allLines);
        }
    };
    return res;
})();;
/* END /2static/script/cru/review/comment/comment-tetris.js */
/* START /2static/script/cru/review/comment/comment-thread.js */
(function() {
    var delay = "normal";
    var type = "swing";
    AJS.CRU = AJS.CRU || {};
    AJS.CRU.COMMENT = AJS.CRU.COMMENT || {};
    AJS.CRU.COMMENT.THREAD = AJS.CRU.COMMENT.THREAD || {
        expandCommentThread: function($comment, afterExpandCallback) {
            //get the chain of collapsed comments, and open each one of them up.
            var collapsed = $comment.parents(".comment-collapsed");
            collapsed.each(function() {
                var comment = AJS.$(this).children(".comment");
                var commentBody = comment.find(".commentBody");
                var logo = comment.find(".userlogo");
                var avatar = logo.find(".user");
                var avatarSmall = logo.find(".smalluserlogo");
                var arrow = logo.find(".commentNav");
                var excerpt = comment.find(".excerpt");
                var commentContainer = comment.parent();
                var replyContainer = commentContainer.children(".reply-container");

                //restore the saved height info
                var commentOriginalHeight = comment.data("commentOriginalHeight");
                var originalReplyHeight = replyContainer.data("replyThreadHeight");
                var avatarSize = avatar.data("avatarSize");
                comment.data("isAnimating", true);
                excerpt.hide();
                commentBody.show();
                commentBody.animate({opacity:1}, delay, type);

                avatarSmall.animate({width:avatarSize + "px", height:avatarSize + "px"}, delay, type, function() {
                    //show the arrow last to avoid a move/flicker
                    avatarSmall.hide();
                    avatar.show();
                    arrow.show();
                });
                replyContainer.show();
                replyContainer.animate({opacity:1, height:originalReplyHeight + "px"}, delay, type, function() {
                    //reset the height to fluid after animating
                    replyContainer.css("height", null);
                });
                comment.animate({opacity:1, height:commentOriginalHeight + "px"}, delay, type, function() {
                    commentContainer.removeClass("comment-collapsed");
                    comment.data("isAnimating", false);
                });
            });
            if (afterExpandCallback) {
                afterExpandCallback();
            }
        }
    };
})();;
/* END /2static/script/cru/review/comment/comment-thread.js */
/* START /2static/script/cru/review/frx/frx.js */
AJS.CRU.FRX = (function() {
    var postPostLoadSummarize;
    var postPostLoadFunc = function() {
        review.setLoaded(true);
        AJS.CRU.REVIEW.UTIL.startPollingForReviewUpdates();
        // This is executed after all diffs are finished.
        if (postPostLoadSummarize) {
            res.toggleSource(review.frxIds(), review.inlineCommentIds());
        }
        commentator.setCommentWidths(null,true);
        commentator.g_pageCompletelyLoaded = true;
    };

    var optimizedFrxList = function (alreadyLoadedFrx) {
        var MAX_FRX_AUTOLOAD = 10; // The number of FRXs to automatically load when the the initial review page has loaded.

        var allFrxs = review.frxs();

        var idsWithComments = [];
        var idsWithoutComments = [];
        var idsThatAreDirs = []; // less likely that we want these really

        for (var i = 0, len = allFrxs.length; i < len; i++) {
            var frx = allFrxs[i];
            var id = frx.id();
            if (id === alreadyLoadedFrx) {
                continue;
            } else if (frx.hasComments()) {
                idsWithComments.push(id);
            } else if (frx.isDirectory()) {
                idsThatAreDirs.push(id);
            } else {
                idsWithoutComments.push(id);
            }
        }

        return idsWithComments
                    .concat(idsWithoutComments)
                    .concat(idsThatAreDirs)
                        .slice(0, MAX_FRX_AUTOLOAD);
    };

    var filterFrxsFromHash = function () {
        if (/^#f-/.test(window.location.hash)) {
            var csv = window.location.hash.replace(/^#f-/, '');
            AJS.CRU.REVIEW.UTIL.filterAndExpandFrxs(csv.split(','));
        }
    };

    var activateFirstUnfilteredFrx = function () {
        var frxToActivate;

        if (!AJS.$('#frx-overview').hasClass('filtered')) {
            frxToActivate = AJS.$('#generalComments')[0];
        } else {
            var nonFilteredFrxs = AJS.$.grep(review.frxs(), function (frx) { return !frx.isFiltered(); });
            if (nonFilteredFrxs.length > 0) {
                frxToActivate = AJS.$('#frxouter' + nonFilteredFrxs[0].id())[0];
            }
        }
        AJS.CRU.FRX.NAV.setCurrentFrxAndScroll(frxToActivate);
    };

    var res = {

        reloadFrxs : function (opts, frxIds) {
            frxIds = frxIds || review.frxIds();
            //reset some global vars to reload the diffs
            for (var i = 0, len = frxIds.length; i < len; i++) {
                review.frx(frxIds[i]).setLoaded(false);
            }
            commentator.g_pageCompletelyLoaded = false;
            var loadOpts = {
                diffMode : opts.diffOpts,
                isForcedReload : true
            };
            AJS.CRU.FRX.AJAX.prioritisedFrxLoad(frxIds, null, loadOpts);
        },

        hideFrx : function (frxId) {
            if (frxId === 'generalComments') {
                AJS.$('#generalComments,#frx-overview').addClass('filtered');
            } else {
                AJS.$('#frxouter' + frxId + ',#frx-list-item' + frxId).addClass('filtered');
                review.frx(frxId).setFiltered(true);
            }
        },

        showFrx : function (frxId) {
            if (frxId === 'generalComments') {
                AJS.$('#generalComments,#frx-overview').removeClass('filtered');
            } else {
                AJS.$('#frxouter' + frxId + ',#frx-list-item' + frxId).removeClass('filtered');
                review.frx(frxId).setFiltered(false);
            }
        },

        toggleFrx : function (frxId, onlyOpen) {
            var cruFrxNav = AJS.CRU.FRX.NAV;
            var current = cruFrxNav.getCurrentFrxElement();
            if (frxId === 'generalComments') {
                var $generalOuter = AJS.$('#' + frxId);
                var $generalInner = $generalOuter.find('#generalCommentsInner');
                if ($generalInner.is(':visible') && $generalOuter[0] === current && !onlyOpen) {
                    $generalInner.removeClass("open").addClass("closed");
                } else {
                    $generalInner.removeClass("closed").addClass("open");
                    cruFrxNav.setCurrentFrx($generalOuter[0], { sticky: true });
                }
            } else {
                var frx = review.frx(frxId);
                var frxOuter = AJS.$('#frxouter' + frxId)[0];
                if (frx.isExpanded() && frxOuter === current && !onlyOpen) {
                    res.collapseFrxs([frxId]);
                } else {
                    res.expandFrxs([frxId]);
                    cruFrxNav.setCurrentFrx(frxOuter, { sticky: true });
                }
            }
        },

        expandFrxs : function (frxIds) {
            var cruFrxAjax = AJS.CRU.FRX.AJAX;
            frxIds = AJS.$.makeArray(frxIds);
            var frxIdsToLoad = [];
            var expandedSome = false;
            for (var i = 0, len = frxIds.length; i < len; ++i) {
                var frxId = frxIds[i];
                var frx = review.frx(frxId);
                if (!frx.isExpanded()) {
                    AJS.$("#frxouter" + frxId)
                        .removeClass("closed minimised")
                        .addClass("open");
                    frx.setExpanded(true);
                    setFrxDimHeight(frxId);
                    expandedSome = true;
                }
            }

            if (expandedSome) {
                cruFrxAjax.frxLoad(frxIdsToLoad);
            }
        },

        expandFrx : function (frxId) {
            var frx = review.frx(frxId);
            if (frx && !frx.isExpanded()) {
                res.expandFrxs(frx.id());
            }
        },

        collapseFrxs : function (frxIds) {
            if (AJS.CRU.UI.isSingleFrxView() && !AJS.CRU.UI.isFullscreen()) {
                return;
            }
            frxIds = AJS.$.makeArray(frxIds);

            for (var i = 0, len = frxIds.length; i < len; ++i) {
                var frxId = frxIds[i];
                AJS.CRU.FRX.collapseFrx(frxId);
            }
        },

        collapseFrx : function (frxId) {
            AJS.$("#frxouter" + frxId)
                .removeClass("open")
                .addClass("closed");
            review.frx(frxId).setExpanded(false);
        },

        updateFiltererdFrxCount : function () {
            var unfilteredCount = AJS.$('#navigation-tree .frx-list-item:not(.filtered) > .frx').length;
            var filteredCount = AJS.$('#navigation-tree .frx-list-item.filtered > .frx').length;

            AJS.$('#filter-frxs-shown-count').text(unfilteredCount);
            AJS.$('#filter-frxs-unshown-count').text(filteredCount);

            if (AJS.$('#frxFilterOptions li.selected').length > 0) {
                AJS.$("#navigation-filter-link").parent().addClass("filter-enabled");
            } else {
                AJS.$("#navigation-filter-link").parent().removeClass("filter-enabled");
            }

            if (review.frxs().length > 0 && unfilteredCount === 0) {
                AJS.$('#frxs-filtered-warning').show();
            } else {
                AJS.$('#frxs-filtered-warning').hide();
            }
            syncFrxButtons();
        },

        goToNextElement : function (navigateForwards) {
            var scrollToComment = AJS.$('#search-comment').hasClass('selected');
            var scrollToFrx = AJS.$('#search-file').hasClass('selected');
            var scrollToDefect = AJS.$('#search-defect').hasClass('selected');
            var successor;
            var init;
            if (navigateForwards) {
                successor = 'next';
                init = 'first';
            } else {
                successor = 'previous';
                init = 'last';
            }

            var nextCommentMap;
            var nextCommentFrxOuter;
            var comment_nav = AJS.CRU.COMMENT.NAV;
            var commentDomId = comment_nav.getCurrentCommentDomId();
            var commentOpts = {
                skipNonDefects: scrollToDefect && !scrollToComment,
                skipReadComments: false,
                destination: commentDomId ? successor : init
            };

            if (scrollToComment || scrollToDefect) {
                nextCommentMap = comment_nav.navigateFindComment(commentOpts);

                if (nextCommentMap.scrollToDomId) {
                    if (commentDomId) {
                        if (commentDomId.replace(/[^0-9]/g, '') === nextCommentMap.scrollToDomId) {
                            scrollToComment = false;
                            scrollToDefect = false;
                        }
                    }
                } else if (!nextCommentMap.frxId) {
                    scrollToComment = false;
                    scrollToDefect = false;
                }
            }

            if (scrollToFrx) {
                var frx_nav = AJS.CRU.FRX.NAV;
                var frxId = frx_nav.getCurrentFrxId();
                var nextFrxId = frx_nav.getGotoFrxId({
                    frxId: frxId,
                    destination: frxId ? successor : init
                });
                var nextFrxOuter = AJS.$('#frxouter' + nextFrxId);

                if (frxId === nextFrxId) {
                    scrollToFrx = false;
                }
            }

            var $elementWithSpinner;
            if (navigateForwards) {
                $elementWithSpinner = AJS.$('#next-element-link');
            } else {
                $elementWithSpinner = AJS.$('#prev-element-link');
            }

            if (scrollToFrx && (scrollToComment || scrollToDefect)) {
                if (nextCommentMap.frxId) {
                    nextCommentFrxOuter = AJS.$('#frxouter' + nextCommentMap.frxId);
                } else {
                    nextCommentFrxOuter = AJS.$('#generalComments');
                }
                var nextFrxAfterNextComment = nextCommentFrxOuter.nextAll('#' + nextFrxOuter.attr('id')).length;
                if (navigateForwards && nextFrxAfterNextComment > 0 ||
                        !navigateForwards && nextFrxAfterNextComment === 0) {
                    comment_nav.navigateDirectlyToComment(commentOpts, nextCommentMap, $elementWithSpinner);
                } else {
                    res.scrollFrxPane(nextFrxId);
                }
            } else if (scrollToFrx) {
                res.scrollFrxPane(nextFrxId);
            } else if (scrollToComment || scrollToDefect) {
                comment_nav.navigateDirectlyToComment(commentOpts, nextCommentMap, $elementWithSpinner);
            } else {
                var $button = AJS.$('#' + (navigateForwards ? 'next' : 'prev') + '-element-link');
                $button.effect('highlight', { color: '#f00'}, 800);
            }
        },

        filterFrxs : function () {
            var frxFilterOptions = AJS.$.map(
                AJS.$('#frxFilterOptions li.selected'),
                function (e) { return AJS.$(e).attr('id').replace("frx-filter-", ""); }
            );
            var incomplete = AJS.$.inArray('incomplete', frxFilterOptions) >= 0;
            var comments = AJS.$.inArray('comments', frxFilterOptions) >= 0;
            var unreadcomments = AJS.$.inArray('unreadcomments', frxFilterOptions) >= 0;
            var subtasks = AJS.$.inArray('subtasks', frxFilterOptions) >= 0;
            var unresolvedsubtasks = AJS.$.inArray('unresolvedsubtasks', frxFilterOptions) >= 0;
            var draftcomments = AJS.$.inArray('draftcomments', frxFilterOptions) >= 0;
            var defects = AJS.$.inArray('defects', frxFilterOptions) >= 0;
            var binary = AJS.$.inArray('binary', frxFilterOptions) >= 0;
            var nonbinary = AJS.$.inArray('nonbinary', frxFilterOptions) >= 0;
            var nondirectory = AJS.$.inArray('nondirectory', frxFilterOptions) >= 0;
            var newsincecomplete = AJS.$.inArray('newsincecomplete', frxFilterOptions) >= 0;
            var isCommentLevelFilter = unreadcomments || subtasks || unresolvedsubtasks || draftcomments || defects;

            var filterEnabled = frxFilterOptions.length > 0;
            if (filterEnabled) {
                AJS.$("#clear-filter-link").show();
            } else {
                AJS.$("#clear-filter-link").hide();
            }

            var frxs = review.frxs();
            for (var i = 0, len = frxs.length; i < len; ++i) {
                var frx = frxs[i];

                //if any frx-level filters do not match, hide this frx
                if ((incomplete &&
                        frx.isComplete()) ||
                    (comments &&
                        !frx.hasComments()) ||
                    (binary &&
                        !frx.isBinary()) ||
                    (nonbinary &&
                        frx.isBinary()) ||
                    (nondirectory &&
                        frx.isDirectory()) ||
                    (newsincecomplete &&
                        !frx.isNewSinceComplete())) {
                    res.hideFrx(frx.id());
                    continue;
                }
                if (isCommentLevelFilter) {
                    //on a comment-level filter, hide if no comments
                    if (!frx.hasComments()) {
                        res.hideFrx(frx.id());
                        continue;
                    }
                    var hide = true;
                    var frxComments = frx.comments();
                    for (var j = 0, cLen = frxComments.length; j < cLen; j++) {
                        var frxComment = frxComments[j];
                        var status = frxComment.issueStatus();
                        if (status) {
                            status = status.toLowerCase();
                        }
                        //show this frx if any comment matches all the comment-level filters
                        if (!(unreadcomments && !(frxComment.status() === 'unread' || frxComment.status() === 'leaveUnread') ||
                                subtasks && !(frxComment.issueKey() && frxComment.issueKey() != '') ||
                                unresolvedsubtasks && !(frxComment.issueKey() && status !== 'resolved' && status !== 'closed') ||
                                draftcomments && !(frxComment.draft()) ||
                                defects && !(frxComment.defect()))) {
                            hide = false;
                            break;
                        }
                    };
                    if (hide) {
                        res.hideFrx(frx.id());
                        continue;
                    }
                }

                res.showFrx(frx.id());
                res.expandFrx(frx.id());
            }
            AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
            AJS.CRU.FRX.NAV.visibleFrxsChanged();
            var $currentFrx = AJS.$(AJS.CRU.FRX.NAV.getCurrentFrxElement());
            if (!$currentFrx.hasClass('frxouter') || $currentFrx.hasClass('filtered')) {
                activateFirstUnfilteredFrx();
            }
            AJS.$('#navigation-tree .folder').parent().addClass('filtered');
            AJS.$('#navigation-tree .frx-list-item:has(li.frx-list-item:not(.filtered))').removeClass('filtered');
            AJS.CRU.FRX.updateFiltererdFrxCount();
        },

        postPostLoadFunc : postPostLoadFunc,

        defaultPostLoad : function (summarize) {
            postPostLoadSummarize = summarize;
            AJS.$(window).bind('panes-resized', function () {
                commentator.setCommentWidths(null, true);
            });
        },

        revPostLoad : function (summarize) {
            res.defaultPostLoad(summarize);
            var cru = AJS.CRU;
            var cru_frx = cru.FRX;
            var anchoredFrx = cru_frx.NAV.checkFrxAnchor();
            if (!anchoredFrx) {
                anchoredFrx = cru.COMMENT.NAV.checkCommentAnchor();
            }
            if (!anchoredFrx) {
                filterFrxsFromHash();
            }
            if (!anchoredFrx) {
                anchoredFrx = res.checkSegmentAnchor();
            }
            if (!anchoredFrx && window.location.hash) {
                AJS.CRU.FRX.scrollFrxPane('generalComments', window.location.hash.replace('#', ''));
                AJS.CRU.UI.highlightElements(AJS.$(window.location.hash).children('.overview-item'));
            }
            cru_frx.AJAX.setIsInitialFrxLoad();
            cru_frx.AJAX.frxLoad(optimizedFrxList(anchoredFrx));
        },

        checkSegmentAnchor : function () {
            var m = /^#frx([0-9]+)_seg/.exec(window.location.hash);
            if (m) {
                var frxId = m[1];
                var frx = review.frx(frxId);
                var $segment = AJS.$(window.location.hash);

                var isInlineButNotLoaded = $segment.length === 0;
                if (isInlineButNotLoaded) {
                    if (frx.isLoaded()) {
                        res.scrollToSegment(frxId, window.location.hash);
                    } else {
                        var onDone = function () {
                            res.scrollToSegment(frxId, window.location.hash);
                        };
                        AJS.CRU.FRX.AJAX.prioritisedFrxLoad(frx.id(), onDone);
                    }
                    return frxId;
                } else {
                    window.location.hash = '';
                    AJS.CRU.FRX.NAV.setCurrentFrxAndScroll('generalComments');
                }
            }
            return null;
        },

        //todo: use this method instead of hashes to jump between them - for now this is only used when loading the page
        scrollToSegment: function (frxId, segId) {
            var cruFrx = AJS.CRU.FRX,
                cruFrxNav = cruFrx.NAV;
            if (!AJS.CRU.UI.isSingleFrxView()) {
                cruFrx.expandFrx(frxId);
            }
            cruFrxNav.setCurrentFrx(frxId);
            var $seg = AJS.$(segId);
            if ($seg.length > 0) {
                AJS.$('#frx-pane').scrollTo($seg[0], {
                    axis: 'y',
                    offset: -2
                });
            }
        },

        toggleAllFrxsSoftWrapping : function (enabled) {
            if (enabled) {
                AJS.$('#reviewpage').removeClass('noWrapping').addClass('wrapping');
            } else {
                AJS.$('#reviewpage').addClass('noWrapping').removeClass('wrapping');
            }
        },

        toggleAllFrxsDiffMode : function (mode) {
            res.reloadFrxs({ diffOpts : {diffLayout: mode}});
        },

        toggleSource : function (ids) {
            if (commentator.showSource) {
                commentator.showSource = false;
                AJS.$('#reviewpage').addClass("hide-source");
                AJS.$('#show_source_button').removeClass('selected');

                commentator.toggleComments("above");
            } else {
                commentator.showSource = true;
                AJS.$('#reviewpage').removeClass("hide-source");
                AJS.$('#show_source_button').addClass('selected');
            }
        },

        toggleCommitMessages : function (btn) {
            AJS.$(btn).toggleClass('pressed');
            toggleType(review.frxIds(),'changeset_detailsfrxinner');
        },

        changeBlankLines: function (permaId, frxId, bl) {
             res.changeWhitespace(permaId, frxId, null, bl);
        },

        changeWhitespace : function (permaId, frxId, ws, bl) {
            var params = res.diffParams(frxId, {whiteSpace: ws, blankLines: bl}) + "&showDiffContextBar=true";
            params += AJS.CRU.FRX.AJAX.frxRevisionParams(frxId);
            AJS.CRU.FRX.AJAX.toggleSourceType(permaId, frxId, false, params);
        },

        whiteSpaceParams: function (frx, ws) {
            if (ws) {
                frx.setWhitespace(ws);
            } else {
                ws = frx.getWhitespace();
            }
            return ws;
        },

        blankLinesParams: function (frx, bl) {
            if (bl !== null && bl !== undefined) {
                frx.setIgnoreBlankLines(bl);
            } else {
                bl = frx.getIgnoreBlankLines();
            }
            return bl;
        },

        contextParams: function (frx, context) {
            if (context) {
                frx.setContext(context);
            } else {
                context = frx.getContext();
            }
            return context;
        },

        diffLayoutParams: function (frx, opt) {
            if (opt) {
                frx.setDiffLayout(opt);
            } else {
                opt = frx.getDiffLayout();
            }
            return opt;
        },

        diffParamsMap: function (frxId, map) {
            var frx = review.frx(frxId);
            map = map || {};
            if (frx) {
                map.context = res.contextParams(frx, map.context);
                map.diffLayout = res.diffLayoutParams(frx, map.diffLayout);
                map.blankLines = res.blankLinesParams(frx, map.blankLines);
                map.whiteSpace = res.whiteSpaceParams(frx, map.whiteSpace);
            }
            return map;
        },

        diffParams: function (frxId, map) {
            map = res.diffParamsMap(frxId, map);
            var result = '';
            if (map.context) {
                result += '&u=' + map.context;
            }
            if (map.diffLayout) {
                result += '&fv=' + map.diffLayout;
            }
            if (map.blankLines) {
                result += '&bl=' + map.blankLines;
            }
            if (map.whiteSpace) {
                result += '&ws=' + map.whiteSpace;
            }
            return result;
        },

        reloadSingleFrx : function (permaId, frxId, diffLayout) {
            var params = res.diffParams(frxId, {'diffLayout' : diffLayout}) + "&showDiffContextBar=true";
            params += AJS.CRU.FRX.AJAX.frxRevisionParams(frxId);
            AJS.CRU.FRX.AJAX.toggleSourceType(permaId, frxId, false, params, null);
        },

        toggleDiffModeAjax : function (permaId, frxId, u, done) {
            var params = res.diffParams(frxId, {'context': u});
            params += AJS.CRU.FRX.AJAX.frxRevisionParams(frxId);
            AJS.CRU.FRX.AJAX.toggleSourceType(permaId, frxId, false, params, done);
        },

        frxActivated : function (frxOuter) {
            var cruFrx = AJS.CRU.FRX,
                cruFrxNav = cruFrx.NAV,
                cruFrxAjax = cruFrx.AJAX,
                cruCommentNav = AJS.CRU.COMMENT.NAV;

            var frxId = frxOuter.id.replace('frxouter', '');
            AJS.$(frxOuter).addClass("activeFrx");
            if (frxId === 'generalComments') {
                AJS.$("#frx-overview").addClass("activeFrx");
            } else {
                AJS.$("#frx-list-item" + frxId).addClass("activeFrx");

                var frx = review.frx(frxId);
                if (frx.isLoaded()) {
                    cruCommentNav.visibleCommentsChanged();
                } else {
                    // If the frx hasn't loaded yet, then we should push it to the top of the load stack.
                    cruFrxAjax.prioritisedFrxLoad(frxId);
                }
                if (!cruFrxAjax.isFrxControlsRendered(frxId)) {
                    cruFrxAjax.unshelveFrxControls(frxId);
                }
            }

            if (AJS.CRU.UI.isSingleFrxView()) {
                cruFrxNav.visibleFrxsChanged();
            }

            syncFrxButtons();
        },

        frxDeactivated : function (frxOuter) {
            var frxId = frxOuter.id.replace('frxouter', '');
            AJS.$(frxOuter).removeClass("activeFrx");
            if (frxId === 'generalComments') {
                // general controls
                AJS.$("#frx-overview").removeClass("activeFrx");
            } else {
                AJS.$("#frx-list-item" + frxId).removeClass("activeFrx");
            }
        },

        dimFrx : function (frxId) {
            // sanity check
            if (!review.frx(frxId)) {
                return;
            }
            var $inner = AJS.$("#sourcefrxinner" + frxId);

            var $blanket = AJS.$("<div id='frx-dim-"+frxId+"' class='frx-dim'>&nbsp;</div>" +
                                 "<div class='frx-dim-msg'>Updating source...</div>");

            $blanket.css({
                height: $inner.height(),
                width: AJS.$("#sourceTable" + frxId).width() // Source table can push out from parent dimensions
            });
            $inner.prepend($blanket);
        },

        scrollFrxPane : function (containerId, innerId) {
            var cruFrxNav = AJS.CRU.FRX.NAV;
            cruFrxNav.setCurrentFrxAndScroll(containerId);
            if (innerId) {
                var $inner = AJS.$(cruFrxNav.getCurrentFrxElement()).find('#' + innerId);
                if ($inner.length === 1) {
                    AJS.$('#frx-pane').scrollTo($inner[0]);
                }
            }
        }
    };

    var syncFrxButtons = function () {
        // Sync the file nav buttons
        var frxs = AJS.$.grep(review.frxIds(), function (frxId) {
            return !review.frx(frxId).isFiltered();
        });

        if (!AJS.$('#generalComments').hasClass('filtered')) {
            frxs = AJS.$.merge(["generalComments"], frxs);
        }

        var frxCount = frxs.length;
        var $toolbars = AJS.$("#frxs").find("div.toolbar");
        if (frxCount === 0) {
            $toolbars.find("li.prev-file-button, li.next-file-button").addClass("disabled");
        } else {
            var frxIndex = AJS.$(frxs).index(AJS.CRU.FRX.NAV.getCurrentFrxId());
            var firstFrx = frxIndex === 0;
            var lastFrx = frxIndex === frxCount - 1;
            $toolbars.find("li.prev-file-button").toggleClass("disabled", firstFrx);
            $toolbars.find("li.next-file-button").toggleClass("disabled", lastFrx);
        }
    };

    var setFrxDimHeight = function (frxId) {
        AJS.$("#frx-dim"+frxId).css("height", AJS.$("#sourcefrxinner" + frxId).height());
    };

    return res;
})();

;
/* END /2static/script/cru/review/frx/frx.js */
/* START /2static/script/cru/review/frx/frx-event.js */
AJS.$(document).ready(function() {

    var cruFrx = AJS.CRU.FRX,
        cruFrxNav = cruFrx.NAV,
        cruUi = AJS.CRU.UI;

    cruFrx.updateFiltererdFrxCount(); // Setup the count in the filter div

    // Clicking on nav tree directories:
    //   1. If we click on the actual link, then if it is an FRX directory, activate it
    //      (handled by .scroll-to-frx live event).
    //   2. Otherwise, toggle the subtree
    //   3. If we click on the parent span.folder, then toggle it (dont activate the frx)
    AJS.$("#navigation-tree .tree span.folder").live('click', function(event) {
        var $target = AJS.$(event.target);
        if (!$target.is('a.frx-dir-item')) {
            var $folder = AJS.$(this);
            if ($folder.hasClass("open")) {
                $folder.removeClass("open")
                     .addClass("closed");
            } else {
                $folder.removeClass("closed")
                     .addClass("open");
            }
            $folder.next('.tree').toggle();
        }
    });

    /** Focus the FRX and handle collapse/expand when clicking the header. */
    AJS.$("#frx-pane .frx-header").live('click', function (event) {
        var frxId = this.id.replace(/^frx-header-/, '');
        var $target = AJS.$(event.target);
        var clickedToolbar = $target.closest('#frx-toolbar-' + frxId).length === 1;
        var clickedMenu = clickedToolbar && $target.closest('.frx-actions-secondary').length === 1;
        var clickedAUIInlineHover = $target.closest(".aui-inline-dialog").length === 1;
        cruFrx.toggleFrx(frxId, !clickedToolbar || clickedMenu || clickedAUIInlineHover);
    });

    AJS.$(".frxControls a.toggleFileRead").live("click", function() {
        var frx = review.frx(AJS.$(this).closest('.frxControls').attr('id').replace('frxControls', ''));
         cruFrx.AJAX.toggleFileRead(frx.id(), true);
    });

    function scrollToGeneralComments(innerId) {
        cruFrx.scrollFrxPane('generalComments', innerId);
        cruFrxNav.scrollToCommentForm();
    }

    if (AJS.$('#reviewpage').length === 1) {
        AJS.$('#navigation-tree .frx-list-item:not(.filtered) > span > a.scroll-to-frx').live('click', function() {
            var frxId = AJS.$(this).attr('id').replace(/scroll-to-frx/, '');
            cruFrxNav.setCurrentFrxAndScroll(frxId);
            cruFrxNav.scrollToCommentForm();
        });

        AJS.$('#frx-overview:not(.filtered) .comments a').live('click', function() {
            scrollToGeneralComments('general-comments');
            cruUi.highlightElements(AJS.$('#general-comments').children('.overview-item'));
        });

        AJS.$('#frx-overview:not(.filtered) .details a').live('click', function() {
            scrollToGeneralComments('details');
            cruUi.highlightElements(AJS.$('#details').children('.overview-item'));
        });

        AJS.$('#frx-overview:not(.filtered) .objectives a').live('click', function() {
            scrollToGeneralComments('objectives');
            cruUi.highlightElements(AJS.$('#objectives').children('.overview-item'));
        });

        AJS.$('#frx-overview:not(.filtered) .summary a').live('click', function() {
            scrollToGeneralComments('summary');
            cruUi.highlightElements(AJS.$('#summary').children('.overview-item'));
        });

        AJS.$('#frx-overview:not(.filtered) .notifications a').live('click', function() {
            scrollToGeneralComments('notifications');
            cruUi.highlightElements(AJS.$('#notifications').children('.overview-item'));
        });

        if (!window.location.hash) {
            scrollToGeneralComments();
        }
    }


});
;
/* END /2static/script/cru/review/frx/frx-event.js */
/* START /2static/script/cru/review/frx/frx-ajax.js */
AJS.CRU.FRX.AJAX = (function() {

    var FRXS_FOR_LOAD = [];
    var IS_AUTO_LOADING = false;
    var IS_INITIAL_FRX_LOAD = false;

    var setFrxRevisionParams = function (frxId, from, to) {
        to = to || "";
        from = from || "";
        review
            .frx(frxId)
                .setVisibleFromRevision(from)
                .setVisibleToRevision(to);
    };

    var frxLoadDiff = function (frxId, onCompleteFunc, loadOpts) {
        loadOpts = loadOpts || {};

        var cruFrx = AJS.CRU.FRX,
            cruFrxNav = cruFrx.NAV;

        var frx = review.frx(frxId);
        var fromRev = loadOpts.fromRev || frx.visibleFromRevision();
        var toRev = loadOpts.toRev || frx.visibleToRevision();

        var changeDiff = !(fromRev === frx.visibleFromRevision() && toRev === frx.visibleToRevision());
        var shouldReload = !frx.isLoading() && (!frx.isLoaded() || loadOpts.isForcedReload || changeDiff);

        var onComplete = function (resp) {
            if (resp) {
                AJS.$("#frxinner"+frxId).removeClass("frx-loading");
                frx.setLoading(false);
            }
            if (resp && resp.worked && shouldReload) {
                res.reloadFrxModel(frxId, resp, false);
                res.updateFrxDiffControls(frxId, resp);
                res.updateFrxSlider(frxId, resp);
                res.updateNavListClasses(frxId, resp);
                rebindEventsAndRefreshComments(frxId);
                if (loadOpts.rescanComments || cruFrxNav.getCurrentFrxId() === frxId) {
                    AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
                }
                frx.setLoaded(true);
            }

            if (AJS.$.browser.msie) {
                if (document.location.hash.split("-")[1] != frxId) {
                   AJS.$("#frxouter" + frxId).addClass("minimised");
                }
            }

            if (onCompleteFunc) {
                onCompleteFunc();
            }
        };

        if (shouldReload) {
            frx.setLoading(true);
            cruFrx.dimFrx(frxId);
            var elementIdToUpdate = 'frxinner' + frxId;
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) +'/loadFrxAjax';
            var diffModeOpts = cruFrx.diffParams(frxId, loadOpts.diffMode);
            var noCacheRand = Math.floor(Math.random() * 999999);
            setFrxRevisionParams(frxId, fromRev, toRev);
            var pars = 'frxid=' + frxId + '&dontCache=' + noCacheRand + diffModeOpts + res.frxRevisionParams(frxId);
            AJS.FECRU.AJAX.ajaxUpdate(url, pars, elementIdToUpdate, onComplete);
        } else {
            onComplete(null); //allow the onComplete to finish despite not loading.
        }
    };

    var inFrxLoadQueue = function (frxId) {
        var queue = FRXS_FOR_LOAD;
        for (var i = 0, len = queue.length; i < len; i++) {
            if (frxId === queue[i].id) {
                return i;
            }
        }
        return -1;
    };

    // Initial frxs which are loaded are limited to 20 in AJS.CRU.FRX.optimizedFrxList()
    var doSerialisedFrxLoad = function () {
        IS_AUTO_LOADING = true;

        // load and chain
        if (FRXS_FOR_LOAD.length > 0) {
            // We store either an integer id or a map of id/onDone in the array, so here we check for the objects
            var frxOpts = FRXS_FOR_LOAD[0];
            var frxId = frxOpts.id;
            var onDone = frxOpts.onDone;
            var loadOpts = frxOpts.loadOpts;

            var onComp = function() {
                review.frx(frxId).setOutOfDate(false);
                AJS.CRU.CREATE.bindUpdateFrxFromFormToggle('#updateFrxFromFormToggle' + frxId, frxId);
                if (AJS.CRU.FRX.NAV.getCurrentFrxId() === frxId) {
                    res.markFileRead(frxId, false);
                }

                // Execute the callback if it exists
                onDone && onDone();
                // Order of FRX load queue may have changed, so we need to do a lookup.
                var queueIndex = inFrxLoadQueue(frxId);
                if (queueIndex >= 0) {
                    // This should always be the case, since only this method should remove items from the queue.
                    FRXS_FOR_LOAD.splice(queueIndex, 1);
                }
                doSerialisedFrxLoad(); // chain
            };

            frxLoadDiff(frxId, onComp, loadOpts); // load
        } else if (IS_INITIAL_FRX_LOAD) { // when finished chaining
            var _commentator = commentator; // local alias
            var cru_frx = AJS.CRU.FRX;

            _commentator.setCommentWidths();
            if (cru_frx.postPostLoadFunc) {
                cru_frx.postPostLoadFunc();
            }
            AJS.$("#show_source_button").removeClass("disabled").addClass("selected").children("a").click(function () {
                cru_frx.toggleSource(review.frxIds());
            });
            _commentator.setCommentWidths();
            commentAjaxController.loadCommentIssueStatuses();
            _commentator.updateCommentCount();
            IS_INITIAL_FRX_LOAD = false;
            IS_AUTO_LOADING = false;
        } else {
            IS_AUTO_LOADING = false;
        }
    };

    var rebindEventsAndRefreshComments = function (frxId) {
        tetrisCommentController.renderTetrisCommentMarkersForFrx(frxId);
        commentAjaxController.loadCommentIssueStatuses(frxId);
    };

    var res = {
        frxRevisionParams : function (frxId) {
            var frx = review.frx(frxId);
            var params = "";
            if (!frx) {
                return params;
            }
            if( frx.visibleFromRevision() ) {
                params += "&fromRev=" + frx.visibleFromRevision();
            }
            params += "&toRev=" + frx.visibleToRevision();
            return params;
        },

        updateFrxView : function (from, to, frxId) {
            setFrxRevisionParams(frxId, from, to);
            var oldInnerHTML = AJS.$('#frxouter' + frxId).html();
            res.shelveFrxControls(frxId);
            AJS.CRU.FRX.dimFrx(frxId);
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/updateFrxViewAjax';
            var pars = "frxid=" + frxId + res.frxRevisionParams(frxId) + AJS.CRU.FRX.diffParams(frxId);
            var elementIdToUpdate = 'frxouter' + frxId;
            var onComp = function(origRequest) {
                if (!origRequest.worked) {
                    // if the request didn't work, make sure the spinner
                    // is taken out, and the old text restored.
                    AJS.$('#frxouter' + frxId).html(oldInnerHTML);
                } else {
                    res.unshelveFrxControls(frxId);
                    res.reloadFrxModel(frxId, origRequest, true);
                    commentator.setCommentWidths(null, true);
                    res.updateFrxDiffControls(frxId, origRequest);
                    rebindEventsAndRefreshComments(frxId);
                    AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
                }
            };
            AJS.FECRU.AJAX.ajaxUpdate(url, pars, elementIdToUpdate, onComp);
            return false;
        },

        isFrxControlsRendered: function (frxId) {
            return AJS.$('#frx-context-info-' + frxId).children('.frxControls:first').length === 1;
        },

        shelveFrxControls: function (frxId) {
            AJS.$('#frxControlsContainer' + frxId).append(AJS.$('#frx-context-info-' + frxId).children('.frxControls:first'));
        },

        unshelveFrxControls: function (frxId) {
            AJS.$('#frx-context-info-' + frxId).append(AJS.$('#frxControlsContainer' + frxId).children('.frxControls:first'));
        },

        reloadFrxModel: function (frxId, resp, keepSliderRevs) {
            var oldFrx = review.frx(frxId);
            var isExpanded = oldFrx.isExpanded();
            if (keepSliderRevs) {
                var sliderFrxRevisions = oldFrx.getSliderFrxRevisions();
            }

            eval('var frxModelLoader = ' + resp.frxModelLoader);
            frxModelLoader();

            var newFrx = review.frx(frxId);
            newFrx
                .setLoaded(true)
                .setExpanded(isExpanded);
            if (keepSliderRevs) {
                newFrx.setSliderFrxRevisions(sliderFrxRevisions);
            }
        },

        updateFrxDiffControls: function (frxId, frxAjaxResp) {
            var contents = frxAjaxResp.diff;
            if (contents) {
                AJS.$('#diffOptionsFormPlaceholder' + frxId).html(frxAjaxResp.diff);
            } else {
                AJS.$('#diffOptionsFormPlaceholder' + frxId).hide();
            }
            // reset the fisheye diff link
            var toRev = review.frx(frxId).visibleToSCMRevision();
            var fromRev = review.frx(frxId).visibleFromSCMRevision();
            if (toRev === fromRev) {
                AJS.$('#fisheyeDiffUrl' + frxId).hide();
            } else {
                AJS.$('#fisheyeDiffUrl' + frxId + ' a')
                        .attr('href', AJS.$('#baseFisheyeDiffUrl' + frxId).val() + '?r1=' + fromRev + '&r2=' + toRev);
                AJS.$('#fisheyeDiffUrl' + frxId).show();
            }
        },

        updateFrxSlider: function (frxId, frxAjaxResp) {
            var $sliderDiv = AJS.$('#frxSliderDiv-' + frxId);
            $sliderDiv.html(frxAjaxResp.slider);
            // Sliders in the edit content panel of manage files should be disabled
            if ($sliderDiv.hasClass('editContent')) {
                AJS.$('#frxSlider_'+frxId).slider('disable');
            }
        },

        updateFrxEditRevisionsDropdown: function (frxId) {
            // Synchronize the edit revisions select options.
            var frx = review.frx(frxId);
            var $revisionSelect = AJS.$('#changeDiffSelect' + frxId);
            if ($revisionSelect.length === 1) {
                var crucibleRevision = frx.frxRevisionToCruRevisionMap();
                $revisionSelect.find('option:disabled').attr('disabled', false);
                $revisionSelect.find(
                    AJS.$.map(frx.frxRevisions(), function (frxRevision) {
                        return 'option[value=' + crucibleRevision[frxRevision] + ']';
                    }).join()
                ).attr('disabled', true);
                // Select the option that says "select a revision to add"
                $revisionSelect[0].selectedIndex = 0;
                AJS.$('#changeDiffAddRevision' + frxId).attr(
                    'disabled',
                    $revisionSelect.find('option:enabled').length === 0
                );
            }
        },

        updateNavListClasses: function (frxId, frxAjaxResp) {
            AJS.$('#frx-list-item' + frxId + ' > span, #frx-list-item' + frxId + ' > span')
                .removeClass('frx-added frx-copied frx-deleted frx-changed frx-complete frx-incomplete')
                .addClass(frxAjaxResp.navListClasses);
        },

        /**
         * Causes an frx to be marked for immediate loading from the server. If there is a queue of frxs to be loaded, then it
         * is prepended to the front of the list, otherwise it is automatically loaded.
         * @param {Array} frxIds the ids of the frxs to load. May be a single frx id.
         * @param {Function} onDone callback to execute after each frx has been loaded
         * @param {Object} loadOpts options to pass to the server for loading of the frx
         */
        prioritisedFrxLoad : function (frxIds, onDone, loadOpts) {
            frxIds = AJS.$.makeArray(frxIds);
            // Reverse through the list to preserve original order as we push onto the front of the array
            for (var i = frxIds.length - 1; i >= 0; i--) {
                var frxId = frxIds[i];
                var queueIndex = inFrxLoadQueue(frxId);
                if (queueIndex < 0) {
                    // Push this frxId to the front of the list of loading frxs
                    FRXS_FOR_LOAD.unshift({
                        id: frxId,
                        onDone: onDone,
                        loadOpts: loadOpts,
                        rescanComments: (i === 0 && !IS_AUTO_LOADING) ? true : false // only rescan comments for the first frx if we aren't already loading
                    });
                } else if (queueIndex > 0) {
                    // Move to the front if not already there.
                    var frxOpts = FRXS_FOR_LOAD.splice(queueIndex, 1)[0];
                    FRXS_FOR_LOAD.unshift(frxOpts);
                }
            }
            if (!IS_AUTO_LOADING) {
                doSerialisedFrxLoad();
            }
        },

        /**
         * Causes an frx to be marked for loading from the server. If there is a queue of frxs to be loaded, then it
         * is pushed to the back of the list, otherwise it is automatically loaded.
         * @param {Array} frxIds the ids of the frxs to load. May be a single frx id.
         * @param {Function} onDone callback to execute after each frx has been loaded
         * @param {Object} loadOpts options to pass to the server for loading of the frx
         */
        frxLoad : function (frxIds, onDone, loadOpts) {
            frxIds = AJS.$.makeArray(frxIds);
            for (var i = 0, len = frxIds.length; i < len; i++) {
                var frxId = frxIds[i];
                var queueIndex = inFrxLoadQueue(frxId);
                if (queueIndex < 0) {
                    // Push this frxId to the end of the list of loading frxs
                    FRXS_FOR_LOAD.push({
                        id: frxId,
                        onDone: onDone,
                        loadOpts: loadOpts,
                        rescanComments: (i === 0 && !IS_AUTO_LOADING) ? true : false // only rescan comments for the first frx if we aren't already loading
                    });
                }
            }
            if (!IS_AUTO_LOADING) {
                doSerialisedFrxLoad();
            }
        },

        setIsInitialFrxLoad : function () {
            IS_INITIAL_FRX_LOAD = true;
        },

        markAllFilesRead : function (updatedFiles, permaId, onComplete) {
            for (var i = 0, len = review.frxs().length; i < len; i++) {
                review.frxs()[i].setComplete(true);
            }
            if (updatedFiles.length > 0) {
                var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/fileReadStatusAjax/';
                var params = {
                    markAsRead: true,
                    fid: AJS.$.map(updatedFiles, function (frx) { return frx.id(); })
                };

                AJS.FECRU.AJAX.ajaxDo(url, params, onComplete);
            }
            return false;
        },

        toggleFileRead : function (frxId, force) {
            var frx = review.frx(frxId);
            if (!frx) {
                return;
            }
            var oldStatus = frx.readStatus();
            if (oldStatus === 'read' || oldStatus === 'unread') {
                res.leaveFileUnread(frxId);
            } else {
                res.markFileRead(frxId, force);
            }
        },

        changeFileReadStatusClass : function(frxId, markAsRead, newStatus) {
            var $frxControls = AJS.$('#frxControls' + frxId);
            var frx = review.frx(frxId);
            var oldStatus = frx.readStatus();
            if (oldStatus === newStatus) {
                return;
            }
            $frxControls.removeClass(oldStatus).addClass(newStatus);
            var toggleCompleteSelectors = "#frx-list-item" + frxId + " > span.frx," +
                                                  "#frx-list-item" + frxId + " > span.folder," +
                                                  "#frxouter" + frxId;
            var $frxElems = AJS.$(toggleCompleteSelectors);
            if (markAsRead) {
                $frxElems.removeClass("frx-incomplete")
                         .addClass("frx-complete");
            } else {
                $frxElems.addClass("frx-incomplete")
                         .removeClass("frx-complete");
            }

            frx.setReadStatus(newStatus);
        },

        updateFileReadStatus : function(frxId, permaId, markAsRead) {
            var frx = review.frx(frxId);
            var oldStatus = frx.readStatus();
            var newStatus = markAsRead ? 'read' : 'leaveUnread';
            if (!review.writable() || frx.isReadStatusLocked() || oldStatus === newStatus) {
                return false;
            }
            // Prevent simultaneous requests.
            frx.lockReadStatus();

            var $frxControls = AJS.$('#frxControls' + frxId)
                    .removeClass(oldStatus)
                    .addClass('readStatusLocked');

            var done = function(resp) {
                $frxControls.removeClass('readStatusLocked');
                if (resp.worked) {
                    res.changeFileReadStatusClass(frxId, markAsRead, newStatus);
                } else {
                    $frxControls.addClass(oldStatus);
                }
                frx.unlockReadStatus();
            };

            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/fileReadStatusAjax/';
            var params = { "frxId" : frxId, "markAsRead" : markAsRead };

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

            return false;
        },

        markFileRead : function (frxId, force) {
            if (review.isDraft()) {
                return;
            }
            var frx = review.frx(frxId);
            if (!frx) {
                return;
            }
            var leaveUnread = frx.readStatus() === 'leaveUnread';
            if (!leaveUnread || force) {
                frx.setComplete(true);
                res.updateFileReadStatus(frxId, permaId, true);
            }
        },

        leaveFileUnread : function (frxId) {
            review.frx(frxId).setComplete(false);
            res.updateFileReadStatus(frxId, permaId, false);
        },

        toggleSourceType : function (permaId, frxId, showAnnotation, params, handler) {
            AJS.CRU.FRX.dimFrx(frxId);
            var url = AJS.CRU.UTIL.jsonUrlBase(permaId) +'/toggleSourceTypeAjax';
            var showAnnotationStr = '';
            var $dof = AJS.$('#diffOptionsForm');
            var $diffContextToggle = AJS.$('#diffControlsToggle' + frxId);
            var elementIdToUpdate = 'frxouter' + frxId;
            var oldInnerHTML = AJS.$('#frxouter' + frxId).html();
            res.shelveFrxControls(frxId);

            var onComp = function(origRequest) {
                if (!origRequest.worked) {
                    // if the request didn't work, make sure the spinner
                    // is taken out, and the old text restored.
                    AJS.$('#frxouter' + frxId).html(oldInnerHTML);
                } else {
                    res.unshelveFrxControls(frxId);
                    res.reloadFrxModel(frxId, origRequest, true);
                    commentator.setCommentWidths(null, true);
                    res.updateFrxDiffControls(frxId, origRequest);
                    rebindEventsAndRefreshComments(frxId);
                    AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
                    if (handler) {
                        handler(origRequest);
                    }
                }
            };
            if (showAnnotation) {
                showAnnotationStr = '&showAnnotation=true';
                $dof.hide();
                $diffContextToggle.hide();
            } else {
                $dof.show();
                $diffContextToggle.show();
            }
            var pars = 'frxid=' + frxId + showAnnotationStr + params;
            AJS.FECRU.AJAX.ajaxUpdate(url, pars, elementIdToUpdate, onComp);
            return false;
        }
    };
    return res;
})();
;
/* END /2static/script/cru/review/frx/frx-ajax.js */
/* START /2static/script/cru/review/frx/frx-nav.js */
AJS.CRU.FRX.NAV = {};

(function() {

    var currentFrxId = null;

    function asFrxId(frxHandle) {
        if (frxHandle instanceof AJS.$) {
            frxHandle = frxHandle[0];
        }
        if (!frxHandle) {
            return null;
        } else if (frxHandle.nodeType) {
            return frxHandle.id.replace(/(frxouter|frxinner)/, '');
        } else if (typeof frxHandle === 'string') {
            return frxHandle;
        }
        return null;
    }

    AJS.CRU.FRX.NAV.setCurrentFrxAndScroll = function (frxHandle) {
        AJS.CRU.FRX.NAV.setCurrentFrx(frxHandle, { scroll: true });
    };

    AJS.CRU.FRX.NAV.setCurrentFrx = function (frxHandle, options) {
        var cruFrx = AJS.CRU.FRX,
            cruFrxAjax = cruFrx.AJAX,
            cruFrxNav = cruFrx.NAV;

        options = AJS.$.extend({
            scroll: false,
            sticky: false
        }, options);

        var scrollTracker = cruFrxNav.scroller;
        
        var frxId = asFrxId(frxHandle);
        if (currentFrxId !== frxId) {
            if (currentFrxId !== null) {
                var $currentOuter = AJS.$(cruFrxNav.getCurrentFrxElement());
                cruFrx.frxDeactivated($currentOuter[0]);
                AJS.$(document).trigger('frx-deactive', [frxId]);
            }

            currentFrxId = frxId;

            if (frxId !== null) {
                var $itemOuter = AJS.$(cruFrxNav.getCurrentFrxElement());
                cruFrx.expandFrx(frxId);


                if (options.scroll) {
                    scrollTracker && scrollTracker.ignoreNextScroll();
                    AJS.$('#frx-pane').scrollTo($itemOuter[0]);
                }
                if (scrollTracker) {
                    scrollTracker.setCurrentElement($itemOuter[0]);
                    if (options.sticky) {
                        scrollTracker.makeCurrentElementSticky();
                    }
                }

                cruFrx.frxActivated($itemOuter[0]);

                var frx = review.frx(frxId);
                if (frx && frx.isLoaded()) {
                    cruFrxAjax.markFileRead(frxId);
                } else if (frxId === 'generalComments') {
                    cruFrxAjax.markFileRead(frxId);
                }
            }

            scrollNavigationTreeToFrxLink(frxId);

        } else {
            // Process the options on the current FRX.
            if (options.scroll) {
                AJS.$('#frx-pane').scrollTo(cruFrxNav.getCurrentFrxElement());
            }
            if (options.sticky) {
                scrollTracker && scrollTracker.makeCurrentElementSticky();
            }
        }
    };

    var scrollNavigationTreeToFrxLink;
    if (AJS.CRU.disableScrollTracking) {
        scrollNavigationTreeToFrxLink = function () {};
    } else {
        scrollNavigationTreeToFrxLink = function (frxId) {
            var scrollToId = frxId === 'generalComments' ? 'frx-overview' : ('frx-list-item' + frxId);
            var threshold = 40;
            var $link = AJS.$('#' + scrollToId);
            if ($link.filter(':in-viewport-vert(' + threshold + ', content-navigation-panel)').length === 0) {
                var offset = -threshold;
                if ($link.filter(':above-the-top(' + threshold + ', content-navigation-panel)').length === 0) {
                    offset = threshold - AJS.$('#content-navigation-panel').height();
                }
                AJS.$('#content-navigation-panel').scrollTo($link[0], {
                    axis: 'y',
                    offset: offset
                });
            }
        };
    }

    AJS.CRU.FRX.NAV.visibleFrxsChanged = function () {
        var cruFrx = AJS.CRU.FRX,
            cruFrxNav = cruFrx.NAV;
        var scrollTracker = cruFrxNav.scroller;
        if (scrollTracker) {
            scrollTracker.rescan();
        } else {
            var ids = AJS.$.map(review.frxs(), function (frx) {
                return '#frxouter' + frx.id();
            });
            ids.push("#generalComments");
            var $frxs = AJS.$(ids.join());
            // We always have at least the general comments.
            if (currentFrxId) {
                if (AJS.$.inArray(cruFrxNav.getCurrentFrxElement(), $frxs.get()) < 0) {
                    cruFrxNav.setCurrentFrx($frxs[0]);
                }
            } else {
                cruFrxNav.setCurrentFrx($frxs[0]);
            }
        }
    };

    AJS.CRU.FRX.NAV.getCurrentFrxId = function () {
        return currentFrxId;
    };

    AJS.CRU.FRX.NAV.getCurrentFrxElement = function () {
        if (currentFrxId) {
            var domId = currentFrxId === 'generalComments' ? 'generalComments' : 'frxouter' + currentFrxId;
            return AJS.$('#' + domId)[0];
        } else {
            return null;
        }
    };

    var defaultOpts = {
        destination : 'first',
        skipCompleteFrxs : false
    };

    /**
     * @param {String} opts.destination location to scroll to - may be <tt>first</tt>, <tt>last</tt>, <tt>previous</tt> or <tt>next</tt>.
     * @param {String} opts.frxId the frx id of the frx to scroll to. This value is ignored if the <tt>opts.destination</tt> is <tt>first</tt> or <tt>last</tt>.
     * @param {Boolean} opts.skipCompleteFrxs if true, frxs which have been marked as complete will be passed over and ignored.
     */
    AJS.CRU.FRX.NAV.gotoFrx = function (opts) {
        var frx = review.frx(opts.frxId);
        // if the frx is loaded, just go to it
        if( !frx || frx.isLoaded() ) {
            scrollToFrx(opts);
            return;
        }

        // otherwise, we must make sure to load the frx first
        var onComp = function() {
            commentator.setCommentWidths(null,true);
            scrollToFrx(opts);
        };
        AJS.CRU.FRX.AJAX.prioritisedFrxLoad(opts.frxId, onComp);
    };

    AJS.CRU.FRX.NAV.getGotoFrxId = function (opts) {
        return getScrollToFrxId(opts);
    };

    /**
     * Scroll to a comment form if one exists inside the current frx
     *
     */
    AJS.CRU.FRX.NAV.scrollToCommentForm = function() {
        var frxId = AJS.CRU.FRX.NAV.getCurrentFrxId();

        //if there is a comment form open, scroll to it
        var forms = commentator.getDisplayingForms();
        for (var i = 0, len = forms.length; i < len; i++) {
            var $form = forms[i].getForm();
            var $frxouter = $form.closest('.frxouter');
            if ($frxouter.length > 0) {
                var commentFormFrxId = $frxouter.attr('id').replace('frxouter', '');
                if (commentFormFrxId === frxId) {
                    //scroll to it
                    AJS.$('#frx-pane').scrollTo($form, {
                        axis: 'y',
                        offset: -100
                    });
                    $form.find('.commentTextarea').focus();
                    break;
                }
            }
        }
    };

    AJS.CRU.FRX.NAV.checkFrxAnchor = function () {
        var frxId;
        if (/^#CFR-\d+/.test(window.location.hash)) {
            frxId = window.location.hash.replace(/^#CFR-(\d+).*$/, '$1');
            if (review.frx(frxId)) {
                AJS.CRU.FRX.NAV.setCurrentFrxAndScroll(frxId);
                return frxId;
            } else {
                window.location.hash = '';
                AJS.CRU.FRX.NAV.setCurrentFrxAndScroll('generalComments');
            }
        }
    };

    var scrollToFrx = function (settings) {
        var frxId = getScrollToFrxId(settings);
        if (frxId) {
            AJS.CRU.FRX.NAV.setCurrentFrxAndScroll(frxId);
        }
    };

    var getScrollToFrxId = function (settings) {
        var opts = {};

        AJS.$.extend(opts, defaultOpts, settings);

        var frxIds = AJS.$.merge(["generalComments"],
                AJS.$.grep(review.frxIds(), function (frxId) {
                    return !review.frx(frxId).isFiltered();
                })
            );

        if (!frxIds || frxIds.length === 0) {
            return null;
        }

        var destinationFrxId = null;
        var destination = opts.destination;
        var skipCompleteFrxs = opts.skipCompleteFrxs;

        if (destination === 'first' && !skipCompleteFrxs) {
            destinationFrxId = frxIds[0];
        } else if (destination === 'last' && !skipCompleteFrxs) {
            destinationFrxId = frxIds[frxIds.length-1];
        } else {
            // Find the source frx index in the list of frxs
            var index = -1;
            if (destination === 'last') {
                index = frxIds.length;
            } else if (destination !== 'first') {
                for( var j = 0, len = frxIds.length; j < len; j++ ) {
                    if( opts.frxId == frxIds[j] ) {
                        index = j;
                        break;
                    }
                }
            }

            // If we're iterating over unread comments, we want to keep jumping comments
            // when the comment is marked as 'read'.
            do {
                if (destination == 'previous' || destination == 'last') {
                    if (frxIds[index-1]) {
                        destinationFrxId = frxIds[index-1];
                        index--;
                    } else {
                        destinationFrxId = skipCompleteFrxs ? destinationFrxId : frxIds[index];
                        break;
                    }
                } else if (destination == 'next' || destination == 'first') {
                    if (frxIds[index+1]) {
                        destinationFrxId = frxIds[index+1];
                        index++;
                    } else {
                        destinationFrxId = skipCompleteFrxs ? destinationFrxId : frxIds[index];
                        break;
                    }
                } else {
                    destinationFrxId = frxIds[index];
                    break;
                }
                var frx = review.frx(destinationFrxId);
                var isFrxComplete = frx && frx.isComplete();
            } while( skipCompleteFrxs && isFrxComplete );
        }

        return destinationFrxId;
    };
})();

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

    // namespaces
    var $ = AJS.$,
        cruFrx = AJS.CRU.FRX,
        cruFrxNav = cruFrx.NAV;

    if ($('#reviewpage').length === 0) {
        return;
    }

    cruFrxNav.scroller = null;

    var $pane = AJS.$('#frx-pane'),
        paneOffset = $pane.offset(),
        prevTopFrxId = null;

    function stickFrxHeader(frxId, adjustment) {
        $('#frx-header-container-' + frxId).addClass('sticky-header');
        $('#frx-header-' + frxId).css({
            top: paneOffset.top - adjustment
        });
    }

    function unstickFrxHeader(frxId) {
        $('#frx-header-container-' + frxId).removeClass('sticky-header');
        $('#frx-header-' + frxId).css({
            top: ''
        });
    }


    function makeFrxHeadersSticky() {
        if (cruFrxNav.scroller !== null) {
            var topFrxElem = cruFrxNav.scroller.getAbsoluteTopElement();
            var topFrxId = topFrxElem ? topFrxElem.id.replace('frxouter', '') : null;
            if (prevTopFrxId && prevTopFrxId !== topFrxId) {
                unstickFrxHeader(prevTopFrxId);
            }

            var adjustment = 0;
            var topHeaderHeight = $('#frx-header-container-' + topFrxId).height();
            var secondFromTopFrxElem = cruFrxNav.scroller.getAbsoluteTopElement(topHeaderHeight + 2);
            if (secondFromTopFrxElem !== topFrxElem) {
                var diff = $(secondFromTopFrxElem).offset().top - paneOffset.top;
                adjustment = 0 < diff && diff < topHeaderHeight ? topHeaderHeight - diff : 0;
            }

            stickFrxHeader(topFrxId, adjustment);
            prevTopFrxId = topFrxId;
        }
    };


    var resizeFrxHeaders = (function () {

        var stickyOffsetStyle = null,
            singleFrxOffsetStyle = null,
            widthStyle = null;

        // initialize the style objects
        (function () {
            function makeCssRule(selector, ruleBody) {
                var styleSheet = document.styleSheets[0];
                var index = 0;
                if (styleSheet.insertRule) {
                    styleSheet.insertRule(selector + '{' + ruleBody + '}', index);
                    return styleSheet.cssRules[index].style;
                } else {
                    styleSheet.addRule(selector, ruleBody, index);
                    return styleSheet.rules[index].style;
                }
            }

            var offsetRuleBody = 'top: 0px; left: 0px;';

            // IE only accepts one selector per rule, so separate these two.
            stickyOffsetStyle = makeCssRule('.frx-header-container.sticky-header .frx-header', offsetRuleBody);
            singleFrxOffsetStyle = makeCssRule('.single-frx-view .frx-header-container .frx-header', offsetRuleBody);

            widthStyle = makeCssRule('.frx-header', 'width: 0px;');
        })();

        // actual function implementation
        return function () {
            var $shield = $('#content-shield')
                .show();
            var scrollTop = $pane.scrollTop();
            var $frxContent = $('#frx-content')
                .hide();

            // setTimeout to make the browser actually show the shield.
            setTimeout(function () {
                stickyOffsetStyle.top = singleFrxOffsetStyle.top = (paneOffset.top) + 'px';
                stickyOffsetStyle.left = singleFrxOffsetStyle.left = (paneOffset.left + 1) + 'px';
                widthStyle.width = ($pane.width() - $.getScrollbarWidth()) + 'px';

                $frxContent.show();
                $pane.scrollTop(scrollTop);
                $shield.hide();
            }, 0);
        };
    })();

    cruFrxNav.enableFrxScrollTracking = function () {
        if (cruFrxNav.scroller === null) {
            cruFrxNav.scroller = AJS.CRU.WIDGETS.makeScrollTracker({
                container: $("#frx-pane")[0],
                threshold: 70,
                selector: function () {
                    var ids = $.map(review.frxs(), function (frx) {
                        return '#frxouter' + frx.id();
                    });
                    ids.push("#generalComments");
                    return $(ids.join()).filter(':visible');
                },
                active: function () {
                    cruFrxNav.setCurrentFrx(this);
                }
            });

            $pane.scroll(makeFrxHeadersSticky);
        }
    };

    cruFrxNav.disableFrxScrollTracking = function () {
        $pane.unbind('scroll', makeFrxHeadersSticky);
        prevTopFrxId = null;
        cruFrxNav.scroller = null;
    };

    $(window).bind('panes-resized', function () {
        paneOffset = $pane.offset();
        resizeFrxHeaders();
        makeFrxHeadersSticky();
    });

    // Prevent the sticky header from sticking out the side of the FRX pane when resizing.
    (function () {
        var $header = null;
        $("#content-resizable")
            .bind('resizestart', function () {
                if ($('body').is('.single-frx-view')) {
                    $header = $('#frx-header-' + cruFrxNav.getCurrentFrxId());
                } else {
                    var frxHeaderContainerIds = $.map(review.frxIds(), function (frxId) {
                        return '#frx-header-container-' + frxId;
                    });

                    $header = $(frxHeaderContainerIds.length > 0 ? frxHeaderContainerIds.join(',') : [])
                        .filter('.sticky-header')
                        .find('.frx-header');
                }

                if ($header.length === 1) {
                    // Need to set absolute position separately to get the correct offsetParent.
                    $header.css('position', 'absolute');
                    $header.css({
                        top: $header.position().top - $header.offsetParent().offset().top,
                        left: 1
                    });
                }
            })
            .bind('resizestop', function () {
                $header.css({
                    position: '',
                    top: '',
                    left: ''
                });
            });
    })();

    if ($('body').hasClass('all-frx-view')) {
        cruFrxNav.enableFrxScrollTracking();
    };
    resizeFrxHeaders();

});
;
/* END /2static/script/cru/review/frx/frx-nav.js */
/* START /2static/script/cru/create/create.js */
if (AJS.CRU === undefined) {
    AJS.CRU = {};
}
AJS.CRU.CREATE = {};

(function() {
    AJS.CRU.CREATE.changesets = {}; // map of csid -> array of revids
    AJS.CRU.CREATE.revid2csid = {};
    AJS.CRU.CREATE.revidsInIteration = {};
    AJS.CRU.CREATE.repnameInUse = "";

    AJS.CRU.CREATE.addLatestRevision = function (frxId, revid) {
        var done = function (resp) {
            if (resp.worked) {
                AJS.$("#frx-outdated"+frxId).hide();
            }
        };
        return AJS.CRU.CREATE.addRevisionFromView('add', frxId, revid, done);
    };

    AJS.CRU.CREATE.addRevisionFromView = function (command, frxId, revid, done) {
        var cruFrx = AJS.CRU.FRX;
        var params = {
            "revid" : revid,
            "command" : command,
            "frxId" : frxId,
            "attachMethod" : "ITERATION"     // string value of Review.AttachMethod.ITERATION
        };
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/editRevisionsFromRevisionAjax/";

        var frxOuterId = 'frxouter' + frxId;
        var oldInnerHTML = AJS.$("#"+frxOuterId).html();

        cruFrx.AJAX.shelveFrxControls(frxId);
        cruFrx.dimFrx(frxId);
        var onComp = function(resp) {
            if (!resp.worked) {
                // if the request didn't work, make sure the spinner
                // is taken out, and the old text restored.
                AJS.$("#"+frxOuterId).html(oldInnerHTML);
            } else {
                if (resp.reviewDueDate) {
                    //update review due date text
                    AJS.$('#review-meta-info .review-due-text').html(resp.reviewDueDate);
                }
                if (resp.frxExists) {
                    var cruFrxAjax = cruFrx.AJAX;
                    cruFrxAjax.unshelveFrxControls(frxId);
                    cruFrxAjax.reloadFrxModel(frxId, resp, false);
                    cruFrxAjax.updateFrxSlider(frxId, resp);
                    cruFrxAjax.updateFrxDiffControls(frxId, resp);
                    cruFrxAjax.updateFrxEditRevisionsDropdown(frxId);
                } else {
                    AJS.CRU.CREATE.removeFrxFromPage(frxId);
                }
            }
            if (done) {
                done({worked: resp.worked});
            }
        };
        AJS.FECRU.AJAX.ajaxUpdate(url, params, frxOuterId, onComp);
        return false;
    };

    AJS.CRU.CREATE.removeFrxFromPage = function(frxId) {
        //remove frx
        var frx = review.frx(frxId);
        var currentlyViewing;
        var currentFrxElem = AJS.CRU.FRX.NAV.getCurrentFrxElement();
        if (currentFrxElem) {
            var currentFrxElementId = AJS.$(currentFrxElem).attr('id');
            currentlyViewing = frxId === (currentFrxElementId !== 'generalComments' ? currentFrxElementId.replace(/^frxouter/, '') : currentFrxElementId);
        }
        review.removeFrx(frx);
        var $toRemove = AJS.$('#frx-list-item' + frxId);
        var $selectNext;
        var $toRemoveFolderSpan = $toRemove.children(".folder");
        if ($toRemoveFolderSpan.length > 0) {
            //if this is an frx with children, dont remove it, just remove the classes on it
            $toRemove.removeClass("activeFrx");
            $toRemoveFolderSpan.html($toRemoveFolderSpan.children(".frx-dir-item").html()).removeClass("frx-incomplete");
            if (currentlyViewing) {
                $selectNext = $toRemove.find('.frx:first');
            }
        } else {
            while ($toRemove.hasClass('frx-list-item') && $toRemove.siblings().length === 0) {
                if ($toRemove.parent().attr('id') === 'tree-root') {
                    break;
                }
                var _$toRemove = $toRemove.parents('.frx-list-item:first');
                if (_$toRemove.length === 1) {
                    $toRemove = _$toRemove;
                } else {
                    break;
                }
            }
            //get next frx to select
            if (currentlyViewing) {
                $selectNext = $toRemove.next().find('.frx:first');
            }
            if ($toRemove) {
                $toRemove.remove();
            }
        }

        if (currentlyViewing) {
            if ($selectNext.length !== 1) {
                $selectNext = AJS.$('#navigation-tree .frx').filter(':last');
            }
            if ($selectNext.length !== 1) {
                AJS.CRU.CREATE.removeActiveFrx();
                AJS.$('#scroll-to-general-comments').click();
                AJS.$('#frx-pane').scrollTo(AJS.$('#generalComments'));
            } else {
                $selectNext.find('.scroll-to-frx').click();
                var newfrxId = $selectNext.find('.scroll-to-frx').attr('id').split('scroll-to-frx')[1];
                AJS.$('#frx-pane').scrollTo(AJS.$('#frxouter'+newfrxId));
            }
        }
        AJS.$('#frxouter' + frxId).remove();
        AJS.$('#frxControlsContainer' + frxId).remove();

        AJS.CRU.FRX.NAV.visibleFrxsChanged();
        AJS.CRU.COMMENT.NAV.visibleCommentsChanged();
    };

    AJS.CRU.CREATE.retrieveNewFrxs = function(done) {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/retrieveNewFrxsAjax/";

        var onComp = function (resp) {
            if (resp.worked) {
                var activeTreeElem = AJS.$('#navigation-tree .activeFrx').attr('id');
                AJS.$('#navigation-tree').html(resp.navTree);
                AJS.$('#navigation-tree .activeFrx').removeClass('activeFrx');
                AJS.$('#' + activeTreeElem).addClass('activeFrx');
                AJS.$('#emptyNote').hide();

                var insertAfter = 'generalComments';
                var frxs = resp.frxs;
                var frxIds = [];
                for (var i = 0, len = frxs.length; i < len; i++) {
                    var frxId = frxs[i].frxId;
                    var frxHtml = frxs[i].frxHtml;
                    if (!review.frx(frxId)) {
                        AJS.$(frxHtml).insertAfter('#' + (insertAfter !== 'generalComments' ? 'frxouter' + insertAfter : insertAfter));
                        eval('var frxModelLoader = ' + frxs[i].frxModelLoader);
                        frxModelLoader(insertAfter);
                        frxIds.push(frxId);
                    }
                    if (review.frx(frxId)) {
                        insertAfter = frxId;
                    }
                }

                if (done) {
                    done();
                }

                AJS.CRU.FRX.AJAX.prioritisedFrxLoad(frxIds);
            }
        };

        AJS.FECRU.AJAX.ajaxUpdate(url, {}, null, onComp);
        return false;

    };

    var addRemoveRevisionToIter = function (add, revid) {
        var busyid = "busyRev" + revid;
        var nochangeid = "";
        var $revPath = AJS.$("#revPath" + revid);
        toggleNodeAndImage(busyid, true, false, true);
        if (add) {
            toggleNodeAndImage("addRev" + revid, false, true, true);
            nochangeid = "addRev" + revid;
            $revPath.css("textDecoration","");
        } else {
            toggleNodeAndImage("remRev" + revid, false, true, true);
            nochangeid = "remRev" + revid;
            $revPath.css("textDecoration","line-through");
        }
        var params = {
            "revid" : revid,
            "command" : add ? "add" : "remove",
            "attachMethod" : AJS.$("#attachMethod").val(),
            "fromRevision" : AJS.$("#fromRevision").val()
        };
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/editRevisionsAjax/";

        return doAddRemoveCall(url, params, busyid, nochangeid);
    };

    AJS.CRU.CREATE.addRevisionToIter = function (revid) {
        addRemoveRevisionToIter(true, revid);
    };

    AJS.CRU.CREATE.removeRevisionFromIter = function (revid) {
        addRemoveRevisionToIter(false, revid);
    };

    AJS.CRU.CREATE.addAllSearchRevisions = function (url) {
        AJS.$('#addAllBusy').css('display','inline');
        url += "&command=addSearch";
        url += "&search=" + AJS.$("#qlStr").val();
        url += "&spage=" + AJS.$("#results.thisPageNum").val();
        url += "&attachMethod=" + AJS.$("#attachMethod").val();
        url += "&fromRevision=" + AJS.$("#fromRevision").val();
        window.location=url;
    };

    var doAddMultipleCall = function (url, params) {
        var done = function(resp) {
            if (!resp.worked || AJS.FECRU.AJAX.checkError(resp)) {
                return false;
            }
            updateRespMsgBusy(resp.msgHtml);

            var updatedChangesets = {};

            AJS.$.each(resp.addedRevids, function() {
                var addedRevid = this;
                updatedChangesets[AJS.CRU.CREATE.revid2csid[addedRevid]] = 1;
                AJS.CRU.CREATE.revidsInIteration[addedRevid] = 1;
                toggleNodeAndImage("addRev" + addedRevid, false, true, true);
                toggleNodeAndImage("remRev" + addedRevid, true, false, true);
            });
            for (var csid in updatedChangesets) {
                updateChangesetAddRemove(csid);
            }
        };

        AJS.$.post(url, params, done, "json");
    };

    AJS.CRU.CREATE.addRevisionsToIter = function (permaId, revIdArray) {
        AJS.$.each(revIdArray, function() {
            AJS.$("#revPath" + this).css("textDecoration","");
        } );
        var params = {
            "revid" : revIdArray,
            "command" : "add",
            "attachMethod" : AJS.$("#attachMethod").val(),
            "fromRevision" : AJS.$("#fromRevision").val()
        };
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/editRevisionsAjax/";

        return doAddMultipleCall(url, params);
    };


    var doAddRemoveCall = function (url, params, busyid, nochangeid) {
        var done = function(resp) {
            if (!resp.worked || AJS.FECRU.AJAX.checkError(resp)) {
                toggleNodeAndImage(busyid, false, true, true);
                toggleNodeAndImage(nochangeid, true, false, true);
                return false;
            }
            updateRespMsgBusy(resp.msgHtml);

            var updatedChangesets = {};

            toggleNodeAndImage(busyid, false, true, true);
            toggleNodeAndImage(nochangeid, true, false, true);

            AJS.$.each(resp.addedRevids, function() {
                updatedChangesets[AJS.CRU.CREATE.revid2csid[this]] = 1;
                AJS.CRU.CREATE.revidsInIteration[this] = 1;
                toggleNodeAndImage("addRev" + this, false, true, true);
                toggleNodeAndImage("remRev" + this, true, false, true);
            } );
            AJS.$.each(resp.removedRevids, function() {
                updatedChangesets[AJS.CRU.CREATE.revid2csid[this]] = 1;
                AJS.CRU.CREATE.revidsInIteration[this] = 0;
                toggleNodeAndImage("addRev" + this, true, false, true);
                toggleNodeAndImage("remRev" + this, false, true, true);
            } );

            for (var csid in updatedChangesets) {
                updateChangesetAddRemove(csid);
            }
        };

        AJS.$.post(url, params, done, "json");
    };

    var updateChangesetAddRemove = function (csid) {
        var revids = AJS.CRU.CREATE.changesets[csid];
        if (!revids) {
            return;
        }
        var inCount = 0;
        AJS.$.each(revids, function() {
            if (AJS.CRU.CREATE.revidsInIteration[this] == 1) {
                inCount++;
            }
        } );
        var showAdd = inCount < revids.length;
        var showRemove = inCount > 0;
        var showSome = showAdd && showRemove;
        if (showSome) {
            showAdd = false;
            showRemove = false;
        }
        toggleNodeAndImage("addCs" + csid, showAdd, !showAdd, true);
        toggleNodeAndImage("remCs" + csid, showRemove, !showRemove, true);
        toggleNodeAndImage("containsSome" + csid, showSome, !showSome, true);
    };

    AJS.CRU.CREATE.addChangesetToIter = function (csid) {
        addRemoveChangesetToIter(true, csid);
    };

    AJS.CRU.CREATE.removeChangesetFromIter = function (csid) {
        addRemoveChangesetToIter(false, csid);
    };

    var addRemoveChangesetToIter = function (add, csid) {
        var busyid = "busyCs" + csid;
        var nochangeid = "";
        if (!add) {
            nochangeid = "remCs" + csid;
        } else if (AJS.$("#addCs" + csid).is(":hidden")) {
            nochangeid = "containsSome" + csid;
        } else {
            nochangeid = "addCs" + csid;
        }
        toggleNodeAndImage(nochangeid, false, true, true);
        toggleNodeAndImage(busyid, true, false, true);

        var params = {
            "csid" : csid,
            "command" : add ? "add" : "remove",
            "sourceName" : AJS.CRU.CREATE.repnameInUse,
            "attachMethod" : AJS.$("#attachMethod").val(),
            "fromRevision" : AJS.$("#fromRevision").val()
        };

        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/editRevisionsAjax/';

        doAddRemoveCall(url, params, busyid, nochangeid);
    };

    AJS.CRU.CREATE.removeAllRevs = function () {
        var pars = 'command=removeAll&sourceName=' + AJS.CRU.CREATE.repnameInUse;
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/editRevisionsAjax/';
        doAddRemoveCall(url, pars);
    };

    AJS.CRU.CREATE.addSearchFileRevToReview = function (revisionId, repName) {
        addRemoveRevisionToIter(true, revisionId, repName);
    };

    AJS.CRU.CREATE.removeSearchFileRevToReview = function (revisionId, repName) {
        addRemoveRevisionToIter(false, revisionId, repName);
    };

    AJS.CRU.CREATE.dirListRevs = [];

    //this is for the file browse tab
    AJS.CRU.CREATE.addFileRevisionToReview = function (latestRevId) {
        var newSelectedRevId = document.forms["selectRevisionForm" + latestRevId].revId.value;
        addRemoveFileRevisionReview(true, newSelectedRevId, latestRevId);
    };

    AJS.CRU.CREATE.removeFileRevisionFromReview = function (latestRevId) {
        var newSelectedRevId = document.forms["selectRevisionForm" + latestRevId].revId.value;
        addRemoveFileRevisionReview(false, newSelectedRevId, latestRevId);
    };

    var addRemoveFileRevisionReview = function (add, revid, latestRevId, onEval) {
        var busyid = "busyRev" + latestRevId;
        var nochangeid = "";
        toggleNodeAndImage(busyid, true, false, true);
        if (add) {
            toggleNodeAndImage("addRev" + latestRevId, false, true, true);
            nochangeid = "addRev" + latestRevId;
        } else {
            toggleNodeAndImage("remRev" + latestRevId, false, true, true);
            nochangeid = "remRev" + latestRevId;
        }

        var params = {
            "revid" : revid,
            "command" : add ? "add" : "remove",
            "attachMethod" : AJS.$("#attachMethod").val(),
            "fromRevision" : AJS.$("#fromRevision").val()
        };
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/editRevisionsAjax/';

        return doAddRemoveFRCall(url, params, busyid, nochangeid, latestRevId, onEval);
    };

    var doAddRemoveFRCall = function (url, params, busyid, nochangeid, latestRevId, onEval) {
        var done = function(resp) {
            try {
                // take out the spinner image regardless of what the response is
                toggleNodeAndImage(busyid, false, true, true);
                toggleNodeAndImage(nochangeid, true, false, true);
                if (!resp.worked || AJS.FECRU.AJAX.checkError(resp)) {
                    return false;
                }
                updateRespMsgBusy(resp.msgHtml);
                //todo fix this looks wrong
                for (var i = 0; i < resp.removedRevids.length; i++) {
                    AJS.CRU.CREATE.dirListRevs[latestRevId] = "";
                    toggleNodeAndImage("addRev" + latestRevId, true, false, true);
                    toggleNodeAndImage("remRev" + latestRevId, false, true, true);
                }
                for (i = 0; i < resp.addedRevids.length; i++) {
                    AJS.CRU.CREATE.dirListRevs[latestRevId] = resp.addedRevids[i];
                    toggleNodeAndImage("addRev" + latestRevId, false, true, true);
                    toggleNodeAndImage("remRev" + latestRevId, true, false, true);
                }
                if (onEval) {
                    onEval();
                }
            } catch (error) {
                window.alert(error);
            }
        };

        AJS.$.post(url, params, done, "json");
    };

    var loadFullHistoryDropdown = function (latestRevId) {
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/loadFullHistoryDropdownAjax/';
        var params = { "latestRevId" : latestRevId };
        var elementIdToUpdate = "selectDiff" + latestRevId;

        return AJS.FECRU.AJAX.ajaxUpdate(url, params, elementIdToUpdate);
    };

    AJS.CRU.CREATE.updateSelectedRev = function (latestRevId) {
        var selectBox = document.forms["selectRevisionForm" + latestRevId].revId;
        var formval = selectBox.value;
        if (formval == "__LOADFULL__") {
            AJS.FECRU.AJAX.startSpin(selectBox, true, "span");
            loadFullHistoryDropdown(latestRevId);
            return;
        }
        var onEval = function() {
            addRemoveFileRevisionReview(true, formval, latestRevId );
        };
        addRemoveFileRevisionReview(false, AJS.CRU.CREATE.dirListRevs[latestRevId], latestRevId, onEval);
    };

    /* patch file revision functions */
    AJS.CRU.CREATE.patches = [];
    AJS.CRU.CREATE.patchesInc = [];
    AJS.CRU.CREATE.patchRevs = [];
    AJS.CRU.CREATE.incPatchRevs = [];

    AJS.CRU.CREATE.addAllPatch = function (sourceName) {
        addRemoveAllPatch(true, sourceName); //todo fix me PATCH: is a kludge
    };

    AJS.CRU.CREATE.removeAllPatch = function (sourceName) {
        addRemoveAllPatch(false, sourceName); //todo fix me PATCH: is a kludge
    };

    // must match Source.SEPARATOR
    AJS.CRU.CREATE.SOURCE_SEPARATOR = ":";

    var addRemoveAllPatch = function (add, sourceName) {
        var patchID = sourceName.split(AJS.CRU.CREATE.SOURCE_SEPARATOR)[1];
        var incRevs = AJS.CRU.CREATE.patchesInc[sourceName];
        var revs = AJS.CRU.CREATE.patches[sourceName];
        AJS.$("#addAll" + patchID).hide();
        AJS.$("#remAll" + patchID).hide();
        AJS.$("#containsSome" + patchID).hide();
        AJS.$("#busy" + patchID).show();

        var params = {
            "revid" : AJS.CRU.CREATE.patches[sourceName],
            "command" : add ? "add" : "remove"
        };

        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + '/editRevisionsAjax/';

        var done = function(resp) {
            var remRev = function(revId) {
                incRevs[revId] = "";
            };
            var addRev = function(revId) {
                incRevs[revId] = revId;
            };
            updateRespMsgBusy(resp, "busy" + patchID);
            if (resp.worked) {
                updateRevTicks(resp.removedRevids, true, remRev);
                updateRevTicks(resp.addedRevids, false, addRev);
            }
            AJS.CRU.CREATE.setAddRemAll(patchID, incRevs, revs);
        };

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

    AJS.CRU.CREATE.setAddRemAll = function (patchID, incRevs, revs) {
        var $addAll = AJS.$("#addAll" + patchID).hide();
        var $remAll = AJS.$("#remAll" + patchID).hide();
        var $hasSome = AJS.$("#containsSome" + patchID).hide();

        var count = countSelected(incRevs);
        if (count == 0) {
            $addAll.show();
        } else if (count < revs.length) {
            $hasSome.show();
        } else {
            $remAll.show();
        }
    };

    var countSelected = function (incRevs) {
        if (!incRevs) {
            return 0;
        }

        var count = 0;
        for (var i = 0, len = incRevs.length; i < len; i++) {
            if (incRevs[i]) {
                count++;
            }
        }
        return count;
    };

    var updateRespMsgBusy = function (resp, busyId) {
        if (resp.worked) {
            AJS.$("#cart").html(resp.msgHtml);
        }
        if (busyId) {
            AJS.$("#"+busyId).hide();
        }
    };

    var updateRevTicks = function (ids, removed, forEachFunc) {
        AJS.$.each(ids, function() {
            if (removed) {
                AJS.$("#addRev" + this).show();
                AJS.$("#remRev" + this).hide();
            } else {
                AJS.$("#addRev" + this).hide();
                AJS.$("#remRev" + this).show();
            }
            if (forEachFunc) {
                forEachFunc(this);
            }
        });
    };

    AJS.CRU.CREATE.addRemoveFileRevision = function (add, revid, imgPostfix, sourceName) {
        var create = AJS.CRU.CREATE;
        var patchID = sourceName.split(create.SOURCE_SEPARATOR)[1];
        var incRevs = create.patchesInc[sourceName];
        var revs = create.patches[sourceName];
        var busyId = "busyRev" + imgPostfix;
        AJS.$("#addRev" + imgPostfix).hide();
        AJS.$("#remRev" + imgPostfix).hide();
        AJS.$("#"+busyId).show();

        var params = {
            "revid" : revid,
            "command" : add ? "add" : "remove"
        };

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

        var done = function(resp) {
            var remRev = function(revId) {
                if (incRevs) {
                    incRevs[revId] = "";
                }
            };
            var addRev = function(revId) {
                if (incRevs) {
                    incRevs[revId] = revId;
                }
            };
            updateRespMsgBusy(resp, busyId);
            if (resp.worked) {
                updateRevTicks(resp.removedRevids, true, remRev);
                updateRevTicks(resp.addedRevids, false, addRev);
            } else {
                if (add) {
                    AJS.$("#addRev" + imgPostfix).show();
                } else {
                    AJS.$("#remRev" + imgPostfix).show();
                }
            }
            create.setAddRemAll(patchID, incRevs, revs);
        };

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

    AJS.CRU.CREATE.storeStickyPreference = function (key, value) {
        var params = {
            "key" : key,
            "value" : value
        };
        var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/storeStickyPreferenceAjax/";

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

    AJS.CRU.CREATE.setSpecificDiffVisible = function (attachMethod) {
        if (attachMethod === 'SPECIFIC_DIFF') {
            AJS.$('#specificDiff').show();
        } else {
            AJS.$('#specificDiff').hide();
        }
        AJS.CRU.CREATE.storeStickyPreference('attachMethod', attachMethod);
    };

    var editDetailsRolesChange = false;

    AJS.CRU.CREATE.submitDetailsForm = function () {
        //reload the entire page for now - wont have to refresh when we can update via polling
        var refreshPage = function() {
            window.location = AJS.CRU.UTIL.urlBase(permaId);
        };
        AJS.CRU.REVIEW.UTIL.postEditDetailsForm(refreshPage);
    };

    AJS.CRU.CREATE.submitDetailsFormAndStart = function (done) {
        //reload the entire page for now - wont have to refresh when we can update via polling
        var approve = function() {
            if (done) {
                done();
            }
            AJS.CRU.UTIL.stateTransition('action:approveReview', permaId);
        };
        AJS.CRU.REVIEW.UTIL.postEditDetailsForm(approve);
    };

    AJS.CRU.CREATE.markChanged = function (rolesChanged) {
        AJS.CRU.UTIL.editDetailsFormChange = true;
        if (rolesChanged) {
            editDetailsRolesChange = true;
        }
    };

    AJS.CRU.CREATE.checkEditForm = function (done) {
        return AJS.CRU.UTIL.checkEditForm(done);
    };

    AJS.CRU.CREATE.checkEditAndSubmitThis = function (formElement) {
        var submitter = function() {
            formElement.form.submit();
        };
        AJS.FECRU.UI.swapDatesIfReversed(function(dateString) {
            return dateString.substring(0, dateString.indexOf("T"));
        });
        AJS.CRU.CREATE.checkEditForm(submitter);
    };

    AJS.CRU.CREATE.command = function (cmd, pid, button) {
        AJS.CRU.UTIL.command(cmd, pid, button);
    };

    //This validates the saneness of the team selection removing and disabling the
    //selection of author and moderator as reviewers.
    AJS.CRU.CREATE.validateTeamSelection = function () {
        var form = document.editDetailsForm;
        var mod = form.newModerator.value;
        var auth = form.newAuthor.value;
        var reviewers = form.reviewers;
        if (reviewers) {
            for (var i = 0, len = reviewers.length; i < len; i++) {
                var reviewer = reviewers[i];
                if (reviewer.value == mod || reviewer.value == auth) {
                    reviewer.checked = false;
                    reviewer.disabled = true;
                } else {
                    reviewer.disabled = false;
                }
            }
        } else if (reviewers) {
            reviewers.checked = false;
            reviewers.disabled = true;
        }
    };

    AJS.CRU.CREATE.addInvitee = function () {
        var $input = AJS.$("#inviteeInput");
        var invitee = $input.val();
        if (!invitee) {
            return;
        }
        var $invitees = AJS.$("#inviteeSpan");

        var $newInvitee = AJS.$("<input name='invitees' type='checkbox' checked='checked' tabindex='8'>")
                .val(invitee);

        var $textSpan = AJS.$("<span>" + invitee + "; </span>");

        var $newSpan = AJS.$("<span id='inv_"+invitee+"'></span>")
                .append($newInvitee)
                .append($textSpan);

        $invitees.append($newSpan);

        AJS.CRU.UTIL.editDetailsFormChange = true;
        $input.val('');
    };
})();
;
/* END /2static/script/cru/create/create.js */
/* START /2static/script/cru/create/create-browse.js */
// File used in the manage files "browse" panel
if (AJS.CRU.CREATE === undefined) {
    AJS.CRU.CREATE = {};
}
AJS.CRU.CREATE.BROWSE = {};

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

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

    var folderToggled = function ($linkNode) {
        // open the parents
        var $node = $findLink($linkNode.attr("href"));
        $node.parents("ul.closed,span.closed").removeClass("closed").addClass("open");
        moveSelectedDirTreeIds($node);

        AJS.$.ajax({
            type: "GET",
            url: $linkNode.attr("href").replace("edit-browse", "edit-browse-filelist"), // HACK!
            success: function(msg) {
                AJS.$("#fileResults").html(msg);
            }
        });
    };

    AJS.CRU.CREATE.BROWSE.browseDirectoryPathLinkFunction = function (event) {
        var $node = AJS.$(event.target);
        var href = $node.attr("href");
        var self = AJS.CRU.CREATE.BROWSE.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, folderToggled, self);
        } else {
            AJS.FECRU.BROWSE.selectLink($node, folderToggled, self);
        }
        return false;
    };
})();;
/* END /2static/script/cru/create/create-browse.js */
/* START /2static/script/cru/create/create-event.js */
AJS.$(document).ready(function() {
    var $activeFrxTreeLI;
    var $activeFrxControl;
    // Edit-content in manage files
    AJS.$('#edit-content-container a.scroll-to-frx').live('click', function() {
        var frxId = AJS.$(this).attr('id').replace(/scroll-to-frx/, '');

        if ($activeFrxTreeLI) {
            $activeFrxTreeLI.removeClass("activeFrx");
        }
        if ($activeFrxControl) {
            $activeFrxControl.hide();
        }
        $activeFrxTreeLI  = AJS.$("#frx-list-item" + frxId).addClass("activeFrx");
        $activeFrxControl = AJS.$("#frxControlsContainer" + frxId).show();

        if ($activeFrxControl.hasClass("autoShowDiffForm")) {
            $activeFrxControl.removeClass("autoShowDiffForm");
            showDiffForm(frxId);
        }
    });

    AJS.CRU.CREATE.removeActiveFrx = function () {
        if ($activeFrxTreeLI) {
            $activeFrxTreeLI.removeClass("activeFrx");
        }
        if ($activeFrxControl) {
            $activeFrxControl.hide();
        }
    };

    AJS.$('button.removeFrx').live('click', function () {
        var $button = AJS.$(this);
        $button.attr('disabled', true);
        var idBits = $button.attr('id').split('-');
        AJS.CRU.CREATE.addRevisionFromView('remove', idBits[1], idBits[2]);
        return false;
    });

    AJS.$('.frxSlider .deleteRev').live('click', function () {
        var $x = AJS.$(this);
        var idBits = $x.attr('id').split('-');
        AJS.FECRU.AJAX.startSpin($x, false, 'span');
        $x.hide();
        AJS.CRU.CREATE.addRevisionFromView('removeRevisions', idBits[1], idBits[2], function () {
            AJS.FECRU.AJAX.stopSpin($x[0], 'span');
            $x.show();
        });
    });

    AJS.$('.addRevision').live('click', function () {
        var $elem = AJS.$(this);
        var revId = $elem.attr('id').replace(/^addRev/, '');
        AJS.CRU.CREATE.addRevisionToIter(revId);
    });

    AJS.$('.removeRevision').live('click', function () {
        var $elem = AJS.$(this);
        var revId = $elem.attr('id').replace(/^remRev/, '');
        AJS.CRU.CREATE.removeRevisionFromIter(revId);
    });

    AJS.$('.addChangeSet').live('click', function () {
        var $elem = AJS.$(this);
        var csId = $elem.attr('id').replace(/^(addCs|containsSome)/, '');
        AJS.CRU.CREATE.addChangesetToIter(csId);
    });

    AJS.$('.removeChangeSet').live('click', function () {
        var $elem = AJS.$(this);
        var csId = $elem.attr('id').replace(/^remCs|removeSomeCs/, '');
        AJS.CRU.CREATE.removeChangesetFromIter(csId);
    });

    AJS.$('.changeDiffAddRevision').live('click', function () {
        var $addButton = AJS.$(this);
        if (!$addButton.attr('disabled')) {
            var frxId = $addButton.attr('id').replace(/^changeDiffAddRevision/, '');
            var revId = AJS.$('#changeDiffSelect' + frxId).val();
            if (revId) {
                $addButton.attr('disabled', true);
                var ajax = AJS.FECRU.AJAX;
                ajax.startSpin($addButton, false, 'span', "edit-revision-spinner");
                var done = function () {
                    ajax.stopSpin($addButton[0], 'span');
                    // Disabled status of button is handled by addRevisionFromView.
                };
                AJS.CRU.CREATE.addRevisionFromView('add', frxId, revId, done);
            }
        }
    });

    AJS.CRU.CREATE.bindUpdateFrxFromFormToggle = function (selector, frx) {
        AJS.InlineDialog(
            selector + ' a.edit-revision',
            'edit-revisions-inline-dialog-form-' + frx,
            function ($container, $editRevisionLink, showPopup) {
                if ($editRevisionLink.is(".toggled-off")) {
                    return;
                }
                $editRevisionLink.addClass("toggled-off");

                var frxId = $editRevisionLink.closest('.updateFrxFromFormToggle').attr('id').replace('updateFrxFromFormToggle', '');
                var $formSpan = AJS.$('#updateFrxFromFormSpan' + frxId);

                if (!$formSpan.is('.allocated')) {
                    var url = AJS.CRU.UTIL.jsonUrlBase(permaId) + "/changeDiffDropdownAjax/";
                    var params = { frxId: frxId };
                    var ajax = AJS.FECRU.AJAX;
                    ajax.startSpin('updateFrxFromFormToggle' + frxId, false, "span", "");

                    ajax.ajaxDo(url, params, function(resp) {
                        ajax.stopSpin(AJS.$('#updateFrxFromFormToggle' + frxId)[0], 'span');
                        if (resp.worked) {
                            if (resp.showDeleteRevisions) {
                                // Enable the delete revision markers.
                                AJS.$('#frxSliderDiv-' + frxId).find('.deleteRev-hidden')
                                    .removeClass("deleteRev-hidden")
                                    .addClass("deleteRev-shown");
                            }

                            $formSpan
                                .html(resp.msgHtml)
                                .addClass('allocated');

                            $container.html($formSpan.show());
                            AJS.CRU.FRX.AJAX.updateFrxEditRevisionsDropdown(frxId);
                            showPopup();
                        }
                        $editRevisionLink.removeClass("toggled-off");
                    });
                } else {
                    $container.html($formSpan.show());
                    showPopup();
                    $editRevisionLink.removeClass("toggled-off");
                }
            },
            {
                hideDelay: 200,
                width: 520,
                offsetY: 0,
                offsetX: 0,
                //overriding posX+posY prevents inlineDialog from calculating the pos using view port coords,
                //instead, using what is inherited from the container ("#updateFrxFromFormToggle" + frx),
                //offset'ed a little to look good.
                posY: 42,
                posX: 6,
                container:"#updateFrxFromFormToggle" + frx,
                cacheContent: false,
                initCallback: function () {
                    var inlineDialog = this;
                    AJS.$(document).bind('frx-deactive.edit-revisions', function () {
                        inlineDialog.hide();
                    });
                    inlineDialog.popup.find('.close').bind('click.edit-revisions', function () {
                        inlineDialog.hide();
                    });
                },
                hideCallback: function () {
                    AJS.$(document).unbind('frx-deactive.edit-revisions');
                    var $popup = this.popup;
                    $popup.find('.close').unbind('click.edit-revisions');
                    // Stash the form span away for the next time we need to show it.
                    $popup.find('.updateFrxFromFormSpan')
                            .hide()
                            .remove()
                            .appendTo('body');
                }
            }
        );
    };
});
;
/* END /2static/script/cru/create/create-event.js */
/* START /2static/script/cru/dialog/dialog.js */
if (!AJS.CRU) {
    AJS.CRU = {};
}
AJS.CRU.DIALOG = {};

(function () {

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

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

})();
;
/* 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 () {
        AJS.$('#dialog-view-drafts').live('click', viewFiltersHandler(['draftcomments']));
        AJS.$('#dialog-view-unread-comments').live('click', viewFiltersHandler(['unreadcomments']));
        AJS.$('#dialog-view-incomplete-frxs').live('click', viewFiltersHandler(['incomplete']));
        AJS.$('#dialog-view-unresolved-jiras').live('click', viewFiltersHandler(['unresolvedsubtasks']));

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

        AJS.$('.dialog-add-to-review').live('click', function () {
            var permaId = AJS.$(this).attr('id').replace(/^dialog-add-to-review-/, '');
            var $container = AJS.CRU.DIALOG.$CONTAINER;
            $container.data('dialog').remove();
            AJS.CRU.UTIL.addToReview(AJS.$.extend($container.data('params'), {
                task: permaId
            }));
        });
    });

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

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

    ui.initStream();

    ui.setDropdowns();

    ui.augmentFormFields("#showid");

    fecru.HOVER.addAllLinkPopups();

    fecru.RSS.setupRSSDialog();

    ui.accordion();
});
;
/* END /2static/script/fecru/onReady.js */
