Nov 15

Това е втория пост за ControlDepo 3 Widgets - Behaviors (по план имам още един :) ).

Преди време ми се наложи да “освежавам” един стар проект използващ една от първите версии на CD3.Behaviors. Тази стара версия имаше само нормален селектор | event-селектори. Там забелязах че много често ползвам pattern (това ако някой на български ми го каже как е, ще съм много благодарен) а именно:


CD3.Behavoirs({
	'#container': function(contaner){
		contener.select('a').invoke('click', doSomeAction);
		contaner.select('a').invoke('mouseover', doSomeOtherAction);
		// .. и така си избирам всичките под елементи на #container и да им добавям event handler
	}
});

Това е примерен код за простичка javascript галерия, която има два бутона предишна / следваща картинка както и списък с thumbnail-и от който пак може да се покаже голямата снимка.  selectImage просто от a таг взема href атрибута и го показва като снимка с ефект и т.н, но то не е важното в случая.


CD3.Behaviors({
	// code
	'#gallery': function(){
		// при натискане на стрелката за предишна снимка да ...
		this.down('a.prev').observe('click', function(){
			var thumbs	 = $('thumbs'),
				selected = thumbs.down('a.selected');

			// покажи или предишнина или послената снимка
			selectImage(((selected && selected.up('li').previous('li')) || thumbs.select('li').last()).down('a'));
		});

		// при натискане на стрелката за следваща снимка да ...
		this.down('a.next').observe('click', function(){
			var thumbs	 = $('thumbs'),
				selected = thumbs.down('a.selected');

			// покажи следващата или първата снимка
			selectImage(((selected && selected.up('li').next('li')) || thumbs.select('li').first()).down('a'));
		});

		// тук правя много прост event delegation
		// избирам натиснатия a таг и викам selectImage с него
		$('thumbs').observe('click', function(e){
			e.stop();
			var a = e.findElement('a');
			if (a) selectImage(a);
		});
	}
	// code
});

Естествено някой от случаите могат да бъдат избегнати с event-delegation | инстанциране на класове, но  ми дойде друга, по-добра (според мен) идея. Добавих нов метод към CD3.Behavoirs - when. CD3.Behavios.when приема 2 параметъра 1вия е selector, а другия е hash object с behavoirs. Идеята се намерят елементи отговарящи на подадения селектор, се добавят и съответните behavoirs. Единственото по особеност тук е че selector-ите на behavoir-ите не се търсят в контекста на document а в контекста на елемента на който отговаря selectora подаден като първи аргумент.

И така със CD3.Behavios.when примера със новинарската джаджа изглежда така:


// ако съществува елементи #galley
// добави слените behaviors от контекста на #gallery
CD3.Behaviors.when('#gallery', {
	// при натискане на стрелката за предишна снимка да ...
	'a.prev:click': function(){
		var thumbs	 = $('thumbs'),
			selected = thumbs.down('a.selected');

		// покажи или предишнина или послената снимка
		selectImage(((selected && selected.up('li').previous('li')) || thumbs.select('li').last()).down('a'));
	},
	// при натискане на стрелката за следваща снимка да ...
	'a.next:click': function(){
		var thumbs	 = $('thumbs'),
			selected = thumbs.down('a.selected');

		// покажи следващата или първата снимка
		selectImage(((selected && selected.up('li').next('li')) || thumbs.select('li').first()).down('a'));
	},
	// избирам натиснатия a таг и викам selectImage с него
	'thumbs:click': {
		a: function(е){
			е.stop();
			selectImage(this);
		}
	}
});

Както се вижда кода е почти същия но сега изглежда доста по-добре и по-четим. В последните ми няколко по-натоварени с javascript проекти си структурирам джаджите в отделни извиквания на when и нещата изглеждат доста добре.

Oct 04

Днес имах малко свободно време и реших да погледна малко един проект, и по-точно javascript честа от него. И се спрях на един простичък Tab панел, нищо особено, но нещо не ми хареса как изглеждаше JavaScipt-а. HTML изглежда така:


<div class="tab-panel">
	<ul class="tab-header">
		<li class="selected">tab 1</li>
		<li>tab 2</li>
		<li>tab 3</li>
	</ul>
	<div class="tab-content">tab 1 content</div>
	<div class="tab-content" style="display: none;">tab 2 content</div>
	<div class="tab-content" style="display: none;">tab 3 content</div>
</div>

За да направя това таб панел използвах просто тази функция:


function tabulize(panel){
	// слагам div.tab-content вместо .tab-content,
	// защото когато селектора има само клас селектира всички елементи и ги проверява,
	// а ако се сложи div.tab-content се селектират се всички div-ове и те се проверяват,
	// което е по-бързо
	var elements	= panel.select('div.tab-content'),
		buttons		= panel.down('ul').select('li');

	// дефинирам тази функция тук,
	// защото искам да имам достъп до elements, buttons
	function activate(item, key){
		// тук 1во се маха клас selected от всички бутони,
		// и се скриват всичките div.tab-content
		buttons.invoke('removeClassName', 'selected');
		elements.invoke('hide');

		// после се показва избрания елемент и се слага клас selected на избрания бутон
		elements[key].show();
		item.addClassName('selected');
	}

	// на всеки бутон се добавя eventhandler за click,
	// който ще подава на activate избрания елемент и неговия номер във buttons масива
	buttons.each(function(item, key){
		item.observe('click', activate.curry(item, key));
	});
}

Това е доста простичко, но и трудно за промени като динамично добавяне на съдържание, ефекти и други. Затова реших да го направя малко по-OOП, като запазя основните идеи:


var TabPanel = Class.create({
	// това е конструктора
	initialize: function(panel){
		panel = $(panel);
		// избираме отново elements и buttons, но ги записваме като инстанс променливи
		this.elements = panel.select('div.tab-content');
		this.buttons = panel.down('ul').select('li').each(function(){
			// единствената разлика е че active не е private функция и инстанс метод
			// и за това тук ползваме bind, а не curry
			item.observe('click', this.activate.bind(this, item, key);
		}.bind(this));
	},
	// active, вече е метод и само "this." e разликата
	activate: function(item, key){
		this.buttons.invoke('removeClassName', 'selected');
		this.elements.invoke('hide');
		this.elements[key].show();
		item.addClassName('selected');
	}
});

Сега става малко по-тежко, но и доста по-extendable и податливо на бъдещи промени. Но тук видях нещо, което пак не ми се хареса е че колкото таб бутони имам толкова пъти и в двете версии викам observe и реших да видя как ще изглежда това, като добавим малко event-delegation, за което трябваха само две промени:


var TabPanel = Class.create({
	initialize: function(panel){
		panel			= $(panel);
		this.elements	= panel.select('div.tab-content');
		this.buttons	= panel.down('ul').select('li');
		// първо променяме тук
		// слагаме click eventhandler на ul (списъка с бутоните)
		// вътре проверяваме дали li елемента, който сме натиснали (ако има такъв)
		// е във масива с бутоните, ако е така активираме табулацията с този номер
		panel.down('ul').observe('click', function(e){
			var key = this.buttons.indexOf(e.findElement('li'));
			if (key != -1) this.activate(key);
		}.bind(this));
	},
	// второто нещо което променяме е тук
	// махаме item параметъра, и на го заменяме с this.buttons[key]
	activate: function(key){
		this.buttons.invoke('removeClassName', 'selected');
		this.buttons[key].addClassName('selected');
		this.elements.invoke('hide');
		this.elements[key].show();
	}
});

И поне на този етап съм доволен, при нужда мога да добавя destroy, addTab и други неща :) А как инстансирам TabPanel-а ? Ами със CD3.Behaviors:


CD3.Behaviors({
	'div.tab-panel': TabPanel
});
Oct 03

Наскоро направих, нещо което от доста време планирам - да събера публикувам част от JavaScript нещата които ползвам, като opensource. Така че ето ги ControlDepo 3 Widgets:

http://github.com/RStankov/controldepo-3-widgets/tree/master

Тук смятам да събера, основните JavaScript неща, които имам като custom form полета, ефекти, prototype допълнения и други подобни. И също така ще публикувам във блога и статии с които да описвам как се работи с дадения компонент (така най-сетне ще имам документация). И така първия и може би любимия ми :

CD3.Behaviors

CD3.Behavoirs e вдъхновен от CSS event:Selectors на Justin Palmer и LowPro на Dan Webb ( както и Behavior на  Ben Nolan, който вече го няма). Общо взето от около една и половина го ползвам и развивам и мисля че доста полезен. Работата на CD3.Behavoirs, условно се разделя на 4ри части:

нормален селектор | event-селектори | event-delegation | инстанциране на класове

Нормалния селектор просто селектира определения css селектори и вика върху всеки елемент подадената функция, като съответния елемент  се предава като първи аргумент и също така функцията се bind-ва към него т.е. като в подадената функция this е този елемент за който тя се извиква. Ето един малък пример:


CD3.Behaviors({
	'#container1': function(){
		this.insert('<strong>текст</strong>');
	},
	'#container1 a': function(a){
		a.observe('click', function(){ alert('#container1 a clicked'); })
	},
	'#container1 span': function(){
		alert('This will not excecute');
	}
});

Горния пример  има 3 селектора:

  1. #container1 - избира елемент с id = container1 и после в него добавя <strong>текст</strong> вътре в него
  2. #container1 a - избира всеки а елемент от container1 и добавя click event към него ( тук а се подава като аргумент, но може да се напише и this.observe )
  3. #container1 span - просто показва че ако няма елемент отговарящ на селектора, функцията не се вика

Event-селекторa разглежда селектора на 2 части {селектор част}:{event част}. Като намира всички елементи от дадения селектор и им добавя event listener-и (чрез Event.observe). Действията са много подобни на тези на CSS event:Selectors на Justin Palmer, да не кажа същите, с някои подобрения.


CD3.Behaviors({
	'#container2 a:click': function(){
		this.toggleClassName('clicked')
	},
	'#container2 a.mouse': {
		mouseover: function(){
			this.innerHTML = 'mouseover';
		},
		mouseout: function(){
			this.innerHTML = 'mouseout';
		}
	}
});

Какво става тук:

  1. #container2 a:click - избира всички а-та от #container2 и им добавя при click event да си сменят className -а
  2. #container2 a.mouse- е малко по-интересно. То селектира всички а-та с клас “mouse” в #container2 и започва да наблюдава (observe) две действия:
    • mouseover - просто добавя текст “mouseover” във а-тa
    • mouseout -просто добавя текст “mouseout” във а-то

Горния пример със нормален селектор би изглеждал така:


CD3.Behaviors({
	'#container2 a': function(a){
		a.observe('click', function(){
			this.toggleClassName('clicked');
		});
	},
	'#container2 a.mouse': function(a){
		a.observe('mouseover', function(){
			this.innerHTML = 'mouseover';
		});
		a.observe('mouseout', function(){
			this.innerHTML = 'mouseout';
		});
	}
});

Но просто има твърде много излишен код тук, а и в като се пише JavaScript - the size matters!

Event-delegation - тук става малко по “сложно” … за обяснение.  По принцип Event-delegation-a е доста лесен, и особено при по-динамични и натоварени javascript приложения си е задължителна практика. И понеже аз доста често го използвах реших да го вкарам във CD3.Behaviors като ползвам нещо подобно на Event.delegate от LowPro. Като основната ми цел беше да го “скрия” така да не се натрапва и според мен стана доста добре:


CD3.Behaviors({
	'#container3:click': {
		'span': function(){
			alert('span was clicked, span innerHTML is "' + this.innerHTML + '"');
		},
		'a': function(){
			alert('link was clicked, span innerHTML is "' + this.innerHTML + '"');
		},
		'div': function(){
			alert('nothing was clicked');
		}
	},
});

Така, какво става тук ? Ами, със нормален event-selector избираме #container3 и му добавяме click event. Само, че когато се натисне #container3 започва да се проверяват подадените селектори - span, a, div, в случая, и когато натиснатия елемент отговаря на някоя селектор се вика съответната функция, който е била зададена към селектора ( като даже и scope-a на функцията се сетва да е съответния елемент, така че ако е натиснат ’span’ елемент this ще е този елемент)

По начина по който съм направил event-delegation-a може да се пишат и такива неща:


CD3.Behaviors({
	'#container3': {
		mouseover: {
			'span': function(){
				this.addClassName('clicked');
			},
			'a': function(){
				this.addClassName('clicked');
			}
		},
		mouseout: {
			'span': function(){
				this.removeClassName('clicked');
			},
			'a': function(){
				this.removeClassName('clicked');
			}
		}
	}
});

Което е все едно да имаме 2 event-селектора - #container3:mouseover и #container3:mouseout, но горния код е доста по-бърз защото селектираме само ведъж #container3 :) да не говорим че е и доста по ясен.

Инстанциране на класове. Едно от най-яките неща в LowPro бяха Behavoirs класовете, обаче така и не ги използвах никъде, а и предпочитам да ползвам нормални prototype класове. Затова направих най-нормалното, което ми се виждаше, да направя всеки нормален prototype клас (без да променям нищо в prototype) да работи със CD3.Behaviors:


var TestWidget = Class.create({
	initialize: function(element, options){
		this.value = options || 0
		element.observe('click', this.click.bind(this));
	},
	click: function(){
		alert('widget with ' + this.value + ' was clicked');
	}
});
CD3.Behaviors({
	'#container4 a.first': TestWidget,
	'#container4 a.second': [TestWidget, 5]
});

Това, може да се напише и по този начин :


CD3.Behaviors({
	'#container4 a.first': function(){
		new TestWidget(this);
	},
	'#container4 a.second': function(){
		new TestWidget(this, 5);
	}
});

Единственото, лошо тук е че [TestWidget, 5] не може да приема (за сега) повече от един параметър, който играе ролята на options. А и повечето Widget класове, които ползвам са само със element и options аргументи.

Това горе-долу са основните части от CD3.Behaviors. В идните сигурно ще напиша как работи CD3.Bahaviors.when и как аз обикновенно използвам CD3.Bahaviors.

Sep 19

Сутринта видях това в builderau.com:

Firefox 3.1 Alpha 2 developer features

Тук става дума за новите неща в Firefox 3.1, която се очаква съвсем скоро да излезе. Основните функции които се споменават тук са:

  • Web worker threads - най-важното от всички, според мен. Това че по-тежките javascript действия, ще бъдат отделени в нова нишка, е страхотно. И май в скоро време Firefox, може да последва примера на Google Chrome.
  • HTML5 Video tag -  добре е че все повече браузъри почват да го поддържат и дано до 1-2 години да може да спокойно да се ползва в production сайтове.
  • CSS2.1 селектори ::before ::after - Страхотно е че ги има тези двете вече, защото във internal нещата, които ползвам работят само под Firefox така че тези ще са полезни
  • CSS3 атрибути -moz-border-image, Word-wrap, Text-shadow, box-shadow and column rule

Като цяло си мисля само, че web work threads и TraceMonkey, за който  John Resign доста говори напоследък, са нещата които могат да бъдат използвани от developer-ите още сега. А пък поддръжката на CSS2.1/CSS3/HTML5 е важна в дългосрочен план, защото колкото повече браузъри вградят тези технологии, толкова по бързо ще може наистина да ги използваме.

Sep 15

Днес ми се наложи да направя нещо такова:


function action(){ /* ... code ... */ };
$(element).observe('click', action);
action.call($(element));

да декларираш функция която да се извика само един път, и при това, че я има като event-handler, е нещо, което винаги съм мразил да правя.
Но за щастие се сетих за “The “other” problem”, и по точно за тази част от поста:

Даже John-David Dalton е направил и Prototype версия : http://pastie.org/255119, от която най-много ми хареса допълнението на Element.fire, която я очаквам в новата версия на Prototype :)

Така че само с това малко парче код:


Element.addMethods({
	fire: Event.fire.wrap(function(proceed, element, eventName, memo) {
		element = $(element);
		var w, event, eventID;
		// ако eventName е например "click mouseover custom:event",
		// ще се симулират деиствията: click mouseover custom:event
		$w(eventName)._each(function(name) {
			// просто ако диствието е custom:event
			// си се вика нормалния Еvent.fire
			if (name.include(':'))
				return proceed(element, name, memo);

			// тук е първо се взема eventID-то на елемента
			// после ако е имало такова eventID се вземат event.cache всичките
			// event handle-и за този event
			eventID = (element._prototypeEventID || [null])[0];
			if (!eventID || !(w = Event.cache[eventID][name])) return false;

			// просто се прави "лъжлив" event обект
			event = Event.extend({ });
			event.eventName = name;
			event.memo = memo || { };

			// извикват се event handle-и
			w._each(function(wrapper) { wrapper(event) });
		});
		return element;
	})
});

Горния проблем се решава просто така:


$(element).observe('click', function action(){ /* ... code ... */ }).fire('click');

Което освен, че е по-кратко, е и по-красиво. / За по-разбираемо, не знам, защото аз си го разбирам де :) /