Konfigurowanie menu kontekstowego jstree prawym przyciskiem myszy dla różnych typów węzłów

85

Widziałem gdzieś w Internecie przykład pokazujący, jak dostosować wygląd menu kontekstowego jstree po kliknięciu prawym przyciskiem myszy (za pomocą wtyczki contextmenu).

Na przykład zezwól moim użytkownikom na usuwanie „dokumentów”, ale nie „folderów” (ukrywając opcję „usuń” z menu kontekstowego folderów).

Teraz nie mogę znaleźć tego przykładu. Czy ktoś może wskazać mi właściwy kierunek? Oficjalna dokumentacja tak naprawdę nie pomogła.

Edytować:

Ponieważ chcę domyślnego menu kontekstowego z tylko jedną lub dwiema drobnymi zmianami, wolałbym nie odtwarzać całego menu (choć oczywiście zrobię to, jeśli to jedyny sposób). Chciałbym zrobić coś takiego:

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

ale to nie działa - element tworzenia jest zawsze wyłączony (alert nigdy się nie pojawia).

MGOwen
źródło

Odpowiedzi:

144

contextmenuPlugin ma już wsparcia dla tego produktu. Z dokumentacji, do której linkowałeś:

items: Oczekuje obiektu lub funkcji, która powinna zwrócić obiekt . Jeśli funkcja jest używana, jest uruchamiana w kontekście drzewa i otrzymuje jeden argument - węzeł, który został kliknięty prawym przyciskiem myszy.

Więc zamiast dawać contextmenuzakodowany obiekt do pracy, możesz podać następującą funkcję. Sprawdza kliknięty element pod kątem klasy o nazwie „folder” i usuwa element menu „usuń”, usuwając go z obiektu:

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

Zwróć uwagę, że powyższe całkowicie ukryje opcję usuwania, ale wtyczka umożliwia również pokazanie elementu podczas wyłączania jego zachowania, poprzez dodanie _disabled: truedo odpowiedniego elementu. W takim przypadku możesz zamiast tego użyć items.deleteItem._disabled = truew ifinstrukcji.

Powinno to być oczywiste, ale pamiętaj, aby zainicjować wtyczkę customMenufunkcją zamiast tego, co miałeś wcześniej:

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

Edycja: Jeśli nie chcesz, aby menu było odtwarzane po każdym kliknięciu prawym przyciskiem myszy, możesz umieścić logikę w programie obsługi akcji dla samego elementu menu usuwania.

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

Edytuj ponownie: po obejrzeniu kodu źródłowego jsTree wygląda na to, że menu kontekstowe jest odtwarzane za każdym razem, gdy jest wyświetlane (zobacz funkcje show()i parse()), więc nie widzę problemu z moim pierwszym rozwiązaniem.

Jednak podoba mi się notacja, którą proponujesz, z funkcją jako wartością _disabled. Potencjalną ścieżką do zbadania jest zawinięcie ich parse()funkcji w swoją własną, która ocenia funkcję disabled: function () {...}i zapisuje wynik _disabled, przed wywołaniem oryginału parse().

Bezpośrednia modyfikacja ich kodu źródłowego również nie będzie trudna. Wiersz 2867 wersji 1.0-rc1 jest odpowiedni:

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

Możesz po prostu dodać linię przed tą, która sprawdza $.isFunction(val._disabled), a jeśli tak, to val._disabled = val._disabled(). Następnie prześlij go twórcom jako łatkę :)

David Tang
źródło
Dzięki. Myślałem, że kiedyś zobaczyłem rozwiązanie polegające na zmianie tylko tego, co wymagało zmiany z domyślnych (zamiast odtwarzania całego menu od zera). Przyjmę tę odpowiedź, jeśli nie ma lepszego rozwiązania przed wygaśnięciem nagrody.
MGOwen
@MGOwen, koncepcyjnie ja jestem modyfikując „default”, ale tak masz rację, że obiekt staje się ponownie utworzone przy każdym wywołaniu funkcji. Jednak wartość domyślna musi najpierw zostać sklonowana, w przeciwnym razie sama wartość domyślna zostanie zmodyfikowana (i będziesz potrzebować bardziej złożonej logiki, aby przywrócić jej pierwotny stan). Alternatywą, o której przychodzi mi do głowy, jest wyjście var itemspoza funkcję, aby została utworzona tylko raz, i zwrócenie wyboru elementów z funkcji, np. return {renameItem: items.renameItem};Lubreturn {renameItem: items.renameItem, deleteItem: items.deleteItem};
David Tang
Szczególnie podoba mi się ten ostatni, w którym modyfikujesz źródło jstree. Wypróbowałem i działa, działa funkcja przypisana do „_disabled” (w moim przykładzie). Ale to nie pomaga, ponieważ nie mogę uzyskać dostępu do węzła (przynajmniej potrzebuję jego atrybutu rel do filtrowania węzłów według typu węzła) z zakresu funkcji. Próbowałem sprawdzić zmienne, które mogłem przekazać z kodu źródłowego jstree, ale nie mogłem znaleźć węzła. Jakieś pomysły?
MGOwen
@MGOwen, wygląda na to <a>, że kliknięty element jest przechowywany w $.vakata.context.tgt. Więc spróbuj spojrzeć w górę $.vakata.context.tgt.attr("rel").
David Tang,
1
w jstree 3.0.8: if ($(node).hasClass("folder")) nie działało. ale tak się if (node.children.length > 0) { items.deleteItem._disabled = true; }
stało
19

Zaimplementowano z różnymi typami węzłów:

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

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

Działa pięknie.

ułożone
źródło
1
Wolę tę odpowiedź, ponieważ opiera się na typeatrybucie, a nie klasie CSS uzyskanej za pomocą jQuery.
Benny Bottema
Jaki kod wstawiasz 'action': function () { /* action */ }do drugiego fragmentu kodu ? Co zrobić, jeśli chcesz korzystać z „normalnych” funkcji i elementów menu, ale po prostu usunąć jedną z nich (np. Usuń Usuń, ale zachowaj Zmień nazwę i Utwórz)? Moim zdaniem tak naprawdę o to pytał OP. Z pewnością nie musisz ponownie pisać funkcji takich jak Zmień nazwę i Utwórz, jeśli usuniesz inny element, taki jak Usuń?
Andy,
Nie jestem pewien, czy rozumiem twoje pytanie. Definiujesz całą funkcjonalność dla pełnego menu kontekstowego (np. Usuń, Zmień nazwę i Utwórz) na itemsliście obiektów, a następnie określasz, które z tych elementów usunąć dla danej funkcji node.typena końcu customMenufunkcji. Gdy użytkownik kliknie dany węzeł type, menu kontekstowe wyświetli listę wszystkich elementów pomniejszonych o wszystkie usunięte w warunku na końcu customMenufunkcji. Nie piszesz ponownie żadnej funkcjonalności (chyba że jstree zmieniło się od czasu tej odpowiedzi trzy lata temu, w którym to przypadku może nie mieć już zastosowania).
ułożone
12

Aby wszystko wyczyścić.

Zamiast tego:

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

Użyj tego:

$("#xxx").jstree({
    'plugins' : 'contextmenu',
    'contextmenu' : {
        'items' : customMenu
    }
});
Mangirdas
źródło
5

Nieco inaczej dostosowałem sugerowane rozwiązanie do pracy z typami, być może może to pomóc komuś innemu:

Gdzie # {$ id_arr [$ k]} jest odniesieniem do kontenera div ... w moim przypadku używam wielu drzew, więc cały ten kod będzie wyjściem do przeglądarki, ale masz pomysł .. Zasadniczo chcę wszystko opcje menu kontekstowego, ale tylko „Utwórz” i „Wklej” w węźle Dysk. Oczywiście z poprawnymi powiązaniami z tymi operacjami później:

<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
źródło
2

Btw: Jeśli chcesz tylko usunąć opcje z istniejącego menu kontekstowego - to zadziałało dla mnie:

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.
źródło
1

Możesz zmodyfikować kod @ Box9, aby dopasować go do wymagań dynamicznego wyłączania menu kontekstowego jako:

function customMenu(node) {

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


       items.deleteItem._disabled = true;
    }   

}  

Musisz dodać jeden atrybut „xyz” w swoich danych XML lub JSOn

user367134
źródło
1

od jsTree 3.0.9 musiałem użyć czegoś takiego jak

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

ponieważ dostarczony nodeobiekt nie jest obiektem jQuery.

Craigh
źródło
1

Odpowiedź Davida wydaje się dobra i skuteczna. Znalazłem inną odmianę rozwiązania, w której można użyć atrybutu a_attr do rozróżnienia różnych węzłów i na tej podstawie można wygenerować różne menu kontekstowe.

W poniższym przykładzie użyłem dwóch typów węzłów Folder i Pliki. Użyłem też różnych ikon, używając glifów. W przypadku węzła typu pliku można uzyskać tylko menu kontekstowe, aby zmienić nazwę i usunąć. W przypadku folderu dostępne są wszystkie opcje, utwórz plik, utwórz folder, zmień nazwę, usuń.

Aby uzyskać pełny fragment kodu, możesz wyświetlić 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);                        
                    }
                }
            });

Początkowe dane json są jak poniżej, gdzie typ węzła jest wymieniony w 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'} },
            ];

Jako część menu kontekstowego, aby utworzyć plik i folder, użyj podobnego kodu poniżej, jak akcja na pliku.

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

jako akcja folderu:

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
źródło