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

我在网上看到过一个例子,展示了如何自定义 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');
}
}
}
}

但它不工作-创建项总是被禁用(警报从未出现)。

87460 次浏览

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

To clear everything.

Instead of this:

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

Use this:

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

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

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

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.

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

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

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

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