/* ********************************************************************* *
 * FILE: dom.js                                                          *
 * PROJ: Personal web site at Cornell University                         *
 * DESC: Javascript DOM utilities                                        *
 * AUTH: weigel                                                          *
 * VERS: $Id: dom.js 3 2008-05-04 08:55:58Z felix $                      *
 * COPY: Copyright Cornell University 2008                               *
 * ********************************************************************* */
 
/**
 * Returns the first element child node of the given node, 
 * or an undefined value if no such node exists. 
 **/

function getFirstElementChild(node) {

  var i;
  var childNode;

  for (i = 0; i < node.childNodes.length; i++) {
    if (node.childNodes[i].nodeType == 1) {
      childNode = node.childNodes[i];
      break;
    }
  }
  return childNode;

}


/**
 * Returns the last element child node of the given node, 
 * or an undefined value if no such node exists. 
 **/

function getLastElementChild(node) {

  var i;
  var childNode;

  for (i = node.childNodes.length - 1; i >= 0 ; i--) {
    if (node.childNodes[i].nodeType == 1) {
      childNode = node.childNodes[i];
      break;
    }
  }
  return childNode;

}


/**
 * Returns the previous sibling of the given node, if any. 
 **/

function getPreviousElementSibling(node) {

  var sibling;

  for (
    sibling = node.previousSibling; 
    sibling && (sibling.nodeType != 1); 
    sibling = sibling.previousSibling
  );
  return sibling;

}


/**
 * Returns the next sibling of the given node, if any. 
 **/

function getNextElementSibling(node) {

  var sibling;

  for (
    sibling = node.nextSibling; 
    sibling && (sibling.nodeType != 1); 
    sibling = sibling.nextSibling
  );
  return sibling;

}


/**
 * Returns the array of element child nodes of the given node, 
 * which is empty if no such node exists. 
 **/

function getElementChildren(node) {

  var i;
  var childNodes;

  childNodes = new Array();
  for (i = 0; i < node.childNodes.length ; i++) {
    if (node.childNodes[i].nodeType == 1) {
      childNodes.push(node.childNodes[i]);
    }
  }
  return childNodes;

}


/**
 * Returns the zero-based number of previous element siblings of the given node. 
 **/

function getSiblingPosition(node) {

  var sibling;
  var count;

  count = 0;
  for (
    sibling = node.previousSibling; 
    sibling; 
    sibling = sibling.previousSibling
  ) {
    if (sibling.nodeType == 1) {
      count++;
    }
  }
  return count;

}


/**
 * Returns the zero-based number of previous element siblings of the given node
 * that have the same tag name as the given node. 
 **/

function getSameSiblingPosition(node) {

  var sibling;
  var count;

  count = 0;
  for (
    sibling = node.previousSibling; 
    sibling; 
    sibling = sibling.previousSibling
  ) {
    if ((sibling.nodeType == 1) && (sibling.nodeName == node.nodeName)) {
      count++;
    }
  }
  return count;

}


/**
 * Tests whether the first given node is a preceding sibling of the second 
 * given node. Returns true if this is the case, false otherwise. 
 **/
 
function isLeftSibling(nodeA, nodeB) {

  var sibling;

  for (
    sibling = nodeB.previousSibling; 
    sibling; 
    sibling = sibling.previousSibling
  ) {
    if (sibling == nodeA) {
      return true;
    }
  }
  return false;

}


/**
 * Returns the lowest common ancestor (LCA) of the two given nodes. 
 **/
 
function getLCA(nodeA, nodeB) {

  return getLCATriangle(nodeA, nodeB)[0];
  
}


/**
 * Returns the lowest common ancestor (LCA) of the two given nodes along with those 
 * two children of the LCA that are the respective ancestors of the two given 
 * nodes. The returned object contains between one and three fields at the 
 * positions 0, 1 and 2. Position 0 contains the LCA; it is never null. 
 * Position 1 contains the highest ancestor of the first given node that is a 
 * child of the LCA, if such a node exists. This node may be the first given 
 * node itself. If the first given node is the LCA, then position 1 is null. 
 * Position 2 is defined by analogy for the second given node. Note that the 
 * two given nodes are identical iff both position 1 and position 2 are null. 
 **/
 
function getLCATriangle(nodeA, nodeB) {

  var pathA;
  var pathB;
  var ancestorA;
  var ancestorB;
  var lca;
  var level;
  var result;
  
  // prepare the result object 
  result = new Object();

  // get the root paths of the two given nodes 
  // (note that they are bottom-up, starting with the leaf) 
  pathA = getElementPath(nodeA);
  pathB = getElementPath(nodeB);

  // get the ancestors of both nodes at the level of the higher node
  if (pathA.length < pathB.length) {
    level = pathA.length;
    ancestorA = nodeA;
    ancestorB = pathB[pathB.length - level];
  }
  else if (pathB.length < pathA.length) {
    level = pathB.length;
    ancestorA = pathA[pathA.length - level];
    ancestorB = nodeB;
  }
  else {
    level = pathA.length;
    ancestorA = nodeA;
    ancestorB = nodeB;
  }
  
  // compare the paths leading to these two ancestors to find the LCA
  while (ancestorA != ancestorB) {
    ancestorA = ancestorA.parentNode;
    ancestorB = ancestorB.parentNode;
    level--;
  }
  
  // store the LCA and its children in the result object 
  result[0] = ancestorA;
  if (level == pathA.length) {
    result[1] = null;
  }
  else {
    result[1] = pathA[pathA.length - level - 1];
  }
  if (level == pathB.length) {
    result[2] = null;
  }
  else {
    result[2] = pathB[pathB.length - level - 1];
  }

  // return the result object 
  return result;
  
}


/**
 * Returns the ancestor of the given element that is at the specified distance 
 * from the given element, or null if no such ancestor exists. 
 **/
 
function getAncestor(element, distance) {

  var ancestor;
  var d;

  for (
    ancestor = element, d = distance; 
    ancestor && (d > 0);
    ancestor = ancestor.parentNode, d--
  );
  return ancestor;

}


/**
 * Returns the root path of the given element as an array of nodes.
 * The path is traversed bottom-up. The last node in the returned array
 * is the document root.
 **/
 
function getElementPath(element) {

  var path;

  path = new Array();
  while (true) {
    path.push(element);
    if (element.parentNode) {
      element = element.parentNode;
    }
    else {
      break;
    }
  }
  return path;

}


/**
 * Returns the textual content of the given element. 
 * The returned string results from concatenating the node values of 
 * all children of the given element whose node type is either text 
 * or CDATA in document order, separated by the given string. 
 **/
 
function getElementContent(element, separator) {

  var c;
  var result;
  
  result = "";
  for (c = element.firstChild; c != null; c = c.nextSibling) {
    if ((c.nodeType == 3) || (c.nodeType == 4)) {
      result += c.nodeValue + separator;
    }
  }
  if (result.length > 0) {
    result = result.substring(0, result.length - separator.length);
  }
  return result;
  
}


/**
 * Returns the textual content of the given element and all its descendant elements. 
 * The returned string results from concatenating the node values of 
 * all nodes in the subtree below the given element whose node type is either text 
 * or CDATA in document order, separated by the given string. 
 **/
 
function getSubtreeContent(element, separator) {

  var c;
  var result;
  
  result = "";
  for (c = element.firstChild; c != null; c = c.nextSibling) {
    if ((c.nodeType == 3) || (c.nodeType == 4)) {
      result += c.nodeValue + separator;
    }
    else if (c.nodeType == 1) {
      result += getSubtreeContent(c, separator) + separator;
    }
  }
  if (result.length > 0) {
    result = result.substring(0, result.length - separator.length);
  }
  return result;
  
}


/**
 * Adds the specified string to the specified attribute of the specified node. 
 **/

function addAttribute(node, attribute, value) {

  var currentValue;
  var newValue;
  var position;

  currentValue = node.getAttribute(attribute);
  if (!currentValue) {
    newValue = value;
  }
  else {
    newValue = currentValue;
    eval("position = currentValue.search(/" + value + "/)");
    if (position == -1) {
      newValue += " " + value;
    }
  }
  node.setAttribute(attribute, newValue);

}


/**
 * Removes the specified string from the specified attribute of the specified node. 
 **/

function removeAttribute(node, attribute, value) {

  var currentValue;
  var newValue;
  var pattern;

  currentValue = node.getAttribute(attribute);
  if (currentValue) {
    eval("newValue = currentValue.replace(/\\s*" + value + "/, \"\")");
    node.setAttribute(attribute, newValue);
  }

}


/**
 * Adds the specified string to the specified attribute of all nodes in the given node set. 
 **/

function addAttributes(nodes, attribute, value) {

  var n;
  
  for (n = 0; n < nodes.length; n++) {
    addAttribute(nodes[n], attribute, value);
  }

}
  

/**
 * Removes the specified string from the specified attribute of all nodes in the given node set. 
 **/

function removeAttributes(nodes, attribute, value) {

  var n;
  
  for (n = 0; n < nodes.length; n++) {
    removeAttribute(nodes[n], attribute, value);
  }

}
  

/**
 * Checks whether the specified string occurs in the value of the specified attribute of the specified node. 
 **/

function hasAttribute(node, attribute, value) {

  var currentValue;
  var position;

  currentValue = node.getAttribute(attribute);
  if (!currentValue) {
    return false;
  }
  else {
    eval("position = currentValue.search(/" + value + "/)");
    return (position != -1);
  }

}


/**
 * A list of attributes to be ignored in wrapper definitions.
 **/
 
var ignoredAttributes = new Object();
ignoredAttributes["onclick"] = true;
ignoredAttributes["ondblclick"] = true;
ignoredAttributes["onkeydown"] = true;
ignoredAttributes["onkeypress"] = true;
ignoredAttributes["onkeyup"] = true;
ignoredAttributes["onmousedown"] = true;
ignoredAttributes["onmousemove"] = true;
ignoredAttributes["onmouseout"] = true;
ignoredAttributes["onmouseover"] = true;
ignoredAttributes["onmouseup"] = true;


/**
 * Tests whether the given attribute is ignored in wrapper definitions.
 **/
 
function isIgnoredAttribute(attribute) {

  return (ignoredAttributes[attribute.nodeName] == true);
  
}


/**
 * Returns the value of the given attribute, with application-specific modifications removed.
 **/
 
function getCleanAttributeValue(attribute) {

  var value;
  if (attribute.nodeName == "class") {
    value = attribute.nodeValue.replace(/\s*wt1-\S+/g, "");
  }
  else {
    value = attribute.nodeValue;
  }
  return value;

}


