// Skimmingstone Category Control for integration into existing publishing workflows.
// Copyright: Firstlight Online

// Superclass called directly from the displaying page. This class is responsible for instantiating the 
// CategoryTree and CategoryList Controller objects with the specified settings.
function SSCategoryControl() {
	/* GLOBAL SETTINGS */
	this.cssPath = 'skimmingstoneStyles.css';				// Path to the CSS file for Category Tree styles.
	this.skimmingstoneHost = 'rn.projects.merito.co.nz';										// Domain name for the Skimmingstone server to communicate with.
	this.categoryWsPath = '/Skimmingstone/webservices/CategoryInterface.asmx';	// Path to the SS Category web service.
	this.taggingWsPath = '/Skimmingstone/webservices/Tagging.asmx';				// Path to the SS Tagging web service.
	this.treeControlName = 'CategoryTreeController';							// Name of the object the CategoryTreeController object is instantiated as.
	this.listControlName = 'CategoryListController';							// Name of the object the CategoryListController object is instantiated as.
	this.documentName = 'document';												// Browser document name.
	this.skimmingstoneSiteRoot = '/Skimmingstone';								// Skimmingstone root used to render images from the correct location.
	this.imagePath = 'http://rn.projects.merito.co.nz/Skimmingstone/images/CategoryTree/';	// Full Url to the images folder the category tree images can be found (Include trailing slash!).
	this.addedCategoriesControlName = ''										// Name of the hidden input for the selected categories to be extracted from in a comma deliminated list.
	this.languageID = 1;														// Skimmingstone language ID to show categories in (English = 1).
	this.categoryTreeDivName = 'categoryTreeDiv';								// Name of the DIV to render the category tree into.
	this.categoryListDivName = 'addedCategoryList';								// Name of the DIV to render the category list into.
	this.categoryTreeErrorMessageFooter = '';									// Error message to append to any error messages shown in the Category Tree.
}

// Call to load and render the control into the page.
SSCategoryControl.prototype.DrawControl = function(authToken, contentID, outputFieldName) {
	this.addedCategoriesControlName = outputFieldName;

	// Render HTML elements.
	document.write('<link href="' + this.cssPath + '" rel="stylesheet" type="text/css" />');
	document.write('<input type="hidden" id="' + this.addedCategoriesControlName + '" name="' + this.addedCategoriesControlName + '"/>');
	document.write('<input type="hidden" id="SelectedCategories" name="SelectedCategories"/>');
	document.write('<table cellspacing="0" cellpadding="5"><tr><td>');
	document.write('<!-- Category tree --><div id="' + this.categoryTreeDivName +'" class="categoryTree"></div></td><td>');
	document.write('<!-- Selector buttons --><input type="button" value="&nbsp;&gt;&gt;&nbsp;" onclick="javascript:' + this.listControlName + '.addCategory(' + this.treeControlName + ');"/><br/>');
	document.write('<input type="button" value="&nbsp;&lt;&lt;&nbsp;" onclick="javascript:' + this.listControlName + '.removeSelected();"/></td><td>');
	document.write('<!-- Selected category list --><div class="categoryTree"><div id="' + this.categoryListDivName +'"></div></div></td></tr></table>');
	
	// Instantiate Controller objects
	document.write('<script language="JavaScript">');
	
	document.write('var ' + this.treeControlName + ' = new CategoryTreeController("' + this.treeControlName + '", "' + this.listControlName + '", ' + this.documentName + ', "", "SelectedCategories", "' + this.skimmingstoneSiteRoot + '", ');
	document.write('"' + this.skimmingstoneHost + '", "' + this.categoryWsPath + '", "' + authToken + '", document.getElementById(\'' + this.categoryTreeDivName + '\'), "' + this.imagePath + '");');
	document.write(this.treeControlName + '.ErrorMessageFooter = "' + this.categoryTreeErrorMessageFooter + '";');
	document.write(this.treeControlName + '.loadCategoryNodes(0, document.getElementById(\'' + this.categoryTreeDivName + '\'));');
	
	document.write('var ' + this.listControlName + ' = new CategoryListController("' + this.listControlName + '", ' + this.documentName + ', "", "' + this.categoryListDivName + '", '); 
	document.write('"' + this.addedCategoriesControlName + '", "' + this.skimmingstoneHost + '", "' + this.taggingWsPath + '", "' + authToken + '");');
	document.write(this.listControlName + '.loadAssignedCategories("' + contentID + '", ' + this.languageID + ');');
	
	document.write('</script>');
}

// SoapWrapper to invoke .NET web services via XmlHttp in both Internet explorer and Mozilla
//
// NOTE: Even though the web service call (SOAPWrapper.Invoke()) can be made over https
// Internet Explorer will still complain  if the enclosing page is using https as it uses 
// an ActiveX object which is out of the browser's control (due to cross site scripting protection).
// This can be overcome by adding the site as a trusted site, or in IE Custom Settings (Misc Section)
// changing Disable/Prompt to Enable.

// Constructor - detect browser and initialize XmlHttp request object
function SOAPWrapper(server, path) {
    this.server = server;
    this.path = path;
    this.error = "";
    this.unavailable = 0;
    this.browser = "";
    this.xmlHttpObj = "";
    
    if(!this.CreateXmlHttpObject()) {
	  this.unavailable = 1;
      this.error = "Critical Failure: " + this.error;
    }
  }

// Ascertain if this browser can handle XMLHTTP and if so create a XMLHTTP object
SOAPWrapper.prototype.CreateXmlHttpObject = function() {
  // Internet Explorer
  if(window.ActiveXObject) {
    this.xmlHttpObj = new ActiveXObject("Microsoft.XMLHTTP");
    this.browser = "ie";
    return true;
  // Netscape/Mozilla
  }else if(window.XMLHttpRequest) {
    this.browser = "ns";
    this.xmlHttpObj = new XMLHttpRequest();
    return true;
  // Opera and others
  } else {
    this.error = "This script is unsupported by your browser";
    return false;
  }
}

// Create a string containing a valid SOAP message to send
// NOTE: This message is created to interact with a .NET web service in mind,
// variations maybe required in order for this to work with non .NET web services
SOAPWrapper.prototype.CreateSoapRequest = function(method, namespace, params) {

  var soapStr = "";
  soapStr += "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>";
  soapStr += "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
  soapStr += "<soap:Body>"
  soapStr += "<" + method + " xmlns=\"" + namespace + "\">";
  
  // Create our parameters
  if(params && params.length > 0) {
    for(i = 0;i < params.length; i++) {
      soapStr += "<" + params[i].name + ">" + params[i].value + "</" + params[i].name + ">";
    }
  }
     
  soapStr += "</"+ method + ">";
  soapStr += "</soap:Body></soap:Envelope>";
	alert(soapStr);
  return soapStr;

}

// Send the request
SOAPWrapper.prototype.Invoke = function(soapRequest, soapAction) {

  // Check that we have a valid XmlHttp object, else fail
  if(!this.xmlHttpObj) {
    this.error = "Invoke() was used without a valid XMLHTTP object";
    return false;
  }

  // IE browser
  if(this.browser == "ie") {
    this.xmlHttpObj.Open("POST", "http://" + this.server + this.path, false); 
  // Netscape/Mozilla browser
  } else if(this.browser == "ns") {
    try {
      // Requires a security permission to access remote resources
      netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
    } catch(ex) {
      this.error = "Failed to get Mozilla browser security permissions to make remote request.";
      return false;
    }
    this.xmlHttpObj.open("POST", "http://" + this.server + this.path, false);
  }
  
  // Set HTTP headers.
  this.xmlHttpObj.setRequestHeader("Host", this.server);
  this.xmlHttpObj.setRequestHeader("Man", "POST " + this.path +  " HTTP/1.1") 
  this.xmlHttpObj.setRequestHeader("Content-Type", "text/xml");
  this.xmlHttpObj.setRequestHeader("Content-Length", soapRequest.length);
  this.xmlHttpObj.setRequestHeader("SOAPAction", "\"" + soapAction + "\"");
  
  // Send request
  this.xmlHttpObj.send(soapRequest);
 
  // Check for HTTP errors
  if(this.xmlHttpObj.status == 200) {
    return this.xmlHttpObj.responseXML;
  } else {
    this.error = this.xmlHttpObj.statusText
    return false;
  }
}

// Read the output message.
SOAPWrapper.prototype.GetResponseParam = function(responseXML, tagName) {
  var node = responseXML.getElementsByTagName(tagName);
  if(node && node.length > 0) {
     return node[0].firstChild.nodeValue;
  } else {
    return -2;
  }
}

// SOAP Parameter Object.
function SOAPParam(name, value) {
  this.name = name;
  
  this.value = value;
}

// Client side script responsible for retrieving and displaying a list of selectable
// categories the client may tag a Content item with.

// Object representing a Skimmingstone Category.
function Category(node) {
    this.ID = 0;
    this.System = false;
    this.ParentID = null;
    this.HasChlidren = false;
    this.Name = new Array();

	for(j=0;j<node.childNodes.length; j++) {
		if(node.childNodes[j].nodeName=='ID') this.ID = node.childNodes[j].firstChild.nodeValue
		if(node.childNodes[j].nodeName=='System') this.System = node.childNodes[j].firstChild.nodeValue=="true";
		if(node.childNodes[j].nodeName=='ParentID') this.ParentID = node.childNodes[j].firstChild.nodeValue;
		if(node.childNodes[j].nodeName=='HasChildren') this.HasChildren = node.childNodes[j].firstChild.nodeValue=="true";
		if(node.childNodes[j].nodeName=='Name' && node.childNodes[j].firstChild) this.Name[this.Name.length] = node.childNodes[j].firstChild.nodeValue;
	}
}

// CategoryTreeController Constructor.
function CategoryTreeController(name, listControlName, doc, clientId, selectedCategoriesId, 
	siteRoot, hostHeader, categoryWebservicePath, authenticationToken, mainCategoryTreeDiv,
	imageBaseUrl) {
	
	this.name = name;
	this.listControlName = listControlName;
	this.document = doc;
	this.clientId = clientId
	this.selectedCategoriesId = selectedCategoriesId;
	this.siteRoot = siteRoot;
	this.hostHeader = hostHeader;
	this.categoryWebservicePath = categoryWebservicePath;
	this.authenticationToken = authenticationToken;
	this.imageBaseUrl = imageBaseUrl;
	
	this.selectedNodes = new Array();
	
	this.categoryTree = mainCategoryTreeDiv; // the root div
	this.selectedCategories = this.document.getElementById(selectedCategoriesId); // the selected categories hidden
	this.dblClickJs = 'javascript:' + this.listControlName + '.nodeDoubleClick(event, ' + this.name + ');';
}	

// Returns an element by its Id.
CategoryTreeController.prototype.getDivById = function(id) {
		return this.document.getElementById(id);
}

// Create a div that is the child of a specified parent div.
CategoryTreeController.prototype.createChildDiv = function(parent, newId) {
	oDiv = this.document.createElement('DIV'); 
	oDiv.id = newId;     
	parent.appendChild(oDiv);
	return oDiv;
}

// Returns the html for a node (will work for any element not just category divs).
CategoryTreeController.prototype.getCategoryName = function(categoryNode) {
	return categoryNode.childNodes[1].innerHTML;
}

// Gets the full hierarchy for a category to display in the selected categories list
CategoryTreeController.prototype.getFullCategoryPath = function(categoryNode) {
	var path = this.getCategoryName(categoryNode);
	var currentparent = categoryNode.parentNode;
	// while this is not a top level category, prepend the parent
	while(currentparent!=null && currentparent.id != this.categoryTree.id) {
		if(currentparent.id!=null && currentparent.id.substring(this.clientId.length + 1).indexOf('_') < 0) {
			currentparent;
			pname = this.getCategoryName(currentparent);
			path = pname + " > " + path;
		}
		currentparent = currentparent.parentNode;
	}
	return path;
}

// Retrieve the categories from the XML returned from the category web service.
CategoryTreeController.prototype.getCategoriesFromXML = function(xml) {
	var categories = new Array();
	
	if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
	var nodes = xml.getElementsByTagName("Category");
	for(i=0;i<nodes.length;i++) {
		category = new Category(nodes[i]);
		categories[categories.length] = category;
	}
	
	return categories;
}

// Parse the XML returned from the Skimmingstone web services to check for success
// or failure of the operation.
CategoryTreeController.prototype.getCategorySuccessFromXML =  function(xml) {
	var categories = new Array();
	if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
	var nodes = xml.getElementsByTagName("Success");
		
	if(nodes.length==1) {
		if(nodes[0].firstChild.nodeValue=='true') {
			return '';
		} else {
			if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
			nodes = xml.getElementsByTagName("Message");
			if(nodes.length==1) {
				if (nodes[0].firstChild.nodeValue == "Invalid authkey") {
					return "Session has timed out. Please refresh the page. If this error persists the credentials maybe wrong.";
				} else {
					return "Critical Failure: " + nodes[0].firstChild.nodeValue;
				}
			} else {
				return "Critical Failure: Success was 'false'. No Message supplied."
			}
		}
	} else {
		return 'Critical Failure: Unable to find Success node';
	}
}

// Appends a child div for a category to the parent div.
CategoryTreeController.prototype.appendChildDiv = function(category, parent) {
	childId = this.clientId + "_" + category.ID;
	
	oDiv = this.createChildDiv(parent, childId);
	oDiv.className = 'categoryTreeItem';
	html = ''
	if(category.HasChildren) {
		// If there are children leave child node out so that the children will be loaded when the node is expanded
		html += '<span class="categoryTreeNode" onclick="javascript:' + this.name + '.expandNode(';
		html += category.ID;
		html += ');"><img src="';
		html += this.imageBaseUrl + 'category_closed.gif" width="11" height="11" name="';
		html += childId;
		html += '_cimage">&nbsp;</span><span id="' + this.clientId + '_';
		html += category.ID;
		html += '_cname" onclick="javascript:' + this.name + '.nodeClick(event);" ondblclick="' +  this.dblClickJs + '" ';
		html += 'class="categoryTreeNode">';
		html += category.Name[0];
		html += '</span>';
	} else {
		// If there are no children draw an empty children div so the row is a leaf node
		html += '<span><img src="';
		html += this.imageBaseUrl + 'x.gif" width="11" height="11">&nbsp;</span>';
		html += '<span id="' + this.clientId + '_';
		html += category.ID;
		html += '_cname" onclick="javascript:event.cancelBubble=true;' + this.name + '.nodeClick(event);" ondblclick="' + this.dblClickJs + '" ';
		html += 'class="categoryTreeNode">';
		html += category.Name[0];
		html += '</span><div id="'+ this.clientId + '_';
		html += category.ID;
		html += '_children" class="empty"></div>';
	}
	oDiv.innerHTML = html;
}

// Stops single words from being highlighted int he browser by a double click
CategoryTreeController.prototype.clearDocumentSelection = function() {
	if(this.document.selection) { 
		this.document.selection.empty() 
	} else if(window.getSelection) {
		window.getSelection().removeAllRanges();
	}
}

// Display a properly formatted error.
CategoryTreeController.prototype.displayError = function (errorString) {
	this.categoryTree.innerHTML = '<div class="categoryTreeError"><p>' + errorString + '</p><p>' + this.ErrorMessageFooter + '</p></div>';
	if(this.onerror) this.onerror(errorString); // call any assigned error handler;
}

// Event handler for when a node is double clicked.
CategoryTreeController.prototype.doDoubleClick = function(e) {
	// Browser compatibility: sets the target node
	var tg = (e.target) ? e.target : e.srcElement

	// Get the category id
	var categoryid = this.getIdFromCname(tg.id);

	// Get the category_ div
	var node = this.document.getElementById(this.clientId + '_' + categoryid);
	while(this.selectedNodes.length>0) {
		this.selectedNodes[0].childNodes[1].className='categoryTreeNode';
		this.selectedNodes.splice(0,1);  
	}

	node.childNodes[1].className = 'categoryTreeNodeSelected';
	this.selectedNodes[this.selectedNodes.length] = node;
	this.clearDocumentSelection();
	
	return this.clientId
}

// Expand a node in the category tree
CategoryTreeController.prototype.expandNode = function(categoryId) {
	// Get the children of element to expand
	var children = this.getDivById(this.clientId + '_' + categoryId + '_children');
	
	if(!children) {
		parentNode = this.getDivById(this.clientId + '_' + categoryId);
		
		this.loadCategoryNodes( categoryId, parentNode ) ;
	} else {
		var im = this.document.images[this.clientId + '_'+ categoryId + '_cimage'];
		if(children.style.display == 'none') {
			im.src= this.imageBaseUrl + 'category_open.gif';
			children.style.display = 'block'
		} else {
			children.style.display = 'none';
		}
		// Set the plus an minus icons in the tree
		this.setExpandImages(categoryId);
	}
}

// Returns the category id for a given node.
CategoryTreeController.prototype.getCategoryId = function(categoryNode) {
	return categoryNode.id.substr(this.clientId.length + 1,999);
}

// Perform an XmlHttp request for all the child categories for the specified
// parent Category ID.
CategoryTreeController.prototype.GetChildCategories = function(parentId) {
	categories = new Array();

	var soapWrapper = new SOAPWrapper(this.hostHeader, this.categoryWebservicePath);
	var params = new Array();

	params[0] = new SOAPParam("token", this.authenticationToken);
	params[1] = new SOAPParam("parentID", parentId);
	var soapMessage = soapWrapper.CreateSoapRequest("GetCategories", "http://www.skimmingstone.com/category", params);
	
	if(soapWrapper.unavailable != 1) {
		var soapResponse = soapWrapper.Invoke(soapMessage, "http://www.skimmingstone.com/category/GetCategories");
		if(!soapResponse) {
			this.displayError("Critical Failure: Unable to obtain top level categories");	
			return null;
		} else {
			var success = this.getCategorySuccessFromXML(soapResponse);
			if(success == '') {
				categories = this.getCategoriesFromXML(soapResponse);			
			} else {
				this.displayError(success);	
				return null;
			}
		}
	} else {
		this.displayError("Critical Failure: Soap Wrapper Unavailable");	
		return null;
	}

	return categories;
}

// Perform an XmlHttp request for all the top level categories.
CategoryTreeController.prototype.GetTopLevelCategories = function() {
	categories = new Array();

	var soapWrapper = new SOAPWrapper(this.hostHeader, this.categoryWebservicePath);
	var params = new Array();

	params[0] = new SOAPParam("token", this.authenticationToken);
	var soapMessage = soapWrapper.CreateSoapRequest("GetTopLevelCategories", "http://www.skimmingstone.com/category", params);
	
	if(soapWrapper.unavailable != 1) {
		var soapResponse = soapWrapper.Invoke(soapMessage, "http://www.skimmingstone.com/category/GetTopLevelCategories");
		if(!soapResponse) {
			this.displayError("Critical Failure: Unable to obtain top level categories. " + soapWrapper.error);	
			return null;
		} else {
			var success = this.getCategorySuccessFromXML(soapResponse);
			if(success == '') {
				categories = this.getCategoriesFromXML(soapResponse);			
			} else {
				this.displayError(success);	
				return null;
			}
		}
	} else {
			this.displayError("Critical Failure: Soap Wrapper Unavailable");	
			return null;
	}

	return categories;
}

// Gets the div used to display the children for the category.
CategoryTreeController.prototype.getChildrenDiv = function(categoryDiv) {
	childrenId = categoryDiv.id + '_children';

	childrenDiv = this.getDivById(childrenId);
	if(!childrenDiv) {
		childrenDiv = this.createChildDiv(categoryDiv, childrenId);
	}
	return childrenDiv;
}

// Parses the cname span id for the categoryId.
CategoryTreeController.prototype.getIdFromCname = function( cnameId ) {
	id = cnameId.substring(0,cnameId.length - 6);
	return id.substring(this.clientId.length + 1);	
}

// Finds the index of a node in the selected node array.
CategoryTreeController.prototype.getSelectedNode = function(categoryId) {
	for(i=0;i<this.selectedNodes.length;i++) {
		// return the index if the node is found
		if(this.selectedNodes[i].id == this.clientId + '_'+ categoryId) return i;
	}
	return -1; //return -1 if node not found
}

// loads the child category nodes for a category id
CategoryTreeController.prototype.loadCategoryNodes = function(categoryId, categoryDiv) {	
	// load the children from the webservice
	childrenDiv = this.getChildrenDiv(categoryDiv);
	if(categoryId==0){
		//get the rootlevel categories
		children = this.GetTopLevelCategories();
	} else {
		// get the child categories
		children = this.GetChildCategories(categoryId);
		childrenDiv.className="branch";
	}
	
	// for each category create a div and append it to the categoryDiv.children
	if(children) {
		for(i=0;i<children.length;i++) {
			this.appendChildDiv(children[i], childrenDiv);
		}
	}
}

// Process the click event for a category (click of the span containing the category name)
CategoryTreeController.prototype.nodeClick = function(e) {
	// Browser compatibility: sets the target node
	var tg = (e.target) ? e.target : e.srcElement

	// Get the category id
	var categoryId = this.getIdFromCname(tg.id);
	// Get the category_ div
	var node = this.document.getElementById(this.clientId + '_' + categoryId);
	if(node) {
		// If it is a ctrl click then it is additive 
		// else we must clear the selectedNodes array
		if(!e.ctrlKey) {
			var currNode = null;
			while(this.selectedNodes.length>0) {
				if(this.selectedNodes[0].id==node.id) currNode = node;
				this.selectedNodes[0].childNodes[1].className='categoryTreeNode';
				this.selectedNodes.splice(0,1);  
			}
			if(currNode != null) this.selectedNodes[0] = node;
		}

		var nodeIndex = this.getSelectedNode(categoryId);

		// If it's not in the selected nodes array the add it, else remove it
		if(nodeIndex==-1) {
			node.childNodes[1].className = 'categoryTreeNodeSelected';
			this.selectedNodes[this.selectedNodes.length] = node;
		} else {
			this.selectedNodes[nodeIndex].childNodes[1].className = 'categoryTreeNode';
			this.selectedNodes.splice(nodeIndex,1);
		}
		this.clearDocumentSelection();
		this.updateHiddenField();
	}

	// Stop the event from calling parent node events
	e.cancelBubble = true;
	e.returnValue = false;
}

// Set the plus and minus icons for a node to show whether it 
// has children (+) or not and if so whether it is expanded (-)
CategoryTreeController.prototype.setExpandImages = function(categoryId, children) {
	// Get the children of element to expand
	var children = this.getDivById(this.clientId + '_' + categoryId + '_children');

	var im = this.document.images[this.clientId + '_'+ categoryId + '_cimage'];
	im.src = this.imageBaseUrl + 'category_closed.gif';
	if(im && children.style.display != 'none') {
		im.src= this.imageBaseUrl + 'category_open.gif';
	}
}

// Set the selected categories hidden field.
CategoryTreeController.prototype.setSelected = function(idArray) {
	// clear down the array by recreating it
	this.selectedNodes = new Array();
	
	if(typeof(idArray)=='number') {
		div = this.getDivById(this.clientId +'_' + idArray);
		if(div) this.selectedNodes[0] = div;
	} else {	
		for(i=0;i<idArray.length;i++) {
			div = this.getDivById(this.clientId +'_' + idArray[i]);
			if(div) this.selectedNodes[i] = div;
		}		
	}
}

// Updates the hidden field with the selected values
CategoryTreeController.prototype.updateHiddenField = function() {
	this.selectedCategories.value='';
	for(i=0;i<this.selectedNodes.length;i++) {
		this.selectedCategories.value += (this.selectedNodes[i].id.substring(this.clientId.length + 1) + ',');
	}
	if(this.selectedCategories.value!='') this.selectedCategories.value = this.selectedCategories.value.substring(0,this.selectedCategories.value.length-1);
}

// Client side script responsible for retrieving, displaying and managing the list
// of already selected categories a Content Item is tagged with.

// Object representing a Skimmingstone AssignedCategory.
function AssignedCategory(node) {
    this.ID = 0;
    this.Name = "";

	for(j=0;j<node.childNodes.length; j++) {
		if(node.childNodes[j].nodeName=='ID') this.ID = node.childNodes[j].firstChild.nodeValue
		if(node.childNodes[j].nodeName=='FullName') this.Name = node.childNodes[j].firstChild.nodeValue;
	}
}

// CategoryListController Constructor.
function CategoryListController(name, doc, clientId, addedCategoryListId, AddedCategoriesId, hostHeader, taggingWebservicePath, authenticationToken) {
	this.name = name;
	
	this.document = doc
	this.clientId = clientId;
	this.addedCategoryListId = addedCategoryListId
	this.AddedCategoriesId = AddedCategoriesId

	this.addedCategoryList = this.document.getElementById(addedCategoryListId);
	this.addedCategories = this.document.getElementById(AddedCategoriesId);

	this.addedCategoryNodes = new Array();
	this.selectedAddedNode = null;
	this.taggingWebservicePath = taggingWebservicePath;
	this.hostHeader = hostHeader;
	this.authenticationToken = authenticationToken;

	this.selectedNode = null;
}

// Adds all selected categories to the selected category list.
CategoryListController.prototype.addCategory = function(categoryTreeController) {
	for(k=0;k<categoryTreeController.selectedNodes.length;k++) {
		var node = categoryTreeController.selectedNodes[k];
		var categoryid = categoryTreeController.getCategoryId(node);
		// Check if it exists
		var add = true;
		for(j=0;j<this.addedCategoryNodes.length;j++){
			if(this.addedCategoryNodes[j] == categoryid) add = false;
		}

		// Only add it if its not already in the list.
		if(add) {
			var path = categoryTreeController.getFullCategoryPath(node);
			this.addedCategoryList.innerHTML += '<div id="' + this.clientId + '_addedcategory_' + categoryid 
				+ '" class="categoryTreeNode" onclick="javascript:' + this.name + '.addedNodeClick(event);" ondblclick="javascript:' + this.name + '.addedNodeDoubleClick(event);">' 
				+ this.indentCategory(path) + "</div>";
			this.addedCategoryNodes[this.addedCategoryNodes.length] = categoryid; 
			this.updateHiddenField();
		}
	}
}

// Process double click event to automatically add to the category list 
// (Double click of the span containing the catergory name)
CategoryListController.prototype.nodeDoubleClick = function(e, categoryTreeController){
	categoryTreeController.doDoubleClick(e);
	
	// Add the double clicked node to the category list 
	this.addCategory(categoryTreeController);
}

// Process the single click event for an already added category 
// (Click of the div containing the already added category name)
CategoryListController.prototype.addedNodeClick = function(e) {
	// Browser compatibility: sets the target node
	var tg = (e.target) ? e.target : e.srcElement
	
	// Get the category id
	var categoryid = tg.id.substring(this.clientId.length + 15);
		
	if(this.selectedAddedNode) var oldNode = this.document.getElementById(this.clientId + '_addedcategory_' + this.selectedAddedNode);
	if(tg) {
	
		if(this.selectedAddedNode == categoryid){
			tg.className = (tg.className == "categoryAddedTreeNode" ? "categoryAddedTreeNodeSelected" : "categoryTreeNode");
		} else {
			tg.className = "categoryAddedTreeNodeSelected";
			if(this.selectedAddedNode) oldNode.className = "categoryAddedTreeNode";
		}
		this.selectedAddedNode = categoryid;
		this.clearDocumentSelection();
	}
}

// Remove the selected node from the select Category list.
CategoryListController.prototype.removeSelected = function(tg) {
	node = this.document.getElementById(this.clientId + '_addedcategory_' + this.selectedAddedNode)
	if (node) {
		this.removeNode(node);
	}
}
	
// Process double click event to automatically remove an item from the added category list 
// (Double click of the span containing the catergory name)
CategoryListController.prototype.addedNodeDoubleClick = function(e) {
	// Browser compatibility: sets the target node
	var tg = (e.target) ? e.target : e.srcElement

	this.removeNode(tg);
}

// Remove a node from the selected category list.
CategoryListController.prototype.removeNode = function(tg) {
	var categoryid = tg.id.substring(this.clientId.length + 15);

	// Remove from added nodes array
	var key = this.getArrayKey(this.addedCategoryNodes, categoryid);
	if(key != -1) this.addedCategoryNodes.splice(key, 1);
	
	// Get the category_ div for current and previously selected node
	var nodeToRemove = this.document.getElementById(this.clientId + "_addedcategory_" + categoryid);
	if(nodeToRemove) this.addedCategoryList.removeChild(nodeToRemove);
	this.selectedAddedNode = null;
	this.updateHiddenField();
}
	

// Stops single words from being highlighted in the browser by a double click.
CategoryListController.prototype.clearDocumentSelection = function() {
	if(this.document.selection) { 
		this.document.selection.empty() 
	} else if(window.getSelection) {
		window.getSelection().removeAllRanges();
	}
}

// Add an added category to the hidden input field for select values.
CategoryListController.prototype.setAdded = function(idArray) {
	// clear down the array by recreating it
	this.addedCategoryNodes = new Array();
	
	for(i=0;i<idArray.length;i++) {
		if(idArray[i]!=-1)this.addedCategoryNodes[i] = idArray[i];
	}
}

// Render an added category to the selected category list.
CategoryListController.prototype.renderCategory = function(catID, catName) {
	var newDiv =  "<div id=\"" + this.clientId + "_addedcategory_" + catID + "\" class=\"categoryAddedTreeNode\" onclick=\"javascript:" + this.name;
	    newDiv += ".addedNodeClick(event);\" ondblclick=\"javascript:" + this.name + ".addedNodeDoubleClick(event);\">" + this.indentCategory(catName) + "</div>";
	this.addedCategoryList.innerHTML += newDiv;
}

// Updates the hidden field with the selected values.
CategoryListController.prototype.updateHiddenField = function() {
	this.addedCategories.value='';
	for(var count=0;count<this.addedCategoryNodes.length;count++) {
		this.addedCategories.value += (this.addedCategoryNodes[count] + ',');
	}
	if(this.addedCategories.value!='') this.addedCategories.value = this.addedCategories.value.substring(0,this.addedCategories.value.length-1);
}

// Generates HTML for indenting of adding category tree.
CategoryListController.prototype.indentCategory = function(catStr){
	if(!catStr.match(">")){
		// No sub cats so no identing needed, return input string
		return catStr;
	} else {
		var catArr = catStr.split(">");
		var indentStr = "";
		for(i=0;i<catArr.length;i++){
			if(i > 0) indentStr += "<br />";
			for(j=1;j<=i;j++){
				indentStr += "&nbsp;&nbsp;";
			}
			if(i>0) indentStr += "&gt;";
			indentStr += catArr[i];
		}
		return indentStr;
	}
}

// Perform an XmlHttp request for all the categories assigned to this 
CategoryListController.prototype.getAssignedCategories = function(contentID, languageID) {
	categories = new Array();
	
	var params = new Array();
	params[0] = new SOAPParam("token", this.authenticationToken);
	params[1] = new SOAPParam("contentID", contentID);
	params[2] = new SOAPParam("languageID", languageID);
	
	var soapWrapper = new SOAPWrapper(this.hostHeader, this.taggingWebservicePath);
	var soapMessage = soapWrapper.CreateSoapRequest("GetAssignedCategoryNames", "http://www.skimmingstone.com/tagging", params);
	
	if(soapWrapper.unavailable != 1) {
		var soapResponse = soapWrapper.Invoke(soapMessage, "http://www.skimmingstone.com/tagging/GetAssignedCategoryNames");
		if(!soapResponse) {
			this.displayError("Critical Failure: Unable to obtain assigned categories. " + soapWrapper.error);	
			return null;
		} else {
			var success = this.getCategorySuccessFromXML(soapResponse);
			if(success == '') {
				// Render the categories into the assigned categories DIV
				// and add to the already selected list
				categories = this.getAssignedCategoriesFromXML(soapResponse);
				for(k=0;k<categories.length;k++) {
					this.renderCategory(categories[k].ID, categories[k].Name);
					this.addedCategoryNodes[this.addedCategoryNodes.length] = categories[k].ID;
				}
				this.updateHiddenField();
			} else {
				this.displayError(success);	
				return null;
			}
		}
	} else {
			this.displayError("Critical Failure: Soap Wrapper Unavailable");	
			return null;
	}

	return categories;
}

// Retrieve the AssignedCategories from the XML returned from the Tagging web service.
CategoryListController.prototype.getAssignedCategoriesFromXML = function(xml) {
	var assignedCategories = new Array();
	if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
	var nodes = xml.getElementsByTagName("AssignedCategory");
	for(i=0;i<nodes.length;i++) {
		var assignedCategory = new AssignedCategory(nodes[i]);
		assignedCategories[assignedCategories.length] = assignedCategory;
	}
	
	return assignedCategories;
}

// Parse the XML returned from the Skimmingstone web services to check for success
// or failure of the operation.
CategoryListController.prototype.getCategorySuccessFromXML =  function(xml) {
	var categories = new Array();
	if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
	var nodes = xml.getElementsByTagName("Success");
	if(nodes.length==1) {
		if(nodes[0].firstChild.nodeValue=='true') {
			return '';
		} else {
			if(window.netscape) netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
			nodes = xml.getElementsByTagName("Message");
			if(nodes.length==1) {
				if (nodes[0].firstChild.nodeValue != "Invalid authkey" && nodes[0].firstChild.nodeValue != "Invalid authentication token") {
					return "Critical Failure: " + nodes[0].firstChild.nodeValue;
				} else {
					return ' ';
				}
			} else {
				return "Critical Failure: Unable to obtain assigned categories. Server response success was 'false'. No Message supplied."
			}
		}
	} else {
		return "Critical Failure: Unable to find Success node";
	}
}

// Function to return the key of the element in the array with the value as supplied by the value argument
CategoryListController.prototype.getArrayKey = function(array, value){
	for(i=0;i<array.length;i++){
		if(array[i] == value) return i;
	}
	return -1;
}

// Using the ContentID of the ContentItem load the already assigned items in the specified language.
// Pass an empty string if this is a new item.
CategoryListController.prototype.loadAssignedCategories = function(contentID, languageID) {
	if (contentID != null && contentID != "") {
		this.getAssignedCategories(contentID, languageID);	
	}
}

// Display a properly formatted error.
CategoryListController.prototype.displayError = function (errorString) {
	var div = document.getElementById(this.addedCategoryListId);
	if (div) div.innerHTML = '<div class="categoryTreeError"><p>' + errorString + '</p></div>';
}