/****************************************************************

  Traction Software, Inc. Confidential and Proprietary Information

  Copyright (c) 1996-2007 Traction Software, Inc.
  All rights reserved.

****************************************************************/

// PLEASE DO NOT DELETE THIS LINE -- make copyright depends on it.


// shared data ------------------------------

var TYPE_CATEGORY = "topic";
var isNav  = (navigator.appName.indexOf("Netscape")  != -1);
var isNav4 = (window.Event) ? true : false;
var isIE   = (navigator.appName.indexOf("Microsoft") != -1);
var isMac  = (navigator.platform.indexOf("Mac")      != -1);
var isSafari;
var isKonqueror;

function ua_safe(prop, def) {
  if ( ua ) {
    return ua(prop,def);
  } else {
    return def;
  }
}

var FORM_ACTION_READ_ONLY  = "/traction";
var FORM_ACTION_READ_WRITE = "/traction";

// override window.open ----------------------------------------

var longurls = new Array();
var nextkey = 0;

function do_window_open(url, name, features) {
  if (url.indexOf("tractionpublish://") == 0) {
    window.location = url;
  } else {
    if (url.length > 2000) {
      var mykey = nextkey ++;
      longurls[mykey] = url;
      var newwin = window.open(FORM_ACTION_READ_ONLY + "?type=urloverride&urlkey=" + mykey, name, features);
      return newwin;
    } else {
      return window.open(url, name, features);
    }
  }
}


function getRootDocumentURL() {

  var root = document.location.href;
  var s = -1;

  if (root.toLowerCase().indexOf("http://") == 0) {
    s = 7;
  } else {
    s = 8;
  }

  if (s != -1) {
    s = root.indexOf("/", s) + 1;
    root = root.substring(0, s);
    Debug.println("For URL ''",
		  document.location.href,
		  "'', root URL = ''",
		  root,
		  "''.");
  } else {
    root = null;
    Debug.println("Root URL for this document (''",
		  document.location.href,
		  "'')could not be derived.");
  }

  return root;

}


function failureServerUnavailable() {
  alert(i18n("xmlrequest_failed_alert_message",
	     "The request failed because the server could not be reached.  Please ensure that your " +
	     "network connection has not been interrupted and that the server is still online."));
}

function failureUnauthorized() {
  alert(i18n("xmlrequest_unauthorized_alert_message",
	     "You are not authorized to perform the requested action with your current credentials.  " +
	     "Please open another browser window, sign in, and then try the action again."));
}



// cursor control ----------------------

/**
 * Keeps track of the number of times setWaitCursor(true) has been
 * called minus the number of times setWaitCursor(false) has been
 * called; it will never be less than 0.
 */
var waiters = 0;

/**
 * Keeps track of how many callers are waiting, and ensures that the
 * document body style's "cursor" property is "wait" whenever there is
 * at least one caller still waiting; otherwise, the "cursor" property
 * will be set to the empty string.
 */
function setWaitCursor( on ) {

  if (on) {
    // one more waiter
    waiters ++;
  } else if (waiters == 0) {
    // wants to un-wait, but there's no one waiting...
    return;
  } else {
    // one less waiter
    waiters --;
  }

  // If the request was to turn it on, set wait cursor.
  if (on) {
    setTimeout( function() {
		  document.body.style.cursor = "wait";
		},
		1 );
  }
  // If the request was to turn it off, only turn it off if the
  // requester was the last one waiting.
  else if (waiters == 0) {
    setTimeout( function() {
		  document.body.style.cursor = "";
		},
		1 );
  }

}

// browser independent functions ------------------------------

function keyCode(e) {
    if (isIE) {
        e = window.event;
	return e.keyCode;
    } else if (isNav) {
    	return e.which;
    } else {
        return -1;
    }
}

function keyCode2(e) {
  try {
    if (!e) e = window.event;
    if (e) {
      if (e.keyCode) return e.keyCode;
      if (e.which) return e.which;
    }
    return -1; // coundn't find it
  } catch (xcp) {
    return -1;
  }    
}

function isEnter(keycode) {
  return (keycode==13);
}

function ignoreEnter(e) {
  return !isEnter(keyCode(e));
}

// DOM functions ------------------------------
// originally from sections.js

function show_default(show, elm) {
  if (show) {
    elm.style.display = "";
  } else {
    elm.style.display = "none";
  }
}

function show_any(show, elm) {
  var specialFn = null;
  try {
    specialFn = (eval("show_" + elm.nodeName));
  } catch (whatever) { }
  if (typeof(specialFn) == "function") {
    Debug.richtext.println("Using show_", elm.nodeName);
    specialFn(show, elm);
  }
  else {
    show_default(show, elm);
  }
}

function show_inline(show, elm) {
  if (show) {
    elm.style.display = "inline";  
  } else {
    elm.style.display = "none";
  }  

  notifyDOMchanged();  // updates hover [ajm 14.Mar.2006]
}

function show_block(show, elm) {
  if (show) {
    elm.style.display = "block";  
  } else {
    elm.style.display = "none";
  }  

  notifyDOMchanged();  // updates hover [ajm 14.Mar.2006]
}

function show_TD(show, elm) {
  if (show) {
    try {
      elm.style.display = "table-cell";
    } catch (xcp) {
      // IE may not like table-cell
      elm.style.display = "inline";
    }
  } else {
    elm.style.display = "none";
  }  
}

function show_TR(show, elm) {
  if (show) {
    try {
      elm.style.display = "table-row";
    } catch (xcp) {
      // IE doesn't like table-row
      elm.style.display = "inline";  
    }
  } else {
    elm.style.display = "none";
  }  

  notifyDOMchanged();  // updates hover [ajm 14.Mar.2006]
}

function show_TABLE(show, elm) {
  if (show) {
    try {
      elm.style.display = "table";
    } catch (xcp) {
      // IE doesn't like table
      elm.style.display = "inline";  
    }
  } else {
    elm.style.display = "none";
  }

  notifyDOMchanged();  // updates hover [ajm 14.Mar.2006]
}

/**
 * The field's disabled attribute is not actually toggled.  Instead,
 * we toggle the readonly attribute and make the field look disabled
 * or enabled.  This way the field still participates in form
 * submission.
 */
function virtuallyDisable(disable, elm) {
  if (elm) {
    toggleReadOnly(disable, elm);
    elm.style.backgroundColor = (disable ? "#ebebe4" : "");
    elm.style.color           = (disable ? "#aca899" : "");
  }
}

function toggleReadOnly(readOnly, elm) {
  if (elm) {
    if (readOnly) {
      elm.setAttribute("readonly", "true");
      elm.readOnly = true;
    } else {
      elm.removeAttribute("readonly");
      elm.readOnly = false;
    }
  }
}

// backward compatibility
function showtable(show, elm) { show_TABLE(show, elm); }
function showinline(show, elm) { show_inline(show, elm); }

/**
 * Doubles the height in pixels of some HTML element 
 * @param id The HTML id attribute of the element to be expanded.
 * @altlist an optional parameter that can be specified to represent a
 * mapping between the given ID to another ID, corresponding to an
 * element to be expanded instead of the element with the given ID.
 * If this parameter is omitted, or does not include a mapping for the
 * given ID, the element with the given ID will be expanded.
 */
function morerows(id, altlist) {

  Debug.println("morerows(\"", id, "\", ", ((altlist == null) ? "no altlist" : (altlist.length + "-entry altlist")), ")");

  var elm;

  if (typeof(altlist) != "undefined" && altlist[id] != null) {
    elm = document.getElementById("mce_editor_" + altlist[id]);
  }

  if (elm == null) {
    elm = document.getElementById(id);
  }

  if (elm != null) {
    var height = outer_height(elm);
    elm.style.height = px(height*2);
  }

  // fixes hover [ajm 16.Dec.2005]
  notifyDOMchanged();
}

function notifyDOMchanged() {
  if (typeof(Events) != "undefined") {
    Events.fireCustom(Events.DOMchanged);
  }  
}

// returns the first parent of node with nodeName == name
function getParentByName(node, name) {
  while (node.parentNode && node.parentNode.nodeName != name) {
    node = node.parentNode;
  }
  return node.parentNode;
}

// gets the node with the nodeName == name that is a child of
// the specified node
function getFirstChildByName(node, name) {
  var children = node.getElementsByTagName(name);
  return (children && children.length > 0) ? children[0] : null;
}

function getFirstSiblingByName(node, name) {
  while (node.nextSibling && node.nextSibling.nodeName != name) {
    node = node.nextSibling;
  }
  return node.nextSibling;
}

function getLastSiblingByName(node, name) {
  var match = null;
  while (node.nextSibling) {
    if (node.nextSibling.nodeName == name) {
      match = node.nextSibling;
    }
    node = node.nextSibling;
  }
  return match;
}

/**
 * This is a Traction-optimized version of the original
 * getElementsByClass function given below (now called
 * getElementsByClassFromRootNode).  Instead of searching generally
 * and inefficiently for all tags with the given name each time the
 * function is called, this function lets the caller specify the list
 * of elements to filter.  This model allows the caller to maintain
 * lists of elements across multiple calls to this function, which can
 * mean many fewer calls to getElementsByTagName. [shep 13.Jan.2006]
 */
function getElementsByClass(searchClass, searchElements) {
  var classElements = new Array();
  var searchLength = searchElements.length;
  var pattern = new RegExp("(^|\\s)" + searchClass + "(\\s|$)");
  var i = 0;
  var j = 0;
  for (i = 0, j = 0; i < searchLength; i ++) {
    if ( pattern.test(searchElements[i].className) ) {
      classElements[j] = searchElements[i];
      j ++;
    }
  }
  return classElements;
}

/**
 * From http://www.dustindiaz.com/getelementsbyclass/ Copyright 2003 -
 * 2005 under the Creative Commons Lisencing.  Design and Development
 * by Dustin Diaz as part of the Web Standards with Imagination
 * network.
 */
function getElementsByClassFromRootNode(searchClass, node, tag) {
  var classElements = new Array();
  if ( node == null )
    node = document;
  if ( tag == null )
    tag = '*';
  var els = node.getElementsByTagName(tag);
  var elsLen = els.length;
  var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
  for (i = 0, j = 0; i < elsLen; i++) {
    if ( pattern.test(els[i].className) ) {
      classElements[j] = els[i];
      j++;
    }
  }
  return classElements;
}

function collection2array(coll) {
  if (new String(coll) != "[object HTMLCollection]") {
    return coll; // <<<<<<<<<<<<<<<<<<<<<<
  }
  var ret = new Array();
  for (var i = 0; i < coll.length; i ++) {
    ret[i] = coll[i];
  }
  return ret;
}

/**
 * General DOM function for inserting a given element into an element
 * before a certain sibling element.
 */
function insertElementBefore(parent, newChild, refChild) {
  parent.insertBefore(newChild, refChild);
}

/**
 * Either inserts the new child node before the reference node's next
 * sibling, or appends the new child node to the parent. Since DOM
 * insertAfter doesn't exist, this function is particularly useful.
 */
function insertElementAfter(parent, newChild, refChild) {
  if (refChild.nextSibling) {
    parent.insertBefore(newChild, refChild.nextSibling);
  } else {
    parent.appendChild(newChild);
  }
}

/**
 * General DOM function for inserting a given element into a table
 * before a certain sibling element, including creating the TR and TD
 * to contain the new child.
 */
function insertTableRowBefore(table, newChild, refChild) {
  // Create a new row and cell, and insert the new child into it.
  var newTR = document.createElement("TR");
  var newTD = document.createElement("TD");
  newTR.appendChild(newTD);
  newTD.appendChild(newChild);
  // Insert the new row into the table body.
  var entryTbody = getFirstChildByName(table, "TBODY");
  entryTbody.insertBefore(newTR, refChild);
}

/**
 * This function returns true if the given element is not null and has
 * descendant elements that are textareas.
 */
function hasDescendantCommentForms(element) {

  if (element == null) {
    return false;
  }

  var textareas = element.getElementsByTagName("textarea");
  return ((textareas != null) && (textareas.length != 0));

}

/**
 * Call this function to give focus to another element if a radio
 * button is clicked.  For example, if you have a radio button that
 * indicates that the value from a text input should be used, this
 * function could be called to give focus to the associated text input
 * when the radio button is clicked.  This function should be used
 * with the onclick event, not onchange event.
 * @param radio the INPUT element object that has been clicked.
 * @param associatedid the id attribute value of the element that is
 * to be focused.
 */
function radioClicked(radio, associatedid) {
  if (radio.checked) {
    var otherelm = document.getElementById(associatedid);
    if (otherelm != null) {
      setTimeout(function() {
		   otherelm.focus();
		 },
		 1 );
    }
  }
}

/**
 * Removes all instances of the given URL parameter from the given
 * URL, and returns the resulting URL.
 */
function removeURLParameter(url, paramname) {
  var param_assignment = new RegExp("&" + paramname + "=[^&]*");
  var newurl = new String(url);
  while (newurl.match(param_assignment)) {
    newurl = url.replace(param_assignment, "");
  }
  return newurl;
}

/**
 * Finds the occurrence of the given old classname in a given
 * element's className property and replaces it with the given new
 * class name.
 */
function replaceClassName(element, oldclass, newclass) {

  var classname = (element != null) ? element.className : null;
  if (classname == null) {
    return;
  }

  if (oldclass == "") {
    if (classname == "") {
      element.className = newclass;
    } else {
      element.className += " " + newclass;
    }
  } else {
    var newclassname = " " + classname + " ";
    var old = new RegExp( "\\s+" + oldclass + "\\s+", "g" );
    newclassname = trim(newclassname.replace(old, " " + newclass + " "));
    element.className = newclassname;
  }

}

// --------------------------------------------------
// shared form functions

// returns the form data application/x-www-form-urlencoded
function fm_submit(fm, omitFields) {
  var ret = "";

  var inputs = fm.getElementsByTagName("input");
  for (var i=0; i<inputs.length; i++) {
    if (!omitFields || (omitFields[inputs[i].name] != true)) {
      var cur = fm_input(inputs[i]);
      ret = fm_append(ret, cur);
    }
  }

  var textareas = fm.getElementsByTagName("textarea");
  for (var i=0; i<textareas.length; i++) {
    if (!omitFields || (omitFields[textareas[i].name] != true)) {
      var cur = fm_textarea(textareas[i]);
      ret = fm_append(ret, cur);
    }
  }

  var selects = fm.getElementsByTagName("select");
  for (var i=0; i<selects.length; i++) {
    if (!omitFields || (omitFields[selects[i].name] != true)) {
      var cur = fm_select(selects[i]);
      ret = fm_append(ret, cur);
    }
  }

  return ret;
}

function fm_append(sb, cur) {
  if (sb) {
    sb += "&";
  }
  sb += cur;

  return sb;
}

function fm_input(input) {
  var ret = "";

  switch (input.type) {
  case "button":
    break;
  case "checkbox":
  case "radio":
    if (input.checked) {
      ret = fm_param(input.name, input.value);
    }
    break;
  default:
    ret = fm_param(input.name, input.value);
    break;
  }

  return ret;
}

function fm_textarea(textarea) {
  return fm_param(textarea.name, textarea.value);
}

function fm_select(select) {

  var val = "";

  var opts = select.options;
  for (var i=0; i<opts.length; i++) {
    if (opts[i].selected) {
      if (val) {
	val += ",";
      }
      val += opts[i].value;
    }
  }

  return fm_param(select.name, val);

}

function serializeSelector(select) {
  var val = "";
  var opts = select.options;
  if (opts.length > 0) {
    val = opts[0].value;
    for (var i = 1; i < opts.length; i ++) {
      val += "," + opts[i].value;
    }
  }
  return val;
}

function fm_param(n,v) {
  return encode_url_parameter(n) + "=" + encode_url_parameter(v);
}


// --------------------------------------------------
// shared functions to make asynchronous XmlHttpRequests and/or update the DOM.

/**
 * Refreshes the labels for the given entry or item, given some
 * content; if no content is specified, an XML GET will be used to
 * request the content representing the updated label display.
 * @param id TractionId for item or entry whose labels are being
 * refreshed.  The argument for this parameter MUST be URL encoded (to
 * accomodate Japanese project names).
 * @param content The content to insert; if not specified, the content
 * will be retrieved from the server using an XMLHttpRequest via GET
 * to retrieve an itemlabels_ view.
 */
function labels_refresh(content, args) {

  var id = args[0];
  var actionfailurecallback = (args.length >= 1) ? args[1] : null;

  Debug.xmlhttp.println("labels_refresh(\"", id, "\", \"", (content == null ? "(null content)" : (content == "" ? "(empty content)" : "(some content)")), "\")");

  // first, remove the old labels table, if it exists.
  var lt = document.getElementById(id.toLowerCase() + "_LT"); // get the "Labels Table"

  var newargs = [ id, lt, actionfailurecallback ];
  if ( !content ) {
    // get the updated labels from the server.
    var url = FORM_ACTION_READ_ONLY + "?type=itemlabels_&tractionid=" + id;
    xmlget_async(url, null, true, labels_refresh_wakeup, newargs);
  } else {
    labels_refresh_wakeup(content, newargs);
  }

}

/**
 * @content the content representing the updated display of the
 * labels.
 * @param args[0] the TractionId of the item or entry whose labels
 * being refreshed.  The argument for this parameter MUST be URL
 * encoded (to accomodate Japanese project names).
 * @param args[1] the element whose content should be replaced by the
 * given content.
 */
function labels_refresh_wakeup(content, args) {

  var id = args[0];
  var lt = args[1];
  var actionfailurecallback = (args.length >= 2) ? args[2] : null;

  if (wasUnauthorized(content)) {
    failureUnauthorized();
    if (actionfailurecallback != null) {
      actionfailurecallback(id);
    }
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }
  if (wasUnreachable(content)) {
    failureServerUnavailable();
    if (actionfailurecallback != null) {
      actionfailurecallback(id);
    }
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }

  if ( lt ) {
    var parent = lt.parentNode;
    parent.removeChild( lt );
  } 

  // create a new DOM object containing the results
//   var tf = document.createElement("DIV"); // no way to create an empty element? :-(
//   tf.innerHTML = content;

  // insert the new fragment into the DOM
  var lta = document.getElementById(id.toLowerCase() + "_LTA"); // get the "Labels Table's Anchor"
  if ( lta ) {
    // Don't want to do this -- need to replace the *contents* of the
    // existing DIV, so that if the response does not contain a DIV
    // with the ID necessary for identifying the lta element on a
    // successive call (as happens in an error condition), the DIV can
    // still be found to dynamically insert the new labels.  Instead,
    // use the content returned to set the innerHTML of the existing
    // DIV.  This is accompanied by a corresponding change to the
    // itemlabels_ view (that generates the new lta) so that the DIV
    // with the lta's standard ID is *not* generated anymore.  This
    // makes it possible to execute dynamic reclassifications on
    // items/entries after an error is experienced, without having the
    // page reload on you.  [shep 09.Jan.2006]
//     var parent = lta.parentNode;
//     parent.replaceChild( tf, lta );
    lta.innerHTML = content;
  } else {
    window.location.reload();
  }

}

/**
 * Updates the display of a comment in the current view or submits an
 * asynchronous XML GET to refresh the display of the given entry, if
 * the browser supports it and if the given entry is displayed in the
 * current view as a comment.
 * @param id TractionId of the entry being refreshed.  The argument
 * for this parameter MUST be URL encoded (to accomodate Japanese
 * project names).
 * @content the content to use to update the display.  If it is not
 * specified, it will be requested from the server.
 * @return true if the content was refreshed or if it will be
 * refreshed pending the response from the server containing the
 * updated content.
 */
function comment_refresh(id, content) {

  Debug.xmlhttp.println("comment_refresh(\"", id, "\", \"", (content == null ? "(null content)" : (content == "" ? "(empty content)" : "(some content)")), "\")");

  // This should retrieve the TD element that contains the content.
  var comment = document.getElementById(id.toLowerCase() + "c");

  // If the comment doesn't exist in this view, we can't replace it.
  if (comment == null) {
    return false;
  }

  // Retrive the updated content from the server if necessary.
  if ( content == null ) {
    var url = FORM_ACTION_READ_ONLY + "?tractionid=" + id + "&type=" + "commentupdate_";
    if (document.fm.proj) {
      url += "&entry_project=" + encode_url_parameter(document.fm.proj.value);
    }
    xmlget_async(url, null, true, comment_refresh_wakeup, id);
  } else {
    comment_refresh_wakeup(content, id);
  }

  return true;

}

/**
 * @param content the content with which to update the display of the
 * given entry displayed as a comment in the current view.
 * @param id TractionId of the entry being refreshed.  The argument
 * for this parameter MUST be URL encoded (to accomodate Japanese
 * project names).
 */
function comment_refresh_wakeup(content, id) {

  if (wasUnauthorized(content)) {
    failureUnauthorized();
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }
  if (wasUnreachable(content)) {
    failureServerUnavailable();
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }

  // create a new TD to hold the content
  var updatedTD = document.createElement("TD");
  // set its ID so that it can be updated in this fashion repeatedly
  updatedTD.id = id.toLowerCase() + "c";
  // style it to bring attention to itself
  updatedTD.className = "comment_ commentinner";
  // populate it with the content
  updatedTD.innerHTML = content;
  // and replace the existing TD node with this new one.
  var comment = document.getElementById(id.toLowerCase() + "c");
  updatedTD.className = comment.className;
  comment.parentNode.replaceChild(updatedTD, comment);

}

/**
 * Submits a comment form with an asynchronous XML POST if supported
 * by the browser.
 * @param id TractionId for item or entry.  The argument for this
 * parameter MUST be URL encoded (to accomodate Japanese project
 * names).
 * @param displayNewForm a flag indicating whether a new form should
 * be displayed in place of the one that is being submitted.
 * @return true if the comment form for the given target is present
 * and the browser supports the XML POST, and false otherwise.
 */
function comment_submit(id, displayNewForm) {

  Debug.xmlhttp.println("comment_submit(\"" + id + "\", " + (displayNewForm ? "display new form" : "don't display new form") + ")");

  var cform = document.getElementById(id.toLowerCase() + "cfm");

  // If no form was drawn yet, or if the browser doesn't support XML
  // POST, let the calling context handle it.
  if ( cform == null || ua("supports_xmlhttp", "false") == "false") {
    return false; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }

  var cbutton = document.getElementById(id.toLowerCase() + "cb");
  if ( cbutton != null ) {
    cbutton.disabled = true;
  }

  var img = document.getElementById(id.toLowerCase() + "loading");
  if ( img ) {
    img.src = "/images/loading_on.gif";
  }

  // get the form data from the form
  var poststring = fm_submit(cform);

  // force a specific view
  poststring = fm_append(poststring, "rs={0}+type+comment_");

  // include proj=
  if (document.fm.proj) {
    poststring = fm_append(poststring, "entry_project=" + encode_url_parameter(document.fm.proj.value));
    poststring = fm_append(poststring, "stickyparams=entry_project");
  }

  // do the post
  var container = getParentByName(cform, "DIV");
  var args = [ id, container, displayNewForm ];
  xmlpost_async(FORM_ACTION_READ_WRITE, poststring, img, false, comment_insert, args);

  return true;

}

/**
 * Allows a popup window caller to replace an inline comment form in
 * the launching window with the new comment content.
 * @param id TractionId of the new comment.  The argument for this
 * parameter MUST be URL encoded (to accomodate Japanese project
 * names).
 * @param ctargetid TractionId for the target item or entry of the new
 * comment.  The argument for this parameter MUST be URL encoded (to
 * accomodate Japanese project names).
 * @return true if the new comment content was inserted or if it will
 * be inserted pending the response from the server containing the
 * updated content.
 */
function comment_popupinsert(id, ctargetid) {

  Debug.xmlhttp.println("comment_popupinsert(\"" + id + "\", \"" + ctargetid + "\")");

  // If no form was drawn yet, or if the browser doesn't support XML
  // GET, let the calling context handle it.
  var cform = document.getElementById(ctargetid.toLowerCase() + "cfm");
  if (cform == null || ua("supports_xmlhttp", "false") == "false") {
    return false;
  }

  var url = FORM_ACTION_READ_ONLY + "?tractionid=" + id + "&type=" + "comment_";
  var displayNewForm = (typeof(viewtype) != undefined) && (viewtype == "single") &&
    (ctargetid.indexOf(".") == -1 || ctargetid.indexOf(".00") == ctargetid.indexOf(".")) &&
    (document.getElementById(ctargetid.toLowerCase() + "c") == null);
  var container = getParentByName(cform, "DIV");
  var args = [ ctargetid, container, displayNewForm ];
  xmlget_async(url, null, true, comment_insert, args);

  return true;

}

/**
 * Inserts the content for the new comment on the given item/entry as
 * the contents (innerHTML) of the given element, and displays a new
 * form if requested.
 * @param args[0] TractionId for the target item or entry of the new
 * comment.  The argument for this parameter MUST be URL encoded (to
 * accomodate Japanese project names).
 * @param args[1] the element whose content should be replaced by the
 * given content.
 * @param args[2] a flag indicating whether a new form should be
 * displayed when this one has been successfully submitted.
 */
function comment_insert(content, args) {

  var id = args[0];
  var container = args[1];
  var displayNewForm = args[2];
  Debug.xmlhttp.println("comment_insert(\"" + (content == null ? "(null content)" : (content == "" ? "(empty content)" : "(some content)")) + "\", \"" + id + "\", some " + container.tagName + ", " + (displayNewForm ? "display new form" : "don't display new form") + ")");

  var cbutton = document.getElementById(id.toLowerCase() + "cb");

  if (wasUnauthorized(content)) {
    if ( cbutton != null ) {
      cbutton.disabled = false;
    }
    failureUnauthorized();
    return false; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }
  if (wasUnreachable(content)) {
    if ( cbutton != null ) {
      cbutton.disabled = false;
    }
    failureServerUnavailable();
    return false; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }

  content = new String(content);
  var error = findErrorAndFeedback(content);
  if (error[0] != null && error[0] != "" && error[0] != "undefined") {
    commenterrormessage_insert(id, error);
    if ( cbutton != null ) {
      cbutton.disabled = false;
    }
    return false;
  }

//   if ( isRichText ) {
//     tinyMCE.removeMCEControl('mce_editor_' + mceIndices[id.toLowerCase() + "ac"]);
//     mceIndices[id.toLowerCase() + "ac"] = null;
//   }

  // insert the result in place of the comment form
  container.innerHTML = content;

  if ( document.getElementById(id.toLowerCase() + "commenterror") != null ) {
//     if ( isRichText ) {
//       initRichTextComment(id);
//     }
  } else if ( displayNewForm ) {
    commentform_insert('entry', id + ".00", '&entryaddcomment=true');
  }

  return true;

}

var inlinecomment_pre_registry = new Array();

/**
 * Submits an asynchronous XML GET for a new comment form if supported
 * by the browser and the arrangement and presence of certain elements
 * in the view.
 * @param opttype item,entry,view,tool
 * @param id the TractionId of item or entry being commented on.  The
 * argument for this parameter MUST be URL encoded (to accomodate
 * Japanese project names).
 * @param extras extra url parameters
 * @return true if a comment form will be requested and inserted;
 * false otherwise.
 */
function commentform_insert(opttype, id, extras) {

  Debug.xmlhttp.println("commentform_insert(\"" + opttype + "\", \"" + id + "\", \"" + extras + "\")");

  // Do the id re-mapping immediately so that we are sure to
  // consistently use the same keys for the inline comment
  // pre-registry.  AKJ5795.  [shep 27.Mar.2007]

  // treat item 00 like entry
  if (opttype == "item" && id.length > 3 && id.substring(id.length-3) == ".00") {
    opttype = "entry";
  }

  // remove the item id from the id
  if (opttype == "entry") {
    var period = id.indexOf('.');
    if (period != -1) {
      id = id.substring(0,period);
    }    
  }

  // To fix the error reported in BCG946, pre-register this ID so that
  // double clicks will not have adverse effects.  [shep 08.Mar.2007]
  if (typeof(inlinecomment_pre_registry[id]) != "undefined" &&
      inlinecomment_pre_registry[id]) {
    Debug.xmlhttp.println("Not pulling in new comment form for ", id, " because it's already pre-registered.");
    return false;
  }

  inlinecomment_pre_registry[id] = true;

  if (ua("supports_xmlhttp", "false") == "false") {
    inlinecomment_pre_registry[id] = null;
    return false; // Don't bother taking this route if the browser doesn't support an XmlHttpRequest.
  }

  // first check to see if there's already a form.  we do this by
  // looking for the id+"ow" link, which would have been placed there
  // by the new form.
  //
  // "ow" stands for "open window".  it's the little comment icon that
  // gets drawn when a comment form is displayed.  if this is already
  // there, we know we already have a form.  in that case, we want to
  // open a window instead of drawing a second form.
  var ow = document.getElementById(id.toLowerCase()+"ow");
  if (ow) {
    inlinecomment_pre_registry[id] = null;
    return false;
  }
  
  var parent = null;
  var refChild = null;

  if (opttype == "item") {
    // find the TR to which i want to add a comment
    //
    // "ir" stands for "item row".  it's the row that contains the
    // item.
    var onTR = document.getElementById(id.toLowerCase()+"ir");
    if (onTR) {
      // *** comment on item **********
      parent = onTR.firstChild;
    }
    else {
      // "cs" stands for comment span. it's the item content of an
      // inline comment. this is where we'll put the new box (at the
      // end).
      var onSPAN = document.getElementById(id.toLowerCase()+"cs");
      if (onSPAN) {
	// *** comment on comment item **********
	var beforeTag = getFirstSiblingByName(onSPAN, "BR");
	if (!beforeTag) {
	  // before the commentdetails DIV
	  beforeTag = getLastSiblingByName(onSPAN, "DIV");
	}
	parent = onSPAN.parentNode;
	refChild = beforeTag;
      }
      else {
	var onDIV = document.getElementById(id.toLowerCase()+"i");
	if (onDIV) {
	  // *** proteus comment on item ***
	  parent = onDIV.parentNode;
	  refChild = onDIV.nextSibling;
	}
      }
    }
  }
  else if (opttype == "entry") {
  
    // *** comment on entry **********
    var entryaddcomment = document.getElementById("entryaddcommment");
    if (entryaddcomment) {
      // already has a spot for the comment form -- ideally use the
      // existing form to pop a comment dialog
    } else {
      var onDIV = document.getElementById(id.toLowerCase()+"cd");
      if (onDIV) {
	// *** comment on comment entry **********
	parent = onDIV.parentNode;
      }
      else {
	var onTR = document.getElementById(id.toLowerCase()+"ecr");
	if (onTR) {
	  // *** comment on top level entry, in single or stacked entry view **********
	  parent = onTR.firstChild;
	}
      }
    }
  }

  if (parent != null) {
    var args = [ id, parent, refChild ];
    var url = FORM_ACTION_READ_ONLY + "?type=inlinecomment_&target=" + id + "&opttype=" + opttype + "&nexttabindex= " + inlinecomment_.nextTabIndex;
    inlinecomment_.nextTabIndex += 100;
    if (extras) {
      url += extras;
    }
    xmlget_async(url, null, true, commentform_insert_wakeup, args);
    return true;
  } else {
    inlinecomment_pre_registry[id] = null;
    return false;
  }

}

/**
 * @param content the content to be inserted as a new comment form.
 * @param args[0] TractionId for item or entry being commented on.
 * The argument for this parameter MUST be URL encoded (to accomodate
 * Japanese project names).
 * @param args[1] the parent element into which a new DIV containing
 * the content should be inserted.
 * @param args[2] the reference child of the given parent before which
 * the new content should be inserted, if any.  Can be null.
 */
function commentform_insert_wakeup(content, args) {

  // Break out the arguments.
  var id = args[0];
  var parent = args[1];
  var refChild = args[2];

  Debug.xmlhttp.println("commentform_insert_wakeup(\"" + (content == null ? "(null content)" : (content == "" ? "(empty content)" : "(some content)")) + "\", \"" + id + "\", " + ((parent == null) ? "(no parent)" : ("some " + parent.tagName)) + ", " + ((refChild == null) ? "(no reference child)" : ("some " + refChild.tagName)) + ")");

  if (wasUnauthorized(content)) {
    failureUnauthorized();
    inlinecomment_pre_registry[id] = null;
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }
  if (wasUnreachable(content)) {
    failureServerUnavailable();
    inlinecomment_pre_registry[id] = null;
    return; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }

  var error = findErrorAndFeedback(content);
  if (error[0] != null && error[0] != "" && error[0] != "undefined") {
    alert(error[0]);
    inlinecomment_pre_registry[id] = null;
    return;
  }

  // Create the new DIV to hold the comment form, and populate it.
  var newDIV = document.createElement("DIV");
  newDIV.innerHTML = content;

  // Determine the appropriate insertion function and call it.
  insertionFunction = (parent.tagName.toLowerCase() == "table") ? insertTableRowBefore : insertElementBefore;
  insertionFunction(parent, newDIV, refChild);

  // make sure we can see the new comment form.
  // false says put it at the bottom of the screen 
  // (to show entry in context above it)

  // Don't do this anymore.  See Server15130.09.
  //       if ( newDIV && newDIV.scrollIntoView ) {
  // 	newDIV.scrollIntoView(false);
  //       }

  // fixes hover [ajm 16.Dec.2005]
  notifyDOMchanged();

  Debug.xmlhttp.println("Un-pre-pregistering comment form for ", id);
  inlinecomment_pre_registry[id] = null;

  var targetPublished;
  var findTargetPublishedStatus = content.indexOf("<!--[[targetPublished=");
  if (findTargetPublishedStatus != -1) {
    targetPublished = (content.substring(findTargetPublishedStatus + 22).indexOf("true") == 0);
    Debug.edit.println("Comment target ", id, " published? ", targetPublished);
  }
  else {
    targetPublished = false; // if we can't tell for sure, assume false
  }

  var randomVal;
  var findRandomVal = content.indexOf("<!--[[randomVal=");
  if (findRandomVal != -1) {
    randomVal = parseInt(content.substring(findRandomVal + 16), 10);
  } else {
    randomVal = 0;
  }

  try {

    var formId = id.toLowerCase() + "cfm";
    inlinecomment_.registerForm(formId, targetPublished, randomVal);

    // put the cursor focus in the new comment textarea
    //
    // update: this was a good idea, but it causes Firefox
    // to scroll to the top of the page.  lame...
    //
    // do this after the comment is registered, and clean it up to use
    // the references to the form elements via the registry, rather
    // than gluing together strings to get the ID of some specific
    // form element. [shep 03.Dec.2008]
    if (isIE) {
      var form = document.getElementById(formId);
      if (form.edit_property_author_name) {
	textarea.form.edit_property_author_name.focus();
      } else {
	var editor = inlinecomment_.registry[formId].getTinyMceEditor();
	if (editor) {
	  editor.focus();
	} else {
	  form.edit_content.focus();
	}
      }
    }

  } catch (xcp) {
    // This can happen if we're using a custom inline comment form; the
    // error should be ignored.
  }

}

/**
 * Removes the commment form in the current page, if it exists, for
 * the given item or entry.
 * @param id the TractionId of item or entry that would have been the
 * comment target.  The argument for this parameter MUST be URL
 * encoded (to accomodate Japanese project names).
 */
function commentform_remove(id) {

//   if ( isRichText ) {
//     tinyMCE.removeMCEControl('mce_editor_' + mceIndices[id.toLowerCase() + "ac"]);
//     mceIndices[id.toLowerCase() + "ac"] = null;
//   }

  var cform = document.getElementById(id.toLowerCase() + "cfm");
  if (cform == null) {
    return; // what were they removing?
  }
  var container = cform.parentNode;
  container.parentNode.removeChild( container );

  // fixes hover [ajm 16.Dec.2005]
  notifyDOMchanged();
}

function commenterrormessage_insert(id, error) {

  try {

    var chead = document.getElementById(id.toLowerCase() + "chead");
    var errormessage = error[0];
    var feedbacklink = error[1];
    if (chead == null || errormessage == null) {
      return;
    }

    Debug.xmlhttp.println("commenterrormessage_insert(\"", id, "\", \"", errormessage, "\")");

    var newerrordiv = document.createElement("DIV");
    newerrordiv.className = "commenterror";

    if (feedbacklink != null && feedbacklink != "" && feedbacklink != "undefined") {
      var linkanchor = document.createElement("A");
      linkanchor.href = feedbacklink;
      linkanchor.className = "inlinefeedbacklink";
      linkanchor.target = "_blank";
      linkanchor.innerHTML = i18n("feedback", "feedback");
      newerrordiv.appendChild(linkanchor);
    }

    var errorlabel = document.createTextNode(i18n("Error", "Error") + ":  ");
    var errorspan = document.createElement("SPAN");
    errorspan.className = "errormessage";
    errorspan.innerHTML = errormessage;
    newerrordiv.appendChild(errorlabel);
    newerrordiv.appendChild(errorspan);

    var olderrordiv = document.getElementById(id.toLowerCase() + "commenterror");
    if (olderrordiv != null) {
      olderrordiv.parentNode.replaceChild(newerrordiv, olderrordiv);
      newerrordiv.id = id.toLowerCase() + "commenterror";
    } else {
      chead.appendChild(newerrordiv);
      newerrordiv.id = id.toLowerCase() + "commenterror";
    }

  } catch(xcp) { }

}


// XML RPC ----------------------------------------


//  var xml = xmlreq();
//  xml.open("GET", "/servlet/tsi.Traction?type=inlinecomment", true);

var xml_ = null;

function xmlkill() {
  if (xml_) {
    try {
      xml_.abort();
    } catch (e) {
      // ignore it, we tried
    }
  }
}

var xmlimg;

// -1 = none
//  0 = off
//  1 = on
function xmlprogress(mode, progress) {

  if (!progress) {
    progress = xmlimg;
  } else if ( "string" == typeof(progress) ) {
    progress = document.getElementById( progress );
  }

  if (progress && progress.nodeName == "IMG") {
    var newsrc;
    switch (mode) {
    case 0:
      newsrc = "/images/loading_off.gif";
      break;
    case 1:
      newsrc = "/images/loading_on.gif";
      break;
    default:
    case -1:
      newsrc = "/images/modern/px.gif";
      break;
    }
    Debug.xmlhttp.println("setting ", progress.id, " src to ", newsrc);
    setTimeout(function() {
		 progress.src = newsrc;
	       }, 1);
  }

}


function createThrobberForField(formElement) {
  var img = formElement.nextSibling;
  if (img == null || img.nodeName.toLowerCase() != "img" || img.className.indexOf("throbber" == -1)) {
    img = document.createElement("IMG");
    img.src = "/images/modern/px.gif";
    img.style.width = "16px";
    img.style.height = "16px";
    img.style.marginLeft = "-20px";
    img.style.top = "2px";
    img.style.position = "relative";
    img.className = "throbber";
    insertElementBefore(formElement.parentNode, img, formElement.nextSibling);
  }
  return img;
}


function findErrorAndFeedback(content) {
  var finderrormessage = content.indexOf("<!--ErrorMessage[[[");
  var errormessage = null;
  var feedbacklink = null;
  if (finderrormessage != -1) {
    errormessage = content.substring(finderrormessage + 19, content.indexOf("]]]ErrorMessage-->"));
    findfeedbacklink = content.substring(content.indexOf("<!--FeedbackLink[[["));
    if (findfeedbacklink != -1) {
      feedbacklink = content.substring(findfeedbacklink + 19, content.indexOf("]]]FeedbackLink-->"));
    }
  }
  return [ errormessage, feedbacklink ];
}

function xml_request_unauthorized(xml, alertuser) {
  var ret = false;
  if (wasUnauthorized(xml.responseText)) {
    ret = true;
    if (alertuser) {
      failureUnauthorized();
    }
  }
  return ret;
}

function wasUnauthorized(content) {
  var switchMeanings = (ua("xmlhttp_unreachable_unauthorized_reversed", "false") == "true");
  return ((switchMeanings &&
	   content == "") || // in Safari "" means unauthorized
	  (!switchMeanings &&
	   content == null) || // in other browsers, null means unauthorized
	  (content != null && // non-null response text and
	   content != "" &&   // non-empty response text
	   content.indexOf("<!--loginform") != -1)); // and loginform means unauthorized
}

function xml_request_failed(xml, alertuser) {
  var ret = false;
  if (wasUnreachable(xml.responseText)) {
    ret = true;
    if (alertuser) {
      failureServerUnavailable();
    }
  }
  return ret;
}

function wasUnreachable(content) {
  var switchMeanings = (ua("xmlhttp_unreachable_unauthorized_reversed", "false") == "true");
  return (switchMeanings && content == null) || // in Safari, null means no response
    (!switchMeanings && content == ""); // in other browsers, "" means no response
}

// progress = 16x16 img element
function xmlpost(url, body, progress) {
  var ret = "";

  try {
    xmlprogress(1, progress);

    xml_ = xmlreq();
    if (xml_) {
      xml_.open("POST", url, false);
      xml_.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      if (navigator.userAgent) {
	xml_.setRequestHeader("User-Agent", navigator.userAgent);
      }
      xml_.send(body);

      if (xml_.readyState == 4 && xml_.responseText) {
	ret = xml_.responseText;
      }
    }

    if (xml_request_unauthorized(xml_)) {
      ret = null;
    }

    xmlprogress(0, progress);
  } catch (e) { 
    xmlprogress(0, progress);
    throw e; 
  }

  return ret;

}

// progress = 16x16 image element
function xmlget(url, progress) {
  var ret = "";

  try {
    xmlprogress(1, progress);

    xml_ = xmlreq();
    if (xml_) {
      xml_.open("GET", url, false);
      xml_.send(null);

      if (xml_.readyState == 4 && xml_.responseText) {
	ret = xml_.responseText;
      }
    }

    if (xml_request_unauthorized(xml_)) {
      ret = null;
    }

    xmlprogress(0, progress);
  } catch (e) { 
    xmlprogress(0, progress);
    throw e; 
  }

  return ret;
}

/**
 * NOTE: For methods with a long list of parameters, use xmlpost_async so we
 * don't have URL length issues.
 *
 * @param url url with all parameters
 * @param progress element id or element object of a progress image (16x16)
 * @param waitcursor true or false to show waitcursor during the operation
 * @param callback a function callback(responseText, callbackarg)
 * @param callbackarg object passed to callback to maintain state
 * @param finalProgressState state from xmlprogress to use when complete
 * @return xml object for abort()
 */
function xmlget_async(url, progress, waitcursor, callback, callbackarg, finalProgressState) {

  if (!finalProgressState) {
    finalProgressState = 0;
  }

  try {
    var myxml = xmlreq_create();
    if (myxml) {

      xmlprogress(1, progress);

      if (waitcursor) {
	setWaitCursor( true ); // wait cursor on
      }

      // thread switch to get progress to update
      setTimeout(function() {
		   xmlget_async_wakeup(myxml, url, progress, waitcursor, callback, callbackarg, finalProgressState);
		 },
		 1 );
      return myxml;
    }
  } catch (e) {}
  return null;
}

function xmlget_async_wakeup(myxml, url, progress, waitcursor, callback, callbackarg, finalProgressState) {

  try {
    myxml.open("GET", url, true);
    myxml.onreadystatechange = function()
      {
	if (myxml.readyState == 4 && myxml.responseText != null) {
	  if (xml_request_unauthorized(myxml, false)) {
	    setTimeout(function() { 
			 if (myxml.readyState == 4) {
			   xml_async_callback_notify(callback, null, callbackarg);
			 }
		       }, 1);
// 	    callback(null, callbackarg);
	    xmlprogress(-1, progress);
	  } else {
	    setTimeout(function() { 
			 if (myxml.readyState == 4) {
			   xml_async_callback_notify(callback, myxml.responseText, callbackarg);
			 }
		       }, 1);
// 	    callback(myxml.responseText, callbackarg);
	    xmlprogress((xml_request_failed(myxml, false) ? -1 : finalProgressState), progress);
	  }
	  if (waitcursor) {
	    setWaitCursor( false ); // wait cursor off
	  }
	}
      }
    myxml.send(null);

  } catch (e) {

    xmlprogress(0, progress);
    if (waitcursor) {
      setWaitCursor( false ); // wait cursor off
    }

    throw e;

  }

}

/**
 * IE doesn't want you to modify the DOM inside the onreadystatechange
 * function, so we use setTimeout instead.
 */
function xml_async_callback_notify(callback, responseText, callbackarg) {
  callback(responseText, callbackarg);
}

/**
 * @param url base url for the post, can include some params like type=
 * @param body params in the form p0=v0&p1=v1&...
 * @param progress element id or element object of a progress image (16x16)
 * @param waitcursor true or false to show waitcursor during the operation
 * @param callback function callback(responseText, callbackarg)
 * @param callbackarg object passed to callbackarg
 * @param finalProgressState state from xmlprogress to use when complete
 * @return xml object for abort()
 */
function xmlpost_async(url, body, progress, waitcursor, callback, callbackarg, finalProgressState) {

  if (!finalProgressState) {
    finalProgressState = 0;
  }

  try {
    var myxml = xmlreq_create();
    if (myxml) {

      xmlprogress(1, progress);

      if (waitcursor) {
	setWaitCursor( true ); // wait cursor on
      }

      // thread switch to get progress to update
      setTimeout(function() {
		   xmlpost_async_wakeup(myxml, url, body, progress, waitcursor, callback, callbackarg, finalProgressState);
		 },
		 1 );

      return myxml;
    }
  } catch (e) {}
  return null;
}

function xmlpost_async_wakeup(myxml, url, body, progress, waitcursor, callback, callbackarg, finalProgressState) {

  try {

    myxml.open("POST", url, true);
    myxml.setRequestHeader( "X-Traction-XMLHTTP", "true" );
    myxml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    if (navigator.userAgent) {
      myxml.setRequestHeader("User-Agent", navigator.userAgent);
    }
    myxml.onreadystatechange = function()
      {
	if (myxml.readyState == 4) {
	  if (xml_request_unauthorized(myxml, false)) {
	    setTimeout(function() {
			 if (myxml.readyState == 4) {
			   xml_async_callback_notify(callback, null, callbackarg);
			 }
		       }, 1);
// 	    callback(null, callbackarg);
	    xmlprogress(-1, progress);
	  } else {
	    setTimeout(function() { 
			 if (myxml.readyState == 4) {
			   xml_async_callback_notify(callback, myxml.responseText, callbackarg);
			 }
		       }, 1);
// 	    callback(myxml.responseText, callbackarg);
	    xmlprogress((xml_request_failed(myxml, false) ? -1 : finalProgressState), progress);
	  }
	  if (waitcursor) {
	    setWaitCursor( false ); // wait cursor off
	  }
	}
      }

    myxml.send(body);

  } catch (e) {

    xmlprogress(-1, progress);
    if (waitcursor) {
      setWaitCursor( false ); // wait cursor off
    }

    throw e;

  }

}

function xmlreq() {
  if (xml_ == null) {
    xml_ = xmlreq_create();
  }
  return xml_;
}

function xmlreq_create() {
  var ret;

  try {
    ret = new XMLHttpRequest();
  } 
  catch (e) {
    try {
      ret = new ActiveXObject("Microsoft.XMLHTTP");
    } 
    catch (e) {
      try {
	ret = new ActiveXObject("Msxml2.XMLHTTP");
      }
      catch (e) {
	ret = null;
      }
    }
  }

  return ret;
}

// shared functions ------------------------------

/**
 * Trim a string.  New implementation of this trims all whitespace
 * (not just spaces).  The new version also constructs a String object
 * with any non-null input, making things a little more type safe.
 * See Server14165.
 */
function trim(str) {
  if (str != null) {
    var newstr = new String(str);
    return newstr.replace(/^\s+/,'').replace(/\s+$/,'');
  } else {
    return null;
  }

/*        old implementation */
/*     var s = 0; */
/*     var e = str.length-1; */
/*     while(s<=e && (str.charAt(s)==' ' || str.charAt(s)=='\t' || str.charAt(s)=='\n' || str.charAt(s)=='\r')) { */
/*         s++; */
/*     } */
/*     if (s>e) return ''; */
/*     while(str.charAt(e)==' ' || str.charAt(e)=='\t' || str.charAt(e)=='\n' || str.charAt(e)=='\r') { */
/*         e--; */
/*     } */
/*     return str.substring(s,e+1); */

}

function trimEnd(str) {
  if (str != null) {
    var newstr = new String(str);
    return newstr.replace(/\s+$/,'');
  } else {
    return null;
  }
}

function trimBeginning(str) {
  if (str != null) {
    var newstr = new String(str);
    return newstr.replace(/^\s+/,'');
  } else {
    return null;
  }
}

/**
 * If the given string is longer than the given maximum number of
 * characters, cuts off the extra characters plus two more, and
 * appends an ellipsis (unicode 0x2026).
 */
function truncateWithEllipses(str, maxChars) {
  if (str.length > maxChars) {
    str = str.substring(0, maxChars - 2) + "\u2026";
  }
  return str;
}

// don't use this outside of file
function checkCategory(cat) {

  cat = trim(cat);

  if (cat == "") {
    return false;
  }

  // Trim leading +: if it's present (Server5974)
  if (cat.length >= 2 &&
      cat.charAt(0) == '+' &&
      cat.charAt(1) == ':') {
    cat = cat.substring(2);
  }

  // Don't include these punctuation characters: ?!#;@$%^&*=~/\|()[]{}'`<>
  // For Server5974, added + to the list.
  // For Server15688, added , to the list
  // Removed . from the list [ajm 27.Apr.2007]
  var illegal = /[\?\,!#;@\$%\^&\*\=\~\/\\\|\(\)\[\]\{\}\'\`\<\>\+]+/;
  // Allow colons generally, but not at the beginning or end of a
  // label name, and not two or more in a row. [shep 11.Apr.2005]
  var illegalcolons = /^[:]|[:]{2}|[:]$/;
  var result = cat.match(illegal) || cat.match(illegalcolons);
  if (result) return false;

  return true;

}


// use this instead:
// check category for invalid characters
function isValidCategory(cat) {
  if (!checkCategory(cat)) {
    alert(i18n("psetup_categories_invalid_category_name_warning_message", "Invalid category name:") + "'" + cat + "'");    
    return false;
  } else {
    return true;
  }
}

function checkVerb(verb) {
  // Have to specifically prevent this to avoid confusion, because the
  // editrelationships view isn't going to allow this to fly.  [shep 27.Jul.2007]
  if ((verb == "replies") || (verb == "replies to") || (verb == i18n("replies_active", "replies to"))) {
    alert("The name you have chosen for the new relationship type is reserved for a standard relationship type.\n\nPlease type a different name.");
    return false;
  }
  return true;
}

function isValidVerb(verb) {
  return isValidCategory(verb) && checkVerb(verb);
}

// quotes it appropriately, removes extra colons, etc.
function fixCategory(add) {

  add = trim(add);
  //  add = removeChars(add,'"');

  if (add.charAt(0) == '+') {
    add = add.substring(1);
  }

  if (add.charAt(0) == ':') {
    add = add.substring(1);
  }

  return add;
}

function isValidUserName(str) {
  if (!str || str=='') return false;

  // allow many more characters [ajm 19.Dec.2003]
  //var pattern = /[ ?\.!#;@$%^&*\=\~\/\\|\(\)\[\]\{\}\'\`\<\>\+]+/; 
  var pattern = /[\n\r\"\'\`\(\)]+/;
  var result = str.match(pattern);
  if (result) return false;

  var lower = str.toLowerCase();
  if (lower == 'owner') return false;
  if (lower == 'visitor') return false;
  
  // we passed all the tests
  return true;
}

function isValidProjectName(str) {
  if (!checkProjectName(str)) {
    alert("'" + str + "' " + i18n("jsetup_invalid_project_name_alert_message", "is not a valid project name.\nProject names may consist of numbers and letters, including international characters.\nProject names may not contain punctuation.\nProject names must end with a non-numeric character.\n"));
    return false;
  } else {
    return true;
  }
}

function checkProjectName(str) {

  if (str == null || str == '') {
    return false;
  }

  var illegal = /[ \"_?\,\.!#;:@$%^&*\=\~\/\\|\(\)\[\]\{\}\'\`\<\>\+\-]+/;
  var enddigits = /^[0-9].*$|^.*[0-9]$/;
  if (str.match(illegal) || str.match(enddigits)) {
    return false;
  }

  return true;

}

function deleteNulls(arr) {
  var ins = 0;
  for (var i=0; i<arr.length; i++) {
    if (arr[i] != null) {
      arr[ins++] = arr[i];
    }
  }
  arr.length = ins;
}

function caseInsensitiveCompare(a, b) {
  var al = a.toLowerCase();
  var bl = b.toLowerCase();
  
  return (al == bl) ? 0 : (al < bl) ? -1 : 1;
}

function sortCI(arr) {
  if (arr && arr.sort) {
    arr.sort(caseInsensitiveCompare);
  }
}


/**
 * Lists each value in the given SELECT element as an element in a
 * comma separated list (commas in the values are escaped).
 */
function selectToText(sel) {
  var val = "";
  for (var i = 0; i < sel.options.length; i ++) {
    if (val != "") {
      val += ",";
    }
    val += escape_commas(sel.options[i].value);
  }
  return val;
}


// set a list
function setList(select,array,selectoptions) {
  if (select) {
    var opts = select.options;

    if (!array) {
      select.options.length = 0;
    } else {

      opts.length = 0;
      for (var i=0; i<array.length; i++) {
	opts[i] = new Option(array[i]);
    
	if (selectoptions && indexOf(selectoptions,array[i]) != -1) {
	  opts[i].selected = true;
	}
      }

    }
  }
}

// current selection (for single selects)
function currentSel(sel) {
  if (sel.options.length != 0 && sel.selectedIndex >= 0) {
    return sel.options[sel.selectedIndex].text;
  } else {
    return "";  // ie5 is much happier with this
  }
}

function currentSelValue(sel) {
  if (sel.options.length != 0) {
    return sel.options[sel.selectedIndex].value;
  } else {
    return "";  // ie5 is much happier with this
  }
}

// index of
function indexOf(array,item) {
  for (var i=0; i<array.length; i++) {
    if (array[i]==item) return i;
  }
  return -1;
}

// clear selections
function selectNone(sel) {
    var sz = sel.options.length;
    for (var i=0; i<sz; i++) {
         sel.options[i].selected = false;
    }
}	

function removeAll(sel) {
  sel.options.length = 0;
}

// sel - a select object
function selectAll(sel) {
    var sz = sel.options.length;
    for (var i=0; i<sz; i++) {
         sel.options[i].selected = true;
    }
}

function selectIndexOf(sel,text) {
    var opts = sel.options;
    for (var i=0; i<opts.length; i++) {
        if (text==opts[i].text) return i;
    }
    return -1;
}

function selectIndexOfValue(sel,value) {
    var opts = sel.options;
    for (var i=0; i<opts.length; i++) {
        if (value==opts[i].value) return i;
    }
    return -1;
}

function selected2array(sel) {
    var opts = sel.options;
    var ret = new Array();
    for (var i=0; i<opts.length; i++) {
      if (opts[i].selected) {
	ret.push(opts[i].text);
      }
    }
    return ret;
}

// ins - insert position if it's not there, -1 if it shouldn't be added
function selectOption(sel,text,ins) {
  var i = selectIndexOf(sel,text);
  if (i == -1) {
    if (ins != -1) {
      // it's not there, insert it
      insertOptionAt(sel,text,ins);
      sel.selectedIndex = ins;
    }
  } else {
    sel.selectedIndex = i;
  }
}

function selectIndexOfValue(sel,value) {
  var opts = sel.options;
  for (var i=0; i<opts.length; i++) {
    if (value==opts[i].value) return i;
  }
  return -1;
}

function selectIndexOfText(sel,text,caseSensitive) {
  var opts = sel.options;
  if (caseSensitive) {
    for (var i=0; i<opts.length; i++) {
      if (text==opts[i].text.toLowerCase()) return i;
    }
  } else {
    for (var i=0; i<opts.length; i++) {
      if (text==opts[i].text) return i;
    }
  }
  return -1;
}

function selectOptionByValue(sel,value) {
  var i = selectIndexOfValue(sel,value);
  if (i != -1) {
    sel.selectedIndex = i; 
  }
  return (i != -1);
}

function selectOptionByText(sel,text,caseSensitive) {
  var i = selectIndexOfText(sel,text,caseSensitive);
  if (i != -1) {
    sel.selectedIndex = i;
  }
  return (i != -1);
}

function insertOptionAt(sel,text,index) {
  var opts = sel.options;

  if (index > opts.length) {
    // add it to the end
    index = opts.length;
  } else {
    // move everything back one to make room
    for (var i=opts.length; i > index; i--) {
      opts[i] = new Option(opts[i-1].text);

      // maintain selection
      if (opts[i-1].selected) {
	opts[i].selected = true;
	opts[i-1].selected = false;
      }
    }
  }

  // insert the new one
  opts[index] = new Option(text);
}

// true if the select already contains text
function selectContains(sel,text) {
    return (selectIndexOf(sel,text)!=-1);
}

// remove the spacer from the top
function removeSpacer(sel) {
    if (sel.length>0 && sel.options[0].text==fillerEntry) {
        sel.options[0]=null;
    } 
}

// sort options
function sortOptions(select) {
    // i can't think of a good way to do this right now...
}

function removeSelected(sel) {
    // get all the selected items and add them to the selected list
    var opts = sel.options; 
    var i=0;
    while(i<opts.length) {
        if (opts[i].selected) {
	    opts[i] = null;
        } else {
	    i++;
        }
    }  
}

function addOption(to,text,value) {
    to[to.length] = (value) ? new Option(text,value) : new Option(text);
    to[to.length-1].selected = true;
}

function addTextOption(destselect, text, selected) {
  var destopts = destselect.options;
  var x = selectIndexOf(destselect, text);
  if (x == -1) {
    // add it, selected
    var ins = destopts.length; 
    destopts[ins] = new Option(text);
    destopts[ins].selected = selected;
  } else {
    // already there, select it
    destopts[x].selected = selected;
  }
}

// if it already existed, returns the index of the option selected, otherwise -1
function addValueOption(destselect, text, value, selected, duplicatesok) {
  var destopts = destselect.options;
  var x = selectIndexOfValue(destselect, value);
  if (x == -1 || duplicatesok) {
    // add it, selected
    var ins = destopts.length; 

    // make sure we have a value
    if (!value) value = text;

    destopts[ins] = new Option(text, value);
    destopts[ins].selected = selected;

    return -1;

  } else {
    // already there; select it
    destopts[x].selected = selected;
    return x;
  }
}


function moveSelected(from,to) {
  var opts = from.options;
  var i=0; 
  var ret = false;

  selectNone(to);

  while (i<opts.length) {
    if (opts[i].selected) {
      addOption(to.options,opts[i].text,opts[i].value);
      opts[i]=null;
      ret = true;
    } else {
      i++;
    }
  }
  
  return ret;
}


function copySelected(srcselect, destselect, duplicatesok) {

  if (!srcselect || !destselect) return;

  var srcopts = srcselect.options;
  var destopts = destselect.options;

  selectNone(destselect);  // we're going to modify the selection, so start clean

  for (var i=0; i<srcopts.length; i++) {
    if (srcopts[i].selected) {
      addValueOption(destselect, srcopts[i].text, srcopts[i].value, true, duplicatesok);
    }
  }
}




function selShiftUp(opts) {

  var tmpvalue,tmptext,tmpsel,tmpcls,tmpstyle,tmpopt;
  var ret = false;
  
  // start at one, because you can't move the top one up...
  for(var i=1; i<opts.length; i++) {
    if (opts[i].selected) {

      if (opts[i].selected && opts[i-1].selected) continue; // don't bother switching them...

      // swap them; can't just swap entire Option objects; it doesn't
      // work right.  [shep 26.Sep.2006]
      tmptext  = opts[i].text;
      tmpvalue = opts[i].value;
      tmpsel   = opts[i].selected;
      tmpcls   = opts[i].className;
      tmpstyle = opts[i].style.cssText;

      opts[i].text = opts[i-1].text;
      opts[i].value = opts[i-1].value;
      opts[i].selected = opts[i-1].selected;
      opts[i].className = opts[i-1].className;
      opts[i].style.cssText = opts[i-1].style.cssText;
      opts[i-1].text = tmptext;
      opts[i-1].value = tmpvalue;
      opts[i-1].selected = tmpsel;
      opts[i-1].className = tmpcls;
      opts[i-1].style.cssText = tmpstyle;

      ret = true;
    }
  }
  
  return ret;

}

function selShiftDown(opts) {
  var tmpvalue,tmptext,tmpsel,tmpcls,tmpstyle;
  var ret = false;

  // start at length-2, because you can't move the bottom one down...
  for(var i=(opts.length-2); i >= 0; i--) {
    if (opts[i].selected) {

      if (opts[i].selected && opts[i+1].selected) continue; // don't bother switching them...

      // swap them
      tmptext  = opts[i].text;
      tmpvalue = opts[i].value;
      tmpsel   = opts[i].selected;
      tmpcls   = opts[i].className;
      tmpstyle = opts[i].style.cssText;

      opts[i].text = opts[i+1].text;
      opts[i].value = opts[i+1].value;
      opts[i].selected = opts[i+1].selected;
      opts[i].className = opts[i+1].className;
      opts[i].style.cssText = opts[i+1].style.cssText;
      opts[i+1].text = tmptext;
      opts[i+1].value = tmpvalue;
      opts[i+1].selected = tmpsel;
      opts[i+1].className = tmpcls;
      opts[i+1].style.cssText = tmpstyle;
      ret = true;
    }
  } 
  
  return ret;
}


// make sure we always have the form we need to work with
/* if (!document.urloverride) { */
/*   document.write('<form name="urloverride" method="post" action="/servlet/tsi.Submit" target="_top"><input type="hidden" name="urloverride" value=""></form>'); */
/* } */
function nav(url) {
  if (url && url.length > 2020 && document.urloverride) {
    document.urloverride.urloverride.value = url;
    document.urloverride.submit();
  } else {
    window.location.href = url;
  }
  return false;
}


// this removes any leading space and [[ from the rs expression (see Server3432)
function cleanrs(exp) {
  var re = /\s*\[*(.*)/;
  return exp.replace(re, "$1");
}

function unescape_fromattribute(str) {
  var dq = new RegExp( "&quot;", "g" );
  str = str.replace(dq, "\"");
  var lt = new RegExp( "&lt;", "g" );
  str = str.replace(lt, "\<");
  var gt = new RegExp( "&gt;", "g" );
  str = str.replace(gt, "\>");
  return str;
}

/**
 * Used to escape commas in any serialized data structure that
 * requires it.
 */
function escape_commas(str) {

  // escape escapes
  var bs = new RegExp( "\\\\", "g" );
  str = str.replace(bs, "\\\\");

  // escape commas
  var cm = new RegExp( ",", "g" );
  str = str.replace(cm, "\\,");
  
  return str;
}

/**
 * Used to ensure that there will be no problem setting this value as
 * the value of an attribute that will eventually have to be parsed
 * (by our xformer, or some other parser) in the form in which it is
 * set.  It is not necessary to use this in DOM transformations that
 * involve data that is never going to be treated as raw text/html.
 */
function escape_forattribute(str) {
  if (!str) {
    return str;
  }
  var dq = new RegExp( "\"", "g" );
  str = str.replace(dq, "&quot;");
  var lt = new RegExp( "\<", "g" );
  str = str.replace(lt, "&lt;");
  var gt = new RegExp( "\>", "g" );
  str = str.replace(gt, "&gt;");
  return str;
}

function escape_char(str, c) {
  if (str == null) return null;
  var bs = new RegExp( "\\\\", "g" );
  str = str.replace(bs, "\\\\"); // escape escapes
  var cm = new RegExp( c, "g" );
  str = str.replace(cm, "\\"+c); // escape char
  return str;
}
function unescape_char(str, c) { 
  if (str == null) return null;
  var cm = new RegExp( "\\\\"+c, "g" );
  str = str.replace(cm, c); // unescape char
  var bs = new RegExp( "\\\\\\\\", "g" );
  str = str.replace(bs, "\\"); // unescape escapes
  return str;  
}

/**
 * Used to escape single quotation marks in the literal portions of
 * rapid selector expressions.
 *
 * Verified: [ajm 29.Sep.2005]
 */
function escape_singlequotes(str) {
  return escape_char(str, "'");
}
function unescape_singlequotes(str) {
  return unescape_char(str, "'");
}

// ----------------------------------------------------------------------
// these functions should be preferred over places where we would use
// escape() and unescape().  [ajm 29.Sep.2003]

function encode_url(url) {
  try {
    return encodeURI(url); // not IE 5
  } catch (xcp) {
    return escape(url);
  }
}
function decode_url(url) {
  try {
    return decodeURI(url); // not IE 5
  } catch (xcp) {
    return unescape(url);
  }
}

function encode_url_parameter(url) {
  try {
    return encodeURIComponent(url); // not IE 5
  } catch (xcp) {
    return escape(url);
  }
}
function decode_url_parameter(url) {
  try {
    return decodeURIComponent(url); // not IE 5
  } catch (xcp) {
    return unescape(url);
  }
}

// ----------------------------------------------------------------------


// ----------------------------------------------------------------------
// drag and drop of traction ids and other urls
// --------------------------------------------------

function tid_parseurl(url) {
  var insert = '';
  if (url && ('http://' == url.substring(0,7) ||
	      'https://' == url.substring(0,8))) {
    var match = url + '&'; // this will make parsing easier

    if (match.search( /proj=([^&]*)&/ ) != -1) {
      insert = RegExp.$1;

      if (match.search( /rec=([^&]*)&/ ) != -1) {
	insert += RegExp.$1;
      } else {
	insert = '';
      }
    }

    if (!insert) {
      // test for permalink
      if (match.search( /\/permalink\/([^?&]*)/ ) != -1) {
	insert = RegExp.$1;
      }
    }

    // make sure it's not url encoded
    if (insert) {
      insert = decode_url_parameter(insert);
    }

    // allow urls to be dropped as they are
    if (!insert) {
      insert = url;
    }

  }
  
  return insert;
}

function tid_fillrange(range, insert) {
    if (range != null) {
      // add a space to the id that we're dropping for Server6546 [ajm 19.Mar.2003]
      insert += ' ';

      range.text = insert;
    
      if (range.scrollIntoView) {
	// we may have added it to the end and we
	// want to make sure that the user can see it
	range.scrollIntoView();
      }
    
      // it seems right to give it focus
      window.focus();

      // this isn't acutally necesary, but the next moveStart line of
      // code seems strange, so this should just ensure that what we're
      // doing is right
      range.collapse(false);

      // force the range to surround the text, because setting the text
      // somehow doesn't include the range by default
      range.moveStart('character', -insert.length);
    
      // select the text that we just added
      range.select();
    }  
}


function tid_setup_drop(defaultDropArea) {
  tid_defaultDropArea = defaultDropArea;

  document.body.ondragstart = tid_ondragstart;
  //document.body.ondragenter = tid_ondragenter;
  document.body.ondragover  = tid_ondragover;
  document.body.ondragend   = tid_ondragend;
  document.body.ondrop      = tid_ondrop;
}

var tid_defaultDropArea;
var tid_leaveAlone = false;

function tid_ondragstart() {
  // if we start this here, we don't want to do anything special
  tid_leaveAlone = true;
  event.dataTransfer.effectAllowed = 'move';
  return true;
}

function tid_ondragend() {
  tid_leaveAlone = false;
  return true;
}


//function tid_ondragenter() {
  //  event.dataTransfer.dropEffect = 'move';
  //  return false;
//}

function tid_ondragover() {
  return tid_leaveAlone;
}


function tid_after_drop() {
}

function tid_ondrop() {
  var ignored = tid_ondrop_impl();
  tid_after_drop();
  return ignored;
}

function tid_ondrop_impl() {

  if (tid_leaveAlone) return true;

  event.dataTransfer.dropEffect = 'move';

  // figure out what we dropped and what, as a result, we want to insert
  var url = event.dataTransfer.getData('text');
  var insert = tid_parseurl(url);

  if (!insert) return true;

  // our general strategery is as follows: 
  //
  // 1. check to see if window.event exists.  if it does not, assume
  // that we must drop the text at the end of the entry (selected if
  // possible).
  //
  // 2. if it does, get the drop point from the event (window.event.x,
  // window.event.y) and use it to identify the element usint
  // document.elementFromPoint.  check to see if this point is in the
  // text area.  if it is, we'll try to drop it at that location.
  //
  // 3. use textarea.createTextRange to create the text range.  move
  // it to the point that we're going to drop it (either using
  // textrange.moveToPoint or textrange.move("textedit", 1))
  //
  // 4. set the text of our range to our traction id
  // 
  // 5. select it so that it can be easily moved if necessary
  //

  var droppedAtPoint = false;
  var range = null;

  if (window.event && document.elementFromPoint) {

    var droppedOnElement = document.elementFromPoint(window.event.x, window.event.y);

    // need to make sure droppedOnElement is not null, but also want
    // to make sure that we drop these on reasonable elements.  you
    // should see what happens when we drop these on buttons
    // (isTextEdit is not enough).  
    //
    // to make this generic, we'll look for the dropTractionId=true
    // attribute.  setting the dropTractionId="true" attribute on the elements
    // that we want to allow direct drops
    if (droppedOnElement) {
      var attrs = droppedOnElement.attributes;
      if (attrs) {
	for (var i=0; i < attrs.length; i++) {
	  var curattr = attrs.item(i);
	  if (curattr.nodeName == 'dropTractionId') {

	    if (curattr.nodeValue == 'true') {
	      
	      range = droppedOnElement.createTextRange();
	      range.moveToPoint(window.event.x, window.event.y);
	      
	      droppedAtPoint = true;
	    }
	    
	    break;
	  }
	}
      }
    }
  }
  
  if (!droppedAtPoint && tid_defaultDropArea) {
    range = tid_defaultDropArea.createTextRange();
    range.move('textedit', 1);
  }

  tid_fillrange(range, insert);

  return false;
}

//////////////////////////////////////////////////////////////////////
//
// TractionId drag/drop for TinyMCE
//
// This is deprecated! See js/traction/edit/tinymce/TinyMce.js [shep 16.Nov.2009]
//

var tid_TinyMCE_leaveAlone = false;

function tid_TinyMCE_setup(editor_id, body, doc) {
  try {
    var dragdrop = function() {
      var event = document.frames[editor_id].event;
      event.target = event.srcElement;
      event.target.editor_id = editor_id;
      return tid_TinyMCE_dragdrop(event);
    }
		  
    doc.body.ondragend = dragdrop;
    doc.body.ondragover = dragdrop;
    doc.body.ondragenter = dragdrop;
    doc.body.ondrop = dragdrop;

  } catch (e) {
    alert(e);
  }
}
function tid_TinyMCE_dragdrop(event) {
  var ret = tid_TinyMCE_dragdrop_(event);
  Debug.println(event.type, " - ", ret);
  return ret;
}
function tid_TinyMCE_dragdrop_(event) {

  switch (event.type) {
  case "dragstart":
    tid_TinyMCE_leaveAlone = true;
    event.dataTransfer.effectAllowed = 'move';
    return true;

  case "dragover":
    return tid_TinyMCE_leaveAlone;

  case "dragend":
    tid_TinyMCE_leaveAlone = false;
    return true;

  case "drop":
    if (tid_TinyMCE_leaveAlone) return true;

    // parse the dropped content
    var dropped = event.dataTransfer.getData('text');
    var insert = tid_parseurl(dropped);

    // see if we found anything interesting
    if (!insert) return true;
    
    // make sure this isn't an image (why can't we see the HTML?)
    if (insert.match(/\.(gif|jpg|png|tiff)/)) return true;

    // create a range at the mouse location
    var mydocument = document.getElementById(event.target.editor_id).contentWindow.document;    
    var range = mydocument.body.createTextRange();
    range.moveToPoint(event.x, event.y);
    
    // fill the range
    tid_fillrange(range, insert);

    tid_TinyMCE_leaveAlone = false;
    return false;
  }
}

//////////////////////////////////////////////////////////////////////
//
// helpbox for context sensitive help
//

function helpboxsz(width, height, key) {
  var anchor = "";
  var pound = key.indexOf('#');
  if (pound != -1) {
    anchor = key.substring(pound);
    key = key.substring(0,pound);
  }

  var url = FORM_ACTION_READ_ONLY + "?type=helpbox&key="+key;

  // any any arguments
  for (var i=3; i<arguments.length; i++) {
    url += "&a" + (i-3) + "=" + encode_url_parameter(arguments[i]);
  }

  // add the anchor
  url += anchor;

  window.open(url, "", "width="+width+", height="+height+", scrollbars=yes, titlebar=no, resizable=yes, alwaysRaised=yes, menubar=no, location=no, status=no, toolbar=no, directories=no");
  
}

function helpbox(key) {
  var args = new Array();
  args[0] = 400;
  args[1] = 500;
  for (var i=0; i<arguments.length; i++) {
    args[i+2] = arguments[i];
  }

  helpboxsz.apply(null, args);
}

var SRC_MIN = "/images/modern/icons/hide.gif";
var SRC_MAX = "/images/modern/icons/show.gif";

/**
 * Shows/hides the entry with the given Traction ID.
 * @param id TractionId of the entry that is to be shown/hidden.  The
 * argument for this parameter MUST be URL encoded (to accomodate
 * Japanese project names).
 */
function sh_entry(id) {
  try {
    var img = document.getElementById("sh_i"+id.toLowerCase());
    var link = document.getElementById("sh_l"+id.toLowerCase());
    var table = document.getElementById("sh_t"+id.toLowerCase());
  
    var src = img.src;
    var css = src.indexOf('://');
    if (css != -1) {
      var s = src.indexOf('/', css+3);
      src = src.substring(s);
    }

    if (src == SRC_MIN) {
      if (typeof(itemMenu) != "undefined" && itemMenu != null) {
	itemMenu.hide();
      }
      img.src = SRC_MAX;
      showtable(false,table);
      showtable(true,link);
    }
    else {
      img.src = SRC_MIN;
      showtable(true,table);
      showtable(false,link);
    }
  } catch (xcp) {
    alert(xcp);
  }
}

function toggle(checkbox) {
  checkbox.checked = !checkbox.checked;
}


/**
 * Mozilla based browsers will perform a form submission even if the window
 * containing the form has been closed via window.close().  Internet Explorer,
 * however, does not consistently do this.  Therefore, when a form submission
 * and a window close must be triggered by a single event, we must perform the
 * submit and then close the window when the BODY element's onunload event is
 * triggered.  This flag keeps track of whether a submit has been triggered so
 * that the closeIfReady() function called for the BODY's onunload event knows
 * whether or not to close the window.
 */
var readyToClose = false;

function closeIfReady() {
  if (readyToClose) {
    window.close();
  }
}

/**
 * Navigates the current view to the given URL, replacing the URL's proj
 * parameter value with the value of the selected option in the given SELECT
 * element.
 * projectselector - The DOM object for the SELECT element whose value should
 *   be used for the new value of the URL proj parameter.
 * sampleProjectURL - The template URL that should be used to navigate the view,
 *   after replacing the URL proj parameter's value with the value from the
 *   given SELECT element.
 */
function switchProjects(projectselector, projectURLTemplate) {

  Debug.println("switchProjects(projectselector(tagName/name/id): " +
	(projectselector != null ? (projectselector.tagName + "/" + projectselector.name + "/" + projectselector.id) : "(null)") + ", " +
	"projectURLTemplate: '" + (projectURLTemplate != null ? projectURLTemplate : "(null)") + "')");

  if (projectselector == null || projectselector.tagName.toLowerCase() != "select" || projectURLTemplate == null) {
    return false;
  }

  var projMarker;
  if (projectURLTemplate.toLowerCase().indexOf("{p}") != -1) {
    projMarker = new RegExp( "\\{p\\}", "gi" );
  } else if (projectURLTemplate.toLowerCase().indexOf("%7bp%7d") != -1) {
    projMarker = new RegExp( "%7bp%7d", "gi" );
  } else {
    // No marker to replace.
    return false;
  }

  var projName = new String(projectselector[projectselector.selectedIndex].value);
  var URL = new String(projectURLTemplate + '&');
  URL = URL.replace(projMarker, encode_url_parameter(projName));
  URL = URL.substring(0, URL.length - 1); // trim the trailing ampersand.

  Debug.println("switchProjects final URL:\n\"" + URL + "\"");

  document.location.href = URL;

}

// ----------------------------------------------------------------------
// email address completer
//

var EAC_LEFT = 1;
var EAC_RIGHT = 2;
var EAC_ALIGNMENT = EAC_LEFT;
var EAC_TYPE_COMPLETER = "emailcompleter";
var eac_Element;
var eac_DIV;
var eac_TABLE;
var eac_TBODY;
var eac_TDs = new Array();
var eac_hits = new Array();
var eac_count = 0;
var eac_selected = 0;
var eac_last;
var eac_prefix = "";
var eac_suffix = "";
var eac_lastonsubmit = null;
var eac_HighlightMatches = true;
var eac_xmlget = null; // instance to the previous request so we can cancel it
var EAC_SEND_EMPTY = false;
var eac_Extras = null;

function eac_setup(formElement, skipProgress) {
  if (formElement) {
    formElement.onkeydown  = eac_onkeydown;
    formElement.onkeyup    = eac_onkeyup;
    formElement.onkeypress = eac_onkeypress;
    formElement.onblur     = eac_onblur;
    formElement.autocomplete = "off";
    formElement.setAttribute("AutoComplete", "off");

    // add a throbber
    if (!skipProgress) {
      createThrobberForField(formElement);
    }
  }
}

function noop() {}

function eac_disable(formElement) {
  if (formElement) {
    formElement.onkeydown  = noop;
    formElement.onkeyup    = noop;
    formElement.onkeypress = noop;
    formElement.onblur     = noop;
    formElement.autocomplete = "on";
    formElement.setAttribute("AutoComplete", "on");
  }
}

// by cancelling up/down, we don't get strange behavior where cursor
// goes to the beginning.  by cancelling enter, we don't get form
// submits.  safari still tries to submit the form, but changing the
// onsubmit seems to fix that.
function eac_onkeypress(event) {
  var code = keyCode2(event);
  switch (code) {
  case 9:
    // we accept on keypress because onkeyup fires in the next cell
    eac_accept(eac_selected);
    return;
  case 13: // enter
  case 38: // up
  case 39: // down
    return false;
  }
}

function eac_scrub(formElement) {
  var str = trim(formElement.value);
  while (str.charAt(str.length-1) == ';' || str.charAt(str.length-1) == ',') {
    str = str.substring(0,str.length-1);
  }
  formElement.value = str;
}

var eac_disable_onblur = false;

function eac_div_focus(event) {
  Debug.println("eac_div_focus");
  eac_show();
  eac_Element.focus();
}

function eac_onblur(event) {
  Debug.println("eac_onblur");
  try {
    if (!eac_disable_onblur) {
      eac_abort();
      eac_scrub(this);
      eac_hide();
    }
  } catch (e) {}
}

var eac_selected_onkeydown = 0;

function eac_onkeydown(event) {

  eac_selected_onkeydown = eac_selected;
  
  // we only operate on one text input
  eac_Element = this;

  // remember the last value
  eac_last = eac_Element.value;

  // preserve onsubmit, but don't let safari fire on return
  eac_lastonsubmit = document.fm.onsubmit;
  document.fm.onsubmit = function() { return false; }

}

function eac_onkeyup(event) {

  // we only operate on one text input
  eac_Element = this;

  // preserve onsubmit
  document.fm.onsubmit = eac_lastonsubmit;

  var debugSelect;

  // handle up, down, enter
  var code = keyCode2(event);
  switch (code) {
  case 13: // enter
    eac_accept(eac_selected);
    return false;
  case 59: // ;
//   case 188: // ,  -- not good for AD
//   case 32: // space  -- ditto
    //case 9:  // tab  -- doesn't work right
    eac_accept(eac_selected);
    return;
  case 37: // left
  case 39: // right
  case 27: // escape
    eac_hide();
    return;
  case 38: // up
    if (eac_selected_onkeydown > 0) {
      eac_selected = eac_selected_onkeydown - 1;
    }
    eac_select(eac_selected, true);
    return;
  case 40: // down
    if (eac_selected_onkeydown < eac_TDs.length - 1) {
      eac_selected = eac_selected_onkeydown+1;
    }
    eac_select(eac_selected, true);
    return;
  default: 
    Debug.println("eac_onkeyup:  code = " + code);
  }

  var mycount = ++eac_count;

  if (eac_DIV) {
    eac_DIV.style.display = "none";
  }

  // interrupt anything in process (it will be old anyway)
  eac_abort();

  if (trim(eac_Element.value).length > 0 || EAC_SEND_EMPTY) {

    // sleep for long enough that we don't slow up typing
    setTimeout(function() {
		 eac_onkeyup_wakeup(mycount, debugSelect);
	       },
	       250);

  }
}

// q.lastIndexOf(';', insertionIndex-1);
function eac_break_start(q, insertionIndex) {
  for (var i=insertionIndex-1; i>=0; i--) {
    switch (q.charAt(i)) {
    case ';':
//     case ',':
      return i;
    }
  }
  return -1;
}

// q.indexOf(';', insertionIndex);
function eac_break_end(q, insertionIndex) {
  for (var i=insertionIndex; i<q.length; i++) {
    switch (q.charAt(i)) {
    case ';':
//     case ',':
      return i;
    }
  }
  return -1;
}

function eac_onkeyup_wakeup(mycount, debugSelect) {
  if (eac_count == mycount) {
    // noone requested since, show completions

    // kill any previous request
    eac_abort();

    // Using this here because it supports a more robust binding
    // mechanism than the HTML attribute method employed by the
    // triggerCustomEvent code used elsewhere in this autocompleter
    // code.
    if (typeof(Events) != "undefined") {
      Events.fireCustom("eacbeforerequest");
    }

    // figure out exactly what to send to the server.  we only want to
    // complete what they're currently typing.  we need a reliable way
    // to know where they are in the value and then take everything from
    // that point back the last ; or beginning if there is none.
    //
    // since selection support has varied between browsers, we opt for
    // the simple method of comparing the last value to the current
    // and finding their first difference.  note that if we switched
    // to using a textarea (like gmail) we could probably handle this
    // better.  we could also more easily see a large list of email
    // addresses
    //
    var insertionIndex = eac_get_insertion(eac_Element);
    var q = eac_Element.value;

    var start = eac_break_start(q, insertionIndex);
    var end = eac_break_end(q, insertionIndex);

    // save prefix / suffix for completion
    if (start < 0) {
      start = 0;
      eac_prefix = "";
    }
    else {
      start += 1; // past ;
      eac_prefix = trim(q.substring(0, start)) + " "; // pad
    }

    if (end < 0) {
      end = q.length;
      eac_suffix = "";
    }
    else {
      eac_suffix = trim(q.substring(end+1));
    }

    // only search the remaing part
    q = trim(q.substring(start, end));

    /*     document.fm.addrbcc.value = q; */

    Debug.println("Extras: " + eac_Extras);

    if (q != "" || EAC_SEND_EMPTY) {
      var url = FORM_ACTION_READ_ONLY + "?type="+EAC_TYPE_COMPLETER+"&q="+encode_url_parameter(q);
      if (eac_Extras != null && eac_Extras != "") {
	url += eac_Extras;
      }

      // fire new request
      var throbber = (eac_Element.nextSibling != null && eac_Element.nextSibling.nodeName == "IMG") ? eac_Element.nextSibling : null;
      eac_xmlget = xmlget_async(url, throbber, (throbber == null), eac_callback, [ eac_Element, debugSelect ], -1);
    }
  }
}

function eac_abort() {
    if (eac_xmlget != null && eac_xmlget.readyState == 1) {
      try {
	Debug.println("abort()");

	eac_xmlget.abort();
	eac_xmlget = null;
      } catch (e) {
	alert(e);
      }
    }
}

function eac_callback(responseText, callbackarg) {
  // restore the global (in case we started two at the same time)
  eac_Element = callbackarg[0];
  if (responseText == null) {
    // failed, stop trying -- disable completion
    eac_disable(eac_Element);
  } else {
    // show the resutls
    var hits = eval(responseText);
    if (hits && hits != null) {
      eac_show_results(hits, callbackarg[1]);    
    }
  }
}

function eac_show_results(hits, debugSelect) {

  if (debugSelect) {
    removeAll(debugSelect);
    var opts = debugSelect.options;

    for (var i=0; i<hits.length; i++) {
      var cur = hits[i];  // [ match, email, complete ]
      
      var text = (cur[1]
		  ? cur[1] + " ("+cur[2]+")"
		  : cur[2]);

      opts[opts.length] = new Option(text, cur[1]);
    }
  }
  
  if (!eac_DIV) {
    var div = document.createElement("DIV");
    
    div.id = "eac_DIV";
    div.style.display = "none";
    div.style.padding = "0px";
    if (EAC_ALIGNMENT == EAC_RIGHT) {
      div.style.borderTop = "1px solid #ccc";
    }
    div.style.borderLeft = "1px solid #999";
    div.style.borderRight = "1px solid #333";
    div.style.borderBottom = "1px solid #333";
    div.style.backgroundColor = "#eee";
    div.style.position = "absolute";
    div.style.zIndex = "100";
    div.style.fontSize = "11px";
    div.style.fontFamily = "Verdana, Arial, Helvetica, san-serif";
    div.style.overflow = "auto";
//     div.style.height = "172px";

    // by using a table, sizing comes free
/*     div.style.width = "400px"; */
/*     div.style.height = "100px"; */

    var table = document.createElement("TABLE");
    var tbody = document.createElement("TBODY");
    var tr = document.createElement("TR");
    var td = document.createElement("TD");

    tr.appendChild(td);
    tbody.appendChild(tr);
    table.appendChild(tbody);
    div.appendChild(table);
//     table.style.borderSpacing = "2px";
//     table.style.padding = "0";

    document.body.appendChild(div);

    div.onfocus = eac_div_focus;

    // we'll need these later
    eac_DIV = div;
    eac_TABLE = table;
    eac_TBODY = tbody;
  }

  // position for this field
  if (EAC_ALIGNMENT == EAC_RIGHT) {

    Debug.println(client_width(document.body)+" - "+getpos(eac_Element, "offsetLeft")+" - "+outer_width(eac_Element)+" = "+px(client_width(document.body) - getpos(eac_Element, "offsetLeft") - outer_width(eac_Element)));

    var right = client_width(document.body) - getpos(eac_Element, "offsetLeft") - outer_width(eac_Element);
    if (right < 0) {
      right = 8;
    }
    eac_DIV.style.right = px(right);
  } else {
    eac_DIV.style.left = px(getpos(eac_Element, "offsetLeft"));
  }
  eac_DIV.style.top = px(getpos(eac_Element, "offsetTop") + outer_height(eac_Element));

  if (hits.length > 0) {
    // create a fresh tbody
    eac_TABLE.removeChild(eac_TBODY);
    eac_TBODY = document.createElement("TBODY");
    eac_TABLE.appendChild(eac_TBODY);

    eac_TDs.length = 0;

    var searchText = eac_Element.value.toLowerCase();
    var searchLen  = searchText.length;

    var text = "";
    for (var i=0; i<hits.length; i++) {

      // new TR, TD
      var tr = document.createElement("TR");
      var td = document.createElement("TD");

      eac_TDs[i] = td;
      eac_hits[i] = hits[i];

      if (i==0) {
	eac_select(i, true);
      }

//       td.style.paddingRight = "4px";
      td.style.fontSize = "11px";
      td.style.fontFamily = "Verdana, Arial, Helvetica, san-serif";
      td.style.cursor = ua("css_cursor_property_pointer_value", "pointer");

      td.onmouseover = eac_row_onmouseover;
      td.onmousedown = eac_row_onmousedown;
      td.onclick = eac_row_onclick;

      var finalDisplayText;

      if (eac_HighlightMatches && searchLen > 0) {

	var displayText = hits[i][2];
	finalDisplayText = "";
	var matchText = displayText.toLowerCase();
	var lastMatch = matchText.indexOf(searchText);
	while (lastMatch != -1) {
	  finalDisplayText += displayText.substring(0, lastMatch) + "<b>" +
	    displayText.substring(lastMatch, lastMatch + searchLen) + "</b>";
	  displayText = displayText.substring(lastMatch + searchLen);
	  matchText = displayText.toLowerCase();
	  lastMatch = matchText.indexOf(searchText);
	}
	finalDisplayText += displayText;

      } else {

	finalDisplayText = hits[i][2];

      }

      // set the content
      td.innerHTML = finalDisplayText

      // add them in
      tr.appendChild(td);
      eac_TBODY.appendChild(tr);
    }

    eac_show();
  }
}

function eac_select(index, scrollIntoView) {
  // clear all
  for (var i=0; i<eac_TDs.length; i++) {
    eac_TDs[i].style.backgroundColor = "#eee";
  }
  // select current
  if (index < eac_TDs.length) {
    eac_TDs[index].style.backgroundColor = "#b5d5ff";
    if (scrollIntoView) {
      eac_TDs[index].scrollIntoView(false);
    }
  }
  eac_selected = index;
}

function eac_show() {
  eac_DIV.style.height = "";
  eac_DIV.style.display = "";

  eac_DIV.style.overflow = "hidden";
  eac_DIV.style.overflow = "auto";


  triggerCustomEvent("eacshow");
  setTimeout(function() {
	       if (outer_height(eac_DIV) > 172) {
		 eac_DIV.style.height = "172px";
	       }
	     }, 10);
}

function eac_hide() {
  if (eac_DIV) {
    eac_DIV.style.display = "none";
  }
  triggerCustomEvent("eachide");
}

function eac_accept(index) {
  if (eac_DIV != null && eac_DIV.style.display != "none") {
    eac_Element.value = eac_prefix + eac_hits[index][1]+"; " + eac_suffix;
    eac_hide();
  }
}

function td2index(td) {
  var children = eac_TBODY.childNodes;
  for (var i=0; i<children.length; i++) {
    if (children[i].firstChild == td) {
      return i;
    }
  }  
  return 0;
}

/**
 * last = cjn@tra
 * cur  = cjn@trac
 * returns 8 (cur.length)
 *
 * last = cjn@tra
 * cur  = cj@tra
 * returns 2
 */
function eac_get_insertion(formElement) {
  var last = eac_last;
  var cur = formElement.value;

  if (!last) last = "";

  var ret = cur.length;

  for (var i=0; i<cur.length && i<last.length; i++) {
    if (cur.charAt(i) != last.charAt(i)) {
      ret = i;

      if (cur.length > last.length) {
	ret += 1;
      }
      break;
    }
  }

  //  var x = "";
  //  for (var i=0; i<ret; i++) {
  //    x += "*";
  //  }
  //  document.fm.addrcc.value = x;
  
  return ret;
}

function eac_row_onmouseover() {
  eac_select(td2index(this), false);
}

function eac_row_onmousedown() {
  eac_disable_onblur = true;
}

function eac_row_onclick() {
  eac_accept(td2index(this));
  eac_scrub(eac_Element);
  eac_disable_onblur = false;
}

function getStyle(elm, styleName, styleMember) {
  var ret = "";

  if (window.getComputedStyle) {
    ret = window.getComputedStyle(elm,null).getPropertyValue(styleName);
  } else if (elm.currentStyle) {
    ret = eval("elm.currentStyle." + (styleMember ? styleMember : styleName));
  }

  return ret;
}

// measure = "offsetLeft" or "offsetTop"
function getpos(formElement,measure) {
  var ret = 0;
  var lastStatic = 0;
  var afterStatic = 0;

  while (formElement) {
//      Debug.println(formElement.tagName+"("+formElement.id+")"+"["+measure+"]="+formElement[measure]+" ("+getStyle(formElement,"position")+")");
    ret += formElement[measure];

    if (lastStatic == 0 && 
	measure == "offsetLeft" &&
	getStyle(formElement, "position") != "relative" && 
	formElement.offsetParent && 
	getStyle(formElement.offsetParent, "position") == "relative") {

      // we're not relative but our parent is.  check for IE5.5 - IE6
      // bug (fixed in IE7) [ajm 01.May.2006]

      lastStatic = formElement[measure];
      afterStatic = 0;
    } else {
      afterStatic += formElement[measure];
    }

    formElement = formElement.offsetParent;
  }

  // adjust for IE bug.  hope this doesn't happen when it shouldn't.
  // note that we only try for IE and IE7, which fixes the bug,
  // doesn't seem to be bothered by this check.
  if (isIE && lastStatic > afterStatic) {
     ret -= afterStatic;
//     Debug.println("using nasty trick to fix IE5.5/6");
  } else {
//     Debug.println("==================== "+ret+" != "+lastStatic+"*2 ====================");
  }

  return ret;
}

function getleft(formElement) { 
//   Debug.println("getleft("+formElement.tagName+"/"+formElement.id+")");
  return getpos(formElement,"offsetLeft"); 
}
function gettop(formElement) { return getpos(formElement,"offsetTop"); }

function client_width(container) {
  return (container.clientWidth > 0) ? container.clientWidth : window.innerWidth;
}
function outer_width(element) {
  return (element.style.width && !isNaN(element.style.width)) ? nopx(element.style.width) : element.offsetWidth;
}
function outer_height(element) {
  return (element.style.height && !isNaN(element.style.height)) ? nopx(element.style.height) : element.offsetHeight;
}
function nopx(size) {
  if (typeof(size) == "number") {
    return size;
  }
  return parseInt(size.replace("px",""));
}
function px(size) {
  return size+"px";
}

/**
 * Scales dimensions
 */
function scaleDimensions(realdim, max) {

  var realw = realdim[0];
  var realh = realdim[1];

  var maxw, maxh;
  if (typeof(max) == "object") {
    try {
      maxw = max[0];
      maxh = max[1];
    } catch (xcp) {
      return realdim;
    }
  } else {
    maxw = maxh = max;
  }
  if (realw <= maxw && realh <= maxh) {
    return realdim;
  }

  var usemaxw;
  if (maxw == maxh) {
    // When scaling both dimensions to the same max value, use the max
    // value for the width if the width dimension is larger than the
    // height dimension; otherwise, use the max value for the height.
    usemaxw = (realw >= realh);
  } else {
    // When scaling dimensions to different max values, use the max
    // value for the width if the width is larger compared to the max
    // width than the height is compared to the max height.
    usemaxw = ( ((100 * realw) / maxw) >= ((100 * realh) / maxh) );
  }

  var neww, newh;
  if (usemaxw) {
    neww = maxw;
    scale = neww / realw;
    newh = realh * scale;
    newh = Math.round(newh);
    Debug.println("scaleDimensions:  ",
		  "Setting w to use max width.  realw x realh = ", realw, " x ", realh, "; ",
		  "scale factor = ", scale, "px / in; ",
		  "scaled, w x h = ", neww, " x ", newh);
  } else {
    newh = maxh;
    scale = newh / realh;
    neww = realw * scale;
    neww = Math.round(neww);
    Debug.println("scaleDimensions:  ",
		  "Setting h to use max height.  realw x realh = ", realw, " x ", realh, "; ",
		  "scale factor = ", scale, " px / in; ",
		  "scaled, w x h = ", neww, " x ", newh);
  }

  return [neww, newh];
  
}


//////////////////////////////////////////////////////////////////////
// 
// debug functions
//

var debugging = false;

function debug_document() {
  if (window.top.debugWindow == null || window.top.debugWindow.closed) {
    window.top.debugWindow =
      window.open("",
                  "Debug",
                  "left=0,top=0,width=400,height=500,scrollbars=yes,"
                  +"status=yes,resizable=yes");
    // open the document for writing
    window.top.debugWindow.document.open();
    window.top.debugWindow.document.write('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">'+
					  "<html><head><title>Javascript Debugging Console</title><style>"+
					  // we need some hacker styling... 
					  "HTML, BODY { margin: 5px; padding: 0px; background-color: #000; color: #eee; font-family: monaco, lucida console, monospace; font-size: 8pt; }"+
					  "</style></head><body><span id=content></span><span id=bottom>&nbsp;</span></body></html>");
    window.top.debugWindow.document.close();
// </body></html>
  }
  return window.top.debugWindow.document;
}

function Debug_print() {
  if (debugging &&
      (this != null ? (typeof(this.enabled) != "undefined" ? this.enabled : true) : true)) {
    var doc = debug_document();
    var elm = doc.getElementById('content');

    for (var i=0; i<arguments.length; i++) elm.innerHTML += arguments[i];

    var bottom = doc.getElementById('bottom');
    if (bottom.scrollIntoView) bottom.scrollIntoView();
  }
}
function Debug_println() {
  if (debugging &&
      (this != null ? (typeof(this.enabled) != "undefined" ? this.enabled : true) : true)) {
    var doc = debug_document();
    var elm = doc.getElementById('content');

    var append = "[" + ((this != null && typeof(this.name) != "undefined" && this.name != null) ? this.name : "general") + "]&nbsp;";
    for (var i=0; i<arguments.length; i++) append += arguments[i];
    append += "<br>";
    elm.innerHTML += append;

    var bottom = doc.getElementById('bottom');
    if (bottom.scrollIntoView) bottom.scrollIntoView();
  }
}

function DebugWriter(writername, initenabled) {
  this.print   = Debug_print;
  this.println = Debug_println;
  if (writername != null && writername != "") {
    this.name = writername;
  }
  this.enabled = (typeof(initenabled) != "undefined") ? initenabled : true;
}

var Debug = new DebugWriter("general", false);
Debug.xmlhttp  = new DebugWriter("xmlhttp", false);
Debug.richtext = new DebugWriter("richtext", true);
Debug.edit     = new DebugWriter("edit", true);
Debug.comment  = new DebugWriter("comment", true);
Debug.file     = new DebugWriter("file", false);

var __startTimes = {
  __DEFAULT: new Date()
};

function startTimer(name) {
  var start = new Date();
  if (typeof(name) != "undefined" || name != null) {
    name = "__DEFAULT";
  }
  __startTimes[name] = start;
}

function stopTimer(name) {
  var stop = new Date();
  if (typeof(name) != "undefined" || name != null) {
    name = "__DEFAULT";
  }
  var start = __startTimes[name];
  if (typeof(start) == "undefined" && start == null) {
    start = __startTimes.__DEFAULT;
  }
  return stop - start;
}

//////////////////////////////////////////////////////////////////////
//
// custom event handlers
//
// the idea is to allow custom handlers to be specified in html and
// have them be processed during javascript
//
function triggerCustomEvent(eventname, targets) {
  if (targets) {
    // explicitly specified targets
    var handler = "on" + eventname;
    for (var i=0; i<targets.length; i++) {
      var attribute = targets[i].getAttribute(handler);
      if (attribute) {
	// this allows the "this" keyword to be used
	targets[i].custom_handler_function = new Function(attribute);
	targets[i].custom_handler_function();
      }
      else if (targets[i][handler]) {
	// "this" keyword already applies
	targets[i][handler]();
      }
    }  
  }
  else {
    // default to target all form elements
    triggerCustomEvent(eventname, document.getElementsByTagName("select"));
    triggerCustomEvent(eventname, document.getElementsByTagName("input"));
    triggerCustomEvent(eventname, document.getElementsByTagName("textarea"));
  }
}


function MessageFormat(str) {
  this.str    = str;
  this.format = MessageFormat_format;
}

function MessageFormat_format() {
  var newstr = this.str;
  for (var i = 0; i < arguments.length; i ++) {
    var s = "\\\{" + new String(i) + "\\\}";
    var r = new RegExp( s, "gi" );
    var val = (typeof(arguments[i]) != "undefined" && arguments[i] != null) ? arguments[i] : "";
    newstr = newstr.replace(r, val);
  }
  return newstr;
}

if (!Array.concat) {
  Array.concat = function() {
    var ret = new Array();
    var cur = 0;
    for (var i = 0; i < arguments.length; i ++) {
      var arr = arguments[i];
      if (arr != null) {
	for (var j = 0; j < arr.length; j ++) {
	  ret[cur] = arr[j];
	  cur ++;
	}
      }
    }
    return ret;
  };
}

var toggle_tip_original_ = 0;

function toggle_tips(elm) {
  try {

    if (elm) {

      var children = elm.getElementsByTagName("DIV");

      // first check their current state.  display == "none" for any
      // is hidden.
      var isHidden = false;
      for (var i=0; i<children.length; i++) {
	if (children[i].className == "tip" && children[i].style.display == "none") {
	  isHidden = true;
	  break;
	}
      }

      if (isHidden) {
	for (var i=0; i<children.length; i++) {
	  if (children[i].className == "tip" && children[i].style.display != "none") {
	    toggle_tip_original_ = i;
	    break;
	  }
	}
      }

      if (isHidden) {
	elm.style.overflow = "auto";
	elm.style.height = "115px";
      } else {
	elm.style.height = "auto";
      }

      for (var i=0; i<children.length; i++) {
	if (children[i].className == "tip") {
	  children[i].style.display = (isHidden) ? "block" : (i == toggle_tip_original_) ? "block" : "none";
	  if (i > 0) {
	    children[i].style.borderTopWidth = (isHidden) ? "1px" : "0px";
	  }
	}
      }

    }

  } catch (xcp) {}
}



// These variables functions are deprecated in favor of the functions
// in traction.js; see Traction.Edit.TinyMce.  They are provided for
// backwards compatibility. [shep 17.Nov.2008]


// -------------------------------------------------------------------
// shared rich text setup / init / cleanup functions

/**
 * When testDesignModeSupport executes, it sets the value of this to
 * true if the browser supports rich text editing.
 */
var isRichText = false;

/**
 * Tests whether designMode is really supported in this browser, in
 * case our useragent properties file considers support to be present
 * but it is not.
 */
function testDesignModeSupport() {
  var uastr = navigator.userAgent.toLowerCase();
  isSafari = (uastr.indexOf("safari") != -1);
  isKonqueror = (uastr.indexOf("konqueror") != -1);
  // Safari and Konqueror are not supported; also, browser must
  // support getElementById function and designMode.  Naturally, the
  // TinyMCE source code must also be included.
  if (typeof(TinyMCE_Engine) != "undefined" && document.getElementById && document.designMode) {
    isRichText = true;
  }
}

/**
 * Certain bugs or odd behaviors must be worked around by "cleaning
 * up" rich text content.  Also, the triggerSave function must be
 * called every time, so we may as well do that as part of the
 * cleanup.  This doesn't do anything if called for a non-rich text
 * situation, but it won't cause any problems, either.
 */
function richTextCleanup(containerID, saveQuiet) {
  Debug.richtext.println("Warning: the richTextCleanup function in shared.js is deprecated in favor of Traction.Edit.TinyMce.cleanUpAndSave in traction.js.");
  if (containerID != null && isRichText) {
    var container = document.getElementById(containerID);
    Debug.println("richTextCleanup:  Container id '", containerID, "' -&gt; ", container);
    if (container != null) {
      if (saveQuiet) {
	triggerQuietSave(tinyMCE.getInstanceById(containerID));
      } else {
	var doc = container.contentWindow.document;
	if (doc != null) {
	  cleanUpLICloseTags(doc);
	  cleanUpTrailingBRs(doc);
	  tinyMCE.triggerSave(true, true);
	}
      }
    }
  }
}


function triggerQuietSave(inst) {
  Debug.richtext.println("Warning: the triggerQuietSave function in shared.js is deprecated in favor of Traction.Edit.TinyMce.triggerQuietSave in traction.js.");
  inst.formElement.value = inst.getBody().innerHTML;
}

/**
 * Gecko-based browsers have the most annoying habit of inserting one
 * or more extra BR tags at the ends of blocks without the user having
 * asked for them.  This happens in Thunderbird and other Gecko-based
 * mailers, and is therefore a problem inherent in the rich text
 * editing implementation, and not a property of TinyMCE.
 */
function cleanUpTrailingBRs(doc) {

  Debug.richtext.println("Warning: the cleanUpTrailingBRs function in shared.js is deprecated.");

  var bodyHTML = new String(doc.body.innerHTML);

  // This handles the case where no block level tags are present
  // around the content.
  bodyHTML = bodyHTML.replace(/(\s*<br>\s*)+$/gi, "");
  // This handles all other block node cases, such as P tags, PRE
  // tags, etc., that end with one or more unnecessary BR tags.
  bodyHTML = bodyHTML.replace(/(\s*<br>\s*)+<\/(p|pre|h1|h2|h3|h4|h5|h6|div)>/gi, "</$2>");

  doc.body.innerHTML = bodyHTML;

}

/**
 * Determines whether there is any non-whitespace, non-markup content
 * in the given string.
 * @param str the string to scan.
 * @return true if the string is composed of only whitespace and tags;
 * false if the string is null or has non-whitespace, non-markup
 * content.
 */
function isHTMLWhiteSpace(str) {
  Debug.richtext.println("Warning: the isHTMLWhiteSpace function in shared.js is deprecated in favor of Traction.Edit.TinyMce.isHTMLWhiteSpace in traction.js.");
  if (str != null) {
    // harden the type to String
    var newstr = new String(str);
    // replace IMG tags with some literal
    newstr = newstr.replace(/<img[^>]+>/gi, "IMG");
    // remove tags
    newstr = newstr.replace(/<[^>]+>/g, "");
    // remove &nbsp; entities
    newstr = newstr.replace(/(&nbsp;)/g, "");
    // remove whitespace
    newstr = newstr.replace(/\s+/g,"");
    // is the string empty?
    return (newstr == "");
  } else {
    return false; // null, not whitespace
  }
}

/**
 * Traverses a document to find all the IMG tags referring to http: or
 * https: resources that don't also have a "traction_rs" attribute.
 * @param doc the HTMLDocumentObject.
 * @return a string containing a comma separated list (commas in the
 * URLs are escaped as \,) of all the SRC attributes of any IMG tag in
 * the given document whose SRC attribute begins with "http:" or
 * "https:" but don't have a traction_rs attribute.  For Server17799,
 * null is returned if a file: URL is found to have been used as an
 * IMG's SRC attribute and the user elected to short-circuit the post
 * in order to make the image point to a legitimate URL.
 */
function getExternalImageURLs(doc) {

  Debug.richtext.println("Warning: the getExternalImageURLs function in shared.js is deprecated in favor of Traction.Edit.TinyMce.getExternalImageUrls in traction.js.");

  var images = doc.getElementsByTagName("img");
  if (images == null || images.length == 0) {
    return "";
  }

  var root = getRootDocumentURL();

  var ret = "";

  for (var i = 0; i < images.length; i ++) {

    var snarfOK = images[i].getAttribute("traction_image_autocapture");
    if (snarfOK == "false") {
      // Just go on to the next IMG tag.  We were returning null here,
      // which is an indication to the caller that the user has (by
      // means of one of the prompts possibly triggered below)
      // cancelled submission to repair some IMG tags.  Fixes
      // Server31791.  [shep 14.Sep.2007]
      continue;
    }

    var src = images[i].getAttribute("src");
    if (src == null) {
      src = "";
    }

    if (src.toLowerCase().indexOf("file:") == 0) {
      var warningmsg = i18n("editentry_alert_message_file_url_image_warning_before",
			    "You have included an image in this document that is a reference to a file on your local filesystem (") +
	src +
	i18n("editentry_alert_message_file_url_image_warning_after",
	     ").  This image will NOT be automatically included when you submit this article.\n\n" +
	     "If you want help fixing this problem, press OK.");
      if (confirm(warningmsg)) {
	// Launch a window containing the appropriate help page.
	window.open(i18n("help_view_newentry", "/share/tsi/help/html/Traction_Help_Guide.htm#Add_New_Article.htm"));
	return null;
      }
      warningmsg = i18n("editentry_alert_message_file_url_image_warning_continuation_confirmation",
			"If you want to continue and post this message as it is, press OK.\n\n" +
			"If you want to cancel the post for now and fix the problem, press Cancel.");
      if (!confirm(warningmsg)) {
	return null;
      }
    }

    var trs = images[i].getAttribute("traction_rs");
    if ((trs == null || trs == "") && // no traction_rs attribute
	(src.toLowerCase().indexOf("http:") == 0 || src.toLowerCase().indexOf("https:")) &&
	(root != null && src.indexOf(root) != 0)) { // resource begins with http: or https:
      Debug.richtext.println("adding external image src \"" + src + "\"");
      if (ret != "") {
	ret += ",";
      }
      ret += escape_commas(src);
    }

  }

  return ret;

}

function cleanUpLICloseTags(doc) {
  Debug.richtext.println("Warning: the cleanUpLICloseTags function in shared.js is deprecated.");
  var bodyHTML = new String(doc.body.innerHTML);
  bodyHTML = bodyHTML.replace(/\<\/li\>/gi, "");
  doc.body.innerHTML = bodyHTML;
}

