YAHOO.namespace('ext.data'); /** * @class YAHOO.ext.data.Tree * @extends YAHOO.ext.util.Observable * The class represents a tree data structure and bubbles all the events for it's nodes. The nodes * in the tree have most standard DOM functionality. * @constructor * @param {Node} root (optional) The root node */ YAHOO.ext.data.Tree = function(root){ this.nodeHash = {}; this.root = null; if(root){ this.setRootNode(root); } this.events = { 'append' : true, 'remove' : true, 'move' : true, 'insert' : true, 'beforeappend' : true, 'beforeremove' : true, 'beforemove' : true, 'beforeinsert' : true }; }; YAHOO.extendX(YAHOO.ext.data.Tree, YAHOO.ext.util.Observable, { pathSeparator: '/', getRootNode : function(){ return this.root; }, setRootNode : function(node){ this.root = node; node.ownerTree = this; node.isRoot = true; return node; }, getNodeById : function(id){ return this.nodeHash[id]; }, registerNode : function(node){ this.nodeHash[node.id] = node; }, unregisterNode : function(node){ delete this.nodeHash[node.id]; }, toString : function(){ return '[Tree'+(this.id?' '+this.id:'')+']'; } }); /** * @class YAHOO.ext.tree.Node * @extends YAHOO.ext.util.Observable * @cfg {String} text The text for this node * @cfg {String} id The id for this node * @constructor * @param {Object} attributes The attributes/config for the node */ YAHOO.ext.data.Node = function(attributes){ this.attributes = attributes || {}; this.leaf = this.attributes.leaf; this.id = this.attributes.id; if(!this.id){ this.id = YAHOO.util.Dom.generateId(null, 'ynode-'); this.attributes.id = this.id; } this.childNodes = []; if(!this.childNodes.indexOf){ // indexOf is a must this.childNodes.indexOf = function(o){ for(var i = 0, len = this.length; i < len; i++){ if(this[i] == o) return i; } return -1; }; } this.parentNode = null; this.firstChild = null; this.lastChild = null; this.previousSibling = null; this.nextSibling = null; this.events = { 'append' : true, 'remove' : true, 'move' : true, 'insert' : true, 'beforeappend' : true, 'beforeremove' : true, 'beforemove' : true, 'beforeinsert' : true }; }; YAHOO.extendX(YAHOO.ext.data.Node, YAHOO.ext.util.Observable, { fireEvent : function(evtName){ // first do standard event for this node if(YAHOO.ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){ return false; } // then bubble it up to the tree if the event wasn't cancelled if(this.ownerTree){ if(this.ownerTree.fireEvent.apply(this.ownerTree, arguments) === false){ return false; } } return true; }, isLeaf : function(){ return this.leaf === true; }, setFirstChild : function(node){ this.firstChild = node; }, setLastChild : function(node){ this.lastChild = node; }, isLast : function(){ return (!this.parentNode ? true : this.parentNode.lastChild == this); }, isFirst : function(){ return (!this.parentNode ? true : this.parentNode.firstChild == this); }, hasChildNodes : function(){ return !this.isLeaf() && this.childNodes.length > 0; }, appendChild : function(node){ var multi = false; if(node instanceof Array){ multi = node; }else if(arguments.length > 1){ multi = arguments; } // if passed an array or multiple args do them one by one if(multi){ for(var i = 0, len = multi.length; i < len; i++) { this.appendChild(multi[i]); } }else{ if(this.fireEvent('beforeappend', this.ownerTree, this, node) === false){ return false; } var index = this.childNodes.length; var oldParent = node.parentNode; // it's a move, make sure we move it cleanly if(oldParent){ if(node.fireEvent('beforemove', node.getOwnerTree(), node, oldParent, this, index) === false){ return false; } oldParent.removeChild(node); } var index = this.childNodes.length; if(index == 0){ this.setFirstChild(node); } this.childNodes.push(node); node.parentNode = this; var ps = this.childNodes[index-1]; if(ps){ node.previousSibling = ps; ps.nextSibling = node; } this.setLastChild(node); node.setOwnerTree(this.getOwnerTree()); this.fireEvent('append', this.ownerTree, this, node, index); if(oldParent){ node.fireEvent('move', this.ownerTree, node, oldParent, this, index); } return node; } }, removeChild : function(node){ var index = this.childNodes.indexOf(node); if(index == -1){ return false; } if(this.fireEvent('beforeremove', this.ownerTree, this, node) === false){ return false; } // remove it from childNodes collection this.childNodes.splice(index, 1); // update siblings if(node.previousSibling){ node.previousSibling.nextSibling = node.nextSibling; } if(node.nextSibling){ node.nextSibling.previousSibling = node.previousSibling; } // update child refs if(this.firstChild == node){ this.setFirstChild(node.nextSibling); } if(this.lastChild == node){ this.setLastChild(node.previousSibling); } node.setOwnerTree(null); // clear any references from the node node.parentNode = null; node.previousSibling = null; node.nextSibling = null; this.fireEvent('remove', this.ownerTree, this, node); return node; }, insertBefore : function(node, refNode){ if(!refNode){ // like standard Dom, refNode can be null for append return this.appendChild(node); } // nothing to do if(node == refNode){ return false; } if(this.fireEvent('beforeinsert', this.ownerTree, this, node, refNode) === false){ return false; } var index = this.childNodes.indexOf(refNode); var oldParent = node.parentNode; var refIndex = index; // when moving internally, indexes will change after remove if(oldParent == this && this.childNodes.indexOf(node) < index){ refIndex--; } // it's a move, make sure we move it cleanly if(oldParent){ if(node.fireEvent('beforemove', node.getOwnerTree(), node, oldParent, this, index, refNode) === false){ return false; } oldParent.removeChild(node); } if(refIndex == 0){ this.setFirstChild(node); } this.childNodes.splice(refIndex, 0, node); node.parentNode = this; var ps = this.childNodes[refIndex-1]; if(ps){ node.previousSibling = ps; ps.nextSibling = node; } node.nextSibling = refNode; node.setOwnerTree(this.getOwnerTree()); this.fireEvent('insert', this.ownerTree, this, node, refNode); if(oldParent){ node.fireEvent('move', this.ownerTree, node, oldParent, this, refIndex, refNode); } return node; }, item : function(index){ return this.childNodes[index]; }, replaceChild : function(newChild, oldChild){ this.insertBefore(newChild, oldChild); this.removeChild(oldChild); return oldChild; }, indexOf : function(child){ return this.childNodes.indexOf(child); }, getOwnerTree : function(){ // if it doesn't have one, look for one if(!this.ownerTree){ var p = this; while(p){ if(p.ownerTree){ this.ownerTree = p.ownerTree; break; } p = p.parentNode; } } return this.ownerTree; }, setOwnerTree : function(tree){ // if it's move, we need to update everyone if(tree != this.ownerTree){ if(this.ownerTree){ this.ownerTree.unregisterNode(this); } this.ownerTree = tree; var cs = this.childNodes; for(var i = 0, len = cs.length; i < len; i++) { cs[i].setOwnerTree(tree); } if(tree){ tree.registerNode(this); } } }, getPath : function(attr){ attr = attr || 'id'; var p = this.parentNode; var b = [this.attributes[attr]]; while(p){ b.unshift(p.attributes[attr]); p = p.parentNode; } var sep = this.getOwnerTree().pathSeparator; return sep + b.join(sep); }, bubble : function(fn, scope, args){ var p = this; while(p){ if(fn.call(scope || p, args || p) === false){ break; } p = p.parentNode; } }, cascade : function(fn, scope, args){ if(fn.call(scope || this, args || this) !== false){ var cs = this.childNodes; for(var i = 0, len = cs.length; i < len; i++) { cs[i].cascade(fn, scope, args); } } }, eachChild : function(fn, scope, args){ var cs = this.childNodes; for(var i = 0, len = cs.length; i < len; i++) { if(fn.call(scope || this, args || cs[i]) === false){ break; } } }, findChild : function(attribute, value){ var cs = this.childNodes; for(var i = 0, len = cs.length; i < len; i++) { if(cs[i].attributes[attribute] == value){ return cs[i]; } } return null; }, /** * Sorts this nodes children using the supplied sort function * @param {Function} fn * @param {Object} scope */ sort : function(fn, scope){ var cs = this.childNodes; var len = cs.length; if(len > 0){ var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn; cs.sort(sortFn); for(var i = 0; i < len; i++){ var n = cs[i]; n.previousSibling = cs[i-1]; n.nextSibling = cs[i+1]; if(i == 0){ this.setFirstChild(n); } if(i == len-1){ this.setLastChild(n); } } } }, contains : function(node){ return node.isAncestor(this); }, isAncestor : function(node){ var p = this.parentNode; while(p){ if(p == node){ return true; } p = p.parentNode; } return false; }, toString : function(){ return '[Node'+(this.id?' '+this.id:'')+']'; } });