2 Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.net/yui/license.txt
8 var Dom = YAHOO.util.Dom,
9 Event = YAHOO.util.Event,
11 Widget = YAHOO.widget;
16 * The treeview widget is a generic tree building tool.
18 * @title TreeView Widget
19 * @requires yahoo, event
20 * @optional animation, json
21 * @namespace YAHOO.widget
25 * Contains the tree view state data and the root node.
28 * @uses YAHOO.util.EventProvider
30 * @param {string|HTMLElement} id The id of the element, or the element itself that the tree will be inserted into. Existing markup in this element, if valid, will be used to build the tree
31 * @param {Array|object|string} oConfig (optional) An array containing the definition of the tree. (see buildTreeFromObject)
34 YAHOO.widget.TreeView = function(id, oConfig) {
35 if (id) { this.init(id); }
37 if (!Lang.isArray(oConfig)) {
40 this.buildTreeFromObject(oConfig);
41 } else if (Lang.trim(this._el.innerHTML)) {
42 this.buildTreeFromMarkup(id);
46 var TV = Widget.TreeView;
51 * The id of tree container element
58 * The host element for this tree
66 * Flat collection of all nodes in this tree. This is a sparse
67 * array, so the length property can't be relied upon for a
68 * node count for the tree.
76 * We lock the tree control while waiting for the dynamic loader to return
83 * The animation to use for expanding children, if any
84 * @property _expandAnim
91 * The animation to use for collapsing children, if any
92 * @property _collapseAnim
99 * The current number of animations that are executing
100 * @property _animCount
107 * The maximum number of animations to run at one time.
114 * Whether there is any subscriber to dblClickEvent
115 * @property _hasDblClickSubscriber
119 _hasDblClickSubscriber: false,
122 * Stores the timer used to check for double clicks
123 * @property _dblClickTimer
124 * @type window.timer object
127 _dblClickTimer: null,
130 * A reference to the Node currently having the focus or null if none.
131 * @property currentFocus
132 * @type YAHOO.widget.Node
137 * If true, only one Node can be highlighted at a time
138 * @property singleNodeHighlight
143 singleNodeHighlight: false,
146 * A reference to the Node that is currently highlighted.
147 * It is only meaningful if singleNodeHighlight is enabled
148 * @property _currentlyHighlighted
149 * @type YAHOO.widget.Node
154 _currentlyHighlighted: null,
157 * Sets up the animation for expanding children
158 * @method setExpandAnim
159 * @param {string} type the type of animation (acceptable values defined
160 * in YAHOO.widget.TVAnim)
162 setExpandAnim: function(type) {
163 this._expandAnim = (Widget.TVAnim.isValid(type)) ? type : null;
167 * Sets up the animation for collapsing children
168 * @method setCollapseAnim
169 * @param {string} the type of animation (acceptable values defined in
170 * YAHOO.widget.TVAnim)
172 setCollapseAnim: function(type) {
173 this._collapseAnim = (Widget.TVAnim.isValid(type)) ? type : null;
177 * Perform the expand animation if configured, or just show the
178 * element if not configured or too many animations are in progress
179 * @method animateExpand
180 * @param el {HTMLElement} the element to animate
181 * @param node {YAHOO.util.Node} the node that was expanded
182 * @return {boolean} true if animation could be invoked, false otherwise
184 animateExpand: function(el, node) {
185 this.logger.log("animating expand");
187 if (this._expandAnim && this._animCount < this.maxAnim) {
188 // this.locked = true;
190 var a = Widget.TVAnim.getAnim(this._expandAnim, el,
191 function() { tree.expandComplete(node); });
194 this.fireEvent("animStart", {
208 * Perform the collapse animation if configured, or just show the
209 * element if not configured or too many animations are in progress
210 * @method animateCollapse
211 * @param el {HTMLElement} the element to animate
212 * @param node {YAHOO.util.Node} the node that was expanded
213 * @return {boolean} true if animation could be invoked, false otherwise
215 animateCollapse: function(el, node) {
216 this.logger.log("animating collapse");
218 if (this._collapseAnim && this._animCount < this.maxAnim) {
219 // this.locked = true;
221 var a = Widget.TVAnim.getAnim(this._collapseAnim, el,
222 function() { tree.collapseComplete(node); });
225 this.fireEvent("animStart", {
239 * Function executed when the expand animation completes
240 * @method expandComplete
242 expandComplete: function(node) {
243 this.logger.log("expand complete: " + this.id);
245 this.fireEvent("animComplete", {
249 // this.locked = false;
253 * Function executed when the collapse animation completes
254 * @method collapseComplete
256 collapseComplete: function(node) {
257 this.logger.log("collapse complete: " + this.id);
259 this.fireEvent("animComplete", {
263 // this.locked = false;
267 * Initializes the tree
269 * @parm {string|HTMLElement} id the id of the element that will hold the tree
273 this._el = Dom.get(id);
274 this.id = Dom.generateId(this._el,"yui-tv-auto-id-");
277 * When animation is enabled, this event fires when the animation
281 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
282 * @parm {String} type the type of animation ("expand" or "collapse")
284 this.createEvent("animStart", this);
287 * When animation is enabled, this event fires when the animation
289 * @event animComplete
291 * @param {YAHOO.widget.Node} node the node that is expanding/collapsing
292 * @parm {String} type the type of animation ("expand" or "collapse")
294 this.createEvent("animComplete", this);
297 * Fires when a node is going to be collapsed. Return false to stop
301 * @param {YAHOO.widget.Node} node the node that is collapsing
303 this.createEvent("collapse", this);
306 * Fires after a node is successfully collapsed. This event will not fire
307 * if the "collapse" event was cancelled.
308 * @event collapseComplete
310 * @param {YAHOO.widget.Node} node the node that was collapsed
312 this.createEvent("collapseComplete", this);
315 * Fires when a node is going to be expanded. Return false to stop
319 * @param {YAHOO.widget.Node} node the node that is expanding
321 this.createEvent("expand", this);
324 * Fires after a node is successfully expanded. This event will not fire
325 * if the "expand" event was cancelled.
326 * @event expandComplete
328 * @param {YAHOO.widget.Node} node the node that was expanded
330 this.createEvent("expandComplete", this);
333 * Fires when the Enter key is pressed on a node that has the focus
334 * @event enterKeyPressed
336 * @param {YAHOO.widget.Node} node the node that has the focus
338 this.createEvent("enterKeyPressed", this);
341 * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a Click.
342 * The listener may return false to cancel toggling and focusing on the node.
345 * @param oArgs.event {HTMLEvent} The event object
346 * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked
348 this.createEvent("clickEvent", this);
351 * Fires when the focus receives the focus, when it changes from a Node
352 * to another Node or when it is completely lost (blurred)
353 * @event focusChanged
355 * @param oArgs.oldNode {YAHOO.widget.Node} Node that had the focus or null if none
356 * @param oArgs.newNode {YAHOO.widget.Node} Node that receives the focus or null if none
359 this.createEvent('focusChanged',this);
362 * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a double Click
363 * @event dblClickEvent
365 * @param oArgs.event {HTMLEvent} The event object
366 * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked
369 this.createEvent("dblClickEvent", {
371 onSubscribeCallback: function() {
372 self._hasDblClickSubscriber = true;
377 * Custom event that is fired when the text node label is clicked.
378 * The node clicked is provided as an argument
382 * @param {YAHOO.widget.Node} node the node clicked
383 * @deprecated use clickEvent or dblClickEvent
385 this.createEvent("labelClick", this);
388 * Custom event fired when the highlight of a node changes.
389 * The node that triggered the change is provided as an argument:
390 * The status of the highlight can be checked in
391 * <a href="YAHOO.widget.Node.html#property_highlightState">nodeRef.highlightState</a>.
392 * Depending on <a href="YAHOO.widget.Node.html#property_propagateHighlight">nodeRef.propagateHighlight</a>, other nodes might have changed
393 * @event highlightEvent
395 * @param node{YAHOO.widget.Node} the node that started the change in highlighting state
397 this.createEvent("highlightEvent",this);
403 // store a global reference
404 TV.trees[this.id] = this;
406 // Set up the root node
407 this.root = new Widget.RootNode(this);
409 var LW = Widget.LogWriter;
411 this.logger = (LW) ? new LW(this.toString()) : YAHOO;
413 this.logger.log("tree init: " + this.id);
415 // YAHOO.util.Event.onContentReady(this.id, this.handleAvailable, this, true);
416 // YAHOO.util.Event.on(this.id, "click", this.handleClick, this, true);
419 //handleAvailable: function() {
420 //var Event = YAHOO.util.Event;
424 * Builds the TreeView from an object.
425 * This is the method called by the constructor to build the tree when it has a second argument.
426 * A tree can be described by an array of objects, each object corresponding to a node.
427 * Node descriptions may contain values for any property of a node plus the following extra properties: <ul>
428 * <li>type: can be one of the following:<ul>
429 * <li> A shortname for a node type (<code>'text','menu','html'</code>) </li>
430 * <li>The name of a Node class under YAHOO.widget (<code>'TextNode', 'MenuNode', 'DateNode'</code>, etc) </li>
431 * <li>a reference to an actual class: <code>YAHOO.widget.DateNode</code></li></ul></li>
432 * <li>children: an array containing further node definitions</li></ul>
433 * @method buildTreeFromObject
434 * @param oConfig {Array} array containing a full description of the tree
437 buildTreeFromObject: function (oConfig) {
438 var logger = this.logger;
439 logger.log('Building tree from object');
440 var build = function (parent, oConfig) {
441 var i, item, node, children, type, NodeType, ThisType;
442 for (i = 0; i < oConfig.length; i++) {
444 if (Lang.isString(item)) {
445 node = new Widget.TextNode(item, parent);
446 } else if (Lang.isObject(item)) {
447 children = item.children;
448 delete item.children;
449 type = item.type || 'text';
451 switch (Lang.isString(type) && type.toLowerCase()) {
453 node = new Widget.TextNode(item, parent);
456 node = new Widget.MenuNode(item, parent);
459 node = new Widget.HTMLNode(item, parent);
462 if (Lang.isString(type)) {
463 NodeType = Widget[type];
467 if (Lang.isObject(NodeType)) {
468 for (ThisType = NodeType; ThisType && ThisType !== Widget.Node; ThisType = ThisType.superclass.constructor) {}
470 node = new NodeType(item, parent);
472 logger.log('Invalid type in node definition: ' + type,'error');
475 logger.log('Invalid type in node definition: ' + type,'error');
479 build(node,children);
482 logger.log('Invalid node definition','error');
488 build(this.root,oConfig);
491 * Builds the TreeView from existing markup. Markup should consist of <UL> or <OL> elements containing <LI> elements.
492 * Each <LI> can have one element used as label and a second optional element which is to be a <UL> or <OL>
493 * containing nested nodes.
494 * Depending on what the first element of the <LI> element is, the following Nodes will be created: <ul>
495 * <li>plain text: a regular TextNode</li>
496 * <li>anchor <A>: a TextNode with its <code>href</code> and <code>target</code> taken from the anchor</li>
497 * <li>anything else: an HTMLNode</li></ul>
498 * Only the first outermost (un-)ordered list in the markup and its children will be parsed.
499 * Nodes will be collapsed unless an <LI> tag has a className called 'expanded'.
500 * All other className attributes will be copied over to the Node className property.
501 * If the <LI> element contains an attribute called <code>yuiConfig</code>, its contents should be a JSON-encoded object
502 * as the one used in method <a href="#method_buildTreeFromObject">buildTreeFromObject</a>.
503 * @method buildTreeFromMarkup
504 * @param id{string|HTMLElement} The id of the element that contains the markup or a reference to it.
506 buildTreeFromMarkup: function (id) {
507 this.logger.log('Building tree from existing markup');
508 var build = function (markup) {
509 var el, child, branch = [], config = {}, label, yuiConfig;
510 // Dom's getFirstChild and getNextSibling skip over text elements
511 for (el = Dom.getFirstChild(markup); el; el = Dom.getNextSibling(el)) {
512 switch (el.tagName.toUpperCase()) {
516 expanded: Dom.hasClass(el,'expanded'),
517 title: el.title || el.alt || null,
518 className: Lang.trim(el.className.replace(/\bexpanded\b/,'')) || null
520 // I cannot skip over text elements here because I want them for labels
521 child = el.firstChild;
522 if (child.nodeType == 3) {
523 // nodes with only whitespace, tabs and new lines don't count, they are probably just formatting.
524 label = Lang.trim(child.nodeValue.replace(/[\n\t\r]*/g,''));
526 config.type = 'text';
527 config.label = label;
529 child = Dom.getNextSibling(child);
533 if (child.tagName.toUpperCase() == 'A') {
534 config.type = 'text';
535 config.label = child.innerHTML;
536 config.href = child.href;
537 config.target = child.target;
538 config.title = child.title || child.alt || config.title;
540 config.type = 'html';
541 var d = document.createElement('div');
542 d.appendChild(child.cloneNode(true));
543 config.html = d.innerHTML;
544 config.hasIcon = true;
547 // see if after the label it has a further list which will become children of this node.
548 child = Dom.getNextSibling(child);
549 switch (child && child.tagName.toUpperCase()) {
552 config.children = build(child);
555 // if there are further elements or text, it will be ignored.
557 if (YAHOO.lang.JSON) {
558 yuiConfig = el.getAttribute('yuiConfig');
560 yuiConfig = YAHOO.lang.JSON.parse(yuiConfig);
561 config = YAHOO.lang.merge(config,yuiConfig);
569 this.logger.log('ULs or OLs can only contain LI elements, not other UL or OL. This will not work in some browsers','error');
573 children: build(child)
582 var markup = Dom.getChildrenBy(Dom.get(id),function (el) {
583 var tag = el.tagName.toUpperCase();
584 return tag == 'UL' || tag == 'OL';
587 this.buildTreeFromObject(build(markup[0]));
589 this.logger.log('Markup contains no UL or OL elements','warn');
593 * Returns the TD element where the event has occurred
594 * @method _getEventTargetTdEl
597 _getEventTargetTdEl: function (ev) {
598 var target = Event.getTarget(ev);
599 // go up looking for a TD with a className with a ygtv prefix
600 while (target && !(target.tagName.toUpperCase() == 'TD' && Dom.hasClass(target.parentNode,'ygtvrow'))) {
601 target = Dom.getAncestorByTagName(target,'td');
603 if (Lang.isNull(target)) { return null; }
604 // If it is a spacer cell, do nothing
605 if (/\bygtv(blank)?depthcell/.test(target.className)) { return null;}
606 // If it has an id, search for the node number and see if it belongs to a node in this tree.
608 var m = target.id.match(/\bygtv([^\d]*)(.*)/);
609 if (m && m[2] && this._nodes[m[2]]) {
616 * Event listener for click events
617 * @method _onClickEvent
620 _onClickEvent: function (ev) {
622 td = this._getEventTargetTdEl(ev),
625 toggle = function () {
629 Event.preventDefault(ev);
632 // For some reason IE8 is providing an event object with
633 // most of the fields missing, but only when clicking on
634 // the node's label, and only when working with inline
635 // editing. This generates a "Member not found" error
636 // in that browser. Determine if this is a browser
637 // bug, or a problem with this code. Already checked to
638 // see if the problem has to do with access the event
639 // in the outer scope, and that isn't the problem.
640 // Maybe the markup for inline editing is broken.
648 node = this.getNodeByElement(td);
653 // exception to handle deprecated event labelClick
654 // @TODO take another look at this deprecation. It is common for people to
655 // only be interested in the label click, so why make them have to test
656 // the node type to figure out whether the click was on the label?
657 target = Event.getTarget(ev);
658 if (Dom.hasClass(target, node.labelStyle) || Dom.getAncestorByClassName(target,node.labelStyle)) {
659 this.logger.log("onLabelClick " + node.label);
660 this.fireEvent('labelClick',node);
663 // If it is a toggle cell, toggle
664 if (/\bygtv[tl][mp]h?h?/.test(td.className)) {
667 if (this._dblClickTimer) {
668 window.clearTimeout(this._dblClickTimer);
669 this._dblClickTimer = null;
671 if (this._hasDblClickSubscriber) {
672 this._dblClickTimer = window.setTimeout(function () {
673 self._dblClickTimer = null;
674 if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) {
679 if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) {
688 * Event listener for double-click events
689 * @method _onDblClickEvent
692 _onDblClickEvent: function (ev) {
693 if (!this._hasDblClickSubscriber) { return; }
694 var td = this._getEventTargetTdEl(ev);
697 if (!(/\bygtv[tl][mp]h?h?/.test(td.className))) {
698 this.fireEvent('dblClickEvent', {event:ev, node:this.getNodeByElement(td)});
699 if (this._dblClickTimer) {
700 window.clearTimeout(this._dblClickTimer);
701 this._dblClickTimer = null;
706 * Event listener for mouse over events
707 * @method _onMouseOverEvent
710 _onMouseOverEvent:function (ev) {
712 if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) {
713 target.className = target.className.replace(/\bygtv([lt])([mp])\b/gi,'ygtv$1$2h');
717 * Event listener for mouse out events
718 * @method _onMouseOutEvent
721 _onMouseOutEvent: function (ev) {
723 if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) {
724 target.className = target.className.replace(/\bygtv([lt])([mp])h\b/gi,'ygtv$1$2');
728 * Event listener for key down events
729 * @method _onKeyDownEvent
732 _onKeyDownEvent: function (ev) {
733 var target = Event.getTarget(ev),
734 node = this.getNodeByElement(target),
736 KEY = YAHOO.util.KeyListener.KEY;
740 this.logger.log('UP');
742 if (newNode.previousSibling) {
743 newNode = newNode.previousSibling;
745 newNode = newNode.parent;
747 } while (newNode && !newNode._canHaveFocus());
748 if (newNode) { newNode.focus(); }
749 Event.preventDefault(ev);
752 this.logger.log('DOWN');
754 if (newNode.nextSibling) {
755 newNode = newNode.nextSibling;
758 newNode = (newNode.children.length || null) && newNode.children[0];
760 } while (newNode && !newNode._canHaveFocus);
761 if (newNode) { newNode.focus();}
762 Event.preventDefault(ev);
765 this.logger.log('LEFT');
767 if (newNode.parent) {
768 newNode = newNode.parent;
770 newNode = newNode.previousSibling;
772 } while (newNode && !newNode._canHaveFocus());
773 if (newNode) { newNode.focus();}
774 Event.preventDefault(ev);
777 this.logger.log('RIGHT');
780 if (newNode.children.length) {
781 newNode = newNode.children[0];
783 newNode = newNode.nextSibling;
785 } while (newNode && !newNode._canHaveFocus());
786 if (newNode) { newNode.focus();}
787 Event.preventDefault(ev);
790 this.logger.log('ENTER: ' + newNode.href);
793 window.open(node.href,node.target);
795 window.location(node.href);
800 this.fireEvent('enterKeyPressed',node);
801 Event.preventDefault(ev);
804 this.logger.log('HOME');
805 newNode = this.getRoot();
806 if (newNode.children.length) {newNode = newNode.children[0];}
807 if (newNode._canHaveFocus()) { newNode.focus(); }
808 Event.preventDefault(ev);
811 this.logger.log('END');
812 newNode = newNode.parent.children;
813 newNode = newNode[newNode.length -1];
814 if (newNode._canHaveFocus()) { newNode.focus(); }
815 Event.preventDefault(ev);
818 // this.logger.log('PAGE_UP');
820 // case KEY.PAGE_DOWN:
821 // this.logger.log('PAGE_DOWN');
823 case 107: // plus key
825 this.logger.log('Shift-PLUS');
826 node.parent.expandAll();
828 this.logger.log('PLUS');
832 case 109: // minus key
834 this.logger.log('Shift-MINUS');
835 node.parent.collapseAll();
837 this.logger.log('MINUS');
846 * Renders the tree boilerplate and visible nodes
850 var html = this.root.getHtml(),
853 if (!this._hasEvents) {
854 Event.on(el, 'click', this._onClickEvent, this, true);
855 Event.on(el, 'dblclick', this._onDblClickEvent, this, true);
856 Event.on(el, 'mouseover', this._onMouseOverEvent, this, true);
857 Event.on(el, 'mouseout', this._onMouseOutEvent, this, true);
858 Event.on(el, 'keydown', this._onKeyDownEvent, this, true);
860 this._hasEvents = true;
864 * Returns the tree's host element
866 * @return {HTMLElement} the host element
870 this._el = Dom.get(this.id);
876 * Nodes register themselves with the tree instance when they are created.
878 * @param node {Node} the node to register
881 regNode: function(node) {
882 this._nodes[node.index] = node;
886 * Returns the root node of this tree
888 * @return {Node} the root node
890 getRoot: function() {
895 * Configures this tree to dynamically load all child data
896 * @method setDynamicLoad
897 * @param {function} fnDataLoader the function that will be called to get the data
898 * @param iconMode {int} configures the icon that is displayed when a dynamic
899 * load node is expanded the first time without children. By default, the
900 * "collapse" icon will be used. If set to 1, the leaf node icon will be
903 setDynamicLoad: function(fnDataLoader, iconMode) {
904 this.root.setDynamicLoad(fnDataLoader, iconMode);
908 * Expands all child nodes. Note: this conflicts with the "multiExpand"
909 * node property. If expand all is called in a tree with nodes that
910 * do not allow multiple siblings to be displayed, only the last sibling
914 expandAll: function() {
916 this.root.expandAll();
921 * Collapses all expanded child nodes in the entire tree.
922 * @method collapseAll
924 collapseAll: function() {
926 this.root.collapseAll();
931 * Returns a node in the tree that has the specified index (this index
932 * is created internally, so this function probably will only be used
933 * in html generated for a given node.)
934 * @method getNodeByIndex
935 * @param {int} nodeIndex the index of the node wanted
936 * @return {Node} the node with index=nodeIndex, null if no match
938 getNodeByIndex: function(nodeIndex) {
939 var n = this._nodes[nodeIndex];
940 return (n) ? n : null;
944 * Returns a node that has a matching property and value in the data
945 * object that was passed into its constructor.
946 * @method getNodeByProperty
947 * @param {object} property the property to search (usually a string)
948 * @param {object} value the value we want to find (usuall an int or string)
949 * @return {Node} the matching node, null if no match
951 getNodeByProperty: function(property, value) {
952 for (var i in this._nodes) {
953 if (this._nodes.hasOwnProperty(i)) {
954 var n = this._nodes[i];
955 if ((property in n && n[property] == value) || (n.data && value == n.data[property])) {
965 * Returns a collection of nodes that have a matching property
966 * and value in the data object that was passed into its constructor.
967 * @method getNodesByProperty
968 * @param {object} property the property to search (usually a string)
969 * @param {object} value the value we want to find (usuall an int or string)
970 * @return {Array} the matching collection of nodes, null if no match
972 getNodesByProperty: function(property, value) {
974 for (var i in this._nodes) {
975 if (this._nodes.hasOwnProperty(i)) {
976 var n = this._nodes[i];
977 if ((property in n && n[property] == value) || (n.data && value == n.data[property])) {
983 return (values.length) ? values : null;
987 * Returns the treeview node reference for an anscestor element
988 * of the node, or null if it is not contained within any node
990 * @method getNodeByElement
991 * @param {HTMLElement} the element to test
992 * @return {YAHOO.widget.Node} a node reference or null
994 getNodeByElement: function(el) {
996 var p=el, m, re=/ygtv([^\d]*)(.*)/;
1003 return this.getNodeByIndex(m[2]);
1009 if (!p || !p.tagName) {
1014 while (p.id !== this.id && p.tagName.toLowerCase() !== "body");
1020 * Removes the node and its children, and optionally refreshes the
1021 * branch of the tree that was affected.
1022 * @method removeNode
1023 * @param {Node} The node to remove
1024 * @param {boolean} autoRefresh automatically refreshes branch if true
1025 * @return {boolean} False is there was a problem, true otherwise.
1027 removeNode: function(node, autoRefresh) {
1029 // Don't delete the root node
1030 if (node.isRoot()) {
1034 // Get the branch that we may need to refresh
1035 var p = node.parent;
1040 // Delete the node and its children
1041 this._deleteNode(node);
1043 // Refresh the parent of the parent
1044 if (autoRefresh && p && p.childrenRendered) {
1052 * wait until the animation is complete before deleting
1053 * to avoid javascript errors
1054 * @method _removeChildren_animComplete
1055 * @param o the custom event payload
1058 _removeChildren_animComplete: function(o) {
1059 this.unsubscribe(this._removeChildren_animComplete);
1060 this.removeChildren(o.node);
1064 * Deletes this nodes child collection, recursively. Also collapses
1065 * the node, and resets the dynamic load flag. The primary use for
1066 * this method is to purge a node and allow it to fetch its data
1067 * dynamically again.
1068 * @method removeChildren
1069 * @param {Node} node the node to purge
1071 removeChildren: function(node) {
1073 if (node.expanded) {
1074 // wait until the animation is complete before deleting to
1075 // avoid javascript errors
1076 if (this._collapseAnim) {
1077 this.subscribe("animComplete",
1078 this._removeChildren_animComplete, this, true);
1079 Widget.Node.prototype.collapse.call(node);
1086 this.logger.log("Removing children for " + node);
1087 while (node.children.length) {
1088 this._deleteNode(node.children[0]);
1091 if (node.isRoot()) {
1092 Widget.Node.prototype.expand.call(node);
1095 node.childrenRendered = false;
1096 node.dynamicLoadComplete = false;
1102 * Deletes the node and recurses children
1103 * @method _deleteNode
1106 _deleteNode: function(node) {
1107 // Remove all the child nodes first
1108 this.removeChildren(node);
1110 // Remove the node from the tree
1115 * Removes the node from the tree, preserving the child collection
1116 * to make it possible to insert the branch into another part of the
1117 * tree, or another tree.
1119 * @param {Node} the node to remove
1121 popNode: function(node) {
1122 var p = node.parent;
1124 // Update the parent's collection of children
1127 for (var i=0, len=p.children.length;i<len;++i) {
1128 if (p.children[i] != node) {
1129 a[a.length] = p.children[i];
1135 // reset the childrenRendered flag for the parent
1136 p.childrenRendered = false;
1138 // Update the sibling relationship
1139 if (node.previousSibling) {
1140 node.previousSibling.nextSibling = node.nextSibling;
1143 if (node.nextSibling) {
1144 node.nextSibling.previousSibling = node.previousSibling;
1148 node.previousSibling = null;
1149 node.nextSibling = null;
1152 // Update the tree's node collection
1153 delete this._nodes[node.index];
1157 * Nulls out the entire TreeView instance and related objects, removes attached
1158 * event listeners, and clears out DOM elements inside the container. After
1159 * calling this method, the instance reference should be expliclitly nulled by
1160 * implementer, as in myDataTable = null. Use with caution!
1164 destroy : function() {
1165 // Since the label editor can be separated from the main TreeView control
1166 // the destroy method for it might not be there.
1167 if (this._destroyEditor) { this._destroyEditor(); }
1168 var el = this.getEl();
1169 Event.removeListener(el,'click');
1170 Event.removeListener(el,'dblclick');
1171 Event.removeListener(el,'mouseover');
1172 Event.removeListener(el,'mouseout');
1173 Event.removeListener(el,'keydown');
1174 for (var i = 0 ; i < this._nodes.length; i++) {
1175 var node = this._nodes[i];
1176 if (node && node.destroy) {node.destroy(); }
1179 this._hasEvents = false;
1186 * TreeView instance toString
1188 * @return {string} string representation of the tree
1190 toString: function() {
1191 return "TreeView " + this.id;
1195 * Count of nodes in tree
1196 * @method getNodeCount
1197 * @return {int} number of nodes in the tree
1199 getNodeCount: function() {
1200 return this.getRoot().getNodeCount();
1204 * Returns an object which could be used to rebuild the tree.
1205 * It can be passed to the tree constructor to reproduce the same tree.
1206 * It will return false if any node loads dynamically, regardless of whether it is loaded or not.
1207 * @method getTreeDefinition
1208 * @return {Object | false} definition of the tree or false if any node is defined as dynamic
1210 getTreeDefinition: function() {
1211 return this.getRoot().getNodeDefinition();
1215 * Abstract method that is executed when a node is expanded
1217 * @param node {Node} the node that was expanded
1218 * @deprecated use treeobj.subscribe("expand") instead
1220 onExpand: function(node) { },
1223 * Abstract method that is executed when a node is collapsed.
1224 * @method onCollapse
1225 * @param node {Node} the node that was collapsed.
1226 * @deprecated use treeobj.subscribe("collapse") instead
1228 onCollapse: function(node) { },
1231 * Sets the value of a property for all loaded nodes in the tree.
1232 * @method setNodesProperty
1233 * @param name {string} Name of the property to be set
1234 * @param value {any} value to be set
1235 * @param refresh {boolean} if present and true, it does a refresh
1237 setNodesProperty: function(name, value, refresh) {
1238 this.root.setNodesProperty(name,value);
1240 this.root.refresh();
1244 * Event listener to toggle node highlight.
1245 * Can be assigned as listener to clickEvent, dblClickEvent and enterKeyPressed.
1246 * It returns false to prevent the default action.
1247 * @method onEventToggleHighlight
1248 * @param oArgs {any} it takes the arguments of any of the events mentioned above
1249 * @return {false} Always cancels the default action for the event
1251 onEventToggleHighlight: function (oArgs) {
1253 if ('node' in oArgs && oArgs.node instanceof Widget.Node) {
1255 } else if (oArgs instanceof Widget.Node) {
1260 node.toggleHighlight();
1267 /* Backwards compatibility aliases */
1268 var PROT = TV.prototype;
1270 * Renders the tree boilerplate and visible nodes.
1273 * @deprecated Use render instead
1275 PROT.draw = PROT.render;
1277 /* end backwards compatibility aliases */
1279 YAHOO.augment(TV, YAHOO.util.EventProvider);
1282 * Running count of all nodes created in all trees. This is
1283 * used to provide unique identifies for all nodes. Deleting
1284 * nodes does not change the nodeCount.
1285 * @property YAHOO.widget.TreeView.nodeCount
1292 * Global cache of tree instances
1293 * @property YAHOO.widget.TreeView.trees
1301 * Global method for getting a tree by its id. Used in the generated
1303 * @method YAHOO.widget.TreeView.getTree
1304 * @param treeId {String} the id of the tree instance
1305 * @return {TreeView} the tree instance requested, null if not found.
1308 TV.getTree = function(treeId) {
1309 var t = TV.trees[treeId];
1310 return (t) ? t : null;
1315 * Global method for getting a node by its id. Used in the generated
1317 * @method YAHOO.widget.TreeView.getNode
1318 * @param treeId {String} the id of the tree instance
1319 * @param nodeIndex {String} the index of the node to return
1320 * @return {Node} the node instance requested, null if not found
1323 TV.getNode = function(treeId, nodeIndex) {
1324 var t = TV.getTree(treeId);
1325 return (t) ? t.getNodeByIndex(nodeIndex) : null;
1330 * Class name assigned to elements that have the focus
1332 * @property TreeView.FOCUS_CLASS_NAME
1336 * @default "ygtvfocus"
1339 TV.FOCUS_CLASS_NAME = 'ygtvfocus';
1342 * Attempts to preload the images defined in the styles used to draw the tree by
1343 * rendering off-screen elements that use the styles.
1344 * @method YAHOO.widget.TreeView.preload
1345 * @param {string} prefix the prefix to use to generate the names of the
1346 * images to preload, default is ygtv
1349 TV.preload = function(e, prefix) {
1350 prefix = prefix || "ygtv";
1352 YAHOO.log("Preloading images: " + prefix, "info", "TreeView");
1354 var styles = ["tn","tm","tmh","tp","tph","ln","lm","lmh","lp","lph","loading"];
1355 // var styles = ["tp"];
1359 // save the first one for the outer container
1360 for (var i=1; i < styles.length; i=i+1) {
1361 sb[sb.length] = '<span class="' + prefix + styles[i] + '"> </span>';
1364 var f = document.createElement("div");
1366 s.className = prefix + styles[0];
1367 s.position = "absolute";
1372 f.innerHTML = sb.join("");
1374 document.body.appendChild(f);
1376 Event.removeListener(window, "load", TV.preload);
1380 Event.addListener(window,"load", TV.preload);
1383 var Dom = YAHOO.util.Dom,
1385 Event = YAHOO.util.Event;
1387 * The base class for all tree nodes. The node's presentation and behavior in
1388 * response to mouse events is handled in Node subclasses.
1389 * @namespace YAHOO.widget
1391 * @uses YAHOO.util.EventProvider
1392 * @param oData {object} a string or object containing the data that will
1393 * be used to render this node, and any custom attributes that should be
1394 * stored with the node (which is available in noderef.data).
1395 * All values in oData will be used to set equally named properties in the node
1396 * as long as the node does have such properties, they are not undefined, private or functions,
1397 * the rest of the values will be stored in noderef.data
1398 * @param oParent {Node} this node's parent node
1399 * @param expanded {boolean} the initial expanded/collapsed state (deprecated, use oData.expanded)
1402 YAHOO.widget.Node = function(oData, oParent, expanded) {
1403 if (oData) { this.init(oData, oParent, expanded); }
1406 YAHOO.widget.Node.prototype = {
1409 * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
1416 * This node's child node collection.
1417 * @property children
1423 * Tree instance this node is part of
1430 * The data linked to this node. This can be any object or primitive
1431 * value, and the data can be used in getNodeHtml().
1445 * The depth of this node. We start at -1 for the root node.
1452 * The node's expanded/collapsed state
1453 * @property expanded
1459 * Can multiple children be expanded at once?
1460 * @property multiExpand
1466 * Should we render children for a collapsed node? It is possible that the
1467 * implementer will want to render the hidden data... @todo verify that we
1468 * need this, and implement it if we do.
1469 * @property renderHidden
1472 renderHidden: false,
1475 * This flag is set to true when the html is generated for this node's
1476 * children, and set to false when new children are added.
1477 * @property childrenRendered
1480 childrenRendered: false,
1483 * Dynamically loaded nodes only fetch the data the first time they are
1484 * expanded. This flag is set to true once the data has been fetched.
1485 * @property dynamicLoadComplete
1488 dynamicLoadComplete: false,
1491 * This node's previous sibling
1492 * @property previousSibling
1495 previousSibling: null,
1498 * This node's next sibling
1499 * @property nextSibling
1505 * We can set the node up to call an external method to get the child
1507 * @property _dynLoad
1514 * Function to execute when we need to get this node's child data.
1515 * @property dataLoader
1521 * This is true for dynamically loading nodes while waiting for the
1522 * callback to return.
1523 * @property isLoading
1529 * The toggle/branch icon will not show if this is set to false. This
1530 * could be useful if the implementer wants to have the child contain
1531 * extra info about the parent, rather than an actual node.
1538 * Used to configure what happens when a dynamic load node is expanded
1539 * and we discover that it does not have children. By default, it is
1540 * treated as if it still could have children (plus/minus icon). Set
1541 * iconMode to have it display like a leaf node instead.
1542 * @property iconMode
1548 * Specifies whether or not the content area of the node should be allowed
1557 * If true, the node will alway be rendered as a leaf node. This can be
1558 * used to override the presentation when dynamically loading the entire
1559 * tree. Setting this to true also disables the dynamic load call for the
1568 * The CSS class for the html content container. Defaults to ygtvhtml, but
1569 * can be overridden to provide a custom presentation for a specific node.
1570 * @property contentStyle
1577 * The generated id that will contain the data passed in by the implementer.
1578 * @property contentElId
1584 * Enables node highlighting. If true, the node can be highlighted and/or propagate highlighting
1585 * @property enableHighlight
1589 enableHighlight: true,
1592 * Stores the highlight state. Can be any of:
1594 * <li>0 - not highlighted</li>
1595 * <li>1 - highlighted</li>
1596 * <li>2 - some children highlighted</li>
1598 * @property highlightState
1606 * Tells whether highlighting will be propagated up to the parents of the clicked node
1607 * @property propagateHighlightUp
1612 propagateHighlightUp: false,
1615 * Tells whether highlighting will be propagated down to the children of the clicked node
1616 * @property propagateHighlightDown
1621 propagateHighlightDown: false,
1624 * User-defined className to be added to the Node
1625 * @property className
1642 spacerPath: "http://us.i1.yimg.com/us.yimg.com/i/space.gif",
1643 expandedText: "Expanded",
1644 collapsedText: "Collapsed",
1645 loadingText: "Loading",
1649 * Initializes this node, gets some of the properties from the parent
1651 * @param oData {object} a string or object containing the data that will
1652 * be used to render this node
1653 * @param oParent {Node} this node's parent node
1654 * @param expanded {boolean} the initial expanded/collapsed state
1656 init: function(oData, oParent, expanded) {
1660 this.index = YAHOO.widget.TreeView.nodeCount;
1661 ++YAHOO.widget.TreeView.nodeCount;
1662 this.contentElId = "ygtvcontentel" + this.index;
1664 if (Lang.isObject(oData)) {
1665 for (var property in oData) {
1666 if (oData.hasOwnProperty(property)) {
1667 if (property.charAt(0) != '_' && !Lang.isUndefined(this[property]) && !Lang.isFunction(this[property]) ) {
1668 this[property] = oData[property];
1670 this.data[property] = oData[property];
1675 if (!Lang.isUndefined(expanded) ) { this.expanded = expanded; }
1677 this.logger = new YAHOO.widget.LogWriter(this.toString());
1680 * The parentChange event is fired when a parent element is applied
1681 * to the node. This is useful if you need to apply tree-level
1682 * properties to a tree that need to happen if a node is moved from
1683 * one tree to another.
1685 * @event parentChange
1688 this.createEvent("parentChange", this);
1690 // oParent should never be null except when we create the root node.
1692 oParent.appendChild(this);
1697 * Certain properties for the node cannot be set until the parent
1698 * is known. This is called after the node is inserted into a tree.
1699 * the parent is also applied to this node's children in order to
1700 * make it possible to move a branch from one tree to another.
1701 * @method applyParent
1702 * @param {Node} parentNode this node's parent node
1703 * @return {boolean} true if the application was successful
1705 applyParent: function(parentNode) {
1710 this.tree = parentNode.tree;
1711 this.parent = parentNode;
1712 this.depth = parentNode.depth + 1;
1714 // @todo why was this put here. This causes new nodes added at the
1715 // root level to lose the menu behavior.
1716 // if (! this.multiExpand) {
1717 // this.multiExpand = parentNode.multiExpand;
1720 this.tree.regNode(this);
1721 parentNode.childrenRendered = false;
1723 // cascade update existing children
1724 for (var i=0, len=this.children.length;i<len;++i) {
1725 this.children[i].applyParent(this);
1728 this.fireEvent("parentChange");
1734 * Appends a node to the child collection.
1735 * @method appendChild
1736 * @param childNode {Node} the new node
1737 * @return {Node} the child node
1740 appendChild: function(childNode) {
1741 if (this.hasChildren()) {
1742 var sib = this.children[this.children.length - 1];
1743 sib.nextSibling = childNode;
1744 childNode.previousSibling = sib;
1746 this.children[this.children.length] = childNode;
1747 childNode.applyParent(this);
1749 // part of the IE display issue workaround. If child nodes
1750 // are added after the initial render, and the node was
1751 // instantiated with expanded = true, we need to show the
1752 // children div now that the node has a child.
1753 if (this.childrenRendered && this.expanded) {
1754 this.getChildrenEl().style.display = "";
1761 * Appends this node to the supplied node's child collection
1763 * @param parentNode {Node} the node to append to.
1764 * @return {Node} The appended node
1766 appendTo: function(parentNode) {
1767 return parentNode.appendChild(this);
1771 * Inserts this node before this supplied node
1772 * @method insertBefore
1773 * @param node {Node} the node to insert this node before
1774 * @return {Node} the inserted node
1776 insertBefore: function(node) {
1777 this.logger.log("insertBefore: " + node);
1778 var p = node.parent;
1782 this.tree.popNode(this);
1785 var refIndex = node.isChildOf(p);
1786 //this.logger.log(refIndex);
1787 p.children.splice(refIndex, 0, this);
1788 if (node.previousSibling) {
1789 node.previousSibling.nextSibling = this;
1791 this.previousSibling = node.previousSibling;
1792 this.nextSibling = node;
1793 node.previousSibling = this;
1795 this.applyParent(p);
1802 * Inserts this node after the supplied node
1803 * @method insertAfter
1804 * @param node {Node} the node to insert after
1805 * @return {Node} the inserted node
1807 insertAfter: function(node) {
1808 this.logger.log("insertAfter: " + node);
1809 var p = node.parent;
1813 this.tree.popNode(this);
1816 var refIndex = node.isChildOf(p);
1817 this.logger.log(refIndex);
1819 if (!node.nextSibling) {
1820 this.nextSibling = null;
1821 return this.appendTo(p);
1824 p.children.splice(refIndex + 1, 0, this);
1826 node.nextSibling.previousSibling = this;
1827 this.previousSibling = node;
1828 this.nextSibling = node.nextSibling;
1829 node.nextSibling = this;
1831 this.applyParent(p);
1838 * Returns true if the Node is a child of supplied Node
1840 * @param parentNode {Node} the Node to check
1841 * @return {boolean} The node index if this Node is a child of
1842 * supplied Node, else -1.
1845 isChildOf: function(parentNode) {
1846 if (parentNode && parentNode.children) {
1847 for (var i=0, len=parentNode.children.length; i<len ; ++i) {
1848 if (parentNode.children[i] === this) {
1858 * Returns a node array of this node's siblings, null if none.
1859 * @method getSiblings
1862 getSiblings: function() {
1863 var sib = this.parent.children.slice(0);
1864 for (var i=0;i < sib.length && sib[i] != this;i++) {}
1866 if (sib.length) { return sib; }
1871 * Shows this node's children
1872 * @method showChildren
1874 showChildren: function() {
1875 if (!this.tree.animateExpand(this.getChildrenEl(), this)) {
1876 if (this.hasChildren()) {
1877 this.getChildrenEl().style.display = "";
1883 * Hides this node's children
1884 * @method hideChildren
1886 hideChildren: function() {
1887 this.logger.log("hiding " + this.index);
1889 if (!this.tree.animateCollapse(this.getChildrenEl(), this)) {
1890 this.getChildrenEl().style.display = "none";
1895 * Returns the id for this node's container div
1897 * @return {string} the element id
1899 getElId: function() {
1900 return "ygtv" + this.index;
1904 * Returns the id for this node's children div
1905 * @method getChildrenElId
1906 * @return {string} the element id for this node's children div
1908 getChildrenElId: function() {
1909 return "ygtvc" + this.index;
1913 * Returns the id for this node's toggle element
1914 * @method getToggleElId
1915 * @return {string} the toggel element id
1917 getToggleElId: function() {
1918 return "ygtvt" + this.index;
1923 * Returns the id for this node's spacer image. The spacer is positioned
1924 * over the toggle and provides feedback for screen readers.
1925 * @method getSpacerId
1926 * @return {string} the id for the spacer image
1929 getSpacerId: function() {
1930 return "ygtvspacer" + this.index;
1935 * Returns this node's container html element
1937 * @return {HTMLElement} the container html element
1940 return Dom.get(this.getElId());
1944 * Returns the div that was generated for this node's children
1945 * @method getChildrenEl
1946 * @return {HTMLElement} this node's children div
1948 getChildrenEl: function() {
1949 return Dom.get(this.getChildrenElId());
1953 * Returns the element that is being used for this node's toggle.
1954 * @method getToggleEl
1955 * @return {HTMLElement} this node's toggle html element
1957 getToggleEl: function() {
1958 return Dom.get(this.getToggleElId());
1961 * Returns the outer html element for this node's content
1962 * @method getContentEl
1963 * @return {HTMLElement} the element
1965 getContentEl: function() {
1966 return Dom.get(this.contentElId);
1971 * Returns the element that is being used for this node's spacer.
1973 * @return {HTMLElement} this node's spacer html element
1976 getSpacer: function() {
1977 return document.getElementById( this.getSpacerId() ) || {};
1982 getStateText: function() {
1983 if (this.isLoading) {
1984 return this.loadingText;
1985 } else if (this.hasChildren(true)) {
1986 if (this.expanded) {
1987 return this.expandedText;
1989 return this.collapsedText;
1998 * Hides this nodes children (creating them if necessary), changes the toggle style.
2001 collapse: function() {
2002 // Only collapse if currently expanded
2003 if (!this.expanded) { return; }
2005 // fire the collapse event handler
2006 var ret = this.tree.onCollapse(this);
2008 if (false === ret) {
2009 this.logger.log("Collapse was stopped by the abstract onCollapse");
2013 ret = this.tree.fireEvent("collapse", this);
2015 if (false === ret) {
2016 this.logger.log("Collapse was stopped by a custom event handler");
2021 if (!this.getEl()) {
2022 this.expanded = false;
2024 // hide the child div
2025 this.hideChildren();
2026 this.expanded = false;
2031 // this.getSpacer().title = this.getStateText();
2033 ret = this.tree.fireEvent("collapseComplete", this);
2038 * Shows this nodes children (creating them if necessary), changes the
2039 * toggle style, and collapses its siblings if multiExpand is not set.
2042 expand: function(lazySource) {
2043 // Only expand if currently collapsed.
2044 if (this.expanded && !lazySource) {
2050 // When returning from the lazy load handler, expand is called again
2051 // in order to render the new children. The "expand" event already
2052 // fired before fething the new data, so we need to skip it now.
2054 // fire the expand event handler
2055 ret = this.tree.onExpand(this);
2057 if (false === ret) {
2058 this.logger.log("Expand was stopped by the abstract onExpand");
2062 ret = this.tree.fireEvent("expand", this);
2065 if (false === ret) {
2066 this.logger.log("Expand was stopped by the custom event handler");
2070 if (!this.getEl()) {
2071 this.expanded = true;
2075 if (!this.childrenRendered) {
2076 this.logger.log("children not rendered yet");
2077 this.getChildrenEl().innerHTML = this.renderChildren();
2079 this.logger.log("children already rendered");
2082 this.expanded = true;
2086 // this.getSpacer().title = this.getStateText();
2088 // We do an extra check for children here because the lazy
2089 // load feature can expose nodes that have no children.
2091 // if (!this.hasChildren()) {
2092 if (this.isLoading) {
2093 this.expanded = false;
2097 if (! this.multiExpand) {
2098 var sibs = this.getSiblings();
2099 for (var i=0; sibs && i<sibs.length; ++i) {
2100 if (sibs[i] != this && sibs[i].expanded) {
2106 this.showChildren();
2108 ret = this.tree.fireEvent("expandComplete", this);
2111 updateIcon: function() {
2113 var el = this.getToggleEl();
2115 el.className = el.className.replace(/\bygtv(([tl][pmn]h?)|(loading))\b/gi,this.getStyle());
2121 * Returns the css style name for the toggle
2123 * @return {string} the css class for this node's toggle
2125 getStyle: function() {
2126 // this.logger.log("No children, " + " isDyanmic: " + this.isDynamic() + " expanded: " + this.expanded);
2127 if (this.isLoading) {
2128 this.logger.log("returning the loading icon");
2129 return "ygtvloading";
2131 // location top or bottom, middle nodes also get the top style
2132 var loc = (this.nextSibling) ? "t" : "l";
2134 // type p=plus(expand), m=minus(collapase), n=none(no children)
2136 if (this.hasChildren(true) || (this.isDynamic() && !this.getIconMode())) {
2137 // if (this.hasChildren(true)) {
2138 type = (this.expanded) ? "m" : "p";
2141 // this.logger.log("ygtv" + loc + type);
2142 return "ygtv" + loc + type;
2147 * Returns the hover style for the icon
2148 * @return {string} the css class hover state
2149 * @method getHoverStyle
2151 getHoverStyle: function() {
2152 var s = this.getStyle();
2153 if (this.hasChildren(true) && !this.isLoading) {
2160 * Recursively expands all of this node's children.
2163 expandAll: function() {
2164 var l = this.children.length;
2165 for (var i=0;i<l;++i) {
2166 var c = this.children[i];
2167 if (c.isDynamic()) {
2168 this.logger.log("Not supported (lazy load + expand all)");
2170 } else if (! c.multiExpand) {
2171 this.logger.log("Not supported (no multi-expand + expand all)");
2181 * Recursively collapses all of this node's children.
2182 * @method collapseAll
2184 collapseAll: function() {
2185 for (var i=0;i<this.children.length;++i) {
2186 this.children[i].collapse();
2187 this.children[i].collapseAll();
2192 * Configures this node for dynamically obtaining the child data
2193 * when the node is first expanded. Calling it without the callback
2194 * will turn off dynamic load for the node.
2195 * @method setDynamicLoad
2196 * @param fmDataLoader {function} the function that will be used to get the data.
2197 * @param iconMode {int} configures the icon that is displayed when a dynamic
2198 * load node is expanded the first time without children. By default, the
2199 * "collapse" icon will be used. If set to 1, the leaf node icon will be
2202 setDynamicLoad: function(fnDataLoader, iconMode) {
2204 this.dataLoader = fnDataLoader;
2205 this._dynLoad = true;
2207 this.dataLoader = null;
2208 this._dynLoad = false;
2212 this.iconMode = iconMode;
2217 * Evaluates if this node is the root node of the tree
2219 * @return {boolean} true if this is the root node
2221 isRoot: function() {
2222 return (this == this.tree.root);
2226 * Evaluates if this node's children should be loaded dynamically. Looks for
2227 * the property both in this instance and the root node. If the tree is
2228 * defined to load all children dynamically, the data callback function is
2229 * defined in the root node
2231 * @return {boolean} true if this node's children are to be loaded dynamically
2233 isDynamic: function() {
2237 return (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
2238 // this.logger.log("isDynamic: " + lazy);
2244 * Returns the current icon mode. This refers to the way childless dynamic
2245 * load nodes appear (this comes into play only after the initial dynamic
2246 * load request produced no children).
2247 * @method getIconMode
2248 * @return {int} 0 for collapse style, 1 for leaf node style
2250 getIconMode: function() {
2251 return (this.iconMode || this.tree.root.iconMode);
2255 * Checks if this node has children. If this node is lazy-loading and the
2256 * children have not been rendered, we do not know whether or not there
2257 * are actual children. In most cases, we need to assume that there are
2258 * children (for instance, the toggle needs to show the expandable
2259 * presentation state). In other times we want to know if there are rendered
2260 * children. For the latter, "checkForLazyLoad" should be false.
2261 * @method hasChildren
2262 * @param checkForLazyLoad {boolean} should we check for unloaded children?
2263 * @return {boolean} true if this has children or if it might and we are
2264 * checking for this condition.
2266 hasChildren: function(checkForLazyLoad) {
2270 return ( this.children.length > 0 ||
2271 (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) );
2276 * Expands if node is collapsed, collapses otherwise.
2279 toggle: function() {
2280 if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
2281 if (this.expanded) { this.collapse(); } else { this.expand(); }
2286 * Returns the markup for this node and its children.
2288 * @return {string} the markup for this node and its expanded children.
2290 getHtml: function() {
2292 this.childrenRendered = false;
2294 return ['<div class="ygtvitem" id="' , this.getElId() , '">' ,this.getNodeHtml() , this.getChildrenHtml() ,'</div>'].join("");
2298 * Called when first rendering the tree. We always build the div that will
2299 * contain this nodes children, but we don't render the children themselves
2300 * unless this node is expanded.
2301 * @method getChildrenHtml
2302 * @return {string} the children container div html and any expanded children
2305 getChildrenHtml: function() {
2309 sb[sb.length] = '<div class="ygtvchildren" id="' + this.getChildrenElId() + '"';
2311 // This is a workaround for an IE rendering issue, the child div has layout
2312 // in IE, creating extra space if a leaf node is created with the expanded
2313 // property set to true.
2314 if (!this.expanded || !this.hasChildren()) {
2315 sb[sb.length] = ' style="display:none;"';
2317 sb[sb.length] = '>';
2319 // this.logger.log(["index", this.index,
2320 // "hasChildren", this.hasChildren(true),
2321 // "expanded", this.expanded,
2322 // "renderHidden", this.renderHidden,
2323 // "isDynamic", this.isDynamic()]);
2325 // Don't render the actual child node HTML unless this node is expanded.
2326 if ( (this.hasChildren(true) && this.expanded) ||
2327 (this.renderHidden && !this.isDynamic()) ) {
2328 sb[sb.length] = this.renderChildren();
2331 sb[sb.length] = '</div>';
2337 * Generates the markup for the child nodes. This is not done until the node
2339 * @method renderChildren
2340 * @return {string} the html for this node's children
2343 renderChildren: function() {
2345 this.logger.log("rendering children for " + this.index);
2349 if (this.isDynamic() && !this.dynamicLoadComplete) {
2350 this.isLoading = true;
2351 this.tree.locked = true;
2353 if (this.dataLoader) {
2354 this.logger.log("Using dynamic loader defined for this node");
2358 node.dataLoader(node,
2360 node.loadComplete();
2364 } else if (this.tree.root.dataLoader) {
2365 this.logger.log("Using the tree-level dynamic loader");
2369 node.tree.root.dataLoader(node,
2371 node.loadComplete();
2376 this.logger.log("no loader found");
2377 return "Error: data loader not found or not specified.";
2383 return this.completeRender();
2388 * Called when we know we have all the child data.
2389 * @method completeRender
2390 * @return {string} children html
2392 completeRender: function() {
2393 this.logger.log("completeRender: " + this.index + ", # of children: " + this.children.length);
2396 for (var i=0; i < this.children.length; ++i) {
2397 // this.children[i].childrenRendered = false;
2398 sb[sb.length] = this.children[i].getHtml();
2401 this.childrenRendered = true;
2407 * Load complete is the callback function we pass to the data provider
2408 * in dynamic load situations.
2409 * @method loadComplete
2411 loadComplete: function() {
2412 this.logger.log(this.index + " loadComplete, children: " + this.children.length);
2413 this.getChildrenEl().innerHTML = this.completeRender();
2414 this.dynamicLoadComplete = true;
2415 this.isLoading = false;
2417 this.tree.locked = false;
2421 * Returns this node's ancestor at the specified depth.
2422 * @method getAncestor
2423 * @param {int} depth the depth of the ancestor.
2424 * @return {Node} the ancestor
2426 getAncestor: function(depth) {
2427 if (depth >= this.depth || depth < 0) {
2428 this.logger.log("illegal getAncestor depth: " + depth);
2432 var p = this.parent;
2434 while (p.depth > depth) {
2442 * Returns the css class for the spacer at the specified depth for
2443 * this node. If this node's ancestor at the specified depth
2444 * has a next sibling the presentation is different than if it
2445 * does not have a next sibling
2446 * @method getDepthStyle
2447 * @param {int} depth the depth of the ancestor.
2448 * @return {string} the css class for the spacer
2450 getDepthStyle: function(depth) {
2451 return (this.getAncestor(depth).nextSibling) ?
2452 "ygtvdepthcell" : "ygtvblankdepthcell";
2456 * Get the markup for the node. This may be overrided so that we can
2457 * support different types of nodes.
2458 * @method getNodeHtml
2459 * @return {string} The HTML that will render this node.
2461 getNodeHtml: function() {
2462 this.logger.log("Generating html");
2465 sb[sb.length] = '<table id="ygtvtableel' + this.index + '"border="0" cellpadding="0" cellspacing="0" class="ygtvtable ygtvdepth' + this.depth;
2466 if (this.enableHighlight) {
2467 sb[sb.length] = ' ygtv-highlight' + this.highlightState;
2469 if (this.className) {
2470 sb[sb.length] = ' ' + this.className;
2472 sb[sb.length] = '"><tr class="ygtvrow">';
2474 for (var i=0;i<this.depth;++i) {
2475 sb[sb.length] = '<td class="ygtvcell ' + this.getDepthStyle(i) + '"><div class="ygtvspacer"></div></td>';
2479 sb[sb.length] = '<td id="' + this.getToggleElId();
2480 sb[sb.length] = '" class="ygtvcell ';
2481 sb[sb.length] = this.getStyle() ;
2482 sb[sb.length] = '"><a href="#" class="ygtvspacer"> </a></td>';
2485 sb[sb.length] = '<td id="' + this.contentElId;
2486 sb[sb.length] = '" class="ygtvcell ';
2487 sb[sb.length] = this.contentStyle + ' ygtvcontent" ';
2488 sb[sb.length] = (this.nowrap) ? ' nowrap="nowrap" ' : '';
2489 sb[sb.length] = ' >';
2490 sb[sb.length] = this.getContentHtml();
2491 sb[sb.length] = '</td></tr></table>';
2497 * Get the markup for the contents of the node. This is designed to be overrided so that we can
2498 * support different types of nodes.
2499 * @method getContentHtml
2500 * @return {string} The HTML that will render the content of this node.
2502 getContentHtml: function () {
2507 * Regenerates the html for this node and its children. To be used when the
2508 * node is expanded and new children have been added.
2511 refresh: function() {
2512 // this.loadComplete();
2513 this.getChildrenEl().innerHTML = this.completeRender();
2516 var el = this.getToggleEl();
2518 el.className = el.className.replace(/\bygtv[lt][nmp]h*\b/gi,this.getStyle());
2526 * @return {string} string representation of the node
2528 toString: function() {
2529 return this._type + " (" + this.index + ")";
2532 * array of items that had the focus set on them
2533 * so that they can be cleaned when focus is lost
2534 * @property _focusHighlightedItems
2535 * @type Array of DOM elements
2538 _focusHighlightedItems: [],
2540 * DOM element that actually got the browser focus
2541 * @property _focusedItem
2548 * Returns true if there are any elements in the node that can
2549 * accept the real actual browser focus
2550 * @method _canHaveFocus
2551 * @return {boolean} success
2554 _canHaveFocus: function() {
2555 return this.getEl().getElementsByTagName('a').length > 0;
2558 * Removes the focus of previously selected Node
2559 * @method _removeFocus
2562 _removeFocus:function () {
2563 if (this._focusedItem) {
2564 Event.removeListener(this._focusedItem,'blur');
2565 this._focusedItem = null;
2568 while ((el = this._focusHighlightedItems.shift())) { // yes, it is meant as an assignment, really
2569 Dom.removeClass(el,YAHOO.widget.TreeView.FOCUS_CLASS_NAME );
2573 * Sets the focus on the node element.
2574 * It will only be able to set the focus on nodes that have anchor elements in it.
2575 * Toggle or branch icons have anchors and can be focused on.
2576 * If will fail in nodes that have no anchor
2578 * @return {boolean} success
2580 focus: function () {
2581 var focused = false, self = this;
2583 if (this.tree.currentFocus) {
2584 this.tree.currentFocus._removeFocus();
2587 var expandParent = function (node) {
2589 expandParent(node.parent);
2590 node.parent.expand();
2597 return /ygtv(([tl][pmn]h?)|(content))/.test(el.className);
2600 self.getEl().firstChild ,
2602 Dom.addClass(el, YAHOO.widget.TreeView.FOCUS_CLASS_NAME );
2604 var aEl = el.getElementsByTagName('a');
2608 self._focusedItem = aEl;
2609 Event.on(aEl,'blur',function () {
2610 //console.log('f1');
2611 self.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null});
2612 self.tree.currentFocus = null;
2613 self._removeFocus();
2618 self._focusHighlightedItems.push(el);
2622 //console.log('f2');
2623 this.tree.fireEvent('focusChanged',{oldNode:this.tree.currentFocus,newNode:this});
2624 this.tree.currentFocus = this;
2626 //console.log('f3');
2627 this.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null});
2628 this.tree.currentFocus = null;
2629 this._removeFocus();
2635 * Count of nodes in a branch
2636 * @method getNodeCount
2637 * @return {int} number of nodes in the branch
2639 getNodeCount: function() {
2640 for (var i = 0, count = 0;i< this.children.length;i++) {
2641 count += this.children[i].getNodeCount();
2647 * Returns an object which could be used to build a tree out of this node and its children.
2648 * It can be passed to the tree constructor to reproduce this node as a tree.
2649 * It will return false if the node or any children loads dynamically, regardless of whether it is loaded or not.
2650 * @method getNodeDefinition
2651 * @return {Object | false} definition of the tree or false if the node or any children is defined as dynamic
2653 getNodeDefinition: function() {
2655 if (this.isDynamic()) { return false; }
2657 var def, defs = Lang.merge(this.data), children = [];
2661 if (this.expanded) {defs.expanded = this.expanded; }
2662 if (!this.multiExpand) { defs.multiExpand = this.multiExpand; }
2663 if (!this.renderHidden) { defs.renderHidden = this.renderHidden; }
2664 if (!this.hasIcon) { defs.hasIcon = this.hasIcon; }
2665 if (this.nowrap) { defs.nowrap = this.nowrap; }
2666 if (this.className) { defs.className = this.className; }
2667 if (this.editable) { defs.editable = this.editable; }
2668 if (this.enableHighlight) { defs.enableHighlight = this.enableHighlight; }
2669 if (this.highlightState) { defs.highlightState = this.highlightState; }
2670 if (this.propagateHighlightUp) { defs.propagateHighlightUp = this.propagateHighlightUp; }
2671 if (this.propagateHighlightDown) { defs.propagateHighlightDown = this.propagateHighlightDown; }
2672 defs.type = this._type;
2676 for (var i = 0; i < this.children.length;i++) {
2677 def = this.children[i].getNodeDefinition();
2678 if (def === false) { return false;}
2681 if (children.length) { defs.children = children; }
2687 * Generates the link that will invoke this node's toggle method
2688 * @method getToggleLink
2689 * @return {string} the javascript url for toggling this node
2691 getToggleLink: function() {
2692 return 'return false;';
2696 * Sets the value of property for this node and all loaded descendants.
2697 * Only public and defined properties can be set, not methods.
2698 * Values for unknown properties will be assigned to the refNode.data object
2699 * @method setNodesProperty
2700 * @param name {string} Name of the property to be set
2701 * @param value {any} value to be set
2702 * @param refresh {boolean} if present and true, it does a refresh
2704 setNodesProperty: function(name, value, refresh) {
2705 if (name.charAt(0) != '_' && !Lang.isUndefined(this[name]) && !Lang.isFunction(this[name]) ) {
2708 this.data[name] = value;
2710 for (var i = 0; i < this.children.length;i++) {
2711 this.children[i].setNodesProperty(name,value);
2718 * Toggles the highlighted state of a Node
2719 * @method toggleHighlight
2721 toggleHighlight: function() {
2722 if (this.enableHighlight) {
2723 // unhighlights only if fully highligthed. For not or partially highlighted it will highlight
2724 if (this.highlightState == 1) {
2733 * Turns highlighting on node.
2735 * @param _silent {boolean} optional, don't fire the highlightEvent
2737 highlight: function(_silent) {
2738 if (this.enableHighlight) {
2739 if (this.tree.singleNodeHighlight) {
2740 if (this.tree._currentlyHighlighted) {
2741 this.tree._currentlyHighlighted.unhighlight();
2743 this.tree._currentlyHighlighted = this;
2745 this.highlightState = 1;
2746 this._setHighlightClassName();
2747 if (this.propagateHighlightDown) {
2748 for (var i = 0;i < this.children.length;i++) {
2749 this.children[i].highlight(true);
2752 if (this.propagateHighlightUp) {
2754 this.parent._childrenHighlighted();
2758 this.tree.fireEvent('highlightEvent',this);
2763 * Turns highlighting off a node.
2764 * @method unhighlight
2765 * @param _silent {boolean} optional, don't fire the highlightEvent
2767 unhighlight: function(_silent) {
2768 if (this.enableHighlight) {
2769 this.highlightState = 0;
2770 this._setHighlightClassName();
2771 if (this.propagateHighlightDown) {
2772 for (var i = 0;i < this.children.length;i++) {
2773 this.children[i].unhighlight(true);
2776 if (this.propagateHighlightUp) {
2778 this.parent._childrenHighlighted();
2782 this.tree.fireEvent('highlightEvent',this);
2787 * Checks whether all or part of the children of a node are highlighted and
2788 * sets the node highlight to full, none or partial highlight.
2789 * If set to propagate it will further call the parent
2790 * @method _childrenHighlighted
2793 _childrenHighlighted: function() {
2794 var yes = false, no = false;
2795 if (this.enableHighlight) {
2796 for (var i = 0;i < this.children.length;i++) {
2797 switch(this.children[i].highlightState) {
2810 this.highlightState = 2;
2812 this.highlightState = 1;
2814 this.highlightState = 0;
2816 this._setHighlightClassName();
2817 if (this.propagateHighlightUp) {
2819 this.parent._childrenHighlighted();
2826 * Changes the classNames on the toggle and content containers to reflect the current highlighting
2827 * @method _setHighlightClassName
2830 _setHighlightClassName: function() {
2831 var el = Dom.get('ygtvtableel' + this.index);
2833 el.className = el.className.replace(/\bygtv-highlight\d\b/gi,'ygtv-highlight' + this.highlightState);
2839 YAHOO.augment(YAHOO.widget.Node, YAHOO.util.EventProvider);
2842 * A custom YAHOO.widget.Node that handles the unique nature of
2843 * the virtual, presentationless root node.
2844 * @namespace YAHOO.widget
2846 * @extends YAHOO.widget.Node
2847 * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to
2850 YAHOO.widget.RootNode = function(oTree) {
2851 // Initialize the node with null params. The root node is a
2852 // special case where the node has no presentation. So we have
2853 // to alter the standard properties a bit.
2854 this.init(null, null, true);
2857 * For the root node, we get the tree reference from as a param
2858 * to the constructor instead of from the parent element.
2863 YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, {
2870 * @default "RootNode"
2874 // overrides YAHOO.widget.Node
2875 getNodeHtml: function() {
2879 toString: function() {
2883 loadComplete: function() {
2888 * Count of nodes in tree.
2889 * It overrides Nodes.getNodeCount because the root node should not be counted.
2890 * @method getNodeCount
2891 * @return {int} number of nodes in the tree
2893 getNodeCount: function() {
2894 for (var i = 0, count = 0;i< this.children.length;i++) {
2895 count += this.children[i].getNodeCount();
2901 * Returns an object which could be used to build a tree out of this node and its children.
2902 * It can be passed to the tree constructor to reproduce this node as a tree.
2903 * Since the RootNode is automatically created by treeView,
2904 * its own definition is excluded from the returned node definition
2905 * which only contains its children.
2906 * @method getNodeDefinition
2907 * @return {Object | false} definition of the tree or false if any child node is defined as dynamic
2909 getNodeDefinition: function() {
2911 for (var def, defs = [], i = 0; i < this.children.length;i++) {
2912 def = this.children[i].getNodeDefinition();
2913 if (def === false) { return false;}
2919 collapse: function() {},
2920 expand: function() {},
2921 getSiblings: function() { return null; },
2922 focus: function () {}
2926 var Dom = YAHOO.util.Dom,
2928 Event = YAHOO.util.Event;
2930 * The default node presentation. The first parameter should be
2931 * either a string that will be used as the node's label, or an object
2932 * that has at least a string property called label. By default, clicking the
2933 * label will toggle the expanded/collapsed state of the node. By
2934 * setting the href property of the instance, this behavior can be
2935 * changed so that the label will go to the specified href.
2936 * @namespace YAHOO.widget
2938 * @extends YAHOO.widget.Node
2940 * @param oData {object} a string or object containing the data that will
2941 * be used to render this node.
2942 * Providing a string is the same as providing an object with a single property named label.
2943 * All values in the oData will be used to set equally named properties in the node
2944 * as long as the node does have such properties, they are not undefined, private or functions.
2945 * All attributes are made available in noderef.data, which
2946 * can be used to store custom attributes. TreeView.getNode(s)ByProperty
2947 * can be used to retrieve a node by one of the attributes.
2948 * @param oParent {YAHOO.widget.Node} this node's parent node
2949 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
2951 YAHOO.widget.TextNode = function(oData, oParent, expanded) {
2954 if (Lang.isString(oData)) {
2955 oData = { label: oData };
2957 this.init(oData, oParent, expanded);
2958 this.setUpLabel(oData);
2961 this.logger = new YAHOO.widget.LogWriter(this.toString());
2964 YAHOO.extend(YAHOO.widget.TextNode, YAHOO.widget.Node, {
2967 * The CSS class for the label href. Defaults to ygtvlabel, but can be
2968 * overridden to provide a custom presentation for a specific node.
2969 * @property labelStyle
2972 labelStyle: "ygtvlabel",
2975 * The derived element id of the label for this node
2976 * @property labelElId
2982 * The text for the label. It is assumed that the oData parameter will
2983 * either be a string that will be used as the label, or an object that
2984 * has a property called "label" that we will use.
2991 * The text for the title (tooltip) for the label element
2998 * The href for the node's label. If one is not specified, the href will
2999 * be set so that it toggles the node.
3006 * The label href target, defaults to current window
3017 * @default "TextNode"
3023 * Sets up the node label
3024 * @method setUpLabel
3025 * @param oData string containing the label, or an object with a label property
3027 setUpLabel: function(oData) {
3029 if (Lang.isString(oData)) {
3035 this.labelStyle = oData.style;
3039 this.label = oData.label;
3041 this.labelElId = "ygtvlabelel" + this.index;
3046 * Returns the label element
3047 * @for YAHOO.widget.TextNode
3048 * @method getLabelEl
3049 * @return {object} the element
3051 getLabelEl: function() {
3052 return Dom.get(this.labelElId);
3055 // overrides YAHOO.widget.Node
3056 getContentHtml: function() {
3058 sb[sb.length] = this.href?'<a':'<span';
3059 sb[sb.length] = ' id="' + this.labelElId + '"';
3060 sb[sb.length] = ' class="' + this.labelStyle + '"';
3062 sb[sb.length] = ' href="' + this.href + '"';
3063 sb[sb.length] = ' target="' + this.target + '"';
3066 sb[sb.length] = ' title="' + this.title + '"';
3068 sb[sb.length] = ' >';
3069 sb[sb.length] = this.label;
3070 sb[sb.length] = this.href?'</a>':'</span>';
3077 * Returns an object which could be used to build a tree out of this node and its children.
3078 * It can be passed to the tree constructor to reproduce this node as a tree.
3079 * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not.
3080 * @method getNodeDefinition
3081 * @return {Object | false} definition of the tree or false if this node or any descendant is defined as dynamic
3083 getNodeDefinition: function() {
3084 var def = YAHOO.widget.TextNode.superclass.getNodeDefinition.call(this);
3085 if (def === false) { return false; }
3087 // Node specific properties
3088 def.label = this.label;
3089 if (this.labelStyle != 'ygtvlabel') { def.style = this.labelStyle; }
3090 if (this.title) { def.title = this.title; }
3091 if (this.href) { def.href = this.href; }
3092 if (this.target != '_self') { def.target = this.target; }
3098 toString: function() {
3099 return YAHOO.widget.TextNode.superclass.toString.call(this) + ": " + this.label;
3103 onLabelClick: function() {
3106 refresh: function() {
3107 YAHOO.widget.TextNode.superclass.refresh.call(this);
3108 var label = this.getLabelEl();
3109 label.innerHTML = this.label;
3110 if (label.tagName.toUpperCase() == 'A') {
3111 label.href = this.href;
3112 label.target = this.target;
3122 * A menu-specific implementation that differs from TextNode in that only
3123 * one sibling can be expanded at a time.
3124 * @namespace YAHOO.widget
3126 * @extends YAHOO.widget.TextNode
3127 * @param oData {object} a string or object containing the data that will
3128 * be used to render this node.
3129 * Providing a string is the same as providing an object with a single property named label.
3130 * All values in the oData will be used to set equally named properties in the node
3131 * as long as the node does have such properties, they are not undefined, private or functions.
3132 * All attributes are made available in noderef.data, which
3133 * can be used to store custom attributes. TreeView.getNode(s)ByProperty
3134 * can be used to retrieve a node by one of the attributes.
3135 * @param oParent {YAHOO.widget.Node} this node's parent node
3136 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
3139 YAHOO.widget.MenuNode = function(oData, oParent, expanded) {
3140 YAHOO.widget.MenuNode.superclass.constructor.call(this,oData,oParent,expanded);
3143 * Menus usually allow only one branch to be open at a time.
3145 this.multiExpand = false;
3149 YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, {
3155 * @default "MenuNode"
3161 var Dom = YAHOO.util.Dom,
3163 Event = YAHOO.util.Event;
3166 * This implementation takes either a string or object for the
3167 * oData argument. If is it a string, it will use it for the display
3168 * of this node (and it can contain any html code). If the parameter
3169 * is an object,it looks for a parameter called "html" that will be
3170 * used for this node's display.
3171 * @namespace YAHOO.widget
3173 * @extends YAHOO.widget.Node
3175 * @param oData {object} a string or object containing the data that will
3176 * be used to render this node.
3177 * Providing a string is the same as providing an object with a single property named html.
3178 * All values in the oData will be used to set equally named properties in the node
3179 * as long as the node does have such properties, they are not undefined, private or functions.
3180 * All other attributes are made available in noderef.data, which
3181 * can be used to store custom attributes. TreeView.getNode(s)ByProperty
3182 * can be used to retrieve a node by one of the attributes.
3183 * @param oParent {YAHOO.widget.Node} this node's parent node
3184 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
3185 * @param hasIcon {boolean} specifies whether or not leaf nodes should
3186 * be rendered with or without a horizontal line line and/or toggle icon. If the icon
3187 * is not displayed, the content fills the space it would have occupied.
3188 * This option operates independently of the leaf node presentation logic
3189 * for dynamic nodes.
3190 * (deprecated; use oData.hasIcon)
3192 YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) {
3194 this.init(oData, oParent, expanded);
3195 this.initContent(oData, hasIcon);
3199 YAHOO.extend(YAHOO.widget.HTMLNode, YAHOO.widget.Node, {
3202 * The CSS class for the html content container. Defaults to ygtvhtml, but
3203 * can be overridden to provide a custom presentation for a specific node.
3204 * @property contentStyle
3207 contentStyle: "ygtvhtml",
3211 * The HTML content to use for this node's display
3222 * @default "HTMLNode"
3227 * Sets up the node label
3228 * @property initContent
3229 * @param oData {object} An html string or object containing an html property
3230 * @param hasIcon {boolean} determines if the node will be rendered with an
3233 initContent: function(oData, hasIcon) {
3234 this.setHtml(oData);
3235 this.contentElId = "ygtvcontentel" + this.index;
3236 if (!Lang.isUndefined(hasIcon)) { this.hasIcon = hasIcon; }
3238 this.logger = new YAHOO.widget.LogWriter(this.toString());
3242 * Synchronizes the node.data, node.html, and the node's content
3244 * @param o {object} An html string or object containing an html property
3246 setHtml: function(o) {
3248 this.html = (typeof o === "string") ? o : o.html;
3250 var el = this.getContentEl();
3252 el.innerHTML = this.html;
3257 // overrides YAHOO.widget.Node
3258 getContentHtml: function() {
3263 * Returns an object which could be used to build a tree out of this node and its children.
3264 * It can be passed to the tree constructor to reproduce this node as a tree.
3265 * It will return false if any node loads dynamically, regardless of whether it is loaded or not.
3266 * @method getNodeDefinition
3267 * @return {Object | false} definition of the tree or false if any node is defined as dynamic
3269 getNodeDefinition: function() {
3270 var def = YAHOO.widget.HTMLNode.superclass.getNodeDefinition.call(this);
3271 if (def === false) { return false; }
3272 def.html = this.html;
3279 var Dom = YAHOO.util.Dom,
3281 Event = YAHOO.util.Event,
3282 Calendar = YAHOO.widget.Calendar;
3285 * A Date-specific implementation that differs from TextNode in that it uses
3286 * YAHOO.widget.Calendar as an in-line editor, if available
3287 * If Calendar is not available, it behaves as a plain TextNode.
3288 * @namespace YAHOO.widget
3290 * @extends YAHOO.widget.TextNode
3291 * @param oData {object} a string or object containing the data that will
3292 * be used to render this node.
3293 * Providing a string is the same as providing an object with a single property named label.
3294 * All values in the oData will be used to set equally named properties in the node
3295 * as long as the node does have such properties, they are not undefined, private nor functions.
3296 * All attributes are made available in noderef.data, which
3297 * can be used to store custom attributes. TreeView.getNode(s)ByProperty
3298 * can be used to retrieve a node by one of the attributes.
3299 * @param oParent {YAHOO.widget.Node} this node's parent node
3300 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
3303 YAHOO.widget.DateNode = function(oData, oParent, expanded) {
3304 YAHOO.widget.DateNode.superclass.constructor.call(this,oData, oParent, expanded);
3307 YAHOO.extend(YAHOO.widget.DateNode, YAHOO.widget.TextNode, {
3314 * @default "DateNode"
3319 * Configuration object for the Calendar editor, if used.
3320 * See <a href="http://developer.yahoo.com/yui/calendar/#internationalization">http://developer.yahoo.com/yui/calendar/#internationalization</a>
3321 * @property calendarConfig
3323 calendarConfig: null,
3328 * If YAHOO.widget.Calendar is available, it will pop up a Calendar to enter a new date. Otherwise, it falls back to a plain <input> textbox
3329 * @method fillEditorContainer
3330 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3333 fillEditorContainer: function (editorData) {
3335 var cal, container = editorData.inputContainer;
3337 if (Lang.isUndefined(Calendar)) {
3338 Dom.replaceClass(editorData.editorPanel,'ygtv-edit-DateNode','ygtv-edit-TextNode');
3339 YAHOO.widget.DateNode.superclass.fillEditorContainer.call(this, editorData);
3343 if (editorData.nodeType != this._type) {
3344 editorData.nodeType = this._type;
3345 editorData.saveOnEnter = false;
3347 editorData.node.destroyEditorContents(editorData);
3349 editorData.inputObject = cal = new Calendar(container.appendChild(document.createElement('div')));
3350 if (this.calendarConfig) {
3351 cal.cfg.applyConfig(this.calendarConfig,true);
3352 cal.cfg.fireQueue();
3354 cal.selectEvent.subscribe(function () {
3355 this.tree._closeEditor(true);
3358 cal = editorData.inputObject;
3361 cal.cfg.setProperty("selected",this.label, false);
3363 var delim = cal.cfg.getProperty('DATE_FIELD_DELIMITER');
3364 var pageDate = this.label.split(delim);
3365 cal.cfg.setProperty('pagedate',pageDate[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] + delim + pageDate[cal.cfg.getProperty('MDY_YEAR_POSITION') -1]);
3366 cal.cfg.fireQueue();
3369 cal.oDomContainer.focus();
3372 * Saves the date entered in the editor into the DateNode label property and displays it.
3373 * Overrides Node.saveEditorValue
3374 * @method saveEditorValue
3375 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3377 saveEditorValue: function (editorData) {
3378 var node = editorData.node,
3379 validator = node.tree.validator,
3381 if (Lang.isUndefined(Calendar)) {
3382 value = editorData.inputElement.value;
3384 var cal = editorData.inputObject,
3385 date = cal.getSelectedDates()[0],
3388 dd[cal.cfg.getProperty('MDY_DAY_POSITION') -1] = date.getDate();
3389 dd[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] = date.getMonth() + 1;
3390 dd[cal.cfg.getProperty('MDY_YEAR_POSITION') -1] = date.getFullYear();
3391 value = dd.join(cal.cfg.getProperty('DATE_FIELD_DELIMITER'));
3393 if (Lang.isFunction(validator)) {
3394 value = validator(value,node.label,node);
3395 if (Lang.isUndefined(value)) { return false; }
3399 node.getLabelEl().innerHTML = value;
3402 * Returns an object which could be used to build a tree out of this node and its children.
3403 * It can be passed to the tree constructor to reproduce this node as a tree.
3404 * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not.
3405 * @method getNodeDefinition
3406 * @return {Object | false} definition of the node or false if this node or any descendant is defined as dynamic
3408 getNodeDefinition: function() {
3409 var def = YAHOO.widget.DateNode.superclass.getNodeDefinition.call(this);
3410 if (def === false) { return false; }
3411 if (this.calendarConfig) { def.calendarConfig = this.calendarConfig; }
3419 var Dom = YAHOO.util.Dom,
3421 Event = YAHOO.util.Event,
3422 TV = YAHOO.widget.TreeView,
3423 TVproto = TV.prototype;
3426 * An object to store information used for in-line editing
3427 * for all Nodes of all TreeViews. It contains:
3429 * <li>active {boolean}, whether there is an active cell editor </li>
3430 * <li>whoHasIt {YAHOO.widget.TreeView} TreeView instance that is currently using the editor</li>
3431 * <li>nodeType {string} value of static Node._type property, allows reuse of input element if node is of the same type.</li>
3432 * <li>editorPanel {HTMLelement (<div>)} element holding the in-line editor</li>
3433 * <li>inputContainer {HTMLelement (<div>)} element which will hold the type-specific input element(s) to be filled by the fillEditorContainer method</li>
3434 * <li>buttonsContainer {HTMLelement (<div>)} element which holds the <button> elements for Ok/Cancel. If you don't want any of the buttons, hide it via CSS styles, don't destroy it</li>
3435 * <li>node {YAHOO.widget.Node} reference to the Node being edited</li>
3436 * <li>saveOnEnter {boolean}, whether the Enter key should be accepted as a Save command (Esc. is always taken as Cancel), disable for multi-line input elements </li>
3438 * Editors are free to use this object to store additional data.
3439 * @property editorData
3441 * @for YAHOO.widget.TreeView
3445 whoHasIt:null, // which TreeView has it
3448 inputContainer:null,
3449 buttonsContainer:null,
3450 node:null, // which Node is being edited
3452 // Each node type is free to add its own properties to this as it sees fit.
3456 * Validator function for edited data, called from the TreeView instance scope,
3457 * receives the arguments (newValue, oldValue, nodeInstance)
3458 * and returns either the validated (or type-converted) value or undefined.
3459 * An undefined return will prevent the editor from closing
3460 * @property validator
3462 * @for YAHOO.widget.TreeView
3464 TVproto.validator = null;
3467 * Entry point of the editing plug-in.
3468 * TreeView will call this method if it exists when a node label is clicked
3469 * @method _nodeEditing
3470 * @param node {YAHOO.widget.Node} the node to be edited
3471 * @return {Boolean} true to indicate that the node is editable and prevent any further bubbling of the click.
3472 * @for YAHOO.widget.TreeView
3477 TVproto._nodeEditing = function (node) {
3478 if (node.fillEditorContainer && node.editable) {
3479 var ed, topLeft, buttons, button, editorData = TV.editorData;
3480 editorData.active = true;
3481 editorData.whoHasIt = this;
3482 if (!editorData.nodeType) {
3483 editorData.editorPanel = ed = document.body.appendChild(document.createElement('div'));
3484 Dom.addClass(ed,'ygtv-label-editor');
3486 buttons = editorData.buttonsContainer = ed.appendChild(document.createElement('div'));
3487 Dom.addClass(buttons,'ygtv-button-container');
3488 button = buttons.appendChild(document.createElement('button'));
3489 Dom.addClass(button,'ygtvok');
3490 button.innerHTML = ' ';
3491 button = buttons.appendChild(document.createElement('button'));
3492 Dom.addClass(button,'ygtvcancel');
3493 button.innerHTML = ' ';
3494 Event.on(buttons, 'click', function (ev) {
3495 this.logger.log('click on editor');
3496 var target = Event.getTarget(ev);
3497 var node = TV.editorData.node;
3498 if (Dom.hasClass(target,'ygtvok')) {
3499 node.logger.log('ygtvok');
3500 Event.stopEvent(ev);
3501 this._closeEditor(true);
3503 if (Dom.hasClass(target,'ygtvcancel')) {
3504 node.logger.log('ygtvcancel');
3505 Event.stopEvent(ev);
3506 this._closeEditor(false);
3510 editorData.inputContainer = ed.appendChild(document.createElement('div'));
3511 Dom.addClass(editorData.inputContainer,'ygtv-input');
3513 Event.on(ed,'keydown',function (ev) {
3514 var editorData = TV.editorData,
3515 KEY = YAHOO.util.KeyListener.KEY;
3516 switch (ev.keyCode) {
3518 this.logger.log('ENTER');
3519 Event.stopEvent(ev);
3520 if (editorData.saveOnEnter) {
3521 this._closeEditor(true);
3525 this.logger.log('ESC');
3526 Event.stopEvent(ev);
3527 this._closeEditor(false);
3535 ed = editorData.editorPanel;
3537 editorData.node = node;
3538 if (editorData.nodeType) {
3539 Dom.removeClass(ed,'ygtv-edit-' + editorData.nodeType);
3541 Dom.addClass(ed,' ygtv-edit-' + node._type);
3542 topLeft = Dom.getXY(node.getContentEl());
3543 Dom.setStyle(ed,'left',topLeft[0] + 'px');
3544 Dom.setStyle(ed,'top',topLeft[1] + 'px');
3545 Dom.setStyle(ed,'display','block');
3547 node.fillEditorContainer(editorData);
3549 return true; // If inline editor available, don't do anything else.
3554 * Method to be associated with an event (clickEvent, dblClickEvent or enterKeyPressed) to pop up the contents editor
3555 * It calls the corresponding node editNode method.
3556 * @method onEventEditNode
3557 * @param oArgs {object} Object passed as arguments to TreeView event listeners
3558 * @for YAHOO.widget.TreeView
3561 TVproto.onEventEditNode = function (oArgs) {
3562 if (oArgs instanceof YAHOO.widget.Node) {
3564 } else if (oArgs.node instanceof YAHOO.widget.Node) {
3565 oArgs.node.editNode();
3570 * Method to be called when the inline editing is finished and the editor is to be closed
3571 * @method _closeEditor
3572 * @param save {Boolean} true if the edited value is to be saved, false if discarded
3574 * @for YAHOO.widget.TreeView
3577 TVproto._closeEditor = function (save) {
3578 var ed = TV.editorData,
3582 close = ed.node.saveEditorValue(ed) !== false;
3585 Dom.setStyle(ed.editorPanel,'display','none');
3592 * Entry point for TreeView's destroy method to destroy whatever the editing plug-in has created
3593 * @method _destroyEditor
3595 * @for YAHOO.widget.TreeView
3597 TVproto._destroyEditor = function() {
3598 var ed = TV.editorData;
3599 if (ed && ed.nodeType && (!ed.active || ed.whoHasIt === this)) {
3600 Event.removeListener(ed.editorPanel,'keydown');
3601 Event.removeListener(ed.buttonContainer,'click');
3602 ed.node.destroyEditorContents(ed);
3603 document.body.removeChild(ed.editorPanel);
3604 ed.nodeType = ed.editorPanel = ed.inputContainer = ed.buttonsContainer = ed.whoHasIt = ed.node = null;
3609 var Nproto = YAHOO.widget.Node.prototype;
3612 * Signals if the label is editable. (Ignored on TextNodes with href set.)
3613 * @property editable
3615 * @for YAHOO.widget.Node
3617 Nproto.editable = false;
3620 * pops up the contents editor, if there is one and the node is declared editable
3622 * @for YAHOO.widget.Node
3625 Nproto.editNode = function () {
3626 this.tree._nodeEditing(this);
3632 /** Placeholder for a function that should provide the inline node label editor.
3633 * Leaving it set to null will indicate that this node type is not editable.
3634 * It should be overridden by nodes that provide inline editing.
3635 * The Node-specific editing element (input box, textarea or whatever) should be inserted into editorData.inputContainer.
3636 * @method fillEditorContainer
3637 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3639 * @for YAHOO.widget.Node
3641 Nproto.fillEditorContainer = null;
3645 * Node-specific destroy function to empty the contents of the inline editor panel
3646 * This function is the worst case alternative that will purge all possible events and remove the editor contents
3647 * Method Event.purgeElement is somewhat costly so if it can be replaced by specifc Event.removeListeners, it is better to do so.
3648 * @method destroyEditorContents
3649 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3650 * @for YAHOO.widget.Node
3652 Nproto.destroyEditorContents = function (editorData) {
3653 // In the worst case, if the input editor (such as the Calendar) has no destroy method
3654 // we can only try to remove all possible events on it.
3655 Event.purgeElement(editorData.inputContainer,true);
3656 editorData.inputContainer.innerHTML = '';
3660 * Saves the value entered into the editor.
3661 * Should be overridden by each node type
3662 * @method saveEditorValue
3663 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3664 * @return a return of exactly false will prevent the editor from closing
3665 * @for YAHOO.widget.Node
3667 Nproto.saveEditorValue = function (editorData) {
3670 var TNproto = YAHOO.widget.TextNode.prototype;
3675 * Places an <input> textbox in the input container and loads the label text into it
3676 * @method fillEditorContainer
3677 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3679 * @for YAHOO.widget.TextNode
3681 TNproto.fillEditorContainer = function (editorData) {
3684 // If last node edited is not of the same type as this one, delete it and fill it with our editor
3685 if (editorData.nodeType != this._type) {
3686 editorData.nodeType = this._type;
3687 editorData.saveOnEnter = true;
3688 editorData.node.destroyEditorContents(editorData);
3690 editorData.inputElement = input = editorData.inputContainer.appendChild(document.createElement('input'));
3693 // if the last node edited was of the same time, reuse the input element.
3694 input = editorData.inputElement;
3697 input.value = this.label;
3703 * Saves the value entered in the editor into the TextNode label property and displays it
3704 * Overrides Node.saveEditorValue
3705 * @method saveEditorValue
3706 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3707 * @for YAHOO.widget.TextNode
3709 TNproto.saveEditorValue = function (editorData) {
3710 var node = editorData.node,
3711 value = editorData.inputElement.value,
3712 validator = node.tree.validator;
3714 if (Lang.isFunction(validator)) {
3715 value = validator(value,node.label,node);
3716 if (Lang.isUndefined(value)) { return false; }
3719 node.getLabelEl().innerHTML = value;
3723 * Destroys the contents of the inline editor panel
3724 * Overrides Node.destroyEditorContent
3725 * Since we didn't set any event listeners on this inline editor, it is more efficient to avoid the generic method in Node
3726 * @method destroyEditorContents
3727 * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information
3728 * @for YAHOO.widget.TextNode
3730 TNproto.destroyEditorContents = function (editorData) {
3731 editorData.inputContainer.innerHTML = '';
3734 YAHOO.register("treeview", YAHOO.widget.TreeView, {version: "2.7.0", build: "1799"});