/* * Content-separated javascript tree widget * * Usage: * behaveAs(someUL, Tree) * OR behaveAs(someUL, DraggableTree) * * Extended by Steven J. DeRose, deroses@mail.nih.gov, sderose@acm.org. * * INPUT REQUIREMENTS: * Put class="tree" on topmost UL(s). * Can put class="closed" on LIs to have them collapsed on startup. * * The structure we build is: * li class="children last closed" <=== original li from source * children: there's a UL child * last: no following LI * closed: is collapsed (may be in src) * span class="a children spanClosed" <=== contains children before UL * children: there's a UL (now a sib) * spanClosed: is collapsed * span class="b" <=== +/- click is caught here * span class="c" <=== for spacing and lines * a href="..." <=== original pre-UL stuff (e.g., a) * ul... * */ Tree = Class.create(); Tree.prototype = { /* * Initialise a tree node, converting all its LIs appropriately. * This means go through all li children, and move the content of each * (before any UL child) down into 3 intermediate spans, classes a/b/c. */ initialize: function(options) { this.isDraggable = false; var i,li; this.options = options ? options : {}; if(!this.options.tree) this.options.tree = this; this.tree = this.options.tree; // Set up observer if(this == this.tree) Observable.applyTo(this); // Find all LIs to process // Don't let it re-do a node it's already done. for(i=0;i<this.childNodes.length;i++) { if(this.childNodes[i].tagName && this.childNodes[i].tagName.toLowerCase() == 'li' && !(this.childNodes[i].childNodes[0] && this.childNodes[i].childNodes[0].attributes && this.childNodes[i].childNodes[0].attributes["class"] && this.childNodes[i].childNodes[0].attributes["class"] == "a")) { li = this.childNodes[i]; this.castAsTreeNode(li); // If we've added a DIV to this node, then increment i; while(this.childNodes[i].tagName.toLowerCase() != "li") i++; } } // Not sure what following line is really doing for us.... this.className = this.className.replace(/ ?unformatted ?/, ' '); if(li) { li.addNodeClass('last'); //li.addNodeClass('closed'); if(this.parentNode.tagName.toLowerCase() == "li") { this.treeNode = this.parentNode; } return true; } else { return false; } }, destroy: function() { this.tree = null; this.treeNode = null; if(this.options) this.options.tree = null; this.options = null; }, /** * Convert the given <li> tag into a suitable tree node */ castAsTreeNode: function(li) { behaveAs(li, TreeNode, this.options); }, getIdxOf : function(el) { if(!el.treeNode) el.treeNode = el; // Special case for TreeMultiselectField if(el.treeNode.id.match(/^selector-([^-]+)-([0-9]+)$/)) return RegExp.$2; // Other case for LHS tree of CMS if(el.treeNode.id.match(/([^-]+)-(.+)$/)) return RegExp.$1; else return el.treeNode.id; }, childTreeNodes: function() { var i,item, children = []; for(i=0;item=this.childNodes[i];i++) { if(item.tagName && item.tagName.toLowerCase() == 'li') children.push(item); } return children; }, hasChildren: function() { return this.childTreeNodes().length > 0; }, /** * Turn a normal tree into a draggable one. */ makeDraggable: function() { this.isDraggable = true; var i,item,x; var trees = this.getElementsByTagName('ul'); for(x in DraggableTree.prototype) this[x] = DraggableTree.prototype[x]; DraggableTree.prototype.setUpDragability.apply(this); var nodes = this.getElementsByTagName('li'); for(i=0;item=nodes[i];i++) { for(x in DraggableTreeNode.prototype) item[x] = DraggableTreeNode.prototype[x]; } for(i=0;item=trees[i];i++) { for(x in DraggableTree.prototype) item[x] = DraggableTree.prototype[x]; } for(i=0;item=nodes[i];i++) { DraggableTreeNode.prototype.setUpDragability.apply(item); } for(i=0;item=trees[i];i++) { DraggableTree.prototype.setUpDragability.apply(item); } }, /** * Add the given child node to this tree node. * If 'before' is specified, then it will be inserted before that. */ appendTreeNode : function(child, before) { if(!child) return; // Remove from the old parent node - this will ensure that the classes of the old tree // item are updated accordingly if(child && child.parentTreeNode) { var oldParent = child.parentTreeNode; oldParent.removeTreeNode(child); } var lastNode, i, holder = this; if(lastNode = this.lastTreeNode()) lastNode.removeNodeClass('last'); // Do the actual moving if(before) { child.removeNodeClass('last'); if(holder != before.parentNode) { throw("TreeNode.appendTreeNode: 'before' not contained within the holder"); holder.appendChild(child); } else { holder.insertBefore(child, before); } } else { holder.appendChild(child); } if(this.parentNode && this.parentNode.fixDragHelperDivs) this.parentNode.fixDragHelperDivs(); if(oldParent && oldParent.fixDragHelperDivs) oldParent.fixDragHelperDivs(); // Update the helper classes if(this.parentNode && this.parentNode.tagName.toLowerCase() == 'li') { if(this.parentNode.className.indexOf('closed') == -1) this.parentNode.addNodeClass('children'); this.lastTreeNode().addNodeClass('last'); } // Update the helper variables if(this.parentNode.tagName.toLowerCase() == 'li') child.parentTreeNode = this.parentNode; else child.parentTreeNode = null; if(this.isDraggable) { for(x in DraggableTreeNode.prototype) child[x] = DraggableTreeNode.prototype[x]; DraggableTreeNode.prototype.setUpDragability.apply(child); } }, lastTreeNode : function() { var i, holder = this; for(i=holder.childNodes.length-1;i>=0;i--) { if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == 'li') return holder.childNodes[i]; } }, /** * Remove the given child node from this tree node. */ removeTreeNode : function(child) { // Remove the child var holder = this; try { holder.removeChild(child); } catch(er) { } // Look for remaining children var i, hasChildren = false; for(i=0;i<holder.childNodes.length;i++) { if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == "li") { hasChildren = true; break; } } // Update the helper classes accordingly if(!hasChildren) this.removeNodeClass('children'); else this.lastTreeNode().addNodeClass('last'); // Update the helper variables if(child.parentTreeNode == this.parentNode) { child.parentTreeNode = null; } }, open: function() { }, expose: function() { }, addNodeClass : function(className) { if( this.parentNode.tagName.toLowerCase() == 'li' ) this.parentNode.addNodeClass(className); }, removeNodeClass : function(className) { if( this.parentNode.tagName.toLowerCase() == 'li' ) this.parentNode.removeNodeClass(className); } } TreeNode = Class.create(); TreeNode.prototype = { initialize: function(options) { var spanA, spanB, spanC; var startingPoint, stoppingPoint, childUL; var j; // Basic hook-ups var li = this; this.options = options ? options : {}; this.tree = this.options.tree; if(!this.ajaxExpansion && this.options.ajaxExpansion) this.ajaxExpansion = this.options.ajaxExpansion; if(this.options.getIdx) this.getIdx = this.options.getIdx; // Get this.recordID from the last "-" separated chunk of the id HTML attribute // eg: <li id="treenode-6"> would give a recordID of 6 if(this.id && this.id.match(/([^-]+)-(.+)$/)) this.recordID = RegExp.$1; // Create our extra spans spanA = document.createElement('span'); spanB = document.createElement('span'); spanC = document.createElement('span'); spanA.appendChild(spanB); spanB.appendChild(spanC); spanA.className = 'a ' + li.className.replace('closed','spanClosed'); spanB.className = 'b'; spanB.onclick = TreeNode_bSpan_onclick; spanC.className = 'c'; this.castAsSpanA(spanA); // Add +/- icon to select node that has children if (li.hasChildren() && li.className.indexOf('current') > -1) { li.className = li.className + ' children'; spanA.className = spanA.className + ' children'; } // Find the UL within the LI, if it exists stoppingPoint = li.childNodes.length; startingPoint = 0; childUL = null; for(j=0;j<li.childNodes.length;j++) { // Find last div before first ul (unnecessary in our usage) /* if(li.childNodes[j].tagName && li.childNodes[j].tagName.toLowerCase() == 'div') { startingPoint = j + 1; continue; } */ if(li.childNodes[j].tagName && li.childNodes[j].tagName.toLowerCase() == 'ul') { childUL = li.childNodes[j]; stoppingPoint = j; break; } } // Move all the nodes up until that point into spanC for(j=startingPoint;j<stoppingPoint;j++) { /* Use [startingPoint] every time, because the appentChild removes the node, so it then points to the next one. */ spanC.appendChild(li.childNodes[startingPoint]); } // Insert the outermost extra span into the tree if(li.childNodes.length > startingPoint) li.insertBefore(spanA, li.childNodes[startingPoint]); else li.appendChild(spanA); // Create appropriate node references; if(li.parentNode && li.parentNode.parentNode && li.parentNode.parentNode.tagName.toLowerCase() == 'li') { li.parentTreeNode = li.parentNode.parentNode; } li.aSpan = spanA; li.bSpan = spanB; li.cSpan = spanC; li.treeNode = spanA.treeNode = spanB.treeNode = spanC.treeNode = li; var aTag = spanC.getElementsByTagName('a')[0]; if(aTag) { aTag.treeNode = li; li.aTag = aTag; } else { throw("Tree creation: A tree needs <a> tags inside the <li>s to work properly."); } aTag.onclick = TreeNode_aTag_onclick.bindAsEventListener(aTag); // Process the children if(childUL != null) { if(this.castAsTree(childUL)) { /* ***** RECURSE ***** */ if(this.className.indexOf('closed') == -1) { this.addNodeClass('children'); } } } else { this.removeNodeClass('closed'); } this.setIconByClass(); }, destroy: function() { // Debug.show(this); this.tree = null; this.treeNode = null; this.parentTreeNode = null; if(this.options) this.options.tree = null; this.options = null; if(this.aTag) { this.aTag.treeNode = null; this.aTag.onclick = null; } if(this.aSpan) { this.aSpan.treeNode = null; this.aSpan.onmouseover = null; this.aSpan.onmouseout = null; } if(this.bSpan) { this.bSpan.treeNode = null; this.bSpan.onclick = null; } if(this.cSpan) this.cSpan.treeNode = null; this.aSpan = null; this.bSpan = null; this.cSpan = null; this.aTag = null; }, /** * Cast the given span as the <span class="a"> item for this tree. */ castAsSpanA: function(spanA) { var x; for(x in TreeNode_SpanA) spanA[x] = TreeNode_SpanA[x]; }, /** * Cast the child <ul> as a tree */ castAsTree: function(childUL) { return behaveAs(childUL, Tree, this.options); }, /** * Triggered from clicks on spans of class b, the +/- buttons. * Closed is represented by adding class close to the LI, and * class spanClose to spanA. * Pass 'force' as "open" or "close" to force it to that state, * otherwise it toggles. */ toggle : function(force) { if(this.treeNode.wasDragged || this.treeNode.anchorWasClicked) { this.treeNode.wasDragged = false; this.treeNode.anchorWasClicked = false; return; } /* Note: It appears the 'force' parameter is no longer used. Here is old code that used it: if( force == "open"){ treeOpen( topSpan, el ) } else if( force == "close" ){ treeClose( topSpan, el ) } */ if(this.hasChildren() || this.className.match(/(^| )unexpanded($| )/)) { if(this.className.match(/(^| )closed($| )/) || this.className.match(/(^| )unexpanded($| )/)) this.open(); else this.close(); } }, open : function () { // Normal tree node if(Element.hasClassName(this, 'unexpanded') && !this.hasChildren()) { if(this.ajaxExpansion) this.ajaxExpansion(); } if(!this.className.match(/(^| )closed($| )/)) return; this.removeNodeClass('closed'); this.removeNodeClass('unexpanded'); }, close : function () { this.addNodeClass('closed'); }, expose : function() { if(this.parentTreeNode) { this.parentTreeNode.open(); this.parentTreeNode.expose(); } }, setIconByClass: function() { if(typeof _TREE_ICONS == 'undefined') return; var classes = this.className.split(/\s+/); var obj = this; classes.each(function(className) { var className = className.replace(/class-/, ''); if(_TREE_ICONS[className]) { obj.fileIcon = _TREE_ICONS[className].fileIcon; obj.openFolderIcon = _TREE_ICONS[className].openFolderIcon; obj.closedFolderIcon = _TREE_ICONS[className].closedFolderIcon; throw $break; } else if(className == "Page") { obj.fileIcon = null; obj.openFolderIcon = null; obj.closedFolderIcon = null; } }); this.updateIcon(); }, updateIcon: function() { var icon; if(this.closedFolderIcon && this.className.indexOf('closed') != -1) { icon = this.closedFolderIcon; } else if(this.openFolderIcon && this.className.indexOf('children') != -1) { icon = this.openFolderIcon; } else if(this.fileIcon) { icon = this.fileIcon; } if(icon) this.aTag.style.background = "url(" +icon + ") no-repeat"; else this.aTag.style.backgroundImage = ""; }, /** * Add the given child node to this tree node. * If 'before' is specified, then it will be inserted before that. */ appendTreeNode : function(child, before) { this.treeNodeHolder().appendTreeNode(child, before); }, treeNodeHolder : function(performCast) { if(performCast == null) performCast = true; var uls = this.getElementsByTagName('ul'); if(uls.length > 0) return uls[0]; else { var ul = document.createElement('ul'); this.appendChild(ul); if(performCast) this.castAsTree(ul); return ul; } }, hasChildren: function() { var uls = this.getElementsByTagName('ul'); if(uls.length > 0) { var i,item; for(i=0;item=uls[0].childNodes[i];i++) { if(item.tagName && item.tagName.toLowerCase() == 'li') return true; } } return false; }, /** * Remove the given child node from this tree node. */ removeTreeNode : function(child) { // Remove the child var holder = this.treeNodeHolder(); try { holder.removeChild(child); } catch(er) { } // Look for remaining children var i, hasChildren = false; for(i=0;i<holder.childNodes.length;i++) { if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == "li") { hasChildren = true; break; } } // Update the helper classes accordingly if(!hasChildren) this.removeNodeClass('children'); else this.lastTreeNode().addNodeClass('last'); // Update the helper variables child.parentTreeNode = null; }, lastTreeNode : function() { return this.treeNodeHolder().lastTreeNode(); }, firstTreeNode : function() { var i, holder = this.treeNodeHolder(); for(i=0;i<holder.childNodes.length;i++) { if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == 'li') return holder.childNodes[i]; } }, addNodeClass : function(className) { if(Element && Element.addClassName) { Element.addClassName(this, className); if(className == 'closed') Element.removeClassName(this, 'children'); this.aSpan.className = 'a ' + this.className.replace('closed','spanClosed'); if(className == 'children' || className == 'closed') this.updateIcon(); } }, removeNodeClass : function(className) { if(Element && Element.removeClassName) { Element.removeClassName(this, className); if(className == 'closed' && this.hasChildren()) Element.addClassName(this, 'children'); this.aSpan.className = 'a ' + this.className.replace('closed','spanClosed'); if(className == 'children' || className == 'closed') this.updateIcon(); } }, getIdx : function() { if(this.id.match(/([^-]+)-(.+)$/)) return RegExp.$2; else return this.id; }, getTitle: function() { return this.aTag.innerHTML; }, installSubtree : function(response) { var ul = this.treeNodeHolder(false); ul.innerHTML = response.responseText; ul.appendTreeNode = null; this.castAsTree(ul); /* var i,lis = ul.childTreeNodes(); for(i=0;i<lis.length;i++) { this.tree.castAsTreeNode(lis[i]); } */ // Cued new nodes are nodes added while we were waiting for the expansion to finish if(ul.cuedNewNodes) { var i; for(i=0;i<ul.cuedNewNodes.length;i++) { ul.appendTreeNode(ul.cuedNewNodes[i]); } ul.cuedNewNodes = null; } this.removeNodeClass('closed'); this.addNodeClass('children'); this.removeNodeClass('loading'); this.removeNodeClass('unexpanded'); } } /* Close or Open all the trees, at beginning or on request. sjd. */ function treeCloseAll() { var candidates = document.getElementsByTagName('li'); for (var i=0;i<candidates.length;i++) { var aSpan = candidates[i].childNodes[0]; if(aSpan.childNodes[0] && aSpan.childNodes[0].className == "b") { if (!aSpan.className.match(/spanClosed/) && candidates[i].id != 'record-0' ) { aSpan.childNodes[0].onclick(); } } } } function treeOpenAll() { var candidates = document.getElementsByTagName('li'); for (var i=0;i<candidates.length;i++) { var aSpan = candidates[i].childNodes[0]; if(aSpan.childNodes[0] && aSpan.childNodes[0].className == "b") { if (aSpan.className.match(/spanClosed/)) { aSpan.childNodes[0].onclick(); } } } } TreeNode_aTag_onclick = function(event) { Event.stop(event); jQuery(this.treeNode.tree).trigger('nodeclicked', {node: this.treeNode}); if(!this.treeNode.tree || this.treeNode.tree.notify('NodeClicked', this.treeNode)) { if(this.treeNode.options.onselect) { return this.treeNode.options.onselect.apply(this.treeNode, [event]); } else if(this.treeNode.onselect) { return this.treeNode.onselect(); } } return false; } TreeNode_bSpan_onclick = function() { this.treeNode.toggle(); }; TreeNode_SpanA = { onmouseover : function(event) { this.parentNode.addNodeClass('over'); }, onmouseout : function(event) { this.parentNode.removeNodeClass('over'); } } //-----------------------------------------------------------------------------------------------// DraggableTree = Class.extend('Tree'); DraggableTree.prototype = { initialize: function(options) { this.Tree.initialize(options); this.setUpDragability(); }, setUpDragability: function() { this.isDraggable = true; this.allDragHelpers = []; if(this.parentNode.tagName.toLowerCase() == "li") { this.treeNode = this.parentNode; if(this.treeNode.hasChildren()) { this.treeNode.createDragHelper(); } } }, /** * Turn a draggable tree into a normal one. */ stopBeingDraggable: function() { // this.parentNode.destroy(); this.isDraggable = false; var i,item,nodes = this.getElementsByTagName('li'); for(i=0;item=nodes[i];i++) { item.destroyDraggable(); } for(i=0;item=this.allDragHelpers[i];i++) { Droppables.remove(item); if(item.parentNode){ item.parentNode.removeChild(item); } } this.allDragHelpers = []; }, /** * Convert the given <li> tag into a suitable tree node */ castAsTreeNode: function(li) { behaveAs(li, DraggableTreeNode, this.options); } } DraggableTreeNode = Class.extend('TreeNode'); DraggableTreeNode.prototype = { initialize: function(options) { this.TreeNode.initialize(options); this.setUpDragability(); }, setUpDragability: function() { // Set up drag and drop this.draggableObj = new Draggable(this, TreeNodeDragger); //if(!this.dropperOptions || this.dropperOptions.accept != 'none') Droppables.add(this.aTag, this.dropperOptions ? Object.extend(this.dropperOptions, TreeNodeDropper) : TreeNodeDropper); // Add before DIVs to be Droppable items if(this.parentTreeNode && this.parentTreeNode.createDragHelper){ this.parentTreeNode.createDragHelper(this); } if(this.hasChildren() && this.parentNode.tagName.toLowerCase() == "li") { this.treeNode = this.parentNode; // this.treeNode.createDragHelper(); } // Fix up the <a> click action this.aTag._onclick_before_draggable = this.aTag.onclick; this.aTag.baseClick = this.aTag.onclick; if(this.options.onParentChanged) this.onParentChanged = this.options.onParentChanged; if(this.options.onOrderChanged) this.onOrderChanged = this.options.onOrderChanged; }, /** * Remove all the draggy stuff */ destroyDraggable: function() { Droppables.remove(this.aTag); this.aTag.onclick = this.aTag._onclick_before_draggable; if(this.draggableObj) { this.draggableObj.destroy(); this.draggableObj = null; } }, /* this was commented out because SiteTreeNode takes care of it instead castAsTree: function(childUL) { // Behaving as DraggableTree directly doesn't load in expansion behaviours behaveAs(childUL, Tree, this.options); childUL.makeDraggable(); }, */ /** * Rebuild the "Drag Helper DIVs" that sit around each tree node within this node */ fixDragHelperDivs : function() { var i, holder = this.treeNodeHolder(); // This variable toggles between div & li var lastDiv, expecting = "div"; for(i=0;i<holder.childNodes.length;i++) { if(holder.childNodes[i].tagName) { if(holder.childNodes[i].tagName.toLowerCase() == "div") lastDiv = holder.childNodes[i]; // alert(i + ': ' + expecting + ', ' + holder.childNodes[i].tagName); if(expecting != holder.childNodes[i].tagName.toLowerCase()) { if(expecting == "div") { this.createDragHelper(holder.childNodes[i]); } else { holder.removeChild(holder.childNodes[i]); } i--; } else { // Toggle expecting expecting = (expecting == "div") ? "li" : "div"; } } } // If we were left looking for an li, remove the last div // if(expecting == "li") holder.removeChild(lastDiv); // If we were left looking for a div, add one at the end if(expecting == "div") this.createDragHelper(); }, /** * Create a drag helper within this item. * It will be inserted to the end, or before the 'before' element if that is given. */ createDragHelper : function(before) { // Create the node var droppable = document.createElement('div'); droppable.className = "droppable"; droppable.treeNode = this; this.dragHelper = droppable; this.tree.allDragHelpers[this.tree.allDragHelpers.length] = this.dragHelper; // Insert into the DOM var holder = this.treeNodeHolder(); if(before) holder.insertBefore(droppable, before); else holder.appendChild(droppable); // Make droppable var customOptions = holder.parentNode.dropperOptions ? Object.extend(holder.parentNode.dropperOptions, TreeNodeSeparatorDropper) : TreeNodeSeparatorDropper; if(!customOptions.accept != 'none') { if(Droppables) Droppables.add(droppable, customOptions); } } } TreeNodeDragger = { onStartDrag : function(dragger) { dragger.oldParent = dragger.parentTreeNode; }, revert: true } TreeNodeDropper = { onDrop : function(dragger, dropper, event) { var result = true; // Handle event handlers if(dragger.onParentChanged && dragger.parentTreeNode != dropper.treeNode) result = dragger.onParentChanged(dragger, dragger.parentTreeNode, dropper.treeNode); // Get the future order of the children after the drop completes var i = 0, item = null, items = []; items[items.length] = dragger.treeNode; for(i=0;item=dropper.treeNode.treeNodeHolder().childNodes[i];i++) { if(item != dragger.treeNode) items[items.length] = item; } if(result && dragger.onOrderChanged) result = dragger.onOrderChanged(items, items[0]); if(result) { dropper.treeNode.appendTreeNode(dragger.treeNode, dropper.treeNode.firstTreeNode()); } dragger.wasDragged = true; }, hoverclass : 'dragOver', checkDroppableIsntContained : true } TreeNodeSeparatorDropper = { onDrop : function(dragger, dropper, event) { var result = true; // Handle parent-change handlers if(dragger.onParentChanged && dragger.parentTreeNode != dropper.treeNode) result = dragger.onParentChanged(dragger, dragger.parentTreeNode, dropper.treeNode); // Get the future order of the children after the drop completes var i = 0, item = null, items = []; for(i=0;item=dropper.treeNode.treeNodeHolder().childNodes[i];i++) { if(item == dropper) items[items.length] = dragger.treeNode; if(item != dragger.treeNode) items[items.length] = item; } // Handle order change if(result && dragger.onOrderChanged) result = dragger.onOrderChanged(items, dragger.treeNode); if(result) { dropper.treeNode.appendTreeNode( dragger.treeNode, dropper); } dragger.wasDragged = true; }, hoverclass : 'dragOver', greedy : true, checkDroppableIsntContained : true } //---------------------------------------------------------------------------------------------/// /** * Mix-in for the tree to enable mulitselect support * Usage: * - tree.behaveAs(MultiselectTree) * - tree.stopBehavingAs(MultiselectTree) */ MultiselectTree = Class.create(); MultiselectTree.prototype = { initialize: function() { Element.addClassName(this, 'multiselect'); this.MultiselectTree_observer = this.observeMethod('NodeClicked', this.multiselect_onClick.bind(this)); this.selectedNodes = { } }, destroyDraggable: function() { this.stopObserving(this.MultiselectTree_observer); }, multiselect_onClick : function(selectedNode) { if(selectedNode.selected) { this.deselectNode(selectedNode); } else { this.selectNode(selectedNode); } // Trigger the onselect event return true; }, selectNode: function(selectedNode) { var idx = this.getIdxOf(selectedNode); selectedNode.addNodeClass('selected'); selectedNode.selected = true; this.selectedNodes[idx] = selectedNode.aTag.innerHTML; }, deselectNode : function(selectedNode) { var idx = this.getIdxOf(selectedNode); selectedNode.removeNodeClass('selected'); selectedNode.selected = false; delete this.selectedNodes[idx]; } }