Configuración del menú contextual del botón derecho del ratón jstree para diferentes tipos de nodos

85

He visto un ejemplo en algún lugar en línea que muestra cómo personalizar la apariencia del menú contextual del botón derecho del ratón de jstree (usando el complemento contextmenu).

Por ejemplo, permitir que mis usuarios eliminen "documentos" pero no "carpetas" (ocultando la opción "eliminar" del menú contextual de las carpetas).

Ahora no puedo encontrar ese ejemplo. ¿Alguien puede señalarme en la dirección correcta? La documentación oficial realmente no ayudó.

Editar:

Como quiero el menú contextual predeterminado con solo uno o dos cambios menores, prefiero no volver a crear el menú completo (aunque, por supuesto, lo haré si es la única forma). Lo que me gustaría hacer es algo como esto:

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

pero no funciona: el elemento de creación siempre está deshabilitado (la alerta nunca aparece).

MGOwen
fuente

Respuestas:

144

El contextmenucomplemento ya tiene soporte para esto. De la documentación a la que vinculó:

items: Espera un objeto o una función, que debería devolver un objeto . Si se usa una función, se activa en el contexto del árbol y recibe un argumento: el nodo en el que se hizo clic con el botón derecho.

Entonces, en lugar de proporcionar contextmenuun objeto codificado de forma rígida para trabajar, puede proporcionar la siguiente función. Comprueba el elemento en el que se hizo clic para una clase llamada "carpeta" y elimina el elemento del menú "eliminar" eliminándolo del 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;
}

Tenga en cuenta que lo anterior ocultará la opción de eliminación por completo, pero el complemento también le permite mostrar un elemento mientras deshabilita su comportamiento, agregando _disabled: trueel elemento relevante. En este caso, puede usar items.deleteItem._disabled = truedentro de la ifdeclaración.

Debería ser obvio, pero recuerde inicializar el complemento con la customMenufunción en lugar de lo que tenía anteriormente:

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

Editar: si no desea que el menú se vuelva a crear con cada clic derecho, puede poner la lógica en el controlador de acciones para el elemento del menú de eliminación.

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

Edite de nuevo: después de mirar el código fuente de jsTree, parece que el menú contextual se vuelve a crear cada vez que se muestra de todos modos (consulte las funciones show()y parse()), por lo que no veo ningún problema con mi primera solución.

Sin embargo, me gusta la notación que está sugiriendo, con una función como valor para _disabled. Una ruta potencial para explorar es envolver su parse()función con la suya propia que evalúa la función disabled: function () {...}y almacena el resultado en _disabled, antes de llamar al original parse().

Tampoco será difícil modificar su código fuente directamente. La línea 2867 de la versión 1.0-rc1 es la relevante:

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

Simplemente puede agregar una línea antes de esta que verifique $.isFunction(val._disabled), y si es así val._disabled = val._disabled(),. Luego envíelo a los creadores como parche :)

David Tang
fuente
Gracias. Pensé que una vez vi una solución que implicaba cambiar solo lo que necesitaba cambiar del predeterminado (en lugar de recrear todo el menú desde cero). Aceptaré esta respuesta si no hay una solución mejor antes de que expire la recompensa.
MGOwen
@MGOwen, conceptualmente estoy modificando el "predeterminado", pero sí, tienes razón en que el objeto se vuelve a crear cada vez que se llama a la función. Sin embargo, el valor predeterminado debe clonarse primero; de lo contrario, el valor predeterminado se modificará (y necesitará una lógica más compleja para revertirlo al estado original). Una alternativa en la que puedo pensar es moverme var itemsfuera de la función para que se cree solo una vez y devolver una selección de elementos de la función, por ejemplo, return {renameItem: items.renameItem};oreturn {renameItem: items.renameItem, deleteItem: items.deleteItem};
David Tang
Me gusta especialmente ese último, donde modificas la fuente jstree. Lo probé y funciona, la función asignada a "_disabled" (en mi ejemplo) se ejecuta. Pero, no ayuda porque no puedo acceder al nodo (al menos necesito su atributo rel para filtrar nodos por tipo de nodo) desde dentro del alcance de la función. Intenté inspeccionar las variables que podía pasar desde el código fuente de jstree pero no pude encontrar el nodo. ¿Algunas ideas?
MGOwen
@MGOwen, parece que el <a>elemento en el que se hizo clic está almacenado en $.vakata.context.tgt. Así que intenta mirar hacia arriba $.vakata.context.tgt.attr("rel").
David Tang
1
en jstree 3.0.8: if ($(node).hasClass("folder")) no funcionó. pero esto hizo: if (node.children.length > 0) { items.deleteItem._disabled = true; }
Ryan Vettese
19

Implementado con diferentes tipos de nodos:

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

Y la función 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 maravillosamente.

apilado
fuente
1
Prefiero esta respuesta ya que se basa en el typeatributo en lugar de en una clase CSS obtenida con jQuery.
Benny Bottema
¿Qué código está poniendo 'action': function () { /* action */ }en el segundo fragmento? ¿Qué sucede si desea utilizar la funcionalidad "normal" y los elementos del menú, pero simplemente elimine uno de ellos (por ejemplo, elimine Eliminar pero conserve Renombrar y Crear)? En mi opinión, eso es realmente lo que pedía el OP de todos modos. Seguramente no necesita volver a escribir la funcionalidad para cosas como Cambiar nombre y Crear si elimina otro elemento como ¿Eliminar?
Andy
No estoy seguro de entender tu pregunta. Está definiendo toda la funcionalidad para el menú contextual completo (por ejemplo, Eliminar, Renombrar y Crear) en la itemslista de objetos, luego especifica cuál de estos elementos eliminar para un determinado node.typeal final de la customMenufunción. Cuando el usuario hace clic en un nodo de dado type, el menú contextual enumerará todos los elementos menos los eliminados en el condicional al final de la customMenufunción. No está reescribiendo ninguna funcionalidad (a menos que jstree haya cambiado desde esta respuesta hace tres años, en cuyo caso puede que ya no sea relevante).
apilado el
12

Para despejar todo.

En lugar de esto:

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

Utilizar esta:

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

Sin embargo, he adaptado la solución sugerida para trabajar con tipos de manera un poco diferente, tal vez pueda ayudar a otra persona:

Donde # {$ id_arr [$ k]} es la referencia al contenedor div ... en mi caso, uso muchos árboles, por lo que todo este código será la salida para el navegador, pero entiendes la idea ... Básicamente, quiero todos las opciones del menú contextual, pero solo 'Crear' y 'Pegar' en el nodo Unidad. Obviamente, con los enlaces correctos a esas operaciones más adelante:

<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
fuente
2

Por cierto: si solo desea eliminar opciones del menú contextual existente, esto funcionó para mí:

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

Puede modificar el código @ Box9 para que se adapte a sus requisitos de desactivación dinámica del menú contextual como:

function customMenu(node) {

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


       items.deleteItem._disabled = true;
    }   

}  

Necesita agregar un atributo "xyz" en sus datos XML o JSOn

user367134
fuente
1

a partir de jsTree 3.0.9 necesitaba usar algo como

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

porque el nodeobjeto que se proporciona no es un objeto jQuery.

craigh
fuente
1

La respuesta de David parece fina y eficiente. Encontré otra variación de la solución en la que puede usar el atributo a_attr para diferenciar diferentes nodos y, en función de eso, puede generar un menú contextual diferente.

En el siguiente ejemplo, he usado dos tipos de nodos Carpeta y Archivos. También he usado diferentes íconos usando glyphicon. Para el nodo de tipo de archivo, solo puede obtener el menú contextual para cambiar el nombre y eliminarlo. Para Carpeta, todas las opciones están ahí, crear archivo, crear carpeta, renombrar, eliminar.

Para obtener un fragmento de código completo, puede 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);                        
                    }
                }
            });

Los datos iniciales de json han sido los siguientes, donde se menciona el tipo de nodo dentro de 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 del elemento del menú contextual para crear un archivo y una carpeta, use un código similar a continuación, como acción de archivo.

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 acción de carpeta:

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
fuente