blob: da69fa49b3e866e32a9060e990b02324b0cb716e [file] [log] [blame]
/*
Copyright (c) 2004-2006, The Dojo Foundation
All Rights Reserved.
Licensed under the Academic Free License version 2.1 or above OR the
modified BSD license. For more information on Dojo licensing, see:
http://dojotoolkit.org/community/licensing.shtml
*/
dojo.provide("dojo.widget.TreeNodeV3");
dojo.require("dojo.html.*");
dojo.require("dojo.event.*");
dojo.require("dojo.io.*");
dojo.require("dojo.widget.TreeWithNode");
dojo.widget.defineWidget(
"dojo.widget.TreeNodeV3",
[dojo.widget.HtmlWidget, dojo.widget.TreeWithNode],
function() {
this.actionsDisabled = [];
this.object = {};
},
{
tryLazyInit: true,
/*
* Basic actions one can perform on nodes and, some(addchild) on trees
*/
actions: {
MOVE: "MOVE",
DETACH: "DETACH",
EDIT: "EDIT",
ADDCHILD: "ADDCHILD",
SELECT: "SELECT"
},
labelClass: "",
contentClass: "",
expandNode: null,
labelNode: null,
/**
* can't call it nodeType cause of IE problems
*/
nodeDocType: "",
selected: false,
getnodeDocType: function() {
return this.nodeDocType;
},
cloneProperties: ["actionsDisabled","tryLazyInit","nodeDocType","objectId","object",
"title","isFolder","isExpanded","state"],
/**
* copy cloneProperties with recursion into them
* contains "copy constructor"
*/
clone: function(deep) {
var ret = new this.constructor();
//dojo.debug("start cloning props "+this);
for(var i=0; i<this.cloneProperties.length; i++) {
var prop = this.cloneProperties[i];
//dojo.debug("cloning "+prop+ ":" +this[prop]);
ret[prop] = dojo.lang.shallowCopy(this[prop], true);
}
if (this.tree.unsetFolderOnEmpty && !deep && this.isFolder) {
ret.isFolder = false;
}
//dojo.debug("cloned props "+this);
ret.toggleObj = this.toggleObj;
dojo.widget.manager.add(ret);
ret.tree = this.tree;
ret.buildRendering({},{});
ret.initialize({},{});
if (deep && this.children.length) {
//dojo.debug("deeper copy start");
for(var i=0; i<this.children.length; i++) {
var child = this.children[i];
//dojo.debug("copy child "+child);
if (child.clone) {
ret.children.push(child.clone(deep));
} else {
ret.children.push(dojo.lang.shallowCopy(child, deep));
}
}
//dojo.debug("deeper copy end");
ret.setChildren();
}
return ret;
},
markProcessing: function() {
this.markProcessingSavedClass = dojo.html.getClass(this.expandNode);
dojo.html.setClass(this.expandNode, this.tree.classPrefix+'ExpandLoading');
},
unmarkProcessing: function() {
dojo.html.setClass(this.expandNode, this.markProcessingSavedClass);
},
/**
* get information from args & parent, then build rendering
*/
buildRendering: function(args, fragment, parent) {
//dojo.debug("Build for "+args.toSource());
if (args.tree) {
this.tree = dojo.lang.isString(args.tree) ? dojo.widget.manager.getWidgetById(args.tree) : args.tree;
} else if (parent && parent.tree) {
this.tree = parent.tree;
}
if (!this.tree) {
dojo.raise("Can't evaluate tree from arguments or parent");
}
//dojo.profile.start("buildRendering - cloneNode");
this.domNode = this.tree.nodeTemplate.cloneNode(true);
this.expandNode = this.domNode.firstChild;
this.contentNode = this.domNode.childNodes[1];
this.labelNode = this.contentNode.firstChild;
if (this.labelClass) {
dojo.html.addClass(this.labelNode, this.labelClass);
}
if (this.contentClass) {
dojo.html.addClass(this.contentNode, this.contentClass);
}
//dojo.profile.end("buildRendering - cloneNode");
this.domNode.widgetId = this.widgetId;
//dojo.profile.start("buildRendering - innerHTML");
this.labelNode.innerHTML = this.title;
//dojo.profile.end("buildRendering - innerHTML");
},
isTreeNode: true,
object: {},
title: "",
isFolder: null, // set by widget depending on children/args
contentNode: null, // the item label
expandClass: "",
isExpanded: false,
containerNode: null,
getInfo: function() {
// No title here (title may be widget)
var info = {
widgetId: this.widgetId,
objectId: this.objectId,
index: this.getParentIndex()
}
return info;
},
setFolder: function() {
//dojo.debug("SetFolder in "+this);
this.isFolder = true;
this.viewSetExpand();
if (!this.containerNode) { // maybe this node was unfolderized and still has container
this.viewAddContainer(); // all folders have container.
}
//dojo.debug("publish "+this.tree.eventNames.setFolder);
dojo.event.topic.publish(this.tree.eventNames.afterSetFolder, { source: this });
},
initialize: function(args, frag, parent) {
//dojo.profile.start("initialize");
/**
* first we populate current widget from args,
* then use its data to initialize
* args may be empty, all data inside widget for copy constructor
*/
if (args.isFolder) {
this.isFolder = true;
}
if (this.children.length || this.isFolder) {
//dojo.debug("children found");
//dojo.debug(this.children);
//dojo.debug("isFolder "+args.isFolder);
// viewSetExpand for Folder is set here also
this.setFolder();
} else {
// set expandicon for leaf
this.viewSetExpand();
}
for(var i=0; i<this.actionsDisabled.length;i++) {
this.actionsDisabled[i] = this.actionsDisabled[i].toUpperCase();
}
//dojo.debug("publish "+this.tree.eventNames.changeTree);
dojo.event.topic.publish(this.tree.eventNames.afterChangeTree, {oldTree:null, newTree:this.tree, node:this} );
//dojo.profile.end("initialize");
//dojo.debug("initialize out "+this);
//dojo.debug(this+" parent "+parent);
},
unsetFolder: function() {
this.isFolder = false;
this.viewSetExpand();
dojo.event.topic.publish(this.tree.eventNames.afterUnsetFolder, { source: this });
},
insertNode: function(parent, index) {
if (!index) index = 0;
//dojo.debug("insertNode "+this+" parent "+parent+" before "+index);
if (index==0) {
dojo.html.prependChild(this.domNode, parent.containerNode);
} else {
dojo.html.insertAfter(this.domNode, parent.children[index-1].domNode);
}
},
updateTree: function(newTree) {
if (this.tree === newTree) {
return;
}
var oldTree = this.tree;
dojo.lang.forEach(this.getDescendants(),
function(elem) {
elem.tree = newTree;
});
/**
* UNTESTED
* changes class prefix for all domnodes when moving between trees
*/
if (oldTree.classPrefix != newTree.classPrefix) {
var stack = [this.domNode]
var elem;
var reg = new RegExp("(^|\\s)"+oldTree.classPrefix, "g");
while (elem = stack.pop()) {
for(var i=0; i<elem.childNodes.length; i++) {
var childNode = elem.childNodes[i]
if (childNode.nodeDocType != 1) continue;
// change prefix for classes
dojo.html.setClass(childNode, dojo.html.getClass(childNode).replace(reg, '$1'+newTree.classPrefix));
stack.push(childNode);
}
}
}
var message = {oldTree:oldTree, newTree:newTree, node:this}
dojo.event.topic.publish(this.tree.eventNames.afterChangeTree, message );
dojo.event.topic.publish(newTree.eventNames.afterChangeTree, message );
},
/**
* called every time the widget is added with createWidget or created wia markup
* from addChild -> registerChild or from postInitialize->registerChild
* not called in batch procession
* HTML & widget.createWidget only
* Layout MUST be removed when node is detached
*
*/
addedTo: function(parent, index, dontPublishEvent) {
//dojo.profile.start("addedTo");
//dojo.debug(this + " addedTo "+parent+" index "+index);
//dojo.debug(parent.children);
//dojo.debug(parent.containerNode.innerHTML);
//dojo.debug((new Error()).stack);
if (this.tree !== parent.tree) {
this.updateTree(parent.tree);
}
if (parent.isTreeNode) {
if (!parent.isFolder) {
//dojo.debug("folderize parent "+parent);
parent.setFolder();
parent.state = parent.loadStates.LOADED;
}
}
var siblingsCount = parent.children.length;
// setFolder works BEFORE insertNode
this.insertNode(parent, index);
this.viewAddLayout();
//dojo.debug("siblings "+parent.children);
if (siblingsCount > 1) {
if (index == 0 && parent.children[1] instanceof dojo.widget.Widget) {
parent.children[1].viewUpdateLayout();
}
if (index == siblingsCount-1 && parent.children[siblingsCount-2] instanceof dojo.widget.Widget) {
parent.children[siblingsCount-2].viewUpdateLayout();
}
} else if (parent.isTreeNode) {
// added as the first child
//dojo.debug("added as first");
parent.viewSetHasChildren();
}
if (!dontPublishEvent) {
var message = {
child: this,
index: index,
parent: parent
}
dojo.event.topic.publish(this.tree.eventNames.afterAddChild, message);
}
//dojo.profile.end("addedTo");
},
/**
* Fast program-only hacky creation of widget
*
*/
createSimple: function(args, parent) {
// I pass no args and ignore default controller
//dojo.profile.start(this.widgetType+" createSimple");
//dojo.profile.start(this.widgetType+" createSimple constructor");
if (args.tree) {
var tree = args.tree;
} else if (parent) {
var tree = parent.tree;
} else {
dojo.raise("createSimple: can't evaluate tree");
}
tree = dojo.widget.byId(tree);
//dojo.debug(tree);
var treeNode = new tree.defaultChildWidget();
//dojo.profile.end(this.widgetType+" createSimple constructor");
//dojo.profile.start(this.widgetType+" createSimple mixin");
for(var x in args){ // fastMixIn
treeNode[x] = args[x];
}
//dojo.profile.end(this.widgetType+" createSimple mixin");
// HtmlWidget.postMixIn
treeNode.toggleObj = dojo.lfx.toggle[treeNode.toggle.toLowerCase()] || dojo.lfx.toggle.plain;
//dojo.profile.start(this.widgetType + " manager");
dojo.widget.manager.add(treeNode);
//dojo.profile.end(this.widgetType + " manager");
//dojo.profile.start(this.widgetType + " buildRendering");
treeNode.buildRendering(args, {}, parent);
//dojo.profile.end(this.widgetType + " buildRendering");
treeNode.initialize(args, {}, parent);
//dojo.profile.end(this.widgetType+"createSimple");
if (treeNode.parent) {
delete dojo.widget.manager.topWidgets[treeNode.widgetId];
}
return treeNode;
},
// can override e.g for case of div with +- text inside
viewUpdateLayout: function() {
//dojo.profile.start("viewUpdateLayout");
//dojo.debug("UpdateLayout in "+this);
this.viewRemoveLayout();
this.viewAddLayout();
//dojo.profile.end("viewUpdateLayout");
},
viewAddContainer: function() {
// make controller only if children exist
this.containerNode = this.tree.containerNodeTemplate.cloneNode(true);
this.domNode.appendChild(this.containerNode);
},
/*
viewRemoveContainer: function() {
// make controller only if children exist
this.domNode.removeChild(this.containerNode);
this.containerNode = null;
},
*/
viewAddLayout: function() {
//dojo.profile.start("viewAddLayout");
//dojo.debug("viewAddLayout in "+this);
if (this.parent["isTree"]) {
//dojo.debug("Parent isTree => add isTreeRoot");
// use setClass, not addClass for speed
dojo.html.setClass(this.domNode, dojo.html.getClass(this.domNode) + ' '+this.tree.classPrefix+'IsRoot')
}
//dojo.debug(this.parent.children.length);
//dojo.debug(this.parent.children[this.parent.children.length-1]);
if (this.isLastChild()) {
//dojo.debug("Checked last node for "+this);
//dojo.debug("Parent last is "+this.parent.children[this.parent.children.length-1]);
//dojo.debug("last node => add isTreeLast for "+this);
dojo.html.setClass(this.domNode, dojo.html.getClass(this.domNode) + ' '+this.tree.classPrefix+'IsLast')
}
//dojo.profile.end("viewAddLayout");
//dojo.debug("viewAddLayout out");
},
viewRemoveLayout: function() {
//dojo.debug("viewRemoveLayout in "+this);
//dojo.profile.start("viewRemoveLayout");
//dojo.debug((new Error()).stack);
dojo.html.removeClass(this.domNode, this.tree.classPrefix+"IsRoot");
dojo.html.removeClass(this.domNode, this.tree.classPrefix+"IsLast");
//dojo.profile.end("viewRemoveLayout");
},
viewGetExpandClass: function() {
if (this.isFolder) {
return this.isExpanded ? "ExpandOpen" : "ExpandClosed";
} else {
return "ExpandLeaf";
}
},
viewSetExpand: function() {
//dojo.profile.start("viewSetExpand");
//dojo.debug("viewSetExpand in "+this);
var expand = this.tree.classPrefix+this.viewGetExpandClass();
var reg = new RegExp("(^|\\s)"+this.tree.classPrefix+"Expand\\w+",'g');
dojo.html.setClass(this.domNode, dojo.html.getClass(this.domNode).replace(reg,'') + ' '+expand);
//dojo.debug(dojo.html.getClass(this.domNode))
//dojo.profile.end("viewSetExpand");
this.viewSetHasChildrenAndExpand();
},
viewGetChildrenClass: function() {
return 'Children'+(this.children.length ? 'Yes' : 'No');
},
viewSetHasChildren: function() {
//dojo.debug(this+' '+this.children.length)
var clazz = this.tree.classPrefix+this.viewGetChildrenClass();
var reg = new RegExp("(^|\\s)"+this.tree.classPrefix+"Children\\w+",'g');
dojo.html.setClass(this.domNode, dojo.html.getClass(this.domNode).replace(reg,'') + ' '+clazz);
this.viewSetHasChildrenAndExpand();
},
/**
* set TreeStateChildrenYes-ExpandClosed pair
* needed for IE, because IE reads only last class from .TreeChildrenYes.TreeExpandClosed pair
*/
viewSetHasChildrenAndExpand: function() {
var clazz = this.tree.classPrefix+'State'+this.viewGetChildrenClass()+'-'+this.viewGetExpandClass();
var reg = new RegExp("(^|\\s)"+this.tree.classPrefix+"State[\\w-]+",'g');
dojo.html.setClass(this.domNode, dojo.html.getClass(this.domNode).replace(reg,'') + ' '+clazz);
},
viewUnfocus: function() {
dojo.html.removeClass(this.labelNode, this.tree.classPrefix+"LabelFocused");
},
viewFocus: function() {
dojo.html.addClass(this.labelNode, this.tree.classPrefix+"LabelFocused");
},
viewEmphasize: function() {
dojo.html.clearSelection(this.labelNode);
dojo.html.addClass(this.labelNode, this.tree.classPrefix+'NodeEmphasized');
},
viewUnemphasize: function() {
dojo.html.removeClass(this.labelNode, this.tree.classPrefix+'NodeEmphasized');
},
// ================================ detach from parent ===================================
detach: function() {
if (!this.parent) return;
var parent = this.parent;
var index = this.getParentIndex();
this.doDetach.apply(this, arguments);
dojo.event.topic.publish(this.tree.eventNames.afterDetach,
{ child: this, parent: parent, index:index }
);
},
/* node does not leave tree */
doDetach: function() {
//dojo.debug("doDetach in "+this+" parent "+this.parent+" class "+dojo.html.getClass(this.domNode));
var parent = this.parent;
//dojo.debug(parent.containerNode.style.display)
if (!parent) return;
var index = this.getParentIndex();
this.viewRemoveLayout();
dojo.widget.DomWidget.prototype.removeChild.call(parent, this);
var siblingsCount = parent.children.length;
//dojo.debug("siblingsCount "+siblingsCount);
if (siblingsCount > 0) {
if (index == 0) { // deleted first node => update new first
parent.children[0].viewUpdateLayout();
}
if (index == siblingsCount) { // deleted last node
parent.children[siblingsCount-1].viewUpdateLayout();
}
} else {
if (parent.isTreeNode) {
parent.viewSetHasChildren();
}
}
if (this.tree.unsetFolderOnEmpty && !parent.children.length && parent.isTreeNode) {
parent.unsetFolder();
}
//dojo.debug(parent.containerNode.style.display)
this.parent = null;
},
/**
* publish destruction event so that controller may unregister/unlisten
*/
destroy: function() {
dojo.event.topic.publish(this.tree.eventNames.beforeNodeDestroy, { source: this } );
this.detach();
return dojo.widget.HtmlWidget.prototype.destroy.apply(this, arguments);
},
expand: function(){
if (this.isExpanded) return;
//dojo.profile.start("expand "+this);
//dojo.debug("expand in "+this);
//dojo.profile.start("expand - lazy init "+this);
if (this.tryLazyInit) {
this.setChildren();
this.tryLazyInit = false;
}
//dojo.profile.end("expand - lazy init "+this);
this.isExpanded = true;
this.viewSetExpand();
//dojo.profile.start("expand - showChildren "+this);
/**
* no matter if I have children or not. need to show/hide container anyway.
* use case: empty folder is expanded => then child is added, container already shown all fine
*/
this.showChildren();
//dojo.profile.end("expand - showChildren "+this);
//dojo.profile.end("expand "+this);
},
collapse: function(){
if (!this.isExpanded) return;
this.isExpanded = false;
this.hideChildren();
},
hideChildren: function(){
this.tree.toggleObj.hide(
this.containerNode, this.tree.toggleDuration, this.explodeSrc, dojo.lang.hitch(this, "onHideChildren")
);
},
showChildren: function(){
//dojo.profile.start("showChildren"+this);
this.tree.toggleObj.show(
this.containerNode, this.tree.toggleDuration, this.explodeSrc, dojo.lang.hitch(this, "onShowChildren")
);
//dojo.profile.end("showChildren"+this);
},
onShowChildren: function() {
//dojo.profile.start("onShowChildren"+this);
this.onShow();
//dojo.profile.end("onShowChildren"+this);
dojo.event.topic.publish(this.tree.eventNames.afterExpand, {source: this} );
},
onHideChildren: function() {
this.viewSetExpand();
this.onHide();
dojo.event.topic.publish(this.tree.eventNames.afterCollapse, {source: this} );
},
/* Edit current node : change properties and update contents */
setTitle: function(title) {
var oldTitle = this.title;
this.labelNode.innerHTML = this.title = title;
dojo.event.topic.publish(this.tree.eventNames.afterSetTitle, { source: this, oldTitle:oldTitle });
},
toString: function() {
return '['+this.widgetType+', '+this.title+']';
}
});