# Виджеты jQuery UI: учебное пособие

JavaScript @ 20 Февраль 2010

Перевод статьи Understanding jQuery UI widgets: A tutorial.

«Виджет» для меня обозначает элемент пользовательского интерфейса, как кнопка или нечто более сложное, вроде всплывающего календарика, но в jQuery UI оно может означать класс, свойства которого связаны с HTML элементами; вроде Draggable и Sortable. В действительности, не все то что я назвал бы виджетом, использует $.widget, например – UI Datepicker его не использует.

Изменение элементов: плагины

Возьмем параграф (p) с классом target:

<p class="target">Это параграф.</p>

Это параграф.

Сделаем его зеленым. Мы знаем как: $('.target').css({background: 'green'}).

Для частого использования лучше сделать его плагином:

$.fn.green = function() {
	return this.css({background: 'green'})
}

Однако, предоставляя возможность реализовать некое поведение на выбранных элементах, плагин не дает нам возможности сохранить связь с этими элементами, так чтобы позднее мы могли бы сделать нечто вроде $('.target').off() для удаления зеленого фона, но только в том случае, если мы использовали green для его установки. Также мы не можем связать состояние с элементом, чтобы сделать $('.target').darker(), для чего бы потребовалось знать, насколько зеленый данный элемент в настоящее время.

Хранение состояния в плагинах

Мы могли бы создать объект и связать его с элементом, используя JavaScript expandos:

element.myobject = new Myobject({'target': element})

Вот пример кода:

$.fn.green2 = function() {
	return this.each(function(){
		// связываем наш объект для хранения состояния с элементом
		if (!this.green) this.green = new Green(this);
		this.green.setLevel(15);
	});
};
$.fn.off = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(16);
		delete this.green; // очищаем память
	});
};
$.fn.darker = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(this.green.getLevel()-1);
	});
};
$.fn.lighter = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(this.green.getLevel()+1);
	});
};
 
function Green(target){
	greenlevels = ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0','#fff'];
	this.target = target; // связываем элемент с объектом
	this.level = 0;
	this.getLevel = function() { return this.level; }
	this.setLevel = function(x) {
		this.level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.target.css({background: greenlevels[this.level]});
	}
};

Но это ужасно загрязняет пространство имен $.fn, функциями off, darker and lighter. Существуют способы создания реальных пространств имен в $.fn, однако обычно используется способ, в котором имя вызываемой функции передается в строке. Таким образом element.green2() используется для инициализации плагина, element.green2('darker') или element.green2('lighter') – для того чтобы им управлять.

$.fn.green2 = function(which){
	return this.each(function(){
		if (which === undefined){ // инициализирующий вызов
			// связываем наш объект для хранения состояния с элементом
			if (!this.green) this.green = new Green($(this));
			this.green.setLevel(15);
		}else if (which == 'off'){
			if (this.green) this.green.setLevel(16);
			delete this.green
		}else if (which == 'darker'){
			if (this.green) this.green.setLevel(this.green.getLevel()-1);
		}else if (which == 'lighter'){
			if (this.green) this.green.setLevel(this.green.getLevel()+1);
		}
	});
};
 
function Green(target){
	greenlevels = ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'];
	this.target = target; // связываем элемент с объектом
	this.level = 0;
	this.getLevel = function() { return this.level; }
	this.setLevel = function(x) {
		this.level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.target.css({background: greenlevels[this.level]});
	}
};
<p class="target">Это тестовый параграф.</p>

Это тестовый параграф.

Проблеммы при связывании объекта с плагином

Однако, так вы получаете проблемы с циклическими ссылками (посмотрите this.green = new Green($(this)) передает DOM-элементу ссылку на объект JavaScript, а this.target = target передает объекту JavaScript ссылку на элемент DOM) и утечку памяти: браузеры (особенно Internet Explorer) используют различные уборщики мусора для элементов DOM и объектов JavaScript. Циклические ссылки приводят к тому, что каждый уборщик мусора думает, что другой объект еще используется, и не будет ничего удалять.

Мы также не должны забывать очищать память (используя delete), если плагин нам больше не нужен.

jQuery решает проблему циклических ссылок с помощью плагина $.fn.data: $(element).data('myobject', new Myobject({'target': element})). Но таким образом мы получаем кучу работы с документацией по коду, и это скрывает внутреннюю логику программы. Как известно, шаблоны проектирования отражают слабости языка. Если мы постоянно воспроизводим шаблон, его нужно абстрагировать и автоматизировать использование.

Решение проблеммы: $.widget

Таким образом мы приходим к $.widget. Он создает плагин и взаимодействующий с ним класс JavaScript, связывая экземпляр этого класса с каждым элементом так, чтобы мы могли работать с объектом и влиять на элемент без проблем с утечкой памяти.

Вы все ещё должны создавать конструктор для вашего класса, но вместо реальной функции-конструктора, вам нужен объект-прототип со всеми необходимыми методами. Существует несколько соглашений: функция _init вызывается при создании, функция _destroy вызывается при удалении. Обе из них определены заранее, но вы можете переопределить их (и, скорее всего, вам нужно будет изменить _init). element – это связанный объект jQuery (то, что мы выше называли target).

Методы виджета начинающиеся на «_» – псевдоприватные, они не могут быть вызваны с помощью нотации вида $(element).plugin('string').

var Green3  = {
	_init: function() { this.setLevel(15); },
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
	level: 0,
	getLevel: function() { return this.level; },
	setLevel: function(x) {
		this.level = Math.floor(Math.min(this.greenlevels.length-1, Math.max(0,x)));
		this.element.css({background: this.greenlevels[this.level]});
	},
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	off: function() {
		this.element.css({background: 'none'});
		this.destroy(); // используем встроенную функцию
	}
};

Заметьте – это все лишь программная логика, без учета данных в DOM или памяти.
Теперь нам нужно выбрать имя, которому должно предшествовать имя пространства имен, например «ns.green». К сожалению пространство имен реально не используется, плагин вызывается как $().green(). Имя конструктора – $.ns.green, но вы его использовать никогда не будете, поэтому можете использовать и «официальное» пространство имен «ui». Впрочем определение виджета не могло бы быть проще:

$.widget("ui.green3", Green3);

Управление виджетами

Так что там насчет наших управляющих функций? Все функции, определенные в прототипе, которые не начинаются с символа подчёркивания, доступны извне автоматически: $('.target').green3() создает виджеты, $('.target').green3('darker') управляет ими.

Если ваша функция предназначена быть «геттером», то есть возвращать значение, вместо того чтобы управлять объектом (как $('.target').html() возвращает innerHTML), вы должны сообщить это виджету передав ему строку списка имен (разделенных пробелом или запятой) или массив имен. Отметьте – лишь одно значение для первого элемента объекта jQuery будет возвращено, точно также как .html() или .val().

$.ui.green3.getter = "getLevel otherGetter andAnother";
// или
$.ui.green3.getter = "getLevel, otherGetter, andAnother";
// или
$.ui.green3.getter = ["getLevel","otherGetter","andAnother"];
<p class="target">Это тестовый параграф.</p>

Это тестовый параграф.

Данные для каждого виджета

Проницательный читатель должно быть заметил, что level – свойство класса; одно и то же свойство используется для каждого объекта green3. Это явно не то, что мы хотим – у каждого экземпляра должна быть собственная копия. $.widget определяет еще две функции, которые позволяют нам сохранять и извлекать данные для каждого экземпляра индивидуально: _setData и _getData. Обратите внимание, что эти функции объекта виджета, а не объекта jQuery. Они не используют $(element).data('widgetName'), который возвращает сам виджет. Для хранения данных используется объект this.options. Таким образом:

var Green4  = {
	getLevel: function () { return this._getData('level'); }, // мы также могли использовать this.options.level 
	setLevel: function (x) {      // напрямую, но использование функций дает нам больше гибкости
		var greenlevels = this._getData('greenlevels');
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this._setData('level', level);
		this.element.css({background: greenlevels[level]});
	},
	_init: function() { this.setLevel(this.getLevel()); }, // берем значение по умолчанию и используем его
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	off: function() {
		this.element.css({background: 'none'});
		this.destroy(); // используем встроенную функцию
	}
};
$.widget("ui.green4", Green4);
$.ui.green4.getter = "getLevel";

Значения по умолчанию хранятся в объекте виджета defaults:

$.ui.green4.defaults = {
	level: 15,
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
};

При создании экземпляра плагина, передайте объект options (как в большинстве плагинов), чтобы перезаписать начальные значения: $('.target').green4({level: 8}).

Отметьте, что я также перенес список цветов в объект defaults, и он тоже может быть перезаписан. Возможно этот виджет уже не стоит больше называть «green»!

<p class="target">Это тестовый параграф.</p>

Это тестовый параграф.

This is a test paragraph called with .green4({ level:3, greenlevels: ['#000','#00f','#088', '#0f0', '#880', '#f00', '#808', '#fff'] }).

This is a test paragraph called with .green4({ level:3, greenlevels: ['#000','#00f','#088', '#0f0', '#880', '#f00', '#808', '#fff'] }).

Функции обратного вызова

Программист, внедряющий ваш виджет в свою страницу, возможно захочет сделать что-либо еще в тот момент когда виджет меняет своё состояние. Существует два способа уведомить вызывающую программу о том, что что-то произошло:

Сильное связывание (tightly coupled)

Вызывающая программа предоставляет функцию для вызова в критические моменты. Это так называемые функции обратного вызова или «колбэки» (англ. callback), которые используются в анимациях и Ajax. Мы можем создать функции-колбэки, которые будут вызываться виджетом, и передать их в плагин-конструктор виджета вместе с остальными настройками.

var Green5 = {
	setLevel = function(x){
		//...
		this.element.css({background: greenlevels[level]});
		var callback = this._getData('change');
		if ($.isFunction(callback)) callback(level);
	},
	// ... остальная часть виджета
};
$.widget("ui.green5", Green5);
 
$('.target').green5({change: function(x) { alert ("The color changed to "+x); } });

Слабое связывание (loosely coupled)

Способ также известен как шаблон проектирования «наблюдатель». Виджет посылает сигнал в фреймворк, а вызывающая программа информирует фреймворк, что хочет знать о сигнале. Так работают события по нажатию клавиши мыши (click) или клавиатуры, jQuery позволяет виджету создавать собственные события, а в вызывающей программе связывать их с соответствующими обработчиками:

var Green5 = {
	setLevel = function(x){
		//...
		this.element.css({background: greenlevels[level]});
		this.element.trigger ('green5change', [level]);
	},
	// ... остальная часть виджета
};
$.widget("ui.green5", Green5);
 
$('.target').green5();
$('.target').bind("green5change", function(evt,x) { alert ("The color changed to "+x); });

$.widget позволяет использовать оба метода с помощью метода _trigger. В объекте виджета this._trigger(type, event, data) принимает строку type с названием события, которое вы хотите использовать (используйте какие-нибудь короткие глаголы вроде ‘change’), опционально Event Object (если вы хотите передать нечто вроде метки времени или местоположения мыши – не беспокойтесь о event.type, _trigger изменяет его на специально сконструированное имя события) и любые данные, которые вы хотите передать в обработчик. _trigger создает пользовательское событие с именем widgetName+type, вроде green6change (почему он не делает его как type+'.'+widgetName за гранью моего понимания именование событий таким образом было предметом нескольких обсуждений), устанавливает его в event.type (создавая новый $.event, если он не предоставлен), вызывает this.element.trigger(event, data), затем ищет функцию обратного вызова с помощью callback = this._getData(type) и вызывает её callback.call(this.element[0], event, data).

Заметьте, что это означает, что если data является массивом, сигнатура функций несколько различается для обработчика события и функции обратного вызова. element.trigger() использует apply для того передать каждый элемент данных в отдельном параметре. Поэтому для this._trigger('change', 0, ['one', 'two']) требуется обработчик события вида function(event, a, b) и функция обратного вызова вида function(event, data).

На практике все не так уж и сложно, как кажется. В качестве примера, использование обоих методов:

var Green5  = {
	getLevel: function () { return this._getData('level'); },
	setLevel: function (x) {
		var greenlevels = this._getData('greenlevels');
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this._setData('level', level);
		this.element.css({background: greenlevels[level]});
		this._trigger('change', 0, level);
	},
	_init: function() { this.setLevel(this.getLevel()); }, // берем значение по умолчанию и используем его
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	off: function() {
		this.element.css({background: 'none'});
		this._trigger('done');
		this.destroy(); // используем встроенную функцию
	}
};
$.widget("ui.green5", Green5);
$.ui.green5.getter = "getLevel";
$.ui.green5.defaults = {
	level: 15,
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
};
<p class="target">Это тестовый параграф с уровнем зеленого <span class="level">undefined</span>.</p>

Это тестовый параграф с уровнем зеленого undefined.

//  The on button above does the following:
$('.target').green5({
	/* функция обратного вызова для обработки события "change" */
	change: function(event, level) { $('.level', this).text(level); } 
});
/* обработчик события "done" */
$('.target').bind('green5done', function() { $('.level', this).text('undefined');alert('bye!') });

Отслеживание мыши

Многое из того, что мы хотим делать при помощи виджетов, включает в себя отслеживание мыши, поэтому ui.core.js предоставляет специальный объект, включающий большое количество методов для этого. Все что нам надо сделать для этого – добавить объект $.ui.mouse как прототип нашего виджета:

var Green6 = $.extend({}, $.ui.mouse, {прочие необходимые функции});

Переопределите методы $.ui.mouse (_mouseStart, _mouseDrag, _mouseStop), делающие что-нибудь полезное, вызовите this._mouseInit() в вашем _init и this._mouseDestroy() в destroy. Вы также должны добавить ui.mouse.defaults в defaults вашего виджета. Проще всего это сделать так:

$.ui.widgetName.defaults = $.extend({}, $.ui.mouse.defaults, {мои значения по умолчанию...})

Давайте добавим возможность управления мышью в наш «зеленитель»:

Green6 = $.extend({}, $.ui.green5.prototype, $.ui.mouse,{ // расширяем Green5; создаем новый объект
	_init: function(){
		$.ui.green5.prototype._init.call(this); // вызываем функцию оригинала (родителя)
		this._mouseInit(); // начинаем обработку поведения мыши
	},
	destroy: function(){
		this._mouseDestroy();
		$.ui.green5.prototype.destroy.call(this); // вызываем функцию оригинала
	},
	// нужено переопределить методы _mouse
	_mouseStart: function(e){
		// сохраняет начальное положение мыши
		this._setData('xStart', e.pageX);
		this._setData('levelStart', this._getData('level'));
	},
	_mouseDrag: function(e){
		this.setLevel(this._getData('levelStart') +(e.pageX-this._getData('xStart'))/this._getData('distance'));
	}
});
$.widget("ui.green6", Green6);
$.ui.green6.defaults = $.extend({}, $.ui.mouse.defaults, {
	level: 15,
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
	distance: 10
});
<p class="target">Это тестовый параграф с уровнем зеленого <span class="level">undefined</span>.</p>

Это тестовый параграф с уровнем зеленого undefined.

Tags: , ,

One Response to “Виджеты jQuery UI: учебное пособие”

  1. xbox sales Says:

    best game xbox 360…

    I dispise blackout. Never mind the aircon, never mind TV and stereo, don’t worry the internet. However, if I can not recharge my cell phone therefore i could keep texting, that’s another point. I dispise blackout….

Leave a Reply