2 Copyright (c) 2006, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
8 * The treeview widget is a generic tree building tool.
10 * @title TreeView Widget
11 * @requires yahoo, event
13 * @namespace YAHOO.widget
17 * Contains the tree view state data and the root node.
20 * @uses YAHOO.util.EventProvider
22 * @param {string|HTMLElement} id The id of the element, or the element
23 * itself that the tree will be inserted into.
25 YAHOO.widget.TreeView = function(id) {
26 if (id) { this.init(id); }
29 YAHOO.widget.TreeView.prototype = {
32 * The id of tree container element
39 * The host element for this tree
46 * Flat collection of all nodes in this tree
54 * We lock the tree control while waiting for the dynamic loader to return
61 * The animation to use for expanding children, if any
62 * @property _expandAnim
69 * The animation to use for collapsing children, if any
70 * @property _collapseAnim
77 * The current number of animations that are executing
78 * @property _animCount
85 * The maximum number of animations to run at one time.
92 * Sets up the animation for expanding children
93 * @method setExpandAnim
94 * @param {string} type the type of animation (acceptable values defined
95 * in YAHOO.widget.TVAnim)
97 setExpandAnim: function(type) {
98 if (YAHOO.widget.TVAnim.isValid(type)) {
99 this._expandAnim = type;
104 * Sets up the animation for collapsing children
105 * @method setCollapseAnim
106 * @param {string} the type of animation (acceptable values defined in
107 * YAHOO.widget.TVAnim)
109 setCollapseAnim: function(type) {
110 if (YAHOO.widget.TVAnim.isValid(type)) {
111 this._collapseAnim = type;
116 * Perform the expand animation if configured, or just show the
117 * element if not configured or too many animations are in progress
118 * @method animateExpand
119 * @param el {HTMLElement} the element to animate
120 * @param node {YAHOO.util.Node} the node that was expanded
121 * @return {boolean} true if animation could be invoked, false otherwise
123 animateExpand: function(el, node) {
125 if (this._expandAnim && this._animCount < this.maxAnim) {
126 // this.locked = true;
128 var a = YAHOO.widget.TVAnim.getAnim(this._expandAnim, el,
129 function() { tree.expandComplete(node); });
132 this.fireEvent("animStart", {
146 * Perform the collapse animation if configured, or just show the
147 * element if not configured or too many animations are in progress
148 * @method animateCollapse
149 * @param el {HTMLElement} the element to animate
150 * @param node {YAHOO.util.Node} the node that was expanded
151 * @return {boolean} true if animation could be invoked, false otherwise
153 animateCollapse: function(el, node) {
155 if (this._collapseAnim && this._animCount < this.maxAnim) {
156 // this.locked = true;
158 var a = YAHOO.widget.TVAnim.getAnim(this._collapseAnim, el,
159 function() { tree.collapseComplete(node); });
162 this.fireEvent("animStart", {
176 * Function executed when the expand animation completes
177 * @method expandComplete
179 expandComplete: function(node) {
181 this.fireEvent("animComplete", {
185 // this.locked = false;
189 * Function executed when the collapse animation completes
190 * @method collapseComplete
192 collapseComplete: function(node) {
194 this.fireEvent("animComplete", {
198 // this.locked = false;
202 * Initializes the tree
204 * @parm {string|HTMLElement} id the id of the element that will hold the tree
211 if ("string" !== typeof id) {
213 this.id = this.generateId(id);
217 * When animation is enabled, this event fires when the animation
221 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
222 * @parm {String} type the type of animation ("expand" or "collapse")
224 this.createEvent("animStart", this);
227 * When animation is enabled, this event fires when the animation
229 * @event animComplete
231 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
232 * @parm {String} type the type of animation ("expand" or "collapse")
234 this.createEvent("animComplete", this);
237 * Fires when a node is going to be collapsed. Return false to stop
241 * @param {YAHOO.widget.Node} node the node that is collapsing
243 this.createEvent("collapse", this);
246 * Fires after a node is successfully collapsed. This event will not fire
247 * if the "collapse" event was cancelled.
248 * @event collapseComplete
250 * @param {YAHOO.widget.Node} node the node that was collapsed
252 this.createEvent("collapseComplete", this);
255 * Fires when a node is going to be expanded. Return false to stop
259 * @param {YAHOO.widget.Node} node the node that is expanding
261 this.createEvent("expand", this);
264 * Fires after a node is successfully expanded. This event will not fire
265 * if the "expand" event was cancelled.
266 * @event expandComplete
268 * @param {YAHOO.widget.Node} node the node that was expanded
270 this.createEvent("expandComplete", this);
274 // store a global reference
275 YAHOO.widget.TreeView.trees[this.id] = this;
277 // Set up the root node
278 this.root = new YAHOO.widget.RootNode(this);
282 //YAHOO.util.Event.onContentReady(this.id, this.handleAvailable, this, true);
283 YAHOO.util.Event.on(this.id, "click", this.handleClick, this, true);
286 //handleAvailable: function() {
287 //var Event = YAHOO.util.Event;
292 * Renders the tree boilerplate and visible nodes
296 var html = this.root.getHtml();
297 this.getEl().innerHTML = html;
298 this.firstDraw = false;
302 * Returns the tree's host element
304 * @return {HTMLElement} the host element
308 this._el = document.getElementById(this.id);
314 * Nodes register themselves with the tree instance when they are created.
316 * @param node {Node} the node to register
319 regNode: function(node) {
320 this._nodes[node.index] = node;
324 * Returns the root node of this tree
326 * @return {Node} the root node
328 getRoot: function() {
333 * Configures this tree to dynamically load all child data
334 * @method setDynamicLoad
335 * @param {function} fnDataLoader the function that will be called to get the data
336 * @param iconMode {int} configures the icon that is displayed when a dynamic
337 * load node is expanded the first time without children. By default, the
338 * "collapse" icon will be used. If set to 1, the leaf node icon will be
341 setDynamicLoad: function(fnDataLoader, iconMode) {
342 this.root.setDynamicLoad(fnDataLoader, iconMode);
346 * Expands all child nodes. Note: this conflicts with the "multiExpand"
347 * node property. If expand all is called in a tree with nodes that
348 * do not allow multiple siblings to be displayed, only the last sibling
352 expandAll: function() {
354 this.root.expandAll();
359 * Collapses all expanded child nodes in the entire tree.
360 * @method collapseAll
362 collapseAll: function() {
364 this.root.collapseAll();
369 * Returns a node in the tree that has the specified index (this index
370 * is created internally, so this function probably will only be used
371 * in html generated for a given node.)
372 * @method getNodeByIndex
373 * @param {int} nodeIndex the index of the node wanted
374 * @return {Node} the node with index=nodeIndex, null if no match
376 getNodeByIndex: function(nodeIndex) {
377 var n = this._nodes[nodeIndex];
378 return (n) ? n : null;
382 * Returns a node that has a matching property and value in the data
383 * object that was passed into its constructor.
384 * @method getNodeByProperty
385 * @param {object} property the property to search (usually a string)
386 * @param {object} value the value we want to find (usuall an int or string)
387 * @return {Node} the matching node, null if no match
389 getNodeByProperty: function(property, value) {
390 for (var i in this._nodes) {
391 var n = this._nodes[i];
392 if (n.data && value == n.data[property]) {
401 * Returns a collection of nodes that have a matching property
402 * and value in the data object that was passed into its constructor.
403 * @method getNodesByProperty
404 * @param {object} property the property to search (usually a string)
405 * @param {object} value the value we want to find (usuall an int or string)
406 * @return {Array} the matching collection of nodes, null if no match
408 getNodesByProperty: function(property, value) {
410 for (var i in this._nodes) {
411 var n = this._nodes[i];
412 if (n.data && value == n.data[property]) {
417 return (values.length) ? values : null;
421 * Removes the node and its children, and optionally refreshes the
422 * branch of the tree that was affected.
424 * @param {Node} The node to remove
425 * @param {boolean} autoRefresh automatically refreshes branch if true
426 * @return {boolean} False is there was a problem, true otherwise.
428 removeNode: function(node, autoRefresh) {
430 // Don't delete the root node
435 // Get the branch that we may need to refresh
441 // Delete the node and its children
442 this._deleteNode(node);
444 // Refresh the parent of the parent
445 if (autoRefresh && p && p.childrenRendered) {
453 * Deletes this nodes child collection, recursively. Also collapses
454 * the node, and resets the dynamic load flag. The primary use for
455 * this method is to purge a node and allow it to fetch its data
457 * @method removeChildren
458 * @param {Node} node the node to purge
460 removeChildren: function(node) {
461 while (node.children.length) {
462 this._deleteNode(node.children[0]);
465 node.childrenRendered = false;
466 node.dynamicLoadComplete = false;
475 * Deletes the node and recurses children
476 * @method _deleteNode
479 _deleteNode: function(node) {
480 // Remove all the child nodes first
481 this.removeChildren(node);
483 // Remove the node from the tree
488 * Removes the node from the tree, preserving the child collection
489 * to make it possible to insert the branch into another part of the
490 * tree, or another tree.
492 * @param {Node} the node to remove
494 popNode: function(node) {
497 // Update the parent's collection of children
500 for (var i=0, len=p.children.length;i<len;++i) {
501 if (p.children[i] != node) {
502 a[a.length] = p.children[i];
508 // reset the childrenRendered flag for the parent
509 p.childrenRendered = false;
511 // Update the sibling relationship
512 if (node.previousSibling) {
513 node.previousSibling.nextSibling = node.nextSibling;
516 if (node.nextSibling) {
517 node.nextSibling.previousSibling = node.previousSibling;
521 node.previousSibling = null;
522 node.nextSibling = null;
525 // Update the tree's node collection
526 delete this._nodes[node.index];
530 * TreeView instance toString
532 * @return {string} string representation of the tree
534 toString: function() {
535 return "TreeView " + this.id;
539 * Generates an unique id for an element if it doesn't yet have one
543 generateId: function(el) {
547 id = "yui-tv-auto-id-" + YAHOO.widget.TreeView.counter;
548 ++YAHOO.widget.TreeView.counter;
555 * Abstract method that is executed when a node is expanded
557 * @param node {Node} the node that was expanded
558 * @deprecated use treeobj.subscribe("expand") instead
560 onExpand: function(node) { },
563 * Abstract method that is executed when a node is collapsed.
565 * @param node {Node} the node that was collapsed.
566 * @deprecated use treeobj.subscribe("collapse") instead
568 onCollapse: function(node) { }
572 YAHOO.augment(YAHOO.widget.TreeView, YAHOO.util.EventProvider);
575 * Count of all nodes in all trees
576 * @property YAHOO.widget.TreeView.nodeCount
580 YAHOO.widget.TreeView.nodeCount = 0;
583 * Global cache of tree instances
584 * @property YAHOO.widget.TreeView.trees
589 YAHOO.widget.TreeView.trees = [];
592 * Counter for generating a new unique element id
593 * @property YAHOO.widget.TreeView.counter
597 YAHOO.widget.TreeView.counter = 0;
600 * Global method for getting a tree by its id. Used in the generated
602 * @method YAHOO.widget.TreeView.getTree
603 * @param treeId {String} the id of the tree instance
604 * @return {TreeView} the tree instance requested, null if not found.
607 YAHOO.widget.TreeView.getTree = function(treeId) {
608 var t = YAHOO.widget.TreeView.trees[treeId];
609 return (t) ? t : null;
613 * Global method for getting a node by its id. Used in the generated
615 * @method YAHOO.widget.TreeView.getNode
616 * @param treeId {String} the id of the tree instance
617 * @param nodeIndex {String} the index of the node to return
618 * @return {Node} the node instance requested, null if not found
621 YAHOO.widget.TreeView.getNode = function(treeId, nodeIndex) {
622 var t = YAHOO.widget.TreeView.getTree(treeId);
623 return (t) ? t.getNodeByIndex(nodeIndex) : null;
628 * @method YAHOO.widget.TreeView.addHandler
629 * @param el the elment to bind the handler to
630 * @param {string} sType the type of event handler
631 * @param {function} fn the callback to invoke
634 YAHOO.widget.TreeView.addHandler = function (el, sType, fn) {
635 if (el.addEventListener) {
636 el.addEventListener(sType, fn, false);
637 } else if (el.attachEvent) {
638 el.attachEvent("on" + sType, fn);
644 * @method YAHOO.widget.TreeView.removeHandler
645 * @param el the elment to bind the handler to
646 * @param {string} sType the type of event handler
647 * @param {function} fn the callback to invoke
651 YAHOO.widget.TreeView.removeHandler = function (el, sType, fn) {
652 if (el.removeEventListener) {
653 el.removeEventListener(sType, fn, false);
654 } else if (el.detachEvent) {
655 el.detachEvent("on" + sType, fn);
660 * Attempts to preload the images defined in the styles used to draw the tree by
661 * rendering off-screen elements that use the styles.
662 * @method YAHOO.widget.TreeView.preload
663 * @param {string} prefix the prefix to use to generate the names of the
664 * images to preload, default is ygtv
667 YAHOO.widget.TreeView.preload = function(prefix) {
668 prefix = prefix || "ygtv";
669 var styles = ["tn","tm","tmh","tp","tph","ln","lm","lmh","lp","lph","loading"];
673 for (var i = 0; i < styles.length; ++i) {
674 sb[sb.length] = '<span class="' + prefix + styles[i] + '"> </span>';
677 var f = document.createElement("div");
679 s.position = "absolute";
682 f.innerHTML = sb.join("");
684 document.body.appendChild(f);
686 YAHOO.widget.TreeView.removeHandler(window,
687 "load", YAHOO.widget.TreeView.preload);
691 YAHOO.widget.TreeView.addHandler(window,
692 "load", YAHOO.widget.TreeView.preload);
695 * The base class for all tree nodes. The node's presentation and behavior in
696 * response to mouse events is handled in Node subclasses.
697 * @namespace YAHOO.widget
699 * @uses YAHOO.util.EventProvider
700 * @param oData {object} a string or object containing the data that will
701 * be used to render this node
702 * @param oParent {Node} this node's parent node
703 * @param expanded {boolean} the initial expanded/collapsed state
706 YAHOO.widget.Node = function(oData, oParent, expanded) {
707 if (oData) { this.init(oData, oParent, expanded); }
710 YAHOO.widget.Node.prototype = {
713 * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
720 * This node's child node collection.
727 * Tree instance this node is part of
734 * The data linked to this node. This can be any object or primitive
735 * value, and the data can be used in getNodeHtml().
749 * The depth of this node. We start at -1 for the root node.
756 * The href for the node's label. If one is not specified, the href will
757 * be set so that it toggles the node.
764 * The label href target, defaults to current window
771 * The node's expanded/collapsed state
778 * Can multiple children be expanded at once?
779 * @property multiExpand
785 * Should we render children for a collapsed node? It is possible that the
786 * implementer will want to render the hidden data... @todo verify that we
787 * need this, and implement it if we do.
788 * @property renderHidden
794 * This flag is set to true when the html is generated for this node's
795 * children, and set to false when new children are added.
796 * @property childrenRendered
799 childrenRendered: false,
802 * Dynamically loaded nodes only fetch the data the first time they are
803 * expanded. This flag is set to true once the data has been fetched.
804 * @property dynamicLoadComplete
807 dynamicLoadComplete: false,
810 * This node's previous sibling
811 * @property previousSibling
814 previousSibling: null,
817 * This node's next sibling
818 * @property nextSibling
824 * We can set the node up to call an external method to get the child
833 * Function to execute when we need to get this node's child data.
834 * @property dataLoader
840 * This is true for dynamically loading nodes while waiting for the
841 * callback to return.
842 * @property isLoading
848 * The toggle/branch icon will not show if this is set to false. This
849 * could be useful if the implementer wants to have the child contain
850 * extra info about the parent, rather than an actual node.
857 * Used to configure what happens when a dynamic load node is expanded
858 * and we discover that it does not have children. By default, it is
859 * treated as if it still could have children (plus/minus icon). Set
860 * iconMode to have it display like a leaf node instead.
867 * Specifies whether or not the content area of the node should be allowed
883 spacerPath: "http://us.i1.yimg.com/us.yimg.com/i/space.gif",
884 expandedText: "Expanded",
885 collapsedText: "Collapsed",
886 loadingText: "Loading",
890 * Initializes this node, gets some of the properties from the parent
892 * @param oData {object} a string or object containing the data that will
893 * be used to render this node
894 * @param oParent {Node} this node's parent node
895 * @param expanded {boolean} the initial expanded/collapsed state
897 init: function(oData, oParent, expanded) {
901 this.index = YAHOO.widget.TreeView.nodeCount;
902 ++YAHOO.widget.TreeView.nodeCount;
903 this.expanded = expanded;
906 * The parentChange event is fired when a parent element is applied
907 * to the node. This is useful if you need to apply tree-level
908 * properties to a tree that need to happen if a node is moved from
909 * one tree to another.
911 * @event parentChange
914 this.createEvent("parentChange", this);
916 // oParent should never be null except when we create the root node.
918 oParent.appendChild(this);
923 * Certain properties for the node cannot be set until the parent
924 * is known. This is called after the node is inserted into a tree.
925 * the parent is also applied to this node's children in order to
926 * make it possible to move a branch from one tree to another.
927 * @method applyParent
928 * @param {Node} parentNode this node's parent node
929 * @return {boolean} true if the application was successful
931 applyParent: function(parentNode) {
936 this.tree = parentNode.tree;
937 this.parent = parentNode;
938 this.depth = parentNode.depth + 1;
941 this.href = "javascript:" + this.getToggleLink();
944 if (! this.multiExpand) {
945 this.multiExpand = parentNode.multiExpand;
948 this.tree.regNode(this);
949 parentNode.childrenRendered = false;
951 // cascade update existing children
952 for (var i=0, len=this.children.length;i<len;++i) {
953 this.children[i].applyParent(this);
956 this.fireEvent("parentChange");
962 * Appends a node to the child collection.
963 * @method appendChild
964 * @param childNode {Node} the new node
965 * @return {Node} the child node
968 appendChild: function(childNode) {
969 if (this.hasChildren()) {
970 var sib = this.children[this.children.length - 1];
971 sib.nextSibling = childNode;
972 childNode.previousSibling = sib;
974 this.children[this.children.length] = childNode;
975 childNode.applyParent(this);
981 * Appends this node to the supplied node's child collection
983 * @param parentNode {Node} the node to append to.
984 * @return {Node} The appended node
986 appendTo: function(parentNode) {
987 return parentNode.appendChild(this);
991 * Inserts this node before this supplied node
992 * @method insertBefore
993 * @param node {Node} the node to insert this node before
994 * @return {Node} the inserted node
996 insertBefore: function(node) {
1001 this.tree.popNode(this);
1004 var refIndex = node.isChildOf(p);
1005 p.children.splice(refIndex, 0, this);
1006 if (node.previousSibling) {
1007 node.previousSibling.nextSibling = this;
1009 this.previousSibling = node.previousSibling;
1010 this.nextSibling = node;
1011 node.previousSibling = this;
1013 this.applyParent(p);
1020 * Inserts this node after the supplied node
1021 * @method insertAfter
1022 * @param node {Node} the node to insert after
1023 * @return {Node} the inserted node
1025 insertAfter: function(node) {
1026 var p = node.parent;
1030 this.tree.popNode(this);
1033 var refIndex = node.isChildOf(p);
1035 if (!node.nextSibling) {
1036 return this.appendTo(p);
1039 p.children.splice(refIndex + 1, 0, this);
1041 node.nextSibling.previousSibling = this;
1042 this.previousSibling = node;
1043 this.nextSibling = node.nextSibling;
1044 node.nextSibling = this;
1046 this.applyParent(p);
1053 * Returns true if the Node is a child of supplied Node
1055 * @param parentNode {Node} the Node to check
1056 * @return {boolean} The node index if this Node is a child of
1057 * supplied Node, else -1.
1060 isChildOf: function(parentNode) {
1061 if (parentNode && parentNode.children) {
1062 for (var i=0, len=parentNode.children.length; i<len ; ++i) {
1063 if (parentNode.children[i] === this) {
1073 * Returns a node array of this node's siblings, null if none.
1074 * @method getSiblings
1077 getSiblings: function() {
1078 return this.parent.children;
1082 * Shows this node's children
1083 * @method showChildren
1085 showChildren: function() {
1086 if (!this.tree.animateExpand(this.getChildrenEl(), this)) {
1087 if (this.hasChildren()) {
1088 this.getChildrenEl().style.display = "";
1094 * Hides this node's children
1095 * @method hideChildren
1097 hideChildren: function() {
1099 if (!this.tree.animateCollapse(this.getChildrenEl(), this)) {
1100 this.getChildrenEl().style.display = "none";
1105 * Returns the id for this node's container div
1107 * @return {string} the element id
1109 getElId: function() {
1110 return "ygtv" + this.index;
1114 * Returns the id for this node's children div
1115 * @method getChildrenElId
1116 * @return {string} the element id for this node's children div
1118 getChildrenElId: function() {
1119 return "ygtvc" + this.index;
1123 * Returns the id for this node's toggle element
1124 * @method getToggleElId
1125 * @return {string} the toggel element id
1127 getToggleElId: function() {
1128 return "ygtvt" + this.index;
1132 * Returns the id for this node's spacer image. The spacer is positioned
1133 * over the toggle and provides feedback for screen readers.
1134 * @method getSpacerId
1135 * @return {string} the id for the spacer image
1138 getSpacerId: function() {
1139 return "ygtvspacer" + this.index;
1144 * Returns this node's container html element
1146 * @return {HTMLElement} the container html element
1149 return document.getElementById(this.getElId());
1153 * Returns the div that was generated for this node's children
1154 * @method getChildrenEl
1155 * @return {HTMLElement} this node's children div
1157 getChildrenEl: function() {
1158 return document.getElementById(this.getChildrenElId());
1162 * Returns the element that is being used for this node's toggle.
1163 * @method getToggleEl
1164 * @return {HTMLElement} this node's toggle html element
1166 getToggleEl: function() {
1167 return document.getElementById(this.getToggleElId());
1171 * Returns the element that is being used for this node's spacer.
1173 * @return {HTMLElement} this node's spacer html element
1176 getSpacer: function() {
1177 return document.getElementById( this.getSpacerId() ) || {};
1182 getStateText: function() {
1183 if (this.isLoading) {
1184 return this.loadingText;
1185 } else if (this.hasChildren(true)) {
1186 if (this.expanded) {
1187 return this.expandedText;
1189 return this.collapsedText;
1198 * Generates the link that will invoke this node's toggle method
1199 * @method getToggleLink
1200 * @return {string} the javascript url for toggling this node
1202 getToggleLink: function() {
1203 return "YAHOO.widget.TreeView.getNode(\'" + this.tree.id + "\'," +
1204 this.index + ").toggle()";
1208 * Hides this nodes children (creating them if necessary), changes the
1212 collapse: function() {
1213 // Only collapse if currently expanded
1214 if (!this.expanded) { return; }
1216 // fire the collapse event handler
1217 var ret = this.tree.onCollapse(this);
1219 if (false === ret) {
1223 ret = this.tree.fireEvent("collapse", this);
1225 if (false === ret) {
1229 if (!this.getEl()) {
1230 this.expanded = false;
1232 // hide the child div
1233 this.hideChildren();
1234 this.expanded = false;
1239 // this.getSpacer().title = this.getStateText();
1241 ret = this.tree.fireEvent("collapseComplete", this);
1246 * Shows this nodes children (creating them if necessary), changes the
1247 * toggle style, and collapses its siblings if multiExpand is not set.
1250 expand: function() {
1251 // Only expand if currently collapsed.
1252 if (this.expanded) { return; }
1254 // fire the expand event handler
1255 var ret = this.tree.onExpand(this);
1257 if (false === ret) {
1261 ret = this.tree.fireEvent("expand", this);
1263 if (false === ret) {
1267 if (!this.getEl()) {
1268 this.expanded = true;
1272 if (! this.childrenRendered) {
1273 this.getChildrenEl().innerHTML = this.renderChildren();
1277 this.expanded = true;
1281 // this.getSpacer().title = this.getStateText();
1283 // We do an extra check for children here because the lazy
1284 // load feature can expose nodes that have no children.
1286 // if (!this.hasChildren()) {
1287 if (this.isLoading) {
1288 this.expanded = false;
1292 if (! this.multiExpand) {
1293 var sibs = this.getSiblings();
1294 for (var i=0; i<sibs.length; ++i) {
1295 if (sibs[i] != this && sibs[i].expanded) {
1301 this.showChildren();
1303 ret = this.tree.fireEvent("expandComplete", this);
1306 updateIcon: function() {
1308 var el = this.getToggleEl();
1310 el.className = this.getStyle();
1316 * Returns the css style name for the toggle
1318 * @return {string} the css class for this node's toggle
1320 getStyle: function() {
1321 if (this.isLoading) {
1322 return "ygtvloading";
1324 // location top or bottom, middle nodes also get the top style
1325 var loc = (this.nextSibling) ? "t" : "l";
1327 // type p=plus(expand), m=minus(collapase), n=none(no children)
1329 if (this.hasChildren(true) || (this.isDynamic() && !this.getIconMode())) {
1330 // if (this.hasChildren(true)) {
1331 type = (this.expanded) ? "m" : "p";
1334 return "ygtv" + loc + type;
1339 * Returns the hover style for the icon
1340 * @return {string} the css class hover state
1341 * @method getHoverStyle
1343 getHoverStyle: function() {
1344 var s = this.getStyle();
1345 if (this.hasChildren(true) && !this.isLoading) {
1352 * Recursively expands all of this node's children.
1355 expandAll: function() {
1356 for (var i=0;i<this.children.length;++i) {
1357 var c = this.children[i];
1358 if (c.isDynamic()) {
1359 alert("Not supported (lazy load + expand all)");
1361 } else if (! c.multiExpand) {
1362 alert("Not supported (no multi-expand + expand all)");
1372 * Recursively collapses all of this node's children.
1373 * @method collapseAll
1375 collapseAll: function() {
1376 for (var i=0;i<this.children.length;++i) {
1377 this.children[i].collapse();
1378 this.children[i].collapseAll();
1383 * Configures this node for dynamically obtaining the child data
1384 * when the node is first expanded. Calling it without the callback
1385 * will turn off dynamic load for the node.
1386 * @method setDynamicLoad
1387 * @param fmDataLoader {function} the function that will be used to get the data.
1388 * @param iconMode {int} configures the icon that is displayed when a dynamic
1389 * load node is expanded the first time without children. By default, the
1390 * "collapse" icon will be used. If set to 1, the leaf node icon will be
1393 setDynamicLoad: function(fnDataLoader, iconMode) {
1395 this.dataLoader = fnDataLoader;
1396 this._dynLoad = true;
1398 this.dataLoader = null;
1399 this._dynLoad = false;
1403 this.iconMode = iconMode;
1408 * Evaluates if this node is the root node of the tree
1410 * @return {boolean} true if this is the root node
1412 isRoot: function() {
1413 return (this == this.tree.root);
1417 * Evaluates if this node's children should be loaded dynamically. Looks for
1418 * the property both in this instance and the root node. If the tree is
1419 * defined to load all children dynamically, the data callback function is
1420 * defined in the root node
1422 * @return {boolean} true if this node's children are to be loaded dynamically
1424 isDynamic: function() {
1425 var lazy = (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
1430 * Returns the current icon mode. This refers to the way childless dynamic
1431 * load nodes appear.
1432 * @method getIconMode
1433 * @return {int} 0 for collapse style, 1 for leaf node style
1435 getIconMode: function() {
1436 return (this.iconMode || this.tree.root.iconMode);
1440 * Checks if this node has children. If this node is lazy-loading and the
1441 * children have not been rendered, we do not know whether or not there
1442 * are actual children. In most cases, we need to assume that there are
1443 * children (for instance, the toggle needs to show the expandable
1444 * presentation state). In other times we want to know if there are rendered
1445 * children. For the latter, "checkForLazyLoad" should be false.
1446 * @method hasChildren
1447 * @param checkForLazyLoad {boolean} should we check for unloaded children?
1448 * @return {boolean} true if this has children or if it might and we are
1449 * checking for this condition.
1451 hasChildren: function(checkForLazyLoad) {
1452 return ( this.children.length > 0 ||
1453 (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) );
1457 * Expands if node is collapsed, collapses otherwise.
1460 toggle: function() {
1461 if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
1462 if (this.expanded) { this.collapse(); } else { this.expand(); }
1467 * Returns the markup for this node and its children.
1469 * @return {string} the markup for this node and its expanded children.
1471 getHtml: function() {
1473 this.childrenRendered = false;
1476 sb[sb.length] = '<div class="ygtvitem" id="' + this.getElId() + '">';
1477 sb[sb.length] = this.getNodeHtml();
1478 sb[sb.length] = this.getChildrenHtml();
1479 sb[sb.length] = '</div>';
1484 * Called when first rendering the tree. We always build the div that will
1485 * contain this nodes children, but we don't render the children themselves
1486 * unless this node is expanded.
1487 * @method getChildrenHtml
1488 * @return {string} the children container div html and any expanded children
1491 getChildrenHtml: function() {
1494 sb[sb.length] = '<div class="ygtvchildren"';
1495 sb[sb.length] = ' id="' + this.getChildrenElId() + '"';
1496 if (!this.expanded) {
1497 sb[sb.length] = ' style="display:none;"';
1499 sb[sb.length] = '>';
1501 // Don't render the actual child node HTML unless this node is expanded.
1502 if ( (this.hasChildren(true) && this.expanded) ||
1503 (this.renderHidden && !this.isDynamic()) ) {
1504 sb[sb.length] = this.renderChildren();
1507 sb[sb.length] = '</div>';
1513 * Generates the markup for the child nodes. This is not done until the node
1515 * @method renderChildren
1516 * @return {string} the html for this node's children
1519 renderChildren: function() {
1524 if (this.isDynamic() && !this.dynamicLoadComplete) {
1525 this.isLoading = true;
1526 this.tree.locked = true;
1528 if (this.dataLoader) {
1532 node.dataLoader(node,
1534 node.loadComplete();
1538 } else if (this.tree.root.dataLoader) {
1542 node.tree.root.dataLoader(node,
1544 node.loadComplete();
1549 return "Error: data loader not found or not specified.";
1555 return this.completeRender();
1560 * Called when we know we have all the child data.
1561 * @method completeRender
1562 * @return {string} children html
1564 completeRender: function() {
1567 for (var i=0; i < this.children.length; ++i) {
1568 // this.children[i].childrenRendered = false;
1569 sb[sb.length] = this.children[i].getHtml();
1572 this.childrenRendered = true;
1578 * Load complete is the callback function we pass to the data provider
1579 * in dynamic load situations.
1580 * @method loadComplete
1582 loadComplete: function() {
1583 this.getChildrenEl().innerHTML = this.completeRender();
1584 this.dynamicLoadComplete = true;
1585 this.isLoading = false;
1587 this.tree.locked = false;
1591 * Returns this node's ancestor at the specified depth.
1592 * @method getAncestor
1593 * @param {int} depth the depth of the ancestor.
1594 * @return {Node} the ancestor
1596 getAncestor: function(depth) {
1597 if (depth >= this.depth || depth < 0) {
1601 var p = this.parent;
1603 while (p.depth > depth) {
1611 * Returns the css class for the spacer at the specified depth for
1612 * this node. If this node's ancestor at the specified depth
1613 * has a next sibling the presentation is different than if it
1614 * does not have a next sibling
1615 * @method getDepthStyle
1616 * @param {int} depth the depth of the ancestor.
1617 * @return {string} the css class for the spacer
1619 getDepthStyle: function(depth) {
1620 return (this.getAncestor(depth).nextSibling) ?
1621 "ygtvdepthcell" : "ygtvblankdepthcell";
1625 * Get the markup for the node. This is designed to be overrided so that we can
1626 * support different types of nodes.
1627 * @method getNodeHtml
1628 * @return {string} The HTML that will render this node.
1630 getNodeHtml: function() {
1635 * Regenerates the html for this node and its children. To be used when the
1636 * node is expanded and new children have been added.
1639 refresh: function() {
1640 // this.loadComplete();
1641 this.getChildrenEl().innerHTML = this.completeRender();
1644 var el = this.getToggleEl();
1646 el.className = this.getStyle();
1654 * @return {string} string representation of the node
1656 toString: function() {
1657 return "Node (" + this.index + ")";
1662 YAHOO.augment(YAHOO.widget.Node, YAHOO.util.EventProvider);
1665 * A custom YAHOO.widget.Node that handles the unique nature of
1666 * the virtual, presentationless root node.
1667 * @namespace YAHOO.widget
1669 * @extends YAHOO.widget.Node
1670 * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to
1673 YAHOO.widget.RootNode = function(oTree) {
1674 // Initialize the node with null params. The root node is a
1675 // special case where the node has no presentation. So we have
1676 // to alter the standard properties a bit.
1677 this.init(null, null, true);
1680 * For the root node, we get the tree reference from as a param
1681 * to the constructor instead of from the parent element.
1686 YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, {
1688 // overrides YAHOO.widget.Node
1689 getNodeHtml: function() {
1693 toString: function() {
1697 loadComplete: function() {
1701 collapse: function() {},
1702 expand: function() {}
1706 * The default node presentation. The first parameter should be
1707 * either a string that will be used as the node's label, or an object
1708 * that has a string propery called label. By default, the clicking the
1709 * label will toggle the expanded/collapsed state of the node. By
1710 * changing the href property of the instance, this behavior can be
1711 * changed so that the label will go to the specified href.
1712 * @namespace YAHOO.widget
1714 * @extends YAHOO.widget.Node
1716 * @param oData {object} a string or object containing the data that will
1717 * be used to render this node
1718 * @param oParent {YAHOO.widget.Node} this node's parent node
1719 * @param expanded {boolean} the initial expanded/collapsed state
1721 YAHOO.widget.TextNode = function(oData, oParent, expanded) {
1724 this.init(oData, oParent, expanded);
1725 this.setUpLabel(oData);
1730 YAHOO.extend(YAHOO.widget.TextNode, YAHOO.widget.Node, {
1733 * The CSS class for the label href. Defaults to ygtvlabel, but can be
1734 * overridden to provide a custom presentation for a specific node.
1735 * @property labelStyle
1738 labelStyle: "ygtvlabel",
1741 * The derived element id of the label for this node
1742 * @property labelElId
1748 * The text for the label. It is assumed that the oData parameter will
1749 * either be a string that will be used as the label, or an object that
1750 * has a property called "label" that we will use.
1756 textNodeParentChange: function() {
1759 * Custom event that is fired when the text node label is clicked. The
1760 * custom event is defined on the tree instance, so there is a single
1761 * event that handles all nodes in the tree. The node clicked is
1762 * provided as an argument
1765 * @for YAHOO.widget.TreeView
1766 * @param {YAHOO.widget.Node} node the node clicked
1768 if (this.tree && !this.tree.hasEvent("labelClick")) {
1769 this.tree.createEvent("labelClick", this.tree);
1775 * Sets up the node label
1776 * @method setUpLabel
1777 * @param oData string containing the label, or an object with a label property
1779 setUpLabel: function(oData) {
1781 // set up the custom event on the tree
1782 this.textNodeParentChange();
1783 this.subscribe("parentChange", this.textNodeParentChange);
1785 if (typeof oData == "string") {
1786 oData = { label: oData };
1788 this.label = oData.label;
1792 this.href = oData.href;
1797 this.target = oData.target;
1801 this.labelStyle = oData.style;
1804 this.labelElId = "ygtvlabelel" + this.index;
1808 * Returns the label element
1809 * @for YAHOO.widget.TextNode
1810 * @method getLabelEl
1811 * @return {object} the element
1813 getLabelEl: function() {
1814 return document.getElementById(this.labelElId);
1817 // overrides YAHOO.widget.Node
1818 getNodeHtml: function() {
1821 sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
1822 sb[sb.length] = '<tr>';
1824 for (var i=0;i<this.depth;++i) {
1825 //sb[sb.length] = '<td><div class="' + this.getDepthStyle(i) + '"> </div></td>';
1826 //sb[sb.length] = '<td><div class="' + this.getDepthStyle(i) + '"></div></td>';
1827 sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '"><div class="ygtvspacer"></div></td>';
1830 var getNode = 'YAHOO.widget.TreeView.getNode(\'' +
1831 this.tree.id + '\',' + this.index + ')';
1833 sb[sb.length] = '<td';
1834 // sb[sb.length] = ' onselectstart="return false"';
1835 sb[sb.length] = ' id="' + this.getToggleElId() + '"';
1836 sb[sb.length] = ' class="' + this.getStyle() + '"';
1837 if (this.hasChildren(true)) {
1838 sb[sb.length] = ' onmouseover="this.className=';
1839 sb[sb.length] = getNode + '.getHoverStyle()"';
1840 sb[sb.length] = ' onmouseout="this.className=';
1841 sb[sb.length] = getNode + '.getStyle()"';
1843 sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '">';
1845 sb[sb.length] = '<div class="ygtvspacer">';
1848 sb[sb.length] = '<img id="' + this.getSpacerId() + '"';
1849 sb[sb.length] = ' alt=""';
1850 sb[sb.length] = ' tabindex=0';
1851 sb[sb.length] = ' src="' + this.spacerPath + '"';
1852 sb[sb.length] = ' title="' + this.getStateText() + '"';
1853 sb[sb.length] = ' class="ygtvspacer"';
1854 // sb[sb.length] = ' onkeypress="return ' + getNode + '".onKeyPress()"';
1855 sb[sb.length] = ' />';
1858 //sb[sb.length] = ' ';
1860 sb[sb.length] = '</div>';
1861 sb[sb.length] = '</td>';
1862 sb[sb.length] = '<td ';
1863 sb[sb.length] = (this.nowrap) ? ' nowrap="nowrap" ' : '';
1864 sb[sb.length] = ' >';
1865 sb[sb.length] = '<a';
1866 sb[sb.length] = ' id="' + this.labelElId + '"';
1867 sb[sb.length] = ' class="' + this.labelStyle + '"';
1868 sb[sb.length] = ' href="' + this.href + '"';
1869 sb[sb.length] = ' target="' + this.target + '"';
1870 sb[sb.length] = ' onclick="return ' + getNode + '.onLabelClick(' + getNode +')"';
1871 if (this.hasChildren(true)) {
1872 sb[sb.length] = ' onmouseover="document.getElementById(\'';
1873 sb[sb.length] = this.getToggleElId() + '\').className=';
1874 sb[sb.length] = getNode + '.getHoverStyle()"';
1875 sb[sb.length] = ' onmouseout="document.getElementById(\'';
1876 sb[sb.length] = this.getToggleElId() + '\').className=';
1877 sb[sb.length] = getNode + '.getStyle()"';
1879 sb[sb.length] = ' >';
1880 sb[sb.length] = this.label;
1881 sb[sb.length] = '</a>';
1882 sb[sb.length] = '</td>';
1883 sb[sb.length] = '</tr>';
1884 sb[sb.length] = '</table>';
1890 * Executed when the label is clicked. Fires the labelClick custom event.
1891 * @method onLabelClick
1892 * @param me {Node} this node
1893 * @scope the anchor tag clicked
1894 * @return false to cancel the anchor click
1896 onLabelClick: function(me) {
1897 return me.tree.fireEvent("labelClick", me);
1901 toString: function() {
1902 return "TextNode (" + this.index + ") " + this.label;
1907 * A menu-specific implementation that differs from TextNode in that only
1908 * one sibling can be expanded at a time.
1909 * @namespace YAHOO.widget
1911 * @extends YAHOO.widget.TextNode
1912 * @param oData {object} a string or object containing the data that will
1913 * be used to render this node
1914 * @param oParent {YAHOO.widget.Node} this node's parent node
1915 * @param expanded {boolean} the initial expanded/collapsed state
1918 YAHOO.widget.MenuNode = function(oData, oParent, expanded) {
1920 this.init(oData, oParent, expanded);
1921 this.setUpLabel(oData);
1925 * Menus usually allow only one branch to be open at a time.
1927 this.multiExpand = false;
1932 YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, {
1934 toString: function() {
1935 return "MenuNode (" + this.index + ") " + this.label;
1940 * This implementation takes either a string or object for the
1941 * oData argument. If is it a string, we will use it for the display
1942 * of this node (and it can contain any html code). If the parameter
1943 * is an object, we look for a parameter called "html" that will be
1944 * used for this node's display.
1945 * @namespace YAHOO.widget
1947 * @extends YAHOO.widget.Node
1949 * @param oData {object} a string or object containing the data that will
1950 * be used to render this node
1951 * @param oParent {YAHOO.widget.Node} this node's parent node
1952 * @param expanded {boolean} the initial expanded/collapsed state
1953 * @param hasIcon {boolean} specifies whether or not leaf nodes should
1956 YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) {
1958 this.init(oData, oParent, expanded);
1959 this.initContent(oData, hasIcon);
1963 YAHOO.extend(YAHOO.widget.HTMLNode, YAHOO.widget.Node, {
1966 * The CSS class for the html content container. Defaults to ygtvhtml, but
1967 * can be overridden to provide a custom presentation for a specific node.
1968 * @property contentStyle
1971 contentStyle: "ygtvhtml",
1974 * The generated id that will contain the data passed in by the implementer.
1975 * @property contentElId
1981 * The HTML content to use for this node's display
1988 * Sets up the node label
1989 * @property initContent
1990 * @param {object} An html string or object containing an html property
1991 * @param {boolean} hasIcon determines if the node will be rendered with an
1994 initContent: function(oData, hasIcon) {
1995 if (typeof oData == "string") {
1996 oData = { html: oData };
1999 this.html = oData.html;
2000 this.contentElId = "ygtvcontentel" + this.index;
2001 this.hasIcon = hasIcon;
2006 * Returns the outer html element for this node's content
2007 * @method getContentEl
2008 * @return {HTMLElement} the element
2010 getContentEl: function() {
2011 return document.getElementById(this.contentElId);
2014 // overrides YAHOO.widget.Node
2015 getNodeHtml: function() {
2018 sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
2019 sb[sb.length] = '<tr>';
2021 for (var i=0;i<this.depth;++i) {
2022 //sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '"> </td>';
2023 sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '"><div class="ygtvspacer"></div></td>';
2027 sb[sb.length] = '<td';
2028 sb[sb.length] = ' id="' + this.getToggleElId() + '"';
2029 sb[sb.length] = ' class="' + this.getStyle() + '"';
2030 sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '"';
2031 if (this.hasChildren(true)) {
2032 sb[sb.length] = ' onmouseover="this.className=';
2033 sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
2034 sb[sb.length] = this.tree.id + '\',' + this.index + ').getHoverStyle()"';
2035 sb[sb.length] = ' onmouseout="this.className=';
2036 sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
2037 sb[sb.length] = this.tree.id + '\',' + this.index + ').getStyle()"';
2039 //sb[sb.length] = '> </td>';
2040 sb[sb.length] = '><div class="ygtvspacer"></div></td>';
2043 sb[sb.length] = '<td';
2044 sb[sb.length] = ' id="' + this.contentElId + '"';
2045 sb[sb.length] = ' class="' + this.contentStyle + '"';
2046 sb[sb.length] = (this.nowrap) ? ' nowrap="nowrap" ' : '';
2047 sb[sb.length] = ' >';
2048 sb[sb.length] = this.html;
2049 sb[sb.length] = '</td>';
2050 sb[sb.length] = '</tr>';
2051 sb[sb.length] = '</table>';
2056 toString: function() {
2057 return "HTMLNode (" + this.index + ")";
2062 * A static factory class for tree view expand/collapse animations
2066 YAHOO.widget.TVAnim = function() {
2069 * Constant for the fade in animation
2074 FADE_IN: "TVFadeIn",
2077 * Constant for the fade out animation
2078 * @property FADE_OUT
2082 FADE_OUT: "TVFadeOut",
2085 * Returns a ygAnim instance of the given type
2087 * @param type {string} the type of animation
2088 * @param el {HTMLElement} the element to element (probably the children div)
2089 * @param callback {function} function to invoke when the animation is done.
2090 * @return {YAHOO.util.Animation} the animation instance
2093 getAnim: function(type, el, callback) {
2094 if (YAHOO.widget[type]) {
2095 return new YAHOO.widget[type](el, callback);
2102 * Returns true if the specified animation class is available
2104 * @param type {string} the type of animation
2105 * @return {boolean} true if valid, false if not
2108 isValid: function(type) {
2109 return (YAHOO.widget[type]);
2115 * A 1/2 second fade-in animation.
2118 * @param el {HTMLElement} the element to animate
2119 * @param callback {function} function to invoke when the animation is finished
2121 YAHOO.widget.TVFadeIn = function(el, callback) {
2123 * The element to animate
2130 * the callback to invoke when the animation is complete
2131 * @property callback
2134 this.callback = callback;
2138 YAHOO.widget.TVFadeIn.prototype = {
2140 * Performs the animation
2143 animate: function() {
2146 var s = this.el.style;
2148 s.filter = "alpha(opacity=10)";
2152 var a = new YAHOO.util.Anim(this.el, {opacity: {from: 0.1, to: 1, unit:""}}, dur);
2153 a.onComplete.subscribe( function() { tvanim.onComplete(); } );
2158 * Clean up and invoke callback
2159 * @method onComplete
2161 onComplete: function() {
2168 * @return {string} the string representation of the instance
2170 toString: function() {
2176 * A 1/2 second fade out animation.
2179 * @param el {HTMLElement} the element to animate
2180 * @param callback {Function} function to invoke when the animation is finished
2182 YAHOO.widget.TVFadeOut = function(el, callback) {
2184 * The element to animate
2191 * the callback to invoke when the animation is complete
2192 * @property callback
2195 this.callback = callback;
2199 YAHOO.widget.TVFadeOut.prototype = {
2201 * Performs the animation
2204 animate: function() {
2207 var a = new YAHOO.util.Anim(this.el, {opacity: {from: 1, to: 0.1, unit:""}}, dur);
2208 a.onComplete.subscribe( function() { tvanim.onComplete(); } );
2213 * Clean up and invoke callback
2214 * @method onComplete
2216 onComplete: function() {
2217 var s = this.el.style;
2220 s.filter = "alpha(opacity=100)";
2227 * @return {string} the string representation of the instance
2229 toString: function() {