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

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

Aug 21

Преди 2 дни видях в Ajaxian - A simple solution to the “other” problem with select boxes, което представлява решение на “другия” проблем, както би се превело буквално. Всъщносто там е представен jquery код с който когато от даден html select избереш “друг(other)” ти се появява input поленце където да кажеш какво точно е това другото. Вижте демо-то, защото май не го обясних добре.

Като идея е добре, но нещо jquery кода не ми се вижда много читав:


$(document).ready( function () {
	$('.leader').each( function () {
		var name = $(this).attr('name');
		if ($(this).val()!='other') {
			$(this).next().removeAttr('name').hide();
		 }
	});

	$('.leader').change(onChange);

	function onChange(){
		var desiredName = $(this).attr('name');
		if ($('#'+desiredName).val()=='other') {
			$('#'+desiredName).next().attr('name',desiredName).fadeIn('fast');
		} else {
			$('#'+desiredName).next().removeAttr('name').fadeOut('fast');
		}
	}
});

Много бях изненадан, че това стигна до Ajaxian (въпреки че доста са свалили летвата напоследък). В този код има няколко неща, които не ми харесват:

  1. 2 пъти прави почти едно и също,  избира всичките елементи с клас “leader” и проверява дали е избрана като стойност “other”, за да покаже/скрие следващия елемент.
  2. 2 пъти прави $(’.leader’), като явно е забравил, че едно от най-яките неща в jquery e changing-a. Спокойно е можел да направи просто $(’.leader).each( … ).change( … )
  3. на 3тия ред ( var name = $(this).attr(’name’) ), защо го има това и какво прави така и не разбрах, никъде в този scope не ползва name променливата, а и аз лично бих ползвал getAttribute за извличане на атрибута.
  4. в each-a се вика 3 пъти $() за един и същ елемент ( this в случая), по-добре е добре да се вземе jQuery инстанцията и да се запише в променлива - ще бъде доста по-бързо, когато се ползва.
  5. аз лично нямаше да сложа скобите на if-a в each-a, защото когато се пише javascript всеки байт е важен
  6. така и не ми стана ясно защо декларира функция onChange като може да ползва директно анонимна функция при $(’.leader’).change( … ).
  7. тук добре е направил, че е взел desiredName и го сложил в променлива, но ако някой ми каже защо при положение, че има this му трябва цели 3 пъти да вика jquery css selector с #id (и даже да не го записва в поменлива) ?!?
  8. малко ме подразниха и излишните празни редове и разстояния

Това са горе долу лошите неща, които видях, въпреки че не пиша много jquery код ми се струва че ако напише кода по този начин ще е доста по-добре:


$(document).ready(function(){
	$('.leader').change(function(
		var select = $(this);
		if(select.val() == 'other')
			select.next().attr('name', select.getAttribute('name')).fadeIn('fast');
		else
			select.next().removeAttr('name').fadeOut('fast');
	)).triggerHandler('change');
});

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

п.п. Някои хора виждат и проблем, че само при стойност “other” се появява полето “други” и ако имаш два или повече езика би било проблем. Обаче аз виждам нещата така - това би бил селекта за български език например:


<select name="language">
	<option>Български</option>
	<option>Английски</option>
	<option value="other">Друг ...</option>
</select>

т.е. value може и да е “other” но за потребителя да е всеки избран език ;)

Aug 09

Наскоро, един приятел ме помоли да му направя проста програмка, няма да изпадам в подробности за това какво точно трябваше да има в програмката. И аз реших да я направя по най-простия начин, за който се сетих - Adobe AIR( винаги ми е било странно защо му викат Adobe AIR, защото до колкото знам AIR e Adobe Integrated Runtime…). Някой хора биха се сетили за нещо от типа на JAVA или Flash/Flex подобни, но за мен някак си AIR-a е доста по-интересен и удобен.

Та реших още и да не ползвам стандартната си JavaScript библиотека Prototype, още повече че малко през зъби бих казал че за момента Prototype и AIR не се разбират на 100% (което до версия 1.6.1, да се надявяме, ще се оправи). И така ми останаха 2 избора - jQuery и Mootools. Предпочетох Mootools защото ми е някак си по-близо до сърцето, а и Prototype и Mootools за мен са като 2 разклонения на една идея, така че почнах.

Програмката стана доста бързо и доста добре :) и като цяло Mootools-a се оказа доста удобен, въпреки че имахме някои пререкания. Като например:

  • addEvent в Mootools e observe в Prototype
  • addClass, removeClass, hasClass в Mootools са addClassName, removeClassName, hasClassName в Prototype
  • new Class({ … }) в Mootools e Class.create({ … }) в Prototype
  • getNext, getPrevious, getParent, getFirst, getChildren в Mootools са горе-долу next, previous, up, down, select
  • …. общо взето такива са разлики

Естествено има неща който в Mootools ги има а в Prototype ги няма, но те не са толкова сложни, че да не може да се добавят от Prototype в Mootools и обратно. Например нещата който най-много ми харесаха в Mootools бяха:

  • Class.Extra - наистина страхотна идея, много полезни и даже си мисля да си ги вложа във версията на Prototype, която ползвам
  • Assets.image - това не го ползвах в AIR, но просто е безценно, защото не мога да кажа колко пъти аз и колеги сме имали проблеми с не заредени снимки (под IE6 главно)
  • Array.link - наистина интересен метод, който доста ми помогна във функциите с “неясни” аргументи
  • DomReady - и това в AIR не го ползвах, а и в Prototype си имаме dom:ready но той под IE6 не работи добре
  • FX - ефектите в Script.aculo.us са доста повече и са ми по-удобни, но тези са по-бързи и доста по “стабилни” като се говори за preformance. Това ще се промени със Script.aculo.us 2.0 / Scripty, … но то още не е готово, а и поне за сега ги няма стандартните ефекти и трябва да се правят ръчно.

Поне това са основните неща който настина ми харесаха в Mootools (сигурно и други щеше да има ако бях работил повече).

Но и доста неща с който бях свикнал в Prototype, а в Mootools ги нямаше. Няма да ги изреждам защото не са малко. Но както казах по-горе и Mootools и Prototype позволяват да се добавят extendet и то по доста лесен начин и за това си port-нах 2-3 неща:

Тъй като много ми липсваше Enumerable модула от Prototype и особено invoke метода реших да си го добавя:


Array.implement({
	invoke: function(method){
		var args = $A(arguments).slice(1);
		return this.map(function(value) {
			return value[method].apply(value, args);
		});
	}
});

Тук искам да кажа само, че много ми харесаха implement методите който са вградени към по-голямата част от Mootools Native обектите, за което в Prototype се използва Оbject.extend и prototype атрибута на съответния обект.

Mootools има Element.inject(el[, where]) и Element.grab(el[, where]), но Prototype има Element.insert което според мен е доста по-добра от 2те функции който са в Mootools


Element.implement({
	insert: function(insertions){
		if ($type(insertions) == 'element' || insertions.toElement)
			insertions = { bottom: insertation };

		for(var insert in insertions){
			var element = insertions[insert];
			Element.Inserters.get(insert, this, $(element.toElement ? element.toElement() : element, true));
		}

		return this;
	}
});

Това е само елементарта форма на Protoype фунцията защото в истинската Element.insert се приемаха и само html елементи и класове, и чист html код. Но, това не ми се наложи да го ползвам и за това не си играх да го port-вам.

Други две малки “подобрения”, който сложих бяха:


Event.implement({
	findElement: function(pattern){
		return this.target.match(pattern) ? this.target : this.target.getChildren(pattern)[0];
	}
});

function $w(string){
	if ($type(string) != 'string') return [];
	string.trim();
	return string ? string.split(/\s+/) : [];
}

Общо взето това са моите наблюдения (малко по-дълги се оказаха от колкото си мислех :) ). Поне за сега за мен Prototype си е номер 1, но и Mootools е много добра алтернатива (особено ако Prototype, не си оправят Adobe AIR съпорта ). А и Mootools ми е някак си твърде “лъскав”, докато prototype ми се струва по “hardcore”, но това си е мое мнение.

То дефакто дали човек избере Mootools, jQuery, Prototype, YUI, ExtJS, …. все ще е прав :)

п.п. ако някой има намерение да се занимава със Adobe AIR и Mootools непременно да погледне Snippely от кода му и от него като цяло могат да се научат безценни неща :)

Jul 16

Тези дни нямах много свободно време, но това не ми пречи да помогна малко на колега с JavaScript -а :) .
Така, основния проблем на моя колега беше че има списък със малки снимки и като се натисне някоя от тях, тя трябва да се покаже в цял размер. И както карам колегите в офиса, от доста време, JavaScript-a трябва да е unobtrusive и цялата страница да е функционална дори и с изключен от браузарът JavaScript. HTML кода ще изглежда горе-долу така:


<div id="big_image"><img src="{$picUrl}" alt="" /></div>
// ... code
<ul>
{foreach from=$pictures item=pic}
	<li><a href="{$pic.url}" target="_blank" rel="gallery"><img src="{$pic.thumb}" alt="" /></a></li>
{/foreach}
</ul>

Идеята е ако няма JavaScript просто снимката ще се отвори в нов прозорец, допълнителен плюс е и че ще имаме пътя за оригиналната снимката, достъпен чрез атрибута href на линковете.
Та колегата написъл следното:


Event.observe(window, 'load', function(){
	$$('a').each(function(a){
		a.observe('click', function(e){
			if (a.getAttribute('rel') == 'gallery') {
				$$('#big_image img').setAttribute('src', a.getAttribute('href'));
				e.stop();
			}
		});
	});
});

На пръв поглед не изглежда зле, но само на пръв поглед. Като за начало под много неуважавания IE6 снимката няма се сменя. А и  трябва и да се добави class=”selected” на избраната картинка.

Така че реших първо да пефасонирам кода и се получи това (сложих id=”thumb” на ul-то с малките снимка за по-бързо намиране на елементите):


document.observe('dom:loaded', function(){
	$$('#thumb a[rel=gallery]').invoke('observe', 'click', function(e){
		e.stop();
		$('big_image').down('img').setAttribute('src', this.getAttribute('href'));
	});
});

После добавих кръпката за IE6, за опресняване на снимка защото само със смяна на атрибута src няма да се промени снимката(поне в повечето случай).  И най-сигуния начин, който знам (ако някой знае по-добър, нека сподели) е изцяло да заменям img таг-а с нов img таг и така кода става такъв ( + това за class=”selected” )


document.observe('dom:loaded', function(){
	$$('#thumb a[rel=gallery]').invoke('observe', 'click', function(e){
		e.stop();
		$('big_image').innerHTML = '<img src="' + this.getAttribute('href') + '" />';
		this.up('ul').select('.selected').invoke('removeClassName', 'selected');
		this.addClassName('selected');
	});
});

Горе долу това е, като само в бъдеще един ефект за появяване и скриване на снимката трябва да се добави :)