Постове 6 - 10 от 18 с таг prototype.js

Jun 15

Доста често напоследък ми се налага да правя динамични страници изцяло задвижвани от javascript, като при тях постоянно се добавят и махат dom елементи. Което значи непрекъснато да се добавят и махат event хандлъри, което е меко казано досадно и води до много грешки. Но с вградените в CD3.Behaviors event delegation функции (за тях специално ще има поне един цял пост) тази работа е много лесна, просто делегирам всички действия към елементи които няма да се променят.

До тук всичко звучи много добре, но както винаги IE се появява на сцената с бъг, който е, че submit действието няма bubbling (т.е. не се делегира към родителските елементи на формата). Което е в пълен разрез със спецификация, но какво да се прави свикнали сме.

Този проблем го знам от много време и винаги го заобикалях по един или друг начин. Но наскоро на колега му трябваше бързо решение, което да може да ползва на 2 – 3 места затова за няколко минути написах това за Prototype.js:

Element.addMethods({
    delegateSubmit: function(element, callback){
        return $(element)
            .observe('click', function(e){
                if (e.findElement('form') && e.findElement('input[type=submit],input[type=image]'))
                    callback.call(this, e);
            })
            .observe('keyup', function(e){
                if (e.keyCode  == Event.KEY_RETURN && e.findElement('input') && e.findElement('form'))
                    callback.call(this, e);
            })
        }
});

Като цяло това, което прави тази функция е, че наблюдава за натискане на submit или image бутони или за натискане на enter върху някои input. Лошото в случая е, че страдат и нормалните браузъри като Firefox или Safari.

Затова направих нова версия, която засича дали submit се делегира (за начина, по който разбирам пише по-подробно в тази статия – Detecting event support without browser sniffing)

Element.addMethods({
    delegateSubmit: (function(){
        var el = document.createElement('div'), isSupported = 'onsubmit' in el;

        if (!isSupported){
            el.setAttribute('onsubmit', 'return;');
            isSupported = typeof el.onsubmit == 'function';
        }

        return isSupported ? function(element, callback){
            return Event.observe(element, 'submit', callback);
        } : function(element, callback){
            return $(element)
                .observe('click', function(e){
                    if (e.findElement('form') && e.findElement('input[type=submit],input[type=image]'))
                        callback.call(this, e);
                })
                .observe('keyup', function(e){
                    if (e.keyCode  == Event.KEY_RETURN && e.findElement('input') && e.findElement('form'))
                        callback.call(this, e);
                    })
        };
    })()
});

Това е доста по-добро решение което оправя проблема със submit само, когато има такъв проблем.

Тук бих могъл примерно да използвам Function.wrap върху Event.observe, но нещо не съм фен такива monkey patching неща. А и по-скоро това и хака за делегиране на focus/blur под IE ще са част от моята Event.delegate, която ако имам късмет ще е част от Prototype.js.

Ако някой има по-елегантно решение, няма да му се разсърдя ако го сподели.

May 02

Вчера отделих целия ден да направя някои промени по блога, като основно имах доста забележки към темата (iTheme), която ползвам.

Първо започнах да правя малки промени по дизайна

  • сложих да се виждат таговете към даден пост
  • промених (с малко помощ от колега) изцяло извеждането на коментарите
  • формата за добавяне на коментар претърпя малки графични промени
  • тема вече е изцяло на български език (тук таме може да е останало нещо чуждоезично, но малко по-малко що променям)
  • доста по-добра 404 страница, за която използвах от Google Webmaster Tools - Enhance 404 pages
  • промени извеждането на постовете при търсене.

Аз в мрежата

После започнах промени по секциите

  • Добавих нова страница “Аз в мрежата“, в която има информация за част от различните ми регистрации из мрежата. Скоро ще има още връзки, просто на този етап нямам подходящи лога за тази страница (трябваше да има и страница “Аз съм”, но за нея не остана време :( )
  • Добавих “Подобни постове” към всеки пост, като за него използвам плъгина Contextual Related Posts
  • Изтрих от sidebar-a мета информацията и на нейно място сложих таговете в сайта
  • Преминах през Google Webmaster Tools и пооправих някой връзки в сайта, които не работеха, също така промених и няколко meta description-и и други дребни неща

Промени по кода

И след това започнах да работя по интересната част. Докато оправях темата забелязах, че колкото и да е красива от долу като код е просто … ужасна. Още повече самия WordPress не използва нещо като Smarty, и го кара на голо php (в този пост – “Be smart with smarty“, съм си казал мнението за Smarty). Всички шаблони на темата общо взето изглеждаха така:

<?php get_header(); ?>
	<div id="content">
		// кода за съответния шаблон
	</div>
	<div id="footer">
		<a href="...">WP Theme</a> &amp;
		<a href="...">Icons</a> by <a href="...">N.Design Studio</a>
	</div>
</div>
<?php get_sidebar(); ?>
<?php get_footer(); ?>

след малко refactoring в шаблон кода стана така, като преместих общите части в хедъра и футера:

<?php get_header(); ?>
	// кода за съответния шаблон
<?php get_footer(); ?>

Мина ми през главата, дали да не направя някои плъгин за WordPress, който позволява ползването на Smarty и/или layout файлове, подобни на тези които използвам от години в ControlDepo и които ги има в Rails, но в крайна сметка реших, че през лятото най-вероятно ще мина на някое мое блог решение.

След като подредих  и пренаписах шаблоните, се насочих към самата html структура, в която беше пренебрегнато ползването на ul елементи и други дребни неща който ме дразнеха. Но най-големия проблем от който най-вероятно страда доста SEOто на NeXt е че липсваше h1 елемент (т.е. имаше го но просто винаги беше просто NeXt), а той трябва все пак да е заглавието на самата страница.

JavaScript / CSS / Images

Поради използването на много плъгини се беше натрупало огромно количество външни javascript и css файлове, което много вреди на цялостното зареждане на сайта. Така че минах и събрах всичките css файлове в един all.css (останаха само print.css и css файловете от Highlight Source Pro плъгина, но там още не искам да пипам).

С JavaScript нещата седяха малко по сложно поради това че използвах plugin за live търсенето и за още 2-3 малки неща, решението беше да се отърва от всички плъгини и да си напиша собствени версии използвайки ControlDepo 3 Widgets.  От него използвах тези компоненти:

Отделих javascript-a в два файла – all.js и frontend.js ( както права във всичките си проекти напоследък ), Във all.js са библиотечните файлове – Prototype, Script.aculo.us, no.ie6.js, ControlDepo 3 Widgets а във frontend.js са скриптовете специално за блога ми livesearch, widgets и други малки javascript глезотии. По-важното е в случая е това че вече имам стабилна основа върху която да надграждам в бъдеще.

Всички javascript файлове са най-долу, така че няма смисъл да правя dom:loaded а мога директно да изпълнявам кода си. Също така вече и целия код е unobtrusive, и поне на този етап ако даден посетител няма javascript или му е изключен, всичко в сайта ще си работи.

Последната стъпка беше оптимизацията на снимките за това използвах просто ruby gem-a – smusher, който прекарва всяка снимка през smush.it. Така отстраних с около 40 KB размера на всички картинки от блога.

Финални думи

Като цяло за себе си съм си доволен от работата, която свърших по блога. Останаха някои неща за бъдещето:

  • live comment preview
  • най-сетне да имам about страница
  • да взема под IE да видя как се вижда NeXt
  • да се по съберат снимките в css spirites
  • почистване на css файловете, т.е. изтриване на излишните стилове и оптимизация
  • gzip на css / javascript файловете

п.п. от цялото тази работа около WordPress, ми хрумнаха две идеи за plugin, първия да е Smarty, а втория да е нещо подобно на sprockets което събира всички css / javascript във един файл gzip-ва го, добавя му etags и т.н. Незнам дали е възможно да се направи такова нещо с WordPress, ако някой има желание за нещо такова, с удоволствие бих му помагал.

Apr 18

Наскоро (преди около месец де) излезе Prototype 1.6.1 RC1, основната цел на тази версия е съвместимост с ново излезлият IE8, и Element.Store ( за който бях писал преди време ).

Toчно за  Element.Store исках да драсна някои ред, защото изненадващо за този тип storage няма много материали в нет-а.

Основната идея на Element.Store е да може да се добавя информация (като обекти) към даден  елемент, без да се страхуваме от memory leaks. Това в Prototype е реализирано със следните методи (под $(element) имам предвид референция към prototype extend element):

  • $(element).getStorage() – директен достъп до Storage-а на даденият елемент,  той просто представлява един Hash в който се запазват всички обекти, които искаме да прикачим към елемента. Дефакто store/retrive се явяват shortcut-и на get/set методите на този Hash.
  • $(element).store(key, value) – записва информация в даденият елемент, ключът трябва да е string (или обект с метод toString), после когато искаме да си вземем информацията отново използваме ключа. Възможно е наведнъж да се запишат повече от една двойка ключ – стойност: $(element).store({ key1: ‘value1′, key2: ‘value2′ /* и така на татък */});
  • $(element).retrieve(key, defaultValue) – извлича информация от елемента по даден ключ, като ако не съществува такъв ключ се връща  defaultValue.
  • вероятно $(element).unsetStorage() (ticket | gist) – този метод все още го няма, но е много вероятно да се появи във финалната 1.6.1 версия, той трябва да премахва цялата информация за дадения обект от Storage-a му. Ако искате в момента да ползвате unsetStorage може да вземете моята примерна версия от тук.

Как обаче това би се ползвало в реалния свят ? Първо най-вероятно във финалната версия 1.6.1 цялата event система на Prototype ще използва Element.Store. А аз ще си позволя да дам един малък пример за това как и къде се ползва Element.Store:

Имаме примерно масив с футболисти на Арсенал, и на страницата имаме бутони, чийто id-та се образуват от ‘button_’ + номера на футболиста. Целта е при натискане на бутон за даден футболист да се показва името му с alert.

var footballers = [
	{number: 14, firstName: 'Theo',     lastName: 'Walcott'},
	{number: 12, firstName: 'Carlos',   lastName: 'Vela'},
	{number: 23, firstName: 'Andrei',   lastName: 'Arshavin'},
	{number: 4,  firstName: 'Francesc', lastName: 'Fabregas'},
	{number: 5,  firstName: 'Kolo',     lastName: 'Toure'},
	{number: 25, firstName: 'Emmanuel', lastName: 'Adebayor'}
];

Общо взето без имаме 4 възможни варианта:

// Вариант 1: използваме closure за достъп до футболиста
footballers.each(function(f){
	$('button_' + f.number).observe('click', function(){
		alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
	});
});

// Вариант 2: използване на скит closure
function show(f){
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	$('button_' + f.number).observe('click', show.curry(f));
});

// Вариант 3: използване на референция в самия обект
function show(){
	var f = this.footballer;
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	var button = $('button_' + f.number);
	button.footballer = f;
	button.observe('click', show);
});

// Вариант 4: използване на Element.Store
function show(f){
	var f = this.retrieve('footballer');
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	$('button_' + f.number).store('footballer', f).observe('click', show);
});

Всички варианти работят и правят едно и също (или поне така си мисля :) ), но всеки си има и своите особености. Така Вариант 1 изглежда добър вариант и също така е доста се използва именно този метод, но това което ме притеснява е че анонимната функция към бутона се пре-декларира за всеки бутон (въпреки, че това пак зависи от интерпретатора). Също така при по сложни скриптове може да се получат обърквания кое от къде идва и на къде отива, а и от самия код не е много “чист”. Във Вариант 2 се използва curry, което прави кода малко по-чист, но въпреки, че не е веднага видимо пак се използва closure ( в curry метода ) и отново има генериране на доста анонимни функции.  Вариант 3 променя още структурата кода, като се маха curry, и се добавя референция към футболиста в самия обект, така не се генерират множество функции, но вероятността да се появят memory leak-ове е много голяма ( а и самия код изглежда най-зле от всички варианти ). За мен лично вариант 4 е най-удачния (особено при по-големи проекти), сега към обекта пак се записва референция към дадения футболист, но този път се използва безопасния Element.store .

Надявам се с този малък пример да съм показал част от възможностите който предоставя новия Element.Store механизъм и защо е полезен той :)

В заключения ще дам за пример с CD3.Select класа ми, който се използва за правенето на custom html select контроли. Като просто като се напише new CD3.Select(selectElement), автоматично selectElement-а се замества с група от div/ul/li елементи и така се дава възможност на дизайнерите ни в Pixeldepo, да ги настройват както намерят за добре (всъщност в моите ControlDepo 3 Widgets имам още цял арсенал от компоненти, с подобни функции). Та преди в CD3.Select за капсулирането на връзката между options обектите и техните производни li елементи използвах closure, като след появата на Element.Store промених нещата. При което се забелязах че доста по-добре и глатко  работят самите контроли и много по-малко памет се използва :)

Element.Store използвам още за Event.deletege, който много се надявам да попадне в core-a на Prototype.js и за който в следващата седмица мисля да напиша един пост.

Feb 20

Много бърз и елегантен (според мен) метод, с който можете да имате ‘private’ променливи в клас използвайки Prototype.js

var Person = Class.create({
	initialize: function(name, family){
		this.getName = function(){
			return name;
		}
		this.getFamily = function(){
			return family;
		}
		// code...code
	},
	getFullName: function(){
		return this.getName() + " " + this.getFamily();
	},
	// other methods
});

var p = new Person('Radoslav', 'Stankov');

console.log(p.getName(), p.getFamily(), p.getFullName());
Jan 24

Наскоро ми се наложи да работя по един стар проект на Pixeldepo, по който нямах участие до сега. Промените бяха главно козметични, така че реално аз нямах много работа там. Така че реших да разчистя малко javascript-а в сайта, който въпреки, че работеше без проблемибеше доста стар.

Така едно парче код ми се стори доста интересно (кода използва prototype.js и script.aculo.us):

$('.leftmenu li.header a').each(function(dropa){
	dropa.observe('click', function(){
		var title = dropa.className;
		var theul = $(title);
		if (theul) {
			if (theul.style.display == "none") {
				Effect.BlindDown(theul, {
					duration: 1.0
				});
				var theli = dropa.up(0);
				new Effect.Morph(theli,{
					style: 'padding-bottom: 20px;',
					duration: 1.0
				});
			}
			else {
				Effect.BlindUp(theul, {
					duration: 1.0,
					queue: 'end'
				});
				var theli = dropa.up(0);
					new Effect.Morph(theli,{
						style: 'padding-bottom: 20px;',
						duration: 0.1
				});
			}
		} else {
		}
	});
});

прочети още »