blob: 7155333c235327728c421d8a73821c290fcb465f [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.TreeBasicControllerV3");
dojo.require("dojo.event.*");
dojo.require("dojo.json")
dojo.require("dojo.io.*");
dojo.require("dojo.widget.TreeCommon");
dojo.require("dojo.widget.TreeNodeV3");
dojo.require("dojo.widget.TreeV3");
dojo.widget.defineWidget(
"dojo.widget.TreeBasicControllerV3",
[dojo.widget.HtmlWidget, dojo.widget.TreeCommon],
function(){
this.listenedTrees = {};
},
{
// TODO: do something with addChild / setChild, so that RpcController become able
// to hook on this and report to server
// TODO: make sure keyboard control stuff works when node is moved between trees
// node should be unfocus()'ed when it its ancestor is moved and tree,lastFocus - cleared
/**
* TreeCommon.listenTree will attach listeners to these events
*
* The logic behind the naming:
* 1. (after|before)
* 2. if an event refers to tree, then add "Tree"
* 3. add action
*/
listenTreeEvents: ["afterSetFolder", "afterTreeCreate", "beforeTreeDestroy"],
listenNodeFilter: function(elem) { return elem instanceof dojo.widget.Widget},
editor: null,
initialize: function(args) {
if (args.editor) {
this.editor = dojo.widget.byId(args.editor);
this.editor.controller = this;
}
},
getInfo: function(elem) {
return elem.getInfo();
},
onBeforeTreeDestroy: function(message) {
this.unlistenTree(message.source);
},
onAfterSetFolder: function(message) {
//dojo.profile.start("onTreeChange");
if (message.source.expandLevel > 0) {
this.expandToLevel(message.source, message.source.expandLevel);
}
if (message.source.loadLevel > 0) {
this.loadToLevel(message.source, message.source.loadLevel);
}
//dojo.profile.end("onTreeChange");
},
// down arrow
_focusNextVisible: function(nodeWidget) {
// if this is an expanded folder, get the first child
if (nodeWidget.isFolder && nodeWidget.isExpanded && nodeWidget.children.length > 0) {
returnWidget = nodeWidget.children[0];
} else {
// find a parent node with a sibling
while (nodeWidget.isTreeNode && nodeWidget.isLastChild()) {
nodeWidget = nodeWidget.parent;
}
if (nodeWidget.isTreeNode) {
var returnWidget = nodeWidget.parent.children[nodeWidget.getParentIndex()+1];
}
}
if (returnWidget && returnWidget.isTreeNode) {
this._focusLabel(returnWidget);
return returnWidget;
}
},
// up arrow
_focusPreviousVisible: function(nodeWidget) {
var returnWidget = nodeWidget;
// if younger siblings
if (!nodeWidget.isFirstChild()) {
var previousSibling = nodeWidget.parent.children[nodeWidget.getParentIndex()-1]
nodeWidget = previousSibling;
// if the previous nodeWidget is expanded, dive in deep
while (nodeWidget.isFolder && nodeWidget.isExpanded && nodeWidget.children.length > 0) {
returnWidget = nodeWidget;
// move to the last child
nodeWidget = nodeWidget.children[nodeWidget.children.length-1];
}
} else {
// if this is the first child, return the parent
nodeWidget = nodeWidget.parent;
}
if (nodeWidget && nodeWidget.isTreeNode) {
returnWidget = nodeWidget;
}
if (returnWidget && returnWidget.isTreeNode) {
this._focusLabel(returnWidget);
return returnWidget;
}
},
// right arrow
_focusZoomIn: function(nodeWidget) {
var returnWidget = nodeWidget;
// if not expanded, expand, else move to 1st child
if (nodeWidget.isFolder && !nodeWidget.isExpanded) {
this.expand(nodeWidget);
}else if (nodeWidget.children.length > 0) {
nodeWidget = nodeWidget.children[0];
}
if (nodeWidget && nodeWidget.isTreeNode) {
returnWidget = nodeWidget;
}
if (returnWidget && returnWidget.isTreeNode) {
this._focusLabel(returnWidget);
return returnWidget;
}
},
// left arrow
_focusZoomOut: function(node) {
var returnWidget = node;
// if not expanded, expand, else move to 1st child
if (node.isFolder && node.isExpanded) {
this.collapse(node);
} else {
node = node.parent;
}
if (node && node.isTreeNode) {
returnWidget = node;
}
if (returnWidget && returnWidget.isTreeNode) {
this._focusLabel(returnWidget);
return returnWidget;
}
},
onFocusNode: function(e) {
var node = this.domElement2TreeNode(e.target);
if (node) {
node.viewFocus();
dojo.event.browser.stopEvent(e);
}
},
onBlurNode: function(e) {
var node = this.domElement2TreeNode(e.target);
if (!node) {
return;
}
var labelNode = node.labelNode;
labelNode.setAttribute("tabIndex", "-1");
node.viewUnfocus();
dojo.event.browser.stopEvent(e);
// this could have been set to -1 by the shift+TAB processing
node.tree.domNode.setAttribute("tabIndex", "0");
},
_focusLabel: function(node) {
//dojo.debug((new Error()).stack)
var lastFocused = node.tree.lastFocused;
var labelNode;
if (lastFocused && lastFocused.labelNode) {
labelNode = lastFocused.labelNode;
// help Opera out with blur events
dojo.event.disconnect(labelNode, "onblur", this, "onBlurNode");
labelNode.setAttribute("tabIndex", "-1");
dojo.html.removeClass(labelNode, "TreeLabelFocused");
}
// set tabIndex so that the tab key can find this node
labelNode = node.labelNode;
labelNode.setAttribute("tabIndex", "0");
node.tree.lastFocused = node;
// add an outline - this helps opera a lot
dojo.html.addClass(labelNode, "TreeLabelFocused");
dojo.event.connectOnce(labelNode, "onblur", this, "onBlurNode");
// prevent the domNode from seeing the focus event
dojo.event.connectOnce(labelNode, "onfocus", this, "onFocusNode");
// set focus so that the label wil be voiced using screen readers
labelNode.focus();
},
onKey: function(e) {
if (!e.key || e.ctrkKey || e.altKey) { return; }
// pretend the key was directed toward the current focused node (helps opera out)
var nodeWidget = this.domElement2TreeNode(e.target);
if (!nodeWidget) {
return;
}
var treeWidget = nodeWidget.tree;
if (treeWidget.lastFocused && treeWidget.lastFocused.labelNode) {
nodeWidget = treeWidget.lastFocused;
}
switch(e.key) {
case e.KEY_TAB:
if (e.shiftKey) {
// we're moving backwards so don't tab to the domNode
// it'll be added back in onBlurNode
treeWidget.domNode.setAttribute("tabIndex", "-1");
}
break;
case e.KEY_RIGHT_ARROW:
this._focusZoomIn(nodeWidget);
dojo.event.browser.stopEvent(e);
break;
case e.KEY_LEFT_ARROW:
this._focusZoomOut(nodeWidget);
dojo.event.browser.stopEvent(e);
break;
case e.KEY_UP_ARROW:
this._focusPreviousVisible(nodeWidget);
dojo.event.browser.stopEvent(e);
break;
case e.KEY_DOWN_ARROW:
this._focusNextVisible(nodeWidget);
dojo.event.browser.stopEvent(e);
break;
}
},
onFocusTree: function(e) {
if (!e.currentTarget) { return; }
try {
var treeWidget = this.getWidgetByNode(e.currentTarget);
if (!treeWidget || !treeWidget.isTree) { return; }
// on first focus, choose the root node
var nodeWidget = this.getWidgetByNode(treeWidget.domNode.firstChild);
if (nodeWidget && nodeWidget.isTreeNode) {
if (treeWidget.lastFocused && treeWidget.lastFocused.isTreeNode) { // onClick could have chosen a non-root node
nodeWidget = treeWidget.lastFocused;
}
this._focusLabel(nodeWidget);
}
}
catch(e) {}
},
// perform actions-initializers for tree
onAfterTreeCreate: function(message) {
var tree = message.source;
dojo.event.browser.addListener(tree.domNode, "onKey", dojo.lang.hitch(this, this.onKey));
dojo.event.browser.addListener(tree.domNode, "onmousedown", dojo.lang.hitch(this, this.onTreeMouseDown));
dojo.event.browser.addListener(tree.domNode, "onclick", dojo.lang.hitch(this, this.onTreeClick));
dojo.event.browser.addListener(tree.domNode, "onfocus", dojo.lang.hitch(this, this.onFocusTree));
tree.domNode.setAttribute("tabIndex", "0");
if (tree.expandLevel) {
this.expandToLevel(tree, tree.expandLevel)
}
if (tree.loadLevel) {
this.loadToLevel(tree, tree.loadLevel);
}
},
onTreeMouseDown: function(e) {
},
onTreeClick: function(e){
//dojo.profile.start("onTreeClick");
var domElement = e.target;
//dojo.debug('click')
// find node
var node = this.domElement2TreeNode(domElement);
if (!node || !node.isTreeNode) {
return;
}
var checkExpandClick = function(el) {
return el === node.expandNode;
}
if (this.checkPathCondition(domElement, checkExpandClick)) {
this.processExpandClick(node);
}
this._focusLabel(node);
//dojo.profile.end("onTreeClick");
},
processExpandClick: function(node){
//dojo.profile.start("processExpandClick");
if (node.isExpanded){
this.collapse(node);
} else {
this.expand(node);
}
//dojo.profile.end("processExpandClick");
},
/**
* time between expand calls for batch operations
* @see expandToLevel
*/
batchExpandTimeout: 20,
expandAll: function(nodeOrTree) {
return this.expandToLevel(nodeOrTree, Number.POSITIVE_INFINITY);
},
collapseAll: function(nodeOrTree) {
var _this = this;
var filter = function(elem) {
return (elem instanceof dojo.widget.Widget) && elem.isFolder && elem.isExpanded;
}
if (nodeOrTree.isTreeNode) {
this.processDescendants(nodeOrTree, filter, this.collapse);
} else if (nodeOrTree.isTree) {
dojo.lang.forEach(nodeOrTree.children,function(c) { _this.processDescendants(c, filter, _this.collapse) });
}
},
/**
* expand tree to specific node
*/
expandToNode: function(node, withSelected) {
n = withSelected ? node : node.parent
s = []
while (!n.isExpanded) {
s.push(n)
n = n.parent
}
dojo.lang.forEach(s, function(n) { n.expand() })
},
/**
* walk a node in time, forward order, with pauses between expansions
*/
expandToLevel: function(nodeOrTree, level) {
dojo.require("dojo.widget.TreeTimeoutIterator");
var _this = this;
var filterFunc = function(elem) {
var res = elem.isFolder || elem.children && elem.children.length;
//dojo.debug("Filter "+elem+ " result:"+res);
return res;
};
var callFunc = function(node, iterator) {
_this.expand(node, true);
iterator.forward();
}
var iterator = new dojo.widget.TreeTimeoutIterator(nodeOrTree, callFunc, this);
iterator.setFilter(filterFunc);
iterator.timeout = this.batchExpandTimeout;
//dojo.debug("here "+nodeOrTree+" level "+level);
iterator.setMaxLevel(nodeOrTree.isTreeNode ? level-1 : level);
return iterator.start(nodeOrTree.isTreeNode);
},
getWidgetByNode: function(node) {
var widgetId;
var newNode = node;
while (! (widgetId = newNode.widgetId) ) {
newNode = newNode.parentNode;
if (newNode == null) { break; }
}
if (widgetId) { return dojo.widget.byId(widgetId); }
else if (node == null) { return null; }
else{ return dojo.widget.manager.byNode(node); }
},
/**
* callout activated even if node is expanded already
*/
expand: function(node) {
//dojo.profile.start("expand");
//dojo.debug("Expand "+node.isFolder);
if (node.isFolder) {
node.expand(); // skip trees or non-folders
}
//dojo.profile.end("expand");
},
/**
* safe to call on tree and non-folder
*/
collapse: function(node) {
if (node.isFolder) {
node.collapse();
}
},
// -------------------------- TODO: Inline edit node ---------------------
canEditLabel: function(node) {
if (node.actionIsDisabledNow(node.actions.EDIT)) return false;
return true;
},
editLabelStart: function(node) {
if (!this.canEditLabel(node)) {
return false;
}
if (!this.editor.isClosed()) {
//dojo.debug("editLabelStart editor open");
this.editLabelFinish(this.editor.saveOnBlur);
}
this.doEditLabelStart(node);
},
editLabelFinish: function(save) {
this.doEditLabelFinish(save);
},
doEditLabelStart: function(node) {
if (!this.editor) {
dojo.raise(this.widgetType+": no editor specified");
}
//dojo.debug("editLabelStart editor open "+node);
this.editor.open(node);
},
doEditLabelFinish: function(save, server_data) {
//dojo.debug("Finish "+save);
//dojo.debug((new Error()).stack)
if (!this.editor) {
dojo.raise(this.widgetType+": no editor specified");
}
var node = this.editor.node;
var editorTitle = this.editor.getContents();
this.editor.close(save);
if (save) {
var data = {title:editorTitle};
if (server_data) { // may be undefined
dojo.lang.mixin(data, server_data);
}
if (node.isPhantom) {
// I can't just set node phantom's title, because widgetId/objectId/widgetName...
// may be provided by server
var parent = node.parent;
var index = node.getParentIndex();
node.destroy();
// new node was added!
dojo.widget.TreeBasicControllerV3.prototype.doCreateChild.call(this, parent, index, data);
} else {
var title = server_data && server_data.title ? server_data.title : editorTitle;
// use special method to make sure everything updated and event sent
node.setTitle(title);
}
} else {
//dojo.debug("Kill phantom on cancel");
if (node.isPhantom) {
node.destroy();
}
}
},
makeDefaultNode: function(parent, index) {
var data = {title:parent.tree.defaultChildTitle};
return dojo.widget.TreeBasicControllerV3.prototype.doCreateChild.call(this,parent,index,data);
},
/**
* check that something is possible
* run maker to do it
* run exposer to expose result to visitor immediatelly
* exposer does not affect result
*/
runStages: function(check, prepare, make, finalize, expose, args) {
if (check && !check.apply(this, args)) {
return false;
}
if (prepare && !prepare.apply(this, args)) {
return false;
}
var result = make.apply(this, args);
if (finalize) {
finalize.apply(this,args);
}
if (!result) {
return result;
}
if (expose) {
expose.apply(this, args);
}
return result;
}
});
// create and edit
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
createAndEdit: function(parent, index) {
var data = {title:parent.tree.defaultChildTitle};
if (!this.canCreateChild(parent, index, data)) {
return false;
}
var child = this.doCreateChild(parent, index, data);
if (!child) return false;
this.exposeCreateChild(parent, index, data);
child.isPhantom = true;
if (!this.editor.isClosed()) {
//dojo.debug("editLabelStart editor open");
this.editLabelFinish(this.editor.saveOnBlur);
}
this.doEditLabelStart(child);
}
});
// =============================== clone ============================
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
canClone: function(child, newParent, index, deep){
return true;
},
clone: function(child, newParent, index, deep) {
return this.runStages(
this.canClone, this.prepareClone, this.doClone, this.finalizeClone, this.exposeClone, arguments
);
},
exposeClone: function(child, newParent) {
if (newParent.isTreeNode) {
this.expand(newParent);
}
},
doClone: function(child, newParent, index, deep) {
//dojo.debug("Clone "+child);
var cloned = child.clone(deep);
newParent.addChild(cloned, index);
return cloned;
}
});
// =============================== detach ============================
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
canDetach: function(child) {
if (child.actionIsDisabledNow(child.actions.DETACH)) {
return false;
}
return true;
},
detach: function(node) {
return this.runStages(
this.canDetach, this.prepareDetach, this.doDetach, this.finalizeDetach, this.exposeDetach, arguments
);
},
doDetach: function(node, callObj, callFunc) {
node.detach();
}
});
// =============================== destroy ============================
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
canDestroyChild: function(child) {
if (child.parent && !this.canDetach(child)) {
return false;
}
return true;
},
destroyChild: function(node) {
return this.runStages(
this.canDestroyChild, this.prepareDestroyChild, this.doDestroyChild, this.finalizeDestroyChild, this.exposeDestroyChild, arguments
);
},
doDestroyChild: function(node) {
node.destroy();
}
});
// =============================== move ============================
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
/**
* check for non-treenodes
*/
canMoveNotANode: function(child, parent) {
if (child.treeCanMove) {
return child.treeCanMove(parent);
}
return true;
},
/**
* Checks whether it is ok to change parent of child to newParent
* May incur type checks etc
*
* It should check only hierarchical possibility w/o index, etc
* because in onDragOver event for Between Dnd mode we can't calculate index at once on onDragOVer.
* index changes as client moves mouse up-down over the node
*/
canMove: function(child, newParent){
if (!child.isTreeNode) {
return this.canMoveNotANode(child, newParent);
}
if (child.actionIsDisabledNow(child.actions.MOVE)) {
return false;
}
// if we move under same parent then no matter if ADDCHILD disabled for him
// but if we move to NEW parent then check if action is disabled for him
// also covers case for newParent being a non-folder in strict mode etc
if (child.parent !== newParent && newParent.actionIsDisabledNow(newParent.actions.ADDCHILD)) {
return false;
}
// Can't move parent under child. check whether new parent is child of "child".
var node = newParent;
while(node.isTreeNode) {
//dojo.debugShallow(node.title)
if (node === child) {
// parent of newParent is child
return false;
}
node = node.parent;
}
return true;
},
move: function(child, newParent, index/*,...*/) {
return this.runStages(this.canMove, this.prepareMove, this.doMove, this.finalizeMove, this.exposeMove, arguments);
},
doMove: function(child, newParent, index) {
//dojo.debug("MOVE "+child);
child.tree.move(child, newParent, index);
return true;
},
exposeMove: function(child, newParent) {
if (newParent.isTreeNode) {
this.expand(newParent);
}
}
});
dojo.lang.extend(dojo.widget.TreeBasicControllerV3, {
// -----------------------------------------------------------------------------
// Create node stuff
// -----------------------------------------------------------------------------
canCreateChild: function(parent, index, data) {
if (parent.actionIsDisabledNow(parent.actions.ADDCHILD)) {
return false;
}
return true;
},
/* send data to server and add child from server */
/* data may contain an almost ready child, or anything else, suggested to server */
/*in Rpc controllers server responds with child data to be inserted */
createChild: function(parent, index, data) {
if(!data) {
data = {title:parent.tree.defaultChildTitle};
}
return this.runStages(this.canCreateChild, this.prepareCreateChild, this.doCreateChild, this.finalizeCreateChild, this.exposeCreateChild,
[parent, index, data]);
},
prepareCreateChild: function() { return true; },
finalizeCreateChild: function() {},
doCreateChild: function(parent, index, data) {
//dojo.debug("doCreateChild parent "+parent+" index "+index+" data "+data);
var newChild = parent.tree.createNode(data);
//var newChild = dojo.widget.createWidget(widgetType, data);
parent.addChild(newChild, index);
return newChild;
},
exposeCreateChild: function(parent) {
return this.expand(parent);
}
});