/*
Author: Dean Stringer (deeknow @ pobox . com)
Original Release Date: 22/Mar/2006
This Release Date: 27/Apr/2006
Version: 0.7
Description/Purpose:

	Provides an HTML rendering of an XML micro-format representing
	notes and titles. Lists notes as expandable/collapsable
	single level tree items, and allows the user to double-click an
	expanded note's text body to edit it and its title

	Requires a browser supporting the DOM and parsing of XML
	(this means IE5.5+, Mozilla/Firefox, Netscape6+, Opera8+ amongst others)

	Layout and presentation is driven by DIVs and their CSS classes
	which can be modified in the accompanying treeEdit.css

	The main entry sub tree.init() expects to be fed a string containing
	well formed XML with a micro-format general purpose notes structure 
	like the following where there are one or more 'note' elements...
		
		<?xml version="1.0"?>
		<notes>
		<note>
			<title>Introduction</title>
			<body>This is a demonstration of use of browser-based... </body>
		</note>
		</notes>

	Useful resources:
		http://www.quirksmode.org/dom/
		http://www.w3schools.com/dom/dom_element.asp

LICENSE

treeEdit.js: XML note parse, render as tree, and edit library
Copyright (C) 2006  Dean Stringer (deeknow @ pobox . com)

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

*/

var tree = {};

tree.XmlDoc;		// the XML DOM object that will contain the note nodes
tree.resourcesURL ='';		// css, images etc
tree.allCollapsed = false;
tree.allExpanded = false;
tree.lastEditedID = '';
tree.lastToggledID = '';
tree.collapseOthersOnExpand = true;
tree.lastExpandedID = '';
tree.editing  = false;	// tracks whether user is currently editing an item
tree.editingItemID = '';	// the item we're editing currently
tree.collapseImage = 'smallminus.gif';
tree.expandImage = 'smallplus.gif';
tree.stripHTMLOnSave = false;
tree.escapeHTMLOnSave = true;
tree.collapseOnLoad = true;	// collapses all nodes on data load if true
tree.showIcons = true;	// expand/collapse icon display
tree.editAreaRows = 6;
tree.editAreaCols = 50;
tree.itemNodeName = 'note';
tree.itemTitleNodeName = 'title';
tree.itemBodyNodeName = 'body';
tree.treeNodes; // will contain the XML nodes from the parsed XML
tree.lastEditedBodyValue = '';	// so we can check if an edit has changed
tree.docRoot = '';
tree.sourceFile = '';
tree.customBodyHandler = false;
tree.dataURL = '';
tree.asynchMode = true;	// false if we're operating in local file or text mode only
tree.dataformat = 'xml';
tree.dataReq;	// will perform tree specific asynch requests
var auxDataReq;	// will perform auxillary asynch requests

if (document.getElementById && document.createElement) {
	// setup a couple of global button elements we inject from multiple places
	var lineBreak = document.createElement('BR');
	lineBreak.id='itemLineBreak1';
	var lineBreak2 = document.createElement('BR');
	lineBreak2.id='itemLineBreak2';
	var butt = document.createElement('BUTTON');
	var buttext = document.createTextNode('Close');
	butt.appendChild(buttext);
	butt.setAttribute('id','treeSave');
	butt.onclick = tree_saveEdit;
}

/**
 * Main init() for this library. Calls subs to parse XML and render as HTML
 * tree with expandable/collapsable and editable items.
 * Tree divs are typically displayed in an expanded form so that browsers that
 * aren't supporting JS but *are* supporting CSS can see the actual content,
 * so this sub is called by the parent page to collapse the expanded divs
 * @param {String} txtToParse - the text we want to parse into XML DOM
 * @param {String} mode - one of 'inline' (default) or 'file'
 * @param {String} format - one of 'xml' (default) or 'rss'
*/
tree.init=function (xmlSource, mode, format) {
	tree.dataformat = format;
	if (mode == 'file') {
		tree.loadXML(xmlSource, 'file');
	} else if (mode == 'url') {
		tree.loadXML(xmlSource, 'url');
	} else {
		tree.loadXML(xmlSource, 'inline');
	}
	if (tree.asynchMode != true)	{	// only do if 'false', ie in synchronous mode
		tree.renderForDisplay(format);
	}

}

/**
 * makes a call to tree.XMLtoHTML to render the notes into HTML then spits it into
 * a <div> named treeDisplay
 */
tree.renderForDisplay=function () {
	var treeContent = tree.XMLtoHTML(tree.dataformat);
	var treeDisplayContainer = utils.getByID("treeDisplay");
	treeDisplayContainer.innerHTML = treeContent;

	if ((treeNoteCount > 0)) { //&& tree.collapseOnLoad) {
		var treeMenu = utils.getByID("treeMenu");	/* the expand-all, collapse-all menu */
		treeMenu.style.visibility="visible";
		tree.allCollapsed == false;
		if (tree.collapseOnLoad) {
			tree.collapseAll();
		}
	}
}


/**
 * Takes a string of XML text which is to be parsed into an XML DOM object.
 * Checks if IE and uses ActiveXObject("Microsoft.XMLDOM") if so, otherwise
 * if Mozilla-compatible uses DOMParser()
 * @param {String} xmlBody - the text we want to parse into XML DOM
 * @param {String} mode - one of 'inline', 'file' or 'url'
*/
tree.loadXML=function (xmlBody, mode) {
	// inspired by: http://www.w3schools.com/dom/dom_parser.asp
	// and http://www.webmasterworld.com/forum26/119.htm
	// both non-licensed at the time (Jan/2006)
	if (window.XMLHttpRequest1) {
		// Gecko, Safari 1.2+ and Opera 7.6+
	} else if (window.ActiveXObject) {
		//load xml file - for IE
		tree.XmlDoc = new ActiveXObject("Microsoft.XMLDOM");
		if (mode == 'url') {
			utils.doAsynchRequest('GET', xmlBody, null);	// xmlBody holds the file to fetch
		} else if (mode == 'inline') {
			tree.XmlDoc.loadXML(xmlBody);
		} else {
			tree.XmlDoc.async=false;
			tree.XmlDoc.load(xmlBody);
		}
	} else if (document.implementation && document.implementation.createDocument) {
		// code for Mozilla, etc.
		//alert('loading ' + xmlBody + mode);

		if (mode == 'url') {
			tree.XmlDoc = document.implementation.createDocument("", "", null);
			utils.doAsynchRequest('GET', xmlBody, null);	// xmlBody holds the file to fetch
		} else if (mode == 'inline') {
			var parser = new DOMParser();
			tree.XmlDoc = parser.parseFromString(xmlBody, "text/xml");
		} else if (mode == 'file') {
			tree.XmlDoc = document.implementation.createDocument("", "", null);
			tree.XmlDoc.async=false;
			tree.XmlDoc.load(xmlBody);
		}
		if(tree.XmlDoc == null) alert("xml Doc Load Failed");
	} else {
		alert('Your browser does not handle XML parsing which is required on this page.');
	}
}



/**
 * Steps through the note elements within the parsed XML and writes out a bunch of
 * HTML to the browser where the notes data is wrapped in DIVs and SPANs that invoke
 * Javascript subs to expand/collapse/edit them
 * @param {String} format - one of 'xml' (default) or 'rss'
*/
tree.XMLtoHTML=function () {
	// inspired by: http://www.quirksmode.org/dom/
	// non-licensed at the time (Jan/2006)
	
	if (tree.dataformat == 'rss') {
		tree.itemNodeName = 'item';
		tree.itemTitleNodeName = 'title';
		tree.itemBodyNodeName = 'description';
	}
	tree.docRoot = tree.XmlDoc.documentElement;
	if (! tree.docRoot || tree.docRoot.childNodes.length == 0) {
		// no document root, need to create a new one and add an empty node
		var treeEmptyTitleNode = document.createTextNode("")
		var treeEmptyBodyNode = document.createTextNode("")
		var newRootNode = document.createElement("notes");
		var newNoteNode = document.createElement("note");
		var newNoteTitleNode = document.createElement("title");
		newNoteTitleNode.appendChild(treeEmptyTitleNode);
		newNoteNode.appendChild(newNoteTitleNode);
		var newNoteBodyNode = document.createElement("body");
		newNoteBodyNode.appendChild(treeEmptyBodyNode);
		newNoteNode.appendChild(newNoteBodyNode);
		newRootNode.appendChild(newNoteNode);
		tree.XmlDoc.appendChild(newRootNode);
	}

	tree.treeNodes = tree.XmlDoc.getElementsByTagName(tree.itemNodeName); // load once, can use in other places later
	treeNoteCount = tree.treeNodes.length;
	if (treeNoteCount == 0) { 
		alert('Malformed XML: it seems there were no <note> fields.');
		//return;
		}
	var rowClass = 'treeRowEven';
	var treeContent = "<div class='treeNotes'>";
	for (i=0; i<tree.treeNodes.length; i++)	{
		//alert(tree.treeNodes[i].text);
		var itemID = 'id000' + i;
		// add an 'id' attribute to this node so we can tie the XML node to the DIV we render it into 
		tree.treeNodes[i].setAttribute('id',itemID);
		var thisTitle = '';
		var thisBody = '';
		for (j=0; j<tree.treeNodes[i].childNodes.length; j++)	{
			var thisNode = tree.treeNodes[i].childNodes[j];
			if (thisNode.nodeType != 1) continue;	// skip unless a text node
			if (thisNode.nodeName == tree.itemTitleNodeName) {
				if (thisNode.childNodes.length > 0) {
					// only fetch its value if it actually has a child text node, throws an error otherwise
					thisTitle = thisNode.firstChild.nodeValue;
				}
			}
			if (thisNode.nodeName == tree.itemBodyNodeName) {
				if (thisNode.childNodes.length > 0) {
					// only fetch its value if it actually has a child text node, throws an error otherwise
					thisBody = thisNode.firstChild.nodeValue;
				}
			}
		}

		// if custom presentation of the body is required, call that now
		if (tree.customBodyHandler) {
			thisBody = customFormat(thisBody);
		}

		if ((i % 2) == 0) {	// use alternating row 'class'es
			rowClass = 'treeRowEven';
		} else { rowClass = 'treeRowOdd'; }

		// now spit out the HTML for this row embedding lots of unique ID's based on this itemID
		// so we can reference the elements from elsewhere
		var treeThisDisplayState = 'hidden';
		if (! tree.collapseOnLoad) {
			treeThisDisplayState = 'visible';
		}
		treeContent = treeContent + "<div class='" + rowClass + "'>";
		if (tree.showIcons) {
			treeContent = treeContent + "<a id='itemanchor." + itemID + "' href='javascript:tree.toggleItem(\"" + itemID + "\");' title='Collapse'>" +
				"<img id='itemimg." + itemID + "' src='" + tree.resourcesURL + '/' + tree.collapseImage + "' border='0' style='visibility: " + treeThisDisplayState + ";'/></a>";
		}
		treeContent = treeContent + "<span onclick='javascript:tree.toggleItem(\"" + itemID + "\");'>" + 
		"<h3 class='treeItemTitle' id='itemTitle." + itemID + "' style='display: inline;'>" + thisTitle + "</h3></span>" +
		"<div class='treeItemBody' id='treeItem." + itemID + "' style='display: block; visibility: " + treeThisDisplayState + ";'>" +
		"<div onDblClick='javascript:tree.edit(this)' id='itemSpan." + itemID + "'>" + thisBody +
		"</div>" +
		"</div>" + 
		"</div>";
	}
	treeContent = treeContent + "</div>";
	return treeContent;
}





/**
 * Toggles whether we want to allow users to expand more than one note
 * at a time or not. If in 'true' toggle state will collapse the last
 * openened note before expanding the selected one.
*/
tree.toggleCloseBeforeOpen = function () {
	  var modeCheckbox = utils.getByID('closeb4open');
	  if (tree.collapseOthersOnExpand == false) {
		  tree.collapseOthersOnExpand = true;
		  modeCheckbox.checked = 'checked';
	  } else {
		  tree.collapseOthersOnExpand = false;
		  modeCheckbox.checked = '';
	  }
}


/**
 * Iterates through all 'treeItem's and makes a call to toggleItem() to
 * expand the container DIV
*/
tree.expandAll = function() {
	if (tree.allExpanded == true) {return; }
	var x=document.getElementsByTagName("div")
	for(i = 1; i < x.length; i++) {
		var thisDiv = x[i];
		var thisIDattrib = thisDiv.getAttribute("id");
		if (thisIDattrib != null) {	// in case div doesnt have an id attrib
			var thisID = thisIDattrib.toString();
			if (thisID.indexOf("treeItem.") == '0') {	// will be zero if matches on 1st char of id
				var thisIDPart = thisID.substr(9);
				tree.toggleItem(thisIDPart, 'expand', 'all');
			}
		}
	}
	tree.allExpanded = true;
}


/**
 * Iterates through all 'treeItem's and makes a call to toggleItem() to
 * collapse the container div
*/
tree.collapseAll = function() {
	//if (tree.allCollapsed == true) { return; }
	var x=document.getElementsByTagName("div")
	for(i = 1; i < x.length; i++) {
		var thisDiv = x[i];
		var thisIDattrib = thisDiv.getAttribute("id");
		if (thisIDattrib != null) {
			var thisID = thisIDattrib.toString();
			if (thisID.indexOf("treeItem.") == '0') {	// will be zero if matches on 1st char of id
				var thisIDPart = thisID.substr(9);
				tree.toggleItem(thisIDPart, 'collapse', 'all');
			}
		}
	}
	tree.allCollapsed = true;
}


/**
 * Locates a container treeItem div based on the passed itemid and expands it or
 * collapses it depending on the mode paramater by changing the div's style.visibility
 * and style.display attributes
 * @param {String} itemid ID of the element we want to toggle
 * @param {String} mode One of 'collapse' or 'expand'
*/
tree.toggleItem = function (itemid, mode, allOrOne ) {
  //alert('asked to toggle' + itemid);
  var thisItem = utils.getByID('treeItem.'+itemid);
  var aimg;
  var atitle;
  //alert('ex last = ' + tree.lastExpandedID);
  tree.allCollapsed = false; tree.allExpanded = false;
  if( tree.editing == true && tree.editingItemID == itemid ) {
	// dont want to collapse if we're actually editing this item
	  if (mode != 'expand' && allOrOne != 'all' ) {
		  //alert('Cant collapse this item until you close the edit box');
	  }
	  return;
  }
  
  if( mode=='expand' || ((thisItem.style.display=="none") && (mode != 'collapse')) ) {
	// we're in EXPAND mode
	// if we're NOT in expand-all mode but we ARE in collapse-before-expand mode
	if ((allOrOne != 'all') && (tree.collapseOthersOnExpand == true))  {
		if ((tree.lastExpandedID != '') && (tree.lastExpandedID != itemid)) {
			tree.toggleItem(tree.lastExpandedID, 'collapse');	// collapse the last div we expanded, ie only show one
		}
		tree.lastExpandedID = itemid;
	}
	thisItem.style.display="block";
	thisItem.style.visibility="visible";
    aimg = tree.resourcesURL + '/' + tree.collapseImage;
    atitle = "Collapse";

  } else {
	// we're in COLLAPSE mode
	if (allOrOne == 'all') {
		tree.lastExpandedID = '';	// dont need to collapse ones self
	}
	if (tree.lastExpandedID == itemid) {
		tree.lastExpandedID = '';	// dont need to collapse ones self
	}
	thisItem.style.display="none";
	thisItem.style.visibility="hidden";
	aimg = tree.resourcesURL + '/' + tree.expandImage;
    atitle = "Expand";
  }

  thisImage = utils.getByID( 'itemimg.'+itemid );
  if (thisImage.style.visibility=="hidden") {
	  thisImage.style.visibility="visible";
  }
  thisImage.src = aimg;
  thisImage = utils.getByID( 'itemanchor.'+itemid );
  thisImage.title = atitle;
  tree.lastEditedID = itemid;
}


/**
 * Called when a user doubleClicks a text element to edit it. Finds the
 * parent SPAN tag for the element and replaces its content with a TEXTAREA
 * and a close BUTTON
 * @param {Object} obj - the text element clicked on
 */
tree.edit = function (obj) {
	if (tree.editing) {
		alert('You must close the currently edited item');
		return;
	}
	if (!document.getElementById || !document.createElement) return;
	while (obj.nodeType != 1) { // only want elements (not text nodes etc)
		obj = obj.parentNode;
	}
	if (obj.tagName == 'TEXTAREA' || obj.tagName == 'A') return;
	// move outwards from the DOM postion till we find the DIV container
	while (obj.nodeName != 'DIV' && obj.nodeName != 'HTML') {
		obj = obj.parentNode;
	}
	if (obj.nodeName == 'HTML') return;	// if we got this far theres someit wrong so return
	var itemEditTitle = document.createElement('INPUT');
	// find the item 'id' for this DIV
	for( var x = 0; x < obj.attributes.length; x++ ) {
		if( obj.attributes[x].nodeName.toLowerCase() == 'id' ) {
			tree.editingItemID = obj.attributes[x].nodeValue.substr(9);
			var itemTitle = utils.getByID('itemTitle.' + tree.editingItemID);
			itemEditTitle.value=itemTitle.innerHTML;
			itemEditTitle.id='editableTitle.' + tree.editingItemID;
		}
	}
	var thisBody = getNodeTextByID(tree.treeNodes, tree.itemBodyNodeName, tree.editingItemID);
	tree.lastEditedBodyValue = thisBody;
	//alert("id=" + thisBody);
	var itemBody = thisBody;
	//var itemBody = obj.innerHTML;
	var itemEditBody = document.createElement('TEXTAREA');
	itemEditBody.rows=tree.editAreaRows;
	itemEditBody.cols=tree.editAreaCols;
	itemEditBody.maxlenth=100;
	itemEditBody.id='editableTextarea.' + tree.editingItemID;
	var itemContainer = obj.parentNode;
	itemContainer.insertBefore(itemEditTitle,obj);
	itemContainer.insertBefore(lineBreak,obj);
	itemContainer.insertBefore(itemEditBody,obj);
	itemContainer.insertBefore(lineBreak2,obj);
	itemContainer.insertBefore(butt,obj);
	itemContainer.removeChild(obj);
	itemEditBody.value = itemBody;
	itemEditBody.focus();
	tree.editing = true;
}


/**
 * Called when user clicks on Close BUTTON after editing a notes text.
 * Updates the original text element for the note with the TEXTAREA's
 * new value then deletes the TEXTAREA and BUTTON
 */
function tree_saveEdit() {
	var itemEditBodyElement =  utils.getByID('editableTextarea.' + tree.editingItemID);
	var itemEditBody =  itemEditBodyElement.value;
	saveItemNodeValue(tree.treeNodes, tree.itemBodyNodeName, tree.editingItemID, itemEditBody);	// update the DOM element for this item's body
	if (tree.stripHTMLOnSave == true) { itemEditBody = itemEditBody.replace(/(<([^>]+)>)/ig,"");  }
	if (tree.escapeHTMLOnSave == true) {
		itemEditBody = itemEditBody.replace(/</g, '&lt;'); 
		itemEditBody = itemEditBody.replace(/>/g, '&gt;'); 
	}
	var itemEditBodyRendered = itemEditBody;

	// if custom presentation of the body is required, call that now
	if (tree.customBodyHandler) {
		itemEditBodyRendered = customFormat(itemEditBody);
	}

	var itemEditTitle = utils.getByID('editableTitle.' + tree.editingItemID);
	var itemTitle = utils.getByID('itemTitle.' + tree.editingItemID);
	itemTitleBody = itemEditTitle.value;
	itemTitle.innerHTML = itemTitleBody;
	saveItemNodeValue(tree.treeNodes, tree.itemTitleNodeName, tree.editingItemID, itemTitleBody);	// update the DOM element for this item's title
	var itemContainer = itemEditBodyElement.parentNode;
	var itemBody = document.createElement('span');
	itemBody.innerHTML = "<div onDblClick='javascript:tree.edit(this);' id='itemSpan." + tree.editingItemID + "'>" + 
		itemEditBodyRendered + '</div>';
	itemContainer.insertBefore(itemBody,itemEditBodyElement);
	itemContainer.removeChild(itemEditBodyElement);
	itemContainer.removeChild(itemEditTitle);
	var lineBreak = utils.getByID('itemLineBreak1');
	itemContainer.removeChild(lineBreak);
	lineBreak = utils.getByID('itemLineBreak2');
	itemContainer.removeChild(lineBreak);
	var button = utils.getByID('treeSave');
	itemContainer.removeChild(button);
	tree.editing = false;

	if (tree.lastEditedBodyValue != itemEditBody) {
		var saveNote = confirm('The note has changed, do you want to save?');
		if (saveNote) {
			asynchMsgStatusUpdate('Saving...');
			utils.doAsynchRequest('POST', navLastWord + '.xml', tree.XmlDoc.documentElement);
		}
	}

	tree.editingItemID = '';
	tree.lastEditedBodyValue = '';
	//confirm('Save?');
}
