Configuring jstree right-click contextmenu for different node types

JqueryContextmenuJstree

Jquery Problem Overview


I've seen an example somewhere online showing how to customise the appearance of jstree's right-click context menu (using contextmenu plugin).

For example, allow my users to delete "documents" but not "folders" (by hiding the "delete" option from the context menu for folders).

Now I can't find that example. Can anyone point me in the right direction? The official documentation didn't really help.

Edit:

Since I want the default context menu with only one or two minor changes, I'd prefer to not recreate the whole menu (though of course I will if it's the only way). What I'd like to do is something like this:

"contextmenu" : {
    items: {
        "ccp" : false,
        "create" : {
            // The item label
            "label" : "Create",
            // The function to execute upon a click
            "action": function (obj) { this.create(obj); },
            "_disabled": function (obj) { 
                alert("obj=" + obj); 
                return "default" != obj.attr('rel'); 
            }
        }
    }
}

but it doesn't work - the create item is just always disabled (the alert never appears).

Jquery Solutions


Solution 1 - Jquery

The contextmenu plugin already has support for this. From the documentation you linked to:

> items: Expects an object or a function, which should return an object. If a function is used it fired in the tree's context and receives one argument - the node that was right clicked.

So rather than give contextmenu a hard-coded object to work with, you can supply the following function. It checks the element that was clicked for a class named "folder", and removes the "delete" menu item by deleting it from the object:

function customMenu(node) {
    // The default set of all items
    var items = {
        renameItem: { // The "rename" menu item
            label: "Rename",
            action: function () {...}
        },
        deleteItem: { // The "delete" menu item
            label: "Delete",
            action: function () {...}
        }
    };

    if ($(node).hasClass("folder")) {
        // Delete the "delete" menu item
        delete items.deleteItem;
    }

    return items;
}

Note that the above will hide the delete option completely, but the plugin also allows you to show an item while disabling its behaviour, by adding _disabled: true to the relevant item. In this case you can use items.deleteItem._disabled = true within the if statement instead.

Should be obvious, but remember to initialise the plugin with the customMenu function instead of what you had previously:

$("#tree").jstree({plugins: ["contextmenu"], contextmenu: {items: customMenu}});
//                                                                    ^
// ___________________________________________________________________|

Edit: If you don't want the menu to be recreated on every right-click, you can put the logic in the action handler for the delete menu item itself.

"label": "Delete",
"action": function (obj) {
    if ($(this._get_node(obj)).hasClass("folder") return; // cancel action
}

Edit again: After looking at the jsTree source code, it looks like the contextmenu is being re-created every time it is shown anyway (see the show() and parse() functions), so I don't see a problem with my first solution.

However, I do like the notation you are suggesting, with a function as the value for _disabled. A potential path to explore is to wrap their parse() function with your own one that evaluates the function at disabled: function () {...} and stores the result in _disabled, before calling the original parse().

It won't be difficult either to modify their source code directly. Line 2867 of version 1.0-rc1 is the relevant one:

str += "<li class='" + (val._class || "") + (val._disabled ? " jstree-contextmenu-disabled " : "") + "'><ins ";

You can simply add a line before this one that checks $.isFunction(val._disabled), and if so, val._disabled = val._disabled(). Then submit it to the creators as a patch :)

Solution 2 - Jquery

Implemented with different node types:

$('#jstree').jstree({
    'contextmenu' : {
        'items' : customMenu
    },
    'plugins' : ['contextmenu', 'types'],
    'types' : {
        '#' : { /* options */ },
        'level_1' : { /* options */ },
        'level_2' : { /* options */ }
        // etc...
    }
});

And the customMenu function:

function customMenu(node)
{
    var items = {
        'item1' : {
            'label' : 'item1',
            'action' : function () { /* action */ }
        },
        'item2' : {
            'label' : 'item2',
            'action' : function () { /* action */ }
        }
    }

    if (node.type === 'level_1') {
        delete items.item2;
    } else if (node.type === 'level_2') {
        delete items.item1;
    }

    return items;
}

Solution 3 - Jquery

To clear everything.

Instead of this:

$("#xxx").jstree({
    'plugins' : 'contextmenu',
    'contextmenu' : {
        'items' : { ... bla bla bla ...}
    }
});

Use this:

$("#xxx").jstree({
    'plugins' : 'contextmenu',
    'contextmenu' : {
        'items' : customMenu
    }
});

Solution 4 - Jquery

I have adapted the suggested solution for working with types a bit differently though, perhaps it can help someone else:

Where #{$id_arr[$k]} is the reference to the div container... in my case I use many trees so all this code will be the output to the browser, but you get the idea.. Basically I want all the context menu options but only 'Create' and 'Paste' on the Drive node. Obviously with the correct bindings to those operations later on:

<div id="$id_arr[$k]" class="jstree_container"></div>
</div>
</li>
<!-- JavaScript neccessary for this tree : {$value} -->
<script type="text/javascript" >
jQuery.noConflict();
jQuery(function ($) {
// This is for the context menu to bind with operations on the right clicked node
function customMenu(node) {
    // The default set of all items
    var control;
    var items = {
        createItem: {
            label: "Create",
            action: function (node) { return { createItem: this.create(node) }; }
        },
        renameItem: {
            label: "Rename",
            action: function (node) { return { renameItem: this.rename(node) }; }
        },
        deleteItem: {
            label: "Delete",
            action: function (node) { return { deleteItem: this.remove(node) }; },
            "separator_after": true
        },
        copyItem: {
            label: "Copy",
            action: function (node) { $(node).addClass("copy"); return { copyItem: this.copy(node) }; }
        },
        cutItem: {
            label: "Cut",
            action: function (node) { $(node).addClass("cut"); return { cutItem: this.cut(node) }; }
        },
        pasteItem: {
            label: "Paste",
            action: function (node) { $(node).addClass("paste"); return { pasteItem: this.paste(node) }; }
        }
    };

    // We go over all the selected items as the context menu only takes action on the one that is right clicked
    $.jstree._reference("#{$id_arr[$k]}").get_selected(false, true).each(function (index, element) {
        if ($(element).attr("id") != $(node).attr("id")) {
            // Let's deselect all nodes that are unrelated to the context menu -- selected but are not the one right clicked
            $("#{$id_arr[$k]}").jstree("deselect_node", '#' + $(element).attr("id"));
        }
    });

    //if any previous click has the class for copy or cut
    $("#{$id_arr[$k]}").find("li").each(function (index, element) {
        if ($(element) != $(node)) {
            if ($(element).hasClass("copy") || $(element).hasClass("cut")) control = 1;
        }
        else if ($(node).hasClass("cut") || $(node).hasClass("copy")) {
            control = 0;
        }
    });

    //only remove the class for cut or copy if the current operation is to paste
    if ($(node).hasClass("paste")) {
        control = 0;
        // Let's loop through all elements and try to find if the paste operation was done already
        $("#{$id_arr[$k]}").find("li").each(function (index, element) {
            if ($(element).hasClass("copy")) $(this).removeClass("copy");
            if ($(element).hasClass("cut")) $(this).removeClass("cut");
            if ($(element).hasClass("paste")) $(this).removeClass("paste");
        });
    }
    switch (control) {
        //Remove the paste item from the context menu
        case 0:
            switch ($(node).attr("rel")) {
                case "drive":
                    delete items.renameItem;
                    delete items.deleteItem;
                    delete items.cutItem;
                    delete items.copyItem;
                    delete items.pasteItem;
                    break;
                case "default":
                    delete items.pasteItem;
                    break;
            }
            break;
            //Remove the paste item from the context menu only on the node that has either copy or cut added class
        case 1:
            if ($(node).hasClass("cut") || $(node).hasClass("copy")) {
                switch ($(node).attr("rel")) {
                    case "drive":
                        delete items.renameItem;
                        delete items.deleteItem;
                        delete items.cutItem;
                        delete items.copyItem;
                        delete items.pasteItem;
                        break;
                    case "default":
                        delete items.pasteItem;
                        break;
                }
            }
            else //Re-enable it on the clicked node that does not have the cut or copy class
            {
                switch ($(node).attr("rel")) {
                    case "drive":
                        delete items.renameItem;
                        delete items.deleteItem;
                        delete items.cutItem;
                        delete items.copyItem;
                        break;
                }
            }
            break;

            //initial state don't show the paste option on any node
        default: switch ($(node).attr("rel")) {
            case "drive":
                delete items.renameItem;
                delete items.deleteItem;
                delete items.cutItem;
                delete items.copyItem;
                delete items.pasteItem;
                break;
            case "default":
                delete items.pasteItem;
                break;
        }
            break;
    }
    return items;
$("#{$id_arr[$k]}").jstree({
  // List of active plugins used
  "plugins" : [ "themes","json_data", "ui", "crrm" , "hotkeys" , "types" , "dnd", "contextmenu"],
  "contextmenu" : { "items" : customMenu  , "select_node": true},

Solution 5 - Jquery

Btw: If you just want to remove options from the existing context menu - this worked for me:

function customMenu(node)
{
    var items = $.jstree.defaults.contextmenu.items(node);

    if (node.type === 'root') {
        delete items.create;
        delete items.rename;
        delete items.remove;
        delete items.ccp;
    }

    return items;
}

Solution 6 - Jquery

You can modify @Box9 code as to suit your requirement of dynamic disabling of context menu as:

function customMenu(node) {

  ............
  ................
   // Disable  the "delete" menu item  
   // Original // delete items.deleteItem; 
   if ( node[0].attributes.yyz.value == 'notdelete'  ) {
       
       
       items.deleteItem._disabled = true;
    }   

}  

You need add one attribute "xyz" in your XML or JSOn data

Solution 7 - Jquery

as of jsTree 3.0.9 I needed to use something like

var currentNode = treeElem.jstree('get_node', node, true);
if (currentNode.hasClass("folder")) {
    // Delete the "delete" menu item
    delete items.deleteItem;
}

because the node object that is provided is not a jQuery object.

Solution 8 - Jquery

David's response seems fine and efficient. I have found another variation of the solution where you can use a_attr attribute to differentiate different nodes and based on that you can generate different context menu.

In the below example, I have used two types of nodes Folder and Files. I have used different icons too using glyphicon. For file type node, you can only get context menu to rename and remove. For Folder, all options are there, create file, create folder, rename, remove.

For complete code snippet, you can view https://everyething.com/Example-of-jsTree-with-different-context-menu-for-different-node-type

 $('#SimpleJSTree').jstree({
                "core": {
                    "check_callback": true,
                    'data': jsondata
                    
                },
                "plugins": ["contextmenu"],
                "contextmenu": {
                    "items": function ($node) {
                        var tree = $("#SimpleJSTree").jstree(true);
						if($node.a_attr.type === 'file')
							return getFileContextMenu($node, tree);
						else
							return getFolderContextMenu($node, tree);                        
                    }
                }
            });

Initial json data has been as below, where node type is mentioned within a_attr.

var jsondata = [
                           { "id": "ajson1", "parent": "#", "text": "Simple root node", icon: 'glyphicon glyphicon-folder-open', "a_attr": {type:'folder'} },
                           { "id": "ajson2", "parent": "#", "text": "Root node 2", icon: 'glyphicon glyphicon-folder-open', "a_attr": {type:'folder'} },
                           { "id": "ajson3", "parent": "ajson2", "text": "Child 1", icon: 'glyphicon glyphicon-folder-open', "a_attr": {type:'folder'} },
                           { "id": "ajson4", "parent": "ajson2", "text": "Child 2", icon: 'glyphicon glyphicon-folder-open', "a_attr": {type:'folder'} },
            ];

As part of contect menu item to create a file and folder use similar code below, as file action.

action: function (obj) {
								$node = tree.create_node($node, { text: 'New File', icon: 'glyphicon glyphicon-file', a_attr:{type:'file'} });
								tree.deselect_all();
								tree.select_node($node);
							}

as folder action:

action: function (obj) {
								$node = tree.create_node($node, { text: 'New Folder', icon:'glyphicon glyphicon-folder-open', a_attr:{type:'folder'} });
								tree.deselect_all();
								tree.select_node($node);
							}

Solution 9 - Jquery

Here is my full plugin setup.

var ktTreeDocument = $("#jstree_html_id");

jQuery(document).ready(function () {
    DocumentKTTreeview.init();
});

var DocumentKTTreeview = function() {
    var treeDocument = function() {
        ktTreeDocument.jstree({
            "core": {
                "themes": {
                    "responsive": false
                },
                "check_callback": function(operation, node, node_parent, node_position, more) {
                    documentAllModuleObj.selectedNode = ktTreeDocument.jstree().get_selected('full', true);
                    if (operation === 'delete_node') {
                        if (!confirm('are you sure?')) {
                            return false;
                        }
                    }
                    return true;
                },
                'data': {
                    'dataType': 'json',
                    'url': BASE_URL + ('tree/get/?lazy'),
                    'data': function(node) {
                        return { 'id': node.id };
                    }
                },
            },
            "types": {
                "default": {
                    "icon": "fa fa-folder kt-font-success"
                },
                "file": {
                    "icon": "fa fa-file  kt-font-success"
                }
            },
            "state": { "key": "demo2" },
            "plugins": ["contextmenu", "dnd", "state", "types"],
            "contextmenu": {
                "items": function($node) {
                    var tree = $("#jstree_html_id").jstree(true);
                    return {
                        "Create": {
                            "separator_before": false,
                            "separator_after": false,
                            "label": "Create",
                            "action": function(obj) {
                                tree.create_node($node);
                            }
                        },
                        "Rename": {
                            "separator_before": false,
                            "separator_after": false,
                            "label": "Rename",
                            "action": function(obj) {
                                tree.edit($node);
                            }
                        },
                        "Remove": {
                            "separator_before": false,
                            "separator_after": false,
                            "_disabled": $node.original.root ? true : false,
                            "label": "Remove",
                            "action": function(obj) {
                                tree.delete_node($node);
                            }
                        }
                    };
                }
            }
        })
    }
    return {
        init: function() {
            treeDocument();
        }
    };
}();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMGOwenView Question on Stackoverflow
Solution 1 - JqueryDavid TangView Answer on Stackoverflow
Solution 2 - JquerystackedView Answer on Stackoverflow
Solution 3 - JqueryMangirdasView Answer on Stackoverflow
Solution 4 - JqueryJean G.TView Answer on Stackoverflow
Solution 5 - JqueryFlorian S.View Answer on Stackoverflow
Solution 6 - Jqueryuser367134View Answer on Stackoverflow
Solution 7 - JquerycraighView Answer on Stackoverflow
Solution 8 - JqueryAsif NowajView Answer on Stackoverflow
Solution 9 - JqueryGobinda NandiView Answer on Stackoverflow