/* rsn_gut_manipulation.js
 * functions that locate parts of the GUT (Global Unified Template) so they 
 * can be dynamically replaced or modified.
 *
 * Version 1.2 - modified getFirstNode() to take a 3rd parameter so that 
 *               white space only text nodes can be skipped
 * Version 1.1
 *
 * Copyright (2006) Research Now Plc.
 */
 
/* a SimpleAnswer represents an answer to a single or multi question
 *
 * input - the input element for the answer - probably a radio button or a 
 *         checkbox. This element contains attributes such as the answer's 
 *         precode (the value attr).
 * font  - the element that contains the text of the answer - the answer's 
 *         label
 */
function SimpleAnswer(input, font) {		// rename to AnswerRow?

  // fields
  this.inputElement      = input;
  this.fontElement       = font;
  this.spaceAfterControl = 10;		// set 10 pixels as default
  
  // methods
  this.getPrecode           = _getPrecode;
  this.isLabelOnly          = _isLabelOnly;
  this.getLabel             = _getLabel;
  this.setSpaceAfterControl = _setSpaceAfterControl;
  this.removeNoWrap         = _removeNoWrap;
}

/*** SimpleAnswer methods **/

// returns this SimpleAnswer's precode, or null if it's just a label
function _getPrecode() {
  if ( this.isLabelOnly() )
    return null;
  else if (this.inputElement.type == "radio")	// radio (single)
    return this.inputElement.value;
  else {					// checkbox (multi)
    var matches = /_(\w+)$/.exec(this.inputElement.id);
    return matches[1];
  }
}

// returns whether this SimpleAnswer is just a label (e.g. Group Heading in 
// Confirmit)
function _isLabelOnly() { return this.inputElement == null; }

// gets the label/text of a SimpleAnswer
function _getLabel() {

  // fontElement contains, directly or indirectly, the label of the answer 
  // sought
  var textNode = getFirstNode( this.fontElement, 3 /* Node.TEXT_NODE */ );
  return textNode.data;
}

// sets how many pixels the label of this SimpleAnswer will be to the right 
// of its checkbox/radio input element
function _setSpaceAfterControl(pixels) {
  if ( !this.isLabelOnly() ) {
    var spacesNode = this.inputElement.nextSibling;

    // if input element is still followed by text node containing "&nbsp...", 
    // delete it and implement spacing with CSS
    if ( spacesNode.nodeType == 3 /* Node.TEXT_NODE */ )
      spacesNode.parentNode.removeChild(spacesNode);
    this.inputElement.style.marginRight = pixels + "px";
  }
}

// removes the nowrap attribute from the td cell that contains this 
// SimpleAnswer (thereby allowing the label of a SimpleAnswer to wrap)
function _removeNoWrap() {
  this.fontElement.parentNode.removeAttribute("nowrap");
}


/*** STANDALONE FUNCTIONS ***/

/* gets the first 2 question/answer pairs in the current document and moves 
 * them into a new table so they're shown side by side
 *
 * padding - the number of horizontal pixels of padding to insert between the 
 *           the 1st and 2nd question/answer pairs
 */
function alignSideBySide(padding) {
  
  // create new table - 1 row, 2 columns - with top vertical alignment
  var tableElem   = document.createElement("table");
  var tbodyElem   = document.createElement("tbody");
  var trElem      = document.createElement("tr");
  var tdLeftElem  = document.createElement("td");
  var tdRightElem = document.createElement("td");
  
  // set vertical alignment to top in left and right tds
  tdLeftElem.setAttribute("valign", "top");	// Gecko obeys; MSIE ignores
  tdLeftElem.style.verticalAlign = "top";	// necessary for MSIE
  tdRightElem.setAttribute("valign", "top");	// Gecko obeys; MSIE ignores
  tdRightElem.style.verticalAlign = "top";	// necessary for MSIE
  
  // apply horizontal spacing between left and right question/answer pairs
  tdLeftElem.style.paddingRight = padding + "px";
  
  // table >> tbody >> tr >> (td, td)
  tableElem.appendChild(tbodyElem);
  tbodyElem.appendChild(trElem);
  trElem.appendChild(tdLeftElem);
  trElem.appendChild(tdRightElem);
  
  // remove nowrap attributes from the answers of the qaPairs to move
  removeNoWrap( getAnswers(1) );	// answers of 1st qaPair
  removeNoWrap( getAnswers(2) );	// answers of 2nd qaPair
  
  // get qa pairs we want to move, detach them from the DOM, move them into 
  // the unattached table created above, then attach this table to the DOM
  var qa1        = getQuestionAnswerPair(1);
  var qa2        = getQuestionAnswerPair(2);
  var parentNode = qa1.parentNode;
  parentNode.removeChild(qa1);		// detach from DOM
  parentNode.removeChild(qa2);
  tdLeftElem.appendChild(qa1);		// ...and move into new table
  tdRightElem.appendChild(qa2);
  parentNode.insertBefore(tableElem, parentNode.firstChild);
  
  // annoyingly, as a result of detaching them from the DOM, qa1 and qa2 lose 
  // their font styles so we have to reinstate them
  var excludeHdr = /^h/i;
  applyStyle(qa1, excludeHdr, "fontSize", "10pt");
  applyStyle(qa2, excludeHdr, "fontSize", "10pt");
}

/* applies a CSS rule (property and value params) to an element (parent 
 * param) and all its sub-elements recursively which don't match 
 * childExclusionRegex
 *
 * parent              - the HTML element to which the CSS rule will apply. 
 *                       All child elements below parent will also have the 
 *                       CSS rule applied to them unless their tag name 
 *                       matches childExclusionRegex.
 * childExclusionRegex - sub elements of parent that match this pattern will 
 *                       not have the CSS rule applied to them. EG: /^h/i 
 *                       means no h tags, like h1, below parent will have the 
 *                       CSS rule applied to them.
 * property            - CSS property (e.g. "fontSize" or "marginRight")
 * value               - CSS value (e.g. "12pt" or "50px")
 */
function applyStyle(parent, childExclusionRegex, property, value) {
  var children = parent.childNodes;
  for (var i = 0; i < children.length; i++) {
    var child = children[i];
    
    // apply style recursively to all child elements except excluded ones
    if ( child.nodeType == 1 /* Node.ELEMENT_NODE */ 
    		&& !childExclusionRegex.test(child.tagName) ) {
      child.style[property] = value;
      applyStyle(child, childExclusionRegex, property, value);
    }
  }
}

/* removes the nowrap attr from all td elements inside ansHolder
 *
 * ansHolder - an answers div or table (div or table element that contains 
 *             answers to a single/multi question in Confirmit)
 */
function removeNoWrap(ansHolder) {
  var tdTags = ansHolder.getElementsByTagName("td");
  for (var i = 0; i < tdTags.length; i++)
    tdTags[i].removeAttribute("nowrap");
}

/* removes the nowrap attr from all td elements inside every answers table 
 * in the current HTML document
 */
function removeNoWrapGlobally() {
  for (var i = 1; true; i++) {
    var ansTable = getAnswerTable(i);
    if (ansTable == null)	// no more answer tables: quit looping
      break;
    removeNoWrap(ansTable);
  }
}

/* puts an image to the left or right of the answers of the 1st 
 * question/answer pair in the document
 *
 * imgURL       - the absolute URL (or relative to the document that includes 
 *                this JavaScript source) of the image to insert
 * side         - "left" or "right" to align the image to the left or right 
 *                of the first qa pair's answers
 * imgToTop     - boolean: whether the image should be aligned to the top of 
 *                its containing table
 * answersToTop - boolean: whether the answers of the 1st qa pair should be 
 *                aligned to the top of its containing table
 */
function putImageBesideAnswers(imgURL, side, imgToTop, answersToTop) {
  
  // create new table - 1 row, 2 columns - to hold image and qa pair
  var tableElem   = document.createElement("table");
  var tbodyElem   = document.createElement("tbody");
  var trElem      = document.createElement("tr");
  var tdLeftElem  = document.createElement("td");
  var tdRightElem = document.createElement("td");
  
  // translate left and right tds semantically to image td and answers td
  var imgTd     = side  == "left" ? tdLeftElem : tdRightElem;
  var answersTd = imgTd == tdLeftElem ? tdRightElem : tdLeftElem;
  
  // set vertical alignment of tdLeftElem and tdRightElem if necessary
  if (imgToTop) {
    imgTd.setAttribute("valign", "top");	// for Gecko; MSIE ignores
    imgTd.style.verticalAlign = "top";		// necessary for MSIE
  }
  if (answersToTop) {
    answersTd.setAttribute("valign", "top");	// for Gecko; MSIE ignores
    answersTd.style.verticalAlign = "top";		// necessary for MSIE
  }
  
  // table >> tbody >> tr >> (td, td)
  tableElem.appendChild(tbodyElem);
  tbodyElem.appendChild(trElem);
  trElem.appendChild(tdLeftElem);
  trElem.appendChild(tdRightElem);
  
  // get answers div and replace it with tableElem, thereby detaching it from 
  // the DOM so we can place it inside tableElem
  var answersDiv = getAnswers(1);
  var qaPair     = answersDiv.parentNode;
  qaPair.replaceChild(tableElem, answersDiv);
  
  // move image into imgTd and answers into answersTd
  var imgTag = document.createElement("img");
  imgTag.id  = "javaScriptImageBeside"
  imgTag.setAttribute("src", imgURL);
  imgTd.appendChild(imgTag);
  answersTd.appendChild(answersDiv);
}

/* extracts all answers from the "answer table" (the table element that 
 * contains answers for singles, multis, opens and grids) for single and 
 * multi questions only. Each answer is an instance of SimpleAnswer.
 *
 * aTable - (optional) an answer table container (a table element). If 
 *          absent, the first one in the current document will be used
 *
 * returns an array of SimpleAnswer objects
 */
function getAnswerItems(aTable) {
  if (aTable == null)	// or void 0?
    aTable = getAnswerTable(1);
  var cells       = aTable.getElementsByTagName("td");
  var answerItems = new Array(cells.length);	// 1 answer item per cell

  /* extract input and font elements from each cell. Real answers have an 
   * input and font element. Cells that contain a group heading label 
   * consist of a font element only.
   */
  for (var i = 0; i < answerItems.length; i++) {
    var currentRow = cells[i];
    var inputElem, fontElem;
    var elems = currentRow.getElementsByTagName("input");
    if (elems != null)
      inputElem = elems[0];
    
    elems = currentRow.getElementsByTagName("font");
    if (elems != null)
      fontElem = elems[0];
    
    answerItems[i] = new SimpleAnswer(inputElem, fontElem);

  }
  return answerItems;
}

/* gets the prompt of a given question in the current document. The prompt of 
 * a question is typically the question's textual embodiment, like 
 * "<p>What is your favorite sport?</p>". But a question's prompt may also 
 * include a picture, and hence an img tag. It could be an entire hierarchy 
 * of HTML markup. The whole question prompt is contained in a div tag.
 *
 * qNum - the number of the question whose prompt is sought. EG: 1 gets the 
 *        first question's prompt; 2 gets the 2nd etc..
 *
 * returns a div tag that contains the entire question prompt, if found, or 
 * null
 */
function getQuestionPrompt(qNum) {
  
  // first, get all question prompts: they're all housed in div tags
  var qPrompts = new Array();
  var divTags  = document.getElementsByTagName('div');
  for (var i = 0; i < divTags.length; i++) {
    var divTag = divTags[i];
    if (divTag.className == "questionPrompt") qPrompts.push(divTag);
  }
  
  // then get the one we want
  return qPrompts[--qNum];
}

/* gets a div that holds the answers of a question in Confirmit generated 
 * surveys.
 *
 * ansNum - the ordinal number, in terms of its position in the HTML 
 *          document, of the set of answers wanted. E.G. 1 gets the first 
 *          answers div; 2 gets the 2nd etc.. There can be many answers divs 
 *          per HTML document
 *
 * returns the answers div sought (a div element), if found, or null
 */
function getAnswers(ansNum) {
  
  // put all divs with class == "answers" into answerDivs
  var divTags    = document.getElementsByTagName("div");
  var answerDivs = new Array();
  for (var i = 0; i < divTags.length; i++) {
    var divTag = divTags[i];
    if (divTag.className == "answers") answerDivs.push(divTag);
  }
  
  return answerDivs[--ansNum];
}

/* gets the table that holds the answers of a question in Confirmit generated 
 * surveys. This table is nested inside an answers div. See getAnswers().
 *
 * aTabNum - the ordinal number, in terms of its position in the HTML 
 *           document, of the answer table wanted. E.G. 1 gets the first 
 *           answer table; 2 gets the 2nd one etc..
 *
 * returns the answer table sought (a table element), if found, or null
 */
function getAnswerTable(aTabNum) {
    
  // return the 1st table in the answers div sought: this is the answer table
  var answersDiv = getAnswers(aTabNum);
  return answersDiv == null ? null : 
  			(answersDiv.getElementsByTagName("table"))[0];
}

/* gets a div element that wraps an entire question and answer pair (a 
 * question prompt and an answers table)
 *
 * pairNum - the ordinal number, in terms of its position in the HTML 
 *           document, of the qaPair wanted. E.G. 1 gets the first 
 *           question/answer pair; 2 gets the 2nd etc..
 *
 * returns a div element that contains the question/answer pair sought, if 
 * found, or null
 */
function getQuestionAnswerPair(pairNum) {
  
  // put all divs with class == "qaPair" into qaPairDivs
  var divTags    = document.getElementsByTagName("div");
  var qaPairDivs = new Array();
  for (var i = 0; i < divTags.length; i++) {
    var divTag = divTags[i];
    if (divTag.className == "qaPair") qaPairDivs.push(divTag);
  }
  
  // ...then return the one we want
  return qaPairDivs[--pairNum];
}

/* returns the first node in parentElem whose type matches nodeTypeSought. 
 * EG: 
 * 
 *   getFirstNode( p, 3, true )    // 3 == Node.TEXT_NODE
 *
 * gets the first text node inside parent element p.
 *
 * skipWhiteSpace: skip whitespace only text nodes
 */
function getFirstNode(parentElem, nodeTypeSought, skipWhiteSpace) {
  // regular expression to test if a string is only whitespace
  var regEx = /^\s+$/;
     
  var children = parentElem.childNodes;
  var retVal   = null;
  outer: for (var i = 0; i < children.length; i++) {
    var nextChild = children[i];
    
    if ( nextChild.nodeType == 1 && nodeTypeSought != 1 ) {
      retVal = getFirstNode(nextChild, nodeTypeSought, true);
      if (retVal != null) 
	break;
    }
    
    else if ( nextChild.nodeType == nodeTypeSought ) {
     if (nodeTypeSought == 3 && skipWhiteSpace) {
       var valueToTest = nextChild.nodeValue;
       if (valueToTest.match(regEx)) {
         continue outer;
       }
     }	    
     retVal = nextChild;
     break;
    }

  }
  return retVal;
}