Configurando o menu de contexto do botão direito do mouse jstree para diferentes tipos de nós

85

Eu vi um exemplo em algum lugar online mostrando como personalizar a aparência do menu de contexto do botão direito do mouse do jstree (usando o plug-in contextmenu).

Por exemplo, permitir que meus usuários excluam "documentos", mas não "pastas" (ocultando a opção "excluir" do menu de contexto para pastas).

Agora não consigo encontrar esse exemplo. Alguém pode me apontar na direção certa? A documentação oficial não ajudou muito.

Editar:

Já que quero o menu de contexto padrão com apenas uma ou duas pequenas alterações, prefiro não recriar o menu inteiro (embora seja claro que o fará se for a única maneira). O que eu gostaria de fazer é algo assim:

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

mas não funciona - o item de criação está sempre desabilitado (o alerta nunca aparece).

MGOwen
fonte

Respostas:

145

O contextmenuplugin já tem suporte para isso. Da documentação que você vinculou a:

items: Espera um objeto ou uma função, que deve retornar um objeto . Se uma função for usada, ela será acionada no contexto da árvore e receberá um argumento - o nó que foi clicado com o botão direito.

Portanto, em vez de fornecer contextmenuum objeto embutido em código para trabalhar, você pode fornecer a seguinte função. Ele verifica o elemento que foi clicado para uma classe chamada "pasta" e remove o item de menu "excluir" excluindo-o do objeto:

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

Observe que o acima irá ocultar a opção de exclusão completamente, mas o plugin também permite que você mostre um item enquanto desativa seu comportamento, adicionando _disabled: trueao item relevante. Nesse caso, você pode usar items.deleteItem._disabled = truedentro da ifinstrução.

Deve ser óbvio, mas lembre-se de inicializar o plug-in com a customMenufunção em vez do que você tinha anteriormente:

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

Editar: se não quiser que o menu seja recriado a cada clique com o botão direito, você pode colocar a lógica no manipulador de ação para o próprio item de menu de exclusão.

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

Edite novamente: depois de examinar o código-fonte do jsTree, parece que o menu de contexto está sendo recriado toda vez que é mostrado (veja as funções show()e parse()), portanto, não vejo nenhum problema com minha primeira solução.

No entanto, gosto da notação que você está sugerindo, com uma função como o valor de _disabled. Um caminho potencial a explorar é envolver a parse()função deles com a sua própria, que avalia a função em disabled: function () {...}e armazena o resultado em _disabled, antes de chamar o original parse().

Também não será difícil modificar seu código-fonte diretamente. A linha 2867 da versão 1.0-rc1 é a relevante:

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

Você pode simplesmente adicionar uma linha antes desta que verifica $.isFunction(val._disabled)e, em caso afirmativo val._disabled = val._disabled(),. Em seguida, envie-o aos criadores como um patch :)

David Tang
fonte
Obrigado. Eu pensei ter visto uma vez uma solução envolvendo alterar apenas o que precisava ser alterado do padrão (em vez de recriar todo o menu do zero). Aceitarei essa resposta se não houver solução melhor antes que a recompensa expire.
MGOwen
@MGOwen, conceitualmente estou modificando o "default", mas sim você está certo que o objeto é recriado cada vez que a função é chamada. No entanto, o padrão precisa ser clonado primeiro, caso contrário, o próprio padrão é modificado (e você precisará de uma lógica mais complexa para revertê-lo ao estado original). Uma alternativa que posso pensar é ir var itemspara fora da função para que seja criada apenas uma vez e retornar uma seleção de itens da função, por exemplo, return {renameItem: items.renameItem};oureturn {renameItem: items.renameItem, deleteItem: items.deleteItem};
David Tang
Gosto especialmente desse último, em que você modifica a fonte jstree. Eu tentei e funciona, a função atribuída a "_disabled" (no meu exemplo) é executada. Mas isso não ajuda porque não consigo acessar o nó (pelo menos preciso do atributo rel para filtrar nós por tipo de nó) de dentro do escopo da função. Tentei inspecionar as variáveis ​​que poderia passar do código-fonte jstree, mas não consegui encontrar o nó. Alguma ideia?
MGOwen
@MGOwen, parece que o <a>elemento que foi clicado está armazenado em $.vakata.context.tgt. Portanto, tente olhar para cima $.vakata.context.tgt.attr("rel").
David Tang
1
no jstree 3.0.8: if ($(node).hasClass("folder")) não funcionou. mas isso fez: if (node.children.length > 0) { items.deleteItem._disabled = true; }
Ryan Vettese
19

Implementado com diferentes tipos de nós:

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

E a função 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;
}

Funciona lindamente.

empilhado
fonte
1
Prefiro essa resposta porque depende do typeatributo em vez de uma classe CSS obtida usando jQuery.
Benny Bottema de
Qual código você está colocando 'action': function () { /* action */ }no segundo trecho? E se você quiser usar a funcionalidade "normal" e os itens de menu, mas simplesmente remover um deles (por exemplo, remover Excluir, mas manter Renomear e Criar)? Na minha opinião, era isso que o OP estava perguntando de qualquer maneira. Certamente você não precisa reescrever a funcionalidade para coisas como Renomear e Criar se remover outro item como Excluir?
Andy
Não tenho certeza se entendi sua pergunta. Você está definindo todas as funcionalidades para o menu de contexto completo (por exemplo, Excluir, Renomear e Criar) na itemslista de objetos e, em seguida, especificar quais desses itens remover para um determinado node.typeno final da customMenufunção. Quando o usuário clica em um nó de dado type, o menu de contexto irá listar todos os itens menos os removidos na condicional no final da customMenufunção. Você não está reescrevendo nenhuma funcionalidade (a menos que jstree tenha mudado desde esta resposta, três anos atrás, caso em que pode não ser mais relevante).
empilhado em
12

Para limpar tudo.

Em vez disso:

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

Usa isto:

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

Eu adaptei a solução sugerida para trabalhar com tipos de forma um pouco diferente, embora talvez possa ajudar alguém:

Onde # {$ id_arr [$ k]} é a referência para o contêiner div ... no meu caso, eu uso muitas árvores, então todo esse código será a saída para o navegador, mas você entendeu. Basicamente, eu quero todos as opções do menu de contexto, mas apenas 'Criar' e 'Colar' no nó Drive. Obviamente, com as ligações corretas a essas operações mais tarde:

<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},
Jean Paul AKA el_vete
fonte
2

Aliás: se você deseja apenas remover opções do menu de contexto existente - funcionou para mim:

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.
fonte
1

Você pode modificar o código @ Box9 de acordo com seus requisitos de desativação dinâmica do menu de contexto como:

function customMenu(node) {

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


       items.deleteItem._disabled = true;
    }   

}  

Você precisa adicionar um atributo "xyz" em seus dados XML ou JSOn

user367134
fonte
1

a partir do jsTree 3.0.9 eu precisava usar algo como

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

porque o nodeobjeto fornecido não é um objeto jQuery.

Craigh
fonte
1

A resposta de David parece boa e eficiente. Eu encontrei outra variação da solução onde você pode usar o atributo a_attr para diferenciar nós diferentes e com base nisso você pode gerar um menu de contexto diferente.

No exemplo a seguir, usei dois tipos de nós: Pasta e Arquivos. Também usei ícones diferentes usando o glifo. Para nó de tipo de arquivo, você só pode obter o menu de contexto para renomear e remover. Para Pasta, todas as opções estão lá, criar arquivo, criar pasta, renomear, remover.

Para um snippet de código completo, você pode ver 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);                        
                    }
                }
            });

Os dados json iniciais foram os abaixo, onde o tipo de nó é mencionado em 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'} },
            ];

Como parte do item de menu contect para criar um arquivo e uma pasta, use o código semelhante abaixo, como ação de arquivo.

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

como ação de pasta:

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
fonte