jsTree – context content

Podziel się z innymi!

    Skryptów do wizualizacji struktur drzewiastych nie brakuje. W javascript-cie powstało tego cała masa. Ja preferuje rozwiązania oparte o [jquery](http://jquery.com/) i nawet po przyjęciu tego zawężającego kryterium mam w czym wybierać. To też wypróbowałem parę rozwiązań i ostatecznie zdecydowałem się na [jstree](http://www.jstree.com/).

    Po przejrzeniu wszystkich dem prezentujących możliwości tej aplikacji (Piszę aplikacji bo coś co ma np. pluginy – nie jest już prostym skryptem), a także dokumentacji jestem naprawdę pod wrażeniem. Rozwijanie i zwijanie katalogów to banał, ale do tego dochodzi możliwość przeciągania elementów lub nawet całych gałęzi, możliwość dodawania, edytowania, usuwania, kopiowania elementów. Wspomniany skrypt jest naprawdę dobrze napisany. Daje możliwość przechwycenia wszelkich zachodzących na drzewie wydarzeń i dowolnego ich obsłużenia. Łącząc takie technologie jak ajax i np. php możemy doczytywać rozwijane katalogi lub też zapisywać wszelkie wykonane na drzewie operacje takie jak utworzenie nowego elementu lub też przeniesienie gałęzi w inne miejsce.

    Wspomniałem już o pluginach. Do dyspozycji mamy kilka przydatnych rozszerzeń. **”Cookie plugin”** np. umożliwia zapisanie stanu drzewa. Dzięki temu po przeładowaniu strony wszystkie rozwinięte wcześniej gałęzie nadal takimi pozostają i nie musimy bawić się w ponowne klikanie by dojść do elementu na trzecim poziomie. **”Keyboard navigation”** umożliwia poruszanie się po drzewie przy pomocy klawiatury, a **”Checkbox plugin”** umożliwia zaznaczenie więcej niż jednego elementu poprzez kliknięcie utworzonego przy nazwie elementu checkboxa.

    Jednym z ciekawszych dodatków jest w pełni konfigurowalne menu kontekstowe. **”Context menu plugin”** ma już predefiniowane ustawienie umożliwiające wykonanie takich akcji jak utworzenie podelementu oraz usunięcie lub edycja elementu bieżącego. Demo dodatkowo prezentuje w jaki sposób dodać do niego własną akcję. Menu kontekstowe ma jedną wadę, która objawia się w operze. Ponieważ pojawia się po kliknięciu na prawym przycisku myszki w Operze menu kontekstowe nie zadziała gdyż wspomniana przeglądarka nie daje możliwości nadpisania akcji prawokliku. Z tego też powodu zdecydowałem się nieco zmodyfikować tenże plugin w ten sposób, że po wybraniu danego elementu przy jego nazwie pojawia się mały plusik, którego kliknięcie otwiera menu kontekstowe.

    (function ($) {
    	$.extend($.tree.plugins, {
    		"contextmenu" : {
    			context_menu : {
    				object : $("<ul id='jstree-contextmenu' class='tree-context' />"),
    					data : {
    					t : false,
    					a : false,
    					r : false
    				},
    				isrgtclick: false,
    				defaults : {
    					class_name : "hover",
    					items : {
    						create : {
    							label	: "Create", 
    							icon	: "create",
    							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return 0; return TREE_OBJ.check("creatable", NODE); }, 
    							action	: function (NODE, TREE_OBJ) { TREE_OBJ.create(false, TREE_OBJ.get_node(NODE[0])); },
    							separator_after : true
    						},
    						rename : {
    							label	: "Rename", 
    							icon	: "rename",
    							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return false; return TREE_OBJ.check("renameable", NODE); }, 
    							action	: function (NODE, TREE_OBJ) { TREE_OBJ.rename(NODE); } 
    						},
    						remove : {
    							label	: "Delete",
    							icon	: "remove",
    							visible	: function (NODE, TREE_OBJ) { var ok = true; $.each(NODE, function () { if(TREE_OBJ.check("deletable", this) == false) ok = false; return false; }); return ok; }, 
    							action	: function (NODE, TREE_OBJ) { $.each(NODE, function () { TREE_OBJ.remove(this); }); } 
    						}
    					}
    				},
    				show : function(obj, t, e) {
    					var opts = $.extend(true, {}, this.defaults, t.settings.plugins.contextmenu);
    					obj = $(obj);
    					if(obj.size() == 0) return;
    					this.data.t = t;
    					if(!obj.children("a:eq(0)").hasClass("clicked")) {
    						this.data.a = obj;
    						this.data.r = true;
    						obj.children("a").addClass(opts.class_name);
    						if (e) {
    							e.target.blur();
    						}
    					}
    					else { 
    						this.data.r = false; 
    						this.data.a = (t.selected_arr && t.selected_arr.length > 1) ? t.selected_arr : t.selected;
    					}
     
    					this.object.empty();
    					var str = "";
    					var cnt = 0;
    					for(var i in opts.items) {
    						if(!opts.items.hasOwnProperty(i)) continue;
    						if(opts.items[i] === false) continue;
    						var r = 1;
    						if(typeof opts.items[i].visible == "function") r = opts.items[i].visible.call(null, this.data.a, t);
    						if(r == -1) continue;
    						else cnt ++;
    						if(opts.items[i].separator_before === true) str += "<li class='separator'><span>&nbsp;</span></li>";
    						str += '<li><a href="#" rel="' + i + '" class="' + i + ' ' + (r == 0 ? 'disabled' : '') + '">';
    						if(opts.items[i].icon) str += "<ins " + (opts.items[i].icon.indexOf("/") == -1 ? " class='" + opts.items[i].icon + "' " : " style='background-image:url(\"" + opts.items[i].icon + "\");' " ) + ">&nbsp;</ins>";
    						else str += "<ins>&nbsp;</ins>";
    						str += "<span>" + opts.items[i].label + '</span></a></li>';
    						if(opts.items[i].separator_after === true) str += "<li class='separator'><span>&nbsp;</span></li>";
    					}
    					var tmp = obj.children("a:visible").offset();
    					this.object.attr("class","tree-context tree-" + t.settings.ui.theme_name.toString() + "-context").html(str);
    					var h = this.object.height();
    					var w = this.object.width();
    					var x = tmp.left;
    					var y = tmp.top + parseInt(obj.children("a:visible").height()) + 2;
    					var max_y = $(window).height() + $(window).scrollTop();
    					var max_x = $(window).width() + $(window).scrollLeft();
    					if(y + h > max_y) y = Math.max( (max_y - h - 2), 0);
    					if(x + w > max_x) x = Math.max( (max_x - w - 2), 0);
    					this.object.css({ "left" : (x), "top" : (y) }).fadeIn("fast");
     
    					if (e) {
    						e.preventDefault(); 
    						e.stopPropagation();
    						this.isrgtclick = true;
    					}
    				},
    				hide : function (check_isrgtclick) {
    					if (check_isrgtclick == true && this.isrgtclick == false) {
    						return;
    					}
    					this.isrgtclick = false;
    					if(!this.data.t) return;
    					var opts = $.extend(true, {}, this.defaults, this.data.t.settings.plugins.contextmenu);
    					if(this.data.r && this.data.a) {
    						this.data.a.children("a, span").removeClass(opts.class_name);
    					}
    					this.data = { a : false, r : false, t : false };
    					this.object.fadeOut("fast");
    				},
    				exec : function (cmd) {
    					if($.tree.plugins.contextmenu.context_menu.data.t == false) return;
    					var opts = $.extend(true, {}, $.tree.plugins.contextmenu.context_menu.defaults, $.tree.plugins.contextmenu.context_menu.data.t.settings.plugins.contextmenu);
    					try { opts.items[cmd].action.apply(null, [$.tree.plugins.contextmenu.context_menu.data.a, $.tree.plugins.contextmenu.context_menu.data.t]); } catch(e) { };
    				},
    				add : function (n, t) {
    					if ($('#jstree-show-contextmenu').length == 0) {
    						$(n).children('a').after('<a id="jstree-show-contextmenu" href="#">+</a>');
    						$('#jstree-show-contextmenu').click(function(){
    							$.tree.plugins.contextmenu.context_menu.show(n, t);
    						});
    					}
    				},
    				rem : function () {
    					if ($('#jstree-show-contextmenu').length > 0) {
    						$('#jstree-show-contextmenu').remove();
    					}
    				}
    			},
    			callbacks : {
    				oninit : function () {
    					if(!$.tree.plugins.contextmenu.css) {
    						var css = '#jstree-contextmenu { display:none; position:absolute; z-index:2000; list-style-type:none; margin:0; padding:0; left:-2000px; top:-2000px; } .tree-context { margin:20px; padding:0; width:180px; border:1px solid #979797; padding:2px; background:#f5f5f5; list-style-type:none; }.tree-context li { height:22px; margin:0 0 0 27px; padding:0; background:#ffffff; border-left:1px solid #e0e0e0; }.tree-context li a { position:relative; display:block; height:22px; line-height:22px; margin:0 0 0 -28px; text-decoration:none; color:black; padding:0; }.tree-context li a ins { text-decoration:none; float:left; width:16px; height:16px; margin:0 0 0 0; background-color:#f0f0f0; border:1px solid #f0f0f0; border-width:3px 5px 3px 6px; line-height:16px; }.tree-context li a span { display:block; background:#f0f0f0; margin:0 0 0 29px; padding-left:5px; }.tree-context li.separator { background:#f0f0f0; height:2px; line-height:2px; font-size:1px; border:0; margin:0; padding:0; }.tree-context li.separator span { display:block; margin:0px 0 0px 27px; height:1px; border-top:1px solid #e0e0e0; border-left:1px solid #e0e0e0; line-height:1px; font-size:1px; background:white; }.tree-context li a:hover { border:1px solid #d8f0fa; height:20px; line-height:20px; }.tree-context li a:hover span { background:#e7f4f9; margin-left:28px; }.tree-context li a:hover ins { background-color:#e7f4f9; border-color:#e7f4f9; border-width:2px 5px 2px 5px; }.tree-context li a.disabled { color:gray; }.tree-context li a.disabled ins { }.tree-context li a.disabled:hover { border:0; height:22px; line-height:22px; }.tree-context li a.disabled:hover span { background:#f0f0f0; margin-left:29px; }.tree-context li a.disabled:hover ins { border-color:#f0f0f0; background-color:#f0f0f0; border-width:3px 5px 3px 6px; }';
    						$.tree.plugins.contextmenu.css = this.add_sheet({ str : css });
    					}
    				},
    				onrgtclk : function (n, t, e) {
    					$.tree.plugins.contextmenu.context_menu.show(n, t, e);
    				},
    				onselect : function(n, t) {
    					$.tree.plugins.contextmenu.context_menu.rem();
    					$.tree.plugins.contextmenu.context_menu.add(n, t);
    				},
    				ondeselect : function(n, t) {
    					$.tree.plugins.contextmenu.context_menu.rem();
    				},
    				onchange : function () { 
    					$.tree.plugins.contextmenu.context_menu.hide(true);
    				},
    				beforedata : function () {
    					$.tree.plugins.contextmenu.context_menu.hide(true);
    				},
    				ondestroy : function () {
    					$.tree.plugins.contextmenu.context_menu.hide(true);
    				}
    			}
    		}
    	});
    	$(function () {
    		$.tree.plugins.contextmenu.context_menu.object.hide().appendTo("body");
    		$("#jstree-contextmenu a")
    			.live("click", function (event) {
    				if(!$(this).hasClass("disabled")) {
    					$.tree.plugins.contextmenu.context_menu.exec.apply(null, [$(this).attr("rel")]);
    					$.tree.plugins.contextmenu.context_menu.hide();
    				}
    				event.stopPropagation();
    				event.preventDefault();
    				return false;
    			})
    		$(document).bind("mousedown", function(event) { if($(event.target).parents("#jstree-contextmenu").size() == 0) $.tree.plugins.contextmenu.context_menu.hide(); });
    	});
    })(jQuery);

    Jeśli ktoś chce aby plusik pojawiał się tylko w Operze musi nieco zmodyfikować funkcje onselect i ondeselect dodając stosowny warunek. Przypominam, że [jquery umożliwia w łatwy sposób identyfikację przeglądarki](http://docs.jquery.com/Utilities/jQuery.browser), oczywiście jeśli przedstawia się „prawdziwym imieniem”.

    Idąc dalej śladem zaspokajania własnych potrzeb napisałem też swój plugin. Nazwałem go zgodnie z przyjętą zasadą **”Context conten”** i jak się można domyślić służy on do wyświetlania określonej treści powiązanej z danym elementem. W moim konkretnym przypadku miał służyć do wyświetlania podpowiedzi pod elementem w chwili jego wybrania.

    (function ($) {
    	$.extend($.tree.plugins, {
    		"contextcontent" : {
    			contenxt_content: {
    				class_name: 'jstree-contextcontent',
    				show: function(NODE, TREE_OBJ) {
    					var id = $(NODE).attr('id');
    					var cls = this.class_name;
    					$('#'+id+' > .'+cls).show();
    					$('#'+id+' > .'+cls+' a').click(function() {
    						window.location.href = $(this).attr('href');
    					});
    				},
    				hide : function (NODE, TREE_OBJ) {
    					var id = $(NODE).attr('id');
    					var cls = this.class_name;
    					$('#'+id+' > .'+cls).hide();
    				}
    			},
     
    			callbacks : {
    				oninit : function (TREE_OBJ) {
    					if (TREE_OBJ.settings.plugins.contextcontent.class_name) {
    						$.tree.plugins.contextcontent.contenxt_content.class_name = TREE_OBJ.settings.plugins.contextcontent.class_name;
    					}
    					var cls = $.tree.plugins.contextcontent.contenxt_content.class_name;
    					if(!$.tree.plugins.contextcontent.css) {
     
    						var css = '.'+cls+' { display: none;}';
    						$.tree.plugins.contextcontent.css = this.add_sheet({ str : css });
    					}
    				},
    				onselect : function (NODE, TREE_OBJ) {
    					$.tree.plugins.contextcontent.contenxt_content.show(NODE, TREE_OBJ);
    				},
    				ondeselect : function(NODE, TREE_OBJ) {
    					$.tree.plugins.contextcontent.contenxt_content.hide(NODE, TREE_OBJ);
    				}
    			}
    		}
    	});
    })(jQuery);

    Aby go właściwie użyć trzeba:

    1\. Dołączyć skrypt do dokumentu html

    <script type="text/javascript" src="jquery.js" charset="utf-8"></script>
    <script type="text/javascript" src="jsTree/jquery.tree.js"></script>
    <script type="text/javascript" src="jsTree/plugins/jquery.tree.contextcontent.js"></script>

    2\. Dodać do configa jstree stosowny wpis

    $(function () {
    	$("#demo").tree({
    		plugins : {
    			contextcontent : {
    				class_name: 'jstree-contextcontent'
    			}
    		}
    	});
    });

    3\. No i utworzyć stosowny kod html. Proszę zwrócić uwagę na divy z atrybutem class „jstree-contextcontent”. Nazwę klasy oczywiście można zmienić pod warunkiem, że zrobi się to również w configu.

    <div id="demo">
    <ul>
    	<li id="phtml_1" class="open">
    		<a href="#"><ins>&nbsp;</ins>Root node 1</a>
    		<div class="jstree-contextcontent">Root node 1 tip...</div>
    		<ul>
    			<li id="phtml_2">
    				<a href="#"><ins>&nbsp;</ins>Child node 1</a>
    				<div class="jstree-contextcontent">Child node 1 tip...</div>
    			</li>
    			<li id="phtml_3"><a href="#">
    				<ins>&nbsp;</ins>Child node 2</a>
    				<div class="jstree-contextcontent">Child node 2 tip...</div>
    			</li>
    			<li id="phtml_4">
    				<a href="#"><ins>&nbsp;</ins>Some other child node with longer text</a>
    				<div class="jstree-contextcontent">Some other child node with longer text tip...</div>
    			</li>
    		</ul>
    	</li>
    	<li id="phtml_5"><a href="#">
    		<ins>&nbsp;</ins>Root node 2</a>
    		<div class="jstree-contextcontent">Root node 1 tip...</div>
    	</li>
    </ul>
    </div>
    Podziel się z innymi!

      1 Comment

      1. Paweł Ryznar

        Plugin fajny, contextmenu też ale zamiast blokować pewne akcje przez „valid_children” mogliby dać możliwość wyświetlania innego menu zależnie od wybranej gałęzi czy liścia. Bardziej przyjazne dla użytkownika.

      Dodaj komentarz

      Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *