/* * VARIABLES */ COLCLASS = "p2column"; COLLOADCLASS = "p2loadingcolumn"; HOLDCLASS = "p2colview"; COLSELECTEDHEADERCLASS = "p2selectedcolmsghc"; 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 = "WebResource.axd?d=YvHdf4QZKyaMwWwdpkNgqZiRv85m3VhFijZkNtuA0yg7qb1CdxZ3rjRJLNkV0GG71Xs4sqjjR4l20372nT8BEoSGHk3jsp6Q0&t=633839411907897486"; addcancelPreload = new Image(); addcancelPreload.src = "WebResource.axd?d=YvHdf4QZKyaMwWwdpkNgqZiRv85m3VhFijZkNtuA0yg7qb1CdxZ3rjRJLNkV0GG71Xs4sqjjR4l20372nT8BEoSGHk3jsp6Q0&t=633839411907897486"; addsubmitPreload = new Image(); addsubmitPreload.src = "WebResource.axd?d=YvHdf4QZKyaMwWwdpkNgqZiRv85m3VhFijZkNtuA0yg7qb1CdxZ3rjRJLNkV0GG71Xs4sqjjR4kjaC6exvIbTCBkemogyPIr0&t=633839411907897486"; // This is for overwriting the original Microsoft version to fix a bug. // http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=705049&SiteID=1 function WebForm_CallbackComplete_SyncFixed() { // SyncFix: the original version uses "i" as global thereby resulting in javascript errors when "i" is used elsewhere in consuming pages for (var i = 0; i < __pendingCallbacks.length; i++) { callbackObject = __pendingCallbacks[i]; if (callbackObject && callbackObject.xmlRequest && (callbackObject.xmlRequest.readyState == 4)) { // the callback should be executed after releasing all resources // associated with this request. // Originally if the callback gets executed here and the callback // routine makes another ASP.NET ajax request then the pending slots and // pending callbacks array gets messed up since the slot is not released // before the next ASP.NET request comes. // FIX: This statement has been moved below // WebForm_ExecuteCallback(callbackObject); if (!__pendingCallbacks[i].async) { __synchronousCallBackIndex = -1; } __pendingCallbacks[i] = null; var callbackFrameID = "__CALLBACKFRAME" + i; var xmlRequestFrame = document.getElementById(callbackFrameID); if (xmlRequestFrame) { xmlRequestFrame.parentNode.removeChild(xmlRequestFrame); } // SyncFix: the following statement has been moved down from above; WebForm_ExecuteCallback(callbackObject); } } } // a base set of OO-help from // MooTools, My Object Oriented Javascript Tools. Copyright (c) 2006 Valerio Proietti, , MIT Style License. eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('6 1z={1F:\'1.11\'};4 $Y(9){2(9!=16)};4 $f(9){7(!$Y(9))2 s;7(9.1p)2\'19\';6 f=W 9;7(f==\'z\'&&9.1B){13(9.1H){t 1:2\'19\';t 3:2(/\\S/).1x(9.1v)?\'1y\':\'1t\'}}7(f==\'z\'||f==\'4\'){13(9.1h){t T:2\'1i\';t 1L:2\'1G\';t o:2\'1A\'}7(W 9.M==\'1C\'){7(9.1E)2\'1D\';7(9.1q)2\'5\'}}2 f};4 $O(){6 B={};r(6 i=0;i<5.M;i++){r(6 g I 5[i]){6 G=5[i][g];6 L=B[g];7(L&&$f(G)==\'z\'&&$f(L)==\'z\')B[g]=$O(L,G);Q B[g]=G}}2 B};6 $h=4(){6 8=5;7(!8[1])8=[b,8[0]];r(6 g I 8[1])8[0][g]=8[1][g];2 8[0]};6 $F=4(){r(6 i=0,l=5.M;i 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("root"); root.parent = null; root.setView(this.view); for(ii in this.view.selectedNodes) { root.hasSelectedDescendants = true; break; } } 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 var 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" : ""); parts.push(this.selectable ? "True" : ""); parts.push(has_data ? "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.getClientId = function() { if (!clientId) clientId = this.view().clientIdFor(this); return clientId; } this.setClientId = function(newid) { clientId = newid; } 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 > 1 && this.selectable) { this.selected = true; } var hadSelectedDescendants = this.hasSelectedDescendants; this.hasSelectedDescendants = false; var lastStatus; for(ii=0;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 (!wasSelected && this.selected && this.selectable) { this.selected = true; stateChanged = this.view().addSelected(this); } } if(this.parent) { stateChanged = (this.parent.updateSelectedStatus() || stateChanged); } else { stateChanged = 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){ var selpanel = document.createElement('div'); selpanel = coldiv.appendChild(selpanel); coldiv.className = COLCLASS + " p2selectedcol"; if(!this.view().multiSelectEnabled){ selpanel.innerHTML = 'You Have Selected
'+node.name+""; } else { selpanel.innerHTML = node.name + ' has'; if (!node.selected){ selpanel.innerHTML += ' not'; } selpanel.innerHTML += ' been selected, there are no further areas within ' + node.name + ' available for selection.'; } } if (node.children.length > 0) { if(selpanel){ coldiv.removeChild(selpanel); } //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) { node.children[ii].selected = true; //force node.children[ii] to selected if parent is selected } else if (!node.hasSelectedDescendants){ node.children[ii].selected = false; //node.children[ii] cannot be select if parent has not selected children node.children[ii].hasSelectedDescendants = false;//node.children[ii] cannot have selected children if parent does not } } else if (node.hasSelectedDescendants) { node.children[ii].selected = node.children[ii].selected;//node.children[ii] maintains previous selected status node.children[ii].hasSelectedDescendants = node.children[ii].hasSelectedDescendants;//node.children[ii] maintains previous selected child status } } else { //this are irrelevant outside of multiselect mode, setting just to be sure node.children[ii].selected = false; node.children[ii].hasSelectedDescendants = false; } var newlink = this.makeColItem(node.children[ii]); coldiv.appendChild(newlink); if (node.children[ii].startSelected) { newlink.className += " p2colitemsel"; node.children[ii].startSelected = false; this.lastColItem = newlink; coldiv.scrollTop = newlink.offsetTop; } } coldiv.className = COLCLASS; } else { if(selpanel){ selpanel.className = "p2selectedcolmsg"; }else { var selpanel = document.createElement('div'); selpanel = coldiv.appendChild(selpanel); coldiv.className = COLCLASS + " p2selectedcol"; selpanel.className = "p2selectedcolmsg"; selpanel.innerHTML = 'There are no further areas within ' + node.name + '.' } } } this.makeColItem = function(itemnode, allowDelete) { 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){ var selDiv = document.createElement('div'); selDiv.className = "p2colCheckbox"; if(itemnode.selectable) { if(itemnode.selected){ selDiv.className += " p2colCheckboxOn"; } else if (itemnode.hasSelectedDescendants) { selDiv.className += " p2colCheckboxPartial"; } } else if (itemnode.hasSelectedDescendants) { selDiv.className += " p2colCheckboxDisabledPartial"; } else { selDiv.className += " p2colCheckboxDisabledOff"; } selDiv.onclick = this.onColItemClick.bind(this); div.appendChild(selDiv); } if (itemnode.attributes.New && allowDelete) { div.innerHTML += '
'+itemnode.name+" "; 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; } itemnode.setClientId(this.view().clientIdFor(itemnode)); div.id = this.view().clientIdFor(itemnode); 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 (this.view().multiSelectEnabled && targetElement.className.indexOf('Checkbox') != -1){ el = targetElement.parentNode; if (el.node.selectable) { 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().setLastRenderedNode(el.node); this.view().showColumnFor(el.node); this.view().fireEvent('onselect', el.node); } } this.redrawNode = function(node) { var div = document.getElementById(node.getClientId()); if (div) { if (this.view().multiSelectEnabled) { //div.node.selectable) { var ii; for(ii=0;ii= 0) { break; } } var selDiv = div.childNodes[ii]; selDiv.className = "p2colCheckbox"; if (node.selectable) { if(node.selected) { selDiv.className += " p2colCheckboxOn"; } else if (node.hasSelectedDescendants) { selDiv.className += " p2colCheckboxPartial"; } } else if (node.hasSelectedDescendants) { selDiv.className += " p2colCheckboxDisabledPartial"; } else { selDiv.className += " p2colCheckboxDisabledOff"; } this.redrawNode(node.parent); } } } 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; if(this.view().fireEvent('customadd', node).fired){ //do nothing add handled by external code } else { 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.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, newid, allowDelete) { //if allowDelete parameter not supplied as boolean literal false, assume true allowDelete = (allowDelete === false) ? false : true; 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; if(newid){ newnode.setPath(newnode.parent.getPath() + this.view().pathSeparator() + newid); } else { newnode.setPath(newnode.parent.getPath() + this.view().pathSeparator() + 'cvNew '+this.view().getNewCount()); } var selectable = false; if(node.children.length > 0) { for (var ii = 0; ii < node.children.length; ii++) { if(node.children[ii].selectable){ selectable = true; break; } } } else { selectable = node.selectable; } newnode.selectable = selectable; if(newnode.selectable) { newnode.selected = this.view().hierarchicalSelection && (newnode.parent.selected || this.view().onPathToPruned(newnode.parent)); if(newnode.selected) { newnode.parent.hasSelectedDescendants = true; this.view().fireEvent('selStateChanged',this.view().renderSelectionState()); } } var newdiv = this.makeColItem(newnode, allowDelete); var newpos = node.add(newnode); var re = new RegExp(COLSELECTEDHEADERCLASS); if(this.col().childNodes[0] && this.col().childNodes[0].className.match(re)) { //account for selected message newpos = newpos + 1; } 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 multiSelect = false; var hiddenID = ""; var addingCol; var stored = new Array(); this.selectedNodes = {}; this.prunedNodes = {}; this.pathParts; var lastRenderedNode; 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.getID = function(path) { return path.substring(path.lastIndexOf(pathSeparator)+1, path.length); } this.setLastRenderedNode = function(node) { this.lastRenderedNode = node; } 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) { delete waiting[ii]; return true; } return false; } this.fetchChildrenFor = function(node, shouldntScroll) { this.drawColumnFor(node, shouldntScroll); //force nodes to check for restored client only nodes, as they if (node.hasData() && !this.hasStoredChildNodes(node)) { 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.goToPath = function(path) { while (path.substring(0,1) == ' ') { path = path.substring(1, path.length); } this.pathParts = path.split(pathSeparator); while (this.lastRenderedNode.parent != null) { var id = this.getID(this.lastRenderedNode.getPath()); var found = false; for (var ii = 0; ii < this.pathParts.length; ii++) { if(this.pathParts[ii] == id) { found = true; break; } } if(found) { this.fetchChildrenFor(this.lastRenderedNode, false); return; } this.lastRenderedNode = this.lastRenderedNode.parent; } this.fetchChildrenFor(this.root, false); } 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.viewWidth = this.view().offsetWidth - 2; // 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.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) { if(this.pathParts != null){ this.markIfOnPath(node, false); } 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); if(this.pathParts != null){ this.navigateToPath(node, false); } } this.navigateToPath = function(node, shouldntScroll) { var nextNode = null; if (this.pathParts.length > 0) { if(node.parent != null) { var id = this.getID(node.getPath()); while (this.pathParts[0] != id) { this.pathParts.shift(); } this.pathParts.shift(); } if (node.children != null) { for (var ii = 0; ii < node.children.length; ii++){ if (this.pathParts[0] == this.getID(node.children[ii].getPath())) { nextNode = node.children[ii]; break; } } } } if (nextNode != null){ node = nextNode; this.fetchChildrenFor(node, shouldntScroll); } } this.markIfOnPath = function(node, shouldntScroll) { if (node.children != null) { for (var jj = 0; jj < node.children.length; jj++){ var found = false; var id = this.getID(node.children[jj].getPath()); for (var ii = 0; ii < this.pathParts.length; ii++){ if (this.pathParts[ii] == id) { node.children[jj].startSelected = true; found = true; break; } } if (found) { break; } } } } 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; } this.clientIdFor = function(node) { var nodePath = node.getPath().replace(/\s/g, '-'); return this.view().id+"_node"+nodePath; } /* 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.hasStoredChildNodes = function(node) { var nodePath = node.getPath(); for (var ii=0; ii < stored.length; ii++) { var parentPath = stored[ii].attributes.ParentPath; if(!parentPath && stored[ii].parent) { parentPath = stored[ii].parent.getPath(); } if(parentPath == node.getPath()) { return true; } } return false; } 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; } this.addNodeFromExternal = function(name, id, allowDelete) { addingCol.addWithName(name, id, allowDelete); } /* * 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 ii; for(ii in this.selectedNodes) { if(ii.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 ii; for(ii in this.prunedNodes) { if(ii.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 ii; var stateChanged = false; for(ii in this.prunedNodes) { if(ii.match(re)) { stateChanged = true; delete this.prunedNodes[ii]; } } for(ii in this.selectedNodes) { if(ii.match(re)) { stateChanged = true; delete this.selectedNodes[ii]; } } 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 ii,jj; for (ii in this.selectedNodes) { selected[x] = this.selectedNodes[ii]; x++; } state[0] = selected; if(this.hierarchicalSelection) { x = 0; for (jj in this.prunedNodes) { pruned[x] = this.prunedNodes[jj]; x++; } state[1] = pruned; } return state; } }