为不同节点类型配置jstree右键上下文菜单

88

我曾在网上看到一个示例,展示了如何自定义jstree的右键上下文菜单外观(使用contextmenu插件)。

例如,允许我的用户删除“文档”,但不允许删除“文件夹”(通过在文件夹的上下文菜单中隐藏“删除”选项来实现)。

现在我找不到那个示例了。有人能指点我吗?官方文档并没有真正帮助我。

编辑:

由于我想要默认上下文菜单,并进行一两个小改动,所以我更愿意不重新创建整个菜单(当然,如果这是唯一的方法,我会的)。我想做的是像这样:

"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'); 
            }
        }
    }
}

但是它不起作用 - 创建项目始终被禁用(警报从未出现)。

9个回答

153
contextmenu插件已经支持这个功能。从您提供的文档中可以看到:

items: 需要一个对象或返回对象的函数。如果使用函数,它将在树的上下文中触发并接收一个参数——右键单击的节点。

因此,您可以提供以下函数来代替向contextmenu提供硬编码的对象。该函数会检查被点击的元素是否具有名为“folder”的类,并通过从对象中删除它来移除“delete”菜单项:
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;
}
注意,以上代码将完全隐藏删除选项,但插件还允许您在禁用其行为的同时显示某个项目,方法是在相关项目中添加_disabled:true。在这种情况下,您可以在if语句中使用items.deleteItem._disabled = true
应该很明显,但请记住要使用customMenu函数来初始化插件,而不是之前使用的函数。
$("#tree").jstree({plugins: ["contextmenu"], contextmenu: {items: customMenu}});
//                                                                    ^
// ___________________________________________________________________|

编辑: 如果你不想在每次右键单击时重新创建菜单,你可以将逻辑放在删除菜单项本身的操作处理程序中。

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

再次编辑: 经过查看jsTree源代码,看起来每次显示时上下文菜单都会被重新创建(请参见show()parse()函数),因此我认为我的第一个解决方案没有问题。

但是,我喜欢您建议的符号方式,将函数作为_disabled值。 可以尝试的一个潜在路径是用自己的parse()函数包装他们的函数,并在disabled: function () {...}处评估该函数并将结果存储在_disabled中,然后调用原始的parse()函数。

直接修改他们的源代码也不难。版本1.0-rc1的第2867行是相关的一行代码:

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

你可以在这一行之前添加一行代码来检查 $.isFunction(val._disabled),如果是这样,就执行 val._disabled = val._disabled()。然后将其作为补丁提交给创建者 :)


谢谢。我曾经看到过一种解决方案,只需更改默认设置中需要更改的内容(而不是从头开始重新创建整个菜单)。如果在悬赏期限到期之前没有更好的解决方案,我将接受这个答案。 - MGOwen
@MGOwen,从概念上讲,我确实正在修改“默认值”,但是你说得对,每次调用函数时对象都会被重新创建。然而,默认值需要先进行克隆,否则默认值本身就会被修改(并且您需要更复杂的逻辑将其恢复到原始状态)。我能想到的另一种选择是将 var items 移到函数外部,这样它只会被创建一次,并从函数中返回一些项目的选择,例如 return {renameItem: items.renameItem};return {renameItem: items.renameItem, deleteItem: items.deleteItem}; - David Tang
@Box9 - 这非常有帮助。谢谢! - JasCav
2
在jstree 3.0.8中:if ($(node).hasClass("folder"))无效。但是这个有效:if (node.children.length > 0) { items.deleteItem._disabled = true; } - Ryan Vettese
1
在第一个片段中,你将放置什么代码在 action: function () {...} 中?如果你想要使用“正常”的功能和菜单项,但只是删除其中一个(例如删除 Delete 但保留 Rename 和 Create),该怎么办?在我看来,这才是 OP 实际上所问的。如果你删除了 Delete 这样的另一个项目,肯定不需要重新编写 Rename 和 Create 的功能吧? - Andy
显示剩余3条评论

20

使用不同的节点类型实现:

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

自定义菜单功能:

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;
}

1
我更喜欢这个答案,因为它依赖于type属性,而不是使用jQuery获取CSS类。 - Benny Bottema
在第二个片段中,你将放置什么代码在'action': function () { /* action */ }中?如果你想使用“正常”的功能和菜单项,但只是删除其中一个(例如删除Delete但保留Rename和Create),该怎么办?在我看来,这才是OP真正想问的。如果你删除了Delete这样的另一个项目,肯定不需要重新编写Rename和Create等功能吧? - Andy
我不确定我理解你的问题。您正在定义完整上下文菜单(例如,删除、重命名和创建)的所有功能在对象的items列表中,然后在customMenu函数的末尾指定要删除哪些项目以适用于给定的node.type。当用户单击给定类型的节点时,上下文菜单将列出所有项目,减去在customMenu函数末尾的条件中删除的任何项目。您没有重新编写任何功能(除非jstree自三年前回答以来发生了更改,否则可能不再相关)。 - stacked

12
为了清除所有内容。
不要使用以下方式:
$("#xxx").jstree({
    'plugins' : 'contextmenu',
    'contextmenu' : {
        'items' : { ... bla bla bla ...}
    }
});

使用以下代码:

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

5

我稍微改进了建议的解决方法,适用于处理类型,也许可以帮助其他人:

其中# {$id_arr [$k]}是对div容器的引用……在我的情况下,我使用许多树形目录,因此所有这些代码将成为输出到浏览器的内容,但您可以想象出来。 基本上,我想要所有的上下文菜单选项,但只有“创建”和“粘贴”对驱动器节点有效。 显然,稍后需要正确绑定这些操作:

<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},

3
顺便说一下:如果你只想从现有的上下文菜单中删除选项-这对我很有效:

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;
}


2
从jsTree 3.0.9开始,我需要使用类似以下的内容
var currentNode = treeElem.jstree('get_node', node, true);
if (currentNode.hasClass("folder")) {
    // Delete the "delete" menu item
    delete items.deleteItem;
}

因为提供的node对象不是jQuery对象。

2
这是我的完整插件设置。
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();
        }
    };
}();

2
您可以根据需要修改@Box9的代码,以实现动态禁用上下文菜单,如下所示:
function customMenu(node) {

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


       items.deleteItem._disabled = true;
    }   

}  

您需要在您的XML或JSON数据中添加一个属性"xyz"。

2
David的回复似乎很好和高效。我发现了另一种解决方案的变化,您可以使用a_attr属性来区分不同的节点,并基于此生成不同的上下文菜单。
在下面的示例中,我使用了两种类型的节点Folder和Files。我还使用了不同的图标,使用glyphicon。对于文件类型节点,您只能获取重命名和删除上下文菜单。对于文件夹,所有选项都在那里,创建文件,创建文件夹,重命名,删除。
有关完整的代码片段,您可以查看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);                        
                    }
                }
            });

初始的JSON数据如下,其中节点类型在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'} },
            ];

作为上下文菜单项的一部分,创建文件和文件夹可以使用类似下面的代码,作为文件操作。
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);
                            }

作为文件夹操作:
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);
                            }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接