This commit is contained in:
Ilya Kantor 2014-10-26 22:10:13 +03:00
parent 06f61d8ce8
commit f301cb744d
2271 changed files with 103162 additions and 0 deletions

View file

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
</style>
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
function Voter(options) {
var self = this;
var elem = options.elem;
var voteElem = elem.find('.vote');
// вынесем текущее значение в отдельную переменную
var vote = options.value || 0;
// установить значение без генерации события
setVote(vote, true);
elem.on('click', '.down', onDownClick)
.on('click', '.up', onUpClick)
.on('mousedown selectstart', false);
// ----------- методы -------------
function onDownClick() {
// сам обработчик не меняет голос, он вызывает для этого функцию
voteDecrease();
}
function onUpClick() {
voteIncrease();
}
function voteDecrease() {
self.setVote( vote - 1 );
}
function voteIncrease() {
self.setVote( vote + 1 );
}
// сделали функцию приватной, чтобы её можно было вызывать до объявления
// в коде инициализации
function setVote(newVote, quiet) {
vote = newVote;
voteElem.html( vote );
if (!quiet) {
$(self).triggerHandler({
type: 'change',
value: vote
});
}
}
this.setVote = setVote;
}
var voter = new Voter({
elem: $('#voter'),
value: 5
});
$(voter).on('change', function(e) {
alert(e.value);
});
</script>
</body>
</html>

View file

@ -0,0 +1 @@
[edit src="solution"/]

View file

@ -0,0 +1 @@
{"name":"voter-event","plunk":"uRILPlo0yZNmQoC93SlM"}

View file

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
font-size: 18px;
}
.up, .down {
cursor: pointer;
color: blue;
font-weight: bold;
}
</style>
</head>
<body>
<div id="voter" class="voter">
<span class="down"></span>
<span class="vote">0</span>
<span class="up">+</span>
</div>
<script>
function Voter(options) {
var self = this;
var elem = options.elem;
var voteElem = elem.find('.vote');
// вынесем текущее значение в отдельную переменную
var vote = options.value || 0;
// установить значение без генерации события
setVote(vote, true);
elem.on('click', '.down', onDownClick)
.on('click', '.up', onUpClick)
.on('mousedown selectstart', false);
// ----------- методы -------------
function onDownClick() {
// сам обработчик не меняет голос, он вызывает для этого функцию
voteDecrease();
}
function onUpClick() {
voteIncrease();
}
function voteDecrease() {
self.setVote( vote - 1 );
}
function voteIncrease() {
self.setVote( vote + 1 );
}
// сделали функцию приватной, чтобы её можно было вызывать до объявления
// в коде инициализации
function setVote(newVote, quiet) {
vote = newVote;
voteElem.html( vote );
if (!quiet) {
$(self).triggerHandler({
type: 'change',
value: vote
});
}
}
this.setVote = setVote;
}
var voter = new Voter({
elem: $('#voter'),
value: 5
});
$(voter).on('change', function(e) {
alert(e.value);
});
</script>
</body>
</html>

View file

@ -0,0 +1,25 @@
# Голосовалка "на событиях"
[importance 5]
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя jQuery-механизм генерации событий на объекте.
Пусть каждое изменение голоса сопровождается событием `change`:
```js
var voter = new Voter({
elem: $('#voter'),
value: 5
});
$(voter).on('change', function(e) {
alert(e.value);
});
```
Все изменения голоса должны производиться централизованно, через метод `setVote`, который и генерирует событие.
Результат использования кода выше (планируемый):
[iframe border=1 height=60 src="index.html"].
Исходный документ возьмите из решения задачи [](/task/voter).

View file

@ -0,0 +1,8 @@
[edit src="solution"/]
Обратите внимание:
<ul>
<li>`onLiClick` не генерирует событие `select`. Это обработчик, его роль -- разобраться, что происходит, и передать работу нужным методам.</li>
<li>В событие передаётся массив значений `value`. Код виджета производит всю работу по подготовке этого значения. Было бы совершенно недопустимо, хотя это проще, передавать массив выбранных `LI`. Вообще, элементы -- это внутреннее дело компонента, они могут измениться в любой момент, доступ к ним снаружи крайне нежелателен.</li>
<li>Код получения значений вынесен в отдельную функцию `getValues` -- для чистоты (каждая функция делает свою работу), и потому что скорее всего она ещё где-то понадобится.</li>
</ul>

View file

@ -0,0 +1 @@
{"name":"selectable-list-events","plunk":"GqD0Tve2xqcgr9cfFBJO"}

View file

@ -0,0 +1,79 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.selected {
background: #0f0;
}
li {
cursor: pointer;
}
</style>
</head>
<body>
Клик на элементе выделяет только его.<br>
Shift+Клик добавляет/убирает элемент из выделенных.<br>
<ul>
<li>Винни-Пух</li>
<li>Ослик Иа</li>
<li>Мудрая Сова</li>
<li>Кролик. Просто кролик.</li>
</ul>
<script>
function ListSelect(options) {
var elem = options.elem;
var self = this;
elem.on('click', 'li', onLiClick);
elem.on('selectstart mousedown', false);
function onLiClick(e) {
if (!e.shiftKey) {
deselectAllItems();
}
toggleSelectItem( $(this) );
}
function deselectAllItems() {
elem.children().removeClass('selected');
}
function getValue() {
var value = [];
elem.children('.selected').each(function() {
value.push( $(this).html() );
});
return value;
}
function toggleSelectItem(li) {
li.toggleClass('selected');
$(self).triggerHandler({
type: 'change',
value: getValue()
})
}
}
var select = new ListSelect({
elem: $('ul')
});
$(select).on('change', function(e) {
alert(e.value);
});
</script>
</body>
</html>

View file

@ -0,0 +1 @@
{"name":"selectable-list-events","plunk":"GqD0Tve2xqcgr9cfFBJO"}

View file

@ -0,0 +1,79 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.selected {
background: #0f0;
}
li {
cursor: pointer;
}
</style>
</head>
<body>
Клик на элементе выделяет только его.<br>
Shift+Клик добавляет/убирает элемент из выделенных.<br>
<ul>
<li>Винни-Пух</li>
<li>Ослик Иа</li>
<li>Мудрая Сова</li>
<li>Кролик. Просто кролик.</li>
</ul>
<script>
function ListSelect(options) {
var elem = options.elem;
var self = this;
elem.on('click', 'li', onLiClick);
elem.on('selectstart mousedown', false);
function onLiClick(e) {
if (!e.shiftKey) {
deselectAllItems();
}
toggleSelectItem( $(this) );
}
function deselectAllItems() {
elem.children().removeClass('selected');
}
function getValue() {
var value = [];
elem.children('.selected').each(function() {
value.push( $(this).html() );
});
return value;
}
function toggleSelectItem(li) {
li.toggleClass('selected');
$(self).triggerHandler({
type: 'change',
value: getValue()
})
}
}
var select = new ListSelect({
elem: $('ul')
});
$(select).on('change', function(e) {
alert(e.value);
});
</script>
</body>
</html>

View file

@ -0,0 +1,12 @@
# Список с выделением и событием
[importance 5]
Добавьте в решение задачи [](/task/selectable-list-component) событие `select`.
Оно должно срабатывать при каждом изменении выбора и содержать список выбранных строк.
Во внешнем коде добавьте обработчик к списку, который при изменениях выводит список значений.
[iframe border="1" src="solution"]
В качестве исходного кода возьмите решение задачи [](/task/selectable-list-component).

View file

@ -0,0 +1,9 @@
В этом решении для закрытия селекта по клику вне него используется отслеживание произвольных кликов вне документа.
Альтернатива -- события `focusin/focusout`, т.е. считаем, что пока фокус в селекте -- он открыт. С одной стороны, это более мощный способ, он позволяет перемещаться по элементам управления при помощи [key Tab] и корректно обрабатывать уход при помощи клавиатуры.
С другой стороны, это решение не универсально: если выводится `alert`, то фокус "прыгает" в него, уходя с элемента, а потом возвращается обратно. При этом JavaScript зарегистрирует уход фокуса `focusout` и возвращение `focusin`, хотя по смыслу фокус с элемента никуда не уходил, просто был `alert`.
Побочный эффект -- к закрытию и раскрытию (лишнему) элемента управления при таких "ненамеренных" потерях фокуса. Поэтому был выбран `onclick`.
Решение: [edit src="solution"]Открыть в песочнице[/edit]

View file

@ -0,0 +1 @@
{"name":"customselect","plunk":"NSMq0hilrebQaa5zU0w6"}

View file

@ -0,0 +1,47 @@
.customselect {
width: 200px;
font-size: 14px;
display: inline-block;
}
.customselect-title {
height: 17px;
border: 1px solid #ADD8E6;
background-position: right;
background-image: url(http://js.cx/clipart/select-button.gif);
background-repeat: no-repeat;
padding: 2px;
cursor: pointer;
}
.customselect-options li {
padding: 2px;
cursor: pointer;
}
.customselect-options li:nth-child(even) {
background-color: #f0f8ff;
}
.customselect-options li:hover {
background-color: #7fffd4;
}
.customselect-options {
list-style: none;
margin: 0;
padding: 0;
display: none;
position: absolute;
z-index: 1000;
background: white;
width: 200px;
border-bottom: 1px solid #add8e6;
border-left: 1px solid #add8e6;
border-right: 1px solid #add8e6;
box-sizing: border-box;
}
.customselect-open .customselect-options {
display: block;
}

View file

@ -0,0 +1,61 @@
function CustomSelect(options) {
var self = this;
var elem = options.elem;
elem.on('click', '.customselect-title', onTitleClick);
elem.on('click', 'li', onOptionClick);
var isOpen = false;
// ------ обработчики ------
function onTitleClick(event) {
toggle();
}
// закрыть селект, если клик вне его
function onDocumentClick(event) {
var isInside = $(event.target).closest(elem).length;
if (!isInside) close();
}
function onOptionClick(event) {
close();
var name = $(event.target).html();
var value = $(event.target).data('value');
setValue(name, value);
}
// ------------------------
function setValue(name, value) {
elem.find('.customselect-title').html(name);
$(self).triggerHandler({
type: 'select',
name: name,
value: value
});
}
function toggle() {
if (isOpen) close()
else open();
}
function open() {
elem.addClass('customselect-open');
$(document).on('click', onDocumentClick);
isOpen = true;
}
function close() {
elem.removeClass('customselect-open');
$(document).off('click', onDocumentClick);
isOpen = false;
}
}

View file

@ -0,0 +1 @@
{"name":"customselect","plunk":"NSMq0hilrebQaa5zU0w6"}

View file

@ -0,0 +1,47 @@
.customselect {
width: 200px;
font-size: 14px;
display: inline-block;
}
.customselect-title {
height: 17px;
border: 1px solid #ADD8E6;
background-position: right;
background-image: url(http://js.cx/clipart/select-button.gif);
background-repeat: no-repeat;
padding: 2px;
cursor: pointer;
}
.customselect-options li {
padding: 2px;
cursor: pointer;
}
.customselect-options li:nth-child(even) {
background-color: #f0f8ff;
}
.customselect-options li:hover {
background-color: #7fffd4;
}
.customselect-options {
list-style: none;
margin: 0;
padding: 0;
display: none;
position: absolute;
z-index: 1000;
background: white;
width: 200px;
border-bottom: 1px solid #add8e6;
border-left: 1px solid #add8e6;
border-right: 1px solid #add8e6;
box-sizing: border-box;
}
.customselect-open .customselect-options {
display: block;
}

View file

@ -0,0 +1,61 @@
function CustomSelect(options) {
var self = this;
var elem = options.elem;
elem.on('click', '.customselect-title', onTitleClick);
elem.on('click', 'li', onOptionClick);
var isOpen = false;
// ------ обработчики ------
function onTitleClick(event) {
toggle();
}
// закрыть селект, если клик вне его
function onDocumentClick(event) {
var isInside = $(event.target).closest(elem).length;
if (!isInside) close();
}
function onOptionClick(event) {
close();
var name = $(event.target).html();
var value = $(event.target).data('value');
setValue(name, value);
}
// ------------------------
function setValue(name, value) {
elem.find('.customselect-title').html(name);
$(self).triggerHandler({
type: 'select',
name: name,
value: value
});
}
function toggle() {
if (isOpen) close()
else open();
}
function open() {
elem.addClass('customselect-open');
$(document).on('click', onDocumentClick);
isOpen = true;
}
function close() {
elem.removeClass('customselect-open');
$(document).off('click', onDocumentClick);
isOpen = false;
}
}

View file

@ -0,0 +1,51 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Селект</title>
<script src='http://code.jquery.com/jquery.min.js'></script>
<link rel="stylesheet" href="customselect.css"/>
<script src="customselect.js"></script>
</head>
<body>
<div>Последний результат: <span id="result">...</span></div>
<div id="animal-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<!-- значение хранится в свойстве data-value -->
<li data-value="bird">Птицы</li>
<li data-value="fish">Рыбы</li>
<li data-value="animal">Звери</li>
<li data-value="dino">Динозавры</li>
<li data-value="simplest">Одноклеточные</li>
</ol>
</div>
<div id="food-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<li data-value="carnivore">Плотоядные</li>
<li data-value="herbivore">Травоядные</li>
<li data-value="omnivore">Всеядные</li>
</ol>
</div>
<script>
var animalSelect = new CustomSelect({
elem: $('#animal-select')
});
var foodSelect = new CustomSelect({
elem: $('#food-select')
});
$([animalSelect, foodSelect]).on('select', function (event) {
$('#result').html(event.value)
});
</script>
</body>
</html>

View file

@ -0,0 +1,51 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Селект</title>
<script src='http://code.jquery.com/jquery.min.js'></script>
<link rel="stylesheet" href="customselect.css"/>
<script src="customselect.js"></script>
</head>
<body>
<div>Последний результат: <span id="result">...</span></div>
<div id="animal-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<!-- значение хранится в свойстве data-value -->
<li data-value="bird">Птицы</li>
<li data-value="fish">Рыбы</li>
<li data-value="animal">Звери</li>
<li data-value="dino">Динозавры</li>
<li data-value="simplest">Одноклеточные</li>
</ol>
</div>
<div id="food-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<li data-value="carnivore">Плотоядные</li>
<li data-value="herbivore">Травоядные</li>
<li data-value="omnivore">Всеядные</li>
</ol>
</div>
<script>
var animalSelect = new CustomSelect({
elem: $('#animal-select')
});
var foodSelect = new CustomSelect({
elem: $('#food-select')
});
$([animalSelect, foodSelect]).on('select', function (event) {
$('#result').html(event.value)
});
</script>
</body>
</html>

View file

@ -0,0 +1,20 @@
# Свой селект
[importance 5]
Напишите свой, самостоятельно оформленный, селект.
Требования:
<ul>
<li>Открытие и закрытие по клику на заголовок.</li>
<li>Закрытие селекта происходит при выборе или клике на любое другое место документа, в том числе на другой аналогичный селект.</li>
<li>Событие `"select"` при выборе опции.</li>
<li>Значение опции хранится в атрибуте `data-value` (HTML-структура есть в исходном документе).
</ul>
Например:
[iframe src="solution" border="1" height="200" newwin]
В примере выше два селекта, чтобы можно было проверить процесс открытия-закрытия.
[edit src="task" task/]

View file

@ -0,0 +1 @@
{"name":"customselect-src","plunk":"cCX1mrKxjKiSibESyEaZ"}

View file

@ -0,0 +1,55 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Селект</title>
<script src='http://code.jquery.com/jquery.min.js'></script>
<link rel="stylesheet" href="customselect.css"/>
<script src="customselect.js"></script>
</head>
<body>
<div>Последний результат: <span id="result">...</span></div>
<img src="http://js.cx/clipart/select-button.gif">
<div id="animal-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<!-- значение хранится в свойстве data-value -->
<li data-value="bird">Птицы</li>
<li data-value="fish">Рыбы</li>
<li data-value="animal">Звери</li>
<li data-value="dino">Динозавры</li>
<li data-value="simplest">Одноклеточные</li>
</ol>
</div>
<div id="food-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<li data-value="carnivore">Плотоядные</li>
<li data-value="herbivore">Травоядные</li>
<li data-value="omnivore">Всеядные</li>
</ol>
</div>
<script>
// создаём два селекта
var animalSelect = new CustomSelect({
elem: $('#animal-select')
});
var foodSelect = new CustomSelect({
elem: $('#food-select')
});
// выводим выбранное значение
$([animalSelect, foodSelect]).on('select', function (event) {
$('#result').html(event.value)
});
</script>
</body>
</html>

View file

@ -0,0 +1,354 @@
# Коллбэки и события на компонентах
Компоненты, хоть и каждый сам по себе, обычно интегрированы друг с другом.
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
[cut]
## Коллбэки
Коллбэк (от англ. callback) -- это функция, которую мы передаём куда-либо и ожидаем, что она будет вызвана при наступлении события.
Например, мы можем добавить в `options` для `Menu` новый параметр -- функцию `onselect`, которая будет вызываться при выборе пункта меню:
```js
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
},
*!*
onselect: showSelected
*/!*
});
*!*
function showSelected(href) {
alert(href);
}
*/!*
```
В коде меню нужно будет вызывать её, как-то так:
```js
...
function onItemClick(e) {
e.preventDefault();
*!*
var onselect = options.onselect;
if (onselect) {
onselect(e.currentTarget.getAttribute('href').slice(1));
}
*/!*
}
...
```
Демо:
[example src="menu-callback" height="180"]
## Свои события
Оповещение через коллбэки -- это примерный аналог назначения обработчика через `onсвойство` для DOM-элементов.
Да, он работает, но чтобы можно было назначать сколько угодно обработчиков в любой момент, нужна полноценная поддержка событий, то есть аналог `addEventListener`.
Поддержка событий для компонентов будет выглядеть так:
```js
var menu = new Menu(...);
// любой код может подписаться на событие "select"
menu.on("select", function(item) {
// при выборе пункта меню сработает этот обработчик
alert("Выбран элемент " + item);
});
```
Код, который заинтересован в том, чтобы узнавать, что происходит с меню, подписывается на нужные события вызовом `menu.on(имя события, обработчик)`.
Далее `Menu` при наступлении события вызывает обработчик методом `trigger`, при необходимости передавая ему важные данные в качестве аргументов, вот так:
```js
function Menu(options) {
var self = this;
*!*
// ... при клике на item - сгенерировать событие (trigger)
function onItemClick(e) {
e.preventDefault();
self.trigger("select", e.currentTarget.getAttribute('href').slice(1));
}
*/!*
}
```
Обратим внимание -- события будут генерироваться не на элементе `<div class="menu">`, а на объекте `new Menu`. Сейчас мы разберём методы, которые для этого нужны.
## Методы on, off и trigger
Для поддержки событий в любой объект достаточно добавить три метода: `on`, `off` и `trigger`, которые перечислены ниже в свойствах `EventMixin`:
```js
var EventMixin = {
/**
* Подписка на событие
* Использование:
* menu.on('select', function(item) { ... }
*/
on: function(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Прекращение подписки
* menu.off('select', handler)
*/
off: function(eventName, handler) {
var handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for(var i=0; i<handlers.length; i++) {
if (handlers[i] == handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Генерация события с передачей данных
* this.trigger('select', item);
*/
trigger: function(eventName) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // обработчиков для события нет
}
// вызвать обработчики
var handlers = this._eventHandlers[eventName];
for (var i = 0; i < handlers.length; i++) {
handlers[i].apply(this, [].slice.call(arguments, 1));
}
}
};
```
Объект `EventMixin` -- всего лишь хранилище для функций, которые нужно скопировать в тот объект, которому нужны события.
Пример использования:
```js
//+ run
var obj = {}; // произвольный объект
*!*
// скопируем в него методы для работы с событиями
*/!*
for(var m in EventMixin) obj[m] = EventMixin[m];
*!*
// поставить обработчик на событие hello
*/!*
obj.on("hello", function(a, b, c) {
alert(a + ", " + b + ", " + c);
});
// и ещё один обработчик
obj.on("hello", function() {
alert("привет!");
});
*!*
// генерация события с дополнительными данными
// обычно происходит внутри методов объекта
*/!*
obj.trigger("hello", "data1", "data2", "data3");
```
При запуске этого кода будет поставлено два обработчика на событие `hello`, которые (по очереди) будут вызваны при `obj.trigger(...)`.
[smart header="Внутренний алгоритм `on/off/trigger`"]
Большинство фреймворков, предоставляющих события на произвольных объектах, работают примерно так же, как `on/off/trigger` в примере выше.
**Все обработчики событий `obj` хранятся в "скрытом" свойстве самого `obj`.**
<ul>
<li>Вызов `obj.on(event, handler)` создаёт свойство `obj._eventHandlers`, если его ещё нет, и сохраняет в нём обработчик: `obj._eventHandlers[event].push(handler)`.</li>
<li>Далее вызов `obj.trigger(event, ...)` получает обработчики из `obj._eventHandlers[event]` и выполняет их один за другим.</li>
<li>Вызов `obj.off(event, handler)` удаляет обработчик из `obj._eventHandlers`.</li>
</ul>
[/smart]
## Меню с событиями
Для перехода на события в меню, нужно:
<ol>
<li>Добавить методы из `EventMixin` в объект меню. Их можно скопировать либо в объект, либо в прототип.</li>
<li>Вместо вызова коллбэка `onselect` -- генерируем событие вызовом `trigger('select', значение)`.</li>
</ol>
Код:
```js
function Menu(options) {
var elem;
var self = this;
*!*
for(var method in EventMixin) {
this[method] = EventMixin[method]
}
*/!*
function onItemClick(e) {
e.preventDefault();
*!*
self.trigger("select", currentTarget.getAttribute('href').slice(1));
*/!*
}
// другие методы без изменений
}
```
Во внешнем коде:
```js
var menu = new Menu(...)
menu.on('select', function(value) {
alert('выбрано значение ' + value);
});
```
Результат:
[example src="menu-event"]
## События при помощи jQuery
Фреймворк jQuery предоставляет свои средства для генерации произвольных событий.
Метод [trigger](http://api.jquery.com/trigger/) позволяет генерировать любое событие на элементе и он же -- работает на любом объекте, "обёрнутом" в jQuery: `$(...)`.
Синтаксис для генерации события на элементе:
```js
elem.trigger(event);
```
Объект `event` должен содержать свойство `type` -- тип события (произвольный) и любые другие свойства, которые будут переданы обработчикам.
Например:
```js
$('body').on('hello', function(e) {
alert(e.user)
});
*!*
$('body').trigger({
type: 'hello', // тип события
user: 'Вася', // любые данные
});
*/!*
$('body').off('hello');
```
Есть также альтернативный синтаксис:
```js
elem.trigger(eventType, args);
```
<ul>
<li>`eventType` -- тип события (строка)</li>
<li>`args` -- массив аргументов, которые будут переданы обработчику.</li>
</ul>
Пример использования:
```js
$('body').on('hello', function(e, user) {
alert(user)
});
*!*
// аргументы - в виде массива
$('body').trigger("hello", ["Вася"]);
*/!*
$('body').off('hello');
```
Как правильно, предпочтителен первый синтаксис: `elem.trigger(event)`, поскольку он гораздо гибче, можно указывать любые свойства в любом порядке.
События, сгенерированные таким образом на элементах, всплывают и обрабатываются jQuery наравне с обычными браузерными событиями.
**Для объекта всё точно так же, как и для элемента:**
```js
//+ run
var obj = {}; // произвольный объект
$(obj).on('hello', function(e) {
alert(e.user)
});
$(obj).trigger({
type: 'hello',
user: 'Вася'
});
```
Конечно, события на объектах не всплывают, всё-таки "всплытие" -- это привилегия DOM.
Внутри jQuery делает в точности то же самое, что `EventMixin` -- вызов `on(...)` добавляет обработчик добавляются в "скрытое" свойство объекта, из которого они потом читаются и выполняются вызовом `trigger(...)`.
Это свойство можно легко увидеть:
```js
//+ run
var obj = {};
$(obj).on('hello', function(e) { /* ... */ });
// свойство называется примерно так: jQuery19105266267273109406
// оно имеет такое псевдослучайное название,
// чтобы не перезаписать "настоящие", важные свойства объекта
for(var prop in obj) alert(prop);
```
Технически, можно генерировать событие и на корневом элементе меню `<div class="menu">` и на JavaScript-объекте `new Menu` (`this` внутри конструктора). Однако, обычно используют именно объект: считается, что DOM компонента -- это его сугубо личное дело. Он может в любой момент поменяться и быть пересоздан "с нуля", если потребуется. Залезать в него снаружи и ставить какие-то обработчики никто не имеет права.
## Итого
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
<ul>
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
<li>События -- компонент генерирует их при помощи вызова `trigger` (`EventMixin`, jQuery или другой фреймворк), а внешний код ставит обработчики при помощи `on`.</li>
</ul>
Можно использовать и то и другое одновременно. Например, виджеты фреймворка jQuery UI позволяют передать при создании коллбэк `onselect`, и вместе с тем генерируют событие.
Далее, для простоты, в примерах и задачах мы будем использовать для событий методы `on/off/trigger` из jQuery. Если jQuery не нужен -- всегда можно использовать `EventMixin` или какой-либо другой фреймворк.
[libs]
event-mixin.js
[/libs]

View file

@ -0,0 +1 @@
{"name":"menu-callback","plunk":"vucl0s95kLWICLKw59gR"}

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
<script src="menu.js"></script>
</head>
<body>
<script type="text/template" id="menu-template">
<div class="menu">
<span class="title"><%-title%></span>
</div>
</script>
<script type="text/template" id="menu-list-template">
<ul>
<% for(var key in items) { %>
<li><a href="#<%-key%>"><%-items[key]%></li>
<% } %>
</ul>
</script>
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
},
onselect: showSelected
});
function showSelected(href) {
alert(href);
}
$(document.body).append(menu.getElem());
menu.open();
</script>
</body>
</html>

View file

@ -0,0 +1,59 @@
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
var elemHtml = options.template({title: options.title});
elem = $(elemHtml);
elem.on('mousedown selectstart', false);
elem.on('click', '.title', onTitleClick);
elem.on('click', 'a', onItemClick)
}
function renderItems() {
if (elem.find('ul').length) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
}
function onItemClick(e) {
e.preventDefault();
var onselect = options.onselect;
if (onselect) {
onselect(e.currentTarget.getAttribute('href').slice(1));
}
}
function onTitleClick(e) {
toggle();
}
function open() {
renderItems();
elem.addClass('open');
};
function close() {
elem.removeClass('open');
};
function toggle() {
if (elem.hasClass('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}

View file

@ -0,0 +1,20 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(http://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(http://js.cx/clipart/arrow-down.png);
}

View file

@ -0,0 +1 @@
{"name":"menu-event","plunk":"JHiJOzvcuurjetQqUvDS"}

View file

@ -0,0 +1,48 @@
var EventMixin = {
/**
* Подписка на событие
* Использование:
* menu.on('select', function(item) { ... }
*/
on: function(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Прекращение подписки
* menu.off('select', handler)
*/
off: function(eventName, handler) {
var handlers = this._eventHandlers && this._eventHandlers[eventName];
if (!handlers) return;
for(var i=0; i<handlers.length; i++) {
if (handlers[i] == handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Генерация события с передачей данных
* this.trigger('select', item);
*/
trigger: function(eventName) {
if (!this._eventHandlers || !this._eventHandlers[eventName]) {
return; // обработчиков для события нет
}
// вызвать обработчики
var handlers = this._eventHandlers[eventName];
for (var i = 0; i < handlers.length; i++) {
handlers[i].apply(this, [].slice.call(arguments, 1));
}
}
};

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
<script src="eventMixin.js"></script>
<script src="menu.js"></script>
</head>
<body>
<script type="text/template" id="menu-template">
<div class="menu">
<span class="title"><%-title%></span>
</div>
</script>
<script type="text/template" id="menu-list-template">
<ul>
<% for(var key in items) { %>
<li><a href="#<%-key%>"><%-items[key]%></li>
<% } %>
</ul>
</script>
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
}
});
$(document.body).append(menu.getElem());
menu.open();
menu.on('select', function(value) {
alert('выбрано значение ' + value);
});
</script>
</body>
</html>

View file

@ -0,0 +1,61 @@
function Menu(options) {
var elem;
var self = this;
for(var method in EventMixin) {
this[method] = EventMixin[method]
}
function getElem() {
if (!elem) render();
return elem;
}
function render() {
var elemHtml = options.template({title: options.title});
elem = $(elemHtml);
elem.on('mousedown selectstart', false);
elem.on('click', '.title', onTitleClick);
elem.on('click', 'a', onItemClick)
}
function renderItems() {
if (elem.find('ul').length) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
}
function onItemClick(e) {
e.preventDefault();
self.trigger('select', e.currentTarget.getAttribute('href').slice(1));
}
function onTitleClick(e) {
toggle();
}
function open() {
renderItems();
elem.addClass('open');
};
function close() {
elem.removeClass('open');
};
function toggle() {
if (elem.hasClass('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}

View file

@ -0,0 +1,20 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(http://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(http://js.cx/clipart/arrow-down.png);
}