Multi Select in Dojo Tree

Dojo comes with a very nice widget "Tree".
Tree is bundled in dijit, it allows programmer to represent data in a tree view.
With dojo 1.2, creating an tree is quite easy. The 3 simple steps to create a simple tree are:
1) Create a data store
2) Create a model from this store
3) Create a tree, and assign the model to pick up the data from.

Using the default tree, user can expand/collapse a node, click on the node, and with just a little change in code have the ability to drag and drop nodes. But one thing that the default tree lacks is the ability to select multiple nodes in the tree. In this article, i'll show the simple way of customizing the dojo tree to have the ability to multi select nodes.

The best way to customize an existing widget is to extend the widget & create a new widget from it.
In this article, we'll create a new widget "dijit.TreeCustom", which extends from dijit.Tree.
In the custom widget, following four methods of dijit.Tree should be overridden

1) postCreate
2) _onClick
3) focusNode
4) blurNode

Custom Tree Widget:
Here is the skeleton code for custom tree widget, extending from default dojo tree.
dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");
dojo.require("dojo.dnd.Manager");

dojo.declare("dijit.TreeCustom", dijit.Tree, {
});
(If you need more details on creating an widget, refer my previous article "Creating custom widgets in DOJO")

Adding new properties for Custom tree widget
	lastFocusedNode: null, // To store the currently focused node in the tree.

	allFocusedNodes: [], // array of currently focused nodes. eg., user holds cntrl key and selects multiple nodes.

	ctrlKeyPressed: false, // Flag to find out if ctrl key was pressed, when click happened.
The properties are self-explanatory.

Overriding postCreate:
When dojo engine has finished creating & parsing the TreeCustom widget on browser, postCreate method is called, where in the initilization stuffs can take place. Here is the postCreate method for TreeCustom widget, initializing the properties to proper values.
	postCreate: function(){
		this.inherited(arguments); // Call parent's postCreate
		this.onClick = this.onClickDummy;
		this.allFocusedNodes = [];
		this.lastFocusedNode = null;
	},
Notice that, default onClick is dummied out, since the new tree widget will have to send out array of selected nodes for onClick event, rather than just one selected node. So, a new customOnClick event will be added for this purpose. (See below)

Dummy onclick method (Does nothing)
	onClickDummy: function(item, node) {
	},

Overriding _onClick:
_onClick is the method called by default tree whenever user clicks on the tree node.
	_onClick: function(/*Event*/ e){
		if(e.ctrlKey) {
			this.ctrlKeyPressed = true;
		} else {
			this.ctrlKeyPressed = false;
		}
		this.inherited(arguments); 
		dojo.stopEvent(e);
	},
This method, stores the state of ctrl key pressed, and passes on to default tree's _onClick, which inturn calls focusNode method, which is also one of the method that will be overridden for custom tree widget.

Overriding focusNode:
	focusNode: function(/* _tree.Node */ node){
		this.inherited(arguments); 
		
		this.lastFocusedNode = node;
		if(this.ctrlKeyPressed) {
			// Ctrl key was pressed
		} else {
			// Ctrl key was not pressed, blur the previously selected nodes except the clicked node.
			for(i=0; i < this.allFocusedNodes.length; i++) {
				var temp = this.allFocusedNodes[i];
				if(temp != node)
					this._customBlurNode(this.allFocusedNodes[i]);
			}
			this.allFocusedNodes = [];
		}
		var isExists = false; // Flag to find out if this node already been selected
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var temp = this.allFocusedNodes[i];
			if(temp.item.id == node.item.id) isExists = true;
		}
		if( ! isExists)
			this.allFocusedNodes.push(node);
		this.customOnClick (node.item, node, this.getSelectedItems() );
		this.ctrlKeyPressed = false;
	},
_customBlurNode method:
	_customBlurNode: function(node) {
		var labelNode = node.labelNode;
		dojo.removeClass(labelNode, "dijitTreeLabelFocused");
		labelNode.setAttribute("tabIndex", "-1");
		dijit.setWaiState(labelNode, "selected", false);
	},
customOnClick method: An event handler, that must be overriden by page using this widget. This method is called when user clicks on any node in the tree.
Passes node, item & array of all selected items to the method.
Here is the default implementation. (to avoid runtime exceptions)
	customOnClick: function (item, node, allSelectedItems) {
		//User overridable method.
	},
Overriding blurNode: Since a custom blur node method was used, the default blurNode is no more required. Overriding to cover the default behavior.
	blurNode: function(){
		// Not using, we've our own custom made blur method. See _customBlurNode
	},
Extra method to get all selected items
An utility method for the custom tree, which can be used to get list of all the selected items in the tree.
	// Returns array of currently selected items.
	getSelectedItems: function() {
		var selectedItems = [];
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var iNode = this.allFocusedNodes[i];
			selectedItems.push(iNode.item);
		}
		return selectedItems ;
	},
Final dijit.TreeCustom JS: Put this in TreeCustom.js file under dijit folder. (DOJO_HOME/dijit).
dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");
dojo.require("dojo.dnd.Manager");


dojo.declare("dijit.TreeCustom", dijit.Tree, {
	
	lastFocusedNode: null, // To store the currently focused node in the tree.

	allFocusedNodes: [], // array of currently focused nodes. eg., user holds cntrl key and selects multiple nodes.

	ctrlKeyPressed: false, // Flag to find out if ctrl key was pressed, when click happened.

	postCreate: function(){
		this.inherited(arguments);
		this.onClick = this.onClickDummy;
		this.allFocusedNodes = [];
		this.lastFocusedNode = null;
	},

	focusNode: function(/* _tree.Node */ node){
		this.inherited(arguments); 
		
		this.lastFocusedNode = node;
		if(this.ctrlKeyPressed) {
			// Ctrl key was pressed
		} else {
			// Ctrl key was not pressed, blur the previously selected nodes except the clicked node.
			for(i=0;i < this.allFocusedNodes.length; i++) {
				var temp = this.allFocusedNodes[i];
				if(temp != node)
					this._customBlurNode(this.allFocusedNodes[i]);
			}
			this.allFocusedNodes = [];
		}
		var isExists = false; // Flag to find out if this node already been selected
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var temp = this.allFocusedNodes[i];
			if(temp.item.id == node.item.id) isExists = true;
		}
		if( ! isExists)
			this.allFocusedNodes.push(node);
		this.customOnClick (node.item, node, this.getSelectedItems() );
		this.ctrlKeyPressed = false;
	},

	blurNode: function(){
		// Not using, we've our own custom made blur method. See _customBlurNode
	},
	
	_onBlur:function () {
	},

	_onClick: function(/*Event*/ e){
		if(e.ctrlKey) {
			this.ctrlKeyPressed = true;
		} else {
			this.ctrlKeyPressed = false;
		}
		this.inherited(arguments); 
		dojo.stopEvent(e);
	},

	_customBlurNode: function(node) {
		var labelNode = node.labelNode;
		dojo.removeClass(labelNode, "dijitTreeLabelFocused");
		labelNode.setAttribute("tabIndex", "-1");
		dijit.setWaiState(labelNode, "selected", false);
	},
	
	// Returns array of currently selected items.
	getSelectedItems: function() {
		var selectedItems = [];
		for(i=0;i < this.allFocusedNodes.length; i++) {
			var iNode = this.allFocusedNodes[i];
			selectedItems.push(iNode.item);
		}
		return selectedItems ;
	},

	onClickDummy: function(item, node) {
	},

	customOnClick: function (item, node, allSelectedItems) {
		//User overridable method.
	}

});



Example of using the Custom Tree
Put this html file under dijit/tests/tree.


	Dijit Tree Test

	

	
	

	
	

	
	

	







Let me know of any suggestion/changes for this custom tree code.

In next article, I'll show how to lazy load data in tree using DWR (Ajax).

10 comments:

  1. Hi Harish

    Your blog is very useful. I am working on dojo for first time. Can you help me how to do multiple selection of the nodes in dojo tree and copy them to second listbox

    ReplyDelete
  2. One thing that doesn't seem quite right -- if a node has children and is collapsed and I click on the plus sign to expand that node, it selects that item and calls the customOnClick method. I would not expect it to select an item just to expand it.

    ReplyDelete
  3. You are right Lee. Thats what a default dojo does, the click on plus signs also fires click on node itself.

    We may have to sepearate the plus sign from the node.

    ReplyDelete
  4. Thanks Harish for detailed explanation and code. Saved lot of time for me.

    If you could share info on resources that you referred for writing this would be GREAT.

    ReplyDelete
  5. hi Bhanu,

    It was more of a need for one of my project, which made me do it all by myself, did a little research on how dojo is structured, and used few things from internet.
    I can help you if you have questions, as far as i can.

    ReplyDelete
  6. Hi Harish,

    it is possible to use your custom tree when I include the dojo base like:
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/dojo.xd.js"></script>

    and your custom tree like:
    <script type="text/javascript" src="TreeCustom.js"></script>
    ?

    I tried it, but it doesnt work.

    Thanks in advance,
    Mewel

    ReplyDelete
  7. hi Matthai, sorry for late reply.

    I dont think the above will work, as it may not be able to render dojo widgets well. However, i've not tried it to be sure.

    ReplyDelete
  8. Harish,

    I have a question.

    Is it possible to implement multiple selction of tree nodes by using Shift+click. As I know multi-focus is not possible programatically and hence this is not possible but want to confirm from you.

    Any idea?

    Thanks,
    Bhanu

    ReplyDelete
  9. Hi Bhanu Prakash,

    I have not tried Shift+click. But I believe there is a way to find out if Shift was pressed when mouse click happened in Javascript. So, by combining those two, you should be able to accomplish it. However Practically I haven't done any similar stuff to give you clear solution.

    Please let me know if you do try & find anything interesting.

    Thanks
    Harish

    ReplyDelete
  10. There's also a 'paths' field in the default Tree implementation that exposes the current selection.

    ReplyDelete