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


85

我在网上某个地方看到了一个示例,该示例显示了如何自定义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'); 
            }
        }
    }
}

但它不起作用-始终禁用创建项(永远不会显示警报)。

Answers:


144

contextmenu插件已对此提供支持。从您链接到的文档中:

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

因此contextmenu,可以提供以下功能,而不是使用硬编码的对象。它检查单击名为“文件夹”的类的元素,并通过从对象中删除菜单来删除“删除”菜单项:

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

请注意,以上内容将完全隐藏delete选项,但是该插件还允许您通过添加_disabled: true到相关项目来在禁用其行为的同时显示该项目。在这种情况下,您可以items.deleteItem._disabled = trueif语句内使用。

应该很明显,但是请记住使用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};
邓永锵

我特别喜欢最后一个,您可以在其中修改jstree源。我尝试了一下,它起作用了,分配给“ _disabled”的功能(在我的示例中)运行了。但是,这无济于事,因为我无法从函数范围内访问节点(我至少需要它的rel属性以按节点类型过滤节点)。我尝试检查可以从jstree源代码传递的变量,但找不到该节点。有任何想法吗?
MGOwen

@MGOwen,看起来<a>被单击的元素存储在中$.vakata.context.tgt。因此,请尝试查找$.vakata.context.tgt.attr("rel")
David Tang

1
在jstree 3.0.8中: if ($(node).hasClass("folder")) 无效。但这确实: if (node.children.length > 0) { items.deleteItem._disabled = true; }
Ryan Vettese 2014年

19

用不同的节点类型实现:

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

和customMenu函数:

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 */ }在第二个片段中放入了什么代码?如果要使用“常规”功能和菜单项,而只删除其中之一(例如,删除“删除但保留重命名和创建”)怎么办?我认为,这实际上是OP所要问的。当然,如果您删除了其他项(例如“删除”),则不需要为“重命名”和“创建”之类的功能重写功能?
安迪

我不确定我是否理解您的问题。您要在items对象列表中定义完整上下文菜单的所有功能(例如,“删除”,“重命名”和“创建”),然后node.typecustomMenu函数末尾指定要为给定的内容删除哪些项目。当用户单击给定的节点时type,上下文菜单将列出所有项,减去该customMenu函数结尾处有条件删除的所有项。您无需重新编写任何功能(除非自从三年前回答以来jstree发生了变化,在这种情况下它可能不再适用)。
堆叠

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

2

顺便说一句:如果您只想从现有的上下文菜单中删除选项,这对我有用:

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


1

您可以将@ 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”


1

从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对象。


1

大卫的回应似乎很好而有效。我发现了该解决方案的另一种变体,您可以使用a_attr属性来区分不同的节点,并以此为基础生成不同的上下文菜单。

在下面的示例中,我使用了两种类型的节点“文件夹”和“文件”。我也使用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);
                            }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.