/*
 * VARIABLES
 */
COLCLASS = "p2column";
COLLOADCLASS = "p2loadingcolumn";
HOLDCLASS = "p2colview";
ARBTALLCOL = 2000; //first column is made this tall briefly to get the view size. since I have reservations about most html engines' ability to deal with large dimensions I kept this reasonably small
SCROLLFPS = 30; //frames per second for scolling animation
SCROLLSECS = 0.33; // duration of scrolling animation in seconds

// preload select images to get around IE bug where the images are not shown until mouseover 
// nodedeletePreload = new Image();
// nodedeletePreload.src = '/homes_2_3/homes.web/addcancel.gif';
// addcancelPreload = new Image();
// addcancelPreload.src = '/homes_2_3/homes.web/addcancel.gif';
// addsubmitPreload = new Image();
// addsubmitPreload.src = '/homes_2_3/homes.web/addon.gif';


/*
 * PROTOTYPE CHANGES
 */
	/*
	 * .bind()
	 *
	 * makes JS object callbacks all nice
	 *
	 * based on the version from prototype.js, which is licensed under an open version of the MIT license
	 */
Function.prototype.bind = function(object) {
	var __method = this;
	return function() {
		__method.apply(object, arguments);
	};
};


/*
 * CLASSES
 */

function ScrollManager(view) {
	/*
	 * Manages the scrolling & animation for an instance of ColumnView
	 */
	this.SANE_TIME = 2000; // if we've been animating for more than this (in milliseconds) kill the animation
	
	this.view = view;
	var dest = null; // the destination we're heading toward
	var lastx = null; // the last position. this stops us from getting 'caught' on limits
	
		// animation variables
	var perframe = null;
	var step = null;
	var startTime = 0;
	var timeout = null;
	
	/*
	 * METHODS of SCROLLMANAGER
	 */
		/*
		 * PRIVATE METHODS - haha
		 */
	this._calcAnimation = function() {
		// calculate animation settings
		// call once the view is setup (we need column sizes)
		perframe = Math.round(1000 / SCROLLFPS);
		step = Math.round((dest - view.view().scrollLeft) / (SCROLLFPS * SCROLLSECS));
	};
	this._scrollPulse = function() {
		var stopScroll = false; // kills the next iteration if true
		curDate = new Date();
		if (curDate.getTime() - startTime > this.SANE_TIME) {
			// in case something goes wrong this will kill the animation after a short time
			stopScroll = true;
		}
		if (this.isCloseEnough()) {
			stopScroll = true;
		} else {
			var newx = view.view().scrollLeft + step;
			if (lastx == newx) {
				// we've snagged an edge or something
				stopScroll = true;
			} else {
				lastx = newx;
				this.jumpTo(newx);
			}
		}

		if (stopScroll) {
			this.jumpTo(dest);
			this.stop();
		} else {
			this._setupPulse();
		}
	};
	this._setupPulse = function() {
		timeout = setTimeout(this._scrollPulse.bind(this), perframe); // thanks to OO and .bind we can call this anywhere // make sure private methods can work with this
	};
		/*
		 * PUBLIC METHODS
		 */
	this.isCloseEnough = function() {
		// if we're 'close enough' to the destination
		if (Math.abs(dest - this.view.view().scrollLeft) < Math.abs(step)) {
			return true;
		}
		return false;
	};
		// set the destination. we animate there
	this.headTo = function(x) {
		var curTime = new Date();
		startTime = curTime.getTime();
		dest = x;
		this._calcAnimation();
		this._setupPulse();
	};
	this.jumpTo = function(x) {
		// jump to a position immediately. no animation
		view.view().scrollLeft = x;
	};
	this.stop = function() {
		// kill the transition no matter where we are
		dest = this.view.view().scrollLeft;
		startTime = 0; // will fail all time-based sanity checks
		clearTimeout(timeout);
	};
	this.isMoving = function() {
		// are we in transition somewhere?
		return !this.isCloseEnough(); //
	};
}

	/*
	 *  Deals with the actual fetching of data from the server
	 */
function DataHolder() {
	var root;
	var cache = {};
	var restored = {};
	// class for dealing with asyncronous returns
	this.getRootNode = function() {
		if (typeof(root) == 'undefined') {
			root = new ColumnNode("World");
			root.parent = null;
			root.setView(this.view);
		}
		return root;
	};
	this.setView = function(view) {
		this.view = view; // expects an instance of ColumnView to send data back
	};
	this.getChildrenOf = function(node, callback) {
		if (cache[node.getPath()]) {
			callback(cache[node.getPath()]);
		} else {
			if (node.attributes.New) {
				if (restored[node.getPath()]) {
					for (var jj=0; jj < restored[node.getPath()].length; jj++) {
						var child = restored[node.getPath()][jj];
						child.parent = node;
						node.add(child);
						this.cacheNode(node);
					}
				}
				callback(node);
			} else {
			    this.sendCallback(node.getPath(), this.receiveCallback.bind(this), [node, callback]); // sendCallback is setup by the C#
			}
		}
	};
	this.receiveCallback = function(result, context) {
		// be careful to keep the format assumed here in sync with ColumnView.cs which provides it
		var node = context[0];
		if (result.replace(/[ \t\n\r]/, "") == "") {
			node.setEmpty();
		} else {
			// first catalogue the nodes we already have
			var oldChildren = node.children;
			node.children = new Array();
			if (restored[node.getPath()]) {
				for (var jj=0; jj < restored[node.getPath()].length; jj++) {
					oldChildren.push(restored[node.getPath()][jj]);
				}
			}
			lines = result.split("\n"); // keep in sync with ColumnView.cs
			for (var ii=0; ii < lines.length; ii++) {
				if (line == ";;END;;") {
					break; //ignore everything after this
				}
				var line = lines[ii];
				var parts = line.split("|");
				if (parts.length > 1) {
					// otherwise something is wrong, don't try creating a node
					var newnode = node.createChild(); // parts[0] is name
					newnode.parseData(line);
					for (var jj=0; jj < oldChildren.length; jj++) {
						if (newnode.getPath() == oldChildren[jj].getPath()) {
							// its the same node
							if (oldChildren[jj].startSelected) {
								newnode.startSelected = true;
							}
							break;
						}
					}
				}
			}
			//add new nodes
			for (var ii=0; ii < oldChildren.length; ii++) {
				var child = oldChildren[ii];
				if (child.attributes.New) {
					if (!child.parent)
						child.parent = node;
					this.getChildrenOf(child, function(){}); // flesh out the tree - new nodes will only ever have new node children
					this.cacheNode(node);
					node.add(child);
				}
			}
		}
		context[1](node);
	};
	this.cacheNode = function(node) {
		cache[node.getPath()] = node;
	};
	this.restore = function(dataStr) {
		var node = new ColumnNode();
		node.parseData(dataStr);
		if (node.attributes.ParentPath) {
			node.setView(this.view);
			if (!restored[node.attributes.ParentPath])
				restored[node.attributes.ParentPath] = new Array();
			restored[node.attributes.ParentPath].push(node);
			this.view.store(node);
			this.view.fireEvent('onadd', node);
		}
	};
}

	/*
	 * Each clickable item in each column maps to an instance of
	 * this class
	 */
function ColumnNode(name) {
	this.parent = null; // the node parent, not the column it's in
	this.clientId;
	var view = null;
	this.column = new Column(this);
	this.name = name;
	var has_data = false;
	this.children = new Array();
	this.hasSelectedDescendants = false;
	this.isGrowable = false;
	this.selectable = false;
	this.selected = false;
	this.attributes = {};
	var depth = null;
	var path = "";
	
	this.setView = function(newview) {
		view = newview;
		this.column.setView(newview);
	};
	this.view = function() {
		return view;
	};
	this.createChild = function(pos) {
		// shortcut for adding children (important for bandwidth)
		// position is optional
		this.setEmpty(true);
		var node = new ColumnNode(name);
		node.parent = this;
		node.setView(view);
		if (typeof(pos) == 'undefined') {
			this.children.push(node);
		} else {
			this.children.splice(pos, 0, node);
		}	
		return node;
	};
	this.addChild = function(child, pos) {
		this.setEmpty(true);
		if (typeof(pos) == 'undefined') {
			this.children.push(child);
		} else {
			this.children.splice(pos, 0, child);
		}
	};
	this.attrTrim = function trim(attr)
    {
        s = attr.replace(/^(\s)*/, '');
        s = s.replace(/(\s)*$/, '');
        return s;
    };
	this.parseData = function(datastr) {
		// parses data from the server-created string
		var parts = datastr.split("|");
		this.name = unescape(parts[0]); // parts[0] is name
		this.setPath(unescape(parts[1])); // parts[1] is path
		if (parts.length > 2) {
			// we have an isGrowable flag
			if (parts[2] == "True") {
				this.isGrowable = true;
			}
			if (parts[3] == "True") {
			// can be multiselected
			    this.selectable = true;
			}
			if (parts[4] == "True") {
				// is an empty leaf node, to prevent uneccessary callbacks
				has_data = true;
			}
			if (parts.length > 5) {
				// we have attributes
				attrParts = parts[5].split(":");
				for (var ii=0; ii < attrParts.length ; ii++) {
					keyval = attrParts[ii].split("=");
					this.attributes[unescape(keyval[0])] = unescape(this.attrTrim(keyval[1])); // 0=key, 1=value
				}
			}
		}
		if (this.view() && this.view().multiSelectEnabled) {
		    if (this.view().hierarchicalSelection) {
		        if ((this.view().selectedNodes[this.getPath()] || this.view().ancestorSelectedFor(this)) && !this.view().prunedNodes[this.getPath()] && !this.view().onPathToPruned(this)) {
		            this.selected = true;
		            this.hasSelectedDescendants = !this.isLeaf();
		        } else if (this.view().onPathToPruned(this) || this.view().onPathToSelected(this)) {
		            this.selected = false;
		            this.hasSelectedDescendants = true;
		        } else {
		            this.selected = false;
		            this.hasSelectedDescendants = false;
		        }
		    } else {
		        if (this.view().selectedNodes[this.getPath()]) {
		            this.selected = true;
		            this.hasSelectedDescendants = this.view().onPathToSelected(this);
		        } else if (this.view().onPathToSelected(this)) {
		            this.selected = false;
		            this.hasSelectedDescendants = true;
		        } else {
		            this.selected = false;
		            this.hasSelectedDescendants = false;
		        }
		    }		            
		}
	};
	this.encodeData = function() {
		var datastr = "";
		var parts = new Array();
		parts.push(escape(this.name));
		parts.push(escape(this.getPath()));
		parts.push(this.isGrowable ? "True" : "");
		var attrparts = new Array();
		for (var ii in this.attributes) {
			attrparts.push(escape(ii)+"="+escape(this.attributes[ii]));
		}
		if (!this.attributes.ParentPath)
			attrparts.push("ParentPath="+escape(this.parent.getPath()));
		parts.push(attrparts.join(":"));
		datastr = parts.join("|");
		return datastr;
	};
	this.hasData = function() {
		return has_data;
	};
	this.isLeaf = function() {
		return has_data && this.children.length == 0;
	};
	this.setEmpty = function(val) {
		if (typeof(val) != 'undefined') {
			has_data = val;
		} else {
			has_data = true;
		}
	};
	this.depth = function() {
		if (depth == null) {
			depth = 0;
			var vader = this.parent;
			while (vader != null) {
				depth++;
				vader = vader.parent;
				if (depth > 1000) {
					break; //sanity
				}
			}
		}
		return depth;
	};
	this.add = function(newnode) {
		// find the position, add the node, then return its new position
		var newpos = this.getPosition(newnode);
		this.addChild(newnode, newpos);
		return newpos;
	};
	this.getPosition = function(n) {
		// uses a lame sort to get the position in the parent node for insertion
		return this.search(n, 0, this.children.length-1, 0);
	};
	this.setPath = function(newpath) {
		path = newpath;
	};
	this.getPath = function() {
		return path;
	};
	this.search = function(n, min, max, depth) {
		// I wrote this on cold medicine. sorry -n
		var par = this.children;
		if (par.length == 0) {
			return null;
		}
		if (depth > 100) {
			// probably broken recursion
			return null;
		}
		if (n.name.toLowerCase() < par[min].name.toLowerCase()) {
			return min;
		}
		if (n.name.toLowerCase() > par[max].name.toLowerCase()) {
			return max;
		}
		if (max - min == 1) {
			return max;
		}
		depth++;
		var med = parseInt((max - min) / 2);
		if (n.name.toLowerCase() < par[min+med].name.toLowerCase()) {
			return this.search(n, min, min+med, depth);
		} else {
			return this.search(n, min+med, max, depth);
		}
	};
	this.removeChild = function(node) {
		var ii;
		for (ii=0; ii < this.children.length; ii++) {
			if (node.getPath() == this.children[ii].getPath()) {
				break;
			}
		}
		this.children.splice(ii, 1);
	};	
	this.setSelected = function(isSelected) {
	    if(isSelected) {
	        this.view().addSelected(this);
	        this.selected = true;
	    } else {
	        this.view().removeSelected(this);
	        this.selected = false;
	        if(this.view().multiSelectEnabled && this.view().hierarchicalSelection) {
	            this.hasSelectedDescendants = false;
	        }
	    }
	    if(this.parent) {
	        this.parent.updateSelectedStatus();
	    }
	};	
	this.updateSelectedStatus = function() {
	    var ii;
	    var stateChanged = false;
	    var wasSelected = this.selected;
	    if (this.view().hierarchicalSelection && this.children.length > 0) {
	        this.selected = true; 
	    }
	    var hadSelectedDescendants = this.hasSelectedDescendants;
	    this.hasSelectedDescendants = false; 
	    
	    var lastStatus;
	    for(ii=0;ii<this.children.length;ii++) {
	        if(!this.children[ii].selected) {
	            if (this.view().hierarchicalSelection) {
	                this.selected = false;
	            }
	            if (this.children[ii].hasSelectedDescendants) {
	                this.hasSelectedDescendants = true;
	            }
	        } else {
	            this.hasSelectedDescendants = true;
	        } 
	        if(ii > 0 && (lastStatus != this.children[ii].selected)) {
	            break;
	        }
	        lastStatus = this.children[ii].selected;      
	    }
	    if (this.view().hierarchicalSelection) {
	        if(hadSelectedDescendants && !this.hasSelectedDescendants) {
	            this.selected = false;
	            stateChanged = this.view().removeSelected(this);
	        } 
	        if (!this.wasSelected && this.selected) {
	            this.selected = true;
	            stateChanged = this.view().addSelected(this);
	        }
	    } 
	    if(this.parent) {
	        stateChanged = (this.parent.updateSelectedStatus() || stateChanged);
	    }
	    else {
	        return false;
	    }
	    return stateChanged;
	};
}

	/* Each column maps to an instance of this class.
	 */
function Column(node) {
	var colid; //used to store the div
	var coladdid = null; // store the add div id, if there is one
	var view;
	this.col = function() {
		return document.getElementById(colid);
	};
	this.setView = function(newview) {
		view = newview;
	};
	this.coladd = function() {
		return document.getElementById(coladdid);
	};
	this.view = function() {
		return view;
	};
	this.clear = function() {
		// clears the contents of the column for redrawing
		var coldiv = document.getElementById(colid);
		for (var ii=0; ii < coldiv.childNodes.length; ii++) {
			var child = coldiv.childNodes[ii];
			coldiv.removeChild(child);
		}
		coldiv.className = COLCLASS;
	};
	this.drawInto = function(coldiv) {
		colid = coldiv.id;
		if (node.isGrowable) {
			// "add missing" panel
			this.showAddPanelFor(coldiv);
		}
		
		if(node.selectable && !this.view().multiSelectEnabled){
		    var selpanel = document.createElement('div');
			    selpanel = coldiv.appendChild(selpanel);
			    selpanel.innerHTML = 'You Have Selected<br/><span class="p2selname">'+node.name+"</span>";
			    coldiv.className = COLCLASS + " p2selectedcol";
	    }
			
		if (node.children.length > 0) {
		    if(selpanel){
		        selpanel.className = "p2selectedcolmsghc";
		    }
		    //selpanel.innerHTML = selpanel.innerHTML + 'or choose from below';
			for (var ii=0; ii < node.children.length; ii++) {
				var newnode = node.children[ii];	
				if (this.view().multiSelectEnabled) {
                    if (this.view().hierarchicalSelection) {
                        if (node.selected) {
				            newnode.selected = true;//force newnode to selected if parent is selected
                        } else if (!node.hasSelectedDescendants){
    				        newnode.selected = false;//newnode cannot be select if parent has not selected children
    				        newnode.hasSelectedDescendants = false;//newnode cannot have selected children if parent does not
    				    }
    				} else if (node.hasSelectedDescendants) {
    				    newnode.selected = node.children[ii].selected;//newnode maintains previous selected status
				        newnode.hasSelectedDescendants = node.children[ii].hasSelectedDescendants;//newnode maintains previous selected child status
				    }
				} else {
				    //this are irrelevant outside of multiselect mode, setting just to be sure
				    newnode.selected = false;
				    newnode.hasSelectedDescendants = false;
				}
				
				var newlink = this.makeColItem(newnode);
				node.children[ii].clientId = newlink.id;
				coldiv.appendChild(newlink);
				if (newnode.startSelected) {
					newlink.className += " p2colitemsel";
					newnode.startSelected = false;
					this.lastColItem = newlink;
				}
			}
			coldiv.className = COLCLASS;
		} else {
		    if(selpanel){
		        selpanel.className = "p2selectedcolmsg";
		    }
		}
	};
	this.makeColItem = function(itemnode) {
		var div = document.createElement('div');
		div.className = "p2colitem";
		if(itemnode.isLeaf()) {
		    div.className += " leaf";
		}
		div.node = itemnode;
		div.onclick = this.onColItemClick.bind(this);
		div.setAttribute('unselectable', 'on'); // for IE. Mozilla gets this from CSS
		if (this.view().multiSelectEnabled && itemnode.selectable){
		    var selDiv = document.createElement('div');
		    if(itemnode.selected){
		        selDiv.className = "p2colitemselectable selected";        
		    } else if (itemnode.hasSelectedDescendants) {
		        selDiv.className = "p2colitemselectable partial";
		    } else {
		        selDiv.className = "p2colitemselectable";
		    }
		    selDiv.onclick = this.onColItemClick.bind(this);
		    div.appendChild(selDiv);
		}
		if (itemnode.attributes.New) {
			div.innerHTML += '<div class="p2colitemnewbadge"></div>'+itemnode.name+"&nbsp;";
			var delImg = nodedeletePreload.cloneNode(false);
				delImg.alt = "Delete "+itemnode.name;
				delImg.align = "absmiddle";
				delImg.onclick = this.onDelClick.bind(this);
			div.appendChild(delImg);			
		} else {
			div.innerHTML += itemnode.name;
		}
		div.id = this.getLinkID();
		return div;
	};
	this.onDelClick = function(e) {
		if (!e) {
			e = window.event;
		}
		var el = (e.target) ? e.target : e.srcElement;
		var delnode = el.parentNode.node;
		var childMsg = "";
		if (delnode.children.length > 0)
			childMsg = " and all its children";
		if (confirm('Delete '+delnode.name+childMsg+'?')) {
		    var stateChanged = false; 
		    if(this.view().multiSelectEnabled) {
		        //have to orphan the node while we remove it from selected to avoid 
		        //some state logic based on parent checking in removeSelected
		        var parentOfDeleted = delnode.parent;
		        delnode.parent = null;
		        stateChanged = this.view().removeSelected(delnode);
		        delnode.parent = parentOfDeleted;
		        //remove selected doesn't do this in non-hierarchical selection, so force it
		        if(!this.view().hierarchicalSelection) {
		            stateChanged = (this.view().clearChildStateFor(delnode) || stateChanged);	
		        }	       
		    }
		    this.col().removeChild(el.parentNode);
			delnode.parent.removeChild(delnode);
		    if(this.view().multiSelectEnabled) {
		        stateChanged = (parentOfDeleted.updateSelectedStatus() || stateChanged);
		        this.redrawNode(parentOfDeleted);
		        if (stateChanged) {
			        this.view().fireEvent('selStateChanged',this.view().renderSelectionState());
			    }
		    }
			this.view().fireEvent('ondel', delnode);
			this.view().unstore(delnode);
			this.view().showColumnFor(delnode.parent);
			this.view().fireEvent('onselect', delnode.parent);
			
		}
	};
	this.onColItemClick = function(e) {
		if (!e) {
			e = window.event;
		}
		var targetElement = (e.target) ? e.target : e.srcElement;
		var el;
		
		if (targetElement.className.indexOf('select') != -1 && this.view().multiSelectEnabled){
		    el = targetElement.parentNode;
		    var div = document.getElementById(el.id);
		    if(el.node.selected){
		        el.node.setSelected(false);
		        this.view().fireEvent('selStateChanged',this.view().renderSelectionState());
		    } else {
		        el.node.setSelected(true);
		        this.view().fireEvent('selStateChanged',this.view().renderSelectionState());
		    }
		    this.redrawNode(el.node);	
		}
		else{
		    el = targetElement;
		}
		    
        if (el.node) {
			if (this.lastColItem) {
				this.lastColItem.className = this.lastColItem.className.replace(/ p2colitemsel/, "");
			}
			el.className += " p2colitemsel";
			if (el.className.indexOf("p2colitemnew") >= 0) {
				el.className = el.className.replace(/p2colitemnew/, "p2colitemnewsel");
			}
			this.lastColItem = el;
			this.view().showColumnFor(el.node);
			this.view().fireEvent('onselect', el.node);
		}
	}
	this.redrawNode = function(node) {
	    var div = document.getElementById(node.clientId);
	    if (div && div.node && div.node.selectable) {
	        var ii;
	        for(ii=0;ii<div.childNodes.length;ii++) {
	            if(div.childNodes[ii].tagName == 'DIV' && div.childNodes[ii].className.indexOf("select") >= 0) {
	                break;
	            }
	        }
	        if(node.selected) {
                div.childNodes[ii].className = "p2colitemselectable selected";
	        }
	        else if (node.hasSelectedDescendants) {      
                div.childNodes[ii].className = "p2colitemselectable partial"; 
	        } else {
	            div.childNodes[ii].className = "p2colitemselectable"; 
	        }
            this.redrawNode(div.node.parent);
	    }
	};
	this.getLinkID = function() {
		return colid+"link"+this.col().childNodes.length;
	};
	this.showAddPanelFor = function(col) {
		addpanel = document.createElement('div');
		this.view().view().appendChild(addpanel);
		addpanel.className = "p2additem";
		addpanel.id = col.id+"add";
		coladdid = addpanel.id;
		this.showAddTextFor(addpanel);
		addpanel.style.width = col.offsetWidth+"px";
		addpanel.style.left = col.offsetLeft+"px";
		addpanel.style.top = (this.view().colHeight - addpanel.offsetHeight)+"px";
		col.style.height = (this.view().colHeight-addpanel.offsetHeight)+"px";
	};
	this.showAddTextFor = function(addpanel) {
		addtext = document.createElement('a');
		addpanel.appendChild(addtext);
		addtext.href = "javascript:void(0);";
		addtext.setAttribute("unselectable", "on");
		addtext.innerHTML = "Add Missing";
		addtext.className = "p2addtext";
		addtext.onclick = this.addTextClick.bind(this);
		addtext.style.width = "100%";
	};
	this.addTextClick = function(e) {
		if (!e) {
			e = window.event;
		}
		var el = (e.target) ? e.target : e.srcElement;
		var parentEl = el.parentNode;
		el.style.display = "none";
		var addinput = document.createElement('input');
		parentEl.appendChild(addinput);
		addinput.className = "p2addinput";
		addinput.style.width = (parentEl.offsetWidth - 47)+"px"; // 41 for the submit & cancel buttons
		addinput.style.height = (parentEl.offsetHeight - 6)+"px"; // 6 for an edge
		addinput.onkeypress = this.addInputKeypress.bind(this);
		
		
		// submit button
		addsubmitPreload.align = "absmiddle";
		parentEl.appendChild(addsubmitPreload);
		addsubmitPreload.className = "p2addsubmit";
		addsubmitPreload.onclick = this.handleAdd.bind(this);
		
		// cancel button
		addcancelPreload.align = "absmiddle";
		parentEl.appendChild(addcancelPreload);
		addcancelPreload.className = "p2addcancel";
		addcancelPreload.onclick = this.cancelAdd.bind(this);
		
		// now we can type
		addinput.focus();
		
		// register this column with the CV
		this.col().style.backgroundColor = "#f6fdf8";
		this.view().addingFor(this);
	};
	this.cancelAdd = function(e) {
		this.col().style.backgroundColor = "";
		this.view().addingFor(false);
		
		var el = this.coladd();
		var numchil = el.childNodes.length;
		for(ii = 0; ii < numchil; ii++) {
			el.removeChild(el.childNodes[0]);
		}
		this.showAddTextFor(el);
	};
	this.handleAdd = function(e) {
		if (!e) {
			e = window.event;
		}
		var el = (e.target) ? e.target : e.srcElement;
		var newname = '';
		siblings = el.parentNode.childNodes;
		for (var ii = 0; ii < siblings.length; ii++) {
			var sibling = siblings[ii];
			try {
				if (sibling.tagName.toLowerCase() == 'input') {
					newname = sibling.value;
				}
			} catch (e) { /* swallow - yum */ }
		}
		var coldiv = document.getElementById(colid);
		if (coldiv.className.match(/p2selectedcol/)) {
			this.clear();
		}
		this.addWithName(newname);
	};
	this.addWithName = function(newname) {
		var result = this.view().fireEvent('addvalidate', newname);
		if (result.fired) {
			if (!result.output) {
				return;
			}
		} else {
			if (newname == "") {
				return;
			}
		}
		this.col().style.backgroundColor = "";
		var newnode = new ColumnNode(newname);
		newnode.setView(this.view());
		newnode.parent = node;
		newnode.setEmpty();
		newnode.isGrowable = true; // if it can be added assume it can have additions
		newnode.attributes.New = true;
		newnode.setPath(newnode.parent.getPath() + this.view().pathSeparator() + 'cvNew '+this.view().getNewCount());
		newnode.selectable = this.view().multiSelectEnabled;
		newnode.selected = this.view().hierarchicalSelection && newnode.parent.selected;
		if(newnode.selected) {
		    newnode.parent.hasSelectedDescendants = true;
		}
		//var newpos = node.add(newnode);
		//this.view().cacheNode(node);
		//var newdiv = this.makeColItem(newnode);
        var newdiv = this.makeColItem(newnode);
        newnode.clientId = newdiv.id;
		var newpos = node.add(newnode);
		this.view().cacheNode(node);
		if (newpos == null) {
			this.col().appendChild(newdiv);
		} else {
			this.col().insertBefore(newdiv, this.col().childNodes[newpos]);
		}
		this.cancelAdd();
		this.view().store(newnode);
		this.view().fireEvent('onadd', newnode);
	};
	this.addInputKeypress = function(e) {
		if (!e) {
			e = window.event;
		}
		if (e.keyCode == 13) { //13 is enter/return
			this.handleAdd(e);
			if (e.preventDefault) {
				e.preventDefault(); // stop the form from submitting w3c
			}
			e.returnValue = false; // stop submission in ie
		}
		return true;
	};
	this.remove = function() {
		if (coladdid != null) {
			this.col().parentNode.removeChild(this.coladd());
		}
	};
}

	/* An abstraction for the column view control that
	 * can deal with adding columns, etc.
	 * - takes the id of the container div as an argument
	 * - second argument is the object that will return data for columns as requested
	 *
	 * Works like this: events from clicking go to the column, it can check if it has
	 * what it needs, and clean up the UI as needed, then asks for the data.
	 */
function ColumnView(divid, dataholder) {
	//this pseudo constructor is called in the last line of this class
	var waiting = new Array(null, null); /// columns & nodes we're waiting for data. in case the user changes columns before we have data and it comes back we shouldn't draw it.
	this.numcol = 3; // 3 is just the default. set with setColumnCount()
	this.multiSelectEnabled = false; // false is the default. set with setMultiSelectEnabled()
	this.hierarchicalSelection = false; // false is the default. set with setMultiSelectEnabled()
	this.viewOverlap = 15; // just the default. set with setViewOverlap()
	this.events = {};
	var hiddenID = "";
	var addingCol;
	var stored = new Array();
	this.selectedNodes = {};
	this.prunedNodes = {};
	var pathSeparator = "-"; //default
	
	this.init = function() {
		this.scroller = new ScrollManager(this);
		dataholder.setView(this);
		this.colHeight = false; // will get set once we draw our first column
		this.colWidth = false; // ditto
		this.root = dataholder.getRootNode();
		//this.fetchChildrenFor(this.root);
		//this.curcol = 0;
	};
	this.view = function() {
		/* So why a method instead of keeping a reference?
		 * To stop a circular reference involving the DOM from 
		 * leaking memory in IE.
		 */
		return document.getElementById(divid);
	};
	this.setColumnCount = function(num) {
		this.numcol = num;
	};
	this.setViewOverlap = function(value) {
		// is always in pixels
		this.viewOverlap = parseInt(value);
	};
	this.waitingFor = function(node) {
		ii = this.colNumFor(node);
		waiting[ii] = node.name;
	};
	this.isWaitingFor = function(node) {
		ii = this.colNumFor(node);
		if (typeof(waiting[ii]) != 'undefined'
				&& waiting[ii] == node.name) {
			return true;
		}
		return false;
	};
	this.fetchChildrenFor = function(node, shouldntScroll) {
		this.drawColumnFor(node, shouldntScroll);
		if (node.hasData()) {
		    if(!(this.multiSelectEnabled && node.isLeaf() && !node.isGrowable)) {
			    this.drawChildrenFor(node);
			}
		} else {
			this.waitingFor(node);
			dataholder.getChildrenOf(node, this.childrenAddedTo.bind(this));
		}
	};
	this.childrenAddedTo = function(node) {
		if (node == null) {
			return; // should be an associative array
		} else if (node.children == null) {
			return;
		}
		if (this.isWaitingFor(node)) {
			this.drawChildrenFor(node);
		}
	};
	this.colNumFor = function(node) {
			/// Get the column number
		var colnum = 0;
		var curnode = node;
		while (curnode.parent != null) { // root's parent is null
			curnode = curnode.parent;
			colnum++;
		}
		return colnum;
	};
	this._minScrollHolderWidth = function() {
		return this.view().scrollLeft + this.viewWidth;
		//this.scrollHolder.style.width = (this.colWidth * (ii+1))+"px";
	};
	this.drawColumnFor = function(node, shouldntScroll) {
		var colnum = this.colNumFor(node);
			/// Delete the now redundant columns
		var firstdel = true; //treat the first deletion special to stop view jumping around
		if (colnum > 0) {
			for (var ii=this.curcol; ii >= colnum; ii--) { //count down in case the browser redraws midway, the columns will be disappearing logically
				var delcolid = this.getIdForColNum(ii);
				var delcol = document.getElementById(delcolid);
				if (firstdel) {
					var newwidth = this._minScrollHolderWidth(); // if we make the holder smaller then the view will jump
					this.scrollHolder.style.width = newwidth+"px";
					firstdel = false;
				}
				var delcoladd = document.getElementById(delcolid+"add");
				if (delcoladd) {
					this.view().removeChild(delcoladd);
				}
				if (delcol) {
				    this.view().removeChild(delcol);
				    delete(delcol);
				}
			}
		}
		//no column to show in this case, as the "selected" message is meaningless
		if(!(this.multiSelectEnabled && node.isLeaf()) || node.isGrowable) {
			    /// Create the column
		    var newcol = document.createElement('div');
		    newcol = this.view().appendChild(newcol);
		    newcol.className = COLCLASS;
		    if (!node.isLeaf()) {
		        newcol.className += " "+COLLOADCLASS;
		    }
		    newcol.id = this.getIdForColNum(colnum);
		    newcol.style.zIndex = 500;
    		
			    ///	Automatic column sizing goodness
		    if (!this.colHeight || !this.colWidth) {
				    // Column height
			    newcol.style.height = ARBTALLCOL+"px"; // arbitrary 'big' number. Hopefully not fullscreen on a 30 inch monitor
			    var boxedge = newcol.offsetHeight - ARBTALLCOL; // keep track of box model differences in case the columns have borders, margin, etc
			    this.view().scrollTop = 9999; //arbitrary other big number. should clip
			    this.colHeight = newcol.offsetHeight - this.view().scrollTop - boxedge;
			    this.view().scrollTop = 0;
    		
				    // View inner width
				    // this method is more "bulletproof" than .innerWidth
			    newcol.style.width = ARBTALLCOL+"px";
			    this.view().scrollLeft = 9999;
			    this.viewWidth = newcol.offsetWidth - this.view().scrollLeft;
    			
				    // Column width
				    // 100 is a throwaway number here to get the columns boxedge
			    newcol.style.width = "100px";
			    var boxedge = newcol.offsetWidth - 100; //damn box models
			    this.colWidth = Math.floor((this.viewWidth - this.viewOverlap) / this.numcol) - boxedge;
    		
				    // now create the scroll holder (used when column are deleted but we still need extra space for the animation)
			    scrollHolder = document.createElement('div');
			    this.scrollHolder = this.view().appendChild(scrollHolder);
			    this.scrollHolder.style.position = "absolute";
			    this.scrollHolder.style.backgroundColor = "#eee";
			    this.scrollHolder.style.left = "0px";
			    this.scrollHolder.style.width = this.colWidth+"px";
			    this.scrollHolder.style.height = '5px';
			    this.scrollHolder.zindex = 5;
		    }
		    newcol.style.height = this.colHeight+"px";
		    newcol.style.width = this.colWidth+"px";
		    newcol.style.left = newcol.offsetWidth * colnum + "px";
    		
		    this.curcol = colnum;
		    if (!shouldntScroll) {
			    this.scrollTo(newcol.offsetLeft - newcol.offsetWidth - this.viewOverlap);
		    }
        } 
	};
	this.drawChildrenFor = function(node) {
		var colnum = this.colNumFor(node);
		//Remove selected message from previous column, if present.
		var prevColdiv = document.getElementById(this.getIdForColNum(colnum-1));
		if(prevColdiv)
		{
		    if(prevColdiv.childNodes[0] && !prevColdiv.childNodes[0].node)
		        prevColdiv.removeChild(prevColdiv.childNodes[0]);
		}
		var coldiv = document.getElementById(this.getIdForColNum(colnum))
		
		node.column.drawInto(coldiv);
	};
	this.scrollTo = function(left) {
		// BOUNDS CHECKING
		if (left > (this.curcol-1) * this.colWidth)
			left = (this.curcol-1) * this.colWidth;
		if (left < 0) 
			left = 0;
		if (left != this.view().scrollLeft) {
			this.scroller.headTo(left);
		}
	};
	this.jumpTo = function(left) {
		// bounds checking isn't important for jumping - no animation to mess up
		this.scroller.jumpTo(left);
	};
	this.showColumnFor = function(node, shouldntScroll) {
	    this.fetchChildrenFor(node, shouldntScroll);
	};
	this.getIdForColNum = function(colnum) {
		return this.view().id+"_col"+colnum;
	};
	/* EVENTS
	 * specified by an id of a global function
	 */
	this.setEventId = function(eventKey, funcId) {
		this.events[eventKey] = funcId
	};
	this.fireEvent = function(eventKey, arg) {
		var result = {};
		result['fired'] = false;
		if (this.events[eventKey]) {
			if (typeof(window[this.events[eventKey]]) == 'function') {
				result['fired'] = true;
				result['output'] = window[this.events[eventKey]](arg);
			}
		}
		return result;
	};
	this.setHiddenFieldId = function(id) {
		hiddenID = id;
		this.hiddenfield().value = ""; // reset for reload
	};
	this.hiddenfield = function() {
		return document.getElementById(hiddenID);
	};
	this.setPathSeparator = function(ps) {
		pathSeparator = ps;
	}
	this.pathSeparator = function() {
		return pathSeparator;
	}
	this.store = function(node) {
		// store nodes in case of failed post-back
		stored.push(node);
		this.writeStoredInput();
	};
	this.unstore = function(node) {
		var ii;
		for (ii=0; ii < stored.length; ii++) {
			if (node.getPath() == stored[ii].getPath())
				break;
		}
		stored.splice(ii, 1);
		this.writeStoredInput();
	};
	this.writeStoredInput = function() {
		this.hiddenfield().value = "";
		for (var ii=0; ii < stored.length; ii++) {
			this.hiddenfield().value += stored[ii].encodeData()+"\n";
		}
	};
	this.cacheNode = function(node) {
		dataholder.cacheNode(node);
	};
	this.getNewCount = function() {
		var now = new Date();
		return now.getTime();
	};
	this.addingFor = function(col) {
		if (col && addingCol)
			addingCol.cancelAdd();
		addingCol = col;
	};
	/*
	 * Multiselection code
	 */
	this.setMultiSelectEnabled = function(enabled, hierarchical) {
		this.multiSelectEnabled = enabled;
		if (enabled){
		    this.hierarchicalSelection = hierarchical;
		}
	};
	this.parseSelectedNodeData = function(datastr) {
	    var newnode = new ColumnNode();
	    newnode.setView(this);
	    newnode.parseData(datastr);
	    this.selectedNodes[newnode.getPath()] = newnode;
	};
	this.onPathToSelected = function(node) {
        var re = new RegExp("\^" + node.getPath() + pathSeparator);
        var i;
        for(i in this.selectedNodes) {
            if(i.match(re)) {
                return true;
            }
        }
        return false;
	};	
	this.parsePrunedNodeData = function(datastr) {
	    var newnode = new ColumnNode();
	    newnode.setView(this);
	    newnode.parseData(datastr);
	    this.prunedNodes[newnode.getPath()] = newnode;
	};
	this.onPathToPruned = function(node) {
        var re = new RegExp("\^" + node.getPath() + pathSeparator);
        var i;
        for(i in this.prunedNodes) {
            if(i.match(re)) {
                return true;
            }
        }
        return false;
	};	
	this.addSelected = function(node) {
	    var stateChanged = false;
        if(this.prunedNodes[node.getPath()]) {
		    delete this.prunedNodes[node.getPath()];
		    stateChanged = true;
		} else {
		    this.selectedNodes[node.getPath()] = node; 
		    stateChanged = true;
		}		
		if(this.hierarchicalSelection) {
		    stateChanged = (this.clearChildStateFor(node) || stateChanged);
	    }
	    return stateChanged;
	};
	this.removeSelected = function(node) {
	    var stateChanged = false;
        if(this.selectedNodes[node.getPath()]) {
		    delete this.selectedNodes[node.getPath()];
		    stateChanged = true;
		} else if (this.hierarchicalSelection && this.ancestorSelectedFor(node)){//(this.hierarchicalSelection && this.ancestorSelectedFor(node) && !this.ancestorPrunedFor(node)) {
		    this.prunedNodes[node.getPath()] = node; 
		    stateChanged = true;
		}		
		if(this.hierarchicalSelection) {
		    stateChanged = (this.clearChildStateFor(node) || stateChanged);
	    }
	    return stateChanged;
	};
	this.isPruned = function(node) {
	    if(this.prunedNodes[node.getPath()]) {
	        return true;
	    } else {
	        return false;
	    }
	};
	this.isSelected = function(node) {
	    if(this.selectedNodes[node.getPath()]) {
	        return true;
	    } else {
	        return false;
	    }
	};
	this.clearChildStateFor = function(node) {
        var re = new RegExp("\^" + node.getPath() + pathSeparator);
        var i;
        var stateChanged = false;
        for(i in this.prunedNodes) {
            if(i.match(re)) {
                stateChanged = true;
                delete this.prunedNodes[i];
            }
        }
        for(i in this.selectedNodes) {
            if(i.match(re)) { 
                stateChanged = true;
                delete this.selectedNodes[i];
            }
        } 
        return stateChanged;   
	};
	this.ancestorSelectedFor = function(node) {
        if (this.selectedNodes[node.getPath()]) {
            return true;
        } else if(!node.parent) {
            return false;
        } else {
            return this.ancestorSelectedFor(node.parent);
        }
    };
    this.ancestorPrunedFor = function(node) {
        if (this.prunedNodes[node.getPath()]) {
            return true;
        } else if(!node.parent) {
            return false;
        } else {
            return this.ancestorPrunedFor(node.parent);
        }
    };
	this.renderSelectionState = function() {
	    var state = new Array();
	    var selected = new Array();
	    var pruned = new Array();
	    var x = 0;
	    var i,j;
		for (i in this.selectedNodes) {
			selected[x] = this.selectedNodes[i];
			x++;
		}
		state[0] = selected;
		if(this.hierarchicalSelection) {
	        x = 0;
		    for (j in this.prunedNodes) {
			    pruned[x] = this.prunedNodes[j];
			    x++;
		    }
	        
	        state[1] = pruned;
		} 
		return state;
	};
}
