2 Copyright (c) 2006, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
7 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
8 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
9 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
10 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
12 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
13 TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
14 ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18 * The treeview widget is a generic tree building tool.
20 * @title TreeView Widget
23 * @namespace YAHOO.widget
27 * Contains the tree view state data and the root node.
31 * @param {string|HTMLElement} id The id of the element, or the element
32 * itself that the tree will be inserted into.
34 YAHOO.widget.TreeView = function(id) {
35 if (id) { this.init(id); }
38 YAHOO.widget.TreeView.prototype = {
41 * The id of tree container element
48 * The host element for this tree
55 * Flat collection of all nodes in this tree
63 * We lock the tree control while waiting for the dynamic loader to return
70 * The animation to use for expanding children, if any
71 * @property _expandAnim
78 * The animation to use for collapsing children, if any
79 * @property _collapseAnim
86 * The current number of animations that are executing
87 * @property _animCount
94 * The maximum number of animations to run at one time.
101 * Sets up the animation for expanding children
102 * @method setExpandAnim
103 * @param {string} type the type of animation (acceptable values defined
104 * in YAHOO.widget.TVAnim)
106 setExpandAnim: function(type) {
107 if (YAHOO.widget.TVAnim.isValid(type)) {
108 this._expandAnim = type;
113 * Sets up the animation for collapsing children
114 * @method setCollapseAnim
115 * @param {string} the type of animation (acceptable values defined in
116 * YAHOO.widget.TVAnim)
118 setCollapseAnim: function(type) {
119 if (YAHOO.widget.TVAnim.isValid(type)) {
120 this._collapseAnim = type;
125 * Perform the expand animation if configured, or just show the
126 * element if not configured or too many animations are in progress
127 * @method animateExpand
128 * @param el {HTMLElement} the element to animate
129 * @param node {YAHOO.util.Node} the node that was expanded
130 * @return {boolean} true if animation could be invoked, false otherwise
132 animateExpand: function(el, node) {
134 if (this._expandAnim && this._animCount < this.maxAnim) {
135 // this.locked = true;
137 var a = YAHOO.widget.TVAnim.getAnim(this._expandAnim, el,
138 function() { tree.expandComplete(node); });
141 this.fireEvent("animStart", {
155 * Perform the collapse animation if configured, or just show the
156 * element if not configured or too many animations are in progress
157 * @method animateCollapse
158 * @param el {HTMLElement} the element to animate
159 * @param node {YAHOO.util.Node} the node that was expanded
160 * @return {boolean} true if animation could be invoked, false otherwise
162 animateCollapse: function(el, node) {
164 if (this._collapseAnim && this._animCount < this.maxAnim) {
165 // this.locked = true;
167 var a = YAHOO.widget.TVAnim.getAnim(this._collapseAnim, el,
168 function() { tree.collapseComplete(node); });
171 this.fireEvent("animStart", {
185 * Function executed when the expand animation completes
186 * @method expandComplete
188 expandComplete: function(node) {
190 this.fireEvent("animComplete", {
194 // this.locked = false;
198 * Function executed when the collapse animation completes
199 * @method collapseComplete
201 collapseComplete: function(node) {
203 this.fireEvent("animComplete", {
207 // this.locked = false;
211 * Initializes the tree
213 * @parm {string|HTMLElement} id the id of the element that will hold the tree
220 if ("string" !== typeof id) {
222 this.id = this.generateId(id);
226 * When animation is enabled, this event fires when the animation
230 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
231 * @parm {String} type the type of animation ("expand" or "collapse")
233 this.createEvent("animStart", this);
236 * When animation is enabled, this event fires when the animation
238 * @event animComplete
240 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
241 * @parm {String} type the type of animation ("expand" or "collapse")
243 this.createEvent("animComplete", this);
246 * Fires when a node is going to be expanded. Return false to stop
250 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
252 this.createEvent("collapse", this);
255 * Fires when a node is going to be collapsed. Return false to stop
259 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
261 this.createEvent("expand", this);
265 // store a global reference
266 YAHOO.widget.TreeView.trees[this.id] = this;
268 // Set up the root node
269 this.root = new YAHOO.widget.RootNode(this);
276 * Returns the tree's host element
278 * @return {HTMLElement} the host element
282 this._el = document.getElementById(this.id);
288 * Renders the tree boilerplate and visible nodes
292 var html = this.root.getHtml();
293 this.getEl().innerHTML = html;
294 // document.getElementById('frameFolders').innerHTML = html;
295 this.firstDraw = false;
300 * Nodes register themselves with the tree instance when they are created.
302 * @param node {Node} the node to register
305 regNode: function(node) {
306 this._nodes[node.index] = node;
310 * Returns the root node of this tree
312 * @return {Node} the root node
314 getRoot: function() {
319 * Configures this tree to dynamically load all child data
320 * @method setDynamicLoad
321 * @param {function} fnDataLoader the function that will be called to get the data
322 * @param iconMode {int} configures the icon that is displayed when a dynamic
323 * load node is expanded the first time without children. By default, the
324 * "collapse" icon will be used. If set to 1, the leaf node icon will be
327 setDynamicLoad: function(fnDataLoader, iconMode) {
328 this.root.setDynamicLoad(fnDataLoader, iconMode);
332 * Expands all child nodes. Note: this conflicts with the "multiExpand"
333 * node property. If expand all is called in a tree with nodes that
334 * do not allow multiple siblings to be displayed, only the last sibling
338 expandAll: function() {
340 this.root.expandAll();
345 * Collapses all expanded child nodes in the entire tree.
346 * @method collapseAll
348 collapseAll: function() {
350 this.root.collapseAll();
355 * Returns a node in the tree that has the specified index (this index
356 * is created internally, so this function probably will only be used
357 * in html generated for a given node.)
358 * @method getNodeByIndex
359 * @param {int} nodeIndex the index of the node wanted
360 * @return {Node} the node with index=nodeIndex, null if no match
362 getNodeByIndex: function(nodeIndex) {
363 var n = this._nodes[nodeIndex];
364 return (n) ? n : null;
368 * Returns a node that has a matching property and value in the data
369 * object that was passed into its constructor.
370 * @method getNodeByProperty
371 * @param {object} property the property to search (usually a string)
372 * @param {object} value the value we want to find (usuall an int or string)
373 * @return {Node} the matching node, null if no match
375 getNodeByProperty: function(property, value) {
376 for (var i in this._nodes) {
377 var n = this._nodes[i];
378 if (n.data && value == n.data[property]) {
387 * Returns a collection of nodes that have a matching property
388 * and value in the data object that was passed into its constructor.
389 * @method getNodesByProperty
390 * @param {object} property the property to search (usually a string)
391 * @param {object} value the value we want to find (usuall an int or string)
392 * @return {Array} the matching collection of nodes, null if no match
394 getNodesByProperty: function(property, value) {
396 for (var i in this._nodes) {
397 var n = this._nodes[i];
398 if (n.data && value == n.data[property]) {
403 return (values.length) ? values : null;
407 * Removes the node and its children, and optionally refreshes the
408 * branch of the tree that was affected.
410 * @param {Node} The node to remove
411 * @param {boolean} autoRefresh automatically refreshes branch if true
412 * @return {boolean} False is there was a problem, true otherwise.
414 removeNode: function(node, autoRefresh) {
416 // Don't delete the root node
421 // Get the branch that we may need to refresh
427 // Delete the node and its children
428 this._deleteNode(node);
430 // Refresh the parent of the parent
431 if (autoRefresh && p && p.childrenRendered) {
439 * Deletes this nodes child collection, recursively. Also collapses
440 * the node, and resets the dynamic load flag. The primary use for
441 * this method is to purge a node and allow it to fetch its data
443 * @method removeChildren
444 * @param {Node} node the node to purge
446 removeChildren: function(node) {
447 while (node.children.length) {
448 this._deleteNode(node.children[0]);
451 node.childrenRendered = false;
452 node.dynamicLoadComplete = false;
461 * Deletes the node and recurses children
462 * @method _deleteNode
465 _deleteNode: function(node) {
466 // Remove all the child nodes first
467 this.removeChildren(node);
469 // Remove the node from the tree
474 * Removes the node from the tree, preserving the child collection
475 * to make it possible to insert the branch into another part of the
476 * tree, or another tree.
478 * @param {Node} the node to remove
480 popNode: function(node) {
483 // Update the parent's collection of children
486 for (var i=0, len=p.children.length;i<len;++i) {
487 if (p.children[i] != node) {
488 a[a.length] = p.children[i];
494 // reset the childrenRendered flag for the parent
495 p.childrenRendered = false;
497 // Update the sibling relationship
498 if (node.previousSibling) {
499 node.previousSibling.nextSibling = node.nextSibling;
502 if (node.nextSibling) {
503 node.nextSibling.previousSibling = node.previousSibling;
507 node.previousSibling = null;
508 node.nextSibling = null;
511 // Update the tree's node collection
512 delete this._nodes[node.index];
516 * TreeView instance toString
518 * @return {string} string representation of the tree
520 toString: function() {
521 return "TreeView " + this.id;
525 * Generates an unique id for an element if it doesn't yet have one
529 generateId: function(el) {
533 id = "yui-tv-auto-id-" + YAHOO.widget.TreeView.counter;
534 ++YAHOO.widget.TreeView.counter;
541 * Abstract method that is executed when a node is expanded
543 * @param node {Node} the node that was expanded
544 * @deprecated use treeobj.subscribe("expand") instead
546 onExpand: function(node) { },
549 * Abstract method that is executed when a node is collapsed.
551 * @param node {Node} the node that was collapsed.
552 * @deprecated use treeobj.subscribe("collapse") instead
554 onCollapse: function(node) { }
558 YAHOO.augment(YAHOO.widget.TreeView, YAHOO.util.EventProvider);
561 * Count of all nodes in all trees
562 * @property YAHOO.widget.TreeView.nodeCount
566 YAHOO.widget.TreeView.nodeCount = 0;
569 * Global cache of tree instances
570 * @property YAHOO.widget.TreeView.trees
575 YAHOO.widget.TreeView.trees = [];
578 * Counter for generating a new unique element id
579 * @property YAHOO.widget.TreeView.counter
583 YAHOO.widget.TreeView.counter = 0;
586 * Global method for getting a tree by its id. Used in the generated
588 * @method YAHOO.widget.TreeView.getTree
589 * @param treeId {String} the id of the tree instance
590 * @return {TreeView} the tree instance requested, null if not found.
593 YAHOO.widget.TreeView.getTree = function(treeId) {
594 var t = YAHOO.widget.TreeView.trees[treeId];
595 return (t) ? t : null;
599 * Global method for getting a node by its id. Used in the generated
601 * @method YAHOO.widget.TreeView.getNode
602 * @param treeId {String} the id of the tree instance
603 * @param nodeIndex {String} the index of the node to return
604 * @return {Node} the node instance requested, null if not found
607 YAHOO.widget.TreeView.getNode = function(treeId, nodeIndex) {
608 var t = YAHOO.widget.TreeView.getTree(treeId);
609 return (t) ? t.getNodeByIndex(nodeIndex) : null;
614 * @method YAHOO.widget.TreeView.addHandler
615 * @param el the elment to bind the handler to
616 * @param {string} sType the type of event handler
617 * @param {function} fn the callback to invoke
620 YAHOO.widget.TreeView.addHandler = function (el, sType, fn) {
621 if (el.addEventListener) {
622 el.addEventListener(sType, fn, false);
623 } else if (el.attachEvent) {
624 el.attachEvent("on" + sType, fn);
630 * @method YAHOO.widget.TreeView.removeHandler
631 * @param el the elment to bind the handler to
632 * @param {string} sType the type of event handler
633 * @param {function} fn the callback to invoke
637 YAHOO.widget.TreeView.removeHandler = function (el, sType, fn) {
638 if (el.removeEventListener) {
639 el.removeEventListener(sType, fn, false);
640 } else if (el.detachEvent) {
641 el.detachEvent("on" + sType, fn);
646 * Attempts to preload the images defined in the styles used to draw the tree by
647 * rendering off-screen elements that use the styles.
648 * @method YAHOO.widget.TreeView.preload
649 * @param {string} prefix the prefix to use to generate the names of the
650 * images to preload, default is ygtv
653 YAHOO.widget.TreeView.preload = function(prefix) {
654 prefix = prefix || "ygtv";
655 var styles = ["tn","tm","tmh","tp","tph","ln","lm","lmh","lp","lph","loading"];
659 for (var i = 0; i < styles.length; ++i) {
660 sb[sb.length] = '<span class="' + prefix + styles[i] + '"> </span>';
663 var f = document.createElement("div");
665 s.position = "absolute";
668 f.innerHTML = sb.join("");
670 document.body.appendChild(f);
672 YAHOO.widget.TreeView.removeHandler(window,
673 "load", YAHOO.widget.TreeView.preload);
677 YAHOO.widget.TreeView.addHandler(window,
678 "load", YAHOO.widget.TreeView.preload);
681 * The base class for all tree nodes. The node's presentation and behavior in
682 * response to mouse events is handled in Node subclasses.
683 * @namespace YAHOO.widget
685 * @param oData {object} a string or object containing the data that will
686 * be used to render this node
687 * @param oParent {Node} this node's parent node
688 * @param expanded {boolean} the initial expanded/collapsed state
691 YAHOO.widget.Node = function(oData, oParent, expanded) {
692 if (oData) { this.init(oData, oParent, expanded); }
695 YAHOO.widget.Node.prototype = {
698 * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
705 * This node's child node collection.
712 * Tree instance this node is part of
719 * The data linked to this node. This can be any object or primitive
720 * value, and the data can be used in getNodeHtml().
734 * The depth of this node. We start at -1 for the root node.
741 * The href for the node's label. If one is not specified, the href will
742 * be set so that it toggles the node.
749 * The label href target, defaults to current window
756 * The node's expanded/collapsed state
763 * Can multiple children be expanded at once?
764 * @property multiExpand
770 * Should we render children for a collapsed node? It is possible that the
771 * implementer will want to render the hidden data... @todo verify that we
772 * need this, and implement it if we do.
773 * @property renderHidden
779 * This flag is set to true when the html is generated for this node's
780 * children, and set to false when new children are added.
781 * @property childrenRendered
784 childrenRendered: false,
787 * Dynamically loaded nodes only fetch the data the first time they are
788 * expanded. This flag is set to true once the data has been fetched.
789 * @property dynamicLoadComplete
792 dynamicLoadComplete: false,
795 * This node's previous sibling
796 * @property previousSibling
799 previousSibling: null,
802 * This node's next sibling
803 * @property nextSibling
809 * We can set the node up to call an external method to get the child
818 * Function to execute when we need to get this node's child data.
819 * @property dataLoader
825 * This is true for dynamically loading nodes while waiting for the
826 * callback to return.
827 * @property isLoading
833 * The toggle/branch icon will not show if this is set to false. This
834 * could be useful if the implementer wants to have the child contain
835 * extra info about the parent, rather than an actual node.
842 * Used to configure what happens when a dynamic load node is expanded
843 * and we discover that it does not have children. By default, it is
844 * treated as if it still could have children (plus/minus icon). Set
845 * iconMode to have it display like a leaf node instead.
859 spacerPath: "http://us.i1.yimg.com/us.yimg.com/i/space.gif",
860 expandedText: "Expanded",
861 collapsedText: "Collapsed",
862 loadingText: "Loading",
866 * Initializes this node, gets some of the properties from the parent
868 * @param oData {object} a string or object containing the data that will
869 * be used to render this node
870 * @param oParent {Node} this node's parent node
871 * @param expanded {boolean} the initial expanded/collapsed state
873 init: function(oData, oParent, expanded) {
877 this.index = YAHOO.widget.TreeView.nodeCount;
878 ++YAHOO.widget.TreeView.nodeCount;
879 this.expanded = expanded;
882 * The parentChange event is fired when a parent element is applied
883 * to the node. This is useful if you need to apply tree-level
884 * properties to a tree that need to happen if a node is moved from
885 * one tre to another.
887 * @event parentChange
890 this.createEvent("parentChange", this);
892 // oParent should never be null except when we create the root node.
894 oParent.appendChild(this);
899 * Certain properties for the node cannot be set until the parent
900 * is known. This is called after the node is inserted into a tree.
901 * the parent is also applied to this node's children in order to
902 * make it possible to move a branch from one tree to another.
903 * @method applyParent
904 * @param {Node} parentNode this node's parent node
905 * @return {boolean} true if the application was successful
907 applyParent: function(parentNode) {
912 this.tree = parentNode.tree;
913 this.parent = parentNode;
914 this.depth = parentNode.depth + 1;
917 this.href = "javascript:" + this.getToggleLink();
920 if (! this.multiExpand) {
921 this.multiExpand = parentNode.multiExpand;
924 this.tree.regNode(this);
925 parentNode.childrenRendered = false;
927 // cascade update existing children
928 for (var i=0, len=this.children.length;i<len;++i) {
929 this.children[i].applyParent(this);
932 this.fireEvent("parentChange");
938 * Appends a node to the child collection.
939 * @method appendChild
940 * @param childNode {Node} the new node
941 * @return {Node} the child node
944 appendChild: function(childNode) {
945 if (this.hasChildren()) {
946 var sib = this.children[this.children.length - 1];
947 sib.nextSibling = childNode;
948 childNode.previousSibling = sib;
950 this.children[this.children.length] = childNode;
951 childNode.applyParent(this);
957 * Appends this node to the supplied node's child collection
959 * @param parentNode {Node} the node to append to.
960 * @return {Node} The appended node
962 appendTo: function(parentNode) {
963 return parentNode.appendChild(this);
967 * Inserts this node before this supplied node
968 * @method insertBefore
969 * @param node {Node} the node to insert this node before
970 * @return {Node} the inserted node
972 insertBefore: function(node) {
977 this.tree.popNode(this);
980 var refIndex = node.isChildOf(p);
981 p.children.splice(refIndex, 0, this);
982 if (node.previousSibling) {
983 node.previousSibling.nextSibling = this;
985 this.previousSibling = node.previousSibling;
986 this.nextSibling = node;
987 node.previousSibling = this;
996 * Inserts this node after the supplied node
997 * @method insertAfter
998 * @param node {Node} the node to insert after
999 * @return {Node} the inserted node
1001 insertAfter: function(node) {
1002 var p = node.parent;
1006 this.tree.popNode(this);
1009 var refIndex = node.isChildOf(p);
1011 if (!node.nextSibling) {
1012 return this.appendTo(p);
1015 p.children.splice(refIndex + 1, 0, this);
1017 node.nextSibling.previousSibling = this;
1018 this.previousSibling = node;
1019 this.nextSibling = node.nextSibling;
1020 node.nextSibling = this;
1022 this.applyParent(p);
1029 * Returns true if the Node is a child of supplied Node
1031 * @param parentNode {Node} the Node to check
1032 * @return {boolean} The node index if this Node is a child of
1033 * supplied Node, else -1.
1036 isChildOf: function(parentNode) {
1037 if (parentNode && parentNode.children) {
1038 for (var i=0, len=parentNode.children.length; i<len ; ++i) {
1039 if (parentNode.children[i] === this) {
1049 * Returns a node array of this node's siblings, null if none.
1050 * @method getSiblings
1053 getSiblings: function() {
1054 return this.parent.children;
1058 * Shows this node's children
1059 * @method showChildren
1061 showChildren: function() {
1062 if (!this.tree.animateExpand(this.getChildrenEl(), this)) {
1063 if (this.hasChildren()) {
1064 this.getChildrenEl().style.display = "";
1070 * Hides this node's children
1071 * @method hideChildren
1073 hideChildren: function() {
1075 if (!this.tree.animateCollapse(this.getChildrenEl(), this)) {
1076 this.getChildrenEl().style.display = "none";
1081 * Returns the id for this node's container div
1083 * @return {string} the element id
1085 getElId: function() {
1086 return "ygtv" + this.index;
1090 * Returns the id for this node's children div
1091 * @method getChildrenElId
1092 * @return {string} the element id for this node's children div
1094 getChildrenElId: function() {
1095 return "ygtvc" + this.index;
1099 * Returns the id for this node's toggle element
1100 * @method getToggleElId
1101 * @return {string} the toggel element id
1103 getToggleElId: function() {
1104 return "ygtvt" + this.index;
1108 * Returns the id for this node's spacer image. The spacer is positioned
1109 * over the toggle and provides feedback for screen readers.
1110 * @method getSpacerId
1111 * @return {string} the id for the spacer image
1114 getSpacerId: function() {
1115 return "ygtvspacer" + this.index;
1120 * Returns this node's container html element
1122 * @return {HTMLElement} the container html element
1125 return document.getElementById(this.getElId());
1129 * Returns the div that was generated for this node's children
1130 * @method getChildrenEl
1131 * @return {HTMLElement} this node's children div
1133 getChildrenEl: function() {
1134 return document.getElementById(this.getChildrenElId());
1138 * Returns the element that is being used for this node's toggle.
1139 * @method getToggleEl
1140 * @return {HTMLElement} this node's toggle html element
1142 getToggleEl: function() {
1143 return document.getElementById(this.getToggleElId());
1147 * Returns the element that is being used for this node's spacer.
1149 * @return {HTMLElement} this node's spacer html element
1152 getSpacer: function() {
1153 return document.getElementById( this.getSpacerId() ) || {};
1158 getStateText: function() {
1159 if (this.isLoading) {
1160 return this.loadingText;
1161 } else if (this.hasChildren(true)) {
1162 if (this.expanded) {
1163 return this.expandedText;
1165 return this.collapsedText;
1174 * Generates the link that will invoke this node's toggle method
1175 * @method getToggleLink
1176 * @return {string} the javascript url for toggling this node
1178 getToggleLink: function() {
1179 return "YAHOO.widget.TreeView.getNode(\'" + this.tree.id + "\'," +
1180 this.index + ").toggle()";
1184 * Hides this nodes children (creating them if necessary), changes the
1188 collapse: function() {
1189 // Only collapse if currently expanded
1190 if (!this.expanded) { return; }
1192 // fire the collapse event handler
1193 var ret = this.tree.onCollapse(this);
1195 if (false === ret) {
1199 ret = this.tree.fireEvent("collapse", this);
1201 if (false === ret) {
1205 if (!this.getEl()) {
1206 this.expanded = false;
1210 // hide the child div
1211 this.hideChildren();
1212 this.expanded = false;
1216 // this.getSpacer().title = this.getStateText();
1221 * Shows this nodes children (creating them if necessary), changes the
1222 * toggle style, and collapses its siblings if multiExpand is not set.
1225 expand: function() {
1226 // Only expand if currently collapsed.
1227 if (this.expanded) { return; }
1229 // fire the expand event handler
1230 var ret = this.tree.onExpand(this);
1232 if (false === ret) {
1236 ret = this.tree.fireEvent("expand", this);
1238 if (false === ret) {
1242 if (!this.getEl()) {
1243 this.expanded = true;
1247 if (! this.childrenRendered) {
1248 this.getChildrenEl().innerHTML = this.renderChildren();
1252 this.expanded = true;
1256 // this.getSpacer().title = this.getStateText();
1258 // We do an extra check for children here because the lazy
1259 // load feature can expose nodes that have no children.
1261 // if (!this.hasChildren()) {
1262 if (this.isLoading) {
1263 this.expanded = false;
1267 if (! this.multiExpand) {
1268 var sibs = this.getSiblings();
1269 for (var i=0; i<sibs.length; ++i) {
1270 if (sibs[i] != this && sibs[i].expanded) {
1276 this.showChildren();
1279 updateIcon: function() {
1281 var el = this.getToggleEl();
1283 el.className = this.getStyle();
1289 * Returns the css style name for the toggle
1291 * @return {string} the css class for this node's toggle
1293 getStyle: function() {
1294 if (this.isLoading) {
1295 return "ygtvloading";
1297 // location top or bottom, middle nodes also get the top style
1298 var loc = (this.nextSibling) ? "t" : "l";
1300 // type p=plus(expand), m=minus(collapase), n=none(no children)
1302 if (this.hasChildren(true) || (this.isDynamic() && !this.getIconMode())) {
1303 // if (this.hasChildren(true)) {
1304 type = (this.expanded) ? "m" : "p";
1307 return "ygtv" + loc + type;
1312 * Returns the hover style for the icon
1313 * @return {string} the css class hover state
1314 * @method getHoverStyle
1316 getHoverStyle: function() {
1317 var s = this.getStyle();
1318 if (this.hasChildren(true) && !this.isLoading) {
1325 * Recursively expands all of this node's children.
1328 expandAll: function() {
1329 for (var i=0;i<this.children.length;++i) {
1330 var c = this.children[i];
1331 if (c.isDynamic()) {
1332 alert("Not supported (lazy load + expand all)");
1334 } else if (! c.multiExpand) {
1335 alert("Not supported (no multi-expand + expand all)");
1345 * Recursively collapses all of this node's children.
1346 * @method collapseAll
1348 collapseAll: function() {
1349 for (var i=0;i<this.children.length;++i) {
1350 this.children[i].collapse();
1351 this.children[i].collapseAll();
1356 * Configures this node for dynamically obtaining the child data
1357 * when the node is first expanded. Calling it without the callback
1358 * will turn off dynamic load for the node.
1359 * @method setDynamicLoad
1360 * @param fmDataLoader {function} the function that will be used to get the data.
1361 * @param iconMode {int} configures the icon that is displayed when a dynamic
1362 * load node is expanded the first time without children. By default, the
1363 * "collapse" icon will be used. If set to 1, the leaf node icon will be
1366 setDynamicLoad: function(fnDataLoader, iconMode) {
1368 this.dataLoader = fnDataLoader;
1369 this._dynLoad = true;
1371 this.dataLoader = null;
1372 this._dynLoad = false;
1376 this.iconMode = iconMode;
1381 * Evaluates if this node is the root node of the tree
1383 * @return {boolean} true if this is the root node
1385 isRoot: function() {
1386 return (this == this.tree.root);
1390 * Evaluates if this node's children should be loaded dynamically. Looks for
1391 * the property both in this instance and the root node. If the tree is
1392 * defined to load all children dynamically, the data callback function is
1393 * defined in the root node
1395 * @return {boolean} true if this node's children are to be loaded dynamically
1397 isDynamic: function() {
1398 var lazy = (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
1403 * Returns the current icon mode. This refers to the way childless dynamic
1404 * load nodes appear.
1405 * @method getIconMode
1406 * @return {int} 0 for collapse style, 1 for leaf node style
1408 getIconMode: function() {
1409 return (this.iconMode || this.tree.root.iconMode);
1413 * Checks if this node has children. If this node is lazy-loading and the
1414 * children have not been rendered, we do not know whether or not there
1415 * are actual children. In most cases, we need to assume that there are
1416 * children (for instance, the toggle needs to show the expandable
1417 * presentation state). In other times we want to know if there are rendered
1418 * children. For the latter, "checkForLazyLoad" should be false.
1419 * @method hasChildren
1420 * @param checkForLazyLoad {boolean} should we check for unloaded children?
1421 * @return {boolean} true if this has children or if it might and we are
1422 * checking for this condition.
1424 hasChildren: function(checkForLazyLoad) {
1425 return ( this.children.length > 0 ||
1426 (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) );
1430 * Expands if node is collapsed, collapses otherwise.
1433 toggle: function() {
1434 if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
1435 if (this.expanded) { this.collapse(); } else { this.expand(); }
1440 * Returns the markup for this node and its children.
1442 * @return {string} the markup for this node and its expanded children.
1444 getHtml: function() {
1446 this.childrenRendered = false;
1449 sb[sb.length] = '<div class="ygtvitem" id="' + this.getElId() + '">';
1450 sb[sb.length] = this.getNodeHtml();
1451 sb[sb.length] = this.getChildrenHtml();
1452 sb[sb.length] = '</div>';
1457 * Called when first rendering the tree. We always build the div that will
1458 * contain this nodes children, but we don't render the children themselves
1459 * unless this node is expanded.
1460 * @method getChildrenHtml
1461 * @return {string} the children container div html and any expanded children
1464 getChildrenHtml: function() {
1467 sb[sb.length] = '<div class="ygtvchildren"';
1468 sb[sb.length] = ' id="' + this.getChildrenElId() + '"';
1469 if (!this.expanded) {
1470 sb[sb.length] = ' style="display:none;"';
1472 sb[sb.length] = '>';
1474 // Don't render the actual child node HTML unless this node is expanded.
1475 if ( (this.hasChildren(true) && this.expanded) ||
1476 (this.renderHidden && !this.isDynamic()) ) {
1477 sb[sb.length] = this.renderChildren();
1480 sb[sb.length] = '</div>';
1486 * Generates the markup for the child nodes. This is not done until the node
1488 * @method renderChildren
1489 * @return {string} the html for this node's children
1492 renderChildren: function() {
1497 if (this.isDynamic() && !this.dynamicLoadComplete) {
1498 this.isLoading = true;
1499 this.tree.locked = true;
1501 if (this.dataLoader) {
1505 node.dataLoader(node,
1507 node.loadComplete();
1511 } else if (this.tree.root.dataLoader) {
1515 node.tree.root.dataLoader(node,
1517 node.loadComplete();
1522 return "Error: data loader not found or not specified.";
1528 return this.completeRender();
1533 * Called when we know we have all the child data.
1534 * @method completeRender
1535 * @return {string} children html
1537 completeRender: function() {
1540 for (var i=0; i < this.children.length; ++i) {
1541 // this.children[i].childrenRendered = false;
1542 sb[sb.length] = this.children[i].getHtml();
1545 this.childrenRendered = true;
1551 * Load complete is the callback function we pass to the data provider
1552 * in dynamic load situations.
1553 * @method loadComplete
1555 loadComplete: function() {
1556 this.getChildrenEl().innerHTML = this.completeRender();
1557 this.dynamicLoadComplete = true;
1558 this.isLoading = false;
1560 this.tree.locked = false;
1564 * Returns this node's ancestor at the specified depth.
1565 * @method getAncestor
1566 * @param {int} depth the depth of the ancestor.
1567 * @return {Node} the ancestor
1569 getAncestor: function(depth) {
1570 if (depth >= this.depth || depth < 0) {
1574 var p = this.parent;
1576 while (p.depth > depth) {
1584 * Returns the css class for the spacer at the specified depth for
1585 * this node. If this node's ancestor at the specified depth
1586 * has a next sibling the presentation is different than if it
1587 * does not have a next sibling
1588 * @method getDepthStyle
1589 * @param {int} depth the depth of the ancestor.
1590 * @return {string} the css class for the spacer
1592 getDepthStyle: function(depth) {
1593 return (this.getAncestor(depth).nextSibling) ?
1594 "ygtvdepthcell" : "ygtvblankdepthcell";
1598 * Get the markup for the node. This is designed to be overrided so that we can
1599 * support different types of nodes.
1600 * @method getNodeHtml
1601 * @return {string} The HTML that will render this node.
1603 getNodeHtml: function() {
1608 * Regenerates the html for this node and its children. To be used when the
1609 * node is expanded and new children have been added.
1612 refresh: function() {
1613 // this.loadComplete();
1614 this.getChildrenEl().innerHTML = this.completeRender();
1617 var el = this.getToggleEl();
1619 el.className = this.getStyle();
1627 * @return {string} string representation of the node
1629 toString: function() {
1630 return "Node (" + this.index + ")";
1635 YAHOO.augment(YAHOO.widget.Node, YAHOO.util.EventProvider);
1638 * A custom YAHOO.widget.Node that handles the unique nature of
1639 * the virtual, presentationless root node.
1640 * @namespace YAHOO.widget
1642 * @extends YAHOO.widget.Node
1643 * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to
1646 YAHOO.widget.RootNode = function(oTree) {
1647 // Initialize the node with null params. The root node is a
1648 // special case where the node has no presentation. So we have
1649 // to alter the standard properties a bit.
1650 this.init(null, null, true);
1653 * For the root node, we get the tree reference from as a param
1654 * to the constructor instead of from the parent element.
1659 YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, {
1661 // overrides YAHOO.widget.Node
1662 getNodeHtml: function() {
1666 toString: function() {
1670 loadComplete: function() {
1676 * The default node presentation. The first parameter should be
1677 * either a string that will be used as the node's label, or an object
1678 * that has a string propery called label. By default, the clicking the
1679 * label will toggle the expanded/collapsed state of the node. By
1680 * changing the href property of the instance, this behavior can be
1681 * changed so that the label will go to the specified href.
1682 * @namespace YAHOO.widget
1684 * @extends YAHOO.widget.Node
1686 * @param oData {object} a string or object containing the data that will
1687 * be used to render this node
1688 * @param oParent {YAHOO.widget.Node} this node's parent node
1689 * @param expanded {boolean} the initial expanded/collapsed state
1691 YAHOO.widget.TextNode = function(oData, oParent, expanded) {
1694 this.init(oData, oParent, expanded);
1695 this.setUpLabel(oData);
1700 YAHOO.extend(YAHOO.widget.TextNode, YAHOO.widget.Node, {
1703 * The CSS class for the label href. Defaults to ygtvlabel, but can be
1704 * overridden to provide a custom presentation for a specific node.
1705 * @property labelStyle
1708 labelStyle: "ygtvlabel",
1711 * The derived element id of the label for this node
1712 * @property labelElId
1718 * The text for the label. It is assumed that the oData parameter will
1719 * either be a string that will be used as the label, or an object that
1720 * has a property called "label" that we will use.
1726 textNodeParentChange: function() {
1729 * Custom event that is fired when the text node label is clicked. The
1730 * custom event is defined on the tree instance, so there is a single
1731 * event that handles all nodes in the tree. The node clicked is
1732 * provided as an argument
1735 * @for YAHOO.widget.TreeView
1736 * @param {YAHOO.widget.Node} node the node clicked
1738 if (this.tree && !this.tree.hasEvent("labelClick")) {
1739 this.tree.createEvent("labelClick", this.tree);
1745 * Sets up the node label
1746 * @method setUpLabel
1747 * @param oData string containing the label, or an object with a label property
1749 setUpLabel: function(oData) {
1751 // set up the custom event on the tree
1752 this.textNodeParentChange();
1753 this.subscribe("parentChange", this.textNodeParentChange);
1755 if (typeof oData == "string") {
1756 oData = { label: oData };
1758 this.label = oData.label;
1762 this.href = oData.href;
1767 this.target = oData.target;
1771 this.labelStyle = oData.style;
1774 this.labelElId = "ygtvlabelel" + this.index;
1778 * Returns the label element
1779 * @for YAHOO.widget.TextNode
1780 * @method getLabelEl
1781 * @return {object} the element
1783 getLabelEl: function() {
1784 return document.getElementById(this.labelElId);
1787 // overrides YAHOO.widget.Node
1788 getNodeHtml: function() {
1791 sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
1792 sb[sb.length] = '<tr>';
1794 for (var i=0;i<this.depth;++i) {
1795 // sb[sb.length] = '<td class="ygtvdepthcell"> </td>';
1796 sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '"> </td>';
1799 var getNode = 'YAHOO.widget.TreeView.getNode(\'' +
1800 this.tree.id + '\',' + this.index + ')';
1802 sb[sb.length] = '<td';
1803 // sb[sb.length] = ' onselectstart="return false"';
1804 sb[sb.length] = ' id="' + this.getToggleElId() + '"';
1805 sb[sb.length] = ' class="' + this.getStyle() + '"';
1806 if (this.hasChildren(true)) {
1807 sb[sb.length] = ' onmouseover="this.className=';
1808 sb[sb.length] = getNode + '.getHoverStyle()"';
1809 sb[sb.length] = ' onmouseout="this.className=';
1810 sb[sb.length] = getNode + '.getStyle()"';
1812 sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '">';
1815 sb[sb.length] = '<img id="' + this.getSpacerId() + '"';
1816 sb[sb.length] = ' alt=""';
1817 sb[sb.length] = ' tabindex=0';
1818 sb[sb.length] = ' src="' + this.spacerPath + '"';
1819 sb[sb.length] = ' title="' + this.getStateText() + '"';
1820 sb[sb.length] = ' class="ygtvspacer"';
1821 // sb[sb.length] = ' onkeypress="return ' + getNode + '".onKeyPress()"';
1822 sb[sb.length] = ' />';
1825 sb[sb.length] = ' ';
1827 sb[sb.length] = '</td>';
1828 sb[sb.length] = '<td>';
1829 sb[sb.length] = '<a';
1830 sb[sb.length] = ' id="' + this.labelElId + '"';
1831 sb[sb.length] = ' class="' + this.labelStyle + '"';
1832 //sb[sb.length] = ' href="' + this.href + '"';
1833 sb[sb.length] = ' href="javascript:set_selected_node(\''+this.tree.id + '\',\''+this.index+'\');'+ this.href +'"';
1834 sb[sb.length] = ' target="' + this.target + '"';
1835 sb[sb.length] = ' onclick="return ' + getNode + '.onLabelClick(' + getNode +')"';
1836 if (this.hasChildren(true)) {
1837 sb[sb.length] = ' onmouseover="document.getElementById(\'';
1838 sb[sb.length] = this.getToggleElId() + '\').className=';
1839 sb[sb.length] = getNode + '.getHoverStyle()"';
1840 sb[sb.length] = ' onmouseout="document.getElementById(\'';
1841 sb[sb.length] = this.getToggleElId() + '\').className=';
1842 sb[sb.length] = getNode + '.getStyle()"';
1844 sb[sb.length] = ' >';
1845 sb[sb.length] = this.label;
1846 sb[sb.length] = '</a>';
1847 sb[sb.length] = '</td>';
1848 sb[sb.length] = '</tr>';
1849 sb[sb.length] = '</table>';
1855 * Executed when the label is clicked. Fires the labelClick custom event.
1856 * @method onLabelClick
1857 * @param me {Node} this node
1858 * @scope the anchor tag clicked
1859 * @return false to cancel the anchor click
1861 onLabelClick: function(me) {
1862 return me.tree.fireEvent("labelClick", me);
1866 toString: function() {
1867 return "TextNode (" + this.index + ") " + this.label;
1872 * A menu-specific implementation that differs from TextNode in that only
1873 * one sibling can be expanded at a time.
1874 * @namespace YAHOO.widget
1876 * @extends YAHOO.widget.TextNode
1877 * @param oData {object} a string or object containing the data that will
1878 * be used to render this node
1879 * @param oParent {YAHOO.widget.Node} this node's parent node
1880 * @param expanded {boolean} the initial expanded/collapsed state
1883 YAHOO.widget.MenuNode = function(oData, oParent, expanded) {
1885 this.init(oData, oParent, expanded);
1886 this.setUpLabel(oData);
1890 * Menus usually allow only one branch to be open at a time.
1892 this.multiExpand = false;
1897 YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, {
1899 toString: function() {
1900 return "MenuNode (" + this.index + ") " + this.label;
1905 * This implementation takes either a string or object for the
1906 * oData argument. If is it a string, we will use it for the display
1907 * of this node (and it can contain any html code). If the parameter
1908 * is an object, we look for a parameter called "html" that will be
1909 * used for this node's display.
1910 * @namespace YAHOO.widget
1912 * @extends YAHOO.widget.Node
1914 * @param oData {object} a string or object containing the data that will
1915 * be used to render this node
1916 * @param oParent {YAHOO.widget.Node} this node's parent node
1917 * @param expanded {boolean} the initial expanded/collapsed state
1918 * @param hasIcon {boolean} specifies whether or not leaf nodes should
1921 YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) {
1923 this.init(oData, oParent, expanded);
1924 this.initContent(oData, hasIcon);
1928 YAHOO.extend(YAHOO.widget.HTMLNode, YAHOO.widget.Node, {
1931 * The CSS class for the html content container. Defaults to ygtvhtml, but
1932 * can be overridden to provide a custom presentation for a specific node.
1933 * @property contentStyle
1936 contentStyle: "ygtvhtml",
1939 * The generated id that will contain the data passed in by the implementer.
1940 * @property contentElId
1946 * The HTML content to use for this node's display
1953 * Sets up the node label
1954 * @property initContent
1955 * @param {object} An html string or object containing an html property
1956 * @param {boolean} hasIcon determines if the node will be rendered with an
1959 initContent: function(oData, hasIcon) {
1960 if (typeof oData == "string") {
1961 oData = { html: oData };
1964 this.html = oData.html;
1965 this.contentElId = "ygtvcontentel" + this.index;
1966 this.hasIcon = hasIcon;
1971 * Returns the outer html element for this node's content
1972 * @method getContentEl
1973 * @return {HTMLElement} the element
1975 getContentEl: function() {
1976 return document.getElementById(this.contentElId);
1979 // overrides YAHOO.widget.Node
1980 getNodeHtml: function() {
1983 sb[sb.length] = '<table border="0" cellpadding="0" cellspacing="0">';
1984 sb[sb.length] = '<tr>';
1986 for (var i=0;i<this.depth;++i) {
1987 sb[sb.length] = '<td class="' + this.getDepthStyle(i) + '"> </td>';
1991 sb[sb.length] = '<td';
1992 sb[sb.length] = ' id="' + this.getToggleElId() + '"';
1993 sb[sb.length] = ' class="' + this.getStyle() + '"';
1994 sb[sb.length] = ' onclick="javascript:' + this.getToggleLink() + '"';
1995 if (this.hasChildren(true)) {
1996 sb[sb.length] = ' onmouseover="this.className=';
1997 sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
1998 sb[sb.length] = this.tree.id + '\',' + this.index + ').getHoverStyle()"';
1999 sb[sb.length] = ' onmouseout="this.className=';
2000 sb[sb.length] = 'YAHOO.widget.TreeView.getNode(\'';
2001 sb[sb.length] = this.tree.id + '\',' + this.index + ').getStyle()"';
2003 sb[sb.length] = '> </td>';
2006 sb[sb.length] = '<td';
2007 sb[sb.length] = ' id="' + this.contentElId + '"';
2008 sb[sb.length] = ' class="' + this.contentStyle + '"';
2009 sb[sb.length] = ' >';
2010 sb[sb.length] = this.html;
2011 sb[sb.length] = '</td>';
2012 sb[sb.length] = '</tr>';
2013 sb[sb.length] = '</table>';
2018 toString: function() {
2019 return "HTMLNode (" + this.index + ")";
2024 * A static factory class for tree view expand/collapse animations
2028 YAHOO.widget.TVAnim = function() {
2031 * Constant for the fade in animation
2036 FADE_IN: "TVFadeIn",
2039 * Constant for the fade out animation
2040 * @property FADE_OUT
2044 FADE_OUT: "TVFadeOut",
2047 * Returns a ygAnim instance of the given type
2049 * @param type {string} the type of animation
2050 * @param el {HTMLElement} the element to element (probably the children div)
2051 * @param callback {function} function to invoke when the animation is done.
2052 * @return {YAHOO.util.Animation} the animation instance
2055 getAnim: function(type, el, callback) {
2056 if (YAHOO.widget[type]) {
2057 return new YAHOO.widget[type](el, callback);
2064 * Returns true if the specified animation class is available
2066 * @param type {string} the type of animation
2067 * @return {boolean} true if valid, false if not
2070 isValid: function(type) {
2071 return (YAHOO.widget[type]);
2077 * A 1/2 second fade-in animation.
2080 * @param el {HTMLElement} the element to animate
2081 * @param callback {function} function to invoke when the animation is finished
2083 YAHOO.widget.TVFadeIn = function(el, callback) {
2085 * The element to animate
2092 * the callback to invoke when the animation is complete
2093 * @property callback
2096 this.callback = callback;
2100 YAHOO.widget.TVFadeIn.prototype = {
2102 * Performs the animation
2105 animate: function() {
2108 var s = this.el.style;
2110 s.filter = "alpha(opacity=10)";
2114 var a = new YAHOO.util.Anim(this.el, {opacity: {from: 0.1, to: 1, unit:""}}, dur);
2115 a.onComplete.subscribe( function() { tvanim.onComplete(); } );
2120 * Clean up and invoke callback
2121 * @method onComplete
2123 onComplete: function() {
2130 * @return {string} the string representation of the instance
2132 toString: function() {
2138 * A 1/2 second fade out animation.
2141 * @param el {HTMLElement} the element to animate
2142 * @param callback {Function} function to invoke when the animation is finished
2144 YAHOO.widget.TVFadeOut = function(el, callback) {
2146 * The element to animate
2153 * the callback to invoke when the animation is complete
2154 * @property callback
2157 this.callback = callback;
2161 YAHOO.widget.TVFadeOut.prototype = {
2163 * Performs the animation
2166 animate: function() {
2169 var a = new YAHOO.util.Anim(this.el, {opacity: {from: 1, to: 0.1, unit:""}}, dur);
2170 a.onComplete.subscribe( function() { tvanim.onComplete(); } );
2175 * Clean up and invoke callback
2176 * @method onComplete
2178 onComplete: function() {
2179 var s = this.el.style;
2182 s.filter = "alpha(opacity=100)";
2189 * @return {string} the string representation of the instance
2191 toString: function() {