Index: openacs-4/packages/ajaxhelper/www/resources/yui/treeview/treeview.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/ajaxhelper/www/resources/yui/treeview/treeview.js,v diff -u -r1.3 -r1.4 --- openacs-4/packages/ajaxhelper/www/resources/yui/treeview/treeview.js 8 Sep 2007 14:22:09 -0000 1.3 +++ openacs-4/packages/ajaxhelper/www/resources/yui/treeview/treeview.js 9 Apr 2009 23:15:50 -0000 1.4 @@ -1,15 +1,23 @@ /* -Copyright (c) 2007, Yahoo! Inc. All rights reserved. +Copyright (c) 2009, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.net/yui/license.txt -version: 2.3.0 +version: 2.7.0 */ +(function () { + var Dom = YAHOO.util.Dom, + Event = YAHOO.util.Event, + Lang = YAHOO.lang, + Widget = YAHOO.widget; + + + /** * The treeview widget is a generic tree building tool. * @module treeview * @title TreeView Widget * @requires yahoo, event - * @optional animation + * @optional animation, json * @namespace YAHOO.widget */ @@ -19,15 +27,26 @@ * @class TreeView * @uses YAHOO.util.EventProvider * @constructor - * @param {string|HTMLElement} id The id of the element, or the element - * itself that the tree will be inserted into. + * @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 + * @param {Array|object|string} oConfig (optional) An array containing the definition of the tree. (see buildTreeFromObject) + * */ -YAHOO.widget.TreeView = function(id) { +YAHOO.widget.TreeView = function(id, oConfig) { if (id) { this.init(id); } + if (oConfig) { + if (!Lang.isArray(oConfig)) { + oConfig = [oConfig]; + } + this.buildTreeFromObject(oConfig); + } else if (Lang.trim(this._el.innerHTML)) { + this.buildTreeFromMarkup(id); + } }; -YAHOO.widget.TreeView.prototype = { +var TV = Widget.TreeView; +TV.prototype = { + /** * The id of tree container element * @property id @@ -39,6 +58,7 @@ * The host element for this tree * @property _el * @private + * @type HTMLelement */ _el: null, @@ -91,15 +111,56 @@ maxAnim: 2, /** + * Whether there is any subscriber to dblClickEvent + * @property _hasDblClickSubscriber + * @type boolean + * @private + */ + _hasDblClickSubscriber: false, + + /** + * Stores the timer used to check for double clicks + * @property _dblClickTimer + * @type window.timer object + * @private + */ + _dblClickTimer: null, + + /** + * A reference to the Node currently having the focus or null if none. + * @property currentFocus + * @type YAHOO.widget.Node + */ + currentFocus: null, + + /** + * If true, only one Node can be highlighted at a time + * @property singleNodeHighlight + * @type boolean + * @default false + */ + + singleNodeHighlight: false, + + /** + * A reference to the Node that is currently highlighted. + * It is only meaningful if singleNodeHighlight is enabled + * @property _currentlyHighlighted + * @type YAHOO.widget.Node + * @default null + * @private + */ + + _currentlyHighlighted: null, + + /** * Sets up the animation for expanding children * @method setExpandAnim * @param {string} type the type of animation (acceptable values defined * in YAHOO.widget.TVAnim) */ setExpandAnim: function(type) { - if (YAHOO.widget.TVAnim.isValid(type)) { - this._expandAnim = type; - } + this._expandAnim = (Widget.TVAnim.isValid(type)) ? type : null; }, /** @@ -109,9 +170,7 @@ * YAHOO.widget.TVAnim) */ setCollapseAnim: function(type) { - if (YAHOO.widget.TVAnim.isValid(type)) { - this._collapseAnim = type; - } + this._collapseAnim = (Widget.TVAnim.isValid(type)) ? type : null; }, /** @@ -127,7 +186,7 @@ if (this._expandAnim && this._animCount < this.maxAnim) { // this.locked = true; var tree = this; - var a = YAHOO.widget.TVAnim.getAnim(this._expandAnim, el, + var a = Widget.TVAnim.getAnim(this._expandAnim, el, function() { tree.expandComplete(node); }); if (a) { ++this._animCount; @@ -157,7 +216,7 @@ if (this._collapseAnim && this._animCount < this.maxAnim) { // this.locked = true; var tree = this; - var a = YAHOO.widget.TVAnim.getAnim(this._collapseAnim, el, + var a = Widget.TVAnim.getAnim(this._collapseAnim, el, function() { tree.collapseComplete(node); }); if (a) { ++this._animCount; @@ -207,15 +266,10 @@ * @private */ init: function(id) { + this._el = Dom.get(id); + this.id = Dom.generateId(this._el,"yui-tv-auto-id-"); - this.id = id; - - if ("string" !== typeof id) { - this._el = id; - this.id = this.generateId(id); - } - - /** + /** * When animation is enabled, this event fires when the animation * starts * @event animStart @@ -271,18 +325,87 @@ */ this.createEvent("expandComplete", this); + /** + * Fires when the Enter key is pressed on a node that has the focus + * @event enterKeyPressed + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that has the focus + */ + this.createEvent("enterKeyPressed", this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a Click. + * The listener may return false to cancel toggling and focusing on the node. + * @event clickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + this.createEvent("clickEvent", this); + + /** + * Fires when the focus receives the focus, when it changes from a Node + * to another Node or when it is completely lost (blurred) + * @event focusChanged + * @type CustomEvent + * @param oArgs.oldNode {YAHOO.widget.Node} Node that had the focus or null if none + * @param oArgs.newNode {YAHOO.widget.Node} Node that receives the focus or null if none + */ + + this.createEvent('focusChanged',this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a double Click + * @event dblClickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + var self = this; + this.createEvent("dblClickEvent", { + scope:this, + onSubscribeCallback: function() { + self._hasDblClickSubscriber = true; + } + }); + + /** + * Custom event that is fired when the text node label is clicked. + * The node clicked is provided as an argument + * + * @event labelClick + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node clicked + * @deprecated use clickEvent or dblClickEvent + */ + this.createEvent("labelClick", this); + + /** + * Custom event fired when the highlight of a node changes. + * The node that triggered the change is provided as an argument: + * The status of the highlight can be checked in + * nodeRef.highlightState. + * Depending on nodeRef.propagateHighlight, other nodes might have changed + * @event highlightEvent + * @type CustomEvent + * @param node{YAHOO.widget.Node} the node that started the change in highlighting state + */ + this.createEvent("highlightEvent",this); + + + this._nodes = []; // store a global reference - YAHOO.widget.TreeView.trees[this.id] = this; + TV.trees[this.id] = this; // Set up the root node - this.root = new YAHOO.widget.RootNode(this); + this.root = new Widget.RootNode(this); - var LW = YAHOO.widget.LogWriter; + var LW = Widget.LogWriter; - + // YAHOO.util.Event.onContentReady(this.id, this.handleAvailable, this, true); // YAHOO.util.Event.on(this.id, "click", this.handleClick, this, true); }, @@ -291,25 +414,432 @@ //var Event = YAHOO.util.Event; //Event.on(this.id, //}, + /** + * Builds the TreeView from an object. + * This is the method called by the constructor to build the tree when it has a second argument. + * A tree can be described by an array of objects, each object corresponding to a node. + * Node descriptions may contain values for any property of a node plus the following extra properties: + * @method buildTreeFromObject + * @param oConfig {Array} array containing a full description of the tree + * + */ + buildTreeFromObject: function (oConfig) { + var build = function (parent, oConfig) { + var i, item, node, children, type, NodeType, ThisType; + for (i = 0; i < oConfig.length; i++) { + item = oConfig[i]; + if (Lang.isString(item)) { + node = new Widget.TextNode(item, parent); + } else if (Lang.isObject(item)) { + children = item.children; + delete item.children; + type = item.type || 'text'; + delete item.type; + switch (Lang.isString(type) && type.toLowerCase()) { + case 'text': + node = new Widget.TextNode(item, parent); + break; + case 'menu': + node = new Widget.MenuNode(item, parent); + break; + case 'html': + node = new Widget.HTMLNode(item, parent); + break; + default: + if (Lang.isString(type)) { + NodeType = Widget[type]; + } else { + NodeType = type; + } + if (Lang.isObject(NodeType)) { + for (ThisType = NodeType; ThisType && ThisType !== Widget.Node; ThisType = ThisType.superclass.constructor) {} + if (ThisType) { + node = new NodeType(item, parent); + } else { + } + } else { + } + } + if (children) { + build(node,children); + } + } else { + } + } + }; + + + build(this.root,oConfig); + }, +/** + * Builds the TreeView from existing markup. Markup should consist of <UL> or <OL> elements containing <LI> elements. + * Each <LI> can have one element used as label and a second optional element which is to be a <UL> or <OL> + * containing nested nodes. + * Depending on what the first element of the <LI> element is, the following Nodes will be created: + * Only the first outermost (un-)ordered list in the markup and its children will be parsed. + * Nodes will be collapsed unless an <LI> tag has a className called 'expanded'. + * All other className attributes will be copied over to the Node className property. + * If the <LI> element contains an attribute called yuiConfig, its contents should be a JSON-encoded object + * as the one used in method buildTreeFromObject. + * @method buildTreeFromMarkup + * @param id{string|HTMLElement} The id of the element that contains the markup or a reference to it. + */ + buildTreeFromMarkup: function (id) { + var build = function (markup) { + var el, child, branch = [], config = {}, label, yuiConfig; + // Dom's getFirstChild and getNextSibling skip over text elements + for (el = Dom.getFirstChild(markup); el; el = Dom.getNextSibling(el)) { + switch (el.tagName.toUpperCase()) { + case 'LI': + label = ''; + config = { + expanded: Dom.hasClass(el,'expanded'), + title: el.title || el.alt || null, + className: Lang.trim(el.className.replace(/\bexpanded\b/,'')) || null + }; + // I cannot skip over text elements here because I want them for labels + child = el.firstChild; + if (child.nodeType == 3) { + // nodes with only whitespace, tabs and new lines don't count, they are probably just formatting. + label = Lang.trim(child.nodeValue.replace(/[\n\t\r]*/g,'')); + if (label) { + config.type = 'text'; + config.label = label; + } else { + child = Dom.getNextSibling(child); + } + } + if (!label) { + if (child.tagName.toUpperCase() == 'A') { + config.type = 'text'; + config.label = child.innerHTML; + config.href = child.href; + config.target = child.target; + config.title = child.title || child.alt || config.title; + } else { + config.type = 'html'; + var d = document.createElement('div'); + d.appendChild(child.cloneNode(true)); + config.html = d.innerHTML; + config.hasIcon = true; + } + } + // see if after the label it has a further list which will become children of this node. + child = Dom.getNextSibling(child); + switch (child && child.tagName.toUpperCase()) { + case 'UL': + case 'OL': + config.children = build(child); + break; + } + // if there are further elements or text, it will be ignored. + + if (YAHOO.lang.JSON) { + yuiConfig = el.getAttribute('yuiConfig'); + if (yuiConfig) { + yuiConfig = YAHOO.lang.JSON.parse(yuiConfig); + config = YAHOO.lang.merge(config,yuiConfig); + } + } + + branch.push(config); + break; + case 'UL': + case 'OL': + config = { + type: 'text', + label: '', + children: build(child) + }; + branch.push(config); + break; + } + } + return branch; + }; - /** - * Renders the tree boilerplate and visible nodes - * @method draw + var markup = Dom.getChildrenBy(Dom.get(id),function (el) { + var tag = el.tagName.toUpperCase(); + return tag == 'UL' || tag == 'OL'; + }); + if (markup.length) { + this.buildTreeFromObject(build(markup[0])); + } else { + } + }, + /** + * Returns the TD element where the event has occurred + * @method _getEventTargetTdEl + * @private */ - draw: function() { - var html = this.root.getHtml(); - this.getEl().innerHTML = html; - this.firstDraw = false; + _getEventTargetTdEl: function (ev) { + var target = Event.getTarget(ev); + // go up looking for a TD with a className with a ygtv prefix + while (target && !(target.tagName.toUpperCase() == 'TD' && Dom.hasClass(target.parentNode,'ygtvrow'))) { + target = Dom.getAncestorByTagName(target,'td'); + } + if (Lang.isNull(target)) { return null; } + // If it is a spacer cell, do nothing + if (/\bygtv(blank)?depthcell/.test(target.className)) { return null;} + // If it has an id, search for the node number and see if it belongs to a node in this tree. + if (target.id) { + var m = target.id.match(/\bygtv([^\d]*)(.*)/); + if (m && m[2] && this._nodes[m[2]]) { + return target; + } + } + return null; }, + /** + * Event listener for click events + * @method _onClickEvent + * @private + */ + _onClickEvent: function (ev) { + var self = this, + td = this._getEventTargetTdEl(ev), + node, + target, + toggle = function () { + node.toggle(); + node.focus(); + try { + Event.preventDefault(ev); + } catch (e) { + // @TODO + // For some reason IE8 is providing an event object with + // most of the fields missing, but only when clicking on + // the node's label, and only when working with inline + // editing. This generates a "Member not found" error + // in that browser. Determine if this is a browser + // bug, or a problem with this code. Already checked to + // see if the problem has to do with access the event + // in the outer scope, and that isn't the problem. + // Maybe the markup for inline editing is broken. + } + }; + if (!td) { + return; + } + + node = this.getNodeByElement(td); + if (!node) { + return; + } + + // exception to handle deprecated event labelClick + // @TODO take another look at this deprecation. It is common for people to + // only be interested in the label click, so why make them have to test + // the node type to figure out whether the click was on the label? + target = Event.getTarget(ev); + if (Dom.hasClass(target, node.labelStyle) || Dom.getAncestorByClassName(target,node.labelStyle)) { + this.fireEvent('labelClick',node); + } + + // If it is a toggle cell, toggle + if (/\bygtv[tl][mp]h?h?/.test(td.className)) { + toggle(); + } else { + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } else { + if (this._hasDblClickSubscriber) { + this._dblClickTimer = window.setTimeout(function () { + self._dblClickTimer = null; + if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + }, 200); + } else { + if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + } + } + } + }, + + /** + * Event listener for double-click events + * @method _onDblClickEvent + * @private + */ + _onDblClickEvent: function (ev) { + if (!this._hasDblClickSubscriber) { return; } + var td = this._getEventTargetTdEl(ev); + if (!td) {return;} + + if (!(/\bygtv[tl][mp]h?h?/.test(td.className))) { + this.fireEvent('dblClickEvent', {event:ev, node:this.getNodeByElement(td)}); + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } + } + }, + /** + * Event listener for mouse over events + * @method _onMouseOverEvent + * @private + */ + _onMouseOverEvent:function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])\b/gi,'ygtv$1$2h'); + } + }, + /** + * Event listener for mouse out events + * @method _onMouseOutEvent + * @private + */ + _onMouseOutEvent: function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])h\b/gi,'ygtv$1$2'); + } + }, + /** + * Event listener for key down events + * @method _onKeyDownEvent + * @private + */ + _onKeyDownEvent: function (ev) { + var target = Event.getTarget(ev), + node = this.getNodeByElement(target), + newNode = node, + KEY = YAHOO.util.KeyListener.KEY; + + switch(ev.keyCode) { + case KEY.UP: + do { + if (newNode.previousSibling) { + newNode = newNode.previousSibling; + } else { + newNode = newNode.parent; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus(); } + Event.preventDefault(ev); + break; + case KEY.DOWN: + do { + if (newNode.nextSibling) { + newNode = newNode.nextSibling; + } else { + newNode.expand(); + newNode = (newNode.children.length || null) && newNode.children[0]; + } + } while (newNode && !newNode._canHaveFocus); + if (newNode) { newNode.focus();} + Event.preventDefault(ev); + break; + case KEY.LEFT: + do { + if (newNode.parent) { + newNode = newNode.parent; + } else { + newNode = newNode.previousSibling; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + Event.preventDefault(ev); + break; + case KEY.RIGHT: + do { + newNode.expand(); + if (newNode.children.length) { + newNode = newNode.children[0]; + } else { + newNode = newNode.nextSibling; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + Event.preventDefault(ev); + break; + case KEY.ENTER: + if (node.href) { + if (node.target) { + window.open(node.href,node.target); + } else { + window.location(node.href); + } + } else { + node.toggle(); + } + this.fireEvent('enterKeyPressed',node); + Event.preventDefault(ev); + break; + case KEY.HOME: + newNode = this.getRoot(); + if (newNode.children.length) {newNode = newNode.children[0];} + if (newNode._canHaveFocus()) { newNode.focus(); } + Event.preventDefault(ev); + break; + case KEY.END: + newNode = newNode.parent.children; + newNode = newNode[newNode.length -1]; + if (newNode._canHaveFocus()) { newNode.focus(); } + Event.preventDefault(ev); + break; + // case KEY.PAGE_UP: + // break; + // case KEY.PAGE_DOWN: + // break; + case 107: // plus key + if (ev.shiftKey) { + node.parent.expandAll(); + } else { + node.expand(); + } + break; + case 109: // minus key + if (ev.shiftKey) { + node.parent.collapseAll(); + } else { + node.collapse(); + } + break; + default: + break; + } + }, /** + * Renders the tree boilerplate and visible nodes + * @method render + */ + render: function() { + var html = this.root.getHtml(), + el = this.getEl(); + el.innerHTML = html; + if (!this._hasEvents) { + Event.on(el, 'click', this._onClickEvent, this, true); + Event.on(el, 'dblclick', this._onDblClickEvent, this, true); + Event.on(el, 'mouseover', this._onMouseOverEvent, this, true); + Event.on(el, 'mouseout', this._onMouseOutEvent, this, true); + Event.on(el, 'keydown', this._onKeyDownEvent, this, true); + } + this._hasEvents = true; + }, + + /** * Returns the tree's host element * @method getEl * @return {HTMLElement} the host element */ getEl: function() { if (! this._el) { - this._el = document.getElementById(this.id); + this._el = Dom.get(this.id); } return this._el; }, @@ -392,9 +922,11 @@ */ getNodeByProperty: function(property, value) { for (var i in this._nodes) { - var n = this._nodes[i]; - if (n.data && value == n.data[property]) { - return n; + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + return n; + } } } @@ -412,16 +944,51 @@ getNodesByProperty: function(property, value) { var values = []; for (var i in this._nodes) { - var n = this._nodes[i]; - if (n.data && value == n.data[property]) { - values.push(n); + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + values.push(n); + } } } return (values.length) ? values : null; }, /** + * Returns the treeview node reference for an anscestor element + * of the node, or null if it is not contained within any node + * in this tree. + * @method getNodeByElement + * @param {HTMLElement} the element to test + * @return {YAHOO.widget.Node} a node reference or null + */ + getNodeByElement: function(el) { + + var p=el, m, re=/ygtv([^\d]*)(.*)/; + + do { + + if (p && p.id) { + m = p.id.match(re); + if (m && m[2]) { + return this.getNodeByIndex(m[2]); + } + } + + p = p.parentNode; + + if (!p || !p.tagName) { + break; + } + + } + while (p.id !== this.id && p.tagName.toLowerCase() !== "body"); + + return null; + }, + + /** * Removes the node and its children, and optionally refreshes the * branch of the tree that was affected. * @method removeNode @@ -454,6 +1021,18 @@ }, /** + * wait until the animation is complete before deleting + * to avoid javascript errors + * @method _removeChildren_animComplete + * @param o the custom event payload + * @private + */ + _removeChildren_animComplete: function(o) { + this.unsubscribe(this._removeChildren_animComplete); + this.removeChildren(o.node); + }, + + /** * Deletes this nodes child collection, recursively. Also collapses * the node, and resets the dynamic load flag. The primary use for * this method is to purge a node and allow it to fetch its data @@ -462,17 +1041,32 @@ * @param {Node} node the node to purge */ removeChildren: function(node) { + + if (node.expanded) { + // wait until the animation is complete before deleting to + // avoid javascript errors + if (this._collapseAnim) { + this.subscribe("animComplete", + this._removeChildren_animComplete, this, true); + Widget.Node.prototype.collapse.call(node); + return; + } + + node.collapse(); + } + while (node.children.length) { this._deleteNode(node.children[0]); } + if (node.isRoot()) { + Widget.Node.prototype.expand.call(node); + } + node.childrenRendered = false; node.dynamicLoadComplete = false; - if (node.expanded) { - node.collapse(); - } else { - node.updateIcon(); - } + + node.updateIcon(); }, /** @@ -530,7 +1124,35 @@ delete this._nodes[node.index]; }, + /** + * Nulls out the entire TreeView instance and related objects, removes attached + * event listeners, and clears out DOM elements inside the container. After + * calling this method, the instance reference should be expliclitly nulled by + * implementer, as in myDataTable = null. Use with caution! + * + * @method destroy + */ + destroy : function() { + // Since the label editor can be separated from the main TreeView control + // the destroy method for it might not be there. + if (this._destroyEditor) { this._destroyEditor(); } + var el = this.getEl(); + Event.removeListener(el,'click'); + Event.removeListener(el,'dblclick'); + Event.removeListener(el,'mouseover'); + Event.removeListener(el,'mouseout'); + Event.removeListener(el,'keydown'); + for (var i = 0 ; i < this._nodes.length; i++) { + var node = this._nodes[i]; + if (node && node.destroy) {node.destroy(); } + } + el.innerHTML = ''; + this._hasEvents = false; + }, + + + /** * TreeView instance toString * @method toString @@ -541,19 +1163,23 @@ }, /** - * Generates an unique id for an element if it doesn't yet have one - * @method generateId - * @private + * Count of nodes in tree + * @method getNodeCount + * @return {int} number of nodes in the tree */ - generateId: function(el) { - var id = el.id; + getNodeCount: function() { + return this.getRoot().getNodeCount(); + }, - if (!id) { - id = "yui-tv-auto-id-" + YAHOO.widget.TreeView.counter; - ++YAHOO.widget.TreeView.counter; - } - - return id; + /** + * Returns an object which could be used to rebuild the tree. + * It can be passed to the tree constructor to reproduce the same tree. + * It will return false if any node loads dynamically, regardless of whether it is loaded or not. + * @method getTreeDefinition + * @return {Object | false} definition of the tree or false if any node is defined as dynamic + */ + getTreeDefinition: function() { + return this.getRoot().getNodeDefinition(); }, /** @@ -570,12 +1196,59 @@ * @param node {Node} the node that was collapsed. * @deprecated use treeobj.subscribe("collapse") instead */ - onCollapse: function(node) { } + onCollapse: function(node) { }, + + /** + * Sets the value of a property for all loaded nodes in the tree. + * @method setNodesProperty + * @param name {string} Name of the property to be set + * @param value {any} value to be set + * @param refresh {boolean} if present and true, it does a refresh + */ + setNodesProperty: function(name, value, refresh) { + this.root.setNodesProperty(name,value); + if (refresh) { + this.root.refresh(); + } + }, + /** + * Event listener to toggle node highlight. + * Can be assigned as listener to clickEvent, dblClickEvent and enterKeyPressed. + * It returns false to prevent the default action. + * @method onEventToggleHighlight + * @param oArgs {any} it takes the arguments of any of the events mentioned above + * @return {false} Always cancels the default action for the event + */ + onEventToggleHighlight: function (oArgs) { + var node; + if ('node' in oArgs && oArgs.node instanceof Widget.Node) { + node = oArgs.node; + } else if (oArgs instanceof Widget.Node) { + node = oArgs; + } else { + return false; + } + node.toggleHighlight(); + return false; + } + }; -YAHOO.augment(YAHOO.widget.TreeView, YAHOO.util.EventProvider); +/* Backwards compatibility aliases */ +var PROT = TV.prototype; + /** + * Renders the tree boilerplate and visible nodes. + * Alias for render + * @method draw + * @deprecated Use render instead + */ +PROT.draw = PROT.render; +/* end backwards compatibility aliases */ + +YAHOO.augment(TV, YAHOO.util.EventProvider); + /** * Running count of all nodes created in all trees. This is * used to provide unique identifies for all nodes. Deleting @@ -584,7 +1257,7 @@ * @type int * @static */ -YAHOO.widget.TreeView.nodeCount = 0; +TV.nodeCount = 0; /** * Global cache of tree instances @@ -593,26 +1266,18 @@ * @static * @private */ -YAHOO.widget.TreeView.trees = []; +TV.trees = []; /** - * Counter for generating a new unique element id - * @property YAHOO.widget.TreeView.counter - * @static - * @private - */ -YAHOO.widget.TreeView.counter = 0; - -/** * Global method for getting a tree by its id. Used in the generated * tree html. * @method YAHOO.widget.TreeView.getTree * @param treeId {String} the id of the tree instance * @return {TreeView} the tree instance requested, null if not found. * @static */ -YAHOO.widget.TreeView.getTree = function(treeId) { - var t = YAHOO.widget.TreeView.trees[treeId]; +TV.getTree = function(treeId) { + var t = TV.trees[treeId]; return (t) ? t : null; }; @@ -626,43 +1291,23 @@ * @return {Node} the node instance requested, null if not found * @static */ -YAHOO.widget.TreeView.getNode = function(treeId, nodeIndex) { - var t = YAHOO.widget.TreeView.getTree(treeId); +TV.getNode = function(treeId, nodeIndex) { + var t = TV.getTree(treeId); return (t) ? t.getNodeByIndex(nodeIndex) : null; }; -/** - * Add a DOM event - * @method YAHOO.widget.TreeView.addHandler - * @param el the elment to bind the handler to - * @param {string} sType the type of event handler - * @param {function} fn the callback to invoke - * @static - */ -YAHOO.widget.TreeView.addHandler = function (el, sType, fn) { - if (el.addEventListener) { - el.addEventListener(sType, fn, false); - } else if (el.attachEvent) { - el.attachEvent("on" + sType, fn); - } -}; /** - * Remove a DOM event - * @method YAHOO.widget.TreeView.removeHandler - * @param el the elment to bind the handler to - * @param {string} sType the type of event handler - * @param {function} fn the callback to invoke - * @static - */ + * Class name assigned to elements that have the focus + * + * @property TreeView.FOCUS_CLASS_NAME + * @type String + * @static + * @final + * @default "ygtvfocus" -YAHOO.widget.TreeView.removeHandler = function (el, sType, fn) { - if (el.removeEventListener) { - el.removeEventListener(sType, fn, false); - } else if (el.detachEvent) { - el.detachEvent("on" + sType, fn); - } -}; + */ +TV.FOCUS_CLASS_NAME = 'ygtvfocus'; /** * Attempts to preload the images defined in the styles used to draw the tree by @@ -672,7 +1317,7 @@ * images to preload, default is ygtv * @static */ -YAHOO.widget.TreeView.preload = function(e, prefix) { +TV.preload = function(e, prefix) { prefix = prefix || "ygtv"; @@ -698,24 +1343,30 @@ document.body.appendChild(f); - YAHOO.widget.TreeView.removeHandler(window, - "load", YAHOO.widget.TreeView.preload); + Event.removeListener(window, "load", TV.preload); }; -YAHOO.widget.TreeView.addHandler(window, - "load", YAHOO.widget.TreeView.preload); - +Event.addListener(window,"load", TV.preload); +})(); +(function () { + var Dom = YAHOO.util.Dom, + Lang = YAHOO.lang, + Event = YAHOO.util.Event; /** * The base class for all tree nodes. The node's presentation and behavior in * response to mouse events is handled in Node subclasses. * @namespace YAHOO.widget * @class Node * @uses YAHOO.util.EventProvider * @param oData {object} a string or object containing the data that will - * be used to render this node + * be used to render this node, and any custom attributes that should be + * stored with the node (which is available in noderef.data). + * All values in oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions, + * the rest of the values will be stored in noderef.data * @param oParent {Node} this node's parent node - * @param expanded {boolean} the initial expanded/collapsed state + * @param expanded {boolean} the initial expanded/collapsed state (deprecated, use oData.expanded) * @constructor */ YAHOO.widget.Node = function(oData, oParent, expanded) { @@ -768,21 +1419,6 @@ depth: -1, /** - * The href for the node's label. If one is not specified, the href will - * be set so that it toggles the node. - * @property href - * @type string - */ - href: null, - - /** - * The label href target, defaults to current window - * @property target - * @type string - */ - target: "_self", - - /** * The node's expanded/collapsed state * @property expanded * @type boolean @@ -887,11 +1523,89 @@ */ nowrap: false, + /** + * If true, the node will alway be rendered as a leaf node. This can be + * used to override the presentation when dynamically loading the entire + * tree. Setting this to true also disables the dynamic load call for the + * node. + * @property isLeaf + * @type boolean + * @default false + */ + isLeaf: false, + +/** + * The CSS class for the html content container. Defaults to ygtvhtml, but + * can be overridden to provide a custom presentation for a specific node. + * @property contentStyle + * @type string + */ + contentStyle: "", + + /** + * The generated id that will contain the data passed in by the implementer. + * @property contentElId + * @type string + */ + contentElId: null, + +/** + * Enables node highlighting. If true, the node can be highlighted and/or propagate highlighting + * @property enableHighlight + * @type boolean + * @default true + */ + enableHighlight: true, + +/** + * Stores the highlight state. Can be any of: + * + * @property highlightState + * @type integer + * @default 0 + */ + + highlightState: 0, + + /** + * Tells whether highlighting will be propagated up to the parents of the clicked node + * @property propagateHighlightUp + * @type boolean + * @default false + */ + + propagateHighlightUp: false, + + /** + * Tells whether highlighting will be propagated down to the children of the clicked node + * @property propagateHighlightDown + * @type boolean + * @default false + */ + + propagateHighlightDown: false, + + /** + * User-defined className to be added to the Node + * @property className + * @type string + * @default null + */ + + className: null, + + /** * The node type * @property _type * @private - */ + * @type string + * @default "Node" +*/ _type: "Node", /* @@ -911,11 +1625,25 @@ */ init: function(oData, oParent, expanded) { - this.data = oData; + this.data = {}; this.children = []; this.index = YAHOO.widget.TreeView.nodeCount; ++YAHOO.widget.TreeView.nodeCount; - this.expanded = expanded; + this.contentElId = "ygtvcontentel" + this.index; + + if (Lang.isObject(oData)) { + for (var property in oData) { + if (oData.hasOwnProperty(property)) { + if (property.charAt(0) != '_' && !Lang.isUndefined(this[property]) && !Lang.isFunction(this[property]) ) { + this[property] = oData[property]; + } else { + this.data[property] = oData[property]; + } + } + } + } + if (!Lang.isUndefined(expanded) ) { this.expanded = expanded; } + /** * The parentChange event is fired when a parent element is applied @@ -952,10 +1680,6 @@ this.parent = parentNode; this.depth = parentNode.depth + 1; - if (!this.href) { - this.href = "javascript:" + this.getToggleLink(); - } - // @todo why was this put here. This causes new nodes added at the // root level to lose the menu behavior. // if (! this.multiExpand) { @@ -1101,7 +1825,11 @@ * @return Node[] */ getSiblings: function() { - return this.parent.children; + var sib = this.parent.children.slice(0); + for (var i=0;i < sib.length && sib[i] != this;i++) {} + sib.splice(i,1); + if (sib.length) { return sib; } + return null; }, /** @@ -1173,7 +1901,7 @@ * @return {HTMLElement} the container html element */ getEl: function() { - return document.getElementById(this.getElId()); + return Dom.get(this.getElId()); }, /** @@ -1182,7 +1910,7 @@ * @return {HTMLElement} this node's children div */ getChildrenEl: function() { - return document.getElementById(this.getChildrenElId()); + return Dom.get(this.getChildrenElId()); }, /** @@ -1191,9 +1919,18 @@ * @return {HTMLElement} this node's toggle html element */ getToggleEl: function() { - return document.getElementById(this.getToggleElId()); + return Dom.get(this.getToggleElId()); }, + /** + * Returns the outer html element for this node's content + * @method getContentEl + * @return {HTMLElement} the element + */ + getContentEl: function() { + return Dom.get(this.contentElId); + }, + /* * Returns the element that is being used for this node's spacer. * @method getSpacer @@ -1221,20 +1958,9 @@ }, */ - /** - * Generates the link that will invoke this node's toggle method - * @method getToggleLink - * @return {string} the javascript url for toggling this node - */ - getToggleLink: function() { - return "YAHOO.widget.TreeView.getNode(\'" + this.tree.id + "\'," + - this.index + ").toggle()"; - }, - - /** - * Hides this nodes children (creating them if necessary), changes the + /** + * Hides this nodes children (creating them if necessary), changes the toggle style. * @method collapse - * toggle style. */ collapse: function() { // Only collapse if currently expanded @@ -1275,18 +2001,27 @@ * toggle style, and collapses its siblings if multiExpand is not set. * @method expand */ - expand: function() { + expand: function(lazySource) { // Only expand if currently collapsed. - if (this.expanded) { return; } + if (this.expanded && !lazySource) { + return; + } - // fire the expand event handler - var ret = this.tree.onExpand(this); + var ret = true; - if (false === ret) { - return; + // When returning from the lazy load handler, expand is called again + // in order to render the new children. The "expand" event already + // fired before fething the new data, so we need to skip it now. + if (!lazySource) { + // fire the expand event handler + ret = this.tree.onExpand(this); + + if (false === ret) { + return; + } + + ret = this.tree.fireEvent("expand", this); } - - ret = this.tree.fireEvent("expand", this); if (false === ret) { return; @@ -1297,7 +2032,7 @@ return; } - if (! this.childrenRendered) { + if (!this.childrenRendered) { this.getChildrenEl().innerHTML = this.renderChildren(); } else { } @@ -1319,7 +2054,7 @@ if (! this.multiExpand) { var sibs = this.getSiblings(); - for (var i=0; i 0 || - (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) ); + if (this.isLeaf) { + return false; + } else { + return ( this.children.length > 0 || +(checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) ); + } }, /** @@ -1500,12 +2243,7 @@ this.childrenRendered = false; - var sb = []; - sb[sb.length] = '
'; - sb[sb.length] = this.getNodeHtml(); - sb[sb.length] = this.getChildrenHtml(); - sb[sb.length] = '
'; - return sb.join(""); + return ['
' ,this.getNodeHtml() , this.getChildrenHtml() ,'
'].join(""); }, /** @@ -1520,8 +2258,7 @@ var sb = []; - sb[sb.length] = '
'; + + for (var i=0;i
'; + } + + if (this.hasIcon) { + sb[sb.length] = ' '; + } + + sb[sb.length] = ' 0; + }, + /** + * Removes the focus of previously selected Node + * @method _removeFocus + * @private + */ + _removeFocus:function () { + if (this._focusedItem) { + Event.removeListener(this._focusedItem,'blur'); + this._focusedItem = null; + } + var el; + while ((el = this._focusHighlightedItems.shift())) { // yes, it is meant as an assignment, really + Dom.removeClass(el,YAHOO.widget.TreeView.FOCUS_CLASS_NAME ); + } + }, + /** + * Sets the focus on the node element. + * It will only be able to set the focus on nodes that have anchor elements in it. + * Toggle or branch icons have anchors and can be focused on. + * If will fail in nodes that have no anchor + * @method focus + * @return {boolean} success + */ + focus: function () { + var focused = false, self = this; + if (this.tree.currentFocus) { + this.tree.currentFocus._removeFocus(); + } + + var expandParent = function (node) { + if (node.parent) { + expandParent(node.parent); + node.parent.expand(); + } + }; + expandParent(this); + + Dom.getElementsBy ( + function (el) { + return /ygtv(([tl][pmn]h?)|(content))/.test(el.className); + } , + 'td' , + self.getEl().firstChild , + function (el) { + Dom.addClass(el, YAHOO.widget.TreeView.FOCUS_CLASS_NAME ); + if (!focused) { + var aEl = el.getElementsByTagName('a'); + if (aEl.length) { + aEl = aEl[0]; + aEl.focus(); + self._focusedItem = aEl; + Event.on(aEl,'blur',function () { + //console.log('f1'); + self.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null}); + self.tree.currentFocus = null; + self._removeFocus(); + }); + focused = true; + } + } + self._focusHighlightedItems.push(el); + } + ); + if (focused) { + //console.log('f2'); + this.tree.fireEvent('focusChanged',{oldNode:this.tree.currentFocus,newNode:this}); + this.tree.currentFocus = this; + } else { + //console.log('f3'); + this.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null}); + this.tree.currentFocus = null; + this._removeFocus(); + } + return focused; + }, + + /** + * Count of nodes in a branch + * @method getNodeCount + * @return {int} number of nodes in the branch + */ + getNodeCount: function() { + for (var i = 0, count = 0;i< this.children.length;i++) { + count += this.children[i].getNodeCount(); + } + return count + 1; + }, + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if the node or any children loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if the node or any children is defined as dynamic + */ + getNodeDefinition: function() { + + if (this.isDynamic()) { return false; } + + var def, defs = Lang.merge(this.data), children = []; + + + + if (this.expanded) {defs.expanded = this.expanded; } + if (!this.multiExpand) { defs.multiExpand = this.multiExpand; } + if (!this.renderHidden) { defs.renderHidden = this.renderHidden; } + if (!this.hasIcon) { defs.hasIcon = this.hasIcon; } + if (this.nowrap) { defs.nowrap = this.nowrap; } + if (this.className) { defs.className = this.className; } + if (this.editable) { defs.editable = this.editable; } + if (this.enableHighlight) { defs.enableHighlight = this.enableHighlight; } + if (this.highlightState) { defs.highlightState = this.highlightState; } + if (this.propagateHighlightUp) { defs.propagateHighlightUp = this.propagateHighlightUp; } + if (this.propagateHighlightDown) { defs.propagateHighlightDown = this.propagateHighlightDown; } + defs.type = this._type; + + + + for (var i = 0; i < this.children.length;i++) { + def = this.children[i].getNodeDefinition(); + if (def === false) { return false;} + children.push(def); + } + if (children.length) { defs.children = children; } + return defs; + }, + + + /** + * Generates the link that will invoke this node's toggle method + * @method getToggleLink + * @return {string} the javascript url for toggling this node + */ + getToggleLink: function() { + return 'return false;'; + }, + + /** + * Sets the value of property for this node and all loaded descendants. + * Only public and defined properties can be set, not methods. + * Values for unknown properties will be assigned to the refNode.data object + * @method setNodesProperty + * @param name {string} Name of the property to be set + * @param value {any} value to be set + * @param refresh {boolean} if present and true, it does a refresh + */ + setNodesProperty: function(name, value, refresh) { + if (name.charAt(0) != '_' && !Lang.isUndefined(this[name]) && !Lang.isFunction(this[name]) ) { + this[name] = value; + } else { + this.data[name] = value; + } + for (var i = 0; i < this.children.length;i++) { + this.children[i].setNodesProperty(name,value); + } + if (refresh) { + this.refresh(); + } + }, + /** + * Toggles the highlighted state of a Node + * @method toggleHighlight + */ + toggleHighlight: function() { + if (this.enableHighlight) { + // unhighlights only if fully highligthed. For not or partially highlighted it will highlight + if (this.highlightState == 1) { + this.unhighlight(); + } else { + this.highlight(); + } + } + }, + + /** + * Turns highlighting on node. + * @method highlight + * @param _silent {boolean} optional, don't fire the highlightEvent + */ + highlight: function(_silent) { + if (this.enableHighlight) { + if (this.tree.singleNodeHighlight) { + if (this.tree._currentlyHighlighted) { + this.tree._currentlyHighlighted.unhighlight(); + } + this.tree._currentlyHighlighted = this; + } + this.highlightState = 1; + this._setHighlightClassName(); + if (this.propagateHighlightDown) { + for (var i = 0;i < this.children.length;i++) { + this.children[i].highlight(true); + } + } + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + if (!_silent) { + this.tree.fireEvent('highlightEvent',this); + } + } + }, + /** + * Turns highlighting off a node. + * @method unhighlight + * @param _silent {boolean} optional, don't fire the highlightEvent + */ + unhighlight: function(_silent) { + if (this.enableHighlight) { + this.highlightState = 0; + this._setHighlightClassName(); + if (this.propagateHighlightDown) { + for (var i = 0;i < this.children.length;i++) { + this.children[i].unhighlight(true); + } + } + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + if (!_silent) { + this.tree.fireEvent('highlightEvent',this); + } + } + }, + /** + * Checks whether all or part of the children of a node are highlighted and + * sets the node highlight to full, none or partial highlight. + * If set to propagate it will further call the parent + * @method _childrenHighlighted + * @private + */ + _childrenHighlighted: function() { + var yes = false, no = false; + if (this.enableHighlight) { + for (var i = 0;i < this.children.length;i++) { + switch(this.children[i].highlightState) { + case 0: + no = true; + break; + case 1: + yes = true; + break; + case 2: + yes = no = true; + break; + } + } + if (yes && no) { + this.highlightState = 2; + } else if (yes) { + this.highlightState = 1; + } else { + this.highlightState = 0; + } + this._setHighlightClassName(); + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + } + }, + + /** + * Changes the classNames on the toggle and content containers to reflect the current highlighting + * @method _setHighlightClassName + * @private + */ + _setHighlightClassName: function() { + var el = Dom.get('ygtvtableel' + this.index); + if (el) { + el.className = el.className.replace(/\bygtv-highlight\d\b/gi,'ygtv-highlight' + this.highlightState); + } + } + }; YAHOO.augment(YAHOO.widget.Node, YAHOO.util.EventProvider); +})(); +/** + * A custom YAHOO.widget.Node that handles the unique nature of + * the virtual, presentationless root node. + * @namespace YAHOO.widget + * @class RootNode + * @extends YAHOO.widget.Node + * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to + * @constructor + */ +YAHOO.widget.RootNode = function(oTree) { + // Initialize the node with null params. The root node is a + // special case where the node has no presentation. So we have + // to alter the standard properties a bit. + this.init(null, null, true); + + /* + * For the root node, we get the tree reference from as a param + * to the constructor instead of from the parent element. + */ + this.tree = oTree; +}; +YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, { + + /** + * The node type + * @property _type + * @type string + * @private + * @default "RootNode" + */ + _type: "RootNode", + + // overrides YAHOO.widget.Node + getNodeHtml: function() { + return ""; + }, + + toString: function() { + return this._type; + }, + + loadComplete: function() { + this.tree.draw(); + }, + + /** + * Count of nodes in tree. + * It overrides Nodes.getNodeCount because the root node should not be counted. + * @method getNodeCount + * @return {int} number of nodes in the tree + */ + getNodeCount: function() { + for (var i = 0, count = 0;i< this.children.length;i++) { + count += this.children[i].getNodeCount(); + } + return count; + }, + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * Since the RootNode is automatically created by treeView, + * its own definition is excluded from the returned node definition + * which only contains its children. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if any child node is defined as dynamic + */ + getNodeDefinition: function() { + + for (var def, defs = [], i = 0; i < this.children.length;i++) { + def = this.children[i].getNodeDefinition(); + if (def === false) { return false;} + defs.push(def); + } + return defs; + }, + + collapse: function() {}, + expand: function() {}, + getSiblings: function() { return null; }, + focus: function () {} + +}); +(function () { + var Dom = YAHOO.util.Dom, + Lang = YAHOO.lang, + Event = YAHOO.util.Event; /** * The default node presentation. The first parameter should be * either a string that will be used as the node's label, or an object - * that has a string propery called label. By default, the clicking the + * that has at least a string property called label. By default, clicking the * label will toggle the expanded/collapsed state of the node. By - * changing the href property of the instance, this behavior can be + * setting the href property of the instance, this behavior can be * changed so that the label will go to the specified href. * @namespace YAHOO.widget * @class TextNode * @extends YAHOO.widget.Node * @constructor * @param oData {object} a string or object containing the data that will - * be used to render this node + * be used to render this node. + * Providing a string is the same as providing an object with a single property named label. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. * @param oParent {YAHOO.widget.Node} this node's parent node - * @param expanded {boolean} the initial expanded/collapsed state + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) */ YAHOO.widget.TextNode = function(oData, oParent, expanded) { if (oData) { + if (Lang.isString(oData)) { + oData = { label: oData }; + } this.init(oData, oParent, expanded); this.setUpLabel(oData); } @@ -1745,56 +2925,59 @@ */ label: null, - textNodeParentChange: function() { - - /** - * Custom event that is fired when the text node label is clicked. The - * custom event is defined on the tree instance, so there is a single - * event that handles all nodes in the tree. The node clicked is - * provided as an argument - * - * @event labelClick - * @for YAHOO.widget.TreeView - * @param {YAHOO.widget.Node} node the node clicked - */ - if (this.tree && !this.tree.hasEvent("labelClick")) { - this.tree.createEvent("labelClick", this.tree); - } - - }, + /** + * The text for the title (tooltip) for the label element + * @property title + * @type string + */ + title: null, + + /** + * The href for the node's label. If one is not specified, the href will + * be set so that it toggles the node. + * @property href + * @type string + */ + href: null, /** + * The label href target, defaults to current window + * @property target + * @type string + */ + target: "_self", + + /** + * The node type + * @property _type + * @private + * @type string + * @default "TextNode" + */ + _type: "TextNode", + + + /** * Sets up the node label * @method setUpLabel * @param oData string containing the label, or an object with a label property */ setUpLabel: function(oData) { - // set up the custom event on the tree - this.textNodeParentChange(); - this.subscribe("parentChange", this.textNodeParentChange); - - if (typeof oData == "string") { - oData = { label: oData }; + if (Lang.isString(oData)) { + oData = { + label: oData + }; + } else { + if (oData.style) { + this.labelStyle = oData.style; + } } + this.label = oData.label; - this.data.label = oData.label; - - // update the link - if (oData.href) { - this.href = oData.href; - } - // set the target - if (oData.target) { - this.target = oData.target; - } - - if (oData.style) { - this.labelStyle = oData.style; - } - this.labelElId = "ygtvlabelel" + this.index; + }, /** @@ -1804,156 +2987,145 @@ * @return {object} the element */ getLabelEl: function() { - return document.getElementById(this.labelElId); + return Dom.get(this.labelElId); }, // overrides YAHOO.widget.Node - getNodeHtml: function() { + getContentHtml: function() { var sb = []; - - sb[sb.length] = ''; - sb[sb.length] = ''; - - for (var i=0;i '; - //sb[sb.length] = ''; - sb[sb.length] = ''; - } - - var getNode = 'YAHOO.widget.TreeView.getNode(\'' + - this.tree.id + '\',' + this.index + ')'; - - sb[sb.length] = ''; - - sb[sb.length] = '
'; - - /* - sb[sb.length] = '':''; return sb.join(""); }, - /** - * Executed when the label is clicked. Fires the labelClick custom event. - * @method onLabelClick - * @param me {Node} this node - * @scope the anchor tag clicked - * @return false to cancel the anchor click + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if this node or any descendant is defined as dynamic */ - onLabelClick: function(me) { - return me.tree.fireEvent("labelClick", me); - //return true; + getNodeDefinition: function() { + var def = YAHOO.widget.TextNode.superclass.getNodeDefinition.call(this); + if (def === false) { return false; } + + // Node specific properties + def.label = this.label; + if (this.labelStyle != 'ygtvlabel') { def.style = this.labelStyle; } + if (this.title) { def.title = this.title; } + if (this.href) { def.href = this.href; } + if (this.target != '_self') { def.target = this.target; } + + return def; + }, toString: function() { - return "TextNode (" + this.index + ") " + this.label; + return YAHOO.widget.TextNode.superclass.toString.call(this) + ": " + this.label; + }, + + // deprecated + onLabelClick: function() { + return false; + }, + refresh: function() { + YAHOO.widget.TextNode.superclass.refresh.call(this); + var label = this.getLabelEl(); + label.innerHTML = this.label; + if (label.tagName.toUpperCase() == 'A') { + label.href = this.href; + label.target = this.target; + } } + + + }); +})(); /** - * A custom YAHOO.widget.Node that handles the unique nature of - * the virtual, presentationless root node. + * A menu-specific implementation that differs from TextNode in that only + * one sibling can be expanded at a time. * @namespace YAHOO.widget - * @class RootNode - * @extends YAHOO.widget.Node - * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to + * @class MenuNode + * @extends YAHOO.widget.TextNode + * @param oData {object} a string or object containing the data that will + * be used to render this node. + * Providing a string is the same as providing an object with a single property named label. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. + * @param oParent {YAHOO.widget.Node} this node's parent node + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) * @constructor */ -YAHOO.widget.RootNode = function(oTree) { - // Initialize the node with null params. The root node is a - // special case where the node has no presentation. So we have - // to alter the standard properties a bit. - this.init(null, null, true); - - /* - * For the root node, we get the tree reference from as a param - * to the constructor instead of from the parent element. - */ - this.tree = oTree; -}; +YAHOO.widget.MenuNode = function(oData, oParent, expanded) { + YAHOO.widget.MenuNode.superclass.constructor.call(this,oData,oParent,expanded); -YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, { - - // overrides YAHOO.widget.Node - getNodeHtml: function() { - return ""; - }, + /* + * Menus usually allow only one branch to be open at a time. + */ + this.multiExpand = false; - toString: function() { - return "RootNode"; - }, +}; - loadComplete: function() { - this.tree.draw(); - }, +YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, { - collapse: function() {}, - expand: function() {} + /** + * The node type + * @property _type + * @private + * @default "MenuNode" + */ + _type: "MenuNode" }); +(function () { + var Dom = YAHOO.util.Dom, + Lang = YAHOO.lang, + Event = YAHOO.util.Event; + /** * This implementation takes either a string or object for the - * oData argument. If is it a string, we will use it for the display + * oData argument. If is it a string, it will use it for the display * of this node (and it can contain any html code). If the parameter - * is an object, we look for a parameter called "html" that will be + * is an object,it looks for a parameter called "html" that will be * used for this node's display. * @namespace YAHOO.widget * @class HTMLNode * @extends YAHOO.widget.Node * @constructor * @param oData {object} a string or object containing the data that will - * be used to render this node + * be used to render this node. + * Providing a string is the same as providing an object with a single property named html. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All other attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. * @param oParent {YAHOO.widget.Node} this node's parent node - * @param expanded {boolean} the initial expanded/collapsed state + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) * @param hasIcon {boolean} specifies whether or not leaf nodes should - * have an icon + * be rendered with or without a horizontal line line and/or toggle icon. If the icon + * is not displayed, the content fills the space it would have occupied. + * This option operates independently of the leaf node presentation logic + * for dynamic nodes. + * (deprecated; use oData.hasIcon) */ YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) { if (oData) { @@ -1972,298 +3144,523 @@ */ contentStyle: "ygtvhtml", - /** - * The generated id that will contain the data passed in by the implementer. - * @property contentElId - * @type string - */ - contentElId: null, /** * The HTML content to use for this node's display - * @property content + * @property html * @type string */ - content: null, + html: null, + +/** + * The node type + * @property _type + * @private + * @type string + * @default "HTMLNode" + */ + _type: "HTMLNode", /** * Sets up the node label * @property initContent - * @param {object} An html string or object containing an html property - * @param {boolean} hasIcon determines if the node will be rendered with an + * @param oData {object} An html string or object containing an html property + * @param hasIcon {boolean} determines if the node will be rendered with an * icon or not */ initContent: function(oData, hasIcon) { - if (typeof oData == "string") { - oData = { html: oData }; - } - - this.html = oData.html; + this.setHtml(oData); this.contentElId = "ygtvcontentel" + this.index; - this.hasIcon = hasIcon; - + if (!Lang.isUndefined(hasIcon)) { this.hasIcon = hasIcon; } + }, /** - * Returns the outer html element for this node's content - * @method getContentEl - * @return {HTMLElement} the element + * Synchronizes the node.data, node.html, and the node's content + * @property setHtml + * @param o {object} An html string or object containing an html property */ - getContentEl: function() { - return document.getElementById(this.contentElId); - }, + setHtml: function(o) { - // overrides YAHOO.widget.Node - getNodeHtml: function() { - var sb = []; + this.html = (typeof o === "string") ? o : o.html; - sb[sb.length] = '
'; - sb[sb.length] = ''; - - for (var i=0;i '; - sb[sb.length] = ''; + var el = this.getContentEl(); + if (el) { + el.innerHTML = this.html; } - if (this.hasIcon) { - sb[sb.length] = 'http://developer.yahoo.com/yui/calendar/#internationalization + * @property calendarConfig + */ + calendarConfig: null, + + + + /** + * 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 + * @method fillEditorContainer + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return void + */ + fillEditorContainer: function (editorData) { + + var cal, container = editorData.inputContainer; + + if (Lang.isUndefined(Calendar)) { + Dom.replaceClass(editorData.editorPanel,'ygtv-edit-DateNode','ygtv-edit-TextNode'); + YAHOO.widget.DateNode.superclass.fillEditorContainer.call(this, editorData); + return; + } + + if (editorData.nodeType != this._type) { + editorData.nodeType = this._type; + editorData.saveOnEnter = false; + + editorData.node.destroyEditorContents(editorData); + editorData.inputObject = cal = new Calendar(container.appendChild(document.createElement('div'))); + if (this.calendarConfig) { + cal.cfg.applyConfig(this.calendarConfig,true); + cal.cfg.fireQueue(); + } + cal.selectEvent.subscribe(function () { + this.tree._closeEditor(true); + },this,true); + } else { + cal = editorData.inputObject; + } -}; + cal.cfg.setProperty("selected",this.label, false); -YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, { + var delim = cal.cfg.getProperty('DATE_FIELD_DELIMITER'); + var pageDate = this.label.split(delim); + cal.cfg.setProperty('pagedate',pageDate[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] + delim + pageDate[cal.cfg.getProperty('MDY_YEAR_POSITION') -1]); + cal.cfg.fireQueue(); - toString: function() { - return "MenuNode (" + this.index + ") " + this.label; + cal.render(); + cal.oDomContainer.focus(); + }, + /** + * Saves the date entered in the editor into the DateNode label property and displays it. + * Overrides Node.saveEditorValue + * @method saveEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + */ + saveEditorValue: function (editorData) { + var node = editorData.node, + validator = node.tree.validator, + value; + if (Lang.isUndefined(Calendar)) { + value = editorData.inputElement.value; + } else { + var cal = editorData.inputObject, + date = cal.getSelectedDates()[0], + dd = []; + + dd[cal.cfg.getProperty('MDY_DAY_POSITION') -1] = date.getDate(); + dd[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] = date.getMonth() + 1; + dd[cal.cfg.getProperty('MDY_YEAR_POSITION') -1] = date.getFullYear(); + value = dd.join(cal.cfg.getProperty('DATE_FIELD_DELIMITER')); + } + if (Lang.isFunction(validator)) { + value = validator(value,node.label,node); + if (Lang.isUndefined(value)) { return false; } + } + + node.label = value; + node.getLabelEl().innerHTML = value; + }, + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the node or false if this node or any descendant is defined as dynamic + */ + getNodeDefinition: function() { + var def = YAHOO.widget.DateNode.superclass.getNodeDefinition.call(this); + if (def === false) { return false; } + if (this.calendarConfig) { def.calendarConfig = this.calendarConfig; } + return def; } + }); -/** - * A static factory class for tree view expand/collapse animations - * @class TVAnim - * @static - */ -YAHOO.widget.TVAnim = function() { - return { - /** - * Constant for the fade in animation - * @property FADE_IN - * @type string - * @static - */ - FADE_IN: "TVFadeIn", +})(); +(function () { + var Dom = YAHOO.util.Dom, + Lang = YAHOO.lang, + Event = YAHOO.util.Event, + TV = YAHOO.widget.TreeView, + TVproto = TV.prototype; - /** - * Constant for the fade out animation - * @property FADE_OUT - * @type string - * @static - */ - FADE_OUT: "TVFadeOut", + /** + * An object to store information used for in-line editing + * for all Nodes of all TreeViews. It contains: + *
    + *
  • active {boolean}, whether there is an active cell editor
  • + *
  • whoHasIt {YAHOO.widget.TreeView} TreeView instance that is currently using the editor
  • + *
  • nodeType {string} value of static Node._type property, allows reuse of input element if node is of the same type.
  • + *
  • editorPanel {HTMLelement (<div>)} element holding the in-line editor
  • + *
  • inputContainer {HTMLelement (<div>)} element which will hold the type-specific input element(s) to be filled by the fillEditorContainer method
  • + *
  • 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
  • + *
  • node {YAHOO.widget.Node} reference to the Node being edited
  • + *
  • 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
  • + *
+ * Editors are free to use this object to store additional data. + * @property editorData + * @static + * @for YAHOO.widget.TreeView + */ + TV.editorData = { + active:false, + whoHasIt:null, // which TreeView has it + nodeType:null, + editorPanel:null, + inputContainer:null, + buttonsContainer:null, + node:null, // which Node is being edited + saveOnEnter:true + // Each node type is free to add its own properties to this as it sees fit. + }; + + /** + * Validator function for edited data, called from the TreeView instance scope, + * receives the arguments (newValue, oldValue, nodeInstance) + * and returns either the validated (or type-converted) value or undefined. + * An undefined return will prevent the editor from closing + * @property validator + * @default null + * @for YAHOO.widget.TreeView + */ + TVproto.validator = null; + + /** + * Entry point of the editing plug-in. + * TreeView will call this method if it exists when a node label is clicked + * @method _nodeEditing + * @param node {YAHOO.widget.Node} the node to be edited + * @return {Boolean} true to indicate that the node is editable and prevent any further bubbling of the click. + * @for YAHOO.widget.TreeView + * @private + */ + + + TVproto._nodeEditing = function (node) { + if (node.fillEditorContainer && node.editable) { + var ed, topLeft, buttons, button, editorData = TV.editorData; + editorData.active = true; + editorData.whoHasIt = this; + if (!editorData.nodeType) { + editorData.editorPanel = ed = document.body.appendChild(document.createElement('div')); + Dom.addClass(ed,'ygtv-label-editor'); - /** - * Returns a ygAnim instance of the given type - * @method getAnim - * @param type {string} the type of animation - * @param el {HTMLElement} the element to element (probably the children div) - * @param callback {function} function to invoke when the animation is done. - * @return {YAHOO.util.Animation} the animation instance - * @static - */ - getAnim: function(type, el, callback) { - if (YAHOO.widget[type]) { - return new YAHOO.widget[type](el, callback); + buttons = editorData.buttonsContainer = ed.appendChild(document.createElement('div')); + Dom.addClass(buttons,'ygtv-button-container'); + button = buttons.appendChild(document.createElement('button')); + Dom.addClass(button,'ygtvok'); + button.innerHTML = ' '; + button = buttons.appendChild(document.createElement('button')); + Dom.addClass(button,'ygtvcancel'); + button.innerHTML = ' '; + Event.on(buttons, 'click', function (ev) { + var target = Event.getTarget(ev); + var node = TV.editorData.node; + if (Dom.hasClass(target,'ygtvok')) { + Event.stopEvent(ev); + this._closeEditor(true); + } + if (Dom.hasClass(target,'ygtvcancel')) { + Event.stopEvent(ev); + this._closeEditor(false); + } + }, this, true); + + editorData.inputContainer = ed.appendChild(document.createElement('div')); + Dom.addClass(editorData.inputContainer,'ygtv-input'); + + Event.on(ed,'keydown',function (ev) { + var editorData = TV.editorData, + KEY = YAHOO.util.KeyListener.KEY; + switch (ev.keyCode) { + case KEY.ENTER: + Event.stopEvent(ev); + if (editorData.saveOnEnter) { + this._closeEditor(true); + } + break; + case KEY.ESCAPE: + Event.stopEvent(ev); + this._closeEditor(false); + break; + } + },this,true); + + + } else { - return null; + ed = editorData.editorPanel; } - }, + editorData.node = node; + if (editorData.nodeType) { + Dom.removeClass(ed,'ygtv-edit-' + editorData.nodeType); + } + Dom.addClass(ed,' ygtv-edit-' + node._type); + topLeft = Dom.getXY(node.getContentEl()); + Dom.setStyle(ed,'left',topLeft[0] + 'px'); + Dom.setStyle(ed,'top',topLeft[1] + 'px'); + Dom.setStyle(ed,'display','block'); + ed.focus(); + node.fillEditorContainer(editorData); - /** - * Returns true if the specified animation class is available - * @method isValid - * @param type {string} the type of animation - * @return {boolean} true if valid, false if not - * @static - */ - isValid: function(type) { - return (YAHOO.widget[type]); + return true; // If inline editor available, don't do anything else. } }; -} (); - -/** - * A 1/2 second fade-in animation. - * @class TVFadeIn - * @constructor - * @param el {HTMLElement} the element to animate - * @param callback {function} function to invoke when the animation is finished - */ -YAHOO.widget.TVFadeIn = function(el, callback) { + /** - * The element to animate - * @property el - * @type HTMLElement - */ - this.el = el; + * Method to be associated with an event (clickEvent, dblClickEvent or enterKeyPressed) to pop up the contents editor + * It calls the corresponding node editNode method. + * @method onEventEditNode + * @param oArgs {object} Object passed as arguments to TreeView event listeners + * @for YAHOO.widget.TreeView + */ + TVproto.onEventEditNode = function (oArgs) { + if (oArgs instanceof YAHOO.widget.Node) { + oArgs.editNode(); + } else if (oArgs.node instanceof YAHOO.widget.Node) { + oArgs.node.editNode(); + } + }; + /** - * the callback to invoke when the animation is complete - * @property callback - * @type function - */ - this.callback = callback; - -}; - -YAHOO.widget.TVFadeIn.prototype = { + * Method to be called when the inline editing is finished and the editor is to be closed + * @method _closeEditor + * @param save {Boolean} true if the edited value is to be saved, false if discarded + * @private + * @for YAHOO.widget.TreeView + */ + + TVproto._closeEditor = function (save) { + var ed = TV.editorData, + node = ed.node, + close = true; + if (save) { + close = ed.node.saveEditorValue(ed) !== false; + } + if (close) { + Dom.setStyle(ed.editorPanel,'display','none'); + ed.active = false; + node.focus(); + } + }; + /** - * Performs the animation - * @method animate - */ - animate: function() { - var tvanim = this; + * Entry point for TreeView's destroy method to destroy whatever the editing plug-in has created + * @method _destroyEditor + * @private + * @for YAHOO.widget.TreeView + */ + TVproto._destroyEditor = function() { + var ed = TV.editorData; + if (ed && ed.nodeType && (!ed.active || ed.whoHasIt === this)) { + Event.removeListener(ed.editorPanel,'keydown'); + Event.removeListener(ed.buttonContainer,'click'); + ed.node.destroyEditorContents(ed); + document.body.removeChild(ed.editorPanel); + ed.nodeType = ed.editorPanel = ed.inputContainer = ed.buttonsContainer = ed.whoHasIt = ed.node = null; + ed.active = false; + } + }; + + var Nproto = YAHOO.widget.Node.prototype; + + /** + * Signals if the label is editable. (Ignored on TextNodes with href set.) + * @property editable + * @type boolean + * @for YAHOO.widget.Node + */ + Nproto.editable = false; + + /** + * pops up the contents editor, if there is one and the node is declared editable + * @method editNode + * @for YAHOO.widget.Node + */ + + Nproto.editNode = function () { + this.tree._nodeEditing(this); + }; + + - var s = this.el.style; - s.opacity = 0.1; - s.filter = "alpha(opacity=10)"; - s.display = ""; - var dur = 0.4; - var a = new YAHOO.util.Anim(this.el, {opacity: {from: 0.1, to: 1, unit:""}}, dur); - a.onComplete.subscribe( function() { tvanim.onComplete(); } ); - a.animate(); - }, - - /** - * Clean up and invoke callback - * @method onComplete + /** Placeholder for a function that should provide the inline node label editor. + * Leaving it set to null will indicate that this node type is not editable. + * It should be overridden by nodes that provide inline editing. + * The Node-specific editing element (input box, textarea or whatever) should be inserted into editorData.inputContainer. + * @method fillEditorContainer + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return void + * @for YAHOO.widget.Node */ - onComplete: function() { - this.callback(); - }, + Nproto.fillEditorContainer = null; + /** - * toString - * @method toString - * @return {string} the string representation of the instance + * Node-specific destroy function to empty the contents of the inline editor panel + * This function is the worst case alternative that will purge all possible events and remove the editor contents + * Method Event.purgeElement is somewhat costly so if it can be replaced by specifc Event.removeListeners, it is better to do so. + * @method destroyEditorContents + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.Node */ - toString: function() { - return "TVFadeIn"; - } -}; + Nproto.destroyEditorContents = function (editorData) { + // In the worst case, if the input editor (such as the Calendar) has no destroy method + // we can only try to remove all possible events on it. + Event.purgeElement(editorData.inputContainer,true); + editorData.inputContainer.innerHTML = ''; + }; -/** - * A 1/2 second fade out animation. - * @class TVFadeOut - * @constructor - * @param el {HTMLElement} the element to animate - * @param callback {Function} function to invoke when the animation is finished - */ -YAHOO.widget.TVFadeOut = function(el, callback) { /** - * The element to animate - * @property el - * @type HTMLElement + * Saves the value entered into the editor. + * Should be overridden by each node type + * @method saveEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return a return of exactly false will prevent the editor from closing + * @for YAHOO.widget.Node */ - this.el = el; + Nproto.saveEditorValue = function (editorData) { + }; + + var TNproto = YAHOO.widget.TextNode.prototype; + - /** - * the callback to invoke when the animation is complete - * @property callback - * @type function + + /** + * Places an <input> textbox in the input container and loads the label text into it + * @method fillEditorContainer + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return void + * @for YAHOO.widget.TextNode */ - this.callback = callback; + TNproto.fillEditorContainer = function (editorData) { + + var input; + // If last node edited is not of the same type as this one, delete it and fill it with our editor + if (editorData.nodeType != this._type) { + editorData.nodeType = this._type; + editorData.saveOnEnter = true; + editorData.node.destroyEditorContents(editorData); -}; + editorData.inputElement = input = editorData.inputContainer.appendChild(document.createElement('input')); + + } else { + // if the last node edited was of the same time, reuse the input element. + input = editorData.inputElement; + } -YAHOO.widget.TVFadeOut.prototype = { + input.value = this.label; + input.focus(); + input.select(); + }; + /** - * Performs the animation - * @method animate + * Saves the value entered in the editor into the TextNode label property and displays it + * Overrides Node.saveEditorValue + * @method saveEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.TextNode */ - animate: function() { - var tvanim = this; - var dur = 0.4; - var a = new YAHOO.util.Anim(this.el, {opacity: {from: 1, to: 0.1, unit:""}}, dur); - a.onComplete.subscribe( function() { tvanim.onComplete(); } ); - a.animate(); - }, + TNproto.saveEditorValue = function (editorData) { + var node = editorData.node, + value = editorData.inputElement.value, + validator = node.tree.validator; + + if (Lang.isFunction(validator)) { + value = validator(value,node.label,node); + if (Lang.isUndefined(value)) { return false; } + } + node.label = value; + node.getLabelEl().innerHTML = value; + }; /** - * Clean up and invoke callback - * @method onComplete + * Destroys the contents of the inline editor panel + * Overrides Node.destroyEditorContent + * Since we didn't set any event listeners on this inline editor, it is more efficient to avoid the generic method in Node + * @method destroyEditorContents + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.TextNode */ - onComplete: function() { - var s = this.el.style; - s.display = "none"; - // s.opacity = 1; - s.filter = "alpha(opacity=100)"; - this.callback(); - }, - - /** - * toString - * @method toString - * @return {string} the string representation of the instance - */ - toString: function() { - return "TVFadeOut"; - } -}; - -YAHOO.register("treeview", YAHOO.widget.TreeView, {version: "2.3.0", build: "442"}); + TNproto.destroyEditorContents = function (editorData) { + editorData.inputContainer.innerHTML = ''; + }; +})(); +YAHOO.register("treeview", YAHOO.widget.TreeView, {version: "2.7.0", build: "1799"});