88

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).

Joel Peltonen
  • 13,025
  • 6
  • 64
  • 100
MGOwen
  • 6,562
  • 13
  • 58
  • 67

9 Answers9

153

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 :)

David Tang
  • 92,262
  • 30
  • 167
  • 149
  • Thanks. I thought I once saw a solution involving changing only what needed changing from the default (rather than recreating the whole menu from scratch). I'll accept this answer if there is no better solution before the bounty expires. – MGOwen Jan 05 '11 at 23:25
  • @MGOwen, conceptually I *am* modifying the "default", but yes you're right that the object gets re-created each time the function is called. However, the default needs to be cloned first, otherwise the default itself is modified (and you'll need more complex logic to revert it back to the original state). An alternative I can think of is to move `var items` to outside of the function so its created only once, and return a selection of items from the function, e.g. `return {renameItem: items.renameItem};` or `return {renameItem: items.renameItem, deleteItem: items.deleteItem};` – David Tang Jan 05 '11 at 23:35
  • I like that last one especially, where you modify the jstree source. I tried it and it works, the function assigned to "_disabled" (in my example) runs. But, it doesn't help because I can't access the node (I at least need it's rel attribute to filter nodes by node type) from within the function's scope. I tried inspecting the variables I could pass in from the jstree source code but couldn't find the node. Any ideas? – MGOwen Jan 12 '11 at 03:54
  • @Box9 By the time it gets that far, $.vakata.context.tgt == false. I've looked through all the variables I could find to pass ($.vakata, s, val, i) and there's nothing referencing the node right-clicked. I'll try your original suggestion. Thanks. – MGOwen Jan 17 '11 at 04:24
  • @Box9 - This helped tremendously. Thank you! – JasCav Feb 18 '11 at 15:37
  • 2
    in jstree 3.0.8: `if ($(node).hasClass("folder"))` didn't work. but this did: `if (node.children.length > 0) { items.deleteItem._disabled = true; }` – Ryan Vettese Nov 27 '14 at 14:58
  • 1
    What code are you putting inside `action: function () {...}` in the first snippet? What if you want to use the "normal" functionality and menu items, but simply remove one of them (e.g. remove Delete but keep Rename and Create)? In my view, that's really what the OP was asking anyway. Surely you don't need to re-write functionality for things like Rename and Create if you remove another item such as Delete? – Andy Jan 16 '18 at 10:10
20

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;
}
Nimantha
  • 6,405
  • 6
  • 28
  • 69
stacked
  • 277
  • 3
  • 9
  • 1
    I prefer this answer as it relies on the `type` attribute rather than a CSS class obtained using jQuery. – Benny Bottema Jun 18 '17 at 16:04
  • What code are you putting inside `'action': function () { /* action */ }` in the second snippet? What if you want to use the "normal" functionality and menu items, but simply remove one of them (e.g. remove Delete but keep Rename and Create)? In my view, that's really what the OP was asking anyway. Surely you don't need to re-write functionality for things like Rename and Create if you remove another item such as Delete? – Andy Jan 16 '18 at 10:10
  • I'm not sure I understand your question. You are defining all the functionality for the full context menu (e.g., Delete, Rename, and Create) in the `items` list of objects, then you specify which of these items to remove for a given `node.type` at the end of the `customMenu` function. When the user clicks on a node of given `type`, the context menu will list all items minus any removed in the conditional at the end of the `customMenu` function. You aren't re-writing any functionality (unless jstree has changed since this answer three years ago in which case it may no longer be relevant). – stacked Jan 18 '18 at 05:40
12

To clear everything.

Instead of this:

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

Use this:

$("#xxx").jstree({
    'plugins' : 'contextmenu',
    'contextmenu' : {
        'items' : customMenu
    }
});
Joel Peltonen
  • 13,025
  • 6
  • 64
  • 100
Mangirdas
  • 121
  • 1
  • 2
5

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},
RandomUser
  • 1,843
  • 8
  • 33
  • 65
Jean G.T
  • 1
  • 10
  • 25
3

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;
}
Florian S.
  • 31
  • 2
2

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.

craigh
  • 1,909
  • 1
  • 11
  • 24
2

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);
                            }
Asif Nowaj
  • 346
  • 2
  • 9
2

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();
        }
    };
}();
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Gobinda Nandi
  • 457
  • 7
  • 18
2

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

user367134
  • 922
  • 8
  • 19
  • 31