jsTree – context content

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>

Podwójne potwierdzenie jako zaprzeczenie

Ostatnio, podczas małej firmowej imprezki urządzonej pod hasłem „pieczenia pierników” wywiązała się dyskusja. Uczestnicy, pełni dobrego humoru podrasowanego kropelką „czystego” alkoholu snuli dywagacje na temat znaczenia słowa „bynajmniej”. Oj ile się nasłuchałem interpretacji i domysłów. Nigdy bym nie przypuszczał wielości kontekstów i znaczeń w jakich ludzie są skłonni używać tegoż w sumie prostego i popularnego słowa.

W końcu wsparci wiedzą naszego firmowego copywritera oraz treścią wpisu na blogu [bynajmniej.pl pt. Bynajmniej to nie przynajmniej](http://bynajmniej.pl/bynajmniej-to-nie-przynajmniej), przynajmniej co niektórzy doznali objawienia. A kiedy już wszyscy zakonotowali prawdziwe znaczenie tego zgoła pospolitego słowa, okazało się, że w połączeniu z partykułą „nie” – powstałe w ten sposób wyrażenie „bynajmniej nie” – bynajmniej nie stało się oczywiste w użyciu.

Język polski naprawdę jest pełen niespodzianek i ciekawostek w rodzaju właśnie podwójnych zaprzeczeń. Przy tej okazji przypomniał mi się dowcip, który pozwoliłem sobie odszukać i przytoczyć.

*Profesor filologii polskiej na wykładzie:
– Jak państwo wiecie, w językach słowiańskich jest nie tylko pojedyncze zaprzeczenie. Jest też podwójne zaprzeczenie. A nawet podwójne zaprzeczenie jako potwierdzenie. Nie ma natomiast podwójnego potwierdzenia jako zaprzeczenia.
Na to głos z ostatniej ławki:
– Dobra, dobra…*

Posted in Priv by Zbigniew Heintze

htaccess z www czy bez

Sporadycznie – tworząc nowy serwis, czy stronę internetową publikuję ją na serwerze. Dążąc do optymalizacji pod względem SEO staram się aby serwis dostępny był tylko pod jednym adresem. Dokładnie rzecz ujmując to pod wieloma, ale przy wejściu z alternatywnego adresu użytkownik powinien zostać przekierowany na adres główny, a przy okazji powinien zostać wysłany nagłówek 301 Moved Permanently. Najczęściej alternatywne adresy występują w postaci z przedrostkiem (subdomeną) www i bez. Dla przykładu adres example.com może też wystąpić w wersji www.example.com co wydaje się być jednym i tym samym a tym czasem to dwa zupełnie różne adresy.

Korzystając z dobrodziejstw serwera Apache i tzw. mod-rewrite-a możemy do tego celu użyć plików .htaccess. Ponieważ rzadko modyfikuję wyżej wspomniane pliki, najczęściej wpisując w nie znane i wypróbowane regułki, lubię kiedy są możliwie uniwersalne. W tym przypadku udało mi się znaleźć przykłady, które świetnie się sprawdzają i z uwagi na swą uniwersalność nie wymagają modyfikacji po przekopiowaniu na inny serwer gdzie podpięta jest inna domena.

Wymuszenie subdomeny www…

RewriteCond %{HTTP_HOST} !^www. [NC]
RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]

i na odwrót.

RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ http://%1/$1 [R=301,L]