Thursday, February 27, 2014

JQuery Dynatree - Limit number of multiple selections

JQuery Dynatree is another simple and useful plugin for display tree structures. Additionally it allows selection of nodes, single selection (radio-button) or multiple selections (Check-box). A simple multiple choice dynatree can be loaded with a script similar to this:
$("#dynaTree").dynatree({
        checkbox: true,
        classNames: { checkbox: "dynatree-checkbox " },
        selectMode: 2,
        children: TreeData,
        onSelect: function (select, node) {
            var selNodes = node.tree.getSelectedNodes();           
            var selTitles = $.map(selNodes, function (node) {
                return node.data.title;
            });
            var selKeys = $.map(selNodes, function (node) {
                return node.data.key;
            });           
        },
        onClick: function (node, event) {
            if (node.getEventTargetType(event) == "title") {
                    node.toggleSelect();
                }
        },
        onKeydown: function (node, event) {
            if (event.which == 32) {
                node.toggleSelect();
                return false;
            }
        }
    });
 This tree has selecteMode set to 2 which enables multiple selections. There is no limit to the number of selections. So what if we want to limit the number of selections to say, 5? Here’s how to do it. Every time an click event occurs on the tree onClick function is called first. Right now it only toggles selection if a title is clicked (we don’t need to toggle if click event is “checkbox” because it will result a double toggle). In order to limit selections, what we have to do is get the number of already selected nodes and if it is greater than or equals to 5 simply return false. Then it will not call onSelect function which add node to selected nodes. Here is the code:
$("#dynaTree").dynatree({
        checkbox: true,
        classNames: { checkbox: "dynatree-checkbox " },
        selectMode: 2,
        children: TreeData,
        onSelect: function (select, node) {
            var selNodes = node.tree.getSelectedNodes();
            var selTitles = $.map(selNodes, function (node) {
                return node.data.title;
            });
            var selKeys = $.map(selNodes, function (node) {
                return node.data.key;
            });           
        },
        onClick: function (node, event) {
            var selNodes = node.tree.getSelectedNodes();
            var selTitles = $.map(selNodes, function (node) {
                return node.data.title;
            });           
            if (node.getEventTargetType(event) == "title") {
                    if (selTitles.length >= 5 ) {
                        //disable toggle if selection limit is exceeded
                        return false;
                    }                  
            }               
             else if (node.getEventTargetType(event) == "checkbox") {
// disable selection if hospital limit is exceeded
                    if (selTitles.length >= 5 ) {
                        return false;
                    }                   
               }
        },
        onKeydown: function (node, event) {
            if (event.which == 32) {
              var selNodes = node.tree.getSelectedNodes();
              var selTitles = $.map(selNodes, function (node) {
                                  return node.data.title;
                            });           
              if (selTitles.length >= 5 ) {
                 return false;
              }                   
              else{
                node.toggleSelect();
                return false;
              }
            }
        }
    });

Apart from just disabling the selections when limit exceed, we might want to toggle selection if an already selected node is clicked again so that we can uncheck one node and select another node in its place. For that, we need to check whether the clicked node is selected or not. If selected we can toggle the selection. I modified the code to enable it:
$("#dynaTree").dynatree({
        checkbox: true,
        classNames: { checkbox: "dynatree-checkbox " },
        selectMode: 2,
        children: TreeData,
        onSelect: function (select, node) {
            var selNodes = node.tree.getSelectedNodes();
            var selTitles = $.map(selNodes, function (node) {
                return node.data.title;
            });
            var selKeys = $.map(selNodes, function (node) {
                return node.data.key;
            });           
        },
        onClick: function (node, event) {
            var selNodes = node.tree.getSelectedNodes();
            var selTitles = $.map(selNodes, function (node) {
                return node.data.title;
            });           
            if (node.getEventTargetType(event) == "title") {
              // click on a title
                    if (selTitles.length >= 5 && !node.isSelected()) {
              //disable toggle if limit is exceeded and node is not a selected one
                        return false;
                    }                  
                     else {
                           node.toggleSelect();
                     }
            }               
             else if (node.getEventTargetType(event) == "checkbox") {
              // click on a checkbox
                    if (selTitles.length >= 5 && !node.isSelected()) {
            //disable selection if hospital limit is exceeded and node is not a selected one
                        return false;
                    }                   
               }
        },
        onKeydown: function (node, event) {
            if (event.which == 32) {
var selNodes = node.tree.getSelectedNodes();
              var selTitles = $.map(selNodes, function (node) {
                                  return node.data.title;
                            });           
              if (selTitles.length >= 5 && !node.isSelected()) {
                 return false;
              }                   
              else{
                node.toggleSelect();
                return false;
              }
            }
        }
    });



Wednesday, February 26, 2014

JQuery async-treeview with unique set to true

I’ve been working in a project which uses JQuery treeview plugin to display a set of data which are queried from a database. This projects implementation is a bit old and so is the plugin. But it showed fairly well performance until recent where we had a heavy load of data added to our database. It became extremely slow loading, updating and adding folders to structure so that we frequently got unresponsive script errors.
Basic script to load a treeview is as follows:
   $("#tree").treeview({
        collapsed: true,
        animated: "medium",
        control: "#sidetreecontrol",
        unique: true
    });

Treeview as it is, requires whole tree structure available at the client side before render. So that is a lot of js to process mostly in browsers like IE. There were two options, one was to move on to newer and more efficient plugin and other was to try to use jquery.treeview.async.js which ships with treeview plugin to load data asynchronously.
I considered the second option first and found that with this extra code I could load the tree in chunks. In other words, in initial load it only needs root folder structure and when a node get expanded sub tree is loaded sending an ajax request back to server. It looked like exactly what we were looking for. So I went ahead added relevant functionality to use jquery.treeview.async.js following this blog post.
So that is the same script with few additional parameters ‘url’ and ‘ajax’:
$("#tree").treeview({
        collapsed: true,
        animated: "medium",
        control: "#treecontrol",
        unique: true,
 url: '/Home/GetTreeData/',
        ajax: {
            type: 'post'
        }
    });

It all worked fine until I set unique: true in the treeview because that’s how it was in pervious implementation. There is only one node open at a time when this attribute is set to true. The async treeview with unique set to true is the worst nightmare we could have because it started to send ajax requests for each and every node in the same level for a single expand or collapse. This happened because JQuery treeview handles unique by toggling the all other nodes. And async treeview extends the toggle function to send ajax requests and get child nodes which results a sequence of ajax calls back and forth. So setting unique true in an async tree was definitely not an option if efficiency is what you are after.
So I found a little work around to achieve the same functionality after some research. I simply extended the toggle function to send a second request to collapse any node that is already expanded in the same level for each click. And it reduced the amount of Json calls in a considerable amount. So it works the same as unique set to true but doesn’t send unnecessary ajax requests toggling all same level nodes.
Here is the code:
$("#tree").treeview({
        collapsed: true,
        animated: "medium",
        control: "#treecontrol",
        unique: false,
 url: '/Home/GetTreeData/',
        ajax: {
            type: 'post'
        },
        toggle: function () {
            var collpsable = $(this).parent().find(".collapsable");
            var id = $(this).attr("id")
            $.each(collpsable, function (index, element) {
                if (element.id !== id) {
                    element.children[0].click();
                }
            });
        }
    });