//*****
//* These constants must be kept in sync w/ libraries
//*****

var _minXRelationshipCommentLength = 10;
var _asyncTimeOut = 30000; // timeout for async actions

//*****
//* These are local constants
//*****

var _dialogFadeOut = 3500;

$(function() {
	$.ajaxSetup({
		timeout: _asyncTimeOut
	});
});

var _uniqueID = -1;

function UniqueID() {
    _uniqueID++;
    return _uniqueID;
}

// error message literals
var _genericPermissionDenied = "Permission denied. Check your reputation page to see if you've exceeded the daily limit for performing this action or if you're under probation.";

var _requiresModeration = "Deleting this item now requires moderation. Please refresh the page to get the option to perform moderated actions on the item";

var _deletePermissionDenied = "Permission denied. Check your reputation page to see if you've exceeded the daily limit for performing this action or if you're under probation. Also, if the item being deleted has been around for a while you will not be able to delete it directly. In this case, you can refresh the page, flag the item, and wait for moderators to take care of the issue.";

var _addRelationDenied = 'Permission denied. Check your reputation page to see if you\'ve exceeded your daily limit for this action or if you\'re under probation. If you\'re trying to add a vine, you need "Add vines" permissions. If you\'re trying to add entries to a section, you need "Edit basic information on content pages" permissions.';

var _notSignedIn = "It appears that you're no longer signed in. Refresh the page, sign in, and try again.";

var _duplicateError = "This entry duplicates the content of an existing entry. Make sure that the content you're trying to add is unique. In some cases, you cannot add new entries if you have already added an entry here before.";

var _unexpectedError = "Unexpected error.  Please try again later.";

var _timeOut = "The operation took an unexpectedly long time.  Please try again later.";

function MakeURLReadable(string) {
    return string.replace(/\W/gi, '-');
}

function GetURLDomain(url) {
    tokens = url.split('://');

    if (tokens.length > 1)
        url = tokens[1];

    tokens = url.split('/');

    if (tokens.length > 1)
        url = tokens[0];

    url = url.toLowerCase();
    if (url.substr(0, 4) == 'www.')
        url = url.substr(4);

    return url
}

function OpenInNewWindow(url) {
    var newWindow = window.open(url);

    if (window.focus) {
	newWindow.focus();
    }

    return false;
}

function OpenPopup(url, name, width, height) {
    if (width == null) {
	width = 700;
    }
    if (height == null) {
	height = 500;
    }

    var newWindow = window.open(url, name, 'height='+height+',width='+width + ',scrollbars=1');

    if (newWindow && !newWindow.opener) {
	newWindow.opener = window;
    }

    if (window.focus) {
	newWindow.focus();
    }

    return false;
}

function OpenItemDetails(itemId, escapedItemTitle, ftpc) {
    var itemTitle = null;
    if (escapedItemTitle != null)
	itemTitle = unescape(escapedItemTitle);

    var url = GetItemDetailsURL(itemId, itemTitle);
    if (ftpc != null && ftpc) {
	url += '?ftpc=1';
    }

    window.location = url;
}

function GetItemDetailsURL(itemId, itemTitle) {
    var prefix = 'page';
    if (itemTitle != null)
	prefix = MakeURLReadable(itemTitle);

    return '/details/' + prefix + '/' + itemId + '?ac=det';
}

function ToggleInput(controlID, enable) {
    var disabledValue = 'disabled';
    if (enable) {
	disabledValue = '';
    }

    $('#' + controlID).attr('disabled', disabledValue);
}

function CountLines(text) {
    lines = text.split('\n');

    return lines.length;
}

function MakeURLQuery(parameterList) {
    parameterStrings = []
    for (var i=0; i < parameterList.length; i++) {
	parameterStrings.push(parameterList[i][0] + '=' + encodeURIComponent(parameterList[i][1]));
    }

    return parameterStrings.join('&');
}

// encodes a array of strings supporting any characters so that it can be sent as a GET / POST request parameter
// and successfully decoded into an array
function encodeStringArray(stringArray) {
    var result = '';
    var ch = '';

    for (var i = 0; i < stringArray.length; i++) {
	var encodedElement = '';

	for (var j = 0; j < stringArray[i].length; j++) {
	    ch = stringArray[i].charAt(j); // must use charAt instead of array index to work in IE 7 (maybe impacts other browsers as well)

	    if (ch == '\\') {
		encodedElement += '\\\\';
	    }
	    else if (ch == '|') {
		encodedElement += '\\|';
	    }
	    else {
		encodedElement += ch;
	    }
	}

	result += encodeURIComponent(encodedElement) + '|';
    }
    
    return result;
}

function encodeNumberArray(numberArray) {
    var result = '';

    for (var i = 0; i < numberArray.length; i++) {
	result += encodeURIComponent(numberArray[i]) + '|';
    }
    
    return result;
}

function encodeBooleanArray(boolArray) {
    var result = '';

    for (var i = 0; i < boolArray.length; i++) {
	if (boolArray[i]) {
	    result += '1|';
	}
	else {
	    result += '0|';
	}
    }
    
    return result;
}

// poor man's implementation
function unescapeCharacterEntities(aString) {
    aString = aString.replace(/&lt;/, "<");
    aString = aString.replace(/&gt;/, ">");
    aString = aString.replace(/&quot;/, '"');
    aString = aString.replace(/&amp;/, "&"); //be careful - this must come last to avoid double unescaping the others

    return aString;
}

// returns window [width, height] array on most browsers or [0, 0] on unsupported browsers
function windowSize() {
    var width = 0;
    var height = 0;

    if (window.innerWidth != null)
	{
	    width = window.innerWidth;
	}
    else if (document.documentElement && document.documentElement.clientWidth)
	{
	    width = document.documentElement.clientWidth;
	}
    else if (document.body != null)
	{
	    width = document.body.clientWidth;
	}

    if (window.innerHeight != null)
	{
	    height = window.innerHeight;
	}
    else if (document.documentElement && document.documentElement.clientHeight)
	{
	    height = document.documentElement.clientHeight;
	}
    else if (document.body != null)
	{
	    height = document.body.clientHeight;
	}

    return [width, height];
}

// returns window's offset relative to page as [horizontalMin, horizontalMax, verticalMin, verticalMax] array on most browsers or 0's array on unsupported browsers
function windowOffset() {
    var hMin = 0;
    var hMax = 0;
    var vMin = 0;
    var vMax = 0;

    if (typeof window.pageXOffset != 'undefined') {
	hMin = window.pageXOffset;
    }
    else if (document.documentElement && document.documentElement.scrollLeft) {
	hMin = document.documentElement.scrollLeft;
    }
    else if (document.body.scrollLeft) {
	hMin = document.body.scrollLeft;
    }

    if (typeof window.pageYOffset != 'undefined') {
	vMin = window.pageYOffset;
    }
    else if (document.documentElement && document.documentElement.scrollTop) {
	vMin = document.documentElement.scrollTop;
    }
    else if (document.body.scrollTop) {
	vMin = document.body.scrollTop;
    }

    var wDims = windowSize();

    hMax = hMin + wDims[0];
    vMax = vMin + wDims[1];

    return [hMin, hMax, vMin, vMax];
}

// get position relative to document
// note that IE7- and IE8 in IE7 mode may report inaccurate y coordinates within relative coordinates
function getPosition(obj) {
    var curLeft = curTop = 0;
    if (obj.offsetParent) {
	do {
	    curLeft += obj.offsetLeft;
	    curTop += obj.offsetTop;
	} while (obj = obj.offsetParent);
	return [curLeft,curTop];
    }
}

// set position relative to document for an absolutely positioned element
// note that IE7- and IE8 in IE7 mode may result in inaccurate y coordinates within relative coordinates
function setPosition(obj, x, y) {
    var originalObject = obj;

    // get containing position offset
    var curLeft = curTop = 0;
    if (obj.offsetParent) {
	obj = obj.offsetParent;
	do {
	    curLeft += obj.offsetLeft;
	    curTop += obj.offsetTop;
	} while (obj = obj.offsetParent);

	$(originalObject).css('left', x - curLeft);
	$(originalObject).css('top', y - curTop);
    }
}

function placeAtWindowBottom(jqo) {
    var obj = jqo.get(0);

    var wOffset = windowOffset();
    var windowHeight = windowSize()[1];

    var objHeight = jqo.height();

    jqo.css('top', wOffset[2] + windowHeight - objHeight - 25);
}

function hide(id) {
    $("#"+id).css("display", "none");
}

function showBlock(id) {
    $("#"+id).css("display", "block");
}

function showInline(id) {
    $("#"+id).css("display", "inline");
}

function hideDialog(id) {
    $("#"+id).css("display", "none");
}

function showDialog(id, jqo, anchorPosition) {
    if (jqo == null)
	$("#"+id).css("display", "block");

    // move the dialog to its anchor position in case it was moved before
    var anchorPosition = anchorPosition;
    if (anchorPosition == null)
	anchorPosition = getPosition($("#" + id + "_anchor").get(0));

    var jqoDialog = $("#" + id);
    setPosition(jqoDialog.get(0), anchorPosition[0], anchorPosition[1]);

    // the dialog, when it appears, must be padded from the very edges the window by this amount
    var hMargin = 35;
    var vMargin = 35;

    // check if the bottom of the dialog is below the fold
    // if so, scroll the window down until either the top of the dialog is up against the top of the window
    // or the bottom of the dialog is above the fold, whichever requires the least scroll
    var currentPosition = getPosition(jqoDialog.get(0));
    var width = $("#" + id).width();
    var height = $("#" + id).height();
    var bottom = currentPosition[1] + height;
    var wOffset = windowOffset();

    if (bottom + vMargin > wOffset[3] && currentPosition[1] > vMargin + wOffset[2]) {
     	window.scrollBy(0, Math.min(bottom + vMargin - wOffset[3], currentPosition[1] - vMargin - wOffset[2]));
    }

    // check if the right of the dialog is outside the viewable window after snapping the dialog to its default pos
    // if so, move the dialog left so that either the left of the dialog is up against the left of the window
    // or the right of the dialog is up against the right of the window, whichever requires the least move
    if (currentPosition[0] + width + hMargin > wOffset[1] && currentPosition[0] > hMargin + wOffset[0]) {
	setPosition(jqoDialog.get(0), Math.max(wOffset[1] - width - hMargin, wOffset[0] + hMargin), currentPosition[1]);
    }
}

function GetParentDialogId(caller) {
    return $(caller).parents('.dialog-container:first').attr('id');
}

function HighlightDialogCloseButton(caller, highlight) {
    var jqoButton = $(caller);

    if (highlight)
	jqoButton.css('background-position', '-102px -46px');
    else
	jqoButton.css('background-position', '-58px -46px');
}

// shows a div at the center of the window
function showAlert(id) {
    $("#"+id).css("display", "block");
    $("#"+id).css("z-index", "15");

    var windowDims = windowSize();
    var windowWidth = windowDims[0];
    var windowHeight = windowDims[1];

    var divWidth = $("#" + id).width();
    var divHeight = $("#" + id).height();

    var left = Math.round(parseFloat(windowWidth) / 2.0 - parseFloat(divWidth) / 2.0);
    var top = Math.round(parseFloat(windowHeight) / 2.0 - parseFloat(divHeight) / 2.0);

    left = Math.max(10, left);
    top = Math.max(10, top);

    var wOffset = windowOffset();

    setPosition($("#" + id).get(0), left, wOffset[2] + top);
}

function moveDialogBy(id, dx, dy) 
{
    var offset = $("#" + id).offset();
    
    // set new offset
    $("#" + id).css("left", offset.left + dx);
    $("#" + id).css("top", offset.top + dy);
}

function ClearInputTip(callingObj) {
    var jqo = $(callingObj);

    if (jqo.attr('class').indexOf('input-tip') > -1) {
	cssClass = jqo.attr('class');
	cssClass = trim(cssClass.replace('input-tip', ''));

	jqo.val('');
	jqo.attr('class', cssClass);
    }
}

//*****
//* String scripts
//*****

function AutoCapitalize(str) {
    tokens = str.split(' ');
    for (var i=0; i < tokens.length; i++) {
	token = tokens[i];
	if (token.length == 0) {
	    continue;
	}
        else if (i == 0) {
	    tokens[i] = token.charAt(0).toUpperCase() + token.substr(1);
	}
	else {
	    tokenLowered = token.toLowerCase();
	    if (tokenLowered == 'a' || tokenLowered == 'the' || tokenLowered == 'is' || tokenLowered == 'as' || tokenLowered == 'at' || tokenLowered == 'by' || tokenLowered == 'for' || tokenLowered == 'in' || tokenLowered == 'into' || tokenLowered == 'of' || tokenLowered == 'on' || tokenLowered == 'onto' || tokenLowered == 'to' || tokenLowered == 'with')
		continue;

            tokens[i] = token.charAt(0).toUpperCase() + token.substr(1);
	}
    }

    return tokens.join(' ');
}

//*****
//* Header scripts
//*****

function HighlightVertical(callingObject) {
    var jqoCaller = $(callingObject);
    jqoCaller.css('border-bottom-width', '3px');
    jqoCaller.css('border-bottom-color', 'rgb(45, 166, 214)');
    jqoCaller.css('border-bottom-style', 'solid');
}

function UnhighlightVertical(callingObject) {
    var jqoCaller = $(callingObject);
    jqoCaller.css('border-bottom-width', '');
    jqoCaller.css('border-bottom-color', '');
    jqoCaller.css('border-bottom-style', '');
}

//*****
//* Home page scripts
//*****

function SetHomePageDateTime() {
    var date = new Date();
    var dateString = date.toLocaleDateString();
    var dateTokens = dateString.split(',');
    if (dateTokens.length > 1)
	dateTokens = dateTokens.slice(1);
    dateString = trim(dateTokens.join(','));
    var timeString = date.toLocaleTimeString();
    $('#home-page-date').html(dateString);
    $('#home-page-time').html(timeString);
}

//*****
//* Smart input scripts
//*****

function UpdateHelpfulTextAreaState(input, inFocus) {
    var jqoInput = $(input);
    var jqoState = jqoInput.nextAll('.helpful-text-area-state:first');

    if (inFocus) {
	// clear out tip
	if (jqoState.val() == 'tip') {
	    jqoInput.val('');
	    if (jqoInput.hasClass('faded'))
		jqoInput.removeClass('faded');
	    if (jqoInput.hasClass('italicized'))
		jqoInput.removeClass('italicized');

	    jqoState.val('input');
	}

	// shrink / grow input as needed
	var width = parseInt(jqoInput.nextAll('.helpful-text-area-width:first').val(), 10);
	var initialHeight = parseInt(jqoInput.nextAll('.helpful-text-area-initial-height:first').val(), 10);

	var text = jqoInput.val();
	var newLines = CountLines(text);
	var estimatedHeight = 13 * (parseFloat(text.length * 6) / parseFloat(width) + newLines);
	var targetHeight = 13 * Math.ceil((estimatedHeight + 20) / 13.0);
	var targetHeight = Math.max(initialHeight, targetHeight);
	var actualHeight = jqoInput.height();
	if (actualHeight != targetHeight)
	    jqoInput.height(targetHeight);
    }
    else {
	// show tip if appropriate
	if (trim(jqoInput.val()).length == 0 && jqoState.val() == 'input') {
	    jqoInput.val('');
	    if (!jqoInput.hasClass('faded'))
		jqoInput.addClass('faded');
	    if (!jqoInput.hasClass('italicized'))
		jqoInput.addClass('italicized');
	    var jqoTip = jqoInput.nextAll('.helpful-text-area-tip:first');
	    jqoInput.val(jqoTip.html());

	    jqoState.val('tip');
	}
    }
}

function GetHelpfulTextAreaInput(jqoInput) {
    var jqoState = jqoInput.nextAll('.helpful-text-area-state:first');
    if (jqoState.val() == 'tip')
	return '';

    return jqoInput.val();
}

//*****
//* Catalog search scripts
//*****

function InitializeInlineCatalogSearch() {
    $(document).click(function(e) {
	    var target = null;
	    if (e.target) target = e.target;
	    else if (e.srcElement) target = e.srcElement;

	    if ($(target).parents('.inline-catalog-search-results:first').length == 0 && $(target).parents('.inline-catalog-search-container:first').length == 0) {
		$('.inline-catalog-search-results').each(function() {
			$(this).css('display', 'none');
		    });
	    }
	});
}

function CatalogSearch(searchInputDomId, searchNodeDomId) {
    query = $('#' + searchInputDomId).attr('value');
    if (query == undefined) {
	query = '';
    }
    node = $('#' + searchNodeDomId).attr('value');

    window.location = '/search?query=' + encodeURIComponent(query) + '&node=' + encodeURIComponent(node);
}

function RefreshInlineCatalogSearchResults(event, callingInput) {
    var jqoInput = $(callingInput);
    var inputDomId = jqoInput.attr('id');

    var jqoInputContainer = jqoInput.parents('.inline-catalog-search-container:first');
    var jqoRefinements = jqoInputContainer.nextAll('.inline-catalog-search-refinements:first');
    var resultsDiv = jqoInputContainer.nextAll('.inline-catalog-search-results:first');
    var resultsHtml = resultsDiv.html();

    var query = trim(jqoInput.val());
    var refinements = trim(jqoRefinements.val());

    if (query.length == 0)
	return;

    if (event.keyCode == 38) { // up arrow
	if (resultsHtml == null || resultsHtml.length <= 5) {
	    return;
	}

	var jqoSelected = resultsDiv.find('.inline-catalog-search-result-container-selected:first');
	if (jqoSelected.length != 0) {
	    var jqoPrevContainer = jqoSelected.prevAll('.inline-catalog-search-result-container:first');

	    jqoSelected.removeClass('inline-catalog-search-result-container-selected');

	    if (jqoPrevContainer.length != 0) {
		jqoPrevContainer.addClass('inline-catalog-search-result-container-selected');
	    }
	}
    }
    else if (event.keyCode == 40) { // down arrow
	if (resultsHtml == null || resultsHtml.length <= 5) {
	    return;
	}
	
	var jqoSelected = resultsDiv.find('.inline-catalog-search-result-container-selected:first');
	if (jqoSelected.length == 0) {
	    resultsDiv.find('.inline-catalog-search-result-container:first').addClass('inline-catalog-search-result-container-selected');
	}
	else {
	    var jqoNextContainer = jqoSelected.nextAll('.inline-catalog-search-result-container:first');

	    if (jqoNextContainer.length != 0) {
		jqoSelected.removeClass('inline-catalog-search-result-container-selected');
		jqoNextContainer.addClass('inline-catalog-search-result-container-selected');
	    }
	}
    }
    else if (event.keyCode == 13) { // enter
	var jqoSelected = resultsDiv.find('.inline-catalog-search-result-container-selected:first');
	if (jqoSelected.length == 0) {
	    jqoInputContainer.nextAll('.inline-catalog-search-bypass:first').click();
	}
	else {
	    var selectedId = jqoSelected.attr('id');

	    var cbJs = jqoInputContainer.nextAll('.inline-catalog-search-callback:first').attr('value');
	    var cbJsArgs = jqoInputContainer.nextAll('.inline-catalog-search-callback-args:first').attr('value');
	    if (cbJsArgs == undefined) {
		cbJsArgs = '';
	    }

	    resultsDiv.css('display', 'none');
    
	    if (selectedId.indexOf('result') > -1) {
		var itemId = selectedId.replace('inline-catalog-search-result-', '');
		var itemTitle = jqoSelected.find('.inline-catalog-search-result-item-title:first').attr('value');

		var cbJsFull = cbJs + "('" + itemId + "', '" + escape(itemTitle) + "'";
		if (cbJsArgs.length > 0) {
		    cbJsFull += ', ' + cbJsArgs;
		}
		cbJsFull += ');';

		eval(cbJsFull);
	    }
	    else { // create page case
		InlineCatalogSearchCreatePage(resultsDiv);
	    }
	}
    }
    else { // async search
	window.setTimeout('GetInlineCatalogSearchResults("' + inputDomId + '", "' + escape(query)+ '");', 300);
    }	
}

function InlineCatalogSearchResultMouseOver(callingObj) {
    var jqoSelected = $(callingObj);
    var resultsDiv = jqoSelected.parents('.inline-catalog-search-results:first');

    resultsDiv.find('.inline-catalog-search-result-container-selected').removeClass('inline-catalog-search-result-container-selected');

    jqoSelected.addClass('inline-catalog-search-result-container-selected');
}

function InlineCatalogSearchResultClick(callingObj, itemId) {
    var jqoSelected = $(callingObj);
    var resultsDiv = jqoSelected.parents('.inline-catalog-search-results:first');

    var cbJs = resultsDiv.nextAll('.inline-catalog-search-callback:first').attr('value');
    var cbJsArgs = resultsDiv.nextAll('.inline-catalog-search-callback-args:first').attr('value');
    if (cbJsArgs == undefined) {
	cbJsArgs = '';
    }

    resultsDiv.css('display', 'none');

    if (itemId != null) {
	var itemTitle = jqoSelected.find('.inline-catalog-search-result-item-title:first').attr('value');

	var cbJsFull = cbJs + "('" + itemId  + "', '" + escape(itemTitle) + "'";
	if (cbJsArgs.length > 0) {
	    cbJsFull += ', ' + cbJsArgs;
	}
	cbJsFull += ');';

	eval(cbJsFull);
    }
    else {
	InlineCatalogSearchCreatePage(resultsDiv);
    }
}

function InlineCatalogSearchCreatePage(resultsDiv) {
    var jqoInput = resultsDiv.prevAll('.inline-catalog-search-container:first').find('.inline-catalog-search-box:first');
    var query = trim(jqoInput.val());

    var jqoCreate = resultsDiv.nextAll('.inline-catalog-search-create-callback:first');
    if (jqoCreate.length > 0) { // custom create cb
	var createCbJs = resultsDiv.nextAll('.inline-catalog-search-create-callback:first').attr('value');
	var createCbJsArgs = resultsDiv.nextAll('.inline-catalog-search-create-callback-args:first').attr('value');
	
	var createCbJsFull = createCbJs + "(null, '" + escape(query) + "'";
	if (createCbJsArgs.length > 0) {
	    createCbJsFull += ', ' + createCbJsArgs;
	}
	createCbJsFull += ');';

	eval(createCbJsFull);
	return
    }

	
    var cbJs = resultsDiv.nextAll('.inline-catalog-search-callback:first').attr('value');
    var cbJsArgs = resultsDiv.nextAll('.inline-catalog-search-callback-args:first').attr('value');

    resultsDiv.css('display', 'none');

    CreatePageDialog(query, cbJs, cbJsArgs);
}

function GetInlineCatalogSearchResults(inputDomId, priorQueryEscaped) {
    var jqoInput = $('#' + inputDomId);

    var jqoInputContainer = jqoInput.parents('.inline-catalog-search-container:first');
    var jqoRefinements = jqoInputContainer.nextAll('.inline-catalog-search-refinements:first');
    var resultsDiv = jqoInputContainer.nextAll('.inline-catalog-search-results:first');
    var allowCreate = jqoInputContainer.nextAll('.inline-catalog-search-allow-create:first').val();

    var query = trim(jqoInput.val());
    var refinements = trim(jqoRefinements.val());
    var priorQuery = unescape(priorQueryEscaped);

    if (query != priorQuery) { // exit if the query has changed as a new setTimeout request will occur
	return;
    }

    var jqoLastQuery = jqoInputContainer.nextAll('.inline-catalog-search-last-query:first');
    var lastIssuedQuery = jqoLastQuery.val();
    if (query == lastIssuedQuery)
	return; // we've issued this query before
    jqoLastQuery.val(query);
    
    var postData = MakeURLQuery([['query', query], ['refinements', refinements], ['allowCreate', allowCreate]]);

    $.ajax({
            type: "POST",
                url: "/search/SearchInline",
                data: postData,
		error: function(request, status, error) {
		jqoLastQuery.val('');
	    },
                success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    resultsDiv.html(msg.substr(1));
		    resultsDiv.css('display', 'block');
		}
		else {
		    jqoLastQuery.val('');
		}
	    }
        });    
}

function ActivateInlineCatalogSearchResults(callingInput) {
    var resultsDiv = $(callingInput).parents('.inline-catalog-search-container:first').nextAll('.inline-catalog-search-results:first');

    if (resultsDiv.html() != null && trim(resultsDiv.html()).length > 10) {
	resultsDiv.css('display', 'block');
    }
}

function CloseInlineCatalogSearchResults(callingInput) {
    var resultsDiv = $(callingInput).parents('.inline-catalog-search-container:first').nextAll('.inline-catalog-search-results:first');

    resultsDiv.css('display', 'none');
    resultsDiv.html('&nbsp;');
}

//*****
//* User favorites scripts
//*****

function UpdateFavorites(itemId, like, controlType, callback) {
    // lexical scoping for callback
    var itemId = itemId;
    var controlType = controlType;
    var callback = callback;

    var likeString = '1';
    if (!like)
	likeString = '0';
    var postData = MakeURLQuery([["itemId", itemId], ['like', likeString]]);

    $.ajax({
            type: "POST",
                url: "/favorites/ToggleFavorite",
                data: postData,
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var currentState = like;

		    $('.favorite-button-' + itemId).removeAttr('onclick');
		    $('.favorite-button-' + itemId).unbind('click');
		    $('.favorite-button-' + itemId).click(function() {
			    UpdateFavorites(itemId, !like, controlType);
			});

		    var jqoLinks = $('.favorite-link-' + itemId);
		    if (jqoLinks.length > 0) {
			jqoLinks.removeAttr('onclick');
			jqoLinks.unbind('click');
			jqoLinks.unbind('click');
			jqoLinks.click(function() {
				UpdateFavorites(itemId, !like, controlType);
			    });

			jqoLinks.fadeOut(500);
		    }

		    $('.favorite-button-' + itemId).fadeOut(500, function() {
			    setTimeout('UpdateFavoriteButton("' + itemId + '", ' + currentState + ', "' + controlType + '")', 500);
			});

		    if (callback != null && callback.length > 0)
			eval(callback + "('" + itemId + "', " + currentState + ");");
                }
            }
        });
}

function UpdateFavoriteButton(itemId, currentState, controlType) {
    if (controlType == 'SMALL') {
	if (currentState) {
	    $('.favorite-button-' + itemId).css('background-position', '-334px -283px');
	}
	else {
	    $('.favorite-button-' + itemId).css('background-position', '-318px -283px');
	}
    }
    else if (controlType == 'SMALL_TRANSPARENT') {
	if (currentState) {
	    $('.favorite-button-' + itemId).css('background-position', '-354px -251px');
	}
	else {
	    $('.favorite-button-' + itemId).css('background-position', '-354px -267px');
	}
    }
    else if (controlType == 'MEDIUM') {
	if (currentState) {
	    $('.favorite-button-' + itemId).css('background-position', '-258px -143px');
	}
	else {
	    $('.favorite-button-' + itemId).css('background-position', '-238px -143px');
	}
    }

    var jqoLinks = $('.favorite-link-' + itemId);
    if (jqoLinks.length > 0) {
	if (currentState)
	    jqoLinks.html('Unlike');
	else
	    jqoLinks.html('Like');
    }

    jqoLinks.fadeIn(250);
    $('.favorite-button-' + itemId).fadeIn(250);
}

//*****
//* Item selection scripts
//*****

function AddItemThumbnailSelectionsHTML(domId, html, addToEnd) {
    $('#' + domId).find('.note:first').remove();
    if (addToEnd != null && addToEnd == true)
	$('#' + domId).find('.results-container:first').append(html);
    else
	$('#' + domId).find('.results-container:first').prepend(html);
}

function UpdateItemThumbnailSelectionsHTML(domId, html) {
    $('#' + domId).find('.results-container:first').html(html);
}

function MoveItemThumbnailSelectionUp(caller) {
    var jqoSelection = $(caller).parents('.item-thumbnail-selection-container:first');
    var jqoBefore = jqoSelection.prev();
    if (jqoBefore.length > 0 && jqoBefore.hasClass('item-thumbnail-selection-container')) {
	jqoSelection.insertBefore(jqoBefore);
    }
}

function MoveItemThumbnailSelectionDown(caller) {
    var jqoSelection = $(caller).parents('.item-thumbnail-selection-container:first');
    var jqoAfter = jqoSelection.next();
    if (jqoAfter.length > 0 && jqoAfter.hasClass('item-thumbnail-selection-container')) {
	jqoSelection.insertAfter(jqoAfter);
    }
}

function RemoveItemThumbnailSelection(caller) {
    $(caller).parents('.item-thumbnail-selection-container:first').remove();
}

function GetItemThumbnailSelections(domId) {
    var itemIds = new Array();
    $('#' + domId).find('.item-thumbnail-selection-item-id').each(function() {
	    itemIds.push($(this).val());
	});
    return itemIds;
}

//*****
//* Item picker scripts
//*****

function ItemPickerAdd(itemId, escapedItemTitle, baseId, allowSort) {
    ClearError(baseId + "-error");

    var existingItemIds = GetItemThumbnailSelections(baseId + "-container");
    for (var i=0; i < existingItemIds.length; i++) {
        if (existingItemIds[i] == itemId) {
            SetError(baseId + "-error", "That item is already in your list");
            return;
        }
    }

    var postData = MakeURLQuery([['itemIds', JSON.stringify([itemId])]]);
    if (allowSort)
	postData += '&' + MakeURLQuery([['allowSort', '1']])

    $.ajax({
            type: "POST",
		url: "/lists/RenderItemThumbnailSelection",
		data: postData,
		error: function(request, status, error) {
                HandleGenericError('9', baseId + "-error");
            },
		success: function(msg) {
		var result = JSON.parse(msg);
		
                if (result['SUCCESS']) { //success
                    AddItemThumbnailSelectionsHTML(baseId + '-container', result['RESULT']);
                }
                else {
                    HandleGenericError(String(result['ERROR_CODE']), baseId + "-error");
                }
            }
        });
}

function ItemPickerSetError(baseId, error) {
    if (error == null)
	ClearError(baseId + "-error");
    else
	SetError(baseId + "-error", error);
}

function ItemPickerGetItems(baseId) {
    return GetItemThumbnailSelections(baseId + "-container");
}

//*****
//* Shared modal dialog per page scripts
//*****

function ResetSharedModalDialog() {
    $('#shared-modal-dialog-content').html('<div class="faded italicized centeredarea">Loading...</div>');
}

function SetSharedModalDialogTitle(title) {
    $('#shared-modal-dialog-title').html(title);
}

function GetSharedModalDialogId() {
    return 'shared-modal-dialog';
}

function GetSharedModalDialogContentId() {
    return 'shared-modal-dialog-content';
}

function SetSharedModalDialogWidth(widthString) {
    $('#shared-modal-dialog').css('width', widthString);
}

//****
//* Dynamic form scripts - sync with *FormField templates
//****

function ValidateForm(containerId) {
    var error = false;

    $('#' + containerId).find('.form-field').each(function() {
	    if (!eval($(this).find('.form-field-validate-js:first').attr('value')))
		error = true;
	});

    return error;
}

function SerializeForm(containerId) {
    var dataArray = [];

    $('#' + containerId).find('.form-field').each(function() {
	    dataArray.push(eval($(this).find('.form-field-serialize-js:first').attr('value')));
	});

    return dataArray.join('&');
}

//*****
//* Page creation scripts
//*****

function CreatePageDialog(title, cbJs, cbJsArgs, simpleMode) {
    ResetSharedModalDialog();
    SetSharedModalDialogTitle('Create Page');
    SetSharedModalDialogWidth('750px');

    var dialogId = GetSharedModalDialogId();
    var dialogContentId = GetSharedModalDialogContentId();

    if (simpleMode == true) {
	simpleMode = '1';
    }
    else {
	simpleMode = '0';
    }

    var postData = MakeURLQuery([['title', title], ['cbJs', cbJs], ['cbJsArgs', cbJsArgs], ['simpleMode', simpleMode]]);

    $.ajax({
            type: "POST",
	    url: "/contribute/CreateItemDialog",
	    data: postData,
	    success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + dialogContentId).html(msg.substr(1));

		    $("#create-item-form-vertical").bind("change", function () { UpdateCreatePageForm(); });

		    //    SelectNewPageItemType('${itemType}'); // select item type if there are limitations

		    showAlert(dialogId); // re-center dialog
                }
                else if (msg.charAt(0) == '3') { //unauthenticated
		    hide(dialogId);
		    PopUpSignIn();
                }
            }
	});
}

function ResetCreatePageDupeSearch() {
    $('#create-item-form-dupes-searched').html('false');
}

function UpdateItemTypeNote() {
    var itemType = $('#create-item-form-item-type').val();

    if (itemType.length == 0) {
	hide('create-item-form-item-type-note');
	return;
    }
    
    var postData = "itemType=" + encodeURIComponent(itemType);

    $.ajax({
	    type: "POST",
	    url: "/item.py/ItemTypeNote",
            data: postData,
	    success: function(msg) {
		// purposely hide errors for this non-required info
		if (msg.charAt(0) == '0') { // success
		    if (msg.substr(1).length > 0) {
			$('#create-item-form-item-type-note').html(msg.substr(1));
			showBlock('create-item-form-item-type-note');
		    }
		    else {
			hide('create-item-form-item-type-note');
		    }
		}
	    }
	});
}

function UpdateCreatePageForm() {
    UpdateDocType("create-item-form-item-type", "create-item-form-vertical");
    UpdateItemTypeNote();

    hide('create-item-form-status');
    ToggleButton('create-item-form-save', true);
    ToggleButton('create-item-form-cancel', true);
}

function CreatePage() {
    // clear previous errors
    ClearError('create-item-form-vertical-error');
    $("#create-item-form-item-type-error").html("");
    $("#create-item-form-title-error").html("");
    $("#create-item-form-other-error").html("");
    hide("create-item-form-item-type-error");
    hide("create-item-form-title-error");
    hide("create-item-form-other-error");

    // form validation
    var error = false;
    if ($("#create-item-form-vertical").val().length == 0) { // check category is not blank
	error = true;
	
	SetError('create-item-form-vertical-error', "Please select a category first.");
    }
    if ($("#create-item-form-item-type").val().length == 0) { // check content type
	error = true;
	
	SetError('create-item-form-item-type-error', "Please select the type of content you're adding.");
    }

    if (!ImageSearchValidateForm('create-item-form-image-search')) {
	error = true;
    }

    var title = trim($('#create-item-form-title').val());
    if (title.length == 0) { // check title
	error = true;

	SetError('create-item-form-title-error', "Please enter a title for the new content.");
    }

    if (error) {
	return;
    }

    // advise of request in UI
    showBlock('create-item-form-status');
    ToggleButton('create-item-form-save', false);
    ToggleButton('create-item-form-cancel', false);

    var postData = "";

    var itemType = $("#create-item-form-item-type").val();

    postData = MakeURLQuery([['vertical', $("#create-item-form-vertical").val()], ['itemType', itemType], ['dupesSearched', $('#create-item-form-dupes-searched').html()], ['title', title]]);

    imageURLs = ImageSearchGetSelections('create-item-form-image-search');
    if (imageURLs.length > 0) {
	postData += '&imageURLs=' + encodeURIComponent(JSON.stringify(imageURLs));
    }

    $.ajax({
	    type: "POST",
		url: "/contribute/CreateItemPage",
		data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', 'create-item-form-other-error');
		  hide('create-item-form-status');
		  ToggleButton('create-item-form-save', true);
		  ToggleButton('create-item-form-cancel', true);
	        },
		success: function(msg) {
		   if (msg.charAt(0) == '0') { // success
		       if (msg.charAt(1) == '0') { // complete success
			   var results = JSON.parse(msg.substring(2));
			   var itemId = results['ITEM_ID'];
			   var firstTimePageCreation = results['FIRST_TIME_PAGE_CREATION'];

			   var cbJs = $('#create-page-callback').attr('value');
			   var cbJsArgs = $('#create-page-callback-args').attr('value');
			   if (cbJsArgs == undefined) {
			       cbJsArgs = '';
			   }

			   var simpleModeString = $('#create-page-simple-mode').attr('value');

			   if (simpleModeString.toLowerCase() == 'true') {
			       var cbJsFull = cbJs + "('" + itemId + "'";
			       if (cbJsArgs.length > 0) {
				   cbJsFull += ', ' + cbJsArgs;
			       }
			       cbJsFull += ');';

			       eval(cbJsFull);
			   }
			   else {
			       PopulateItemPageDialog('new-page', itemId, itemType, firstTimePageCreation, cbJs, cbJsArgs);
			   }
		       }
		       else if (msg.charAt(1) == '1') { // potential dupes found
			   $('#create-item-form-dupe-results').html(msg.substring(2));
			   $('#create-item-form-dupes-searched').html('true');
			   $('#create-item-form-dupes').css('display', 'block');

			   hide('create-item-form-status');
			   ToggleButton('create-item-form-save', true);
			   ToggleButton('create-item-form-cancel', true);
		       }
		       else if (msg.charAt(1) == '2') { // exact dupe found
			   $('#create-item-form-dupe-results').html(msg.substring(2));
			   $('#create-item-form-dupes').css('display', 'block');

			   SetError('create-item-form-title-error', "A page with this title and page type already exists. You can contribute to this existing page by clicking through below if it is what you're looking for. Otherwise, disambiguate the title for your new page. For example, multiple movies titled Batman can be differentiated by year: &quot;Batman (1989)&quot; vs. &quot;Batman (1966)&quot;.");

			   hide('create-item-form-status');
			   ToggleButton('create-item-form-save', true);
			   ToggleButton('create-item-form-cancel', true);
		       }
		   }
		   else if (msg.charAt(0) == '6') { // exact dupe found
		       SetError('create-item-form-title-error', "A page with this title and page type already exists. Please make sure you're not trying to add duplicate content. You can contribute to the existing page if it is what you're looking for. Otherwise, disambiguate the title for your new page. For example, multiple movies titled Batman can be differentiated by year: &quot;Batman (1989)&quot; vs. &quot;Batman (1966)&quot;.");
		       hide('create-item-form-dupes');
		       
		       hide('create-item-form-status');
		       ToggleButton('create-item-form-save', true);
		       ToggleButton('create-item-form-cancel', true);
		   }
		   else {
		       HandleEditError(msg, 'create-item-form-other-error');
		       
		       hide('create-item-form-status');
		       ToggleButton('create-item-form-save', true);
		       ToggleButton('create-item-form-cancel', true);
		   }
	        }
	     });
}

//*****
//* Page population Scripts
//*****

function RenderPopulateItemPageDialog(contents) {
    ResetSharedModalDialog();
    SetSharedModalDialogTitle('Enhance Page');
    SetSharedModalDialogWidth('750px');

    var dialogId = GetSharedModalDialogId();
    var dialogContentId = GetSharedModalDialogContentId();

    $('#' + dialogContentId).html(contents);

    showAlert(dialogId); // re-center dialog
}

// mode (string): new-page, converted-stub, existing-page
function PopulateItemPageDialog(mode, itemId, itemType, firstTimePageCreation, cbJs, cbJsArgs) {
    if (mode == 'existing-page') {
        ResetSharedModalDialog();
	SetSharedModalDialogTitle('Enhance Page');
	SetSharedModalDialogWidth('750px');
    }

    var dialogId = GetSharedModalDialogId();
    var dialogContentId = GetSharedModalDialogContentId();

    var postData = MakeURLQuery([['mode', mode], ['itemId', itemId], ['cbJs', cbJs], ['cbJsArgs', cbJsArgs]]);
    if (itemType != null) {
	postData += '&itemType=' + itemType;
    }
    if (firstTimePageCreation) {
	postData += '&firstTimePageCreation=1';
    }

    $.ajax({
            type: "POST",
	    url: "/contribute/PopulateItemPageDialog",
	    data: postData,
	    success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var formHTML = trim(msg.substr(1));

		    if (formHTML.length == 0) {
			var cbJsFull = cbJs + "('" + itemId + "'";
			if (cbJsArgs.length > 0) {
			    cbJsFull += ', ' + cbJsArgs;
			}
			cbJsFull += ');';

			eval(cbJsFull);
		    }
		    else {
			RenderPopulateItemPageDialog(formHTML);
		    }
                }
                else if (msg.charAt(0) == '3') { //unauthenticated
		    hide(dialogId);
		    PopUpSignIn();
                }
            }
	});

}

function PopulateItemPage(itemId) {
    var baseElementId = "populate-item-page";
    // clear previous errors
    ClearError(baseElementId + '-error');

    // form validation
    var error = ValidateForm(baseElementId + '-container');

    if (error) {
	return;
    }

    // advise of request in UI
    showBlock(baseElementId + '-status');
    ToggleButton(baseElementId + '-save', false);

    var postData = MakeURLQuery([['itemId', itemId]]);
    postData += '&' + SerializeForm(baseElementId + '-container');

    $.ajax({
	    type: "POST",
	    url: "/contribute/PopulateItemPage",
	    data: postData,
		timeout: 60000,
            error: function(request, status, error) {
		HandleGenericError('8', baseElementId + '-error');
		hide(baseElementId + '-status');
		ToggleButton(baseElementId + '-save', true);
	    },
            complete: function(request, status) {
		var jqoStatus = $('#' + baseElementId + '-status');
		var jqoButton = $('#' + baseElementId + '-save');

		// conditionally reset state since some call backs may change pages from underneath callback
		if (jqoStatus.length > 0)
		    jqoStatus.css('display', 'none');
		if (jqoButton.length > 0)
		    ToggleButtonByJqo(jqoButton, true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { // success
		    var cbJs = $('#' + baseElementId + '-callback').attr('value');
		    var cbJsArgs = $('#' + baseElementId + '-callback-args').attr('value');
		    if (cbJsArgs == undefined) {
			cbJsArgs = '';
		    }

		    var ftpc = ($('#' + baseElementId + '-ftpc').val().toLowerCase() == 'true');
		    var cbJsFull = cbJs + "('" + itemId + "', null, " + ftpc;
		    if (cbJsArgs.length > 0) {
			cbJsFull += ', ' + cbJsArgs;
		    }
		    cbJsFull += ');';

		    eval(cbJsFull);

		    // conditionally reset state since some call backs may change pages from underneath callback
		    var jqoDialog = $('#' + GetSharedModalDialogId());
		    if (jqoDialog.length > 0)
			jqoDialog.css('display', 'none');
		}
		else {
		    HandleEditError(msg, baseElementId + '-error');
		}
	    }
	});
}

//*****
//* Automated backfill scripts
//*****

function BackfillStub(itemId) {
    var itemId = itemId;

    var postData = MakeURLQuery([['itemId', itemId]]);

    $.ajax({
	    type: "POST",
	    url: "/contribute/BackfillItemPage",
	    timeout: 60000,
	    data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', 'backfill-stub-error');
		hide('backfill-stub-animation');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { // success
		    OpenItemDetails(itemId, escape(msg.substr(1)));
		}
		else {
		    HandleEditError(msg, 'backfill-stub-error');
		    hide('backfill-stub-animation');
		}
	    }
	});
}

function ValidatePercentageString(str) {
    var value = parseInt(str);
    if (isNaN(value) || value < 0 || value > 100) {
	return false;
    }

    return true;
}

function ValidatePercentageControl(obj, formSubmitButtonID, valueRequired, prettify) {
    // handle blanks
    var jqo = $(obj);

    if (!valueRequired && trim(jqo.val()).length == 0) {
	jqo.parents('.percentage-control-container:first').find('.percentage-control-error:first').css("display", "none");
	$("#" + formSubmitButtonID).attr("disabled", "");

	jqo.val('');
	return true;
    }

    var value = parseFloat(jqo.val());
    if (isNaN(value) || value < 0 || value > 100) {
	jqo.parents('.percentage-control-container:first').find('.percentage-control-error:first').css("display", "block");

	$("#" + formSubmitButtonID).attr("disabled", "disabled");
	return false;
    }
    else {
	jqo.parents('.percentage-control-container:first').find('.percentage-control-error:first').css("display", "none");

	$("#" + formSubmitButtonID).attr("disabled", "");

	if (prettify) {
	    $(obj).val(value + '%');
	}
    }

    return true;
}

function GetPercentageInputFieldValue(baseElementId) {
    var valueString = $('#' + baseElementId).find('.input-field:first').val();
    var value = parseFloat(valueString, 10);
    if (isNaN(value) || value < 0 || value > 100) {
        return '';
    }

    return value
}

function ValidatePercentageInputField(baseElementId) {
    var jqoError = $('#' + baseElementId).find('.error-message:first');
    ClearErrorByJqo(jqoError);

    var valueString = trim($('#' + baseElementId).find('.input-field:first').val());
    if (valueString.length == 0)
	return true
    
    if (!ValidatePercentageString(valueString)) {
	SetErrorByJqo(jqoError, 'Please type in a number between 0 and 100.');
	return false;
    }
    return true;
}

function SerializePercentageInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId).find('.field-key:first').attr('value');
    return MakeURLQuery([[fieldKey, GetPercentageInputFieldValue(baseElementId)]]);
}

function ValidatePriceRange(controlID, target) {
    var fieldID = "";
    var otherFieldID = "";
    
    if (target == 'LOW_PRICE') {
	fieldID = controlID + "-low-price";
	otherFieldID = controlID + "-high-price";
    }
    else {
	fieldID = controlID + "-high-price";
	otherFieldID = controlID + "-low-price";
    }

    // check format
    var price = GetPrice(fieldID);
    var otherPrice = GetPrice(otherFieldID);
    var valid = false;
    var errorMsg = "Please enter a positive number for each price range.";
    if (price == null || price >= 0.01) {
	$("#" + fieldID).val(price);

	// copy to the other area if it is blank and vice versa
	if (price != null && otherPrice == null) {
	    $("#" + otherFieldID).val(price);
	    otherPrice = price;
	}
	else if (price == null && otherPrice != null) {
	    $("#" + fieldID).val(otherPrice);
	    price = otherPrice;
	}

	// if the other price is valid
	if (otherPrice == null || otherPrice >= 0.01) {
	    // check the low price is <= the high price if both are non-blank
	    if (price != null && otherPrice != null) {
		if (target == 'LOW_PRICE') {
		    valid = parseFloat(price) <= parseFloat(otherPrice);
		}
		else {
		    valid = parseFloat(price) >= parseFloat(otherPrice);
		}

		if (!valid) {
		    errorMsg = "Please make sure that the start of the price range is less than or equal to the end of the price range.";
		}
	    }
	    else {
		valid = true;
	    }
	}
    }

    if (valid) {
	$("#" + controlID + "-error").html("&nbsp;");
	hide(controlID + "-error");

	return true;
    }
    else {
	$("#" + controlID + "-error").html(errorMsg);
	showBlock(controlID + "-error");

	return false;
    }
}

// returns a price as a string fixed to 0.01 precision from the fieldID
// returns null if the price is blank
// returns -1 if the price is invalid
function GetPrice(fieldID) {
    if (trim($("#" + fieldID).val()) == "") {
        return null;
    }

    var value = parseFloat($("#" + fieldID).val()).toFixed(2);

    if (parseFloat(value) >= 0.01) {
        return value;
    }

    return -1;
}

function GetPriceInputFieldValue(baseElementId) {
    var valueString = trim($("#" + baseElementId + '-price').val());
    if (valueString == "") {
        return null;
    }

    var value = parseFloat(valueString).toFixed(2);

    if (parseFloat(value) >= 0.01) {
        return value;
    }

    return -1;
}

function ValidatePriceInputField(baseElementId) {
    ClearError(baseElementId + '-error');

    if (GetPriceInputFieldValue(baseElementId) == -1) {
	SetError(baseElementId + '-error', "Please enter a positive number");
	return false;
    }

    return true;
}

function SerializePriceInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId + '-field-key').attr('value');
    var value = GetPriceInputFieldValue(baseElementId);
    if (value == null)
	value = '';
    return MakeURLQuery([[fieldKey, value]]);
}

function UpdatePriceRange(controlID, docID, lowPriceKey, highPriceKey, emptyDisplay) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var docID = docID;
    var lowPriceKey = lowPriceKey;
    var highPriceKey = highPriceKey;
    var emptyDisplay = emptyDisplay;

    // form validation
    if (!ValidatePriceRange(controlID, 'LOW_PRICE')) { // note this validates both prices and sets / clears the error msg
        return;
    }

    lowPrice = GetPrice(controlID + "-low-price");
    highPrice = GetPrice(controlID + "-high-price");

    if (lowPrice == null) {
	lowPrice = "";
    }
    if (highPrice == null) {
        highPrice = "";
    }

    var formdata = "docID=" + docID + "&" + lowPriceKey + "=" + lowPrice + "&" + highPriceKey + "=" + highPrice;

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
            type: "POST",
                url: "/item.py/UpdateDoc",
                data: formdata,
                error: function(request, status, error) {
		  HandleGenericError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var html = "";
		    if (lowPrice == highPrice) {
			highPrice = "";
		    }

		    if (lowPrice != "" && highPrice != "") {
			html = "$" + lowPrice + " &ndash; $" + highPrice;
		    }
		    else if (lowPrice != "") {
			html = "$" + lowPrice;
		    }
		    else if (highPrice != "") {
			html = "$" + highPrice;
		    }
		    else {
			html = emptyDisplay;
		    }

		    $("#" + controlID + "-view-value").html(html);

                    ToggleEditControlOff(controlID);
                }
                else if (msg.charAt(0) == 'Q') {
                    ToggleEditControlOff(controlID);
                    showAlert('change-queued');
                }
                else { // error
                    HandleModeratedChangeError(msg, controlID + "-error");
                }
            }
        });
}

function ToggleEditControlOff(controlID) {
    hide('editControlEditView' + controlID);
    hide('editControlDisplay' + controlID);
    $('.edit-control-expanded-' + controlID).css('display', 'none');

    showBlock('editControlDisplayView' + controlID);
    showBlock('editControlEdit' + controlID);
}

function UpdateTitle(itemID) {
    // keep these vars in static scope for ajax cb
    var itemID = itemID;
    
    // clear previous errors
    ClearError('edit-title-error');

    // check value validity
    var value = trim($('#edit-title-value').val());

    if (value.length == 0) { // check for blank
	SetError('edit-title-error', 'Please provide a non-blank value before saving.');
	return;
    }
    else if (value.length > 1000) { // check for length
	SetError('edit-title-error', 'Please use 1000 characters or less.');
	return;
    }
    
    // advise of request in UI
    showBlock('edit-title-status');
    ToggleButton('edit-title-save', false);
    ToggleButton('edit-title-cancel', false);
    
    var formdata = "itemID=" + itemID + "&title=" + encodeURIComponent(value);

    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateTitle",
		data: formdata,
                error: function(request, status, error) {
		  HandleGenericError('8', 'edit-value-error');
	        },
                complete: function(request, status) {
		  hide('edit-title-status');
		  ToggleButton('edit-title-save', true);
		  ToggleButton('edit-title-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success

		    $('#item-title').html(value);

		    hide('edit-title');

		}
		else if (msg.charAt(0) == 'Q') {
		    hide('edit-title');
                    showAlert('change-queued');
                }
		else if (msg.charAt(0) == '2') {
		    SetError('edit-title-error', "Permission denied. Check your reputation page to see if you've exceeded the daily limit for performing this action or if you're under probation. Also, if the item has been around for a while you will not be able to edit its title.");
                }
		else if (msg.charAt(0) == '6') {
		    SetError('edit-title-error', 'This title is already used by another page of the same type. Try disambiguating your title. For example, you can often create unique movie titles by putting the release year in parenthesis or make music titles unique by including the artist name.');
                }
		else { // error
		    HandleModeratedChangeError(msg, 'edit-title-error');
		}
	    }
	});
}

// Posts a long text field asynchronously - meant to be paired with the mako function LongTextConrol
function UpdateLongTextField(controlID, docID, fieldName) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var docID = docID;
    var fieldName = fieldName;
    var fieldValue = trim($("#longTextControlEditValue" + controlID).val());
    
    // clear previous errors
    $("#longTextControlEditError" + controlID).html("");
    hide("longTextControlEditError" + controlID);

    // form validation
    var error = false;
    if (fieldValue.length == 0) { // check for blank
	error = true;
	
	$("#longTextControlEditError" + controlID).html("Please enter something before saving.");
	showBlock("longTextControlEditError" + controlID);
    }
    else if (fieldValue.length > 1000) { // check for length
	error = true;
	
	$("#longTextControlEditError" + controlID).html("Please use 1000 characters or less.");
	showBlock("longTextControlEditError" + controlID);
    }

    if (error) {
	return;
    }

    var formdata = "docID=" + docID + "&" + fieldName + "=" + encodeURIComponent(fieldValue);

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton('longTextControlSave' + controlID, false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: formdata,
                error: function(request, status, error) {
		  HandleGenericError('8', "longTextControlEditError" + controlID);
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton('longTextControlSave' + controlID, true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#longTextControlDisplayValue" + controlID).html(msg.substr(1));

		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { // error
		    HandleModeratedChangeError(msg, "longTextControlEditError" + controlID);
		}
	    }
	});
}

//*****
//* Tracking related scripts - depends on ga.js and an instance 'pageTracker'
//*****

function TrackUser(userSegment) {
    if (typeof pageTracker == 'undefined') { // case in non-production environments
	return;
    }

    pageTracker._setVar(userSegment);
}

function TrackEvent(category, action, label, value) {
    if (typeof pageTracker == 'undefined') { // case in non-production environments
	return;
    }

    pageTracker._trackEvent(category, action, label, value);
}

//*****
//* Authentication related scripts
//*****

// mirrors front-end server check
function ValidateEmail(email) { 
    var atext = "[\\w!#\\$%&'\\*\\+\\-/=\\?\\^`{\\|}~]";
    var addressEx = '^' + atext + '+(\\.' + atext + '+)*@' + atext + '+(\\.' + atext + '+)*$';
    var regEx = new RegExp(addressEx);
    
    return email.length > 0 && email.length <= 255 && email.replace(regEx, '').length == 0;
}

function ParseFullName(fullName) {
    tokens = trim(fullName).split(' ');
    if (tokens.length >= 2) {
        return tokens;
    }

    return null;
}

function ParseCityState(cityState) {
    cityState = trim(cityState);

    match = cityState.match(/^.*[,;\s]+[a-zA-Z]{2}$/);

    if (match != null) {
        city = cityState.substring(0, cityState.length - 2);
        state = cityState.substring(cityState.length - 2);

	city = city.replace('[,;\s]+$', '');
        
	return [city, state];
    }

    return null;
}

function CreateCaptcha(uniqueID, publicKey) {
    if (trim($('#captcha-' + uniqueID).html()).length == 0) {
	Recaptcha.create( publicKey,
			  "captcha-" + uniqueID,
			  {
			      theme: "red"
				  } );

	showBlock('captcha-msg-' + uniqueID);
    }
}

function SignInSetFocus(uniqueID, onSignIn) {
    if (onSignIn) {
	$("#email-" + uniqueID).focus();
    }
    else {
	$("#sign-up-email-" + uniqueID).focus();
    }
}

function PopUpSignIn() {
    if ($('#sign-in-dialog:first').length > 0) {
	showAlert('sign-in-dialog');
	SignInSetFocus(0, true);
	$('#sign-in-dialog').css("z-index", "100"); // pop above other dialogs
    }
}

function ValidateSignIn() {
    return ValidateSignInFull('email', 'password', 'email-error', 'password-error', 'other-error');
}

function ValidateSignInFull(emailField, passwordField, emailErrorField, passwordErrorField, generalErrorField) {
    var email = trim($("#" + emailField).val());
    var password = trim($("#" + passwordField).val());

    // clear previous errors
    $("#" + emailErrorField).html("");
    $("#" + passwordErrorField).html("");
    $("#" + generalErrorField).html("");
    hide(emailErrorField);
    hide(passwordErrorField);
    hide(generalErrorField);

    // form validation
    var error = false;
    if (email.length == 0) {
	error = true;
	
	$("#" + emailErrorField).html("Please enter your email address.");
	showBlock(emailErrorField);
    }
    else if (!ValidateEmail(email)) {
	error = true;
	
	$("#" + emailErrorField).html("Please enter a properly formed email address.");
	showBlock(emailErrorField);
    }
    
    passwordError = CheckPasswordError(password);
    if (passwordError.length > 0) {
	error = true;
	
	$("#" + passwordErrorField).html(passwordError);
	showBlock(passwordErrorField);
    }

    return !error;
}

//*****
//* Item web image search related scripts
//*****

function ImageSearch(callingObject) {
    var jqoCaller = $(callingObject);
    var jqoSearchContainer = jqoCaller.parents('.image-search-container:first');
    var containerId = jqoSearchContainer.attr('id');

    ClearError(containerId + '-error');

    var query = trim(jqoSearchContainer.find('.image-search-input:first').val());

    if (query.length == 0) {
	SetError(containerId + '-search-error', 'Please enter an image search');
	return;
    }

    var postData = MakeURLQuery([['query', query]]);

    // advise of request in UI
    showBlock(containerId + '-status');

    $.ajax({
            type: "POST",
		url: "/page-images/SearchWebImages",
		data: postData,
		error: function(request, status, error) {
                HandleGenericError('8', containerId + '-error');
            },
		complete: function(request, status) {
                hide(containerId + '-status');
            },
		success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    jqoSearchContainer.find('.image-search-results:first').replaceWith(msg.substr(1));
                }
                else { // error
                    HandleGenericError(msg, containerId + '-error');
                }
            }
        });

}

function ImageSearchToggleSelectNone(callingObject) {
    var jqoCaller = $(callingObject);
    var jqoSearchContainer = jqoCaller.parents('.image-search-container:first');

    if (jqoCaller.attr('checked')) {
	jqoSearchContainer.find('.image-search-select').each(function() {
		$(this).attr('checked', '');
	    });
    }
}

function ImageSearchToggleSelect(callingObject) {
    var jqoCaller = $(callingObject);
    var jqoSearchContainer = jqoCaller.parents('.image-search-container:first');

    if (jqoCaller.attr('checked')) {
	jqoSearchContainer.find('.image-search-no-image-available:first').attr('checked', '');
    }
}

function ImageSearchValidateForm(containerId) {
    var selectedURLs = ImageSearchGetSelections(containerId);
    var jqoSearchContainer = $('#' + containerId);
    var containerId = jqoSearchContainer.attr('id');

    ClearError(containerId + '-selection-error');
    ClearError(containerId + '-search-error');

    var maxSelection = parseInt(jqoSearchContainer.find('.image-search-max-selection:first').attr('value'));

    if (selectedURLs == null) {
	SetError(containerId + '-selection-error', 'Please select at least one image or the "Couldn\'t find valid images" option.');
	return false;
    }
    else if (selectedURLs.length > maxSelection) {
	SetError(containerId + '-selection-error', 'Please select no more than ' + maxSelection + ' images');
	return false;
    }

    return true;
}

// returns null for no selection and [] for a purposeful no image selection
function ImageSearchGetSelections(containerId) {
    var jqoSearchContainer = $('#' + containerId);

    if (jqoSearchContainer.find('.image-search-no-image-available:first').attr('checked')) {
	return [];
    }

    var results = [];

    jqoSearchContainer.find('.image-search-select').each(function() {
	    if ($(this).attr('checked')) {
		results.push($(this).attr('value'));
	    }
	});

    if (results.length == 0) {
	return null;
    }

    return results;
}

function SerializeImageSearchInputField(baseElementId) {
    urls = ImageSearchGetSelections(baseElementId);
    return MakeURLQuery([['images', JSON.stringify(urls)]]);
}

function AddItemImageViaImageSearch(baseElementId, imageSelectElementId, itemId) {
    // bring into lexical scope
    var baseElementId = baseElementId;
    var imageSelectElementId = imageSelectElementId;
    var itemId = itemId;

    // clear previous errors
    ClearError(baseElementId + '-other-error');

    // form validation
    if (!ImageSearchValidateForm(imageSelectElementId)) {
	return;
    }

    // advise of request in UI
    showBlock(baseElementId + '-other-status');
    ToggleButton(baseElementId + '-submit', false);

    var postData = MakeURLQuery([['itemId', itemId]]);
    postData += '&' + SerializeImageSearchInputField(imageSelectElementId);

    $.ajax({
	    type: "POST",
	    url: "/page-images/AddItemImages",
	    data: postData,
		timeout: 60000,
            error: function(request, status, error) {
		HandleGenericError('8', baseElementId + '-other-error');
		hide(baseElementId + '-other-status');
		ToggleButton(baseElementId + '-submit', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { // success
		    window.location = msg.substr(1);
		}
		else {
		    HandleEditError(msg, baseElementId + '-other-error');
		       
		    hide(baseElementId + '-other-status');
		    ToggleButton(baseElementId + '-submit', true);
		}
	    }
	});
}

//*****
//* Item image related scripts
//*****

function ViewItemImage(imageElementID, showCaseImageURL, originalImageURL, imageID, itemID) {
    ClearError('view-item-image-error');

    // immediately update image
    $('#' + imageElementID).attr('src', showCaseImageURL);
    $('#' + imageElementID).parents('a:first').attr('href', originalImageURL);

    // get updated controls from server async
    var postData = "imageID=" + imageID + "&itemID=" + itemID;

    $.ajax({
            type: "POST",
		url: "/images.py/ItemImageControls",
		data: postData,
		error: function(request, status, error) {
                HandleGenericError('8', 'view-item-image-error');
            },
		success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $("#item-image-controls").html(msg.substr(1));
                }
                else { //1st char of msg will indicate failure
                    HandleGenericError(msg, 'view-item-image-error');
                }
            }
        });
}

function VoteOnItemImage(itemID, imageID, votingControlBaseID) {
    // lexical scoping for callback
    var votingControlBaseID = votingControlBaseID;

    var postData = "itemID=" + itemID + "&imageID=" + imageID + "&vote=true";

    $.ajax({
            type: "POST",
            url: "/images.py/VoteOnItemImage",
            data: postData,
	    error: function(request, status, error) {
		VoteUpError(votingControlBaseID, '8');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    VoteUpSet(votingControlBaseID);
                }
                else { //success
                    VoteUpError(votingControlBaseID, msg);
                }
            }
        });
}

// this function is the callback for deleting an image
function DeleteItemImage(controlID, imageID, itemID) {
    // lexical scoping for CB
    var controlID = controlID;
    var imageID = imageID;

    var postData = "imageID=" + imageID + "&itemID=" + itemID;
	
    // clear previous errors
    ClearDeleteErrors(controlID);

    // disable buttons and give status
    AdviseDeleteStart(controlID);

    $.ajax({
	    type: "POST",
	    url: "/images.py/DeleteItemImage",
	    data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlID);
		AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    // refresh the page using a server provided url
		    window.location = msg.substr(1);
		}
		else { // handle errors
		    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
		}
	    }
	});
}


//*****
//* Item description related scripts
//*****

function ValidateItemDescription(baseElementId, allowBlank, maxLength, maxLines) {
    ClearError(baseElementId + '-error');

    var description = trim($('#' + baseElementId + '-input-text').val());

    if (!allowBlank && description.length == 0) { // check for blank
        SetError(baseElementId + '-error', 'Please provide a description.');
        return false;
    }
    else if (description.length > maxLength) { // check for length
        SetError(baseElementId + '-error', 'Please use ' + maxLength + ' characters or less.');
        return false;
    }
    else if (CountLines(description) > maxLines) { // check for excessive new lines
        SetError(baseElementId + '-error', 'There are too many lines in your description. Please use ' + maxLines + ' or fewer lines (a new line is used each time you press "Enter").');
        return false;
    }

    return true;
}

function SerializeItemDescription(baseElementId) {
    var descriptionText = trim($('#' + baseElementId + '-input-text').val());
    var description = { 'description': descriptionText };
    return MakeURLQuery([['description', JSON.stringify(description)]]);
}

function PageItemDescriptions(itemID, pageOffset) {
    ClearError('description-paging-error');

    // keep these vars in static scope for async cb
    var itemID = itemID;
    var pageOffset = pageOffset;

    var postData = "itemID=" + itemID + "&pageOffset=" + pageOffset;

    $.ajax({
            type: "POST",
            url: "/item.py/PageItemDescriptions",
            data: postData,
	    error: function(request, status, error) {
                HandleGenericError('8', 'description-paging-error');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $("#descriptions-container").html(msg.substr(1));
                }
                else { //1st char of msg will indicate failure
		    HandleGenericError(msg, 'description-paging-error');
                }
            }
        });
}

function SaveItemDescription(dialogID, itemID, maxItemDescriptionLength, maxItemDescriptionLines) {
    // keep these vars in static scope for ajax cb
    var dialogID = dialogID;
    var itemID = itemID;
    
    // validate input
    if (!ValidateItemDescription(dialogID, false, maxItemDescriptionLength, maxItemDescriptionLines)) {
	return;
    }
    var descriptionText = trim($('#' + dialogID + '-input-text').val());

    var postData = "itemID=" + itemID + "&descriptionText=" + encodeURIComponent(descriptionText);

    // advise of request in UI
    showBlock(dialogID + '-status');
    ToggleButton(dialogID + '-save', false);
    ToggleButton(dialogID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
	    url: "/item.py/AddItemDescription",
	    data: postData,
            error: function(request, status, error) {
		HandleEditError('8', dialogID + '-error');
	    },
            complete: function(request, status) {
		hide(dialogID + '-status');
		ToggleButton(dialogID + '-save', true);
		ToggleButton(dialogID + '-cancel', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#collapsed-description-area').prepend(msg.substr(1));

		    hideDialog(dialogID);

		    // the user cannot add another description for this item
		    hide('add-item-description-link');
		}
		else { // error
		    HandleEditError(msg, dialogID + '-error');
		}
	    }
	});
}

function EditItemDescription(dialogID, itemID, descriptionID, maxItemDescriptionLength, maxItemDescriptionLines) {
    // keep these vars in static scope for ajax cb
    var dialogID = dialogID;
    var itemID = itemID;
    var descriptionID = descriptionID;
    
    // clear previous errors
    ClearError(dialogID + '-error');

    // validate input
    var descriptionText = trim($('#' + dialogID + '-input-text').val());

    if (descriptionText.length == 0) { // check for blank
	SetError(dialogID + '-error', 'Please provide a description.');
	return;
    }
    else if (descriptionText.length > maxItemDescriptionLength) { // check for length
	SetError(dialogID + '-error', 'Please use ' + maxItemDescriptionLength + ' characters or less.');
	return;
    }
    else if (CountLines(descriptionText) > maxItemDescriptionLines) { // check for excessive new lines
	SetError(dialogID + '-error', 'There are too many lines in your description. Please use ' + maxItemDescriptionLines + ' or fewer lines (a new line is used each time you press "Enter").');
	return;
    }

    var postData = "itemID=" + itemID + "&descriptionID=" + descriptionID + "&descriptionText=" + encodeURIComponent(descriptionText);

    // advise of request in UI
    showBlock(dialogID + '-status');
    ToggleButton(dialogID + '-save', false);
    ToggleButton(dialogID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
	    url: "/item.py/EditItemDescription",
	    data: postData,
            error: function(request, status, error) {
		HandleEditError('8', dialogID + '-error');
	    },
            complete: function(request, status) {
		hide(dialogID + '-status');
		ToggleButton(dialogID + '-save', true);
		ToggleButton(dialogID + '-cancel', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#item-description-text-' + descriptionID).html(msg.substr(1));

		    hideDialog(dialogID);
		}
		else { // error
		    HandleEditError(msg, dialogID + '-error');
		}
	    }
	});
}

function EditExternalItemDescription(dialogId, itemId, descriptionId) {
    // keep these vars in static scope for ajax cb
    var dialogId = dialogId;
    var itemId = itemId;
    var descriptionId = descriptionId;
    
    // clear previous errors
    ClearError(dialogId + '-error');

    // validate input
    var url = trim($('#' + dialogId + '-input').val());
    var domain = trim($('#' + dialogId + '-domain').val());

    if (url.length > 0 && !ValidateURL(url)) {
	SetError(dialogId + '-error', "Please enter a URL in the same format you'd use to open a web page in your browser");
	return;
    }

    if (url.length > 0 && GetURLDomain(url).toLowerCase() != domain) {
	SetError(dialogId + '-error', "Please enter a URL from the domain " + domain + " or enter a blank URL to remove this description");
	return;
    }

    // advise of request in UI
    showBlock(dialogId + '-status');
    ToggleButton(dialogId + '-save', false);
    ToggleButton(dialogId + '-cancel', false);

    // handle delete request
    if (url.length == 0) {
	var postData = MakeURLQuery([['targetType', 'ITEM_DESCRIPTION'], ['targetID', descriptionId], ['reason', 'INACCURATE']]);
	$.ajax({
            type: "POST",
                url: "/page/Flag",
                data: postData,
                error: function(request, status, error) {
		    HandleEditError('8', dialogId + '-error');
	        },
                complete: function(request, status) {
		    hide(dialogId + '-status');
		    ToggleButton(dialogId + '-save', true);
		    ToggleButton(dialogId + '-cancel', true);
	        },
                success: function(msg) {
		    if (msg.charAt(0) == '0') { //success
			hideDialog(dialogId);
			showAlert('change-queued');
		    }
		    else { // handle errors
			HandleEditError(msg, dialogId + '-error');
		    }
		}
        });

	return;
    }

    var postData = MakeURLQuery([['itemId', itemId], ['descriptionId', descriptionId], ['url', url]])

    $.ajax({
	    type: "POST",
	    url: "/page/EditExternalItemDescription",
	    data: postData,
            error: function(request, status, error) {
		HandleEditError('8', dialogId + '-error');
	    },
            complete: function(request, status) {
		hide(dialogId + '-status');
		ToggleButton(dialogId + '-save', true);
		ToggleButton(dialogId + '-cancel', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    hideDialog(dialogId);
		    showAlert('change-queued');
		}
		else { // error
		    if (msg.charAt(0) == '1') { // invalid request
			SetError(dialogId + '-error', "Please enter a valid " + domain + " URL about this topic.");
		    }
		    else if (msg.charAt(0) == 'E') { // extraction problems
			SetError(dialogId + '-error', "We were unable to extract a description from the URL.");
		    }
		    else {
			HandleEditError(msg, dialogId + '-error');
		    }
		}
	    }
	});
}

function DeleteItemDescription(controlID, itemID, descriptionID) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;
    var descriptionID = descriptionID;

    // clear previous errors
    ClearDeleteErrors(controlID);

    var postData = "itemID=" + itemID + "&descriptionID=" + descriptionID;

    AdviseDeleteStart(controlID);

    $.ajax({
            type: "POST",
            url: "/item.py/DeleteItemDescription",
            data: postData,
            error: function(request, status, error) {
		  HandleDeleteError('8', controlID);
		  AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $("#item-description-" + descriptionID).remove();

		    showInline('add-item-description-link');
                }
                else { //1st char of msg will indicate failure
		    HandleDeleteError(msg, controlID)
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function VoteOnDescription(itemID, descriptionID, votingControlBaseID) {
    // lexical scoping for callback
    var descriptionID = descriptionID;
    var votingControlBaseID = votingControlBaseID;

    var postData = "itemID=" + itemID + "&descriptionID=" + descriptionID + "&vote=true";

    ClearVoteUpError(votingControlBaseID);

    $.ajax({
            type: "POST",
                url: "/page/VoteOnItemDescription",
                data: postData,
		error: function(request, status, error) {
                VoteUpError(votingControlBaseID, '8');
            },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    VoteUpSet(votingControlBaseID, msg.substr(1));
                }
                else { //success
                    VoteUpError(votingControlBaseID, msg);
                }
            }
        });
}

//*****
//* Date input field related scripts
//*****

function GetDateInputFieldValue(baseElementId) {
    var jqoContainer = $('#' + baseElementId);

    var jqoPresent = jqoContainer.find('.date-input-present');
    
    if (jqoPresent.length > 0 && jqoPresent.attr('checked')) {
	return "PRESENT";
    }

    var elements = GetDateInputFieldElementValues(baseElementId);

    // validate
    // note that all parseInt explicitly specify radix to prevent legacy javascript engines from interpretting leadding 0s as octal
    var year = "";
    if (elements[0] != null) {
	year = elements[0];
	if (year.length != 4 || !(parseInt(year, 10) > 0 && parseInt(year, 10) < 9999)) {
	    return null;
	}
    }
    var month = "";
    if (elements[1] != null) {
	month = elements[1];
	if (month.length != 2 || !(parseInt(month, 10) > 0 && parseInt(month, 10) <= 12)) {
	    return null;
	}
    }
    var day = "";
    if (elements[2] != null) {
	day = elements[2];
	if (day.length != 2 || !(parseInt(day, 10) > 0 && parseInt(day, 10) < 32)) {
	    return null;
	}
    }

    // return
    if (day.length > 0) {
	if ((year + month + day).length != 8) {
	    return null;
	}

	return year + month + day
    }
    else if (month.length > 0) {
	if ((year + month).length != 6) {
	    return null;
	}

	return year + month
    }
    else if (year.length > 0) {
	if (year.length != 4) {
	    return null;
	}

	return year
    }

    return ""
}

function FormatDateValue(dateString) {
    result = "";
    
    elements = ExtractDateElements(dateString);

    if (elements[1] != null) {
	if (elements[1] == "01") {
	    result += "January";
	}
	else if (elements[1] == "02") {
	    result += "February";
	}
	else if (elements[1] == "03") {
	    result += "March";
	}
	else if (elements[1] == "04") {
	    result += "April";
	}
	else if (elements[1] == "05") {
	    result += "May";
	}
	else if (elements[1] == "06") {
	    result += "June";
	}
	else if (elements[1] == "07") {
	    result += "July";
	}
	else if (elements[1] == "08") {
	    result += "August";
	}
	else if (elements[1] == "09") {
	    result += "September";
	}
	else if (elements[1] == "10") {
	    result += "October";
	}
	else if (elements[1] == "11") {
	    result += "November";
	}
	else if (elements[1] == "12") {
	    result += "December";
	}

	if (elements[2] != null) {
	    result += " " + elements[2];
	}

	if (elements[0] != null) {
	    result += ", ";
	}
    }

    if (elements[0] != null) {
	result += elements[0];
    }

    return result;
}

function ToggleDateInputField(baseElementId, enable) {
    var jqoContainer = $('#' + baseElementId);
    var disableValue = 'disabled';
    if (enable) {
	disableValue = '';
    }

    jqoContainer.find('.date-input-year:first').attr('disabled', disableValue);
    jqoContainer.find('.date-input-month:first').attr('disabled', disableValue);
    jqoContainer.find('.date-input-day:first').attr('disabled', disableValue);
}

function ToggleDateInputFieldPresent(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    
    if (jqoContainer.find('.date-input-present:first').attr('checked')) {
	ToggleDateInputField(baseElementId, false);
    }
    else {
	ToggleDateInputField(baseElementId, true);
    }
}

function ValidateDateInputField(baseElementId) {
    var jqoError = $("#" + baseElementId).find('.error-message:first');
    ClearErrorByJqo(jqoError);

    var fieldValue = GetDateInputFieldValue(baseElementId);

    if (fieldValue == null) {
	SetErrorByJqo(jqoError, "Please input a valid date. The year must be in the form YYYY.");
	    
        return false;
    }

    return true;
}

function SerializeDateInputField(baseElementId) {
    var fieldKey = trim($('#' + baseElementId).find('.date-input-field-key:first').attr('value'));
    var fieldValue = GetDateInputFieldValue(baseElementId);

    return MakeURLQuery([[fieldKey, fieldValue]]);
}

function GetDateInputFieldElementValues(baseElementId) {
    var jqoContainer = $('#' + baseElementId);

    var year = null;
    var month = null;
    var day = null;

    var yearVal = trim(jqoContainer.find('.date-input-year:first').val());
    if (yearVal.length > 0) {
        year = yearVal;
        while (year.length < 4) {
            year = "0" + year;
        }
    } else {
        return [year, month, day];
    }

    var monthVal = trim(jqoContainer.find('.date-input-month:first').val());
    if (monthVal.length > 0) {
        month = monthVal;
        if (month.length < 2) {
            month = "0" + month;
        }
    }

    var dayVal = trim(jqoContainer.find('.date-input-day:first').val());
    if (dayVal.length > 0) {
        day = dayVal
        if (day.length < 2) {
            day = "0" + day;
        }
    }

    return [year, month, day];
}

function ExtractDateElements(dateString) {
    var year = null;
    var month = null;
    var date = null;

    if (dateString.length == 8) {
	date = dateString.substr(6);
    }
    if (dateString.length >= 6) {
	month = dateString.substr(4, 2);
    }
    if (dateString.length >= 4) {
	year = dateString.substr(0, 4);
    }

    return [year, month, date];
}

// Posts a change to a doc date value asynchronously - meant to be paired with the mako function DocDateControl
function UpdateDateField(controlID, docID, fieldName, emptyDisplay) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var docID = docID;
    var fieldName = fieldName;
    var emptyDisplay = emptyDisplay;

    // clear previous errors
    ClearError(controlID + '-error');
    
    // form validation
    if (!ValidateDateInputField(controlID)) {
	return;
    }

    var fieldValue = GetDateInputFieldValue(controlID);

    var formdata = "docID=" + docID + "&" + fieldName + '=' + fieldValue;

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    if (fieldValue == "PRESENT") {
			$("#dateControlDisplayValue" + controlID).html("Present");
		    }
		    else if (fieldValue.length > 0) {
			$("#dateControlDisplayValue" + controlID).html(FormatDateValue(fieldValue));
		    }
		    else {
			$("#dateControlDisplayValue" + controlID).html(emptyDisplay);
		    }

		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { //1st char of msg will indicate failure
		    HandleModeratedChangeError(msg, controlID + '-error');
		}
	    }
	});
}

//*****
//* Item text control related scripts
//*****

function ValidateTextInputField(baseElementId) {
    return true;
}

function SerializeTextInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId + '-field-key').attr('value');
    var value = $('#' + baseElementId + '-input').val();
    return MakeURLQuery([[fieldKey, value]]);
}

function UpdateTextField(controlID, itemID, attribute, emptyDisplay) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;
    var attribute = attribute;
    var value = $('#' + controlID + '-edit').val();
    var emptyDisplay = emptyDisplay;
    
    // clear previous errors
    ClearError(controlID + '-error');
    
    var postData = "docID=" + itemID + "&" + SerializeTextInputField(controlID + '-field');

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: postData,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#" + controlID + '-view').html(msg.substr(1));

		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { //1st char of msg will indicate failure
		    HandleModeratedChangeError(msg, controlID + '-error');
		}
	    }
	});
}

function UpdatePercentageAttribute(controlID, itemID, attribute, emptyDisplay) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;
    var attribute = attribute;
    var value = trim($('#' + controlID + '-edit').val());
    var emptyDisplay = emptyDisplay;
    
    // clear previous errors
    ClearError(controlID + '-error');

    // validate value
    var percentage = '';
    if (value.length > 0) {
	percentage = parseFloat(value, 10);
	if (isNaN(percentage) || percentage < 0 || percentage > 100 || value.replace(/[%.\d]+/, "").length != 0) {
	    SetError(controlID + '-error', 'Please input a valid numerical percentage or leave the field blank.');
	    return
	}
    }
    
    var postData = "docID=" + itemID + "&" + attribute + "=" + percentage;

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: postData,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {

		if (msg.charAt(0) == '0') { //success
		    if (value.length > 0) {
			$("#" + controlID + '-view').html(percentage + '%');
		    }
		    else {
			$("#" + controlID + '-view').html(emptyDisplay);
		    }

		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { //1st char of msg will indicate failure
		    HandleModeratedChangeError(msg, controlID + '-error');
		}
	    }
	});
}

//*****
//* Duration control related scripts
//*****


function ToggleDurationInputField(baseElementId, enable) {
    var disableValue = 'disabled';
    if (enable) {
	disableValue = '';
    }

    $('#' + baseElementId + '-hours-input').attr('disabled', disableValue);
    $('#' + baseElementId + '-mins-input').attr('disabled', disableValue);
    $('#' + baseElementId + '-secs-input').attr('disabled', disableValue);
}

function GetDurationInputFieldValue(baseElementId) {
    var result = 0;

    var hours = trim($('#' + baseElementId + '-hours-input').val());
    var minutes = trim($('#' + baseElementId + '-mins-input').val());
    var seconds = trim($('#' + baseElementId + '-secs-input').val());

    if (hours.length > 0) {
	result += 60 * 60 * parseInt(hours, 10);
    }
    if (minutes.length > 0) {
	result += 60 * parseInt(minutes, 10);
    }
    if (seconds.length > 0) {
	result += parseInt(seconds, 10);
    }

    if (result == 0) {
	return '';
    }
    else if (result > 0) {
	return result;
    } else {
	return null; // NaN case due to parse problem
    }
}

function ValidateDurationInputField(baseElementId) {
    ClearError(baseElementId + '-error');
	
    if (GetDurationInputFieldValue(baseElementId) == null) {
        SetError(baseElementId + '-error', "Please enter a valid numerical duration.");
	return false;
    }

    return true;
}

function SerializeDurationInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId + '-field-key').attr('value');
    var value = GetDurationInputFieldValue(baseElementId);

    return MakeURLQuery([[fieldKey, value]]);
}
   
function FormatDuration(seconds) {
    var hours = Math.floor(seconds / (60 * 60));
    var remainder = seconds % (60 * 60);
    var minutes = Math.floor(remainder / 60);
    var seconds = remainder % 60;

    var result = "";

    var hrSep = " hr ";
    var minSep = " min ";
    var sSep = " s";
    
    if (hours > 0) {
	result += hours + hrSep;
    }
    if (minutes > 0) {
	result += minutes + minSep;
    }
    if (seconds > 0) {
	result += seconds + sSep;
    }
    
    return result;
}

// Posts a change to a duration date value asynchronously - meant to be paired with the mako function DocDurationControl
function UpdateDurationField(controlID, docID, fieldName, emptyDisplay) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var docID = docID;
    var docType = docType;
    var fieldName = fieldName;
    var fieldValue = GetDurationInputFieldValue(controlID + '-field');
    var emptyDisplay = emptyDisplay;

    // clear previous form errors
    $("#" + controlID + '-error').html("");
    hide(controlID + '-error');

    // form validation
    if (!ValidateDurationInputField(controlID + '-field'))
	return;

    var formdata = "docID=" + docID + "&" + fieldName + "=" + fieldValue;

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', 'other-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    if (fieldValue > 0) {
			$("#durationControlDisplayValue" + controlID).html(FormatDuration(fieldValue));
		    }
		    else {
			$("#durationControlDisplayValue" + controlID).html(emptyDisplay);
		    }
		    
		    ToggleEditControlOff(controlID);
                }
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
                else { // error
                    HandleModeratedChangeError(msg, controlID + '-error');
		}
	    }
	});
}

//*****
//* Media scripts
//*****

function BackfillMedia(thumbnailsElementId, playerElementId, pageSize) {
    // keep these vars in static scope for ajax cb
    var thumbnailsElementId = thumbnailsElementId;
    var playerElementId = playerElementId;
    
    // get values
    var jqoThumbnailsContainer = $('#' + thumbnailsElementId);
    var itemId = jqoThumbnailsContainer.find('.media-item-id:first').val();
    var controlsMode = jqoThumbnailsContainer.find('.media-controls-mode:first').val();
    var postData = MakeURLQuery([['itemId', itemId], ['pageSize', pageSize], ['thumbnailsContainerId', thumbnailsElementId], ['playerContainerId', playerElementId], ['controlsMode', controlsMode]]);

    // advise of request in UI
    var jqoPlaceholder = jqoThumbnailsContainer.find('.media-backfill-placeholder:first');
    jqoPlaceholder.css('display', 'block');

    var jqoError = jqoThumbnailsContainer.find('.error-message:first');
    
    $.ajax({
	    type: "POST",
		url: "/media/BackfillMedia",
		data: postData,
                error: function(request, status, error) {
		// HandleGenericErrorByJqo('8', jqoError); // do nothing as this can also occur on a page refresh interrupting the current async request
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));
		    jqoPlaceholder.replaceWith(results['html']);

		    jqoThumbnailsContainer.find('.media-paging:first').html(results['pagingHTML']);

		    var autoPlay = false;
		    if (jqoThumbnailsContainer.find('.media-auto-play:first').val().toLowerCase() == 'true')
			autoPlay = true;

		    var noResultsMessage = false;
		    if (jqoThumbnailsContainer.find('.media-backfill-no-results:first').length > 0)
			noResultsMessage = true;

		    var mediaCount = jqoThumbnailsContainer.find('.media-thumbnail-container').length;
		    if (mediaCount == 0) {
			if (noResultsMessage)
			    jqoThumbnailsContainer.find('.media-backfill-no-results:first').css('display', 'block');

			jqoThumbnailsContainer.parents('.media-thumbnails-section:first').find('.search-youtube-videos:first').html('View YouTube Videos');
		    }

		    var alreadyPlaying = ($('#' + playerElementId).css('display') == 'block');

		    if (!alreadyPlaying && mediaCount > 0 && autoPlay)
			AutoPlayMedia(thumbnailsElementId, playerElementId);
		}
		else { //1st char of msg will indicate failure
		    HandleGenericErrorByJqo(msg, jqoError);
		}
	    }
	});
}

function ToggleMediaThumbnail(caller, highlight) {
    var jqoCaller = $(caller);
    if (highlight)
	jqoCaller.find('.media-thumbnail-hover:first').css('display', 'block');
    else
	jqoCaller.find('.media-thumbnail-hover:first').css('display', 'none');
}

function PlayMedia(caller, playerElementId) {
    var jqoContainer = $(caller).parents('.media-thumbnail-container:first');
    var jqoPlayer = $('#' + playerElementId);

    // close the previous media
    var jqoCurrentThumbnailField = jqoPlayer.find('.media-thumbnail-container-id:first');
    if (jqoCurrentThumbnailField.length > 0) { // can't use jquery .click on close element due to IE 7- js quirks
	CloseMedia(jqoPlayer.find('.media-player-controls:first').get(0), jqoCurrentThumbnailField.val());
    }
    
    var title = trim(jqoContainer.find('.media-thumbnail-title:first').attr('title'));
    var snippet = decodeURIComponent(jqoContainer.find('.media-thumbnail-snippet:first').html());
    var controls = jqoContainer.find('.media-thumbnail-controls:first').html();

    jqoPlayer.find('.media-player-title:first').html(title);
    jqoPlayer.find('.media-player-snippet:first').html(snippet);

    var showControls = true;
    if (jqoContainer.parents('.media-thumbnails-container:first').find('.media-controls-mode:first').val() == 'NONE')
	showControls = false;

    if (showControls) {
	jqoPlayer.find('.media-player-controls:first').html(controls);
	// clear out source content to prevent element id collisions
	jqoContainer.find('.media-thumbnail-controls:first').html('');
    }
    else {
	jqoPlayer.find('.media-player-controls:first').css('display', 'none');
    }

    jqoPlayer.css('display', 'block');
}

function AutoPlayMedia(thumbnailsContainerId, playerContainerId) {
    var jqoThumbnails = $('#' + thumbnailsContainerId);

    var jqoMedia = jqoThumbnails.find('.media-thumbnail-container:first');
    if (jqoMedia.length == 0)
	return;

    var title = trim(jqoMedia.find('.media-thumbnail-title:first').attr('title'));
    var snippet = decodeURIComponent(jqoMedia.find('.media-thumbnail-snippet:first').html());
    var controls = jqoMedia.find('.media-thumbnail-controls:first').html();

    var jqoPlayer = $('#' + playerContainerId);
    jqoPlayer.find('.media-player-title:first').html(title);
    jqoPlayer.find('.media-player-snippet:first').html(snippet);

    var showControls = true;
    if (jqoThumbnails.find('.media-controls-mode:first').val() == 'NONE')
	showControls = false;

    if (showControls) {
	jqoPlayer.find('.media-player-controls:first').html(controls);
	// clear out source content to prevent element id collisions
	jqoMedia.find('.media-thumbnail-controls:first').html('');
    }
    else {
	jqoPlayer.find('.media-player-controls:first').css('display', 'none');
    }

    jqoPlayer.css('display', 'block');
}

// hacky workaround for IE issue where media keeps playing after closing a media dialog
function StopMedia(containerId) {
    var jqoSnippet = $('#' + containerId).find('.media-player-snippet:first');
    if (jqoSnippet.length == 1) {
	var html = jqoSnippet.html();
	jqoSnippet.html('');
	jqoSnippet.html(html);
    }
}

// caller can be any DOM object within the player container
function CloseMedia(caller, thumbnailElementId) {
    var jqoPlayer = $(caller).parents('.media-player-container:first');

    jqoPlayer.css('display', 'none');

    var controlsHTML = jqoPlayer.find('.media-player-controls:first').html();
    jqoPlayer.find('.media-player-title:first').html('&nbsp;');
    jqoPlayer.find('.media-player-snippet:first').html('&nbsp;');
    jqoPlayer.find('.media-player-controls:first').html('&nbsp;');

    var showControls = true;
    var jqoThumbnailContainer = $('#' + thumbnailElementId);
    if (jqoThumbnailContainer.length == 0) // may not exist if the user asynchronously paged thumbnails while watching this media
	return;

    if (jqoThumbnailContainer.parents('.media-thumbnails-container:first').find('.media-controls-mode:first').val() == 'NONE')
	showControls = false;

    if (showControls) {
	// clear out source content to prevent element id collisions
	jqoThumbnailContainer.find('.media-thumbnail-controls:first').html(controlsHTML);
    }
}

function EmbedBackfillMedia(caller) {
    var jqoPlayer = $(caller).parents('.media-player-container:first');
    var jqoEmbedLink = jqoPlayer.find('.media-embed:first');
    var jqoEmbeddedNote = jqoPlayer.find('.media-embedded:first');

    var title = trim(jqoPlayer.find('.media-player-title:first').html());
    var itemId = jqoPlayer.find('.media-item-id:first').val();
    var contentURL = jqoPlayer.find('.media-content-url:first').val();
    var contentType = jqoPlayer.find('.media-content-type:first').val();

    var postData = MakeURLQuery([['itemId', itemId], ['title', title], ['contentURL', contentURL], ['contentType', contentType]]);

    $.ajax({
            type: "POST",
                url: "/media/EmbedBackfillMedia",
                data: postData,
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    jqoEmbedLink.css('display', 'none');
		    jqoEmbeddedNote.css('display', 'inline');
                }
            }
        });
}

function UnembedMedia(itemId, contentId, dialogId, playerId, containerId) {
    // keep these vars in static scope for ajax cb
    var itemId = itemId;
    var contentId = contentId;
    var dialogId = dialogId;
    var playerId = playerId;
    var containerId = containerId;

    var mode = $('#' + containerId).find('.media-mode:first').val();

    // clear previous errors
    ClearDeleteErrors(dialogId);

    var postData = MakeURLQuery([['itemId', itemId], ['contentId', contentId], ['playerId', playerId], ['containerId', containerId], ['mode', mode]]);

    AdviseDeleteStart(dialogId);

    $.ajax({
            type: "POST",
                url: "/media/UnembedMedia",
                data: postData,
            error: function(request, status, error) {
                HandleDeleteError('8', dialogId);
                AdviseDeleteDone(dialogId); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    // refresh thumbnails
		    $('#' + containerId).replaceWith(msg.substr(1));

		    // closer player region
		    var jqoPlayer = $('#' + playerId);
		    jqoPlayer.css('display', 'none');
		    jqoPlayer.find('.media-player-title:first').html('&nbsp;');
		    jqoPlayer.find('.media-player-snippet:first').html('&nbsp;');
		    jqoPlayer.find('.media-player-controls:first').html('&nbsp;');
                }
                else { //1st char of msg will indicate failure
		    HandleDeleteError(msg, dialogId);
		    AdviseDeleteDone(dialogId); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function VoteUpMedia(contentId, votingControlBaseId) {
    // lexical scoping for callback
    var contentId = contentId;
    var votingControlBaseId = votingControlBaseId;

    var postData = MakeURLQuery([["contentId", contentId]]);

    ClearVoteUpError(votingControlBaseId);

    $.ajax({
            type: "POST",
                url: "/media/VoteUpMedia",
                data: postData,
		error: function(request, status, error) {
                VoteUpError(votingControlBaseId, '8');
            },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    VoteUpSet(votingControlBaseId);
                }
                else { //success
                    VoteUpError(votingControlBaseId, msg);
                }
            }
        });
}

function PageMediaThumbnails(containerId, playerId, itemId, offset, pageSize) {
    // keep these vars in static scope for ajax cb
    var containerId = containerId;
    var itemId = itemId;
    var offset = offset;
    var pageSize = pageSize;

    var mode = $('#' + containerId).find('.media-mode:first').val();
    var controlsMode = $('#' + containerId).find('.media-controls-mode:first').val();

    var jqoError = $('#' + containerId).find('.error-message:first');
    ClearErrorByJqo(jqoError);

    var postData = MakeURLQuery([['itemId', itemId], ['containerId', containerId], ['playerId', playerId], ['offset', offset], ['pageSize', pageSize], ['mode', mode], ['controlsMode', controlsMode]]);

    $.ajax({
            type: "POST",
            url: "/media/PageMedia",
            data: postData,
	    error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoError);
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') { //success
                    $('#' + containerId).replaceWith(msg.substr(1));
                }
                else { //1st char of msg will indicate failure
		    HandleGenericErrorByJqo(msg, jqoError);
                }
            }
        });
}

function ShowYouTubeVideos(itemId) {
    showAlert('youtube-videos-dialog');

    if (trim($('#youtube-videos').html()).length == 0) {
	SearchYouTubeVideos(itemId);
    }
}

function SearchYouTubeVideos(itemId) {
    ClearError('youtube-videos-search-error');

    var query = trim($('#youtube-videos-query').val());

    if (query.length == 0) {
	SetError('youtube-videos-search-error', 'Please enter a non-blank search.');
	
	return;
    }

    var postData = 'query=' + encodeURIComponent(query) + '&itemId=' + encodeURIComponent(itemId);

    // advise of search
    showBlock('youtube-videos-status');

    $.ajax({
	    type: "POST",
	    url: "/media/SearchYouTubeInline",
	    data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', 'youtube-videos-search-error');
	    },
            complete: function(request, status) {
		hide('youtube-videos-status');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#youtube-videos').html(msg.substr(1));
		}
		else { // handle errors
		    HandleGenericError(msg, 'youtube-videos-search-error');
		}
	    }
	});
}

function AddInlineYouTubeVideo(itemId, previewId) {
    // keep these vars in static scope for ajax cb
    var itemId = itemId;
    var previewId = previewId;

    // clear previous form errors
    ClearError(previewId + "-add-error");

    // grab content
    var title = trim($('#' + previewId + '-media-search-selection-title').val());
    var contentURL = trim($('#' + previewId + '-media-search-selection-url').html());
    var contentType = trim($('#' + previewId + '-media-search-selection-type').html());

    if (title.length == 0) {
	SetError(previewId + '-add-error', 'Please provide a title for the selected clip.');
	return;
    }

    var containerId = trim($('#youtube-videos-dialog').find('.youtube-videos-thumbnails-container-id:first').val());
    var playerId = trim($('#youtube-videos-dialog').find('.youtube-videos-player-container-id:first').val());
    var mode = $('#' + containerId).find('.media-mode:first').val();

    var postData = MakeURLQuery([['itemId', itemId], ["title", title], ["contentURL", contentURL], ['contentType', contentType], ['containerId', containerId], ['playerId', playerId], ['mode', mode]]);

    // freeze buttons and give status
    showBlock(previewId + '-add-status');
    ToggleButton(previewId + '-add', false);

    $.ajax({
	    type: "POST",
		url: "/media/EmbedYouTubeVideo",
		data: postData,
            error: function(request, status, error) {
		HandleEditError('8', previewId + "-add-error");
	    },
            complete: function(request, status) {
		hide(previewId + '-add-status');
		ToggleButton(previewId + '-add', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + containerId).replaceWith(msg.substr(1));

		    hide('youtube-videos-dialog');
		    StopMedia('youtube-videos-dialog');
		}
		else { // error
		    HandleEditError(msg, previewId + "-add-error");
		}
	    }
	});
}

function ToggleAddMedia(dialogID, defaultState) {
    if (defaultState) {
	showBlock(dialogID + '-search-results');

	hide(dialogID + '-snippet-options');
    }
    else {
	hide(dialogID + '-search-results');

	showBlock(dialogID + '-snippet-options');
    }
}

// Adds external media
function AddMedia(itemId, thumbnailsContainerId, playerContainerId, dialogId) {
    // check selection
    var selection = $('[name="' + dialogId + '-option"]:checked').val();

    if (selection == 'inline') {
	AddYouTubeVideo(itemId, thumbnailsContainerId, playerContainerId, dialogId);
	return;
    }
    else if (selection != 'snippet') {
	SetError(dialogId + '-error', 'Please select an option to add media.');
	return;
    }

    // keep these vars in static scope for ajax cb
    var itemId = itemId;
    var thumbnailsContainerId = thumbnailsContainerId;
    var playerContainerId = playerContainerId;
    var dialogId = dialogId;

    var title = trim($("#" + dialogId + "-snippet-title").val());
    var snippet = trim($("#" + dialogId + "-snippet-body").val());

    var mode = $('#' + thumbnailsContainerId).find('.media-mode:first').val();
    
    // clear previous form errors
    ClearError(dialogId + "-error");
    ClearError(dialogId + "-snippet-title-error");
    ClearError(dialogId + "-snippet-body-error");

    // form validation
    var error = false;
    if (title.length == 0) { // check for blank
        error = true;

        SetError("media-title-error", "Please enter a title for this media.");
    }
    if (snippet.length == 0) { // check for blank
        error = true;

	SetError("media-snippet-error", "Please paste the code snippet for this media.");
    }

    if (error) {
        return;
    }

    var postData = MakeURLQuery([['itemId', itemId], ['title', title], ['snippet', snippet], ['thumbnailsContainerId', thumbnailsContainerId], ['playerContainerId', playerContainerId], ['mode', mode]]);

    // freeze buttons and give status
    showBlock(dialogId + '-status');
    ToggleButton(dialogId + '-save', false);
    ToggleButton(dialogId + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/media/EmbedMediaViaSnippet",
		data: postData,
            error: function(request, status, error) {
		HandleEditError('8', dialogId + "-snippet-body-error");
	    },
                complete: function(request, status) {
		  hide(dialogId + '-status');
		  ToggleButton(dialogId + '-save', true);
		  ToggleButton(dialogId + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#" + thumbnailsContainerId).replaceWith(msg.substr(1));

		    hide(dialogId);
		}
		else { // error
		    if (msg.charAt(0) == '5') { // bad snippet
			SetError(dialogId + "-snippet-body-error", "We can't parse the snippet you're trying to embed. Please make sure this is a valid HTML snippet for embedding media from YouTube, MTV, Hulu, imeem, or SeeqPod.");
		    }
		    else {
			HandleEditError(msg, dialogId + "-snippet-body-error");
		    }
		}
	    }
	});
}

// helper called by AddMedia
function AddYouTubeVideo(itemId, thumbnailsContainerId, playerContainerId, dialogId) {
    // keep these vars in static scope for ajax cb
    var thumbnailsContainerId = thumbnailsContainerId;
    var dialogId = dialogId;

    // clear previous form errors
    ClearError(dialogId + "-error");
    ClearError(dialogId + "-search-error");

    // check for errors
    var jqoContentURL = $('#' + dialogId + '-media-search-selection-url');
    if (jqoContentURL.length == 0) {
	SetError(dialogId + '-search-error', 'Please search for the clip you want and click on it to make your selection.');
	return;
    }

    var title = trim($('#' + dialogId + '-media-search-selection-title').val());
    var contentURL = trim(jqoContentURL.html());
    var contentType = trim($('#' + dialogId + '-media-search-selection-type').html());
    if (contentURL.length == 0 || contentType.length == 0) {
	SetError(dialogId + '-search-error', 'Please select a clip to proceed.');
	return;
    }
    else if (title.length == 0) {
	SetError(dialogId + '-search-error', 'Please provide a title for the selected clip.');
	return;
    }

    var mode = $('#' + thumbnailsContainerId).find('.media-mode:first').val();

    var postData = MakeURLQuery([['itemId', itemId], ['title', title], ['contentURL', contentURL], ['contentType', contentType], ['containerId', thumbnailsContainerId], ['playerId', playerContainerId], ['mode', mode]]);

    // freeze buttons and give status
    showBlock(dialogId + '-status');
    ToggleButton(dialogId + '-save', false);
    ToggleButton(dialogId + '-cancel', false);

    $.ajax({
	    type: "POST",
	    url: "/media/EmbedYouTubeVideo",
	    data: postData,
            error: function(request, status, error) {
		HandleEditError('8', dialogId + "-error");
	    },
                complete: function(request, status) {
		  hide(dialogId + '-status');
		  ToggleButton(dialogId + '-save', true);
		  ToggleButton(dialogId + '-cancel', true);
	        },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + thumbnailsContainerId).replaceWith(msg.substr(1));

		    hide(dialogId);
		    StopMedia(dialogId);
		}
		else { // error
		    HandleEditError(msg, dialogId + "-error");
		}
	    }
	});
}

function SearchMedia(dialogID) {
    // keep these vars in static scope for ajax cb
    var dialogID = dialogID;

    ClearError(dialogID + '-search-error');

    var query = trim($('#' + dialogID + '-query').val());

    if (query.length == 0) {
	SetError(dialogID + '-search-error', 'Please enter a non-blank search.');
	
	return;
    }

    var postData = "dialogID=" + encodeURIComponent(dialogID) + "&query=" + encodeURIComponent(query);

    // advise of search
    $('#' + dialogID + '-search-results').html('<i>Searching...</i>');
    hide(dialogID + '-snippet-options');

    $.ajax({
	    type: "POST",
	    url: "/media/Search",
	    data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', dialogID + '-search-error');
		$('#' + dialogID + '-search-results').html('');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + dialogID + '-search-results').html(msg.substr(1));
		}
		else { // handle errors
		    HandleGenericError(msg, dialogID + '-search-error');
		    $('#' + dialogID + '-search-results').html('');
		}
	    }
	});
    
}

function PreviewMedia(dialogID, callingObject) {
    // grab selection title, url, type
    var jqoContainer = $(callingObject).parents('.media-search-result-container:first');
    var title = unescapeCharacterEntities(trim(jqoContainer.find('.media-search-result-title:first').html()));
    var contentURL = trim(jqoContainer.find('.media-search-result-url:first').html());
    var contentType = trim(jqoContainer.find('.media-search-result-type:first').html());

    // set the selection's title, url, type
    $('#' + dialogID + '-media-search-selection-title').val(title);
    $('#' + dialogID + '-media-search-selection-url').html(contentURL);
    $('#' + dialogID + '-media-search-selection-type').html(contentType);

    // draw & unhide the selection
    var width = 285;
    var height = 235;

    var snippet = '<object width="' + width + '" height="' + height + '"><param name="movie" value="' + contentURL + '&autoplay=1"></param><embed src="' + contentURL + '&autoplay=1" type="' + contentType + '" width="' + width + '" height="' + height + '" wmode="opaque"></embed></object>';
    $('#' + dialogID + '-media-search-selection-snippet').html(snippet);

    showBlock(dialogID + '-media-search-selection');
}

function RefreshVideoSearchInputField(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    var jqoStatus = jqoContainer.find('.video-search-status:first');
    var jqoError = jqoContainer.find('.video-search-error:first');

    ClearErrorByJqo(jqoError);

    var query = trim(jqoContainer.find('.video-search-query:first').val());

    if (query.length == 0) {
	SetErrorByJqo(jqoError, 'Please enter a non-blank search.');
	return;
    }

    var postData = MakeURLQuery([['query', query]]);

    // advise of search
    jqoStatus.css('display', 'block');

    $.ajax({
	    type: "POST",
	    url: "/media/SearchWebVideos",
	    data: postData,
            error: function(request, status, error) {
		HandleGenericErrorByJqo('8', jqoError);
	    },
            complete: function(request, status) {
		jqoStatus.css('display', 'none');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    jqoContainer.find(".video-search-results-container:first").html(msg.substr(1));
		}
		else { // handle errors
		    HandleGenericErrorByJqo(msg, jqoError);
		}
	    }
	});
}

function PreviewVideoSearchInputFieldResult(callingObject) {
    // grab selection title, url, type
    var jqoResult = $(callingObject).parents('.video-search-result:first');
    var title = unescapeCharacterEntities(trim(jqoResult.find('.video-search-result-title:first').html()));
    var contentURL = trim(jqoResult.find('.video-search-result-url:first').html());
    var contentType = trim(jqoResult.find('.video-search-result-type:first').html());

    // set the selection's title, url, type
    var jqoContainer = jqoResult.parents('.video-search-results-container:first');
    jqoContainer.find('.video-search-preview-title:first').val(title);

    // draw & unhide the selection
    var width = 350;
    var height = 288;

    var snippet = '<object width="' + width + '" height="' + height + '"><param name="movie" value="' + contentURL + '&autoplay=1"></param><embed src="' + contentURL + '&autoplay=1" type="' + contentType + '" width="' + width + '" height="' + height + '" wmode="opaque"></embed></object>';
    jqoContainer.find('.video-search-preview-snippet:first').html(snippet);
    jqoContainer.find('.video-search-preview-title:first').html(title);

    jqoContainer.find('.video-search-preview:first').css('display', 'block');
}

function GetVideoSearchInputFieldValue(baseElementId) {
    var jqoContainer = $('#' + baseElementId);

    var results = []
    jqoContainer.find('.video-search-result').each(function() {
	    var jqoResult = $(this);

	    if (!jqoResult.find('.video-search-select:first').attr('checked'))
		return;

	    var title = unescapeCharacterEntities(trim(jqoResult.find('.video-search-result-title:first').html()));
	    var contentURL = trim(jqoResult.find('.video-search-result-url:first').html());
	    var contentType = trim(jqoResult.find('.video-search-result-type:first').html());

	    results.push({'title': title, 'contentURL': contentURL, 'contentType': contentType});
	});

    return results;
}

function ValidateVideoSearchInputField(baseElementId) {
    var selections = GetVideoSearchInputFieldValue(baseElementId);

    var jqoContainer = $('#' + baseElementId);
    var jqoError = jqoContainer.find('.error-message:first');
    ClearErrorByJqo(jqoError);

    var maxSelection = parseInt(jqoContainer.find('.video-search-max-selection:first').attr('value'));

    if (selections.length > maxSelection) {
	SetErrorByJqo(jqoError, 'Please select no more than ' + maxSelection + ' images');
	return false;
    }

    return true;
}

function SerializeVideoSearchInputField(baseElementId) {
    return MakeURLQuery([['videos', JSON.stringify(GetVideoSearchInputFieldValue(baseElementId))]]);
}

function PreviewMediaInline(previewId, callingObject) {
    // grab selection title, url, type
    var jqoContainer = $(callingObject).parents('.media-search-result-container:first');
    var title = unescapeCharacterEntities(trim(jqoContainer.find('.media-search-result-title:first').html()));
    var contentURL = trim(jqoContainer.find('.media-search-result-url:first').html());
    var contentType = trim(jqoContainer.find('.media-search-result-type:first').html());

    // set the selection's title, url, type
    $('#' + previewId + '-media-search-selection-title').val(title);
    $('#' + previewId + '-media-search-selection-url').html(contentURL);
    $('#' + previewId + '-media-search-selection-type').html(contentType);

    // draw & unhide the selection
    var width = 425;
    var height = 350;

    var snippet = '<object width="' + width + '" height="' + height + '"><param name="movie" value="' + contentURL + '&autoplay=1"></param><embed src="' + contentURL + '&autoplay=1" type="' + contentType + '" width="' + width + '" height="' + height + '" wmode="opaque"></embed></object>';
    $('#' + previewId + '-media-search-selection-snippet').html(snippet);

    showBlock(previewId + '-media-search-selection');
    showBlock(previewId + '-add-area');
}

function VoteOnMedia(contentID, votingControlBaseID, vote) {
    // lexical scoping for callback
    var contentID = contentID;
    var votingControlBaseID = votingControlBaseID;
    var vote = vote;

    var postData = "contentID=" + contentID + "&vote=" + vote;

    $.ajax({
            type: "POST",
                url: "/item.py/VoteOnMedia",
                data: postData,
		error: function(request, status, error) {
                VoteError(votingControlBaseID, '8');
            },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    VoteSet(votingControlBaseID, vote, msg.substr(1));
                }
                else { //success
                    VoteError(votingControlBaseID, msg);
                }
            }
        });
}

// Removes leading whitespaces
function ltrim( value ) {
    
    var re = /\s*((\S+\s*)*)/;
    return value.replace(re, "$1");
    
}

// Removes ending whitespaces
function rtrim( value ) {
    
    var re = /((\s*\S+)*)\s*/;
    return value.replace(re, "$1");
    
}

// Removes leading and ending whitespaces
function trim( value ) {
    
    return ltrim(rtrim(value));
    
}

function insertlinebreaks(longstring) {
    var result = "";
    var linelength = 100;
    for (var i = 0; i <= longstring.length; i += linelength) {
	result += longstring.substr(i, linelength) + "\n";
    }
    
    return result;
}

//generates HTML for a button that removes an area on click
function RemoveButton(elementId, removecb) {
    return "<input type='button' value='Remove' onclick='$(\"#" + elementId + "\").remove(); " + removecb + " return false;' />";
}

//generates HTML for buttons that move sibling elements up and down
function MoveButton(elementId, minIndex) {
    var minIndex = (minIndex == null) ? 0 : minIndex;

    var moveup = "$(\"#" + elementId + "\").parent().children().filter(function (index) {return index >= " + minIndex + " && $(this).next().is(\"#" + elementId + "\");}).insertAfter(\"#" + elementId + "\");";
    var movedown = "$(\"#" + elementId + "\").parent().children().filter(function (index) {return $(this).prev().is(\"#" + elementId + "\");}).insertBefore(\"#" + elementId + "\");";    
    
    return "<input type='button' value='Move Up' onclick='" + moveup + "' return false;' />&nbsp;<input type='button' value='Move Down' onclick='" + movedown + "' return false;' />";
}

//*****
//* Structural relationship add functions
//*****

function GetStructuralRelationshipAnnotationInputFieldValue(baseElementId, jqoContainer) {
    if (jqoContainer == null) {
	jqoContainer = $('#' + baseElementId);
    }
    return jqoContainer.find('.pending-structural-relationship-annotation:first').val();
}

function ValidateStructuralRelationshipAnnotationInputFieldValue(baseElementId, jqoContainer) {
    if (jqoContainer == null)
	jqoContainer = $('#' + baseElementId);
    var jqoError = jqoContainer.find('.error-message:first');

    ClearErrorByJqo(jqoError);

    var value = jqoContainer.find('.pending-structural-relationship-annotation:first').val();

    var annotationType = jqoContainer.find('.pending-structural-relationship-annotation-type:first').val();

    var required = jqoContainer.find('.pending-structural-relationship-annotation-required:first').val();
    if (required.toLowerCase() == 'true') {
	required = true;
    }
    else {
	required = false;
    }

    var result = true;

    if (required && value.length == 0) {
	result = false;
	SetErrorByJqo(jqoError, 'This field is required');
    }
    if (value.length > 0 && annotationType == 'PERCENTAGE') {
	if (!ValidatePercentageString(value)) {
	    result = false;
	    SetErrorByJqo(jqoError, 'Please provide a number between 0 and 100');
	}
    }

    return result;
}

function QueueStructuralRelationship(itemId, escapedItemTitle, baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    var itemTitle = unescape(escapedItemTitle);

    if (itemId == null) { // new target item case
	itemTitle = AutoCapitalize(itemTitle);

	// copy entry template
	var templateHTML = jqoContainer.find('.add-structural-relationship-new-target-item-template:first').html();
	var pendingContainer = jqoContainer.find('.add-structural-relationship-listing:first');
	pendingContainer.append(templateHTML);

	// munge entry template
	var newEntry = pendingContainer.find('.pending-structural-relationship-entry:last');

	newEntry.find('.item-title:first').attr('value', itemTitle);

	pendingContainer.css('display', 'block');

	return;
    }
    else { // existing target item case
	// ignore if the item has already been queued
	if (jqoContainer.find('#pending-structural-relationship-target-item-id-' + itemId).length > 0)
	    return;

	// copy entry template
	var templateHTML = jqoContainer.find('.add-structural-relationship-template:first').html();
	var pendingContainer = jqoContainer.find('.add-structural-relationship-listing:first');
	pendingContainer.append(templateHTML);

	// munge entry template
	var newEntry = pendingContainer.find('.pending-structural-relationship-entry:last');

	newEntry.find('.item-id:first').attr('value', itemId);
	newEntry.find('.item-id:first').attr('id', 'pending-structural-relationship-target-item-id-' + itemId);
	newEntry.find('.item-link:first').attr('href', GetItemDetailsURL(itemId, itemTitle));
	newEntry.find('.item-link:first').html(itemTitle);

	pendingContainer.css('display', 'block');

	return;
    }
}

function RemovePendingStructuralRelationship(callingObject) {
    $(callingObject).parents('.pending-structural-relationship-entry:first').remove();
}

function UpdatePendingStructuralRelationshipItemTypes(callingObject, itemTypeConstraints) {
    var jqoContainer = $(callingObject).parents('.pending-structural-relationship-entry:first');
    var jqoCategory = jqoContainer.find('.new-target-item-category:first');
    var jqoItemType = jqoContainer.find('.new-target-item-type:first');
    
    if (jqoCategory.val() != '') {
	UpdateDocTypeByJQO(jqoItemType, jqoCategory, itemTypeConstraints);
	jqoItemType.val(''); // work around browser rendering blank but selecting a non-blank value
    }
}

function GetAddStructuralRelationshipInputFieldRelationshipType(baseElementId) {
    return $('#' + baseElementId).find('.add-structural-relationship-type:first').attr('value');
}

function GetAddStructuralRelationshipInputFieldSourceItemId(baseElementId) {
    var jqoSourceItemId = $('#' + baseElementId).find('.add-structural-relationship-source-item-id:first');
    if (jqoSourceItemId.length > 0)
	return jqoSourceItemId.attr('value');
    return null;
}

function GetAddStructuralRelationshipInputFieldValue(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    var relationshipType = GetAddStructuralRelationshipInputFieldRelationshipType(baseElementId);

    var pendingRelationships = []
    jqoContainer.find('.add-structural-relationship-listing:first').find('.pending-structural-relationship-entry').each(function() {
	    jqoEntry = $(this);

	    var itemId = null;
	    var itemTitle = null;
	    var itemType = null;
	    var category = null;

	    if (jqoEntry.hasClass('new-target-item')) {
		itemTitle = trim(jqoEntry.find('.item-title:first').val());
		if (itemTitle.length == 0)
		    itemTitle = null;

		itemType = jqoEntry.find('.new-target-item-type:first').val();
		if (itemType.length == 0)
		    itemType = null;

		category = jqoEntry.find('.new-target-item-category:first').val();
		if (category.length == 0)
		    category = null;
	    }
	    else {
		itemId = jqoEntry.find('.item-id:first').attr('value');
	    }

	    var annotation = null;

	    var jqoAnnotation = jqoEntry.find('.pending-structural-relationship-annotation-container:first');
	    if (jqoAnnotation.length > 0) {
		var annotation = GetStructuralRelationshipAnnotationInputFieldValue(null, jqoAnnotation);
	    }

	    var relationship = { 'relationshipType': relationshipType, 'targetItemId': itemId, 'targetItemTitle': itemTitle, 'targetItemType': itemType, 'targetItemCategory': category, 'comment': annotation };

	    pendingRelationships.push(relationship);
	});

    return pendingRelationships;
}

function ValidateAddStructuralRelationshipInputField(baseElementId, required) {
    var required = required;
    if (required == null || !required)
	required = false;
    else
	required = true;
	    
    var jqoContainer = $('#' + baseElementId);
    var maxCount = jqoContainer.find('.add-structural-relationship-max-count:first').attr('value');
    var relationshipCount = 0;

    var result = true;

    var jqoOtherError = jqoContainer.find('.other-error:first');
    jqoOtherError.html('&nbsp;');
    jqoOtherError.css('display', 'none');

    jqoContainer.find('.add-structural-relationship-listing:first').find('.pending-structural-relationship-entry').each(function() {
	    var jqoEntry = $(this);
	    relationshipCount += 1;

	    // Clear errors
	    var jqoTitleError = jqoEntry.find('.item-title-error:first');
	    var jqoTypeError = jqoEntry.find('.item-type-error:first');
	    var jqoCategoryError = jqoEntry.find('.item-category-error:first');
	    jqoTitleError.html('&nbsp;');
	    jqoTypeError.html('&nbsp;');
	    jqoCategoryError.html('&nbsp;');
	    jqoTitleError.css('display', 'none');
	    jqoTypeError.css('display', 'none');
	    jqoCategoryError.css('display', 'none');

	    if (jqoEntry.hasClass('new-target-item')) {
		itemTitle = trim(jqoEntry.find('.item-title:first').val());
		if (itemTitle.length == 0) {
		    result = false;

		    jqoTitleError.html('Provide a title or remove this entry');
		    jqoTitleError.css('display', 'block');
		}

		itemType = jqoEntry.find('.new-target-item-type:first').val();
		if (itemType.length == 0) {
		    result = false;

		    jqoTypeError.html('Select a page type');
		    jqoTypeError.css('display', 'block');
		}

		category = jqoEntry.find('.new-target-item-category:first').val();
		if (category.length == 0) {
		    result = false;

		    jqoCategoryError.html('Select a category');
		    jqoCategoryError.css('display', 'block');
		}
	    }

	    var jqoAnnotation = jqoEntry.find('.pending-structural-relationship-annotation-container:first');
	    if (jqoAnnotation.length > 0) {
		if (!ValidateStructuralRelationshipAnnotationInputFieldValue(null, jqoAnnotation)) {
		    result = false;
		    return;
		}
	    }
	});

    if (required && relationshipCount == 0) {
	SetErrorByJqo(jqoOtherError, 'You must add at least one entry to proceed');
	result = false;
    }

    if (relationshipCount > maxCount) {
	SetErrorByJqo(jqoOtherError, 'You\'ve added too many entries. You can only add up to ' + maxCount + ' entries.');
	result = false;
    }

    return result;
}

function SerializeAddStructuralRelationshipInputField(baseElementId) {
    var pendingRelationships = GetAddStructuralRelationshipInputFieldValue(baseElementId);

    var jqoContainer = $('#' + baseElementId);
    var relationshipType = GetAddStructuralRelationshipInputFieldRelationshipType(baseElementId);
    
    return MakeURLQuery([['sRel' + relationshipType, JSON.stringify(pendingRelationships)]]);    
}

function IsAddStructuralRelationshipInputFieldAnnotationAvailable(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    return (jqoContainer.find('.pending-structural-relationship-annotation-container:first').length > 0);
}

function ClearAddStructuralRelationshipInputField(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    jqoContainer.find('.add-structural-relationship-listing:first').html('&nbsp;');
}

function ToggleAddStructuralRelationshipInputFieldDialog(baseElementId, freeze) {
    var jqoContainer = $('#' + baseElementId);
    var jqoStatus = jqoContainer.find('.add-structural-relationship-dialog-status:first');
    var jqoSave = jqoContainer.find('.add-structural-relationship-dialog-save:first');
    var jqoCancel = jqoContainer.find('.add-structural-relationship-dialog-cancel:first');
    if (freeze) {
	jqoStatus.css('display', 'block');
	ToggleButtonByJqo(jqoSave, false);
	ToggleButtonByJqo(jqoCancel, false);
    }
    else {
	jqoStatus.css('display', 'none');
	ToggleButtonByJqo(jqoSave, true);
	ToggleButtonByJqo(jqoCancel, true);
    }
}

function ClearAddStructuralRelationshipInputFieldDialogError(baseElementId) {
    ClearErrorByJqo($('#' + baseElementId).find('.add-structural-relationship-dialog-error:first'));
}

function HandleAddStructuralRelationshipInputFieldDialogError(baseElementId, errorCode) {
    var jqoError = $('#' + baseElementId).find('.add-structural-relationship-dialog-error:first');
    HandleAddRelationshipError(null, errorCode, jqoError);
}

function InitializeAddStructuralRelationshipInputFieldEntry(baseElementId, itemId, itemTitle, dialogMode) {
    // copy entry template
    var jqoContainer = $('#' + baseElementId);

    if (itemId  == null) {
	var templateHTML = jqoContainer.find('.add-structural-relationship-new-target-item-template:first').html();
	var pendingContainer = jqoContainer.find('.add-structural-relationship-listing:first');
        pendingContainer.append(templateHTML);

        // munge entry template
        var newEntry = pendingContainer.find('.pending-structural-relationship-entry:last');

        newEntry.find('.item-title:first').attr('value', itemTitle);
	if (dialogMode)
	    newEntry.find('.remove-pending-structural-relationship:first').remove();

        pendingContainer.css('display', 'block');
    }
    else { // existing target item case
        // ignore if the item has already been queued
        if (jqoContainer.find('#pending-structural-relationship-target-item-id-' + itemId).length > 0)
            return;

        // copy entry template
        var templateHTML = jqoContainer.find('.add-structural-relationship-template:first').html();
        var pendingContainer = jqoContainer.find('.add-structural-relationship-listing:first');
        pendingContainer.append(templateHTML);

        // munge entry template
        var newEntry = pendingContainer.find('.pending-structural-relationship-entry:last');

        newEntry.find('.item-id:first').attr('value', itemId);
        newEntry.find('.item-id:first').attr('id', 'pending-structural-relationship-target-item-id-' + itemId);
        newEntry.find('.item-link:first').attr('href', GetItemDetailsURL(itemId, itemTitle));
        newEntry.find('.item-link:first').html(itemTitle);

	if (dialogMode)
	    newEntry.find('.remove-pending-structural-relationship:first').remove();

        pendingContainer.css('display', 'block');

        return;
    }
}


//*****
//* Simple structural relationship control scripts
//*****

function ProposeSimpleStructuralRelationship(itemId, escapedItemTitle, adderBaseElementId) {
    var jqoAdderContainer = $('#' + adderBaseElementId);
    var itemTitle = unescape(escapedItemTitle);
    itemTitle = AutoCapitalize(itemTitle);

    if (IsAddStructuralRelationshipInputFieldAnnotationAvailable(adderBaseElementId + '-dialog')) {
	ClearAddStructuralRelationshipInputField(adderBaseElementId + '-dialog');
	ClearAddStructuralRelationshipInputFieldDialogError(adderBaseElementId + '-dialog');
	InitializeAddStructuralRelationshipInputFieldEntry(adderBaseElementId + '-dialog', itemId, itemTitle, true);
	showDialog(adderBaseElementId + '-dialog');
    }
    else if (itemId == null) {
	ClearAddStructuralRelationshipInputField(adderBaseElementId + '-dialog');
	ClearAddStructuralRelationshipInputFieldDialogError(adderBaseElementId + '-dialog');
	InitializeAddStructuralRelationshipInputFieldEntry(adderBaseElementId + '-dialog', itemId, itemTitle, true);
	showDialog(adderBaseElementId + '-dialog');
    }
    else {
	AddSimpleStructuralRelationship(adderBaseElementId, itemId);
    }
}

function AddSimpleStructuralRelationship(adderBaseElementId, itemId) {
    // bring into scope
    var adderBaseElementId = adderBaseElementId;
    var itemId = itemId;
    var inline = true;
    var relationships = null;
    if (itemId == null) { // annotation dialog case
	inline= false;
	if (!ValidateAddStructuralRelationshipInputField(adderBaseElementId))
	    return;
	relationships = GetAddStructuralRelationshipInputFieldValue(adderBaseElementId);
    }
    else {
	relationships = [{ 'relationshipType': GetAddStructuralRelationshipInputFieldRelationshipType(adderBaseElementId), 'targetItemId': itemId, 'targetItemTitle': null, 'targetItemType': null, 'targetItemCategory': null, 'comment': null }];
    }

    // keep these vars in static scope for ajax cb
    var baseElementId = adderBaseElementId.replace('-add-srel', '');
    var viewId = baseElementId + '-view';
    var editId = baseElementId + '-edit';

    var sourceItemId = GetAddStructuralRelationshipInputFieldSourceItemId(adderBaseElementId);
    
    // count number of existing relations
    var offset = $('#' + viewId).children().length;
    
    var postData = MakeURLQuery([["baseElementId", baseElementId], ["sourceItemId", sourceItemId], ['&offset', offset], ['relationships', JSON.stringify(relationships)]]);

    // show status during save
    var jqoError = null;
    if (inline) {
	showBlock(adderBaseElementId + '-status');
	jqoError = $('#' + adderBaseElementId + '-error');
    }
    else {
	ToggleAddStructuralRelationshipInputFieldDialog(adderBaseElementId, true);
    }

    $.ajax({
	    type: "POST",
	    url: "/relationships/AddSimpleStructuralRelationships",
	    data: postData,
            error: function(request, status, error) {
		if (inline)
		    HandleAddRelationshipError(null, '8', jqoError);
		else
		    HandleAddStructuralRelationshipInputFieldDialogError(adderBaseElementId, '8');
	    },
            complete: function(request, status) {
		if (inline) {
		    hide(adderBaseElementId + '-status');
		}
		else {
		    ToggleAddStructuralRelationshipInputFieldDialog(adderBaseElementId, false);
		}
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));
		    view = results['viewHtml'];
		    edit = results['editHtml'];

		    if (offset == 0) {
			$('#' + viewId).html(''); // clear special messages when there are 0 relations
			$('#' + editId).html('');
		    }

		    $('#' + viewId).append(view);
		    $('#' + editId).append(edit);

		    // if there are now multiple entries, show a notice about order changing
		    if (offset + relationships.length > 1) {
			showBlock(baseElementId + '-add-note');
		    }

		    if (!inline)
			hide(adderBaseElementId + '-dialog');
		}
                else if (msg.charAt(0) == 'Q') {
		    if (!inline)
			hide(adderBaseElementId + '-dialog');

		    var notice = msg.substr(1);

		    $('#change-queued-partial-message').html(notice);
		    showAlert('change-queued-partial');
                }
		else { //1st char of msg will indicate failure
		    if (inline)
			HandleAddRelationshipError(null, msg.charAt(0), jqoError);
		    else
			HandleAddStructuralRelationshipInputFieldDialogError(adderBaseElementId, msg.charAt(0));
		}
	    }
	});
}

//*****
//* Thumbnail structural relationship scripts
//*****

function AddThumbnailStructuralRelationships(adderBaseElementId, containerId) {
    // keep these vars in static scope for ajax cb
    var adderBaseElementId = adderBaseElementId;
    var containerId = containerId;

    var jqoAdder = $('#' + adderBaseElementId);
    var jqoError = jqoAdder.find('.add-thumbnail-relationships-error:first');

    // clear previous errors
    ClearErrorByJqo(jqoError);

    // form validation
    if (!ValidateAddStructuralRelationshipInputField(adderBaseElementId, true))
	return;

    // prepare post
    var sourceItemId = GetAddStructuralRelationshipInputFieldSourceItemId(adderBaseElementId);
    var relationships = GetAddStructuralRelationshipInputFieldValue(adderBaseElementId);
    
    var postData = MakeURLQuery([["sourceItemId", sourceItemId], ["containerId", containerId], ['relationships', JSON.stringify(relationships)]]);

    // freeze buttons during save
    var jqoStatus = jqoAdder.find('.add-thumbnail-relationships-status:first');
    var jqoSave = jqoAdder.find('.add-thumbnail-relationships-save:first');
    var jqoCancel = jqoAdder.find('.add-thumbnail-relationships-cancel:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoSave, false);
    ToggleButtonByJqo(jqoCancel, false);

    $.ajax({
	    type: "POST",
		url: "/relationships/AddThumbnailStructuralRelationships",
		data: postData,
                error: function(request, status, error) {
		HandleAddRelationshipError(null, '8', jqoError);
	        },
                complete: function(request, status) {
		jqoStatus.css('display', 'none');
		ToggleButtonByJqo(jqoSave, true);
		ToggleButtonByJqo(jqoCancel, true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#" + containerId).append(msg.substr(1));

		    hide(adderBaseElementId);
		    ClearAddStructuralRelationshipInputField(adderBaseElementId);
		}
                else if (msg.charAt(0) == 'Q') {
		    hide(adderBaseElementId);
		    ClearAddStructuralRelationshipInputField(adderBaseElementId);

		    var notice = msg.substr(1);

		    $('#change-queued-partial-message').html(notice);
		    showAlert('change-queued-partial');
                }
		else { // handle errors
		    HandleAddRelationshipError(null, msg, jqoError);
		}
	    }
	});
}

function ToggleThumbnailRelationshipEdit(caller, containerId, addContainerId, enable) {
    var jqoCaller = $(caller);
    var jqoContainer = $('#' + containerId);
    var jqoAddContainer = $('#' + addContainerId);
    var jqoEditContainers = jqoContainer.find('.thumbnail-relationship-edit-controls');
    var jqoEditContainers2 = jqoAddContainer.find('.thumbnail-relationship-edit-controls');

    if (enable) {
	jqoCaller.css('display', 'none');
	jqoCaller.next().css('display', 'inline');
	jqoEditContainers.css('display', 'block');
	jqoEditContainers2.css('display', 'block');
    }
    else {
	jqoCaller.css('display', 'none');
	jqoCaller.prev().css('display', 'inline');
	jqoEditContainers.css('display', 'none');
	jqoEditContainers2.css('display', 'none');
    }
}

//*
//* Add relationship shared functions - note naming convention has become deprecated, aka Doc Picker (DP)
//*

function RelAdderGetRelTypes(elementID) {
    var relTypes = new Array();

    $('#' + elementID).find('.rel-types').each(function() {
	    // only grab instances, not the hidden template
	    if ( $(this).parents('.new-doc-template').length == 0 &&
		 $(this).parents('.annotation-template').length == 0 ) {

		var typeBundle = new Array();
		$(this).children('[type="checkbox"]').each(function() {
			if ($(this).attr('checked')) {
			    typeBundle.push($(this).attr('value'));
			}
		    });
		relTypes.push(typeBundle);
	    }
	});


    return relTypes;
}

function HandleAddRelationshipError(controlID, msg, jqoError) {
    if (jqoError == null)
	jqoError = $('#error' + controlID);

    if (msg.charAt(0) == 'D') {
	var title = msg.substr(1);

	SetErrorByJqo(jqoError, 'The new page you\'re trying to create titled "' + title + '" seems to already exist. See if you can find the existing page via the search results on the left. If the existing page is what you\'re looking for, add it and remove the new page you\'re trying to create. Otherwise, disambiguate the title for the new page. For example, two versions of the movie Batman can be differentiated like so: "Batman (1989)" vs. "Batman (1966).');
    }
    else {
	HandleRelationError(msg, null, jqoError);
    }
}

//*
//* End of shared add relationship functions
//*

function SetGenericError(errorChar, elementId) {
    SetGenericErrorByJqo(errorChar, $('#' + elementId));
}

function SetGenericErrorByJqo(serverMsg, jqo) {
    if (serverMsg.charAt(0) == '2') { // permission denied
        jqo.html(_genericPermissionDenied);
    }
    else if (serverMsg.charAt(0) == '3') { // not signed in
        jqo.html(_notSignedIn);
    }
    else if (serverMsg.charAt(0) == '4') {
	jqo.html("The item you're trying to interact with is no longer available.  It may have been deleted. Try reloading the page to see if the item still exists.");
    }
    else if (serverMsg.charAt(0) == '7') {
        jqo.html(_requiresModeration);
    }
    else if (serverMsg.charAt(0) == '8') { // timeout
        jqo.html(_timeOut);
    }
    else { // unexpected
        jqo.html(_unexpectedError);
    }
}

function HandleGenericError(serverMsg, errorElementID) {
    HandleGenericErrorByJqo(serverMsg, $('#' +errorElementID));
}

function HandleGenericErrorByJqo(serverMsg, jqo) {
    SetGenericErrorByJqo(serverMsg, jqo);
    if (jqo.get(0).tagName.toLowerCase() == 'div')
	jqo.css('display', 'block');
    else
	jqo.css('display', 'inline');
}

function HandleModeratedChangeError(serverMsg, errorElementID) {
    if (serverMsg.charAt(0) == '6' || serverMsg.charAt(0) == 'A') { // duplicate error
	SetError(errorElementID, 'It seems like you have already submitted this request or voted as a moderator on this or a related request from your current account or computer. You cannot submit another request on this item until pending changes on this item complete the moderation process.');
    }
    else {
	HandleGenericError(serverMsg, errorElementID);
    }
}

function HandleEditErrorByJqo(serverMsg, jqo) {
    if (serverMsg.charAt(0) == '6') { // duplicate error
        SetErrorByJqo(jqo, _duplicateError);
    }
    else {
	HandleGenericErrorByJqo(serverMsg, jqo);
    }
}

function HandleEditError(serverMsg, errorElementID) {
    HandleEditErrorByJqo(serverMsg, $('#' +errorElementID));
}

function HandleRelationshipErrorByJqo(serverMsg, jqoError) {
    HandleRelationError(serverMsg, null, jqoError);
}

function HandleRelationError(serverMsg, errorElementID, jqoError) {
    if (jqoError == null)
	jqoError = $("#" + errorElementID);

    if (serverMsg.charAt(0) == '2') { // permission denied
	jqoError.html(_addRelationDenied);
    }
    else if (serverMsg.charAt(0) == '3') { // not signed in
	jqoError.html(_notSignedIn);
    }
    else if (serverMsg.charAt(0) == '5') { // user errors specific to relation adding scenariso
	jqoError.html("You're either trying to add the current page as an entry under one of its sections or you're trying to add too many entries under this section. Please try adding fewer entries or different entries. Note that in some cases, you may be unable to add new entries to a section with too many existing entries.");
    }
    else if (serverMsg.charAt(0) == '6') {
	jqoError.html("You're trying to add an entry that already exists. You may need to refresh the page to see this if someone added this entry recently.");
    }
    else if (serverMsg.charAt(0) == 'A') {
	jqoError.html('You\'re trying to add an entry that you tried to add before or that you voted on as a moderator using your current account or computer. Because the addition of this entry is pending consensus, you cannot request to add it again until the pending change completes the moderation process.');
    }
    else if (serverMsg.charAt(0) == '8') { // timeout
        jqoError.html(_timeOut);
    }
    else { // unexpected
	HandleGenericErrorByJqo(serverMsg, jqoError);
    }
    
    if (jqoError.get(0).tagName.toLowerCase() == 'div')
        jqoError.css('display', 'block');
    else
        jqoError.css('display', 'inline');
}

function HandleContactError(serverMsg, errorElementID) {
    if (serverMsg.charAt(0) == '2') { // duplicate error
        SetError(errorElementID, "The user you selected has blocked you from doing this.");
    }
    if (serverMsg.charAt(0) == 'L') {
        SetError(errorElementID, "Either you or the contact has reached a limit in the maximum number of contacts. Users cannot follow more than 100 other users or have more than 5000 followers, friends, or blocked contacts.");
    }
    else {
	HandleGenericError(serverMsg, errorElementID);
    }
}

function HandleContactErrorByJqo(serverMsg, jqoError) {
    if (serverMsg.charAt(0) == '2') { // duplicate error
        SetErrorByJqo(jqoError, "The user you selected has blocked you from doing this.");
    }
    if (serverMsg.charAt(0) == 'L') {
        SetErrorByJqo(jqoError, "Either you or the contact has reached a limit in the maximum number of contacts. Users cannot follow more than 100 other users or have more than 5000 followers, friends, or blocked contacts.");
    }
    else {
	HandleGenericErrorByJqo(serverMsg, jqoError);
    }
}

//*****
//* Structural relationship related scripts
//*****

function EditStructuralRelationship(controlID, relID, requireComment) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var parts = controlID.split('-edit-dialog-');
    var rootID = parts[0];
    var relID = relID;

    // clear previous errors
    ClearError(controlID + "-error");

    // form validation
    var comment = $('.' + controlID + '-annotation-class:first').val();
    comment = trim(comment);
    if (comment.length == 0 && requireComment) { // check the form
	SetError(controlID + '-error', 'Please fill in the comment.');

	return
    }

    var postData = "id=" + relID + "&comment=" + encodeURIComponent(comment);

    // freeze buttons during save
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/relationships.py/EditStructuralRelationship",
		data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var viewComment = '';
		    var editComment = '';
		    if (comment.length > 0) {
			viewComment = ' (' + comment + ')';
			editComment = '(' + comment + ')';
		    }
		    
		    $('#' + rootID + '-view-comment-' + relID).html(viewComment);
		    $('#' + rootID + '-edit-comment-' + relID).html(editComment);
		    
		    hide(controlID);
		}
                else if (msg.charAt(0) == 'Q') {
                    showAlert('change-queued');
		    hide(controlID);
                }
		else { // handle errors
		    HandleRelationError(msg, controlID + '-error');
		}
	    }
	});
}

function DeleteSRelationship(callingObj) {
    // lexical scoping for CB
    var editID = $(callingObj).parents('.rel-edit-row:first').attr('id');
    var parts = editID.split('-edit-');
    var rootID = parts[0];
    var relID = parts[1];
    var controlID = rootID + '-delete-' + relID;

    var viewID = rootID + '-view-' + relID;
    var viewContainer = rootID + '-view';    
    var editContainer = rootID + '-edit';    

    var postData = "id=" + relID;

    // clear previous errors
    ClearDeleteErrors(controlID);

    AdviseDeleteStart(controlID);

    $.ajax({
            type: "POST",
            url: "/relationships.py/DeleteStructuralRelationship",
            data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlID);
                AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var leadingRemoved = false;
		    if ($('#' + viewContainer).children().eq(0).attr('id') == viewID) {
			leadingRemoved = true;
		    }

		    $("#" + viewID).remove();
		    $("#" + editID).remove();

		    // handle case where all relations are removed
		    if ($('#' + viewContainer).children().length == 0) {
			$("#" + viewContainer).html('Unavailable');
			$("#" + editContainer).html('<tr><td>Be the first to add to this section!</td><td>&nbsp;</td></tr>');
		    }
		    else if (leadingRemoved) {
			// remove the annoying leading comma if the relation removed was the first one
			var html = $("#" + viewContainer).html();
			var leadingCommaPos = html.indexOf(', ');
			html = html.substring(0, leadingCommaPos) + html.substr(leadingCommaPos + 2);
			$("#" + viewContainer).html(html);
		    }

		    hide(controlID);
                }
                else { // handle errors
                    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function DeleteItem(controlID, itemID) {
    // lexical scoping for CB
    var controlID = controlID;
    var itemID = itemID;

    var postData = "itemID=" + itemID;

    // clear previous errors
    ClearDeleteErrors(controlID);

    AdviseDeleteStart(controlID);

    $.ajax({
            type: "POST",
                url: "/item.py/DeleteItem",
                data: postData,
	    error: function(request, status, error) {
                HandleDeleteError('8', controlID);
                AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    window.location = '/deleted/' + itemID;
                }
                else { // handle errors
                    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

//*****
//* Flagging related scripts
//*****

function FlagInit(controlID) {
    showDialog(controlID);

    // workaround FF form caching issue
    if ($('#' + controlID + '-reason').length > 0) {
	$('#' + controlID + '-reason').val('');
    }
}

function FlagIssue(controlID, targetType, targetID) {
    // lexical scoping for CB
    var controlID = controlID;
    var targetType = targetType;
    var targetID = targetID;


    // clear previous errors and check form
    ClearError(controlID + "-error");
    
    var reason = null;
    var comment = null;
    var dupeSelection = null;
    var jqoReason = $("#" + controlID + "-reason");
    if (jqoReason.length > 0) {
	ClearError(controlID + "-reason-error");
	reason = jqoReason.val();

	if (reason.length == 0) {
	    SetError(controlID + "-reason-error", "Please select a reason for flagging this content.");
	    return
	}
	else if (reason == 'COPYRIGHT') {
	    SetError(controlID + "-error", 'Please follow the instructions highlighted to file a formal complaint against this content.');
	    return
	}
	else if (reason == 'DUPLICATE') {
	    ClearError(controlID + "-dupe-error");

	    if ($("[name='" + controlID + "-dupe-selection']:checked").length == 0) {
		SetError(controlID + "-dupe-error", "Please select the duplicated content.");
		return
	    }

	    dupeSelection = $("[name='" + controlID + "-dupe-selection']:checked").val();
	}
	else {
	    comment = trim($('#' + controlID + '-comment').val());
	}
    }

    var formdata = "targetType=" + targetType + "&targetID=" + targetID;
    if (reason != null) {
	formdata += '&reason=' + reason;

	if (reason == 'DUPLICATE') {
	    formdata += '&dupeTargetID=' + dupeSelection;
	}
	else {
	    formdata += '&comment=' + encodeURIComponent(comment);
	}
    }

    // disable buttons and advise of status
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-submit', false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
            type: "POST",
                url: "/item.py/Flag",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-submit', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    hide(controlID + '-submit-view');
                    showBlock(controlID + '-success-view');
                }
                else { // handle errors
                    HandleModeratedChangeError(msg, controlID + '-error');
                }
            }
        });
}

function FlagControlReasonChanged(controlID) {
    ClearError(controlID + "-error");
    ClearError(controlID + "-reason-error");
    ClearError(controlID + "-dupe-error");

    var reason = $('#' + controlID + '-reason').val();
    if (reason == 'COPYRIGHT') {
	$('#' + controlID + '-comment-row').css('display', '');
	hide(controlID + '-comment');
	hide(controlID + '-select-dupe');
	showBlock(controlID + '-copyright-notice');
    }
    else if (reason == 'DUPLICATE') {
	hide(controlID + '-comment-row');
	$('#' + controlID + '-select-dupe').css('display', '');
	$('#' + controlID + '-dupe-search-init').click();
    }
    else {
	$('#' + controlID + '-comment-row').css('display', '');
	showInline(controlID + '-comment');
	hide(controlID + '-select-dupe');
	hide(controlID + '-copyright-notice');
    }
}

function DupeItemSearchSubmit(controlID, itemID) {
    var query = $('#' + controlID + '-dupe-item-search').val();
    if (query.length == 0) {
	hide(controlID + '-dupe-results');
	$('#' + controlID + '-search-note').html('Please enter a non-blank query.');
	showBlock(controlID + '-search-note');
	return
    }

    DupeItemSearch(controlID, itemID, query, 0);
}

function DupeItemSearch(controlID, itemID, query, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var itemID = itemID;

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var formdata = "controlID=" + encodeURIComponent(controlID) + "&itemID=" + itemID + "&offset=" + offset;
    if (query == -1) { // the code to read the query off the page for paging
	formdata += '&query=' + encodeURIComponent($('#' + controlID + '-query-for-paging').html());
    }
    else if (query != null) {
	formdata += '&query=' + encodeURIComponent(query);
    } else {
	$('#' + controlID + '-dupe-item-search').val($('#' + controlID + '-item-title').html());
    }
    $('#' + controlID + '-search-note').html('Searching...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');

    $.ajax({
            type: "POST",
                url: "/flag.py/DupeItemSearch",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleEditError(msg, controlID + '-dupe-error');
                }
            }
        });
}

function DupeItemDescriptionSearch(controlID, descriptionID, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var descriptionID = descriptionID;

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var postData = "controlID=" + encodeURIComponent(controlID) + "&descriptionID=" + descriptionID + "&offset=" + offset;
    $('#' + controlID + '-search-note').html('Loading...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');

    $.ajax({
            type: "POST",
                url: "/flag.py/DupeItemDescriptionSearch",
                data: postData,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleEditError(msg, controlID + '-dupe-error');
                }
            }
        });
}    

function DupeXRelationshipList(controlID, relID, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var relID = relID;

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var formdata = "controlID=" + encodeURIComponent(controlID) + "&relID=" + relID + "&offset=" + offset;
    $('#' + controlID + '-search-note').html('Listing...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');

    $.ajax({
            type: "POST",
                url: "/flag.py/DupeExploratoryRelationshipSearch",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleEditError(msg, controlID + '-dupe-error');
                }
            }
        });
}

function DupeItemImageSearch(controlID, targetID, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var targetID = targetID;

    var parts = targetID.split('|');
    var itemID = parts[0];
    var imageID = parts[1];

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var formdata = "controlID=" + encodeURIComponent(controlID) + "&itemID=" + itemID + '&imageID=' + imageID + "&offset=" + offset;

    $('#' + controlID + '-search-note').html('Loading...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');
    
    $.ajax({
            type: "POST",
                url: "/flag.py/DupeItemImageSearch",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleEditError(msg, controlID + '-dupe-error');
                }
            }
        });
}    

function DupeReviewSearch(controlID, reviewID, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var reviewID = reviewID;

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var formdata = "controlID=" + encodeURIComponent(controlID) + "&reviewID=" + reviewID + "&offset=" + offset;
    $('#' + controlID + '-search-note').html('Loading...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');

    $.ajax({
            type: "POST",
                url: "/flag.py/DupeReviewSearch",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleEditError(msg, controlID + '-dupe-error');
                }
            }
        });
}    

function DupeMediaSearch(controlID, mediaID, offset) {
    // lexical scoping for CB
    var controlID = controlID;
    var mediaID = mediaID;

    // clear previous errors
    ClearError(controlID + '-dupe-error');

    // prep async request
    var formdata = "controlID=" + encodeURIComponent(controlID) + "&mediaID=" + mediaID + "&offset=" + offset;
    $('#' + controlID + '-search-note').html('Loading...');
    hide(controlID + '-dupe-results');
    showBlock(controlID + '-search-note');

    $.ajax({
            type: "POST",
                url: "/flag.py/DupeMediaSearch",
                data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-dupe-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-search-note');
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-dupe-results').html(msg.substr(1));
		    showBlock(controlID + '-dupe-results');
                }
                else { // handle errors
                    HandleGenericError(msg, controlID + '-dupe-error');
                }
            }
        });
}

//*****
//* Exploratory relationship scripts
//*****

function QueueExploratoryRelationship(itemId, escapedItemTitle, baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    var itemTitle = unescape(escapedItemTitle);

    if (itemId == null) { // new target item case
	itemTitle = AutoCapitalize(itemTitle);

	// copy entry template
	var templateHTML = jqoContainer.find('.add-exploratory-relationship-new-target-item-template:first').html();
	var pendingContainer = jqoContainer.find('.add-exploratory-relationships-listing:first');
	pendingContainer.prepend(templateHTML);

	// munge entry template
	var newEntry = pendingContainer.find('.pending-exploratory-relationship-entry:first');

	newEntry.find('.item-title:first').attr('value', itemTitle);
    }
    else { // existing target item case
	// ignore if the item has already been queued
	if (jqoContainer.find('#pending-exploratory-relationship-target-item-id-' + itemId).length > 0)
	    return;

	// copy entry template
	var templateHTML = jqoContainer.find('.add-exploratory-relationship-template:first').html();
	var pendingContainer = jqoContainer.find('.add-exploratory-relationships-listing:first');
	pendingContainer.prepend(templateHTML);

	// munge entry template
	var newEntry = pendingContainer.find('.pending-exploratory-relationship-entry:first');

	newEntry.find('.item-id:first').attr('value', itemId);
	newEntry.find('.item-id:first').attr('id', 'pending-exploratory-relationship-target-item-id-' + itemId);
	newEntry.find('.item-link:first').attr('href', GetItemDetailsURL(itemId, itemTitle));
	newEntry.find('.item-link:first').html(itemTitle);

	// get bridging relationship types
	var sourceItemId = jqoContainer.find('.source-item-id:first').attr('value');
	UpdateExploratoryRelationshipTypes(newEntry, sourceItemId, itemId);
    }

    pendingContainer.css('display', 'block');
    return;
}

function UpdatePendingExploratoryRelationshipItemTypes(callingObject) {
    var jqoCaller = $(callingObject);
    var jqoContainer = jqoCaller.parents('.pending-exploratory-relationship-entry:first');
    var jqoCategory = jqoContainer.find('.new-target-item-category:first');
    var jqoItemType = jqoContainer.find('.new-target-item-type:first');

    if (jqoCaller.attr('class').indexOf('item-type') > -1) {
	if (jqoCategory.val() != '' && jqoItemType.val() != '') {
	    var sourceItemId = jqoContainer.parents('.add-exploratory-relationships-container:first').find('.source-item-id:first').val();
	    UpdateExploratoryRelationshipTypes(jqoContainer, sourceItemId, null, jqoItemType.val());
	}
    }
    else if (jqoCategory.val() != '') {
        UpdateDocTypeByJQO(jqoItemType, jqoCategory);
        jqoItemType.val(''); // work around browser rendering blank but selecting a non-blank value
    }
}

function RemovePendingExploratoryRelationship(callingObject) {
    $(callingObject).parents('.pending-exploratory-relationship-entry:first').remove();
}

// targetItemId is null whent targetItemType is specified and vice versa
function UpdateExploratoryRelationshipTypes(jqoEntry, sourceItemId, targetItemId, targetItemType) {
    // lexical scoping for callback
    var jqoEntry = jqoEntry;

    var postData = MakeURLQuery([['sourceItemId', sourceItemId]]);
    if (targetItemId != null)
	postData += '&targetItemId=' + targetItemId;
    else
	postData += '&targetItemType=' + targetItemType;

    $.ajax({
	    type: "POST",
	    url: "/relationships/BridgingExploratoryRelationshipTypes",
	    data: postData,
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    // add relationship check boxes
		    jqoEntry.find('.relationship-types:first').html(msg.substr(1));
		    jqoEntry.find('.relationship-types:first').css('display', 'block');
		}
	    }
	});
}

function ClearAddExploratoryRelationshipsInputField(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    var jqoEntries = jqoContainer.find('.add-exploratory-relationships-listing:first');
    jqoEntries.html('&nbsp;');
    jqoEntries.css('display', 'none');
}

function GetExploratoryRelationshipTypes(jqoEntry) {
    var relTypes = new Array();

    jqoEntry.find('.relationship-types:first').children('[type="checkbox"]').each(function() {
	    if ($(this).attr('checked')) {
		relTypes.push($(this).attr('value'));
	    }
	});

    return relTypes;
}

function ValidateAddExploratoryRelationshipsInputField(baseElementId, required) {
    var jqoContainer = $('#' + baseElementId);
    var required = required;
    if (required == null || !required)
	required = false;
    else
	required = true;

    var result = true;
    var relationshipCount = 0;
    var jqoOtherError = jqoContainer.find('.add-exploratory-relationships-error:first');
    ClearErrorByJqo(jqoOtherError);

    jqoContainer.find('.add-exploratory-relationships-listing:first').find('.pending-exploratory-relationship-entry').each(function() {
	    var jqoEntry = $(this);
	    relationshipCount += 1;

	    // Clear errors
	    var jqoTitleError = jqoEntry.find('.item-title-error:first');
	    var jqoTypeError = jqoEntry.find('.item-type-error:first');
	    var jqoCategoryError = jqoEntry.find('.item-category-error:first');
	    var jqoRelationshipTypesError = jqoEntry.find('.relationship-types-error:first');
	    var jqoCommentsError = jqoEntry.find('.relationship-comment-error:first');
	    ClearErrorByJqo(jqoTitleError);
	    ClearErrorByJqo(jqoTypeError);
	    ClearErrorByJqo(jqoCategoryError);
	    ClearErrorByJqo(jqoRelationshipTypesError);
	    ClearErrorByJqo(jqoCommentsError);

	    if (jqoEntry.hasClass('new-target-item')) {
		itemTitle = trim(jqoEntry.find('.item-title:first').val());
		if (itemTitle.length == 0) {
		    result = false;

		    SetErrorByJqo(jqoTitleError, 'Provide a title or remove this entry');
		}

		itemType = jqoEntry.find('.new-target-item-type:first').val();
		if (itemType.length == 0) {
		    result = false;

		    SetErrorByJqo(jqoTypeError, 'Select a page type');
		}

		category = jqoEntry.find('.new-target-item-category:first').val();
		if (category.length == 0) {
		    result = false;

		    SetErrorByJqo(jqoCategoryError, 'Select a category');
		}
	    }

	    if (GetExploratoryRelationshipTypes(jqoEntry) == 0) {
		result = false;
		SetErrorByJqo(jqoRelationshipTypesError, 'Select the type(s) of connections');
	    }

	    var comments = trim(jqoEntry.find('.relationship-comment:first').val());
	    if (comments.length == 0) {
		result = false;
		SetErrorByJqo(jqoCommentsError, 'Describe the connection');
	    }
	    else if (comments.length < _minXRelationshipCommentLength) {
		result = false;
		SetErrorByJqo(jqoCommentsError, 'Please make sure your commentary is at least ' + _minXRelationshipCommentLength + ' characters long.');
	    }
	    else if (comments.length > 1000) {
		result = false;
		SetErrorByJqo(jqoCommentsError, 'Your comments are too long. Please use 1000 characters or less.');
	    }
	});

    if (required && relationshipCount == 0) {
	result = false;
	SetErrorByJqo(jqoOtherError, 'Please add one or more entries to proceed');
    }

    return result;
}

function GetAddExploratoryRelationshipsInputFieldValue(baseElementId) {
    var jqoContainer = $('#' + baseElementId);

    var pendingRelationships = [];

    jqoContainer.find('.add-exploratory-relationships-listing:first').find('.pending-exploratory-relationship-entry').each(function() {
	    var jqoEntry = $(this);

	    var itemId = null;
	    var itemTitle = null;
	    var itemType = null;
	    var itemCategory = null;
	    
	    if (jqoEntry.hasClass('new-target-item')) {
		itemTitle = trim(jqoEntry.find('.item-title:first').val());
		itemType = jqoEntry.find('.new-target-item-type:first').val();
		itemCategory = jqoEntry.find('.new-target-item-category:first').val();
	    }
	    else
		itemId = jqoEntry.find('.item-id:first').val();

	    var relationshipTypes = GetExploratoryRelationshipTypes(jqoEntry);
	    var comment = trim(jqoEntry.find('.relationship-comment:first').val());

	    var relationship = { 'relationshipTypes': relationshipTypes, 'targetItemId': itemId, 'targetItemTitle': itemTitle, 'targetItemType': itemType, 'targetItemCategory': itemCategory, 'comment': comment };

	    pendingRelationships.push(relationship);
	});

    return pendingRelationships;
}

function SerializeAddExploratoryRelationshipsInputField(baseElementId) {
    var pendingRelationships = GetAddExploratoryRelationshipsInputFieldValue(baseElementId);

    return MakeURLQuery([['xRels', JSON.stringify(pendingRelationships)]]);
}

function AddExploratoryRelationships(dialogId, mode, newRelationshipsContainerId) {
    var mode = mode;
    if (mode == null) {
	mode = 'FULL_PAGE'
    }
    var newRelationshipsContainerId = newRelationshipsContainerId;

    // clear previous errors
    var jqoError = $('#' + dialogId + '-error');
    ClearErrorByJqo(jqoError);

    // form validation
    if (!ValidateAddExploratoryRelationshipsInputField(dialogId + '-field', true))
	return;

    var sourceItemId = $('#' + dialogId).find('.source-item-id:first').val();
    var postData = MakeURLQuery([['sourceItemId', sourceItemId], ['view', mode]]);
    postData += '&' + SerializeAddExploratoryRelationshipsInputField(dialogId + '-field');

    // freeze buttons during save
    showBlock(dialogId + '-status');
    ToggleButton(dialogId + '-save', false);
    ToggleButton(dialogId + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/relationships/AddExploratoryRelationships",
		data: postData,
                error: function(request, status, error) {
		HandleAddRelationshipError(null, '8', jqoError);
	        },
                complete: function(request, status) {
		hide(dialogId + '-status');
		ToggleButton(dialogId + '-save', true);
		ToggleButton(dialogId + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    if (mode == 'FULL_PAGE') {
			$("#explore-pane-contents").prepend(msg.substr(1));
			$("#explore-view").get(0).scrollTop = 0; // scroll to top
		    }
		    else { // COMPACT_VIEW
			$("#" + newRelationshipsContainerId).prepend(msg.substr(1));
		    }

		    hide(dialogId);
		    ClearAddExploratoryRelationshipsInputField(dialogId + '-field');
		}
		else if (msg.charAt(0) == '6') {
		    SetErrorByJqo(jqoError, "You're trying to add a vine to something you've already added before.");
		}
		else { // handle errors
		    HandleAddRelationshipError(null, msg, jqoError);
		}
	    }
	});
}

// controlID and rowID are strings
// relID is an int
function DeleteExploratoryRelationship(controlID, rowID, relID) {
    // lexical scoping for CB
    var controlID = controlID;
    var rowID = rowID;
    var relID = relID;

    var formdata = "id=" + relID;

    // clear previous errors
    ClearDeleteErrors(controlID);
    AdviseDeleteStart(controlID);

    $.ajax({
            type: "POST",
                url: "/relationships.py/DeleteExploratoryRelationship",
                data: formdata,
            error: function(request, status, error) {
                HandleDeleteError('8', controlID);
                AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $("#" + controlID).remove();
		    $("#" + rowID).remove();
                }
                else { // handle errors
                    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function DeleteCompactExploratoryRelationship(deleteControlId, relationshipId) {
    // lexical scoping for CB
    var deleteControlId = deleteControlId;

    var postData = "id=" + relationshipId;

    // clear previous errors
    ClearDeleteErrors(deleteControlId);
    AdviseDeleteStart(deleteControlId);

    $.ajax({
            type: "POST",
            url: "/relationships/DeleteExploratoryRelationship",
            data: postData,
            error: function(request, status, error) {
                HandleDeleteError('8', deleteControlId);
                AdviseDeleteDone(deleteControlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $("#" + deleteControlId).parents('.compact-exploratory-relationship:first').remove();
                }
                else { // handle errors
                    HandleDeleteError(msg, deleteControlId);
		    AdviseDeleteDone(deleteControlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

//*****
//* Thumbnail relationship related scripts
//*****

function EditThumbnailRelationship(controlID, relID, requireComment) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var relID = relID;
    
    // clear previous errors
    ClearError(controlID + "-error");

    // form validation
    var comment = $('.' + controlID + '-annotation-class:first').val();
    comment = trim(comment);
    if (comment.length == 0 && requireComment) { // check the form
	SetError(controlID + '-error', 'Please fill in the comment.');

	return
    }

    var postData = "id=" + relID + "&comment=" + encodeURIComponent(comment);

    // freeze buttons during save
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/relationships.py/EditStructuralRelationship",
		data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#relationship-comment-' + relID).html(comment);
		    hide(controlID);
		}
                else if (msg.charAt(0) == 'Q') {
                    showAlert('change-queued');
		    hide(controlID);
                }
		else { // handle errors
		    HandleRelationError(msg, controlID + '-error');
		}
	    }
	});
}

// controlID is a string
// relID is an int
function DeleteThumbnailRelationship(controlID, containerID, relID) {
    // lexical scoping for CB
    var controlID = controlID;
    var containerID = containerID;
    var relID = relID;

    var postData = "id=" + relID;

    // clear previous errors
    ClearError(containerID + '-error');
    AdviseDeleteStart(controlID);

    $.ajax({
            type: "POST",
            url: "/relationships.py/DeleteStructuralRelationship",
            data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlID);
		AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $(".relationship-cell-" + relID).each(function() {
			    $(this).remove();
			});

		    hide(controlID);
                }
                else { // handle errors
                    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function PageTRels(itemID, relationshipTypes, showComments, controlID, containerID, offset) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var containerID = containerID;

    var postData = "itemID=" + itemID + "&relationshipTypes=" + relationshipTypes + "&offset=" + offset;
    postData += '&showComments=' + showComments + '&containerID=' + containerID;

    // clear previous errors
    ClearError(controlID + '-error');

    $.ajax({
	    type: "POST",
	    url: "/relationships/PageThumbnailRelationships",
	    data: postData,
            error: function(request, status, error) {
		HandleRelationError('8', controlID + '-error');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#" + containerID).html(msg.substr(1));
		}
		else { // handle errors
		    HandleRelationError(msg, controlID + '-error');
		}
	    }
	});
}

//used for callbacks upon removing a relation
//checks whether the container is empty and if so, hides it so it doesn't take up weird whitespace
function removerelationcb(containerId) {
    if ($("#" + containerId).html().length == 0) {
	$("#" + containerId).css("display", "none");
    }
}

//used for getting relations that were added using addrelationcb from a container
function getrelations(containerId) {
    var ids = new Array();
    //var titles = new Array(); //commenting out for now as titles are never needed to create relations
    var comments = new Array();

    $("." + containerId).each(function() {
	    ids.push($(this).attr("id").replace(containerId, ""));
	    //titles.push($(this).html());
	});

    $("." + containerId + "comment").each(function() {
	    comments.push($(this).html());
	});

    return [ids, comments];
}

function CheckArray(arrayObj, value) {
    for (var i = 0; i < arrayObj.length; i++) {
	if (arrayObj[i] == value) {
	    return true;
	}
    }

    return false;
}

//*****
//* Helper scripts for adding new items
//*****

function UpdateDocType(docTypeSelectID, verticalSelectID) {
    UpdateDocTypeByJQO($('#' + docTypeSelectID), $('#' + verticalSelectID), null);
}

function UpdateDocTypeByJQO(jqoDocTypeSelect, jqoVerticalSelect, docTypeConstraints) {
    // annoying hack becase IE doesn't properly handle display: none for OPTION elements
    var defaultOption = '<option value="" selected="selected">SELECT</option>';

    // cross-vertical
    var personOption = '<option id="person" value="PERSON">Person</option>';
    var personValue = "PERSON";

    // music
    var albumOption = '<option id="album" value="ALBUM">Album</option>';
    var albumValue = "ALBUM";

    var compositionOption = '<option id="composition" value="COMPOSITION">Composition</option>';
    var compositionValue = "COMPOSITION";

    var groupOption = '<option id="group" value="GROUP">Group (band, orchestra, etc.)</option>';
    var groupValue = "GROUP";

    var singleOption = '<option id="single" value="SINGLE">Single</option>';
    var singleValue = "SINGLE";

    // movies 
    var movieOption = '<option id="movie" value="MOVIE">Movie</option>';
    var movieValue = "MOVIE";

    // tv
    var tvSeriesOption = '<option id="tv-series" value="TV_SERIES">TV Series</option>';
    var tvSeriesValue = "TV_SERIES";

    // fashion
    var fashionProductValue = "FASHION_PRODUCT";
    var fashionProductOption = '<option id="fashion-product" value="FASHION_PRODUCT">Product</option>';

    var fashionBrandValue = "FASHION_BRAND";
    var fashionBrandOption = '<option id="fashion-brand" value="FASHION_BRAND">Brand</option>';

    var fashionStyleValue = "FASHION_STYLE";
    var fashionStyleOption = '<option id="fashion-style" value="FASHION_STYLE">Style</option>';

    var fashionEnsembleValue = "FASHION_ENSEMBLE";
    var fashionEnsembleOption = '<option id="fashion-ensemble" value="FASHION_ENSEMBLE">Fashion Ensemble</option>';

    // syntactic sugar for a person under fashion
    var fashionPersonOption = '<option id="person" value="PERSON">Designer or Celebrity</option>';

    // taste
    var drinkOption = '<option id="drink" value="DRINK">Beer, Spirits, Beverages</option>';
    var drinkValue = "DRINK";

    var cocktailOption = '<option id="cocktail" value="COCKTAIL">Cocktail</option>';
    var cocktailValue = "COCKTAIL";

    var foodOption = '<option id="food" value="FOOD">Food</option>';
    var foodValue = "FOOD";

    var wineOption = '<option id="wine" value="WINE">Wine</option>';
    var wineValue = "WINE";

    var wineryOption = '<option id="winery" value="WINERY">Winery</option>';
    var wineryValue = "WINERY";

    var grapeOption = '<option id="grape" value="GRAPE">Wine Grape</option>';
    var grapeValue = "GRAPE";

    var regionOption = '<option id="region" value="REGION">Wine Region</option>';
    var regionValue = "REGION";

    // book
    var authorOption = '<option id="author" value="PERSON">Author</option>'; // syntactic sugar for a person under fashion

    var bookValue = "BOOK";
    var bookOption = '<option id="book" value="BOOK">Book</option>';

    // tech
    var webAppOption = '<option id="web-app" value="WEB_APP">Web Site / App</option>';
    var webAppValue = "WEB_APP";

    var softwareOption = '<option id="software" value="SOFTWARE">Software</option>';
    var softwareValue = "SOFTWARE";

    var gadgetOption = '<option id="gadget" value="GADGET">Gadget</option>';
    var gadgetValue = "GADGET";

    var techCompanyOption = '<option id="tech-company" value="TECH_COMPANY">Tech Company</option>';
    var techCompanyValue = "TECH_COMPANY";
    
    jqoDocTypeSelect.html(defaultOption); 

    if (jqoVerticalSelect.val() == "movies")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, movieValue)) {
		jqoDocTypeSelect.append(movieOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(personOption);
	    }

	    /* don't auto-select as this is confusing
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, movieValue)) {
		jqoDocTypeSelect.attr('value', movieValue); // setting by attr instead of val() works in both IE6 and modern browsers 
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "tv")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, tvSeriesValue)) {
		jqoDocTypeSelect.append(tvSeriesOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(personOption);
	    }

	    /* don't auto-select as this is confusing
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, movieValue)) {
		jqoDocTypeSelect.attr('value', movieValue); // setting by attr instead of val() works in both IE6 and modern browsers 
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "music")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, albumValue)) {
		jqoDocTypeSelect.append(albumOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, compositionValue)) {
		jqoDocTypeSelect.append(compositionOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, groupValue)) {
		jqoDocTypeSelect.append(groupOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(personOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, singleValue)) {
		jqoDocTypeSelect.append(singleOption);
	    }

	    /* don't auto-select as this is confusing
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, albumValue)) {
		jqoDocTypeSelect.attr('value', albumValue);
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "fashion")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, fashionBrandValue)) {
		jqoDocTypeSelect.append(fashionBrandOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(fashionPersonOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, fashionEnsembleValue)) {
		jqoDocTypeSelect.append(fashionEnsembleOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, fashionProductValue)) {
		jqoDocTypeSelect.append(fashionProductOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, fashionStyleValue)) {
		jqoDocTypeSelect.append(fashionStyleOption);
	    }

	    /* don't auto-select as this is confusing
	    // default value
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, fashionStyleValue)) {
		jqoDocTypeSelect.attr('value', fashionStyleValue);
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "taste")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, drinkValue)) {
		jqoDocTypeSelect.append(drinkOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, cocktailValue)) {
		jqoDocTypeSelect.append(cocktailOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, foodValue)) {
		jqoDocTypeSelect.append(foodOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(personOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, wineValue)) {
		jqoDocTypeSelect.append(wineOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, wineryValue)) {
		jqoDocTypeSelect.append(wineryOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, grapeValue)) {
		jqoDocTypeSelect.append(grapeOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, regionValue)) {
		jqoDocTypeSelect.append(regionOption);
	    }

	    /* don't auto-select as this is confusing
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, wineValue)) {
		jqoDocTypeSelect.attr('value', wineValue);
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "books")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, personValue)) {
		jqoDocTypeSelect.append(authorOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, bookValue)) {
		jqoDocTypeSelect.append(bookOption);
	    }

	    /* don't auto-select as this is confusing
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, bookValue)) {
		jqoDocTypeSelect.attr('value', bookValue);
	    }
	    */
        }
    else if (jqoVerticalSelect.val() == "tech")
        {
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, gadgetValue)) {
		jqoDocTypeSelect.append(gadgetOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, softwareValue)) {
		jqoDocTypeSelect.append(softwareOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, techCompanyValue)) {
		jqoDocTypeSelect.append(techCompanyOption);
	    }
	    if (docTypeConstraints == null || CheckArray(docTypeConstraints, webAppValue)) {
		jqoDocTypeSelect.append(webAppOption);
	    }
        }

    jqoDocTypeSelect.attr('value', '');
}

// used to sort an array of pairs
// a pair being a two element array representing the sortable key followed by the value
function ComparePair(a, b) {
    var keyA = a[0];
    var keyB = b[0];

    return ((keyA < keyB) ? -11 : ((keyA > keyB) ? 1: 0));
}

// move an absolute positioned element with elementID near the move but not touching it
// uses the event to position the tool tip optimally
// note that the event must be a JQuery event to assure cross-browser compatibility
function MoveByMouse(elementID, event) {
    var xOffset = 3;
    var yOffset = 25;

    var hMargin = 35;
    var vMargin = 35;

    //default position
    var x = event.pageX + xOffset;
    var y = event.pageY + yOffset;
    
    var width = $("#" + elementID).width();
    var height = $("#" + elementID).height();
    var wOffset = windowOffset();

    // check if the tool tip extends past the right of the browser window
    if (x + width + hMargin > wOffset[1] && x > hMargin + wOffset[0]) {
	x -= Math.min(x + width + hMargin - wOffset[1], x - hMargin - wOffset[0]);
    }

    // check if the bottom of the dialog is below the fold
    if (y + height + vMargin > wOffset[3] && y > vMargin + wOffset[2]) {
	var yNew = event.pageY - height - 5; // move the element above the mouse with offset; offset is smaller due to asymmetric mouse positioning
	if (yNew > 0) {
	    y = yNew;
	}
    }

    // move the element to its final position, factoring in nested containing coordinate systems
    setPosition($('#' + elementID).get(0), x, y);
}

// uniqueID (string): the uniqueID postfix for the description preview
// docID (string): the document ID for the document featured in the preview
function LoadDescriptionPreview(uniqueID, docID) {
    // check whether the preview is yet to be loaded
    if (trim($("#item-preview-status-" + uniqueID).val()) == 'ready') {
	// render image if the preview includes an image
	if ($("#large-image-" + uniqueID) != undefined && $("#large-image-url-" + uniqueID).length > 0) {
	    $("#large-image-" + uniqueID).html(PreviewImageHTML(uniqueID));
	}

	// only do an async get of full preview info if needed
	if ($(".item-preview-custom-" + uniqueID + ':first').val().toLowerCase() == 'true') {
	    var uniqueID = uniqueID; // keep in lexical scope for callback
	    var postData = "itemId=" + docID;
	
	    $.ajax({
		    type: "POST",
			url: "/summary/HoverDialog",
			data: postData,
			success: function(msg) {
			if (msg.charAt(0) == '0') { //success
			    $("#item-preview-main-" + uniqueID).html(msg.substr(1));
			    $("#item-preview-status-" + uniqueID).val("loaded");
			}
			// do nothing on errors so status remains unset and this preview will try to reload next time
		    }
		});
	}
	else
	    $("#item-preview-status-" + uniqueID).val("loaded");
    }
}

function BindDescriptionPreview(callingObj) {
    var callingObj = callingObj; // maintain lexical scope

    // prevent error if this is called before document is ready since this can be called for onload events
    $(document).ready(function() {
	    var elementID = $(callingObj).attr('id');
    
	    $("#" + elementID).bind("mouseover", function(e) {
		    DescriptionPreviewMouseOver(elementID, e);
		});
	    $("#" + elementID).bind("mouseout", function(e) {
		    PreviewMouseOut(elementID, e);
		});
	    $("#" + elementID).bind("mousemove", function(e) {
		    PreviewMouseMove(elementID, e);
		});
	});
}

function DescriptionPreviewMouseOver(elementID, evt) {
    var baseID = elementID.replace('image-thumb-', '');

    TrackEvent('Hovers', '/HoverPreview/Description');

    LoadDescriptionPreview(baseID, $('#hover-preview-did-' + baseID).html());
    MoveByMouse('hover-preview-' + baseID, evt);
    showBlock('hover-preview-' + baseID);
}

function PreviewMouseOut(elementID, evt) {
    var baseID = elementID.replace('image-thumb-', '');

    hide('hover-preview-' + baseID);
}

function PreviewMouseMove(elementID, evt) {
    var baseID = elementID.replace('image-thumb-', '');
    MoveByMouse('hover-preview-' + baseID, evt);
}

function PreviewImageHTML(uniqueID) {
    var imageURL = $("#large-image-url-" + uniqueID).attr('href');

    return '<img src="' + imageURL + '" />';
}

/*
//
// Begin implementation of OrderedTable functions that go with the OrderedTable mako template function
// Consider putting into a class at some point
//
function OrderedTableAddRow(controlID, html) {
    $("#" + controlID + "-heading").css("display", "table-row"); // to do: this doesn't work on IE

    rowID = OrderedTableNextRowID(controlID);
    numCols = $("#" + controlID + "-heading").children().length;
    $("#" + controlID).append("<tr id='" + controlID + "-row-" + rowID + "' class='" + controlID + "-row'>" + html + "</tr>");
    $("#" + controlID).append("<tr id='" + controlID + "-error-" + rowID + "' class='errormsg " + controlID + "-error'><td colspan='" + numCols + "'>&nbsp;</td></tr>");
    $("#" + controlID).append("<tr id='" + controlID + "-changed-" + rowID + "' class='invisible " + controlID + "-changed'><td colspan='" + numCols + "'>" + rowID + "</td></tr>");

    $("#" + controlID + "-next-row-id").html((rowID + 1).toString());
}

function OrderedTableDeleteRow(controlID, rowID) {
    // dirty all rows under this before removing it
    var controlID = controlID; // put this in lexical scope for the callback below
    $("#" + controlID + "-changed-" + rowID + " ~ ." + controlID + "-changed").each(function() {
	    var rowID = $(this).attr('id').replace(controlID + "-changed-", "");
	    OrderedTableDirtyRow(controlID, rowID);
	});

    // delete row and metadata
    $("#" + controlID + "-row-" + rowID).remove();
    $("#" + controlID + "-error-" + rowID).remove();
    $("#" + controlID + "-changed-" + rowID).remove();
}

function OrderedTableDirtyRow(controlID, rowID) {
    $("#" + controlID + "-changed-" + rowID + " td").html(rowID);
}

function OrderedTableCountRows(controlID) {
    return $("." + controlID + "-row").length;
}

function OrderedTableNextRowID(controlID) {
    return parseInt($("#" + controlID + "-next-row-id").html());
}

function OrderedTableError(controlID, rowID, msg) {
    $("#" + controlID + "-error-" + rowID + " td").append(msg + "<br />");
    $("#" + controlID + "-error-" + rowID).css("display", "table-row");
}

function OrderedTableClearError(controlID, rowID, msg) {
    currentMsgs = $("#" + controlID + "-error-" + rowID + " td").html();
    $("#" + controlID + "-error-" + rowID + " td").html(currentMsgs.replace(msg + "<br />", ""));

    currentMsgs = $("#" + controlID + "-error-" + rowID + " td").html();
    if (currentMsgs.length == 0 || currentMsgs == "&nbsp;") {
	$("#" + controlID + "-error-" + rowID).css("display", "none");
    }
    if (currentMsgs.length == 0) {
	$("#" + controlID + "-error-" + rowID + " td").html("&nbsp;");
    }
}

function OrderedTableErrorCount(controlID) {
    var erroredRows = 0;
    $("." + controlID + "-error").each(function() {
	    if (trim($(this).html()).length != 0) {
		erroredRows++
	    }
	});

    if (erroredRows > 0) {
	return erroredRows;
    }
    else {
	return 0;
    }
}

// return an in-order array of jQuery objects for each dirtied row
function OrderedTableGetChangedRows(controlID) {
    var rows = [];
	
    var controlID = controlID; // put this in lexical scope for the callback below
    $("." + controlID + "-changed").each(function() {
            var rowID = $(this).attr('id').replace(controlID + "-changed-", "");
            rows.push($("#" + controlID + "-row-" + rowID));
        });

    return rows;
}

*/

//
// These functions pair with the mako function DeleteControl
//
function ClearDeleteErrors(controlID) {
    hide("deleteError" + controlID);
    $("#deleteError" + controlID).html("&nbsp;");
}

function HandleDeleteError(msg, controlID) {
    var errorMsgID = "deleteError" + controlID;

    if (msg.charAt(0) == '2') { // permission denied
	$("#" + errorMsgID).html(_deletePermissionDenied);
	showBlock(errorMsgID);
    }
    else { // default
	HandleGenericError(msg, errorMsgID);
    }
}

function AdviseDeleteStart(controlID) {
    showBlock(controlID + '-delete-status');
    ToggleButton(controlID + '-delete-submit', false);
    ToggleButton(controlID + '-delete-cancel', false);
}

function AdviseDeleteDone(controlID) {
    hide(controlID + '-delete-status');
    ToggleButton(controlID + '-delete-submit', true);
    ToggleButton(controlID + '-delete-cancel', true);
}

//*****
//* Album track related scripts
//*****
function AddTrack() {
    var jqoInput = $('#new-track-name');
    var trackName = trim(jqoInput.val());

    if (trackName.length > 0) {
	// copy an empty-track plus error row and append to the list of tracks
	var newTrack = $("#empty-track").clone(true);
	var newTrackError = $("#empty-track-error").clone(true);

	newTrack.insertBefore("#new-track");
	newTrackError.insertBefore("#new-track");

	newTrack.css("display", '');

	// set the values
	var lastTrackNumber = 0;
	if ($('.new-track-entry').length > 0) {
	    lastTrackNumber = parseInt($('.new-track-entry:last .track-number:first').val(), 10);
	    if (isNaN(lastTrackNumber) || lastTrackNumber < 1) {
		lastTrackNumber = $('.new-track-entry').length;
	    }
	}

	newTrack.find(".track-number:first").val(lastTrackNumber + 1);
	newTrack.find(".track-name:first").val(trackName);

	// show the heading
	$('#new-tracks-heading').css('display', '');
	
	// remove the old id and reclass the element
	newTrack.attr("id", "");
	newTrack.attr("class", "new-track-entry");
	newTrackError.attr("id", "");
	newTrackError.attr("class", "new-track-error");

	// clear the new track input grid
	jqoInput.val('');
    }
}


function GetTrackNumbers() {
    var trackNumbers = new Array();

    $('.new-track-entry').each(function() {
	    trackNumbers.push(parseInt($(this).find('.track-number:first').val(), 10));
	});

    return trackNumbers;
}

function ValidateTrackNumber(trackNumber) {
    return (!isNaN(trackNumber) && trackNumber >= 1);
}

function GetMaxFromTrackNumbers(trackNumbers) {
    var maxTrackNumber = 1;

    for (var i = 0; i < trackNumbers.length; i++) {
	if (ValidateTrackNumber(trackNumbers[i])) {
	    maxTrackNumber = Math.max(trackNumbers[i], maxTrackNumber);
	}
    }

    return maxTrackNumber
}

function GetMinFromTrackNumbers(trackNumbers) {
    var minTrackNumber = 1;

    for (var i = 0; i < trackNumbers.length; i++) {
	if (ValidateTrackNumber(trackNumbers[i])) {
	    minTrackNumber = Math.min(trackNumbers[i], minTrackNumber);
	}
    }

    return minTrackNumber
}

// replaces all NaN's in trackNumbers with a stub value <= than the next valid value or equal to the max value
// if all following values are invalid
function ScrubTrackNumbers(inputTrackNumbers) {
    // copy array
    var trackNumbers = new Array();
    for (var i = 0; i < inputTrackNumbers.length; i++) {
	trackNumbers.push(inputTrackNumbers[i]);
    }
    
    var lastNumber = GetMaxFromTrackNumbers(trackNumbers);

    for (var i = trackNumbers.length - 1; i >= 0; i--) {
	if (!ValidateTrackNumber(trackNumbers[i])) {
	    trackNumbers[i] = lastNumber;
	}
	else {
	    lastNumber = trackNumbers[i];
	}
    }

    return trackNumbers
}

function SetNewTrackError(jqoErrorContainer, errorClass, msg) {
    // set error
    var errorSpan = jqoErrorContainer.find('.' + errorClass + ':first');
    errorSpan.html(msg);

    // show div
    errorSpan.css('display', 'inline');

    // show container
    jqoErrorContainer.css('display', '');
}

function ClearNewTrackError(jqoErrorContainer, errorClass) {
    // clear error
    var errorSpan = jqoErrorContainer.find('.' + errorClass + ':first');
    errorSpan.html('&nbsp;');

    // hide div
    errorSpan.css('display', 'none');

    // hide container if all other error spans are hidden
    var hide = true;
    jqoErrorContainer.find('.errormsg').each(function() {
	    if ($(this).css('display') == 'inline') {
		hide = false;
	    }
	});
    if (hide) {
	jqoErrorContainer.css('display', 'none');
    }
}

function MoveNewTrack(obj) {
    var container = $(obj).parents('.new-track-entry:first');
    var errorContainer = container.next();

    // clear previous error
    ClearNewTrackError(errorContainer, 'new-track-number-error');

    var errorMsg = "Please input a positive number for the track number.";
    var newOrder = parseInt(trim(container.find(".track-number:first").val()));
    if (ValidateTrackNumber(newOrder)) {
	var trackNumbers = new Array();

	// represent the current value as undeclared
	trackNumbers.push(NaN);

	// grab all preceding values
	var prev = container;
	while (true) {
	    var prev = prev.prev();
	    if (prev.attr('id') == 'new-tracks-heading') {
		break;
	    }
	    prev = prev.prev();

	    trackNumbers = [parseInt(prev.find('.track-number:first').val(), 10)].concat(trackNumbers);
	}
	var currentPlace = trackNumbers.length - 1;

	// grab all following values
	var next = container;
	while (true) {
	    var next = next.next().next();
	    if (next.attr('id') == 'new-track') {
		break;
	    }

	    trackNumbers.push(parseInt(next.find('.track-number:first').val(), 10));
	}

	// replace all NaN's in trackNumbers
	trackNumbers = ScrubTrackNumbers(trackNumbers);

	// find where to insert the entry
	var insertPlace = trackNumbers.length;
	if (trackNumbers.length > 0 && newOrder <= trackNumbers[0]) {
	    insertPlace = 0;
	}
	else {
	    for (var i = 1; i < trackNumbers.length; i++) {
		if (newOrder <= trackNumbers[i] && newOrder > trackNumbers[i-1]) {
		    insertPlace = i;
		    break;
		}
	    }
	}

	// move
	if (currentPlace == insertPlace) {
	    return;
	}
	else if (insertPlace == 0 || trackNumbers.length == 0) {
	    errorContainer.insertAfter("#new-tracks-heading");
	    container.insertAfter("#new-tracks-heading");
	}
	else if (insertPlace == trackNumbers.length) {
	    container.insertBefore("#new-track:last");
	    errorContainer.insertBefore("#new-track:last");
	}
	else {
	    container.insertBefore(".new-track-entry:eq(" + insertPlace + ")");
	    errorContainer.insertAfter(container);
	}
    }
    else {
	SetNewTrackError(errorContainer, 'new-track-number-error', errorMsg);
    }
}

function DeleteNewTrack(obj) {
    var container = $(obj).parents('.new-track-entry:first');
    var errorContainer = container.next();

    container.remove();
    errorContainer.remove();
}

function CheckTrackName(obj) {
    var container = $(obj).parents('.new-track-entry:first');
    var errorContainer = container.next();

    ClearNewTrackError(errorContainer, 'new-track-name-error');
    if (trim($(obj).val()).length == 0) {
	SetNewTrackError(errorContainer, 'new-track-name-error', 'Please enter a track name.');
    }
}

function SaveNewTracks(docID) {
    // clear previous errors
    ClearError('save-tracks-error');

    // form validation
    var error = false;
    $(".new-track-error").each(function() {
	    if ($(this).css('display') != 'none') {
		error = true;
	    }
	});

    if (error) {
	SetError("save-tracks-error", "Please address the highlighted issues above before saving.");
	return;
    }
    
    var formdata = "did=" + docID;
    var trackNumbers = [];
    var trackNames = [];
    var trackLengths = [];

    $('.new-track-entry').each(function() {
	    trackNumbers.push(parseInt($(this).find('.track-number:first').val(), 10));
	    trackNames.push(trim($(this).find('.track-name:first').val()));
	    trackLengths.push(0); // track lengths no longer supported on track creation
	});

    if (trackNumbers.length == 0) {
	SetError("save-tracks-error", "Please add some tracks before saving.");
	return;
    }
    
    // make sure there are no duplicate track numbers
    // the following logic works because track sorting ensures that the track numbers are monotonically increasing
    for (var i = 0; i < trackNumbers.length - 1; i++) {
	if (trackNumbers[i] == trackNumbers[i+1]) {
	    SetError("save-tracks-error", "Please make sure that every track number is unique.");
	    return;
	}
    }
    
    formdata += "&trackNumbers=" + encodeNumberArray(trackNumbers) + "&trackNames=" + encodeStringArray(trackNames) + "&trackLengths=" + encodeNumberArray(trackLengths);

    // freeze buttons and give status
    showBlock('save-tracks-status');
    ToggleButton('save-tracks-save', false);
    ToggleButton('save-tracks-cancel', false);

    $.ajax({
	    type: "POST",
	    url: "/item.py/AddTracks",
	    data: formdata,
            error: function(request, status, error) {
		HandleEditError('8', 'save-tracks-error');
	    },
            complete: function(request, status) {
		  hide('save-tracks-status');
		  ToggleButton('save-tracks-save', true);
		  ToggleButton('save-tracks-cancel', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var template = msg.substr(1);
		    parts = template.split('|'); // the first part is the display, the second part is the editable row
		    
		    $("#tracks-view-table").append(decodeURIComponent(parts[0]));
		    $("#tracks-edit-table").append(decodeURIComponent(parts[1]));

		    // cleanup dialog
                    hide('add-tracks-dialog');
                    $('.new-track-entry').remove();
                    $('.new-track-error').remove();
		    
		    // hide empty note
		    hide("tracks-empty");
		    hide("tracks-add-first");

		    $("#tracks-reorder-action").css("display", "inline");
		}
                else if (msg.charAt(0) == 'Q') {
		    // cleanup dialog
                    hide('add-tracks-dialog');
                    $('.new-track-entry').remove();
                    $('.new-track-error').remove();

                    showAlert('change-queued');
                }
		else if (msg.charAt(0) == 'D') {
		    var title = msg.substr(1);

		    SetError('save-tracks-error', 'The new track you\'re trying to add titled "' + title + '" conflicts with the title of an existing track for this album or another album. Try disambiguating the title for the new track by adding the album name in parentheses or by appending some other information.');
		}
		else { // handle errors
		    HandleRelationError(msg, 'save-tracks-error');
		}
	    }
	});
}

function ToggleTracksReorderOff() {
    hide('tracks-reorder');
    showBlock('tracks-edit-normal'); 
    $(".track-order").css('display', 'none');
}

function ToggleTracksReorderOn() {
    hide('tracks-edit-normal'); 
    showBlock('tracks-reorder');

    // must iterator setting display individually on each row due to quirkiness in setting undefined css attributes
    // in IE for more than 1 element at a time
    $("td.track-order").each(function () {
	    $(this).css('display', 'block');
	});
}

function AddTracksDialog() {
    // hack: clear input because FF likes to cache a previous input value on page refresh from the wrong cell
    $('#new-track-name').val('');
    
    showDialog('add-tracks-dialog');
}


function MoveTrack(relID) {
    // clear previous error
    var errorMsg = "Please input a positive number for the track number.";

    $("#track-error-" + relID + " td").html("&nbsp;");
    hide("track-error-" + relID);

    var edit = $('#track-edit-' + relID);
    var error = $('#track-error-' + relID);

    var newOrder = trim($("#track-edit-row-" + relID).val());
    var newOrderParsed = parseInt(newOrder, 10);

    if (newOrder.length > 0 && newOrder.replace(/[\d]+/, "").length == 0 && ValidateTrackNumber(newOrderParsed)) {
	newOrder = newOrderParsed;

	var trackNumbers = new Array();

	// represent the current value as undeclared
	trackNumbers.push(NaN);

	// grab all preceding values
	var prev = edit;
	while (true) {
	    var prev = prev.prev();
	    if (prev.attr('id') == 'tracks-edit-stopper') {
		break;
	    }
	    prev = prev.prev();

	    trackNumbers = [parseInt(prev.find('.track-number:first').val(), 10)].concat(trackNumbers);
	}
	var currentPlace = trackNumbers.length - 1;

	// grab all following values
	$('.track-edit-row:gt(' + currentPlace + ')').each(function() {
		trackNumbers.push(parseInt($(this).find('.track-number:first').val(), 10));
	    });

	// replace all NaN's in trackNumbers
	trackNumbers = ScrubTrackNumbers(trackNumbers);

	// find where to insert the entry
	var insertPlace = trackNumbers.length;
	if (trackNumbers.length > 0 && newOrder <= trackNumbers[0]) {
	    insertPlace = 0;
	}
	else {
	    for (var i = 1; i < trackNumbers.length; i++) {
		if (newOrder <= trackNumbers[i] && newOrder > trackNumbers[i-1]) {
		    insertPlace = i;
		    break;
		}
	    }
	}

	// move
	if (currentPlace == insertPlace) {
	    return;
	}
	else if (insertPlace == 0 || trackNumbers.length == 0) {
	    error.insertAfter("#tracks-edit-stopper");
	    edit.insertAfter("#tracks-edit-stopper");
	}
	else if (insertPlace == trackNumbers.length) {
	    error.insertAfter(".track-error-row:last");
	    edit.insertBefore(error);
	}
	else {
	    edit.insertBefore(".track-edit-row:eq(" + insertPlace + ")");
	    error.insertAfter(edit);
	}
    }
    else {
	$("#track-error-" + relID + " td").html(errorMsg);
	$("#track-error-" + relID).css('display', '');
    }
}

function SaveTracksReorder() {
    // clear past error
    $("#tracks-reorder-error").html("&nbsp;");
    hide('tracks-reorder-error');

    // form validation
    var error = false;
    $(".track-error-row").each(function() {
	    if ($(this).css("display") != "none") {
		error = true;
	    }
	});

    if (error) {
	$("#tracks-reorder-error").html("Please addressed the highlighted issues before saving.");
	showBlock('tracks-reorder-error');

	return;
    }
    
    // get order
    var relIDs = [];
    var trackNumbers = [];
    $(".track-edit-row").each(function() {
	    var order = trim($(this).find(".track-number:first").val());
	    order = parseInt(order, 10);
	    trackNumbers.push(order);

	    relIDs.push(trim($(this).attr('id').replace('track-edit-', '')));
	});

    // make sure there are no duplicate track numbers
    // the following logic works because track sorting ensures that the track numbers are monotonically increasing
    for (var i = 0; i < trackNumbers.length - 1; i++) {
	if (trackNumbers[i] == trackNumbers[i+1]) {
	    SetError("tracks-reorder-error", "Please make sure that every track number is unique.");
	    return;
	}
    }

    var formdata = "rids=" + encodeStringArray(relIDs) + '&trackNumbers=' + encodeNumberArray(trackNumbers);

    // freeze buttons and give status
    showBlock('reorder-tracks-status');
    ToggleButton('reorder-tracks-save', false);
    ToggleButton('reorder-tracks-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/item.py/ReorderTracks",
		data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', 'reorder-tracks-error');
	        },
                complete: function(request, status) {
		  hide('reorder-tracks-status');
		  ToggleButton('reorder-tracks-save', true);
		  ToggleButton('reorder-tracks-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var template = msg.substr(1);
		    $("#tracks-view-table").html(template);
		    // no need to update edit table since the user was editing that

		    ToggleTracksReorderOff();
		}
		else if (msg.charAt(0) == 'Q') { //success
		    ToggleTracksReorderOff();
		    showAlert('change-queued');
		}
		else { // handle errors
		    HandleRelationError(msg, "tracks-reorder-error");
		}
	    }
	});
}

// this function is the callback for deleting a track
function DeleteTrackRel(controlID, relID, docID) {
    // lexical scoping for CB
    var controlID = controlID;
    var formdata = "rid=" + relID + "&did=" + docID;
	
    // clear previous errors
    ClearDeleteErrors(controlID);

    // disable buttons and give status
    AdviseDeleteStart(controlID);
    
    $.ajax({
	    type: "POST",
		url: "/item.py/DeleteTrack",
		data: formdata,
                error: function(request, status, error) {
		  HandleDeleteError('8', controlID);
		  AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#track-view-" + relID).remove();

		    $("#track-edit-" + relID).remove();
		    $("#track-error-" + relID).remove();

		    if ($("#tracks-edit-table").children().length == 2) {
			// show empty note when we're down to nothing but a header and the hidden note
			showBlock("tracks-empty");
			hide("tracks-reorder-action");
			showInline("tracks-add-first");
		    }

		    hide(controlID);
		}
		else { // handle errors
		    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
		}
	    }
	});
}

function KeyToAddTrackInputField(event, callingObject) {
    if (event.keyCode == 13) {
	AddToAddTrackInputField(callingObject);
    }
}

function AddToAddTrackInputField(callingObject) {
    var jqoContainer = $(callingObject).parents('.add-tracks-container:first');
    var baseElementId = jqoContainer.attr('id');
    var jqoInput = $(callingObject).parents('.add-tracks-add-container:first').find('.add-tracks-new-track:first');
    var trackName = trim(jqoInput.val());

    if (trackName.length > 0) {
	// copy an empty-track plus error row and append to the list of tracks
	var newTrack = jqoContainer.find(".add-tracks-entry-template:last").clone(true);
	var newTrackError = jqoContainer.find(".add-tracks-error-template:last").clone(true);

	newTrack.insertBefore("#" + baseElementId + ' .add-tracks-add-container:first');
	newTrackError.insertBefore("#" + baseElementId + ' .add-tracks-add-container:first');

	newTrack.css("display", '');

	// set the values
	var lastTrackNumber = 0;
	if (jqoContainer.find('.add-tracks-entry-container').length > 0) {
	    lastTrackNumber = parseInt(jqoContainer.find('.add-tracks-entry-container:last .add-tracks-entry-number:first').val(), 10);
	    if (isNaN(lastTrackNumber) || lastTrackNumber < 1) {
		lastTrackNumber = jqoContainer.find('.add-tracks-entry-container').length;
	    }
	}

	newTrack.find(".add-tracks-entry-number:first").val(lastTrackNumber + 1);
	newTrack.find(".add-tracks-entry-name:first").val(trackName);

	// show the heading
	jqoContainer.find('.add-tracks-heading:first').css('display', '');
	
	// remove the old id and reclass the element
	newTrack.attr('class', "add-tracks-entry-container");
	newTrackError.removeClass('add-tracks-error-template');
	newTrackError.addClass("add-tracks-error-container");

	// clear the new track input grid and focus on it
	jqoInput.val('');
	jqoInput.focus();
    }
}

function ClearAddTrackInputFieldEntryError(jqoErrorContainer, errorClass) {
    // clear error
    var jqoError = jqoErrorContainer.find('.' + errorClass + ':first');
    ClearErrorByJqo(jqoError);

    // hide container if all other error spans are hidden
    var hide = true;
    jqoErrorContainer.find('.error-message').each(function() {
            if ($(this).css('display') == 'inline') {
                hide = false;
            }
        });
    if (hide) {
        jqoErrorContainer.css('display', 'none');
    }
}

function SetAddTrackInputFieldEntryError(jqoErrorContainer, errorClass, msg) {
    // set error
    var jqoError = jqoErrorContainer.find('.' + errorClass + ':first');
    SetErrorByJqo(jqoError, msg);

    // show container
    jqoErrorContainer.css('display', '');
}


function ReorderAddTrackInputField(callingObject) {
    var baseElementId = $(callingObject).parents('.add-tracks-container:first').attr('id');
    var container = $(callingObject).parents('.add-tracks-entry-container:first');
    var errorContainer = container.next();

    // clear previous error
    ClearAddTrackInputFieldEntryError(errorContainer, 'add-tracks-entry-number-error');

    var errorMsg = "Please input a positive number for the track number.";
    var newOrder = parseInt(trim(container.find(".add-tracks-entry-number:first").val()));
    if (ValidateTrackNumber(newOrder)) {
	var trackNumbers = new Array();

	// represent the current value as undeclared
	trackNumbers.push(NaN);

	// grab all preceding values
	var prev = container;
	while (true) {
	    var prev = prev.prev();
	    if (prev.hasClass('add-tracks-heading')) {
		break;
	    }
	    prev = prev.prev();

	    trackNumbers = [parseInt(prev.find('.add-tracks-entry-number:first').val(), 10)].concat(trackNumbers);
	}
	var currentPlace = trackNumbers.length - 1;

	// grab all following values
	var next = container;
	while (true) {
	    var next = next.next().next();
	    if (next.hasClass('add-tracks-add-container')) {
		break;
	    }

	    trackNumbers.push(parseInt(next.find('.add-tracks-entry-number:first').val(), 10));
	}
	
	// replace all NaN's in trackNumbers
	trackNumbers = ScrubTrackNumbers(trackNumbers);

	// find where to insert the entry
	var insertPlace = trackNumbers.length;
	if (trackNumbers.length > 0 && newOrder <= trackNumbers[0]) {
	    insertPlace = 0;
	}
	else {
	    for (var i = 1; i < trackNumbers.length; i++) {
		if (newOrder <= trackNumbers[i] && newOrder > trackNumbers[i-1]) {
		    insertPlace = i;
		    break;
		}
	    }
	}

	// move
	if (currentPlace == insertPlace) {
	    return;
	}
	else if (insertPlace == 0 || trackNumbers.length == 0) {
	    errorContainer.insertAfter('#' + baseElementId + ' .add-tracks-heading:first');
	    container.insertAfter('#' + baseElementId + ' .add-tracks-heading:first');
	}
	else if (insertPlace == trackNumbers.length) {
	    container.insertBefore('#' + baseElementId + ' .add-tracks-add-container:first');
	    errorContainer.insertBefore('#' + baseElementId + ' .add-tracks-add-container:first');
	}
	else {
	    container.insertBefore('#' + baseElementId + '.add-tracks-entry-container:eq(' + insertPlace + ")");
	    errorContainer.insertAfter(container);
	}
    }
    else {
	SetAddTrackInputFieldEntryError(errorContainer, 'add-tracks-entry-number-error', errorMsg);
    }
}

function ValidateAddTrackInputFieldEntry(callingObject) {
    var container = $(callingObject).parents('.add-tracks-entry-container:first');
    var errorContainer = container.next();

    ClearAddTrackInputFieldEntryError(errorContainer, 'add-tracks-entry-name-error');
    if (trim($(callingObject).val()).length == 0) {
        SetAddTrackInputFieldEntryError(errorContainer, 'add-tracks-entry-name-error', 'Please enter a track name.');
    }
}

function DeleteAddTrackInputFieldEntry(callingObject) {
    var container = $(callingObject).parents('.add-tracks-entry-container:first');
    var errorContainer = container.next();

    container.remove();
    errorContainer.remove();
}

function ValidateAddTrackInputField(baseElementId) {
    var jqoContainer = $('#' + baseElementId);
    ClearErrorByJqo(jqoContainer.find('.add-tracks-other-error:first'));

    var error = false;
    jqoContainer.find(".add-tracks-error-container").each(function() {
	    if ($(this).css('display') != 'none') {
		error = true;
	    }
	});
    
    var trackNumbers = [];

    jqoContainer.find('.add-tracks-entry-container').each(function() {
	    trackNumbers.push(parseInt($(this).find('.add-tracks-entry-number:first').val(), 10));
	});
    
    // make sure there are no duplicate track numbers
    // the following logic works because track sorting ensures that the track numbers are monotonically increasing
    for (var i = 0; i < trackNumbers.length - 1; i++) {
	if (trackNumbers[i] == trackNumbers[i+1]) {
	    SetErrorByJqo(jqoContainer.find('.add-tracks-other-error:first'), "Please make sure that every track number is unique.");
	    error = true;
	}
    }

    return !error;
}

function SerializeAddTrackInputField(baseElementId) {
    var tracks = []

    $('#' + baseElementId + ' .add-tracks-entry-container').each(function() {
	    var number = parseInt($(this).find('.add-tracks-entry-number:first').val(), 10);
	    var name = trim($(this).find('.add-tracks-entry-name:first').val());

	    tracks.push({'trackNumber': number, 'trackTitle': name});
	});

    return MakeURLQuery([['tracks', JSON.stringify(tracks)]]);
}

//
// Generic error block getters / setters
//
function SetError(errorDivID, msg) {
    SetErrorByJqo($("#" + errorDivID), msg);
}

function ClearError(errorDivID) {
    ClearErrorByJqo($("#" + errorDivID));
}

function SetErrorByJqo(jqo, msg) {
    jqo.html(msg);
    if (jqo.get(0).tagName.toLowerCase() == 'div')
        jqo.css('display', 'block');
    else
        jqo.css('display', 'inline');
}

function ClearErrorByJqo(jqo) {
    jqo.html("&nbsp;");
    jqo.css('display', 'none');
}

//
// Used in conjunction with TimeDurationControl for validation
//
function ValidateTimeDuration(me, jsErrorCB) {
    if (GetTimeDuration(me) >= 0) {
	jsErrorCB(me, '');
    }
    else {
	jsErrorCB(me, 'Please enter a blank or something like "33", "2:15" or "1:12:00"');
    }
}

function GetTimeDuration(obj) {
    var value = trim($(obj).val());

    // either the value is blank
    if (value.length == 0) {
	return 0;
    }

    // or it is a positive int separated by 0 to 2 ":"'s
    var parts = value.split(':');
    if (parts.length >= 3) {
        return -1;
    }

    var total = 0;
    var number = 0;
    for (var i=0; i < parts.length; i++) {
	number = parseInt(parts[i], 10);
        if (number >= 0) {
	    total = 60 * total + number;
        }
	else {
            return -1;
	}
    }

    return total;
}

function BrowseLeft(elementID, offset, totalRelations) {
    BrowsePage(elementID, offset - 1, totalRelations);
}

function BrowseRight(elementID, offset, totalRelations) {
    BrowsePage(elementID, offset + 1, totalRelations);
}

function BrowsePage(elementID, offset, totalRelations) {
    var formdata = "parentDocID=" + elementID.replace('browse-row-', '') + '&currentOffset=' + offset + '&totalRelations=' + totalRelations;

    var relIDs = [];
    $('#' + elementID + ' .rel-id').each(function() {
	    relIDs.push($(this).html());
	});
    
    var docIDs = [];
    $('#' + elementID + ' .doc-id').each(function() {
	    docIDs.push($(this).html());
	});

    formdata += '&relDocIDs='
    for (var i = offset; i < totalRelations && i - offset < 3; i++) {
	formdata += docIDs[i] + '|';
    }

    formdata += '&bridgingRelID=' + relIDs[offset];

    $.ajax({
	    type: "POST",
		url: "/browse.py/browseresult",
		data: formdata,
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + elementID).children().slice(1).remove();
		    $('#' + elementID).append(msg.substr(1));
		}
		else { // handle errors
		    HandleGenericError(msg, elementID + '-error');
		}
	    }
	});
}

// this function must be kept in sync w/ the frontend utility function of the same name
function ValidateURL(url) {
    var pattern = new RegExp("^(https?://)" 
			     + "(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" //user@ 
			     + "(([0-9]{1,3}\\.){3}[0-9]{1,3}" // IP- 199.194.52.184 
			     + "|" // allows either IP or domain 
			     + "([0-9a-z_!~*'()-]+\\.)*" // tertiary domain(s)- www. 
			     + "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\\." // second level domain 
			     + "[a-z]{2,6})" // first level domain- .com or .museum 
			     + "(:[0-9]{1,4})?" // port number- :80 
			     + "((/?)|" // a slash isn't required if there is no file name 
			     + "(/[0-9a-z_!~*'().;?:@&=+$,%#-/]+)+/?)$");

    if (pattern.test(url.toLowerCase())) {
	return true;
    }

    return false;
}

function ValidateSimpleString(str) {
    return str.replace(/[\w\s\.,]+/, "").length == 0;
}

function ToggleButton(buttonID, enabled) {
    ToggleButtonByJqo($('#' + buttonID), enabled);
}

function ToggleButtonByJqo(jqo, enabled) {
    if (enabled) {
	jqo.attr('disabled', '');
	jqo.addClass('button');
	jqo.removeClass('button-disabled');
    }
    else {
	jqo.attr('disabled', 'disabled');
	jqo.addClass('button-disabled');
	jqo.removeClass('button');
    }
}

//*****
//* Async paging control related functions
//*****

function HighlightPageFlipper(caller, prev, next, size) {
    if (size == null)
	size = 'NORMAL';

    var jqoContainer = $(caller).parents('.page-flipper-container:first');

    var jqoPrev = jqoContainer.find('.page-flipper-prev:first');
    if (jqoPrev.hasClass('page-flipper-disabled'))
	prev = false;
    else {
	if (size == 'LARGE') {
	    if (prev) {
		jqoPrev.css('color', 'rgb(0, 0, 0)');
		jqoPrev.css('background-color', 'rgb(230, 230, 230)');
		jqoPrev.find('.page-icon:first').css('background-position', '-68px -156px');
	    }
	    else {
		jqoPrev.css('color', 'rgb(90, 90, 90)');
		jqoPrev.css('background-color', 'rgb(242, 242, 242)');
		jqoPrev.find('.page-icon:first').css('background-position', '-34px -156px');
	    }
	} else {
	    if (prev) {
		jqoPrev.css('color', 'rgb(0, 0, 0)');
		jqoPrev.css('background-color', 'rgb(230, 230, 230)');
		jqoPrev.find('.page-icon:first').css('background-position', '-11px -135px');
	    }
	    else {
		jqoPrev.css('color', 'rgb(147, 149, 152)');
		jqoPrev.css('background-color', 'rgb(242, 242, 242)');
		jqoPrev.find('.page-icon:first').css('background-position', '0 -135px');
	    }
	}
    }

    var jqoNext = jqoContainer.find('.page-flipper-next:first');
    if (jqoNext.hasClass('page-flipper-disabled'))
	next = false;
    else {
	if (size == 'LARGE') {
	    if (next) {
		jqoNext.css('color', 'rgb(0, 0, 0)');
		jqoNext.css('background-color', 'rgb(230, 230, 230)');
		jqoNext.find('.page-icon:first').css('background-position', '-85px -156px');
	    }
	    else {
		jqoNext.css('color', 'rgb(90, 90, 90)');
		jqoNext.css('background-color', 'rgb(242, 242, 242)');
		jqoNext.find('.page-icon:first').css('background-position', '-51px -156px');
	    }
	} else {
	    if (next) {
		jqoNext.css('color', 'rgb(0, 0, 0)');
		jqoNext.css('background-color', 'rgb(230, 230, 230)');
		jqoNext.find('.page-icon:first').css('background-position', '-33px -135px');
	    }
	    else {
		jqoNext.css('color', 'rgb(147, 149, 152)');
		jqoNext.css('background-color', 'rgb(242, 242, 242)');
		jqoNext.find('.page-icon:first').css('background-position', '-22px -135px');
	    }
	}
    }

    var jqoMiddle = jqoContainer.find('.page-flipper-middle:first');
    if (size == 'LARGE') {
	if (!prev && !next)
	    jqoMiddle.css('background-position', '-60px -27px');
	else if (prev)
	    jqoMiddle.css('background-position', '-95px -27px');
	else if (next)
	    jqoMiddle.css('background-position', '-130px -27px');
    }
    else if (size == 'NORMAL') {
        if (!prev && !next)
            jqoMiddle.css('background-position', '-37px -50px');
        else if (prev)
            jqoMiddle.css('background-position', '-37px -94px');
        else if (next)
            jqoMiddle.css('background-position', '-37px -72px');
    }
}

function HighlightVerticalPageFlipper(caller, prev, next) {
    var jqoContainer = $(caller).parents('.page-flipper-container:first');

    var jqoPrev = jqoContainer.find('.page-flipper-prev:first');
    if (jqoPrev.hasClass('page-flipper-disabled'))
	prev = false;
    else {
	if (prev) {
	    jqoPrev.css('background-color', 'rgb(230, 230, 230)');
	    jqoPrev.find('.page-icon:first').css('background-position', '-99px -109px');
	}
	else {
	    jqoPrev.css('background-color', 'rgb(242, 242, 242)');
	    jqoPrev.find('.page-icon:first').css('background-position', '-78px -109px');
	}
    }

    var jqoNext = jqoContainer.find('.page-flipper-next:first');
    if (jqoNext.hasClass('page-flipper-disabled'))
	next = false;
    else {
	if (next) {
	    jqoNext.css('background-color', 'rgb(230, 230, 230)');
	    jqoNext.find('.page-icon:first').css('background-position', '-99px -120px');
	}
	else {
	    jqoNext.css('background-color', 'rgb(242, 242, 242)');
	    jqoNext.find('.page-icon:first').css('background-position', '-78px -120px');
	}
    }

    var jqoMiddle = jqoContainer.find('.page-flipper-middle:first');
    if (!prev && !next)
	jqoMiddle.css('background-position', '-57px -89px');
    else if (prev)
	jqoMiddle.css('background-position', '-101px -89px');
    else if (next)
	jqoMiddle.css('background-position', '-79px -89px');
}

function ToggleTallPagingButtonByJqo(jqo, enable) {
    if (enable) {
	jqo.css('display', 'block');
    }
    else {
	jqo.css('display', 'none');
    }
}

function HighlightPageLeft(caller, highlight) {
    var jqo = $(caller);
    if (highlight) {
	jqo.css('background-color', 'rgb(230, 230, 230)');
	jqo.find('.page-icon:first').css('background-position', '-11px -135px');
    }
    else {
	jqo.css('background-color', 'rgb(242, 242, 242)');
	jqo.find('.page-icon:first').css('background-position', '0 -135px');
    }
}

function HighlightPageRight(caller, highlight) {
    var jqo = $(caller);
    if (highlight) {
	jqo.css('background-color', 'rgb(230, 230, 230)');
	jqo.find('.page-icon:first').css('background-position', '-33px -135px');
    }
    else {
	jqo.css('background-color', 'rgb(242, 242, 242)');
	jqo.find('.page-icon:first').css('background-position', '-22px -135px');
    }
}

// the following locking mechanisms actually work in javascript because javascript is pseudo-synchronous

// returns true if the control is locked in the middle of a page attempt
function AsyncPagingIsLocked(elementID) {
    return $('#' + elementID + ' #paging-lock').html() == '1';
}

// locks an async paging control
function AsyncPagingLock(elementID) {
    $('#' + elementID + ' #paging-lock').html('1');
}

// unlocks an async paging control
function AsyncPagingUnlock(elementID) {
    $('#' + elementID + ' #paging-lock').html('0');
}

//*****
//* Voting control related functions
//*****

function VoteSet(baseID, vote, voteCountText) {
    if (vote) {
	hide(baseID + '-vote-up');
	showInline(baseID + '-voted-up');
	showInline(baseID + '-vote-down');
	hide(baseID + '-voted-down');
    }
    else {
	showInline(baseID + '-vote-up');
	hide(baseID + '-voted-up');
	hide(baseID + '-vote-down');
	showInline(baseID + '-voted-down');
    }

    $('#' + baseID + '-votes').html(voteCountText);
}

function VoteError(baseID, errorCode) {
    hide(baseID + '-vote-up');
    hide(baseID + '-voted-up');
    hide(baseID + '-divider');
    hide(baseID + '-vote-down');
    hide(baseID + '-voted-down');

    SetGenericError(errorCode, baseID + '-error');
    showInline(baseID + '-error');
}

function VoteUpSet(baseID, countText) {
    var jqoActionLabel = $("#" + baseID + ' .vote-action-label:first');
    jqoActionLabel.html("Liked");

    countText = trim(countText);
    if (countText != null ) {
	var jqoCount = $("#" + baseID + ' .vote-count:first');
	if (jqoCount.length > 0)
	    jqoCount.replaceWith(countText);
    }
}

function VoteUpError(baseID, errorCode) {
    //hide(baseID + '-vote-up');
    //hide(baseID + '-voted-up');

    SetGenericError(errorCode, baseID + '-error');
    showInline(baseID + '-error');
}

function ClearVoteUpError(baseID) {
    //hide(baseID + '-vote-up');
    //hide(baseID + '-voted-up');

    ClearError(baseID + '-error');
    hide(baseID + '-error');
}

//*****
//* Explore control related functions
//*****

function PageExploratoryRelationships(caller, itemId, offset) {
    var postData = MakeURLQuery([["itemId", itemId], ['offset', offset]]);

    var jqoCaller = $(caller);
    var jqoContainer = jqoCaller.parents('.exploratory-relationships-container:first');
    var jqoError = jqoContainer.find('.exploratory-relationships-container-error:first');

    // clear previous errors
    ClearErrorByJqo(jqoError);

    $.ajax({
	    type: "POST",
	    url: "/relationships/PageExploratoryRelationships",
	    data: postData,
            error: function(request, status, error) {
		HandleRelationshipErrorByJqo('8', jqoError)
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    jqoContainer.html(msg.substr(1));
		}
		else { // handle errors
		    HandleRelationshipErrorByJqo(msg, jqoError)
		}
	    }
	});
}

function ExplorePageAsync(itemID) {
    // check lock
    if (AsyncPagingIsLocked('explore-view')) {
	return;
    }

    var jqoContainer = $('#explore-view');

    // sufficiently close to the last currently available page?
    if ( jqoContainer.get(0).scrollHeight - jqoContainer.get(0).scrollTop > 400 ) {
	return;
    }

    // is there more data?
    var nextPageOffset = parseInt($('#explore-view #next-page-offset').html(), 10);
    if (nextPageOffset == -1) {
	return;
    }

    // set lock before going async
    AsyncPagingLock('explore-view');
    
    // prep async call
    var formdata = "itemID=" + itemID + '&offset=' + nextPageOffset;
    var refinement = $('#explore-view #explore-pane-refinements').html();
    if (refinement != 'null') {
	formdata += '&refinement=' + refinement;
    }
    var incomingRelationshipID = $('#explore-view-incoming-relationship-id').html();
    if (incomingRelationshipID.length > 0) {
	formdata += '&incomingRelationshipID=' + incomingRelationshipID;
    }
    
    // post
    $.ajax({
	    type: "POST",
		url: "/relationships.py/Explore",
		data: formdata,
            error: function(request, status, error) {
		HandleGenericError('8', 'explore-view-error');
	    },
            complete: function(request, status) {
		AsyncPagingUnlock('explore-view');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    // remove the previous next-page-offset element as this will be replaced in the new appendage
		    $('#explore-view #next-page-offset').parents('tr:first').remove();
		    
		    $('#explore-pane-contents').append(msg.substr(1));
		}
		else { // handle errors
		    HandleRelationError(msg, 'explore-view-error');
		}
	    }
	});
}

function Explore(itemID, refinement) {
    // check lock
    if (AsyncPagingIsLocked('explore-view')) {
	return;
    }

    // clear previous error
    ClearError('explore-view-error');

    // set lock before going async
    AsyncPagingLock('explore-view');
    
    // get post ready
    var formdata = "itemID=" + itemID;
    var refinement = refinement; // lexical scoping
    if (refinement != null) {
	formdata += '&refinement=' + refinement;
    }

    // fade out
    $('#explore-pane-lock').html('1'); // prevent timing conditions when trying to fade back in

    $('#explore-pane-contents').fadeOut('fast', function() {
	    showBlock('explore-pane-loading');
	    $('#explore-pane-lock').html('0'); // unlock to allow fade in
	});

    // post
    $.ajax({
	    type: "POST",
		url: "/relationships.py/Explore",
		data: formdata,
            error: function(request, status, error) {
		HandleGenericError('8', 'explore-view-error');
	    },
            complete: function(request, status) {
		AsyncPagingUnlock('explore-view');
	    },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    // update refinements
		    $('#explore-controls .relationship-refinement-text').css('display', 'none');
		    $('#explore-controls .relationship-refinement-link').css('display', 'inline');

		    if (refinement == null) {
			$('#explore-controls #refine-link-all').css('display', 'none');
			$('#explore-controls #refine-text-all').css('display', 'inline');

			// set hidden data for async paging
			$('#explore-pane-refinements').html('null');
		    }
		    else {
			$('#explore-controls #refine-link-' + refinement).css('display', 'none');
			$('#explore-controls #refine-text-' + refinement).css('display', 'inline');

			// set hidden data for async paging
			$('#explore-pane-refinements').html(refinement);
		    }

		    $('#explore-pane-contents').html(msg.substr(1));

		    // use helper to wait until the fade out is done to fade in results
		    ExploreCBHelper();
		}
		else { // handle errors
		    HandleRelationError(msg, 'explore-view-error');
		}
	    }
	});
}

function ExploreCBHelper() {
    if ($('#explore-pane-lock').html() == '1') {
	window.setTimeout('ExploreCBHelper();', 100);
	return;
    }
    else {
	hide('explore-pane-loading');
	$('#explore-pane-contents').fadeIn('normal');

	return;
    }
}

function EditExploratoryRelationship(controlID, relID, view) {
    // keep these vars in static scope for cb
    var controlID = controlID;
    var relID = relID;
    var view = view;

    // clear prior errors
    ClearError(controlID + '-error');
    
    // check whether all required fields are filled
    var relTypes = RelAdderGetRelTypes(controlID);
    relTypes = relTypes[0]; // since this control only allows editing of 1 relationship at a time
    
    if (relTypes.length == 0) {
	SetError(controlID + '-error', "Please check one or more applicable vine types.");
	return;
    }

    var comment = trim($('#' + controlID + '-comment').val());
    if (comment.length == 0) {
	SetError(controlID + '-error', "Please provide some commentary.");
	return;
    }
    if (comment.length < _minXRelationshipCommentLength) {
	SetError( controlID + '-error', 'Please make sure your commentary is at least ' +
		  _minXRelationshipCommentLength + ' characters long.' );
	return;
    }
    if (comment.length > 1000) {
	SetError(controlID + '-error', "Your comments are too long. Please use 1000 characters or less.");
	return;
    }

    var postData = "relationshipID=" + relID + "&comment=" + encodeURIComponent(comment) + "&types=" + encodeStringArray(relTypes) + '&view=' + view;
    
    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
            type: "POST",
                url: "/relationships/EditExploratoryRelationship",
                data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  if ($('#' + controlID).length > 0) { // not present if the containing element is replaced
		      hide(controlID + '-status');
		      ToggleButton(controlID + '-save', true);
		      ToggleButton(controlID + '-cancel', true);
		  }
	        },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    if (view == 'FULL_PAGE') {
			var results = JSON.parse(msg.substr(1));

			$('#exploratory-relationship-heading-' + relID).html(results['HEADING']);
			$('#exploratory-relationship-commentary-' + relID).replaceWith(results['COMMENTARY_HTML']);

			hide(controlID);
		    } else { // compact view
			$('#' + controlID).parents('.compact-exploratory-relationship:first').replaceWith(msg.substr(1));
		    }
                }
                else { // error
                    HandleRelationError(msg, controlID + "-error");
                }
            }
        });
}

function VoteOnExploratoryRelationship(relationshipId, votingControlBaseID) {
    // lexical scoping for callback
    var votingControlBaseID = votingControlBaseID;
    
    var formdata = "relationshipId=" + relationshipId + "&vote=true";

    ClearVoteUpError(votingControlBaseID);

    $.ajax({
            type: "POST",
                url: "/relationships/Vote",
                data: formdata,
            error: function(request, status, error) {
		VoteUpError(votingControlBaseID, '8');
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    VoteUpSet(votingControlBaseID, msg.substr(1));
                }
                else { //success
		    VoteUpError(votingControlBaseID, msg);
                }
            }
        });
}

//*****
//* Dynamic menu related scripts
//*****

$(function() {
	InitializeDynamicMenus();

	$(document).click(function(e) {
		var target = null;
		if (e.target) target = e.target;
		else if (e.srcElement) target = e.srcElement;

		if ($(target).parents('.expandable-menu-container:first').length == 0) {
		    $('.expandable-menu-active').each(function() {
			    $(this).find('.expandable-menu-expanded:first').css('display', 'none');
			    $(this).removeClass('expandable-menu-active');
			});
		}
        });
});

function InitializeDynamicMenus() { // deprecated eventing style due to performance impace
    $('.menu-heading').each(function() {
	    $(this).click(function(e) {
		    $(this).parents('.expandable-menu-container:first').find('.menu-expanded:first').css('display', 'block');
		    return false; // prefer link follow through
		});
	});

    $(document).click(function(e) {
	    $('.menu-expanded').each(function() {
		    $(this).css('display', 'none');
		});
	});
}

function ShowExpandableMenu(caller) {
    var jqoCaller = $(caller);
    var jqoParent = jqoCaller.parents('.expandable-menu-container:first');
    jqoParent.addClass('expandable-menu-active');

    var jqoMenu = jqoParent.find('.expandable-menu-expanded:first');
    if (jqoMenu.css('display') == 'block')
	jqoMenu.css('display', 'none');
    else
	jqoMenu.css('display', 'block');
}

//*****
//* Hierarchy picker related scripts
//*****

function ExpandChildren(jqoThis) {
    var jqoChildren = jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-children-container:first');

    if (jqoChildren.length > 0) {
	jqoChildren.css('display', 'block');
	jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-expand-button:first').css('display', 'none');
	jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-collapse-button:first').css('display', 'inline');
    }
}

function CollapseChildren(jqoThis) {
    var jqoChildren = jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-children-container:first');

    if (jqoChildren.length > 0) {
	jqoChildren.css('display', 'none');
	jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-expand-button:first').css('display', 'inline');
	jqoThis.parents('.hierarchy-node-container:first').find('.hierarchy-collapse-button:first').css('display', 'none');
    }
}

function ToggleNode(obj, controlID, minSelection, maxSelection) {
    // figure out if this results in a check or uncheck
    var jqoCheckbox = $(obj).parents('.hierarchy-node-container:first').children("[type='checkbox']").eq(0);
    var state = jqoCheckbox.attr('checked');

    if (state) {
	UncheckNode(jqoCheckbox);
    }
    else {
	CheckNode(jqoCheckbox);
    }
}

function ToggleNodeFromCheckbox(obj, controlID, minSelection, maxSelection) {
    // figure out if this results in a check or uncheck
    var jqoCheckbox = $(obj).parents('.hierarchy-node-container:first').children("[type='checkbox']").eq(0);
    var state = jqoCheckbox.attr('checked');

    if (state) {
	CheckNode(jqoCheckbox);
    }
    else {
	UncheckNode(jqoCheckbox);
    }
}

function UncheckNode(jqoCheckbox) {
    // uncheck node
    jqoCheckbox.attr('checked', '');

    // uncheck all child nodes if needed
    var jqoChildrenContainer = jqoCheckbox.parents('.hierarchy-node-container:first').find('.hierarchy-children-container:first');
    
    if (jqoChildrenContainer.length > 0) {
	jqoChildrenContainer.children(".hierarchy-node-container").each(function() {
		UncheckNode($(this).children("[type='checkbox']:first"));
	    });
    }
}

function CheckNode(jqoCheckbox) {
    // check node
    jqoCheckbox.attr('checked', 'checked');

    // check all parent nodes
    var jqoParentContainer = jqoCheckbox.parents('.hierarchy-node-container:first').parents('.hierarchy-node-container:first');
    if (jqoParentContainer.length > 0) {
	CheckNode(jqoParentContainer.children("[type='checkbox']:first"));
    }

    // expand children if any
    var jqoChildrenContainer = jqoCheckbox.parents('.hierarchy-node-container:first').find('.hierarchy-children-container:first');
    
    if (jqoChildrenContainer.length > 0) {
	ExpandChildren(jqoCheckbox);
    }
}

function GetSelectedNodes(controlID) {
    var selectedNodeIDs = GetSelectedNodesHelper($('#' + controlID));

    return selectedNodeIDs;
}

function GetSelectedNodesHelper(jqoContainer) {
    var selectedNodeIDs = new Array();

    jqoContainer.children(".hierarchy-node-container").each(function() {
	    var jqoCheckbox = $(this).children("[type='checkbox']:first");

	    if (jqoCheckbox.attr('checked')) {
		var jqoChildrenContainer = $(this).children('.hierarchy-children-container:first');
		var selectedDescendentNodeIDs = new Array();

		if (jqoChildrenContainer.length > 0) {
		    selectedDescendentNodeIDs = GetSelectedNodesHelper(jqoChildrenContainer);
		}

		if (selectedDescendentNodeIDs.length > 0) {
		    selectedNodeIDs = selectedNodeIDs.concat(selectedDescendentNodeIDs);
		}
		else {
		    selectedNodeIDs.push(jqoCheckbox.val());
		}
	    }
	});

    return selectedNodeIDs;
}

//*****
//* Category picker related scripts - depends on hierarchy picker related scripts
//*****

function GetCategoryInputFieldValue(baseElementId) {
    return GetSelectedNodes(baseElementId + '-category-input');
}

function ValidateCategoryInputField(baseElementId, minSelection, maxSelection) {
    ClearError(baseElementId + '-category-error');

    var selection = GetCategoryInputFieldValue(baseElementId);

    if (selection.length < minSelection) {
	SetError(baseElementId + '-category-error', 'You must select at least ' + minSelection + ' paths.');
	return false;
    }
    else if (selection.length > maxSelection) {
	SetError(baseElementId + '-category-error', 'You cannot select more than ' + maxSelection + ' paths');
	return false;
    }

    return true;
}

function SerializeCategoryInputField(baseElementId) {
    return 'categories=' + GetCategoryInputFieldValue(baseElementId).join('|');
}

function UpdateCategories(controlID, itemID, minSelection, maxSelection) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;

    ClearError(controlID + '-other-error');

    // validate
    if (!ValidateCategoryInputField(controlID, minSelection, maxSelection)) {
	return;
    }

    // get values
    var postData = "itemId=" + itemID + "&categories=" + GetCategoryInputFieldValue(controlID).join('|');

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/page/UpdateCategories",
		data: postData,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-other-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-display-value').html(msg.substr(1));
		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { //1st char of msg will indicate failure
		    HandleModeratedChangeError(msg, controlID + '-other-error');
		}
	    }
	});
}

//*****
//* Multi-select edit control related scripts
//*****

function GetMultiSelectInputFieldValue(baseElementId) {
    var values = new Array();

    $('#' + baseElementId).find('[type="checkbox"]').each(function() {
	    if ($(this).attr('checked')) {
		values.push($(this).val());
	    }
	});

    return values;
}

function ValidateMultiSelectInputField(baseElementId) {
    return true;
}

function SerializeMultiSelectInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId).find('.field-key:first').attr('value');
    var values = GetMultiSelectInputFieldValue(baseElementId);

    return MakeURLQuery([[fieldKey, values.join('|')]]);
}

function UpdateMultiSelectValues(controlID, itemID, attributeKey) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;
    
    // clear previous errors
    ClearError(controlID + '-error');
    
    // check values
    var selectedValues = GetMultiSelectInputFieldValue(controlID + '-picker');

    var postData = "docID=" + itemID + "&" + SerializeMultiSelectInputField(controlID + '-picker');

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
	    type: "POST",
		url: "/item.py/UpdateDoc",
		data: postData,
                error: function(request, status, error) {
		  HandleEditError('8', controlID + '-error');
	        },
                complete: function(request, status) {
		  hide(controlID + '-status');
		  ToggleButton(controlID + '-save', true);
		  ToggleButton(controlID + '-cancel', true);
	        },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $('#' + controlID + '-display-value').html(msg.substr(1));
		    ToggleEditControlOff(controlID);
		}
		else if (msg.charAt(0) == 'Q') {
		    ToggleEditControlOff(controlID);
		    showAlert('change-queued');
		}
		else { //1st char of msg will indicate failure
		    HandleModeratedChangeError(msg, controlID + '-error');
		}
	    }
	});
}

//*****
//* Single-select edit control related scripts
//*****

function ValidateSingleSelectInputField(baseElementId) {
    return true;
}

function GetSingleSelectInputFieldValue(baseElementId) {
    return $('#' + baseElementId + '-select').val();
}

function SerializeSingleSelectInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId + '-field-key').attr('value');
    return MakeURLQuery([[fieldKey, GetSingleSelectInputFieldValue(baseElementId)]]);
}

function UpdateSingleSelectValue(controlID, itemID, attributeKey) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;

    // clear previous errors
    ClearError(controlID + '-error');

    var postData = "docID=" + itemID + "&" + SerializeSingleSelectInputField(controlID + '-picker');

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
            type: "POST",
                url: "/item.py/UpdateDoc",
                data: postData,
                error: function(request, status, error) {
		HandleEditError('8', controlID + '-error');
	    },
                complete: function(request, status) {
		hide(controlID + '-status');
		ToggleButton(controlID + '-save', true);
		ToggleButton(controlID + '-cancel', true);
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $('#' + controlID + '-display-value').html(msg.substr(1));
                    ToggleEditControlOff(controlID);
                }
                else if (msg.charAt(0) == 'Q') {
                    ToggleEditControlOff(controlID);
                    showAlert('change-queued');
                }
                else { //1st char of msg will indicate failure
                    HandleModeratedChangeError(msg, controlID + '-error');
                }
            }
        });
}

//*****
//* Link edit control related scripts
//*****

function GetLinkInputFieldValue(baseElementId) {
    var link = trim($('#' + baseElementId + '-input').val());

    if (link.length == 0)
	return link;

    // try auto-appending http:// if needed
    if (!ValidateURL(link)) {
        link = 'http://' + link;
	if (!ValidateURL(link))
	    return null;
    }

    return link;
}

function ValidateLinkInputField(baseElementId) {
    ClearError(baseElementId + '-error');

    if (GetLinkInputFieldValue(baseElementId) == null) {
    	SetError(baseElementId + '-error', "Please input a valid URL in the same format as you would use to open the site in your browser.");
	return false;
    }
    
    return true;
}

function SerializeLinkInputField(baseElementId) {
    var fieldKey = $('#' + baseElementId + '-field-key').attr('value');
    return MakeURLQuery([[fieldKey, GetLinkInputFieldValue(baseElementId)]]);
}

function UpdateLink(controlID, itemID, attributeKey) {
    // keep these vars in static scope for ajax cb
    var controlID = controlID;
    var itemID = itemID;

    // clear previous errors
    ClearError(controlID + '-error');

    // check value
    if (!ValidateLinkInputField(controlID + '-input')) {
	return;
    }

    var postData = "docID=" + itemID + "&" + SerializeLinkInputField(controlID + '-input');

    // advise of request in UI
    showBlock(controlID + '-status');
    ToggleButton(controlID + '-save', false);
    ToggleButton(controlID + '-cancel', false);

    $.ajax({
            type: "POST",
                url: "/item.py/UpdateDoc",
                data: postData,
                error: function(request, status, error) {
		HandleEditError('8', controlID + '-error');
	    },
                complete: function(request, status) {
		hide(controlID + '-status');
		ToggleButton(controlID + '-save', true);
		ToggleButton(controlID + '-cancel', true);
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $('#' + controlID + '-display-value').html(msg.substr(1));
                    ToggleEditControlOff(controlID);
                }
                else if (msg.charAt(0) == 'Q') {
                    ToggleEditControlOff(controlID);
                    showAlert('change-queued');
                }
                else { //1st char of msg will indicate failure
                    HandleModeratedChangeError(msg, controlID + '-error');
                }
            }
        });
}

//*****
//* Browse related scripts
//*****

function ToggleBrowseDialog(itemId, dialog, caller) {
    if (dialog == 'MEDIA') {
	var dialogId = GetSharedModalDialogId();
	hide(dialogId);

	$('#browse-play-media-' + itemId).click();
    }
    if (dialog == 'SUMMARY') {
	$(caller).parents('.dialog-container:first').css('display', 'none');
	StopMedia('preview-media-' + itemId + '-1');

	var jqoSummaryButton = $('#browse-play-media-' + itemId).parents('.browse-item-heading-container:first').find('.item-summary-button-container:first a');

	ShowItemSummary(itemId, jqoSummaryButton.get(0));
    }
}

function ShowItemSummary(itemId, caller) {
    // scoping for ajax callback
    var caller = caller;

    TrackEvent('Clicks', '/PreviewItem/' + itemId);

    ResetSharedModalDialog();
    SetSharedModalDialogTitle($(caller).parents('.item-summary-button-container:first').find('.item-summary-dialog-title:first').html());
    SetSharedModalDialogWidth('500px');

    var dialogId = GetSharedModalDialogId();
    var dialogContentId = GetSharedModalDialogContentId();

    var postData = MakeURLQuery([['itemId', itemId]]);

    showDialog(dialogId, null, getPosition(caller));

    $.ajax({
            type: "POST",
	    url: "/summary/SummaryDialog",
	    data: postData,
	    success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + dialogContentId).html(msg.substr(1));
		    showDialog(dialogId, null, getPosition(caller));

		    InitFacebook();
                }
            }
	});
}

function HideParentItemSummaryDialog(caller) {
    var dialogId = GetParentDialogId(caller);
    if (dialogId != undefined && dialogId != null)
	hide(dialogId);
}

function PrependBrowseResults(html, resultCount) {
    var jqoContainer = $('.browse-results-container:first');
    for (i=0; i < resultCount; i++) 
	jqoContainer.find('.browse-result:last').remove();

    jqoContainer.find('.browse-result:first').before(html);
    var jqoNew = jqoContainer.find('.browse-result:lt(' + resultCount + ')');
    jqoNew.css('display', 'none');
    jqoNew.fadeIn('normal');
    InitFacebook();
}

//*****
//* Refinement related scripts
//*****

function InitializeRefinements() {
    // clear all refinement checkboxes on page refresh since Firefox annoyingly caches everything w/o asking
    $('.refinement-container').each(function() {
	    $(this).find('.attribute-select:checked').each(function() {
		    if ($(this).attr('class').indexOf('ancestor-browse-nodes') > -1) {
			return; // ancestor is a hidden checkbox that should always be checked
		    }
		    else {
			$(this).attr('checked', '');
		    }
		});
	});

    $('.refinement-container-expanded').each(function() {
	    $(this).find('.attribute-select:checked').each(function() {
		    if ($(this).attr('class').indexOf('ancestor-browse-nodes') > -1) {
			return; // ancestor is a hidden checkbox that should always be checked
		    }
		    else {
			$(this).attr('checked', '');
		    }
		});
	});
}

function ToggleRefinementsPane(controlID, expanded) {
    if (expanded) {
	$('.condensed-refinement').each(function() {
		$(this).css('display', 'none');
	    });

	showBlock(controlID + '-collapse-refinements-button');
	showBlock(controlID + '-expanded');
    }
    else {
	hide(controlID + '-collapse-refinements-button');
	hide(controlID + '-expanded');

	$('.condensed-refinement').each(function() {
		$(this).css('display', 'block');
	    });
    }
}

function ToggleRefinement(callingObj, checkboxClass) {
    var targetState = null;

    // if the event occured on the selector, just use the selector's current state as the target state
    if ($(callingObj).hasClass('attribute-select')) {
	if ($(callingObj).attr('checked')) {
	    targetState = 'checked';
	}
	else {
	    targetState = '';
	}
    }
    else if ($(callingObj).hasClass(checkboxClass + '-inline-link')) { // for inline links, determine target state from font-weight
	if ($(callingObj).hasClass('browse-link-selected')) 
	    targetState = '';
	else
	    targetState = 'checked';
    }
    else { // otherwise, the target state is the opposite of the state on the corresponding input selector
	if ($(callingObj).siblings('.attribute-select:first').attr('checked')) {
	    targetState = '';
	}
	else {
	    targetState = 'checked';
	}
    }

    // implement the target state on each selector in case selector's are mirrored (i.e. collapsed and expanded views)
    $('.' + checkboxClass).each(function() {
	    $(this).attr('checked', targetState);
	});

    // (un)highlight inline links as appropriate
    $('.' + checkboxClass + '-inline-link').each(function() {
	    if (targetState == 'checked') {
		$(this).addClass('browse-link-selected');
	    }
	    else {
		$(this).removeClass('browse-link-selected');
	    }
	});
}

function Refine(callingAttr, mode, controlID, resultContainerID) {
    // lexical scoping
    var callingAttr = callingAttr;
    var mode = mode;
    var controlID = controlID;
    var resultContainerID = resultContainerID;

    // grab refinement values
    var query = $('#' + controlID + '-base-query').html();
    var browseNodes = new Array();
    var anchorBrowseNodes = new Array();
    var activeAttributes = new Array();

    // read from expanded version since it contains a superset of the refinements in the collapsed version
    // and both are mirrored
    $('.refinement-container-expanded').each(function() { 
	    var attr = $(this).find('.attribute-name:first').html();

	    var values = new Array();
	    
	    // special case category since we need to handle the hierarchical nature of it correctly
	    if (attr == 'BROWSE') {
		var leafs = new Array();
		
		$(this).find('.attribute-select:checked').each(function() {
			if ($(this).attr('class').indexOf('ancestor-browse-nodes') > -1) {
			    anchorBrowseNodes = $(this).val().split('|');
			}
			else {
			    leafs.push($(this).val());
			}
		    });

		if (leafs.length == 0) { // use the ancestor if no leafs are selected
		    browseNodes = anchorBrowseNodes;
		}
		else { // use the selected leafs and don't include the ancestor since it'll be OR'd in the search
		    browseNodes = leafs;
		}
	    }
	    else {
		$(this).find('.attribute-select:checked').each(function() {
			values.push($(this).val());
		    });

		// update menu heading
		if (values.length == 0) {
		    $('#' + attr + '-menu-heading').html('Select... ▼');
		}
		else {
		    $('#' + attr + '-menu-heading').html(values.length + ' Selection(s) ▼');
		}
	    }

	    if (values.length > 0) {
		activeAttributes.push(attr);
	    }

	    for (var i = 0; i < values.length; i ++) {
		query += ' ' + attr + ':' + values[i];
	    }
	});

    query = trim(query);

    // update static refinement links with new urls based on the async refinements
    $('.refinement-link-static').each(function() {
	    // identify the attribute name
	    var container = $(this).parents('.refinement-container-expanded:first');
	    if (container.length == 0) {
		container = $(this).parents('.refinement-container:first');
	    }

	    var attribute = trim(container.find('.attribute-name:first').html());
	    var attributeValue = trim($(this).parents('.attribute-value-container:first').find('.attribute-value:first').html());

	    // special case browse node since this is not a standard query refinement
	    if (attribute == 'BROWSE') {
		// take the current query refinements as is and rewrite the url
		var url = $(this).attr('href');
		var urlParts = url.split('?');
		var urlQuery = urlParts[1];
		var queryParts = urlQuery.split('&');
		for (var i=0; i < queryParts.length; i++) {
		    if (queryParts[i].indexOf('refinements=') > -1) {
			queryParts[i] = 'refinements=' + encodeURIComponent(query);
		    }
		}
		url = urlParts[0] + '?' + queryParts.join('&');

		$(this).attr('href', url);

		return;
	    }

	    // sub in the attribute name-value pair into the query, replacing any pre-existing values for the same attribute
	    var pattern = new RegExp(attribute + ':[\\w-]+[\\s]*');
	    var staticQuery = query.replace(pattern, "");
	    staticQuery += ' ' + attribute + ':' + attributeValue;
	    staticQuery = trim(staticQuery);

	    // rewrite the url
	    var url = $(this).attr('href');
	    var urlParts = url.split('?');
	    var urlQuery = urlParts[1];
	    var queryParts = urlQuery.split('&');
	    for (var i=0; i < queryParts.length; i++) {
		if (queryParts[i].indexOf('refinements=') > -1) {
		    queryParts[i] = 'refinements=' + encodeURIComponent(staticQuery);
		}
		else if (queryParts[i].indexOf('b=') > -1) {
		    queryParts[i] = 'b=' + encodeURIComponent(browseNodes.join('|'));
		}
	    }
	    url = urlParts[0] + '?' + queryParts.join('&');

	    $(this).attr('href', url);
	});

    // update static sort links with new urls based on the async refinements
    $('.sort-link').each(function() {
	    // rewrite the url
	    var url = $(this).attr('href');

	    if (url.indexOf('refinements=') == -1 && query.length > 0) {
		url += '&refinements='
	    }
	    if (url.indexOf('b=') == -1 && browseNodes.length > 0) {
		url += '&b='
	    }

	    url = url.replace(/refinements=[^&]*/, 'refinements=' + encodeURIComponent(query));
	    if (browseNodes.length > 0)
		url = url.replace(/b=[^&]*/, 'b=' + encodeURIComponent(browseNodes.join('|')));
	    $(this).attr('href', url);
	});

    var postData = MakeURLQuery([['mode', mode], ['query', query], ['anchorBrowseNodes', anchorBrowseNodes]]);
    if (browseNodes.length > 0)
	postData += '&' + MakeURLQuery([['browseNodes', browseNodes.join('|')]])

    // get sort value
    if ($('#refinement-selected-sort-value').length > 0) {
	postData += '&sort=' + trim($('#refinement-selected-sort-value').html());
    }


    // fade out
    $('#' + controlID + '-fade-out-lock').html('1'); // prevent timing conditions when trying to fade back in

    $('#' + resultContainerID).fadeOut('slow', function() {
	    showBlock(resultContainerID + '-refinement-status');
	    $('#' + controlID + '-fade-out-lock').html('0'); // unlock to allow fade in
	});
    
    // UI advisement for async request
    ClearError(resultContainerID + '-refinement-error');

    // post
    $.ajax({
	    type: "POST",
	    url: "/refine.py",
	    data: postData,
            error: function(request, status, error) {
                HandleGenericError('8', resultContainerID + '-refinement-error');
            },
            complete: function(request, status) {
		hide(resultContainerID + '-refinement-status');
            },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    // update collapsed refinements
		    $('.refinement-container').each(function() {
			    var attr = $(this).find('.attribute-name:first').html();

			    // skip the calling refinement
			    if (attr == callingAttr) {
				return;
			    }

			    // skip attributes that had state prior to the refinement
			    for (var i=0; i < activeAttributes.length; i++) {
				if (activeAttributes[i] == attr)
				    return;
			    }

			    $(this).find('.attribute-value-container').each(function() {
				    var value = trim($(this).find('.attribute-value:first').html());

				    var binValues = results['BINS'][attr];

				    var enabled = true;

				    // if a bin is not available
				    // then it should be empty so that all its values are disabled
				    if (binValues == undefined) {
					enabled = false;
				    } else if (binValues[value] == undefined) {
					enabled = false;
				    }

				    var attrContainer = $(this);
				    var attrEnabledElement = attrContainer.find('.attribute-value-enabled:first');
				    var attrDisabledElement = attrContainer.find('.attribute-value-disabled:first');
				    if (enabled) {
					if (attrEnabledElement.attr('tagName') == 'div') {
					    attrEnabledElement.css('display', 'block');
					}
					else {
					    attrEnabledElement.css('display', 'inline');
					}
					attrDisabledElement.css('display', 'none');
				    }
				    else {
					attrEnabledElement.css('display', 'none');
					if (attrDisabledElement.attr('tagName') == 'div') {
					    attrDisabledElement.css('display', 'block');
					}
					else {
					    attrDisabledElement.css('display', 'inline');
					}
				    }
				});
			});

		    // update expanded refinements
		    $('.refinement-container-expanded').each(function() {
			    var attr = $(this).find('.attribute-name:first').html();

			    // skip the calling refinement
			    if (attr == callingAttr) {
				return;
			    }

			    // skip attributes that had state prior to the refinement
			    for (var i=0; i < activeAttributes.length; i++) {
				if (activeAttributes[i] == attr)
				    return;
			    }

			    $(this).find('.attribute-value-container').each(function() {
				    var value = trim($(this).find('.attribute-value:first').html());

				    var binValues = results['BINS'][attr];

				    var enabled = true;

				    // if a bin is not available
				    // then it should be empty so that all its values are disabled
				    if (binValues == undefined) {
					enabled = false;
				    } else if (binValues[value] == undefined) {
					enabled = false;
				    }

				    var attrContainer = $(this);
				    var attrEnabledElement = attrContainer.find('.attribute-value-enabled:first');
				    var attrDisabledElement = attrContainer.find('.attribute-value-disabled:first');
				    if (enabled) {
					if (attrEnabledElement.attr('tagName') == 'div') {
					    attrEnabledElement.css('display', 'block');
					}
					else {
					    attrEnabledElement.css('display', 'inline');
					}
					attrDisabledElement.css('display', 'none');
				    }
				    else {
					attrEnabledElement.css('display', 'none');
					if (attrDisabledElement.attr('tagName') == 'div') {
					    attrDisabledElement.css('display', 'block');
					}
					else {
					    attrDisabledElement.css('display', 'inline');
					}
				    }
				});
			});
		    
		    // update results pane
		    $('#' + resultContainerID).html(results["RESULT_HTML"]);

		    // update results count
		    $('#refinement-results-summary').html(results["RESULTS_SUMMARY_HTML"]);

                    // use helper to wait until the fade out is done to fade in results
                    RefineHelper(controlID, resultContainerID);
		}
		else { // handle errors
		    HandleGenericError(msg, resultContainerID + '-refinement-error');
		}
	    }
	});
}

function RefineHelper(controlID, resultContainerID) {
    if ($('#' + controlID + '-fade-out-lock').html() == '1') {
	window.setTimeout('RefineHelper("' + controlID + '", "' + resultContainerID + '");', 100);
        return;
    }
    else {
	hide(resultContainerID + '-refinement-status');
	$('#' + resultContainerID).fadeIn('normal');
	if (correctPNG) // IE6- transparency correction
	    correctPNG();
	InitFacebook();
	return;
    }
}

//*****
//* Item feed & review related scripts
//*****

function PageItemFeed(caller, itemId, page) {
    var jqoContainer = $(caller).parents('.item-feed-container:first');
    var jqoError = jqoContainer.find('.item-feed-error:first');
    ClearErrorByJqo(jqoError);

    var refinement = trim(jqoContainer.find('.feed-refinement:first').val());

    var postData = MakeURLQuery([["itemId", itemId], ["page", page], ['refinement', refinement]]);

    $.ajax({
            type: "POST",
            url: "/feed/GetItemFeedPage",
            data: postData,
	    error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoError);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    jqoContainer.html(msg.substr(1));
		    InitFacebook(); // refresh dynamically added profile image on page
                }
                else { //1st char of msg will indicate failure
		    HandleGenericErrorByJqo(msg, jqoError);
                }
            }
        });
}

function RefineItemFeed(caller, itemId, refinement, maxEntries, allowPaging) {
    var jqoContainer = $(caller).parents('.item-feed-container:first');
    var jqoError = jqoContainer.find('.item-feed-error:first');
    var refinement = refinement; // lexical scoping

    ClearErrorByJqo(jqoError);

    var allowPagingParam = '1';
    if (!allowPaging)
	allowPagingParam = '0';
    var postData = MakeURLQuery([["itemId", itemId], ["refinement", refinement], ['allowPaging', allowPagingParam]]);
    if (maxEntries != null)
	postData += '&' + MakeURLQuery([['maxEntries', maxEntries]]);

    $.ajax({
            type: "POST",
            url: "/feed/GetItemFeedPage",
            data: postData,
	    error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoError);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    jqoContainer.html(msg.substr(1));
		    InitFacebook(); // refresh dynamically added profile image on page
                }
                else { //1st char of msg will indicate failure
		    HandleGenericErrorByJqo(msg, jqoError);
                }
            }
        });
}

function ToggleFeedCommentContextualControl(caller, enable) {
    var jqoContainer = $(caller).parents('.item-feed-post-container:first');
    var jqoControl = jqoContainer.find('.review-rating-container:first');
    var jqoPrivacy = jqoContainer.find('.review-privacy-container:first');

    if (enable)
	jqoPrivacy.css('display', 'block');

    if (jqoControl.length > 0) {
	if (enable)
	    jqoControl.css('display', 'block');
	else {
	    var containerId = jqoContainer.attr('id');
	    // wait in case an event when propagated sets the rating to be non-null
	    window.setTimeout('HideFeedCommentContextualControls("' + containerId + '");', 100);
	}
    }

    if (!enable) { // defer this case and other setTimeout invocations to end 
	var containerId = jqoContainer.attr('id');
	// wait in case an event when propagated sets the rating to be non-null
	window.setTimeout('HideFeedCommentContextualControls("' + containerId + '");', 100);
    }
}

function HideFeedCommentContextualControls(containerId) {
    var jqoContainer = $('#' + containerId);
    var jqoControl = jqoContainer.find('.review-rating-container:first');
    var jqoPrivacy = jqoContainer.find('.review-privacy-container:first');
    var jqoInput = jqoContainer.find('.item-feed-post-text:first');
    var hide = true;
    if (trim(GetHelpfulTextAreaInput(jqoInput)).length > 0)
	hide = false;
    else if (jqoControl.length > 0 && GetRating(jqoControl) != -1)
	hide = false;
    else if (jqoPrivacy.find('input:first').attr('checked'))
	hide = false;

    if (hide) {
	if (jqoControl.length > 0) {
	    jqoControl.css('display', 'none');
	}

	jqoPrivacy.css('display', 'none');
    }
}

function AddReview(caller, itemId, maxReviewTitleLength, maxReviewBodyLength, maxReviewBodyLines) {
    // keep these vars in static scope for ajax cb
    var jqoContainer = $(caller).parents('.item-feed-post-container:first');
    var itemId = itemId;

    var jqoRatingControl = jqoContainer.find('.review-rating-container:first');
    var jqoPrivacyControl = jqoContainer.find('.review-privacy:first');

    var rating = -1;
    if (jqoRatingControl.length > 0)
	rating = GetRating(jqoRatingControl);

    var isPrivate = 0;
    if (jqoPrivacyControl.attr('checked'))
	isPrivate = 1;

    var jqoInput = jqoContainer.find('.item-feed-post-text:first');
    var comment = trim(GetHelpfulTextAreaInput(jqoInput));

    var width = parseInt(jqoContainer.find('.item-feed-width:first').val(), 10);

    // clear previous form errors
    var jqoError = jqoContainer.find('.add-review-error:first');
    ClearErrorByJqo(jqoError);

    // form validation
    var error = false;
    if (comment.length == 0) { // check for blank
	SetErrorByJqo(jqoError, "Please enter a comment first.");
	return;
    }
    else if (comment.length > maxReviewBodyLength) {
	SetErrorByJqo(jqoError, "Please keep your comment under " + maxReviewBodyLength + " characters.");
	return;
    }
    else if (CountLines(comment) > maxReviewBodyLines) {
	SetErrorByJqo(jqoError, 'There are too many lines in your comment. Please use ' + maxReviewBodyLines + ' or fewer lines (a new line is used each time you press "Enter").');
	return;
    }

    var postData = MakeURLQuery([['itemId', itemId], ['comment', comment], ['width', width], ['isPrivate', isPrivate]]);
    if (rating > -1)
	postData += '&rating=' + 10 * rating;

    // advise user of save status
    var jqoStatus = jqoContainer.find('.add-review-status:first');
    var jqoButton = jqoContainer.find('.add-review-button:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoButton, false);

    $.ajax({
	    type: "POST",
	    url: "/reviews/AddReview",
	    data: postData,
            error: function(request, status, error) {
		HandleEditErrorByJqo('8', jqoError);
	    },
            complete: function(request, status) {
		jqoStatus.css('display', 'none');
		ToggleButtonByJqo(jqoButton, true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    /* not in use for now
		    if (results['EMAIL_SETTINGS_UNSET']) {
			showAlert('inline-list-update-email-settings-dialog');
		    }
		    */

		    jqoContainer.nextAll(".item-feed-container:first").prepend(results['REVIEW_HTML']);

		    // clear inputs
		    jqoInput.val('');
		    UpdateHelpfulTextAreaState(jqoInput.get(0), false);
		    if (rating > -1) {
			jqoRatingControl.remove();
			// CancelRating(jqoContainer.find('.rating-cancel:first').get(0));
			// HideFeedCommentRatingControl(jqoContainer.attr('id'));
		    } else if (jqoRatingControl.length > 0)
			jqoRatingControl.css('display', 'none');

		    // handle Facebook story publication
		    if (!results['PROMPT_FB_POST'])
			return;

		    InitFacebook(); // refresh dynamically added profile image on page

		    var fbMessage = results['FB_POST']['MESSAGE'];
		    var fbAttachment = results['FB_POST']['ATTACHMENT'];
		    var fbActionLinks = results['FB_POST']['ACTION_LINKS'];

		    FB.Connect.streamPublish(fbMessage, fbAttachment, fbActionLinks, null, 'Post:', FacebookStreamPublishCallback);
		}
		else { // error
		    HandleEditErrorByJqo(msg, jqoError);
		}
	    }
	});
}

function EditReview(dialogId, reviewId, maxReviewBodyLength, maxReviewBodyLines) {
    // keep these vars in static scope for ajax cb
    var dialogId = dialogId;
    var reviewId = reviewId;

    var jqoDialog = $('#' + dialogId);

    var rating = -1;
    var jqoRatingControl = jqoDialog.find('.edit-review-rating:first');
    if (jqoRatingControl.length > 0)
	rating = GetRating(jqoRatingControl);

    var jqoInput = jqoDialog.find('.edit-review-text:first');
    var comment = trim(GetHelpfulTextAreaInput(jqoInput));

    // clear previous form errors
    var jqoError = jqoDialog.find('.edit-review-error:first');
    ClearErrorByJqo(jqoError);

    // form validation
    if (comment.length == 0) { // check for blank
	SetErrorByJqo(jqoError, "Please provide a comment.");
	return;
    }
    else if (comment.length > maxReviewBodyLength) {
	SetErrorByJqo(jqoError, "Please keep your comment under " + maxReviewBodyLength + " characters.");
	return;
    }
    else if (CountLines(comment) > maxReviewBodyLines) {
        SetErrorByJqo(jqoError, 'There are too many lines in your comment. Please use ' + maxReviewBodyLines + ' or fewer lines (a new line is used each time you press "Enter").');
	return;
    }

    var postData = MakeURLQuery([["reviewId", reviewId], ['text', comment]]);
    if (rating > -1)
	postData += '&rating=' + 10 * rating;

    // advise user of save status
    var jqoStatus = jqoDialog.find('.edit-review-status:first');
    var jqoSave = jqoDialog.find('.edit-review-save:first');
    var jqoCancel = jqoDialog.find('.edit-review-cancel:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoSave, false);
    ToggleButtonByJqo(jqoCancel, false);

    $.ajax({
	    type: "POST",
	    url: "/reviews/EditReview",
	    data: postData,
            error: function(request, status, error) {
		HandleEditErrorByJqo('8', jqoError);
	    }, 
	    complete: function(request, status) {
		jqoStatus.css('display', 'none');
		ToggleButtonByJqo(jqoSave, true);
		ToggleButtonByJqo(jqoCancel, true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#review-" + reviewId).replaceWith(msg.substr(1));

		    hide(dialogId);
		}
		else { // error
		    HandleEditErrorByJqo(msg, jqoError);
		}
	    }
	});
}

function DeleteReview(controlId, reviewId) {
    // lexical scoping for CB
    var controlId = controlId;
    var reviewId = reviewId;

    var postData = MakeURLQuery([["reviewId", reviewId]]);

    // clear previous errors
    ClearDeleteErrors(controlId);

    AdviseDeleteStart(controlId);

    $.ajax({
            type: "POST",
            url: "/reviews/DeleteReview",
            data: postData,
            error: function(request, status, error) {
                HandleDeleteError('8', controlId);
                AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var jqoReview = $('#review-' + reviewId);
		    var jqoPrev = jqoReview.prev();
		    if (jqoPrev.hasClass('item-feed-divider'))
			jqoPrev.remove()
                    jqoReview.remove();

                    hide(controlId);
                }
                else { // handle errors
                    HandleDeleteError(msg, controlId);
		    AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
                }
            }
        });
}

function VoteOnReview(reviewID, votingControlBaseID) {
    // lexical scoping for callback
    var reviewID = reviewID;
    var votingControlBaseID = votingControlBaseID;
    
    var postData = "reviewID=" + reviewID;

    ClearVoteUpError(votingControlBaseID);
    
    $.ajax({
            type: "POST",
                url: "/reviews.py/Vote",
                data: postData,
            error: function(request, status, error) {
		VoteUpError(votingControlBaseID, '8');
            },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    VoteUpSet(votingControlBaseID, msg.substr(1));
                }
                else { //success
		    VoteUpError(votingControlBaseID, msg);
                }
            }
        });
}

// assumes h, s, l values are 0 - 255
function HSLToRGB(h, s, l) {
    if (s == 0) {
	return [255, 255, 255];
    }
    
    // normalize
    h = h / 255.0;
    s = s / 255.0;
    l = l / 255.0;
    
    var q;
    if (l < 0.5) {
	q = l * (1 + s);
    }
    else {
	q = l + s - l * s;
    }
    
    var p = 2 * l - q;

    var r = PQTToRGB(p, q, h + 1/3);
    var g = PQTToRGB(p, q, h);
    var b = PQTToRGB(p, q, h - 1/3);

    return [r * 255, g * 255, b * 255];
}

function PQTToRGB(p, q, t){
    if (t < 0) {
	t += 1;
    }

    if (t > 1) {
	t -= 1;
    }
    
    if (t < 1/6) {
	return p + (q - p) * 6 * t;
    }

    if (t < 1/2) {
	return q;
    }

    if (t < 2/3) {
	return p + (q - p) * 6 * (2/3 - t);
    }

    return p;
};

// takes a rating in the range [0, 10] and converts to a color from red to green
function RatingToColor(rating) {
    var hue = rating * 7;

    var rgb = HSLToRGB(hue, 255, 100);

    return "rgb(" + Math.round(rgb[0]) + ", " + Math.round(rgb[1]) + ", " + Math.round(rgb[2]) + ")";
}

function GetRatingLabel(rating) {
    switch(rating) {
    case 0:
	return 'Abysmal';
    case 1:
	return 'Horrible';
    case 2:
	return 'Bad';
    case 3:
	return 'Poor';
    case 4:
	return 'Subpar';
    case 5:
	return 'Average';
    case 6:
	return 'Decent';
    case 7:
	return 'Good';
    case 8:
	return 'Great';
    case 9:
	return 'Exceptional';
    case 10:
	return 'Perfect';
    }
}

function ShowRating(jqoContainer, rating) {
    var color = RatingToColor(rating);
    if (rating > -1) {
	jqoContainer.find('.rating-bar:lt(' + rating+1 + ')').css('background-color', color);
	jqoContainer.find('.rating-bar:lt(' + rating+1 + ')').css('color', 'black');
	jqoContainer.find('.rating-bar:gt(' + rating + ')').css('background-color', 'white');
	jqoContainer.find('.rating-bar:gt(' + rating + ')').css('color', '#808080');
    } else {
	jqoContainer.find('.rating-bar').css('background-color', 'white');
	jqoContainer.find('.rating-bar').css('color', '#808080');
    }

    jqoRatingDescription = jqoContainer.find('.rating-description:first');
    if (rating > -1) {
	jqoRatingDescription.html(GetRatingLabel(rating));
	jqoRatingDescription.css('display', 'inline');
    } else {
	jqoRatingDescription.css('display', 'none');
    }
}

function GetRating(jqoContainer) {
    return parseInt(jqoContainer.find('.rating-selection:first').val(), 10);
}

function HighlightRating(caller) {
    var jqoCaller = $(caller);
    var jqoContainer = jqoCaller.parents('.rating-control-container:first');

    var rating = jqoCaller.prevAll('.rating-bar').length;
    ShowRating(jqoContainer, rating);
}

function UnhighlightRating(caller) {
    var jqoCaller = $(caller);
    var jqoContainer = jqoCaller.parents('.rating-control-container:first');
    var rating = GetRating(jqoContainer);

    ShowRating(jqoContainer, rating);
}

function SelectRating(caller) {
    var jqoCaller = $(caller);
    var jqoContainer = jqoCaller.parents('.rating-control-container:first');
    var rating = jqoCaller.prevAll('.rating-bar').length;

    jqoContainer.find('.rating-selection:first').val(rating);
    var jqoRatingCancel = jqoContainer.find('.rating-cancel:first');
    if (jqoRatingCancel.length > 0 )
	jqoRatingCancel.css('display', 'inline');
}

function CancelRating(caller) {
    var jqoCaller = $(caller);
    var jqoContainer = jqoCaller.parents('.rating-control-container:first');

    ShowRating(jqoContainer, -1);
    jqoContainer.find('.rating-selection:first').val(-1);
    var jqoRatingCancel = jqoContainer.find('.rating-cancel:first');
    if (jqoRatingCancel.length > 0 )
	jqoRatingCancel.css('display', 'none');

    ToggleFeedCommentContextualControl(caller, false);
}

//*****
//* Top contributors / contributions related scripts
//*****

function PageContributors(containerID, itemID, offset) {
    // keep these vars in static scope for ajax cb
    var containerID = containerID;

    // clear previous errors
    ClearError(containerID + '-error');

    var postData = "containerID=" + containerID + "&itemID=" + itemID + "&offset=" + offset;

    $.ajax({
            type: "POST",
            url: "/reputation.py/GetPageContributors",
            data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', containerID + '-error');
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $('#' + containerID).html(msg.substr(1));
                }
		else { //1st char of msg will indicate failure
                    HandleGenericError(msg, containerID + '-error');
                }
            }
        });
}

function PageContributions(containerID, userID, offset) {
    // keep these vars in static scope for ajax cb
    var containerID = containerID;

    // clear previous errors
    ClearError(containerID + '-error');

    var postData = "containerID=" + containerID + "&userID=" + userID + "&offset=" + offset;

    $.ajax({
            type: "POST",
            url: "/reputation.py/GetContributions",
            data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', containerID + '-error');
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $('#' + containerID).html(msg.substr(1));
                }
		else { //1st char of msg will indicate failure
                    HandleGenericError(msg, containerID + '-error');
                }
            }
        });
}

//*****
//* Contact management related scripts
//*****

function ClearContactManagementError(domBaseID) {
    hide(domBaseID + '-error');
}

function SetContactManagementError(domBaseID, errorCode) {
    HandleContactError(errorCode, domBaseID + '-error');
    showBlock(domBaseID + '-error');
}

function GetContactSelection(containingId) {
    encryptedUserIds = new Array();

    $('#' + containingId).find('.contact-selection-input').each(function() {
	    if ($(this).attr('checked')) {
		encryptedUserIds.push($(this).val());
	    }
	});

    return encryptedUserIds;
}

function Follow(targetUserID, domBaseID, includeStats) {
    ClearContactManagementError(domBaseID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var domBaseID = domBaseID;
    var followFriendDialogID = domBaseID + '-follow-friend-request';
    var emailSettingsDialogID = 'manage-contacts-email-settings';

    var postData = "targetUserID=" + targetUserID + "&domBaseID=" + domBaseID + '&followFriendDialogID=' + encodeURIComponent(followFriendDialogID) + '&emailSettingsDialogID=' + encodeURIComponent(emailSettingsDialogID) + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/Follow",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(domBaseID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    var results = JSON.parse(msg.substr(1));
		
		    // update allowed state transitions with this contact
		    $('#' + domBaseID).replaceWith(results['NEW_OPTIONS_HTML']);

		    if (includeStats) {
			$('#contact-stats-' + targetUserID).replaceWith(results['CONTACT_STATS_HTML']);
		    }

		    // launch friend request dialog
		    if (!results['IS_FRIEND']) {
			$('#' + followFriendDialogID + '-contents').replaceWith(results['FRIEND_REQUEST_HTML']);

			if (results['EMAIL_SETTINGS_UNSET']) {
			    hide('follow-friend-request-dialog-skip');
			    showInline('follow-friend-request-dialog-skip-to-email-settings');
			}
			else {
			    showInline('follow-friend-request-dialog-skip');
			    hide('follow-friend-request-dialog-skip-to-email-settings');
			}

			showAlert(followFriendDialogID);
			InitFacebook(); // redraw facebook profile icons
		    }
		    else if (results['EMAIL_SETTINGS_UNSET']) { // the friends request path also needs to check for email settings unset
			showAlert(emailSettingsDialogID);
		    }
                }
                else {
		    SetContactManagementError(domBaseID, msg);
                }
            }
        });

}

function StopFollowing(targetUserID, domBaseID, includeStats) {
    ClearContactManagementError(domBaseID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var domBaseID = domBaseID;
    var includeStats = includeStats;

    var postData = "targetUserID=" + targetUserID + "&domBaseID=" + domBaseID + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/StopFollowing",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(domBaseID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    var results = JSON.parse(msg.substr(1));
		
		    // update allowed state transitions with this contact
		    $('#' + domBaseID).replaceWith(results['NEW_OPTIONS_HTML']);

		    if (includeStats) {
			$('#contact-stats-' + targetUserID).replaceWith(results['CONTACT_STATS_HTML']);
		    }
                }
                else {
		    SetContactManagementError(domBaseID, msg);
                }
            }
        });

}

function ShowAddFriendDialog(targetUserID, domBaseID, includeStats) {
    ClearContactManagementError(domBaseID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var domBaseID = domBaseID;
    var dialogID = 'add-friend-dialog';

    var postData = "targetUserID=" + targetUserID + "&manageContactControlsID=" + domBaseID + '&dialogID=' + dialogID + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/RenderAddFriendRequest",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(domBaseID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + dialogID).find('.add-friend-dialog-content:first').replaceWith(msg.substr(1));
		    showAlert(dialogID);
		    InitFacebook(); // redraw facebook profile icons
                }
                else {
		    SetContactManagementError(domBaseID, msg);
                }
            }
        });
}

function ShowRemoveFriendDialog(targetUserID, domBaseID, includeStats) {
    ClearContactManagementError(domBaseID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var domBaseID = domBaseID;
    var dialogID = 'remove-friend-dialog';

    var postData = "targetUserID=" + targetUserID + "&manageContactControlsID=" + domBaseID + '&dialogID=' + dialogID + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/RenderRemoveFriendRequest",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(domBaseID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + dialogID).find('.remove-friend-dialog-content:first').replaceWith(msg.substr(1));
		    showAlert(dialogID);
		    InitFacebook(); // redraw facebook profile icons
                }
                else {
		    SetContactManagementError(domBaseID, msg);
                }
            }
        });
}

function AddFollowFriend(targetUserID, manageContactControlsID, dialogID, includeStats) {
    AddFriend(targetUserID, manageContactControlsID, dialogID, dialogID + '-contents', 'follow-friend-request-dialog-error', 'follow-friend-request-dialog-status', 'follow-friend-request-dialog-add', 'follow-friend-request-dialog-skip', 'follow-friend-request-dialog-message', 'manage-contacts-email-settings', includeStats);
}

function AddFriendOnly(targetUserID, manageContactControlsID, dialogID, includeStats) {
    AddFriend(targetUserID, manageContactControlsID, dialogID, 'add-friend-dialog-content', 'add-friend-dialog-error', 'add-friend-dialog-status', 'add-friend-dialog-ok', 'add-friend-dialog-cancel', 'add-friend-dialog-message', null, includeStats);
}

function AddFriend(targetUserID, manageContactControlsID, dialogID, dialogContentID, errorID, statusID, addButtonID, cancelButtonID, messageInputID, emailSettingsDialogID, includeStats) {
    // keep these vars in static scope for ajax cb
    var targetUserID = targetUserID;
    var manageContactControlsID = manageContactControlsID;
    var dialogID = dialogID;
    var dialogContentID = dialogContentID;
    var errorID = errorID;
    var statusID = statusID;
    var addButtonID = addButtonID;
    var cancelButtonID = cancelButtonID;
    var messageInputID = messageInputID;
    var emailSettingsDialogID = emailSettingsDialogID;

    // clear previous errors
    ClearError(errorID);

    // create post data
    var postData = "targetUserID=" + targetUserID + '&manageContactControlsID=' + encodeURIComponent(manageContactControlsID) + '&dialogID=' + encodeURIComponent(dialogID) + '&includeStats=' + includeStats;

    if (emailSettingsDialogID != null) {
	postData += '&emailSettingsDialogID=' + encodeURIComponent(emailSettingsDialogID);
    }

    var message = trim($('#' + messageInputID).val());
    if (message.length > 0) {
	postData += '&message=' + encodeURIComponent(message);
    }

    // advise of request in UI
    showBlock(statusID);
    ToggleButton(addButtonID, false);
    ToggleButton(cancelButtonID, false);

    $.ajax({
            type: "POST",
            url: "/contacts/do/AddFriend",
            data: postData,
            error: function(request, status, error) {
                HandleContactError('8', errorID);
                hide(statusID);
                ToggleButton(addButtonID, true);
                ToggleButton(cancelButtonID, true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    // update allowed state transitions with this contact
		    $('#' + manageContactControlsID).replaceWith(results['NEW_OPTIONS_HTML']);

                    $('#' + dialogContentID).html(results['CONFIRMATION_HTML']);
		    showAlert(dialogID);
		    InitFacebook(); // redraw facebook profile icons
		    
		    $('#' + dialogID).fadeOut(2000, function() {
			    if (emailSettingsDialogID != null && results['EMAIL_SETTINGS_UNSET']) {
				showAlert(emailSettingsDialogID);
			    }
			});
                }
                else { //1st char of msg will indicate failure
		    if (msg.charAt(0) == '6') {
			SetError(errorID, 'You have already sent a friend request to this user. Please wait for them to accept your request.');
		    }
		    else {
			HandleContactError(msg, errorID);
		    }

		    hide(statusID);
		    ToggleButton(addButtonID, true);
		    ToggleButton(cancelButtonID, true);
                }
            }
        });
}

function AcceptFriendRequest(requestingUserId) {
    // keep these vars in static scope for ajax cb
    var requestingUserId = requestingUserId;

    // convenience variables
    var errorId = 'friend-request-error-' + requestingUserId;
    var statusId = 'friend-request-status-' + requestingUserId;
    var acceptButtonId = 'friend-request-accept-' + requestingUserId;
    var ignoreButtonId = 'friend-request-ignore-' + requestingUserId;

    // clear previous errors
    ClearError(errorId);

    // create post data
    var postData = "requestingUserId=" + encodeURIComponent(requestingUserId);

    // advise of request in UI
    showBlock(statusId);
    ToggleButton(acceptButtonId, false);
    ToggleButton(ignoreButtonId, false);

    $.ajax({
            type: "POST",
            url: "/contacts/do/AcceptFriendRequest",
            data: postData,
            error: function(request, status, error) {
                HandleContactError('8', errorId);
                hide(statusId);
		ToggleButton(acceptButtonId, true);
		ToggleButton(ignoreButtonId, true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
                    $('#friend-request-container-' + requestingUserId).replaceWith(msg.substr(1));
                }
                else { //1st char of msg will indicate failure
		    HandleContactError(msg, errorId);
		    hide(statusId);
		    ToggleButton(acceptButtonId, true);
		    ToggleButton(ignoreButtonId, true);
                }
            }
        });
}

function IgnoreFriendRequest(callingObj, requestingUserId, messageId) {
    // keep these vars in static scope for ajax cb
    var callingObj = callingObj;
    var requestingUserId = requestingUserId;
    var messageId = messageId;

    // convenience variables
    var errorId = 'friend-request-error-' + requestingUserId;
    var statusId = 'friend-request-status-' + requestingUserId;
    var acceptButtonId = 'friend-request-accept-' + requestingUserId;
    var ignoreButtonId = 'friend-request-ignore-' + requestingUserId;

    // clear previous errors
    ClearError(errorId);

    // create post data
    var postData = "requestingUserId=" + encodeURIComponent(requestingUserId) + "&messageId=" + encodeURIComponent(messageId);

    // advise of request in UI
    showBlock(statusId);
    ToggleButton(acceptButtonId, false);
    ToggleButton(ignoreButtonId, false);

    $.ajax({
            type: "POST",
            url: "/contacts/do/IgnoreFriendRequest",
            data: postData,
            error: function(request, status, error) {
                HandleContactError('8', errorId);
                hide(statusId);
		ToggleButton(acceptButtonId, true);
		ToggleButton(ignoreButtonId, true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $(callingObj).parents('.feed-entry-wrapper:first').remove();
                }
                else { //1st char of msg will indicate failure
		    HandleContactError(msg, errorId);
		    hide(statusId);
		    ToggleButton(acceptButtonId, true);
		    ToggleButton(ignoreButtonId, true);
                }
            }
        });
}

function RemoveFriend(targetUserID, manageContactControlsID, dialogID, includeStats) {
    // keep these vars in static scope for ajax cb
    var targetUserID = targetUserID;
    var manageContactControlsID = manageContactControlsID;
    var dialogID = dialogID;

    // clear previous errors
    ClearError('remove-friend-dialog-error');

    // create post data
    var postData = "targetUserID=" + targetUserID + '&manageContactControlsID=' + encodeURIComponent(manageContactControlsID) + '&dialogID=' + encodeURIComponent(dialogID) + '&includeStats=' + includeStats;

    // advise of request in UI
    showBlock('remove-friend-dialog-status');
    ToggleButton('remove-friend-dialog-ok', false);
    ToggleButton('remove-friend-dialog-cancel', false);

    $.ajax({
            type: "POST",
            url: "/contacts/do/RemoveFriend",
            data: postData,
            error: function(request, status, error) {
                HandleGenericError('8', 'remove-friend-dialog-error');
		hide('remove-friend-dialog-status');
		ToggleButton('remove-friend-dialog-ok', true);
		ToggleButton('remove-friend-dialog-cancel', true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    // update allowed state transitions with this contact
		    $('#' + manageContactControlsID).replaceWith(results['NEW_OPTIONS_HTML']);

                    $('#' + dialogID).find('.remove-friend-dialog-content:first').html(results['CONFIRMATION_HTML']);
		    showAlert(dialogID);
		    InitFacebook(); // redraw facebook profile icons
		    
		    $('#' + dialogID).fadeOut(2000, function() {
			    return;
			});
                }
                else { //1st char of msg will indicate failure
                    HandleGenericError(msg, 'remove-friend-dialog-error');
		    hide('remove-friend-dialog-status');
		    ToggleButton('remove-friend-dialog-ok', true);
		    ToggleButton('remove-friend-dialog-cancel', true);
                }
            }
        });
}

function ShowBlockUserDialog(targetUserID, domBaseID, includeStats) {
    ClearContactManagementError(domBaseID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var domBaseID = domBaseID;
    var dialogID = 'prompt-block-user-dialog';

    var postData = "targetUserID=" + targetUserID + "&manageContactControlsID=" + encodeURIComponent(domBaseID) + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/RenderBlockRequest",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(domBaseID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + dialogID).find('.prompt-block-user-dialog-contents:first').replaceWith(msg.substr(1));
		    showAlert(dialogID);
		    InitFacebook(); // redraw facebook profile icons
                }
                else {
		    SetContactManagementError(domBaseID, msg);
                }
            }
        });

}

function BlockUser(targetUserID, manageContactControlsID, confirmationDialogId, includeStats) {
    // keep these vars in static scope for ajax cb
    var targetUserID = targetUserID;
    var manageContactControlsID = manageContactControlsID;
    var confirmationDialogId = confirmationDialogId;

    // clear previous errors
    ClearError('block-user-dialog-error');

    // create post data
    var postData = "targetUserID=" + targetUserID + '&manageContactControlsID=' + encodeURIComponent(manageContactControlsID) + '&includeStats=' + includeStats;

    // advise of request in UI
    showBlock('block-user-dialog-status');
    ToggleButton('block-user-dialog-ok', false);
    ToggleButton('block-user-dialog-cancel', false);

    $.ajax({
            type: "POST",
            url: "/contacts/do/Block",
            data: postData,
            error: function(request, status, error) {
                HandleGenericError('8', 'block-user-dialog-error');
            },
	    complete: function(request, status) {
		hide('block-user-dialog-status');
		ToggleButton('block-user-dialog-ok', true);
		ToggleButton('block-user-dialog-cancel', true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    // update allowed state transitions with this contact
		    $('#' + manageContactControlsID).replaceWith(msg.substr(1));

		    hide('prompt-block-user-dialog');

		    showAlert(confirmationDialogId);
		    InitFacebook(); // redraw facebook profile icons
		    
		    $('#' + confirmationDialogId).fadeOut(_dialogFadeOut, function() {
			    return;
			});
                }
                else { //1st char of msg will indicate failure
                    HandleContactError(msg, 'block-user-dialog-error');
                }
            }
        });
}

function UnblockUser(targetUserID, manageContactControlsID, includeStats) {
    ClearContactManagementError(manageContactControlsID);

    // lexical scoping for callback
    var targetUserID = targetUserID;
    var manageContactControlsID = manageContactControlsID;

    var postData = "targetUserID=" + targetUserID + "&manageContactControlsID=" + encodeURIComponent(manageContactControlsID) + '&includeStats=' + includeStats;

    $.ajax({
            type: "POST",
            url: "/contacts/do/Unblock",
            data: postData,
	    error: function(request, status, error) {
                SetContactManagementError(manageContactControlsID, '8');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    // update allowed state transitions with this contact
		    $('#' + manageContactControlsID).replaceWith(msg.substr(1));
                }
                else {
		    SetContactManagementError(manageContactControlsID, msg);
                }
            }
        });

}

//*****
//* Notifications related scripts
//*****

function ShowMoreNotifications(view, timestamp) {
    // create post data
    var postData = 'view=' + encodeURIComponent(view) + "&timestamp=" + timestamp;

    // advise of request in UI
    hide('events-feed-more');
    showBlock('events-feed-more-status');

    $.ajax({
            type: "POST",
            url: "/notifications/ShowMore",
            data: postData,
            error: function(request, status, error) {
                HandleGenericError('8', 'events-feed-more-error');
            },
	    complete: function(request, status) {
		hide('events-feed-more-status');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#events-feed').append(msg.substr(1));
                }
                else { //1st char of msg will indicate failure
                    HandleGenericError(msg, 'events-feed-more-error');
                }
            }
        });
}

//*****
//* User list related scripts
//*****

function AddItemToList(itemID, listName, containerID, removeDomID, viewToRemoveFrom, mode) {
    // keep these vars in static scope for ajax cb
    var itemID = itemID;
    var listName = listName;
    var containerID = containerID;
    var removeDomID = removeDomID;
    var viewToRemoveFrom = viewToRemoveFrom;
    var mode = mode;
    if (mode == null)
	mode = 'PANEL';

    // clear previous errors
    ClearError(containerID + '-error');

    // create post data
    var postData = MakeURLQuery([["itemID", itemID], ['listName', listName], ['containerID', containerID], ['mode', mode]]);

    if (removeDomID != null && viewToRemoveFrom != null) {
	postData += '&removeDomID=' + removeDomID + '&viewToRemoveFrom=' + viewToRemoveFrom;
    }

    // advise of request in UI
    var jqoStatus = $('#' + containerID + '-status');
    if (jqoStatus.length > 0)
	jqoStatus.css('display', 'block');

    $.ajax({
            type: "POST",
            url: "/lists/AddItemToList",
            data: postData,
            error: function(request, status, error) {
                HandleEditError('8', containerID + '-error');
            },
	    complete: function(request, status) {
		if (jqoStatus.length > 0)
		    jqoStatus.css('display', 'none');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    $('#' + containerID).replaceWith(results['NEW_INLINE_CONTROL_HTML']);

		    showAlert('inline-list-update-confirmation-dialog');
		    
		    $('#inline-list-update-confirmation-dialog').fadeOut(3500, function() {
			    if (results['EMAIL_SETTINGS_UNSET']) {
				showAlert('inline-list-update-email-settings-dialog');
			    }
			});
                }
                else if (msg.charAt(0) == '5') { //1st char of msg will indicate failure
		    SetError(containerID + '-error', "You've exceeded the maximum allowed size for your collections. Please remove some entries from one or more collections and try again.");
                }
                else {
		    HandleEditError(msg, containerID + '-error');
                }
            }
        });
}

function RemoveItemFromList(itemID, listName, containerID, removeDomID, viewToRemoveFrom, mode) {
    // keep these vars in static scope for ajax cb
    var itemID = itemID;
    var listName = listName;
    var containerID = containerID;
    var removeDomID = removeDomID;
    var viewToRemoveFrom = viewToRemoveFrom;
    var mode = mode;
    if (mode == null)
        mode = 'PANEL';

    // clear previous errors
    ClearError(containerID + '-error');

    // create post data
    var postData = MakeURLQuery([["itemID", itemID], ['listName', listName], ['containerID', containerID], ['mode', mode]]);

    if (removeDomID != null && viewToRemoveFrom != null) {
	postData += '&removeDomID=' + removeDomID + '&viewToRemoveFrom=' + viewToRemoveFrom;
    }

    // advise of request in UI
    showBlock(containerID + '-status');

    $.ajax({
            type: "POST",
            url: "/lists/RemoveItemFromList",
            data: postData,
            error: function(request, status, error) {
                HandleEditError('8', containerID + '-error');
            },
	    complete: function(request, status) {
                hide(containerID + '-status');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    $('#' + containerID).replaceWith(results['NEW_INLINE_CONTROL_HTML']);
		    if (removeDomID != null && viewToRemoveFrom != null) {
			if (results['REMOVE_FROM_VIEW']) {
			    $('#' + removeDomID).remove();
			}
		    }

		    showAlert('inline-list-update-confirmation-dialog');
		    
		    $('#inline-list-update-confirmation-dialog').fadeOut(3500, function() {
			    return;
			});
                }
                else { //1st char of msg will indicate failure
		    HandleEditError(msg, containerID + '-error');
                }
            }
        });
}

//*****
//* User comments related scripts
//*****

function ToggleComments(baseId, enable) {
    if (enable) {
	showBlock(baseId + '-comments');

	hide(baseId + '-show-comments');
	showInline(baseId + '-hide-comments');
    }
    else {
	hide(baseId + '-hide-comments');
	showInline(baseId + '-show-comments');

	hide(baseId + '-comments');
    }
}

function ShowComments(parentId, baseId, parentItemId, targetType, width) {
    // handle case where comments were already loaded
    if (trim($('#' + baseId + '-loaded').html()) == '1') {
	ToggleComments(baseId, true);
	return;
    }

    // bring into lexical scope
    var baseId = baseId;
    var targetType = targetType;
    var parentId = parentId;
    var parentItemId = parentItemId;

    // clear errors
    ClearError(baseId + '-comments-loading-error');

    // create post data
    var postData = MakeURLQuery([["baseId", baseId], ['targetType', targetType], ['parentId', parentId], ["parentItemId", parentItemId], ['width', width]]);

    // advise of request in UI
    showBlock(baseId + '-comments-loading-status');

    $.ajax({
            type: "POST",
            url: "/comments/RenderComments",
            data: postData,
            error: function(request, status, error) {
                HandleGenericError('8', baseId + '-comments-loading-error');
            },
	    complete: function(request, status) {
                hide(baseId + '-comments-loading-status');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + baseId + '-comments').replaceWith(msg.substr(1));

		    $('#' + baseId + '-loaded').html('1');

		    ToggleComments(baseId, true);
                }
                else { //1st char of msg will indicate failure
		    HandleGenericError(msg, baseId + '-comments-loading-error');
                }
            }
        });
}

function AddComment(parentId, baseId, targetType, parentItemId, maxCommentLength) {
    // bring into lexical scope
    var baseId = baseId;
    var targetType = targetType;
    var parentId = parentId;
    var parentItemId = parentItemId;

    var jqoContainer = $('#' + baseId + '-comments');
    var jqoError = jqoContainer.find('.add-comment-error:first');

    // clear errors
    ClearErrorByJqo(jqoError);

    // check errors
    var jqoInput = jqoContainer.find('.add-comment-text:first');
    var comment = trim(GetHelpfulTextAreaInput(jqoInput));

    if (comment.length == 0) {
	SetErrorByJqo(jqoError, 'Please provide a comment.');
	return;
    }
    
    if (comment.length > maxCommentLength) {
	SetErrorByJqo(jqoError, 'Please limit your comment to ' + maxCommentLength + ' characters or less.');
	return;
    }

    // create post data
    var postData = MakeURLQuery([["baseId", baseId], ['targetType', targetType], ['parentId', parentId], ["parentItemId", parentItemId], ['comment', comment]]);

    // advise of request in UI
    var jqoStatus = jqoContainer.find('.add-comment-status:first');
    var jqoSave = jqoContainer.find('.add-comment-save:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoSave, false);

    $.ajax({
            type: "POST",
            url: "/comments/AddComment",
            data: postData,
            error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoError);
            },
	    complete: function(request, status) {
		jqoStatus.css('display', 'none');
		ToggleButtonByJqo(jqoSave, true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + baseId + '-existing-comments').append(msg.substr(1));

		    // clear old comment
		    jqoInput.val('');
		    UpdateHelpfulTextAreaState(jqoInput.get(0), false);

		    InitFacebook(); // redraw facebook profile icons
                }
                else if (msg.charAt(0) == '5') { //1st char of msg will indicate failure
		    SetErrorByJqo(jqoError, 'The maximum number of allowed comments has been posted here. No additional comments are allowed.');
                }
                else {
		    HandleGenericErrorByJqo(msg, jqoError);
                }
            }
        });
}

function RemoveComment(baseId, targetType, commentId) {
    // bring into lexical scope
    var baseId = baseId;
    var targetType = targetType;
    var commentId = commentId;

    // clear errors
    ClearError(baseId + '-comment-' + commentId + '-error');

    // create post data
    var postData = 'targetType=' + encodeURIComponent(targetType) + '&commentId=' + encodeURIComponent(commentId);

    // advise of request in UI
    showBlock(baseId + '-comment-' + commentId + '-status');

    $.ajax({
            type: "POST",
            url: "/comments/RemoveComment",
            data: postData,
            error: function(request, status, error) {
                HandleEditError('8', baseId + '-comment-' + commentId + '-error');
            },
	    complete: function(request, status) {
		hide(baseId + '-comment-' + commentId + '-status');
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#' + baseId + '-comment-' + commentId).remove();
                }
                else { //1st char of msg will indicate failure
		    HandleEditError(msg, baseId + '-comment-' + commentId + '-error');
                }
            }
        });
}

//*****
//* Messaging and sharing related scripts
//*****

function SendMessage(sendingUserId, receivingUserId, areFriends, pendingFriends, senderBlocked, maxMessageSize, maxMessageLines, activitiesFeedDomId) {
    // bring into lexical scope
    var sendingUserId = sendingUserId;
    var receivingUserId = receivingUserId;
    var areFriends = areFriends;
    var pendingFriends = pendingFriends;
    var senderBlocked = senderBlocked;
    var maxMessageSize = maxMessageSize;
    var maxMessageLines = maxMessageLines;
    var activitiesFeedDomId = activitiesFeedDomId;

    // clear errors
    ClearError('new-message-error');
  
    var errorBlocked = 'The recipient has blocked you.';
    var errorPendingFriendship = "You can't send messages to this recipient until the recipient approves your friend request.";
    var errorLackingFriendship = "You can only send messages to your friends. Add this person as a friend in order to message this person.";

    if (sendingUserId == receivingUserId) {
	SetError('new-message-error', "You can't send a message to yourself.");
	return;
    }

    // check whether messaging is blocked
    if (senderBlocked) {
	SetError('new-message-error', errorBlocked);
	return;
	}

    // check whether they're friends or pending friends
    if (pendingFriends) {
	SetError('new-message-error', errorPendingFriendship);
	return;
    }
    if (!areFriends) {
	SetError('new-message-error', errorLackingFriendship);
	return;
    }

    // get inputs
    var message = trim($('#new-message-text').val());
    var isPrivate = false;
    if ($('#new-message-private').attr('checked')) {
	isPrivate = true;
    }

    // validate message
    if (message.length == 0) {
	SetError('new-message-error', "Please enter a message.");
	return;
    }

    if (message.length > maxMessageSize) {
	SetError('new-message-error', "Your message is too long. Please keep your message to " + maxMessageSize + " characters or less.");
	return;
    }

    if (CountLines(message) > maxMessageLines) {
	SetError('new-message-error', "There are too many lines in your message. Please keep your message to " + maxMessageLines + " lines or fewer. Note that a new line is created each time you press \"Enter\".");
	return;
    }

    // create post data
    var postData = 'receivingUserId=' + encodeURIComponent(receivingUserId) + '&message=' + encodeURIComponent(message) + '&private=' + isPrivate;

    // advise of request in UI
    showBlock('new-message-status');
    ToggleButton('new-message-send', false);

    $.ajax({
            type: "POST",
            url: "/messaging/SendMessage",
            data: postData,
            error: function(request, status, error) {
                HandleEditError('8', 'new-message-error');
            },
	    complete: function(request, status) {
		hide('new-message-status');
		ToggleButton('new-message-send', true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    var feedSizeString = trim($('#new-message-activities-feed-tracking-count').html());
		    if (feedSizeString == '0') {
			$('#' + activitiesFeedDomId).replaceWith(results['ACTIVITY_HTML']);
			$('#new-message-activities-feed-tracking-count').html('1');
		    }
		    else {
			$('#' + activitiesFeedDomId).prepend(results['ACTIVITY_HTML']);
		    }

		    $('#new-message-text').val('');
		    $('#new-message-private').attr('checked', '');
                }
                else if (msg.charAt(0) == '5') {
		    var results = JSON.parse(msg.substr(1));

		    errorCode = results['ERROR_CODE'];

		    if (errorCode == 'MESSAGE_BLOCKED') {
			SetError('new-message-error', errorBlocked);
		    }
		    else if (errorCode == 'MESSAGE_PENDING_FRIEND_REQUEST') {
			SetError('new-message-error', errorPendingFriendship);
		    }
		    else if (errorCode == 'RECIPIENT_IS_NOT_FRIEND') {
			SetError('new-message-error', errorLackingFriendship);
		    }
                }
		else {
		    HandleEditError(msg, 'new-message-error');
		}
            }
        });
}

function RemoveMessage(callingObj, controlId, messageId) {
    // bring into lexical scope
    var callingObj = callingObj;
    var controlId = controlId;
    var messageId = messageId;

    var messageIds = new Array();
    messageIds.push(messageId);
    var postData = "messageIds=" + encodeStringArray(messageIds);

    // clear previous errors
    ClearDeleteErrors(controlId);

    // disable buttons and give status
    AdviseDeleteStart(controlId);

    $.ajax({
	    type: "POST",
	    url: "/messaging/RemoveMessages",
	    data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlId);
		AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $(callingObj).parents('.feed-entry-wrapper:first').remove();

		    hide(controlId);
		}
		else { // handle errors
		    HandleDeleteError(msg, controlId);
		    AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
		}
	    }
	});
}

function RemoveMessages(callingObj, controlId, messageIdsContainer) {
    // bring into lexical scope
    var callingObj = callingObj;
    var controlId = controlId;

    // get message IDs
    var messageIdsString = trim($('#' + messageIdsContainer).html());
    var tokens = messageIdsString.split('|');
    
    var messageIds = new Array();
    for (var i=0; i < tokens.length; i++) {
	messageIds.push(tokens[i]);
    }

    var postData = "messageIds=" + encodeStringArray(messageIds);

    // clear previous errors
    ClearDeleteErrors(controlId);

    // disable buttons and give status
    AdviseDeleteStart(controlId);

    $.ajax({
	    type: "POST",
	    url: "/messaging/RemoveMessages",
	    data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlId);
		AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $(callingObj).parents('.feed-entry-wrapper:first').remove();

		    hide(controlId);
		}
		else { // handle errors
		    HandleDeleteError(msg, controlId);
		    AdviseDeleteDone(controlId); // only do this on error since in other case, the original dialog disappears making this unnecessary
		}
	    }
	});
}

function ShowShareOnSiteDialog(baseDomId, itemId) {
    // lexical scoping for callback
    var baseDomId = baseDomId;
    var itemId = itemId;

    var postData = MakeURLQuery([["baseDomId", baseDomId], ["itemId", itemId]]);

    $.ajax({
            type: "POST",
            url: "/messaging/RenderSharingDialog",
            data: postData,
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + baseDomId + '-share-on-site-dialog').find('.share-on-site-contents:first').html(msg.substr(1));
		    showAlert(baseDomId + '-share-on-site-dialog');
                }
            }
        });
}

function ShareOnSite(containerId, itemId, maxMessageLength, maxMessageLines) {
    // bring into lexical scope
    var containerId = containerId;
    var itemId = itemId;
    var maxMessageLength = maxMessageLength;
    var maxMessageLines = maxMessageLines;

    // get dom objects
    var jqoContainer = $('#' + containerId);
    var jqoSelectionError = jqoContainer.find('.contact-selection-error:first');
    var jqoOtherError = jqoContainer.find('.contact-other-error:first');
    var jqoStatus = jqoContainer.find('.share-status:first');
    var jqoOK = jqoContainer.find('.ok-button:first');
    var jqoCancel = jqoContainer.find('.cancel-button:first');

    // clear errors
    ClearErrorByJqo(jqoSelectionError);
    ClearErrorByJqo(jqoOtherError);
    var error = false;
  
    // validate contact selection
    encryptedUserIds = GetContactSelection(containerId);
    if (encryptedUserIds.length == 0) {
	SetErrorByJqo(jqoSelectionError, 'Please select at least one contact you would like to share this content with.');
	error = true;
    }

    // validate optional message
    var message = trim(jqoContainer.find('.share-message:first').val());

    if (message.length > maxMessageLength) {
	SetErrorByJqo(jqoOtherError, "Your message is too long. Please keep your message to " + maxMessageLength + " characters or less.");
	error = true;
    }
    else if (CountLines(message) > maxMessageLines) {
	SetErrorByJqo(jqoOtherError, "There are too many lines in your message. Please keep your message to " + maxMessageLines + " lines or fewer. Note that a new line is created each time you press \"Enter\".");
	error = true;
    }

    if (error) {
	return;
    }

    // create post data
    var postData = 'receivingUserIds=' + encodeStringArray(encryptedUserIds) + '&message=' + encodeURIComponent(message) + '&itemId=' + encodeURIComponent(itemId);

    // advise of request in UI
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoOK, false);
    ToggleButtonByJqo(jqoCancel, false);

    $.ajax({
            type: "POST",
            url: "/messaging/Share",
            data: postData,
            error: function(request, status, error) {
                HandleContactErrorByJqo('8', jqoError);
           },
	    complete: function(request, status) {
		jqoStatus.css('display', 'none');
		ToggleButtonByJqo(jqoOK, true);
		ToggleButtonByJqo(jqoCancel, true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    hide(containerId);
		    
		    showAlert('sharing-confirmation-dialog');
		    $('#sharing-confirmation-dialog').fadeOut(_dialogFadeOut, function() {
			    return;
			});
                }
		else {
		    HandleContactErrorByJqo(msg, jqoError);
		}
            }
        });

}

function HideExpandedSharingControl(baseDomId) {
    hide(baseDomId + '-expanded-area');
    hide(baseDomId + '-expanded-link');
    showInline(baseDomId + '-expand-link');
}

function ShowExpandedSharingControl(baseDomId, itemId) {
    ClearError(baseDomId + '-other-error');

    showBlock(baseDomId + '-expanded-area');

    if (trim($('#' + baseDomId + '-expanded-area-loaded').html()) != '0') {
	return;
    }

    // lexical scoping for callback
    var baseDomId = baseDomId;
    var itemId = itemId;

    var postData = "baseDomId=" + encodeURIComponent(baseDomId) + "&itemId=" + encodeURIComponent(itemId);

    $.ajax({
            type: "POST",
            url: "/messaging/RenderSharingControl",
            data: postData,
	    error: function(request, status, error) {
                HandleEditError('8', baseDomId + '-other-error');
            },
            success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + baseDomId + '-expanded-area-contents').html(msg.substr(1));

		    $('#' + baseDomId + '-expanded-area-loaded').html('1');
                }
                else {
		    HandleEditError(msg, baseDomId + '-other-error');
                }
            }
        });
}

function Share(baseId, itemId, maxMessageLength, maxMessageLines) {
    // bring into lexical scope
    var baseId = baseId;
    var itemId = itemId;
    var maxMessageLength = maxMessageLength;
    var maxMessageLines = maxMessageLines;

    // clear errors
    ClearError(baseId + '-selection-error');
    ClearError(baseId + '-other-error');
    var error = false;
  
    // validate contact selection
    encryptedUserIds = GetContactSelection(baseId + '-expanded-area-contents');
    if (encryptedUserIds.length == 0) {
	SetError(baseId + '-selection-error', 'Please select at least one contact you would like to share this content with.');
	error = true;
    }

    // validate optional message
    var message = trim($('#' + baseId + '-message').val());

    if (message.length > maxMessageLength) {
	SetError(baseId + '-other-error', "Your message is too long. Please keep your message to " + maxMessageLength + " characters or less.");
	error = true;
    }
    else if (CountLines(message) > maxMessageLines) {
	SetError(baseId + '-other-error', "There are too many lines in your message. Please keep your message to " + maxMessageLines + " lines or fewer. Note that a new line is created each time you press \"Enter\".");
	error = true;
    }

    if (error) {
	return;
    }

    // create post data
    var postData = 'receivingUserIds=' + encodeStringArray(encryptedUserIds) + '&message=' + encodeURIComponent(message) + '&itemId=' + encodeURIComponent(itemId);

    // advise of request in UI
    showBlock(baseId + '-status');
    ToggleButton(baseId + '-ok', false);
    ToggleButton(baseId + '-cancel', false);

    $.ajax({
            type: "POST",
            url: "/messaging/Share",
            data: postData,
            error: function(request, status, error) {
                HandleContactError('8', baseId + '-other-error');
           },
	    complete: function(request, status) {
		hide(baseId + '-status');
		ToggleButton(baseId + '-ok', true);
		ToggleButton(baseId + '-cancel', true);
            },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    HideExpandedSharingControl(baseId);
		    
		    showAlert('sharing-confirmation-dialog');
		    $('#sharing-confirmation-dialog').fadeOut(_dialogFadeOut, function() {
			    return;
			});
                }
		else {
		    HandleContactError(msg, baseId + '-other-error');
		}
            }
        });

}

//*****
//* Settings related scripts
//*****

function EditEmailSettingsInline(dialogId) {
    // lexical scoping
    var dialogId = dialogId;

    // clear previous errors
    ClearError("inline-edit-email-settings-error");

    // form validation
    if ($("[name='inline-edit-email-settings-value']:checked").length == 0) {
	SetError('inline-edit-email-settings-error', 'Please select an option.');
	return;
    }

    var updatesSetting = $("[name='inline-edit-email-settings-value']:checked").val();
    var jsCB = trim($('#inline-edit-email-settings-callback').html());

    var postData = 'updatesSetting=' + encodeURIComponent(updatesSetting);

    // advise of request in UI
    showBlock('inline-edit-email-settings-status');
    ToggleButton('inline-edit-email-settings-save', false);

    $.ajax({
            type: "POST",
            url: "/account/EditSettings",
            data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', 'inline-edit-email-settings-error');
	    },
            complete: function(request, status) {
		hide('inline-edit-email-settings-status');
		ToggleButton('inline-edit-email-settings-save', true);
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    hide(dialogId);

		    if (jsCB.length > 0) {
			eval(jsCB);
		    }
                }
                else { //1st char of msg will indicate failure
		    HandleGenericError(msg, 'inline-edit-email-settings-error');
                }
            }
        });
}

//*****
//* Promotion related scripts
//*****

function PromoteInlineSearchResult(itemId, escapedItemTitle) { // callback from inline post search box
    var postData = MakeURLQuery([['itemId', itemId]]);

    $.ajax({
            type: "POST",
            url: "/favorites/PostFavorite",
	    data: postData,
            success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    PrependBrowseResults(msg.substr(1), 1);
		}
	    }
        });    
}


//*****
//* Syndication related scripts
//*****

function ColorPickerChanged(callingObject) {
    var pattern = new RegExp("#[0-9abcdef]{6,6}");

    color = ColorPickerValue(callingObject);

    if (color != null) {
	$(callingObject).nextAll('.errormsg:first').css('display', 'none');

	$(callingObject).next().css('background', color);
    }
    else {
	$(callingObject).nextAll('.errormsg:first').css('display', 'block');
    }
}

function ColorPickerValue(callingObject) {
    var color = $(callingObject).val();

    if (color.toLowerCase() != 'none') {
	color = '#' + color;
    }

    var pattern = new RegExp("#[0-9abcdef]{6,6}");

    if (color.toLowerCase() == 'none' || pattern.test(color.toLowerCase())) {
	return color.toLowerCase();
    }
    else {
	return null;
    }
}

function PromotionControlPanelChangeBackgroundColor(callingObject, itemId, widgetDomId) {
    var color = ColorPickerValue(callingObject);

    if (color == null) {
	return;
    }

    $('#' + widgetDomId).css('background', color);

    PromotionControlPanelUpdateEmbedCode(itemId);
}

function PromotionControlPanelChangeBorderColor(callingObject, itemId, widgetDomId) {
    var color = ColorPickerValue(callingObject);

    if (color == null) {
	return;
    }

    if (color == 'none') {
	$('#' + widgetDomId).css('border', 'none');
    }
    else {
	$('#' + widgetDomId).css('border', 'solid 1px ' + color);
    }

    PromotionControlPanelUpdateEmbedCode(itemId);
}

function PromotionControlPanelChangeLinkColor(callingObject, itemId, widgetDomId) {
    var color = ColorPickerValue(callingObject);

    if (color == null) {
	return;
    }

    var widgetContainer = $('#' + widgetDomId);

    if (color == 'none') {
	widgetContainer.find('.widget-link').each(function() {
		$(this).css('color', '');
		$(this).css('border-color', '');
	    });
    }
    else {
	widgetContainer.find('.widget-link').each(function() {
		$(this).css('color', color);
		$(this).css('border-color', color);
	    });
    }

    PromotionControlPanelUpdateEmbedCode(itemId);
}

function PromotionControlPanelUpdateEmbedCode(itemId) {
    postData = 'type=PROMOTION_FULL_CONTROL&iids=' + itemId;

    var background = $('#promotion-full-widget-code-background').val();
    if (background != 'none') {
	background = '#' + background;
    }

    var borderColor = $('#promotion-full-widget-code-border-color').val();
    if (borderColor != 'none') {
	borderColor = '#' + borderColor;
    }

    var linkColor = $('#promotion-full-widget-code-link-color').val();
    if (linkColor != 'none') {
	linkColor = '#' + linkColor;
    }

    postData += '&background=' + encodeURIComponent(background);
    postData += '&borderColor=' + encodeURIComponent(borderColor);
    postData += '&linkColor=' + encodeURIComponent(linkColor);

    $.ajax({
            type: "POST",
            url: "/syndication/GetEmbedCode",
            data: postData,
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#promotion-full-widget-code').val(trim(msg.substr(1)));
                }
            }
        });
}

//*****
//* Contest related scripts
//*****

function DisplayLocalContests(city, region, country, latitude, longitude) {
    postData = MakeURLQuery([['city', city], ['region', region], ['country', country], ['latitude', latitude], ['longitude', longitude]]);

    $.ajax({
            type: "POST",
            url: "/contests/GetLocalBanners",
            data: postData,
            success: function(msg) {
                if (msg.charAt(0) == '0' && msg.substr(1).length > 0) { //success
		    $('#contest-banners').append(msg.substr(1));
		    $('#contest-banners').parents('div:first').css('display', 'block');
                }
            }
        });
}

//*****
//* Facebook Connect related scripts
//*****

function InitFacebook() {
    var jqoInit = $("#facebook-async-init");
    if (jqoInit.length > 0)
	jqoInit.click();
}

function FBShowSocialPreview() {
    window.setTimeout('FBShowSocialPreviewHelper();', 2000);
}

function FBShowSocialPreviewHelper() {
    var jqoImage = $('#fb-social-preview-image-container').find('img:first');
    var src = jqoImage.attr('src');
    var tokens = src.split('-');
    var postfix = tokens.pop();
    var index = parseInt(postfix.split('.')[0]);
    var newIndex = 1;
    if (index < 3)
	newIndex = index + 1;
    else
	return;
    tokens.push(newIndex + '.jpg');

    jqoImage.fadeOut(1000, function() {
	    jqoImage.attr('src', tokens.join('-'));
	    jqoImage.fadeIn(1000, function() {
		    window.setTimeout('FBShowSocialPreviewHelper();', 2000);
		});
	});
}

function SeedFBConnectFavorites() {
    postData = MakeURLQuery([['context', 'connect']]);
    $.ajax({
            type: "POST",
		url: "/facebook/SeedUserFavorites",
                data: postData,
            error: function(request, status, error) {
		$('#user-favorites').find('.note:first').html('Unable to pre-fill your favorites based on your Facebook profile. Please manually select recent favorites.');
	    },
            success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    UpdateItemThumbnailSelectionsHTML('user-favorites', msg.substr(1));
		}
		else {
		$('#user-favorites').find('.note:first').html('Unable to pre-fill your favorites based on your Facebook profile. Please manually select recent favorites.');
		}
	    }
        });    
}

function AddFBConnectFavorites(itemId, escapedItemTitle) {
    ClearError("user-favorites-error");

    var existingItemIds = GetItemThumbnailSelections("user-favorites");
    for (var i=0; i < existingItemIds.length; i++) {
	if (existingItemIds[i] == itemId) {
	    SetError("user-favorites-error", "That item is already listed among your favorites");
	    return;
	}
    }
    
    var postData = MakeURLQuery([['itemIds', encodeStringArray([itemId])]]);

    $.ajax({
            type: "POST",
            url: "/facebook/AddUserFavorites",
	    data: postData,
            error: function(request, status, error) {
		HandleGenericError('9', "user-favorites-error");
	    },
            success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    AddItemThumbnailSelectionsHTML('user-favorites', msg.substr(1));
		}
		else {
		    HandleGenericError(msg.charAt(0), "user-favorites-error");
		}
	    }
        });    
}

function RegisterFacebookAccount(caller) {
    var jqoContainer = $(caller).parents('.registration-form:first');

    var fullName = trim(jqoContainer.find('.full-name-input:first').val());
    var location = trim(jqoContainer.find('.location-input:first').val());
    var faveIds = GetItemThumbnailSelections("user-favorites");

    // clear previous errors
    var jqoNameError = jqoContainer.find('.full-name-error:first');
    var jqoLocationError = jqoContainer.find('.location-error:first');
    var jqoFavoritesError = jqoContainer.find('#user-favorites-error:first');
    var jqoOtherError = jqoContainer.find('.other-error:first');
    ClearErrorByJqo(jqoNameError);
    ClearErrorByJqo(jqoLocationError);
    ClearErrorByJqo(jqoFavoritesError);
    ClearErrorByJqo(jqoOtherError);

    // form validation
    var error = false;
    if (ParseFullName(fullName) == null) {
	error = true;
	
	SetErrorByJqo(jqoNameError, 'Please enter a full name of the form "First Last"');
    }
    if (ParseCityState(location) == null) {
	error = true;
	
	SetErrorByJqo(jqoLocationError, 'Please enter a city and state such as "Palo Alto, CA"');
    }
    if (faveIds.length < 5) {
	error = true;
	
	SetErrorByJqo(jqoFavoritesError, 'Please select at least 5 favorites');
    }
    
    if (error) {
	return;
    }

    // prompt for email permission
    FB.Connect.showPermissionDialog('email', function(result) { RegisterFacebookAccountCb(jqoContainer, result, fullName, location, faveIds); });
}

function RegisterFacebookAccountCb(jqoContainer, grantedPermissions, fullName, location, faveIds) {
    var jqoOtherError = jqoContainer.find('.other-error:first');

    // create post data
    var redirect = trim(jqoContainer.find('.redirect-input:first').val());
    var postData = MakeURLQuery([["fullName", fullName], ['location', location], ['redirect', redirect], ['faveIds', JSON.stringify(faveIds)]]);

    // advise of request in UI
    var jqoStatus = jqoContainer.find('.status-message:first');
    var jqoOK = jqoContainer.find('.ok-button:first');
    var jqoCancel = jqoContainer.find('.cancel-button:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoOK, false);
    ToggleButtonByJqo(jqoCancel, false);

    $.ajax({
            type: "POST",
		url: "/facebook/RegisterCb",
		data: postData,
		error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoOtherError);
            },
		complete: function(request, status) {
                jqoStatus.css('display', 'none');
                ToggleButtonByJqo(jqoOK, true);
                ToggleButtonByJqo(jqoCancel, true);
            },
		success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));
		    
                    $('#next-url').val(results['NEXT_URL']);

		    var fbAttachment = results['FB_POST']['ATTACHMENT'];
		    var fbActionLinks = results['FB_POST']['ACTION_LINKS'];
		    FB.Connect.streamPublish('', fbAttachment, fbActionLinks, null, results['FB_POST']['PROMPT'], RegisterFacebookAccountPublishCb);
                }
                else if (msg.charAt(0) == 'E') {
                    SetErrorByJqo(jqoOtherError, 'Please allow SwingVine to send you emails. SwingVine only sends email for important issues such as password resets and security issues and when you opt-in to emails about messages from your friends on SwingVine.');
                }
                else if (msg.charAt(0) == '6') {
                    SetErrorByJqo(jqoOtherError, 'This Facebook account is already registered. Go to the home page and use Facebook Connect to sign in.');
                }
                else {
                    HandleGenericErrorByJqo(msg, jqoOtherError);
                }
            }
        });
}

function RegisterFacebookAccountPublishCb(postId, exception) {
    // track facebook post
    if (postId != null && postId != 'null' && exception == null)
	TrackEvent('Shares', '/FacebookConnect/StreamPublish/List/' + postId);
    else
	TrackEvent('Errors', '/FacebookConnect/StreamPublish/List/' + postId + '/' + exception);

    window.location = $('#next-url').val();

}

function FBConnectPickFavorites(caller) {
    var jqoContainer = $(caller).parents('.form-container:first');

    var faveIds = GetItemThumbnailSelections("user-favorites");

    // clear previous errors
    var jqoFavoritesError = jqoContainer.find('#user-favorites-error:first');
    var jqoOtherError = jqoContainer.find('.other-error:first');
    ClearErrorByJqo(jqoFavoritesError);
    ClearErrorByJqo(jqoOtherError);

    // form validation
    if (faveIds.length < 5) {
	SetErrorByJqo(jqoFavoritesError, 'Please select at least 5 favorites');
	return;
    }

    // create post data
    var redirect = trim(jqoContainer.find('.redirect-input:first').val());
    var postData = MakeURLQuery([['redirect', redirect], ['faveIds', JSON.stringify(faveIds)]]);

    // advise of request in UI
    var jqoStatus = jqoContainer.find('.status-message:first');
    var jqoOK = jqoContainer.find('.ok-button:first');
    var jqoCancel = jqoContainer.find('.cancel-button:first');
    jqoStatus.css('display', 'block');
    ToggleButtonByJqo(jqoOK, false);
    ToggleButtonByJqo(jqoCancel, false);

    $.ajax({
            type: "POST",
		url: "/facebook/SaveFavorites",
		data: postData,
		error: function(request, status, error) {
                HandleGenericErrorByJqo('8', jqoOtherError);
            },
		complete: function(request, status) {
                jqoStatus.css('display', 'none');
                ToggleButtonByJqo(jqoOK, true);
                ToggleButtonByJqo(jqoCancel, true);
            },
	    success: function(msg) {
		var results = JSON.parse(msg);

		if (!results['SUCCESS']) {
                    HandleGenericErrorByJqo(String(results['ERROR_CODE']), jqoOtherError);
		    return;
		}
		window.location = results['TO_URL'];
            }
        });
}

function FBConnectSeedSocialResults() {
    postData = MakeURLQuery([['context', 'connect']]);
    $.ajax({
	    type: "POST",
	    url: "/facebook/GetFriendFavorites",
		data: postData,
            error: function(request, status, error) {
		$('#social-seeding-waiting').remove();
		HandleGenericError('9', 'social-seeding-error');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { // success
		    var results = JSON.parse(msg.substr(1));

		    $('#content').html($('#success-content').html());

		    var fbAttachment = results['FB_POST']['ATTACHMENT'];
		    var fbActionLinks = results['FB_POST']['ACTION_LINKS'];

		    FB.Connect.streamPublish(results['FB_POST']['MESSAGE'], fbAttachment, fbActionLinks, null, results['FB_POST']['PROMPT'], FBConnectPublishSummaryCb);
		}
		else if (msg.charAt(0) == '2') {
		    $('#social-seeding-waiting').remove();
		    SetError('social-seeding-error', 'You did not properly complete the initial Connect setup step. Please go back to the home page and try to Connect again.');
		}
		else {
		    $('#social-seeding-waiting').remove();
		    HandleGenericError(msg.charAt(0), 'social-seeding-error');
		}
	    }
	});
}

function FBConnectPublishSummaryCb(postId, exception) {
    // track post
    if (postId != null && postId != 'null' && exception == null) {
	TrackEvent('Shares', '/FacebookConnect/StreamPublish/Popular/' + postId);
    }
    else {
	TrackEvent('Errors', '/FacebookConnect/StreamPublish/Popular/' + postId + '/' + exception);
    }

    window.location = $('#next-page-url').val();
}

function FBConnectMoreSocialPrompt() {
    var jqoContainer = $('#fb-more-social-prompt-container');
    postData = MakeURLQuery([['query', 'foo']]); // workaround chrome issue with empty xmlhttprequests
    $.ajax({
            type: "POST",
		url: "/facebook/MoreSocialPrompt",
                data: postData,
	    success: function(msg) {
		var results = JSON.parse(msg);

		if (!results['SUCCESS'] || !results['SHOW_PROMPT'])
		    return;
		
		var html = results['PROMPT_HTML'];
		jqoContainer.html(html);
		jqoContainer.css('display', 'block');
		InitFacebook();
            }
        });
}

function FacebookStreamPublishCallback(postId, exception) {
    // track facebook post
    if (postId != null && postId != 'null' && exception == null)
	TrackEvent('Shares', '/FacebookConnect/StreamPublish/Item/' + postId);
    else
	TrackEvent('Errors', '/FacebookConnect/StreamPublish/Item/' + postId + '/' + exception);
}


//*****
//* Account related scripts
//*****

// for use in conjunction with account pages

function ValidateSignUp(uniqueID) {
    var domIdPrefix = 'sign-up-' + uniqueID + '-'; // phase this in

    var email = trim($("#sign-up-email-" + uniqueID).val());

    var fullName = trim($("#" + domIdPrefix + 'fullname').val());
    var location = trim($("#" + domIdPrefix + 'location').val());

    var password = $("#sign-up-password-" + uniqueID).val();
    var verifyPassword = $("#sign-up-verify-password-" + uniqueID).val();

    // clear previous errors
    $("#sign-up-email-error-" + uniqueID).html("");
    $("#sign-up-password-error-" + uniqueID).html("");

    ClearError(domIdPrefix + 'fullname-error');
    ClearError(domIdPrefix + 'location-error');

    $("#sign-up-other-error-" + uniqueID).html("");
    hide("sign-up-email-error-" + uniqueID);
    hide("sign-up-password-error-" + uniqueID);
    hide("sign-up-other-error-" + uniqueID);

    // form validation
    var error = false;
    if (email.length == 0) {
	error = true;
	
	$("#sign-up-email-error-" + uniqueID).html("Please enter your email address.");
	showBlock("sign-up-email-error-" + uniqueID);
    }
    else if (!ValidateEmail(email)) {
	error = true;
	
	$("#sign-up-email-error-" + uniqueID).html("Please enter a properly formed email address.");
	showBlock("sign-up-email-error-" + uniqueID);
    }

    if (ParseFullName(fullName) == null) {
	error = true;
	
	SetError(domIdPrefix + 'fullname-error', 'Please enter a full name of the form "First Last"');
    }
    if (ParseCityState(location) == null) {
	error = true;
	
	SetError(domIdPrefix + 'location-error', 'Please enter a city and state such as "Palo Alto, CA"');
    }
    
    passwordError = CheckPasswordError(password);
    if (password != verifyPassword) {
	error = true;
	
	$("#sign-up-password-error-" + uniqueID).html("Please enter a password and confirm it exactly.");
	showBlock("sign-up-password-error-" + uniqueID);
    }
    else if (passwordError.length > 0) {
	error = true;
	
	$("#sign-up-password-error-" + uniqueID).html(passwordError);
	showBlock("sign-up-password-error-" + uniqueID);
    }

    if (error) {
	return !error;
    }

    // check captcha last since it only has values if the user filled the rest
    var captchaChallenge = trim($("#captcha-" + uniqueID + " #recaptcha_challenge_field").val());
    var captchaResponse = trim($("#captcha-" + uniqueID + " #recaptcha_response_field").val());

    if (captchaChallenge.length == 0) {
	error = true;
	
	$("#sign-up-other-error-" + uniqueID).html("There's something wrong with this page. Please try reloading the page.");
	showBlock("sign-up-other-error-" + uniqueID);
    }
    else if (captchaResponse.length == 0) {
	error = true;
	
	$("#sign-up-other-error-" + uniqueID).html("Please transcribe the words / numbers displayed above before proceeding.");
	showBlock("sign-up-other-error-" + uniqueID);
    }

    return !error;
}

function ValidateInviteUse(baseId) {
    var domIdPrefix = '';
    if (baseId != null) {
	domIdPrefix = baseId + '-';
    }

    var email = trim($('#' + domIdPrefix + 'email').val());
    var firstname = trim($("#" + domIdPrefix + "firstname").val());
    var lastname = trim($("#" + domIdPrefix + "lastname").val());
    var city = trim($("#" + domIdPrefix + "city").val());
    var state = trim($("#" + domIdPrefix + "state").val());
    var password = $("#" + domIdPrefix + "password").val();
    var verifyPassword = $("#" + domIdPrefix + "verify-password").val();

    // clear previous errors
    ClearError(domIdPrefix + "email-error");
    ClearError(domIdPrefix + "first-name-error");
    ClearError(domIdPrefix + "last-name-error");
    ClearError(domIdPrefix + "city-error");
    ClearError(domIdPrefix + "state-error");
    ClearError(domIdPrefix + "password-error");
    ClearError(domIdPrefix + "other-error");

    // form validation
    var error = false;

    if (email.length == 0) {
	error = true;
	
	SetError(domIdPrefix + "email-error", "Please enter your email address.");
    }
    else if (!ValidateEmail(email)) {
	error = true;
	
	SetError(domIdPrefix + "email-error", "Please enter a properly formed email address.");
    }
    
    if (firstname.length == 0) {
	error = true;
	
	SetError(domIdPrefix + "first-name-error", "Please enter your first name.");
    }
    if (lastname.length == 0) {
	error = true;
	
	SetError(domIdPrefix + "last-name-error", "Please enter your last name.");
    }

    if (city.length == 0) {
	error = true;
	
	SetError(domIdPrefix + "city-error", "Please enter your city.");
    }
    if (state.length != 2) {
	error = true;
	
	SetError(domIdPrefix + "state-error", "Please use a two letter state abbreviation.");
    }

    passwordError = CheckPasswordError(password);
    if (password != verifyPassword) {
	error = true;
	
	SetError(domIdPrefix + "password-error", "Please enter a password and confirm it exactly.");
    }
    else if (passwordError.length > 0) {
	error = true;
	
	SetError(domIdPrefix + "password-error", passwordError);
    }

    if (error) {
	return !error;
    }
}

function StartPasswordReset(randomID) {
    var email = trim($("#reminder-email-address-" + randomID).val());

    // clear previous errors
    $("#reminder-email-error-" + randomID).html("");
    hide("reminder-email-error-" + randomID);

    // form validation
    var error = false;
    if (email.length == 0) {
	error = true;
	
	$("#reminder-email-error-" + randomID).html("Please enter your email address.");
	showBlock("reminder-email-error-" + randomID);
    }
    else if (!ValidateEmail(email)) {
	error = true;
	
	$("#reminder-email-error-" + randomID).html("Please enter a properly formed email address.");
	showBlock("reminder-email-error-" + randomID);
    }
    
    if (error) {
	return false;
    }

    var formdata = "email=" + encodeURIComponent(email);

    // advise of request in UI
    showBlock('forgot-password-status-' + randomID);
    ToggleButton('forgot-password-ok-' + randomID, false);
    ToggleButton('forgot-password-cancel-' + randomID, false);
    
    $.ajax({
	    type: "POST",
		url: "/account.py/startpasswordreset",
		data: formdata,
                error: function(request, status, error) {
		  HandleEditError('8', 'remind-email-error-' + randomID);
	        },
            complete: function(request, status) {
		hide('forgot-password-status-' + randomID);
		ToggleButton('forgot-password-ok-' + randomID, true);
		ToggleButton('forgot-password-cancel-' + randomID, true);
	    },
		success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    hide('forgot-password-action-' + randomID);
		    showBlock('sign-in-' + randomID); 
		    $("#forgot-password-msg-" + randomID).html("OK. We've sent a password reset email to <strong>" + email + "</strong>.");
		    showBlock('forgot-password-msg-' + randomID); 
		}
		else if (msg.charAt(0) == '4') {
		    SetError("reminder-email-error-" + randomID, "We don't have the specified email address on record. Please double-check the address you input.");
		}
		else { //1st char of msg will indicate failure
		    HandleGenericError(msg, "reminder-email-error-" + randomID);
		}
	    }
	});
}

function ValidateResetPassword() {
    var email = trim($("[name='email']").val());
    var key = trim($("[name='key']").val());
    var rememberMe = trim($("[name='rememberme']").val());
    var password = $("[name='password']").val();
    var verifyPassword = $("[name='verify-password']").val();

    // clear previous errors
    $("#password-error").html("");
    $("#other-error").html("");
    hide("password-error");
    hide("other-error");

    // form validation
    var error = false;
    passwordError = CheckPasswordError(password);
    if (password != verifyPassword) {
	error = true;
	
	$("#password-error").html("Please enter a password and confirm it exactly.");
	showBlock("password-error");
    }
    else if (passwordError.length > 0) {
	error = true;
	
	$("#password-error").html(passwordError);
	showBlock("password-error");
    }

    // no need to check email or key since these are hidden fields

    if (error) {
	return false;
    }
    else {
	return true;
    }

}

function RegisterDifferentEmail() {
    var email = trim($("#register-different-email-address").val());
    var key = trim($('#register-different-email-key').val());

    // clear previous errors
    ClearError("register-different-email-error");

    // form validation
    var error = false;
    if (email.length == 0) {
	error = true;
	
	SetError("register-different-email-error", "Please enter your email address.");
    }
    else if (!ValidateEmail(email)) {
	error = true;
	
	SetError("register-different-email-error", "Please enter a properly formed email address.");
    }
    
    if (error) {
	return false;
    }

    var postData = "email=" + encodeURIComponent(email) + '&key=' + encodeURIComponent(key);

    // advise of request in UI
    showBlock('register-different-email-status');
    ToggleButton('register-different-email-ok', false);
    ToggleButton('register-different-email-cancel', false);
    
    $.ajax({
	    type: "POST",
		url: "/account/RegisterDifferentEmail",
		data: postData,
            error: function(request, status, error) {
		HandleEditError('8', 'register-different-email-error');
		hide('register-different-email-status');
		ToggleButton('register-different-email-ok', true);
		ToggleButton('register-different-email-cancel', true);
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    $("#register-different-email-area").html("OK. We sent a confirmation email to <strong>" + email + "</strong>. Click on the link in the email to complete your sign up. You may need to check your spam folder for the confirmation email.");
		}
		else {
		    if (msg.charAt(0) == '1') {
			SetError("register-different-email-error", "Invalid request. Please make sure that you clicked or copy and pasted the exact link given for creating your account.");
		    }
		    else if (msg.charAt(0) == '4') {
			SetError("register-different-email-error", "Invalid request. Please make sure that you clicked or copy and pasted the exact link given for creating your account. In some cases, the link may have expired and you may need to restart the account registration process.");
		    }
		    else if (msg.charAt(0) == '6') {
			SetError("register-different-email-error", "This email address is already registered. If you already have an account, sign in using your existing account in the area on the left.");
		    }
		    else { //1st char of msg will indicate failure
			HandleGenericError(msg, "register-differetn-email-error");
		    }
		    
		    hide('register-different-email-status');
		    ToggleButton('register-different-email-ok', true);
		    ToggleButton('register-different-email-cancel', true);
		}
	    }
	});
}

function EditProfile(dialogId) {
    // lexical scoping
    var dialogId = dialogId;

    var firstName = trim($('#edit-profile-first-name').val());
    var lastName = trim($('#edit-profile-last-name').val());

    var city = trim($('#edit-profile-city').val());
    var state = trim($('#edit-profile-state').val());

    var personalURL = trim($('#edit-profile-personal-url').val());


    // Clear prior errors
    ClearError('edit-profile-error');
    ClearError('edit-profile-first-name-error');
    ClearError('edit-profile-last-name-error');
    ClearError('edit-profile-city-error');
    ClearError('edit-profile-state-error');
    ClearError('edit-profile-personal-url-error');

    // Validate data
    error = false;

    if (firstName.length == 0) {
        error = true;

        SetError("edit-profile-first-name-error", "Please enter your first name.");
    }
    if (lastName.length == 0) {
        error = true;

        SetError("edit-profile-last-name-error", "Please enter your last name.");
    }

    if (city.length == 0) {
        error = true;

        SetError("edit-profile-city-error", "Please enter your city.");
    }
    if (state.length != 2) {
        error = true;

        SetError("edit-profile-state-error", "Please enter a two letter state abbreviation.");
    }

    if (personalURL.length > 0 && !ValidateURL(personalURL)) {
	error = true;

	SetError("edit-profile-personal-url-error", "Please input a URL that includes http:// and is in the same format as you would use to open the page in your browser.");
    }

    if (error) {
	return;
    }

    var postData = "firstName=" + encodeURIComponent(firstName) + "&lastName=" + encodeURIComponent(lastName) + "&city=" + encodeURIComponent(city) + "&state=" + encodeURIComponent(state) + "&personalURL=" + encodeURIComponent(personalURL);

    // advise of request in UI
    showBlock('edit-profile-status');
    ToggleButton('edit-profile-ok', false);
    ToggleButton('edit-profile-cancel', false);
    
    $.ajax({
            type: "POST",
            url: "/account/UpdateProfile",
            data: postData,
	    error: function(request, status, error) {
		HandleEditError('8', 'edit-profile-error');
	    },
	    complete: function(request, status) {
		hide('edit-profile-status');
		ToggleButton('edit-profile-ok', true);
		ToggleButton('edit-profile-cancel', true);
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    window.location = window.location;
                }
		else if (msg.charAt(0) == '5') {
		    SetError('edit-profile-error', "One or more of the fields you're trying to save is too long. Please shorten your entries and try again.");
		}
                else {
                    HandleEditError(msg, 'edit-profile-error');
                }
            }
        });

}

function CheckPasswordError(password) {
    if (password.length == 0) { // check password length
	return "Please enter a non-blank password.";
    }
    else if (password.length < 6) { // enforce minimal password strength
	return "Please provide a password that is 6 characters.";
    }
    else if (password.length > 50) { // enforce maximum password length
	return "Please provide a password that is 50 characters or less.";
    }

    return '';
}
    
function ValidateChangeEmail() {
    var password = trim($("[name='password']").val());
    var newEmail = trim($("[name='newemail']").val());
    var verifyEmail = trim($("[name='verifyemail']").val());

    // clear previous errors
    $("#change-email-error").html("");
    $("#change-email-password-error").html("");
    $("#change-email-email-error").html("");
    hide("change-email-error");
    hide("change-email-password-error");
    hide("change-email-email-error");

    // form validation
    var error = false;
    passwordError = CheckPasswordError(password);
    if (passwordError.length != 0) {
	error = true;
	
	$("#change-email-password-error").html(passwordError);
	showBlock("change-email-password-error");
    }

    if (newEmail.length == 0 || newEmail != verifyEmail) { // check email length and matching
	error = true;
	
	$("#change-email-email-error").html("Please enter your new email address twice and make sure that both entries match.");
	showBlock("change-email-email-error");
    }
    else if (!ValidateEmail(newEmail)) {
	error = true;
	
	$("#change-email-email-error").html("Please enter a properly formed email address.");
	showBlock("change-email-email-error");
    }

    if (error) {
	return false;
    }
    else {
	return true;
    }
}

function ValidateChangePassword() {
    var password = trim($("[name='password']").val());
    var newPassword = trim($("[name='newpassword']").val());
    var verifyPassword = trim($("[name='verifypassword']").val());

    // clear previous errors
    $("#change-password-error").html("");
    $("#change-password-old-password-error").html("");
    $("#change-password-new-password-error").html("");
    hide("change-password-error");
    hide("change-password-old-password-error");
    hide("change-password-new-password-error");

    // form validation
    var error = false;
    passwordError = CheckPasswordError(password);
    if (passwordError.length != 0) {
	error = true;
	
	$("#change-password-old-password-error").html(passwordError);
	showBlock("change-password-old-password-error");
    }
    
    newPasswordError = CheckPasswordError(newPassword);
    if (newPassword != verifyPassword) { // check new password length and matching
	error = true;
	
	$("#change-password-new-password-error").html("Please enter your new password twice and make sure that both entries match.");
	showBlock("change-password-new-password-error");
    }
    else if (newPasswordError.length > 0) {
	error = true;
	
	$("#change-password-new-password-error").html(newPasswordError);
	showBlock("change-password-new-password-error");
    }

    if (error) {
	return false;
    }
    else {
	return true;
    }
}

function ValidateDeactivateAccount() {
    var password = trim($("[name='password']").val());
    var reason = trim($("[name='reason']").val());

    // clear previous errors
    $("#deactivate-error").html("");
    hide("deactivate-error");
    $("#deactivate-password-error").html("");
    hide("deactivate-password-error");

    // form validation
    var error = false;
    passwordError = CheckPasswordError(password);
    if (passwordError.length > 0) { // check password length
	error = true;
	
	$("#deactivate-password-error").html(passwordError);
	showBlock("deactivate-password-error");
    }
    
    // to do: store reason somewhere

    if (error) {
	return false;
    }
    else {
	return true;
    }
}

function EditEmailSettings(dialogId) {
    // lexical scoping
    var dialogId = dialogId;

    // clear previous errors
    ClearError("edit-email-settings-error");

    // form validation
    if ($("[name='update-notifications-value']:checked").length == 0 || $("[name='message-notifications-value']:checked").length == 0 || $("[name='comment-notifications-value']:checked").length == 0 || $("[name='system-notifications-value']:checked").length == 0) {
	SetError('edit-email-settings-error', 'Please select an option for each email type.');
	return;
    }

    var updatesSetting = $("[name='update-notifications-value']:checked").val();
    var messagesSetting = $("[name='message-notifications-value']:checked").val();
    var commentsSetting = $("[name='comment-notifications-value']:checked").val();
    var systemSetting = $("[name='system-notifications-value']:checked").val();
    
    var postData = 'updatesSetting=' + encodeURIComponent(updatesSetting) + '&messagesSetting=' + encodeURIComponent(messagesSetting) + '&commentsSetting=' + encodeURIComponent(commentsSetting) + '&systemSetting=' + encodeURIComponent(systemSetting);

    // advise of request in UI
    showBlock('edit-email-settings-status');
    ToggleButton('edit-email-settings-save', false);

    $.ajax({
            type: "POST",
            url: "/account/EditSettings",
            data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', 'edit-email-settings-error');
	    },
            complete: function(request, status) {
		hide('edit-email-settings-status');
		ToggleButton('edit-email-settings-save', true);
	    },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    showAlert(dialogId);

		    $('#' + dialogId).fadeOut(_dialogFadeOut, function() {
			    return;
			});
                }
                else { //1st char of msg will indicate failure
		    HandleGenericError(msg, 'edit-email-settings-error');
                }
            }
        });
}


function RequestAccount() {
    var email = $("#request-account-email").val();

    // clear previous errors
    ClearError("request-account-error");

    // form validation
    if (email.length == 0) { // check email length and matching
	SetError('request-account-error', "Please enter an email address to request an account.");
	return;
    }
    else if (!ValidateEmail(email)) {
	SetError('request-account-error', 'Please enter a properly formed email address.');
	return
    }

    var formdata = "email=" + encodeURIComponent(email);

    // advise of request in UI
    showBlock('request-account-status');
    ToggleButton('request-account-ok', false);
    ToggleButton('request-account-cancel', false);

    $.ajax({
            type: "POST",
                url: "/account.py/request",
                data: formdata,
                error: function(request, status, error) {
		HandleGenericError('8', 'request-account-error');
	    },
            complete: function(request, status) {
		hide('request-account-status');
		ToggleButton('request-account-ok', true);
		ToggleButton('request-account-cancel', true);
	    },
                success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    // update email on page 
		    hide("request-account-action");

		    $("#request-account-note").html("Thanks for your request.  We'll email you at <strong>" + email + "</strong> as soon as we have an account ready for you.");
		    showBlock('request-account-complete');
                }
                else { //1st char of msg will indicate failure
		    if (msg.charAt(0) == '4') {
			SetError('request-account-error', 'The provided email is already in use. You may have already requested or recieved an account for this address.  If not, please check that the email address is spelled correctly.');
		    }
		    else {
			SetError('request-account-error', 'Unexpected error. Please try again.');
		    }
                }
            }
        });

}

//*****
//* Profile image related scripts
//*****

// this function is the callback for deleting an image
function DeleteProfileImage(controlID, imageID, userID) {
    // lexical scoping for CB
    var controlID = controlID;
    var imageID = imageID;
    var userID = userID;

    var postData = "imageID=" + imageID + "&userID=" + userID;
	
    // clear previous errors
    ClearDeleteErrors(controlID);

    // disable buttons and give status
    AdviseDeleteStart(controlID);

    $.ajax({
	    type: "POST",
	    url: "/profile.py/DeleteImage",
	    data: postData,
            error: function(request, status, error) {
		HandleDeleteError('8', controlID);
		AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') { //success
		    // refresh the page using a server provided url
		    window.location = msg.substr(1);
		}
		else { // handle errors
		    HandleDeleteError(msg, controlID);
		    AdviseDeleteDone(controlID); // only do this on error since in other case, the original dialog disappears making this unnecessary
		}
	    }
	});
}

function SendInvites() {
    // clear previous errors and grab data
    ClearError("invite-other-error");
    ClearError("invite-email-subject-error");
    ClearError("invite-email-body-error");

    var error = false;

    var subject = trim($('#invite-email-subject').val());
    var greeting = $('#invite-email-greeting').val();
    var body = trim($('#invite-email-body').val());

    var recipientNames = new Array();
    var recipientEmails = new Array();
    var followRecipients = new Array();

    $('.invite-row').each(function() {
	    $(this).find('.invite-name-error:first').html('&nbsp;');
	    $(this).find('.invite-name-error:first').css('display', 'none');

	    $(this).find('.invite-email-error:first').html('&nbsp;');
	    $(this).find('.invite-email-error:first').css('display', 'none');

	    var recipientName = trim($(this).find('.invite-name:first').val());
	    var recipientEmail = trim($(this).find('.invite-email:first').val());
	    if (recipientName.length > 0) {
		recipientNames.push(recipientName);
		recipientEmails.push(recipientEmail);

		var checked = false;
		if ($(this).find('.invite-follow:first').attr('checked')) {
		    checked = true;
		}
		followRecipients.push(checked);

		if (recipientName.length == 0) {
		    error = true;

		    $(this).find('.invite-name-error:first').html('Please enter a name for the invite recipient.');
		    $(this).find('.invite-name-error:first').css('display', 'block');
		}
	
		if (!ValidateEmail(recipientEmail)) {
		    error = true;

		    $(this).find('.invite-email-error:first').html('Please enter a valid email address.');
		    $(this).find('.invite-email-error:first').css('display', 'block');
		}
	    }

	});

    // form validation
    if (subject.length == 0) {
	error = true;
	
	SetError("invite-email-subject-error", "Please enter a subject for the invite email.");
    }

    if (body.length == 0) {
	error = true;
	
	SetError("invite-email-body-error", "Please enter a body for the invite email.");
    }

    if (recipientNames.length == 0) {
	error = true;
	
	SetError("invite-other-error", "Please specify the recipient names and email addresses for the invites.");
    }
    
    if (error) {
	return;
    }

    // prep request
    var postData = 'subject=' + encodeURIComponent(subject) + '&greeting=' + greeting + '&body=' + encodeURIComponent(body) + '&recipientNames=' + encodeStringArray(recipientNames) + '&recipientEmails=' + encodeStringArray(recipientEmails) + '&followRecipients=' + encodeBooleanArray(followRecipients) ;

    showBlock('invite-status');
    ToggleButton('send-invites', false);

    $.ajax({
            type: "POST",
                url: "/contacts/do/SendInvites",
                data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', 'invite-other-error');
	        },
                complete: function(request, status) {
		  hide('invite-status');
		  ToggleButton('send-invites', true);
	        },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    var results = JSON.parse(msg.substr(1));

		    var deliveredCount = results['DELIVERED_COUNT'];

		    if (deliveredCount == recipientNames.length) {
			hide('invites-sent-partial-notice');

			//clear form
			$('.invite-row').each(function() {
				$(this).find('.invite-name:first').val('');
				$(this).find('.invite-email:first').val('');
				$(this).find('.invite-follow:first').attr('checked', 'checked');
			    });
		    }
		    else {
			if (deliveredCount > 0) {
			    showInline('invites-sent-partial-notice');
			}

			var deliveryResults = results['RESULTS'];

			var inviteRows = $('.invite-row');
			for (var i=0; i < deliveryResults.length; i++) {
			    if (!deliveryResults[i]['SUCCESS']) {
				if (deliveryResults[i]['ERROR_CODE'] == 'EMAIL_ADDRESS_EXISTS') {
				    inviteRows.eq(i).find('.invite-email-error:first').html('This email account already has an open invitation.');
				    inviteRows.eq(i).find('.invite-email-error:first').css('display', 'block');
				}
				else {
				    inviteRows.eq(i).find('.invite-email-error:first').html('There was an unexpected error delivering email to this recipient. Please verify the email address and try again later.');
				    inviteRows.eq(i).find('.invite-email-error:first').css('display', 'block');
				}
			    }
			}
		    }

		    if (deliveredCount > 0) {
			if (results['PROMPT_EMAIL_SETTINGS']) {
			    showAlert('prompt-email-settings-on-invites');
			}
			else {
			    showAlert('invites-sent');
			}
		    }
                }
		else if (msg.charAt(0) == '2') {
		    SetError("invite-other-error", "You've exceeded the maximum number of allowed email invites within a 24 hour period. This limit prevents spam-related activities, but you can send more invites after waiting a day. Check your reputation page for more info about your permissions.");
		}
                else { // error
                    HandleGenericError(msg, "invite-other-error");
                }
            }
        });
}

function PageInviteHistory(offset) {
    // clear previous error
    ClearError("invite-history-paging-error");

    // prep request
    var postData = 'offset=' + offset;

    $.ajax({
            type: "POST",
                url: "/contacts/do/PageInviteHistory",
                data: postData,
                error: function(request, status, error) {
		  HandleGenericError('8', 'invite-history-paging-error');
	        },
            success: function(msg) {
                if (msg.charAt(0) == '0') { //success
		    $('#invite-history').html(msg.substr(1));
                }
                else { // error
		    HandleGenericError(msg, 'invite-history-paging-error');
                }
            }
        });
}

function SearchContacts(baseID, offset) {
    // lexical scoping for ajax CB
    var baseID = baseID;
    var offset = offset;

    ClearError(baseID + '-error');

    var query = trim($('#' + baseID + '-query').val());

    if (query.length == 0) {
	SetError(baseID + '-error', 'Please type in a search first.');
	
	return;
    }

    var postData = "baseID=" + encodeURIComponent(baseID) + "&query=" + encodeURIComponent(query);

    $('#' + baseID + '-results').html("<span class='note'>Searching...</span>");

    $.ajax({
	    type: "POST",
	    url: "/contacts/do/Search",
	    data: postData,
            error: function(request, status, error) {
		HandleGenericError('8', baseID + '-error');
		$('#' + baseID + '-results').html('');
	    },
	    success: function(msg) {
		if (msg.charAt(0) == '0') {
		    $('#' + baseID + '-results').html(msg.substr(1));
		    InitFacebook();
		}
		else {
		    HandleGenericError(msg, baseID + '-error');
		    $('#' + baseID + '-results').html('');
		}
	    }
        });
    
}

//*****
//* Contact import related scripts
//*****

function ClearInviteForm() {
    $('.invite-name').each(function() {
	    $(this).val('');
	});
    $('.invite-email').each(function() {
	    $(this).val('');
	});
    $('.invite-follow').each(function() {
	    $(this).attr('checked', 'checked');
	});
}

function BulkToggleContactsToImport(callingObject) {
    var targetState = null;

    if ($(callingObject).attr('checked')) {
	targetState = 'checked';
    }
    else {
	targetState = '';
    }

    $('.contact-to-import').each(function() {
	    $(this).attr('checked', targetState);
	});
}

function RelayContactsToImport() {
    // could take a while
    showBlock('import-contacts-status');
    ToggleButton('import-contacts-ok', false);
    ToggleButton('import-contacts-cancel', false);

    var HandleRelayedContacts = opener.HandleRelayedContacts;

    var emails = new Array();
    var names = new Array();
    var name = null;
    $('.contact-to-import').each(function() {
	    // skip unchecked contacts
	    if ($(this).attr('checked')) {
		emails.push($(this).val());

		name = $(this).siblings('.contact-to-import-name').eq(0).html();
		if (name.length > 0) {
		    names.push(name);
		}
		else {
		    names.push(null);
		}
	    }
	});
    HandleRelayedContacts(names, emails);

    ToggleButton('import-contacts-ok', true);
    ToggleButton('import-contacts-cancel', true);

    window.close();
    opener.focus();
}

function NewInviteRowCheck(callingObject) {
    jqoCaller = $(callingObject);

    // new row unneeded if this row is blank
    if (trim(jqoCaller.val()).length == 0) {
	return;
    }

    // new row unneeded if this row isn't last row
    if (jqoCaller.parents('.invite-row:first').next().attr('id') != 'invite-row-template') {
	return;
    }

    NewInviteRows(false, 1);
}

function NewInviteRows(prepend, count) {
    // copy template row and append or prepend
    for (var i=0; i < count; i++) {
	var newRow = $("#invite-row-template").clone(true);

	if (prepend) {
	    newRow.insertAfter("#invite-table-header");
	}
	else {
	    newRow.insertAfter(".invite-row:last");
	}

	newRow.css("display", '');

	// remove the old id and reclass the element
	newRow.attr("id", "");
	newRow.attr("class", "invite-row");
    }
}

function DeleteInvite(callingObject) {
    jqoCaller = $(callingObject);

    if ($('.invite-row').length == 1) { // this is the only row left
	return;
    }

    jqoCaller.parents('.invite-row:first').remove();
}

function HandleRelayedContacts(names, emails) {
    var newRow = null;
    var name = null;

    NewInviteRows(true, names.length);

    var i = 0;
    $('.invite-name').each(function() {
	    if (i >= names.length) {
		return;
	    }

	    name = 'Friend';
	    if (names[i] != null && names[i].length > 0) {
		name = names[i];
	    }

	    $(this).val(name);
	    i++;
	});

    i = 0;
    $('.invite-email').each(function() {
	    if (i >= names.length) {
		return;
	    }

	    $(this).val(emails[i]);
	    i++;
	});
}

function RunGalleryPromotionSlideshow() {
    window.setTimeout('UpdateGalleryPromotionSlideshow(0);', 5000);
}

function UpdateGalleryPromotionSlideshow(updates) {
    if (updates > 3)
	return;

    var jqoImage = $('#gallery-promotion-image');
    var file = jqoImage.attr('src');
    var newFile = null;
    var newHeight = null;
    var fileIndex = file.replace('/images/gallery-promotion-', '').replace('.png', '');
    jqoImage.fadeOut(1000, function() {
	    if (fileIndex == '1-a') {
		newFile = '/images/gallery-promotion-2-a.png';
		newHeight = 310;
	    }
	    else if (fileIndex == '2-a') {
		newFile = '/images/gallery-promotion-1-a.png';
		newHeight = 469;
	    }

	    jqoImage.attr('src', newFile);
	    jqoImage.css('height', newHeight + 'px');
	    jqoImage.fadeIn(200, function() {
		    window.setTimeout('UpdateGalleryPromotionSlideshow(' + (updates+1) + ');', 6000);
		});
	});
}

//*****
//* JSON related scripts
//*****

/*
    http://www.JSON.org/json2.js
    2008-07-15

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array, then it will be used to
            select the members to be serialized. It filters the results such
            that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*global JSON */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", call,
    charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes,
    getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length,
    parse, propertyIsEnumerable, prototype, push, replace, slice, stringify,
    test, toJSON, toString
*/

if (!this.JSON) {

    // Create a JSON object only if one does not already exist. We create the
    // object in a closure to avoid creating global variables.

    JSON = function () {

        function f(n) {
            // Format integers to have at least two digits.
            return n < 10 ? '0' + n : n;
        }

        Date.prototype.toJSON = function (key) {

            return this.getUTCFullYear()   + '-' +
	    f(this.getUTCMonth() + 1) + '-' +
	    f(this.getUTCDate())      + 'T' +
	    f(this.getUTCHours())     + ':' +
	    f(this.getUTCMinutes())   + ':' +
	    f(this.getUTCSeconds())   + 'Z';
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };

        var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
	escapeable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
	gap,
	indent,
	meta = {    // table of character substitutions
	    '\b': '\\b',
	    '\t': '\\t',
	    '\n': '\\n',
	    '\f': '\\f',
	    '\r': '\\r',
	    '"' : '\\"',
	    '\\': '\\\\'
	},
	rep;


        function quote(string) {

	    // If the string contains no control characters, no quote characters, and no
	    // backslash characters, then we can safely slap some quotes around it.
	    // Otherwise we must also replace the offending characters with safe escape
	    // sequences.

            escapeable.lastIndex = 0;
            return escapeable.test(string) ?
                '"' + string.replace(escapeable, function (a) {
			var c = meta[a];
			if (typeof c === 'string') {
			    return c;
			}
			return '\\u' + ('0000' +
					(+(a.charCodeAt(0))).toString(16)).slice(-4);
		    }) + '"' :
                '"' + string + '"';
        }


        function str(key, holder) {

	    // Produce a string from holder[key].

            var i,          // The loop counter.
	    k,          // The member key.
	    v,          // The member value.
	    length,
	    mind = gap,
	    partial,
	    value = holder[key];

	    // If the value has a toJSON method, call it to obtain a replacement value.

            if (value && typeof value === 'object' &&
		typeof value.toJSON === 'function') {
                value = value.toJSON(key);
            }

	    // If we were called with a replacer function, then call the replacer to
	    // obtain a replacement value.

            if (typeof rep === 'function') {
                value = rep.call(holder, key, value);
            }

	    // What happens next depends on the value's type.

            switch (typeof value) {
            case 'string':
	    return quote(value);

            case 'number':

	    // JSON numbers must be finite. Encode non-finite numbers as null.

	    return isFinite(value) ? String(value) : 'null';

            case 'boolean':
            case 'null':

	    // If the value is a boolean or null, convert it to a string. Note:
	    // typeof null does not produce 'null'. The case is included here in
	    // the remote chance that this gets fixed someday.

	    return String(value);

	    // If the type is 'object', we might be dealing with an object or an array or
	    // null.

            case 'object':

	    // Due to a specification blunder in ECMAScript, typeof null is 'object',
	    // so watch out for that case.

	    if (!value) {
		return 'null';
	    }

	    // Make an array to hold the partial results of stringifying this object value.

	    gap += indent;
	    partial = [];

	    // If the object has a dontEnum length property, we'll treat it as an array.

	    if (typeof value.length === 'number' &&
		!(value.propertyIsEnumerable('length'))) {

		// The object is an array. Stringify every element. Use null as a placeholder
		// for non-JSON values.

		length = value.length;
		for (i = 0; i < length; i += 1) {
		    partial[i] = str(i, value) || 'null';
		}

		// Join all of the elements together, separated with commas, and wrap them in
		// brackets.

		v = partial.length === 0 ? '[]' :
                        gap ? '[\n' + gap +
		    partial.join(',\n' + gap) + '\n' +
		    mind + ']' :
		    '[' + partial.join(',') + ']';
		gap = mind;
		return v;
	    }

	    // If the replacer is an array, use it to select the members to be stringified.

	    if (rep && typeof rep === 'object') {
		length = rep.length;
		for (i = 0; i < length; i += 1) {
		    k = rep[i];
		    if (typeof k === 'string') {
			v = str(k, value);
			if (v) {
			    partial.push(quote(k) + (gap ? ': ' : ':') + v);
			}
		    }
		}
	    } else {

		// Otherwise, iterate through all of the keys in the object.

		for (k in value) {
		    if (Object.hasOwnProperty.call(value, k)) {
			v = str(k, value);
			if (v) {
			    partial.push(quote(k) + (gap ? ': ' : ':') + v);
			}
		    }
		}
	    }

	    // Join all of the member texts together, separated with commas,
	    // and wrap them in braces.

	    v = partial.length === 0 ? '{}' :
	    gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
	    mind + '}' : '{' + partial.join(',') + '}';
	    gap = mind;
	    return v;
            }
        }

	// Return the JSON object containing the stringify and parse methods.

        return {
            stringify: function (value, replacer, space) {

		// The stringify method takes a value and an optional replacer, and an optional
		// space parameter, and returns a JSON text. The replacer can be a function
		// that can replace values, or an array of strings that will select the keys.
		// A default replacer method can be provided. Use of the space parameter can
		// produce text that is more easily readable.

                var i;
                gap = '';
                indent = '';

		// If the space parameter is a number, make an indent string containing that
		// many spaces.

                if (typeof space === 'number') {
                    for (i = 0; i < space; i += 1) {
                        indent += ' ';
                    }

		    // If the space parameter is a string, it will be used as the indent string.

                } else if (typeof space === 'string') {
                    indent = space;
                }

		// If there is a replacer, it must be a function or an array.
		// Otherwise, throw an error.

                rep = replacer;
                if (replacer && typeof replacer !== 'function' &&
		    (typeof replacer !== 'object' ||
		     typeof replacer.length !== 'number')) {
                    throw new Error('JSON.stringify');
                }

		// Make a fake root object containing our value under the key of ''.
		// Return the result of stringifying the value.

                return str('', {'': value});
            },


            parse: function (text, reviver) {

		// The parse method takes a text and an optional reviver function, and returns
		// a JavaScript value if the text is a valid JSON text.

                var j;

                function walk(holder, key) {

		    // The walk method is used to recursively walk the resulting structure so
		    // that modifications can be made.

                    var k, v, value = holder[key];
                    if (value && typeof value === 'object') {
                        for (k in value) {
                            if (Object.hasOwnProperty.call(value, k)) {
                                v = walk(value, k);
                                if (v !== undefined) {
                                    value[k] = v;
                                } else {
                                    delete value[k];
                                }
                            }
                        }
                    }
                    return reviver.call(holder, key, value);
                }


		// Parsing happens in four stages. In the first stage, we replace certain
		// Unicode characters with escape sequences. JavaScript handles many characters
		// incorrectly, either silently deleting them, or treating them as line endings.

                cx.lastIndex = 0;
                if (cx.test(text)) {
                    text = text.replace(cx, function (a) {
			    return '\\u' + ('0000' +
					    (+(a.charCodeAt(0))).toString(16)).slice(-4);
			});
                }

		// In the second stage, we run the text against regular expressions that look
		// for non-JSON patterns. We are especially concerned with '()' and 'new'
		// because they can cause invocation, and '=' because it can cause mutation.
		// But just to be safe, we want to reject all unexpected forms.

		// We split the second stage into 4 regexp operations in order to work around
		// crippling inefficiencies in IE's and Safari's regexp engines. First we
		// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
		// replace all simple value tokens with ']' characters. Third, we delete all
		// open brackets that follow a colon or comma or that begin the text. Finally,
		// we look to see that the remaining characters are only whitespace or ']' or
		// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

                if (/^[\],:{}\s]*$/.
		    test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
					     replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

					  // In the third stage we use the eval function to compile the text into a
					  // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
					  // in JavaScript: it can begin a block or an object literal. We wrap the text
					  // in parens to eliminate the ambiguity.

					  j = eval('(' + text + ')');

					  // In the optional fourth stage, we recursively walk the new structure, passing
					  // each name/value pair to a reviver function for possible transformation.

                    return typeof reviver === 'function' ?
					  walk({'': j}, '') : j;
				      }

				      // If the text is not JSON parseable, then a SyntaxError is thrown.

				      throw new SyntaxError('JSON.parse');
				      }
			 };
		    }();
	    }

