renovations

This commit is contained in:
Ilya Kantor 2015-02-21 00:59:02 +03:00
parent 24171550ae
commit a62682e188
49 changed files with 620 additions and 894 deletions

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="723px" height="562px" viewBox="0 0 723 562" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
<!-- Generator: bin/sketchtool 1.3 (252) - http://www.bohemiancoding.com/sketch -->
<title>code-style.svg</title>
<desc>Created with Sketch.</desc>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="code-style.svg" sketch:type="MSArtboardGroup">

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Before After
Before After

View file

@ -2,8 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<style>
table {
border-collapse: collapse;
@ -44,14 +43,13 @@ var users = [
{name: "Даша", age: 30}
];
$('#grid-holder').html(
_.template( $('#grid-template').html().trim(), {list: users})
);
var tmpl = document.getElementById('grid-template').innerHTML.trim();
tmpl = _.template(tmpl);
document.getElementById('grid-holder').innerHTML = tmpl({list: users});
</script>
</body>
</html>

View file

@ -2,8 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
</head>
<body>

View file

@ -18,7 +18,7 @@ var users = [
Результат:
[iframe src="solution"]
[iframe src="solution" height=180]

View file

@ -1,2 +1,5 @@
[edit src="solution"]Решение[/edit]
В решении обратим внимание:
<ul>
<li>Чтобы ссылка `href` была корректной, даже если в ключах `items` попались русские символы и пробелы -- используется функция [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent).</li>
<li>Для вывода `href` при клике на ссылку используется делегирование. Причём обработчик не сам выводит `href`, а лишь разбирается в произошедшем и вызывает функцию `select`, которая представляет действие "выбора" элемента меню. В последующих примерах эта функция станет сложнее.</li>
</ul>

View file

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
@ -15,10 +15,15 @@
</div>
</script>
<!--
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
например русские буквы и пробелы
в этом примере русских букв в ключах items нет, но потенциально они возможны
-->
<script type="text/template" id="menu-list-template">
<ul>
<% for(var key in items) { %>
<li><a href="#<%-key%>"><%-items[key]%></li>
<% for(var name in items) { %>
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
<% } %>
</ul>
</script>
@ -26,19 +31,17 @@
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
cake: "Торт", // cake <a href="#cake">Торт</a>
donut: "Пончик", // donut
chokolate: "Шоколадка" // chokolate
}
});
$(document.body).append(menu.getElem());
menu.open();
document.body.appendChild(menu.getElem());
</script>
</body>
</html>
</html>

View file

@ -7,14 +7,18 @@
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(https://js.cx/clipart/arrow-down.png);
.menu.open .title:before {
content: '▼';
}

View file

@ -7,44 +7,51 @@ function Menu(options) {
}
function render() {
var elemHtml = options.template({title: options.title});
var html = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.on('click', 'a', onItemClick)
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
if (event.target.closest('a')) {
event.preventDefault();
select(event.target.closest('a'));
}
}
}
function renderItems() {
if (elem.find('ul').length) return;
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function onItemClick(e) {
alert(e.currentTarget.getAttribute('href').slice(1));
e.preventDefault();
}
function onTitleClick(e) {
toggle();
function select(link) {
alert(link.getAttribute('href').slice(1));
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};
@ -52,4 +59,4 @@ function Menu(options) {
this.toggle = toggle;
this.close = close;
this.open = open;
}
}

View file

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
@ -26,18 +26,17 @@
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
items: [
"Торт", // cake <a href="#cake">Торт</a>
"Пончик", // donut
"Пирожное", // cake
"Шоколадка", // chockolate
"Шоколадка" // chokolate
]
});
$(document.body).append(menu.getElem());
document.body.appendChild(menu.getElem());
</script>
</body>
</html>
</html>

View file

@ -7,14 +7,18 @@
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(https://js.cx/clipart/arrow-down.png);
.menu.open .title:before {
content: '▼';
}

View file

@ -7,37 +7,41 @@ function Menu(options) {
}
function render() {
var elemHtml = options.template({title: options.title});
var html = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
}
function renderItems() {
if (elem.find('ul').length) return;
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
}
function onTitleClick(e) {
toggle();
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};
@ -45,4 +49,4 @@ function Menu(options) {
this.toggle = toggle;
this.close = close;
this.open = open;
}
}

View file

@ -2,13 +2,16 @@
[importance 5]
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы вместо массива `items` оно принимало *объект* `items`, вот так:
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы оно выводило не просто список, а список ссылок.
<ol>
<li>Вместо массива `items` меню должно принимать *объект* `items`, вот так:
```js
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template(document.getElementById('menu-template').innerHTML),
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML),
*!*
items: {
"donut": "Пончик",
@ -18,11 +21,10 @@ var menu = new Menu({
*/!*
});
```
</ol>
<li>Вывод в шаблоне пусть будет не просто `<li>Пончик</li>`, а через ссылку: `<a href="#donut">Пончик</a>`. При клике на ссылку должно выводиться название из её `href`.</li>
</ol>
Вывод в шаблоне пусть будет не просто `<li>Пончик</li>`, а через ссылку: `<a href="#donut">Пончик</a>`.
При клике на ссылку должно выводиться название из её `href`. Демо:
Демо:
[iframe src="solution" height="130" border="1"]
[edit src="source"]Исходное меню[/edit]

View file

@ -4,27 +4,9 @@
[cut]
Все виджеты можно условно разделить на три группы по генерации DOM:
<ol>
<li>**Получают готовый HTML/DOM и "оживляют" его.**
Большинство виджетов, которые мы видели ранее, получают готовый HTML/DOM и "оживляют" его. Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.
Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.</li>
<li>**Создают DOM для компоненты самостоятельно.**
В ряде случаев даже на простых страницах нужно генерировать DOM динамически. Например, виджет "подсказка" при наведении на элемент создаёт красивую подсказку и показывает её. Элемент генерируется в JavaScript-коде.
В более сложных интерфейсах компоненты генерируют свой DOM на основе данных, полученных с сервера или из других источников.</li>
<li>**=1+2: должны уметь как "оживить" уже готовый DOM, так и создать свой.**
Бывает и так, что виджет должен уметь и то и другое. Например, так работает сервис сообщений Twitter.
При начальной загрузки сервер генерирует HTML с текущими сообщениями, а JavaScript лишь добавляет им "живости". Далее, когда страница уже загружена, он получает с сервера данные для новых сообщений и генерирует HTML уже динамически.
</li>
</ol>
С первой группой виджетов -- вопросов нет. Добавляем обработчики, и дело сделано.
Здесь нам интересна вторая группа. Третья -- это по существу оптимизация с едиными шаблонами на клиенте и сервере, рассмотрим её позже.
Но в сложных интерфейсах разметка изначально отсутствует на странице. Компоненты генерируют свой DOM сами, динамически, на основе данных, полученных с сервера или из других источников.
## Зачем нужны шаблоны?
@ -105,7 +87,7 @@ function Menu(options) {
// сгенерировать HTML, используя шаблон tmpl (см. выше)
// с данными title и items
*/!*
var html = _.template(tmpl, {
var html = _.template(tmpl)({
title: "Сладости",
items: [
"Торт",
@ -163,13 +145,11 @@ var html = _.template(tmpl, {
<dl>
<dt>`tmpl`</dt>
<dd>Шаблон.</dd>
<dt>`data`</dt>
<dd>Объект с данными.</dd>
<dt>`options`</dt>
<dd>Необязательные настройки, например можно поменять разделители.</dd>
</dl>
Эта функция запускает "сборку" шаблона `tmpl` с объектом `data` и возвращает результат в виде строки.
Эта функция запускает "компиляцию" шаблона `tmpl` и возвращает результат в виде функции, которую далее можно запустить с данными и получить строку-результат.
Вот так:
@ -178,7 +158,7 @@ var html = _.template(tmpl, {
*!*
// Шаблон
*/!*
var tmpl = '<span class="title"><%=title%></span>';
var tmpl = _.template('<span class="title"><%=title%></span>');
*!*
// Данные
@ -187,15 +167,13 @@ var data = {
title: "Заголовок"
};
var result = _.template(tmpl, data);
*!*
// Результат подстановки
*/!*
alert(result); // <span class="title">Заголовок</span>
alert( tmpl(data) ); // <span class="title">Заголовок</span>
```
Пример выше похож на операцию "поиск-и-замена": функция `_.template` просто заменила `<%=title%>` в шаблоне `tmpl` на значение свойства `data.title`.
Пример выше похож на операцию "поиск-и-замена": шаблон просто заменил `<%=title%>` на значение свойства `data.title`.
Но возможность вставки JS-кода делает шаблоны сильно мощнее.
@ -209,16 +187,16 @@ var tmpl = '<ul>\
<li><%=i%></li> \
<% } %>\
</ul>';
alert( _.template(tmpl, {count: 5}) );
alert( _.template(tmpl)({count: 5}) ); // <ul><li>1</li><li>2</li>...</ul>
```
Здесь в результат попал сначала текст `<ul>`, потом выполнился код `for`, который последовательно сгенерировал элементы списка, и затем список был закрыт `</ul>`.
## Хранение шаблона в документе
Шаблон -- это многострочный HTML-текст. Объявлять его в скрипте, как сделано выше -- неудобно и некрасиво.
Шаблон -- это многострочный HTML-текст. Записывать его прямо в скрипте -- неудобно.
Один из способов объявления шаблона -- записать его в HTML, в тег <code>&lt;script&gt;</code> с нестандартным `type`, например `"text/template"`:
Один из альтернативных способов объявления шаблона -- записать его в HTML, в тег <code>&lt;script&gt;</code> с нестандартным `type`, например `"text/template"`:
```html
<script type="*!*text/template*/!*" id="menu-template">
@ -253,11 +231,11 @@ var template = document.getElementById('menu-template').innerHTML;
</script>
<script>
var tmpl = document.getElementById('list-template').innerHTML;
var tmpl = _.template(document.getElementById('list-template').innerHTML);
*!*
// ..а вот и результат
var result = _.template(tmpl, {count: 5});
var result = tmpl({count: 5});
document.write( result );
*/!*
</script>
@ -271,15 +249,9 @@ var template = document.getElementById('menu-template').innerHTML;
Оказывается, очень просто.
Вызов `_.template(tmpl, data)` выполняется в два этапа:
<ol>
<li>Разбивает строку `tmpl` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию, которая в специальную переменную-буфер записывает текст из шаблона и выполняет код.</li>
<li>Запускает эту функцию с данными `data`, так что она уже генерирует результат.</li>
</ol>
Вызов `_.template(str)` разбивает строку `str` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию. Тело этой функции создаётся таким образом, что код, который в шаблоне оформлен как `<% ... %>` -- попадает в неё "как есть", а переменные и текст прибавляются к специальному временному "буферу", который в итоге возвращается.
Эти два процесса можно разделить. Функцию из строки-шаблона можно получить в явном виде вызовом `_.template(tmpl)`, без второго аргумента.
Пример:
Взглянем на пример:
```js
//+ run
@ -303,7 +275,7 @@ function(obj) {
}
```
Она является результатом вызова `new Function("obj", "код")`, где код динамическим образом генерируется внутри `_.template` на основе шаблона:
Она является результатом вызова `new Function("obj", "код")`, где `код` динамическим образом генерируется на основе шаблона:
<ol>
<li>Вначале в коде идёт "шапка" -- стандартное начало функции, в котором объявляется переменная `__p`. В неё будет записываться результат.</li>
<li>Затем добавляется блок `with(obj) { ... }`, внутри которого в `__p` добавляются фрагменты HTML из шаблона, а также переменные из выражений `<%=...%>`. Код из `<%...%>` копируется в функцию "как есть".</li>
@ -320,13 +292,13 @@ function(obj) {
<li>Она работает в глобальной области видимости, не имеет доступа к внешним локальным переменным.</li>
<li>Внешний `use strict` на такую функцию не влияет, то есть даже в строгом режиме шаблон продолжит работать.</li>
</ul>
Если мы всё же не хотим использовать `with` -- нужно поставить третий параметр -- `options`, указав параметр `variable` (название переменной с данными).
Если мы всё же не хотим использовать `with` -- нужно поставить второй параметр -- `options`, указав параметр `variable` (название переменной с данными).
Например:
```js
//+ run
alert( _.template("<h1><%=menu.title%></h1>", null, {variable: "menu"}) );
alert( _.template("<h1><%=menu.title%></h1>", {variable: "menu"}) );
```
Результат:
@ -335,7 +307,7 @@ alert( _.template("<h1><%=menu.title%></h1>", null, {variable: "menu"}) );
*!*
function(*!*menu*/!*) {
*/!*
var __t, __p = '', __e = _.escape;
var __t, __p = '';
__p += '<h1>' +
((__t = (menu.title)) == null ? '' : __t) +
'</h1>';
@ -528,10 +500,9 @@ function(obj) {
</script>
<script>
var tmpl = document.getElementById('menu-template').innerHTML;
var tmpl = _.template(document.getElementById('menu-template').innerHTML);
var compiled = _.template(tmpl);
var result = compiled({ title: "Заголовок" });
var result = tmpl({ title: "Заголовок" });
document.write(result);
</script>
@ -553,7 +524,7 @@ function(obj) {
```js
...
var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
var compiled = _.template(tmpl, {sourceURL: '/template/menu-template'});
...
```
@ -571,10 +542,32 @@ var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
Шаблоны полезны для того, чтобы отделить HTML от кода. Это упрощает разработку и поддержку.
В этой главе подробно разобрана система шаблонизации из библиотеки [LoDash](https://lodash.com).
В этой главе подробно разобрана система шаблонизации из библиотеки [LoDash](https://lodash.com):
Теперь, когда мы с ней знакомы, мы можем как использовать её в своих проектах, так и перейти к более глобальному рассмотрению подходов к шаблонизации.
<ul>
<li>Шаблон -- это строка со специальными вставками кода `<% ... %>` или переменных `<%- expr ->`, `<%= expr ->`.</li>
<li>Вызов `_.template(tmpl)` превращает шаблон `tmpl` в функцию, которой в дальнейшем передаются данные --
и она генерирует HTML с ними. Можно вызвать передать данные сразу в `_.template` вторым аргументом.</li>
</ul>
В этой главе мы рассмотрели хранение шаблонов в документе, при помощи `<script>` с нестандартным `type`. Конечно, есть и другие способы, можно хранить шаблоны и в отдельном файле, если шаблонная система или система сборки проектов это позволяют.
Шаблонных систем много. Многие основаны на схожем принципе -- генерации функции из строки, например:
<ul>
<li>[EJS](http://www.embeddedjs.com/)</li>
<li>[Jade](http://jade-lang.com/)</li>
<li>[Handlebars](http://handlebarsjs.com/)</li>
</ul>
Есть и альтернативный подход -- шаблонная система получает "образец" DOM-узела и клонирует его вызовом `cloneNode(true)`, каждый раз изменяя что-то внутри. В отличие от подхода, описанного выше, это будет работать не с произвольной строкой текста, а только и именно с DOM-узлами. Но в некоторых ситуациях у него есть преимущество.
Такой подход используется во фреймворках:
<ul>
<li>[AngularJS](http://angularjs.org)</li>
<li>[Knockout.JS](http://knockoutjs.com/)</li>
</ul>
[libs]
lodash.js
lodash
[/libs]

View file

@ -16,10 +16,12 @@
</script>
<script>
var tmpl = document.getElementById('menu-template').innerHTML;
var tmpl = _.template(
document.getElementById('menu-template').innerHTML,
{sourceURL: '/template/menu-template'}
);
var compiled = _.template(tmpl, null, {sourceURL: '/template/menu-template'});
var result = compiled({ title: "Заголовок" });
var result = tmpl({ title: "Заголовок" });
document.write(result);
</script>

View file

@ -1,94 +0,0 @@
<!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

@ -2,7 +2,6 @@
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.voter {
font-family: Consolas, "Lucida Console", monospace;
@ -14,6 +13,8 @@
font-weight: bold;
}
</style>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<script src="voter.js"></script>
</head>
<body>
@ -24,67 +25,14 @@
</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
elem: document.getElementById('voter')
});
$(voter).on('change', function(e) {
alert(e.value);
voter.setVote(5);
document.getElementById('voter').addEventListener('change', function(e) {
alert(e.detail);
});
</script>

View file

@ -0,0 +1,36 @@
function Voter(options) {
var elem = options.elem;
var voteElem = elem.querySelector('.vote');
elem.onclick = function(event) {
// сам обработчик не меняет голос, он вызывает функцию
if (event.target.closest('.down')) {
voteDecrease();
} else if (event.target.closest('.up')) {
voteIncrease();
}
}
elem.onmousedown = function() {
return false;
};
// ----------- методы -------------
function voteDecrease() {
setVote(+voteElem.innerHTML - 1);
}
function voteIncrease() {
setVote(+voteElem.innerHTML + 1);
}
function setVote(vote) {
voteElem.innerHTML = +vote;
var widgetEvent = new CustomEvent("change", { bubbles: true, detail: +vote });
elem.dispatchEvent(widgetEvent);
};
this.setVote = setVote;
}

View file

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

View file

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

View file

@ -2,22 +2,18 @@
<html>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
.selected {
background: #0f0;
}
li {
cursor: pointer;
}
</style>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<script src="listSelect.js"></script>
</head>
<body>
Клик на элементе выделяет только его.<br>
Shift+Клик добавляет/убирает элемент из выделенных.<br>
Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.<br>
Shift+Клик добавляет промежуток от последнего кликнутого к выделению.<br>
<ul>
<ul id="heroes">
<li>Кристофер Робин</li>
<li>Винни-Пух</li>
<li>Ослик Иа</li>
<li>Мудрая Сова</li>
@ -25,55 +21,16 @@ Shift+Клик добавляет/убирает элемент из выдел
</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')
var listSelect = new ListSelect({
elem: document.querySelector('#heroes')
});
$(select).on('change', function(e) {
alert(e.value);
document.querySelector('#heroes').addEventListener('select', function(event) {
alert(event.value);
});
</script>
</body>
</html>

View file

@ -0,0 +1,81 @@
function ListSelect(options) {
var elem = options.elem;
var lastClickedLi = null;
elem.onmousedown = function() {
return false;
};
elem.onclick = function(e) {
var li = e.target.closest('li');
if (!li) return;
if(e.metaKey || e.ctrlKey) { // для Mac проверяем Cmd, т.к. Ctrl + click там контекстное меню
toggleSelect(li);
} else if (e.shiftKey) {
selectFromLast(li);
} else {
selectSingle(li);
}
dispatchEvent();
lastClickedLi = li;
}
function deselectAll() {
[].forEach.call(elem.children, function(child) {
child.classList.remove('selected')
});
}
function toggleSelect(li) {
li.classList.toggle('selected');
}
function selectSingle(li) {
deselectAll();
li.classList.add('selected');
}
function selectFromLast(target) {
var startElem = lastClickedLi || elem.children[0];
target.classList.add('selected');
if (startElem == target) {
// клик на том же элементе, что и раньше
// это особый случай
return;
}
var isLastClickedBefore = startElem.compareDocumentPosition(target) & 4;
if (isLastClickedBefore) {
for(var elem = startElem; elem != target; elem = elem.nextElementSibling) {
elem.classList.add('selected');
}
} else {
for(var elem = startElem; elem != target; elem = elem.previousElementSibling) {
elem.classList.add('selected');
}
}
}
function dispatchEvent() {
var widgetEvent = new CustomEvent("select", {
bubbles: true,
detail: getSelected()
});
elem.dispatchEvent(widgetEvent);
}
function getSelected() {
return [].map.call(elem.querySelectorAll('.selected'), function(li) {
return li.innerHTML;
});
};
this.getSelected = getSelected;
}

View file

@ -0,0 +1,7 @@
.selected {
background: #0f0;
}
li {
cursor: pointer;
}

View file

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

View file

@ -4,30 +4,32 @@
display: inline-block;
}
.customselect-title {
height: 17px;
border: 1px solid #ADD8E6;
background-position: right;
background-image: url(https://js.cx/clipart/select-button.gif);
background-repeat: no-repeat;
.customselect .title {
height: 20px;
border: 2px groove #ADD8E6;
background: white;
width: 200px;
box-sizing: border-box;
padding: 2px;
line-height: 14px;
cursor: pointer;
text-align: left;
}
.customselect li {
padding: 2px;
cursor: pointer;
}
.customselect-options li {
padding: 2px;
cursor: pointer;
}
.customselect-options li:nth-child(even) {
.customselect li:nth-child(even) {
background-color: #f0f8ff;
}
.customselect-options li:hover {
.customselect li:hover {
background-color: #7fffd4;
}
.customselect-options {
.customselect ul {
list-style: none;
margin: 0;
padding: 0;
@ -42,6 +44,7 @@
border-right: 1px solid #add8e6;
box-sizing: border-box;
}
.customselect-open .customselect-options {
.customselect.open ul {
display: block;
}

View file

@ -1,44 +1,39 @@
function CustomSelect(options) {
var self = this;
var elem = options.elem;
elem.on('click', '.customselect-title', onTitleClick);
elem.on('click', 'li', onOptionClick);
elem.onclick = function(event) {
if (event.target.className == 'title') {
toggle();
} else if (event.target.tagName == 'LI') {
setValue(event.target.innerHTML, event.target.dataset.value);
close();
}
}
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);
if (!elem.contains(event.target)) close();
}
// ------------------------
function setValue(name, value) {
elem.find('.customselect-title').html(name);
function setValue(title, value) {
elem.querySelector('.title').innerHTML = title;
$(self).triggerHandler({
type: 'select',
name: name,
value: value
var widgetEvent = new CustomEvent('select', {
bubbles: true,
detail: {
title: title,
value: value
}
});
elem.dispatchEvent(widgetEvent);
}
function toggle() {
@ -47,14 +42,14 @@ function CustomSelect(options) {
}
function open() {
elem.addClass('customselect-open');
$(document).on('click', onDocumentClick);
elem.classList.add('open');
document.addEventListener('click', onDocumentClick);
isOpen = true;
}
function close() {
elem.removeClass('customselect-open');
$(document).off('click', onDocumentClick);
elem.classList.remove('open');
document.removeEventListener('click', onDocumentClick);
isOpen = false;
}

View file

@ -2,7 +2,7 @@
<html>
<head>
<title>Селект</title>
<script src='http://code.jquery.com/jquery.min.js'></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<link rel="stylesheet" href="customselect.css"/>
<script src="customselect.js"></script>
</head>
@ -11,38 +11,38 @@
<div>Последний результат: <span id="result">...</span></div>
<div id="animal-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<button class="title">Выберите</button>
<ul>
<!-- значение хранится в свойстве 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>
</ul>
</div>
<div id="food-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<button class="title">Выберите</button>
<ul>
<li data-value="carnivore">Плотоядные</li>
<li data-value="herbivore">Травоядные</li>
<li data-value="omnivore">Всеядные</li>
</ol>
</ul>
</div>
<script>
var animalSelect = new CustomSelect({
elem: $('#animal-select')
elem: document.getElementById('animal-select')
});
var foodSelect = new CustomSelect({
elem: $('#food-select')
elem: document.getElementById('food-select')
});
$([animalSelect, foodSelect]).on('select', function (event) {
$('#result').html(event.value)
document.addEventListener('select', function (event) {
document.getElementById('result').innerHTML = event.detail.value;
});
</script>

View file

@ -0,0 +1,50 @@
.customselect {
width: 200px;
font-size: 14px;
display: inline-block;
}
.customselect .title {
height: 20px;
border: 2px groove #ADD8E6;
background: white;
width: 200px;
box-sizing: border-box;
padding: 2px;
line-height: 14px;
cursor: pointer;
text-align: left;
}
.customselect li {
padding: 2px;
cursor: pointer;
}
.customselect li:nth-child(even) {
background-color: #f0f8ff;
}
.customselect li:hover {
background-color: #7fffd4;
}
.customselect ul {
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 ul {
display: block;
}

View file

@ -0,0 +1 @@
// ваш код CustomSelect

View file

@ -2,54 +2,51 @@
<html>
<head>
<title>Селект</title>
<script src='http://code.jquery.com/jquery.min.js'></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<link rel="stylesheet" href="customselect.css"/>
<!-- для вашего кода -->
<script src="customselect.js"></script>
</head>
<body>
<div>Последний результат: <span id="result">...</span></div>
<img src="https://js.cx/clipart/select-button.gif">
<div id="animal-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<button class="title">Выберите</button>
<ul>
<!-- значение хранится в свойстве 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>
</ul>
</div>
<div id="food-select" class="customselect">
<div class="customselect-title">Выберите</div>
<ol class="customselect-options">
<button class="title">Выберите</button>
<ul>
<li data-value="carnivore">Плотоядные</li>
<li data-value="herbivore">Травоядные</li>
<li data-value="omnivore">Всеядные</li>
</ol>
</ul>
</div>
<script>
// создаём два селекта
var animalSelect = new CustomSelect({
elem: $('#animal-select')
elem: document.getElementById('animal-select')
});
var foodSelect = new CustomSelect({
elem: $('#food-select')
elem: document.getElementById('food-select')
});
// выводим выбранное значение
$([animalSelect, foodSelect]).on('select', function (event) {
$('#result').html(event.value)
document.addEventListener('select', function (event) {
document.getElementById('result').innerHTML = event.detail.value;
});
</script>
</body>
</html>
</html>

View file

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

View file

@ -1,6 +1,6 @@
# Коллбэки и события на компонентах
Компоненты, хоть и каждый сам по себе, обычно интегрированы друг с другом.
Компоненты, хоть и каждый сам по себе, обычно как-то общаются с остальной частью страницы
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
@ -15,8 +15,8 @@
```js
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template(document.getElementById('menu-template').innerHTML),
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML,
items: {
"donut": "Пончик",
"cake": "Пирожное",
@ -34,321 +34,84 @@ function showSelected(href) {
*/!*
```
В коде меню нужно будет вызывать её, как-то так:
В коде меню нужно будет вызывать её, например так:
```js
...
function onItemClick(e) {
e.preventDefault();
*!*
var onselect = options.onselect;
if (onselect) {
onselect(e.currentTarget.getAttribute('href').slice(1));
}
*/!*
function select(link) {
options.onselect(link.getAttribute('href').slice(1));
...
}
...
```
Демо:
Полный пример:
[codetabs src="menu-callback" height="180"]
## Свои события
Оповещение через коллбэки -- это примерный аналог назначения обработчика через `onсвойство` для DOM-элементов.
Как мы уже знаем, в современных браузерах DOM-элементы могут [генерировать поизвольные события](/dispatch-events) при помощи встроенных методов, а в IE8- это возможно с использованием фреймворка, к примеру, jQuery.
Да, он работает, но чтобы можно было назначать сколько угодно обработчиков в любой момент, нужна полноценная поддержка событий, то есть аналог `addEventListener`.
Воспользуемся ими, чтобы корневой элемент меню генерировал событие, которое мы назовём `select`, при выборе элемента, и передавал в объект события выбранное значение.
Поддержка событий для компонентов будет выглядеть так:
Для этого модифицируем функцию `select`:
```js
function Menu(options) {
...
function select(link) {
*!*
var widgetEvent = new CustomEvent("select", {
bubbles: true,
// detail - стандартное свойство CustomEvent для произвольных данных
detail: link.getAttribute('href').slice(1)
});
elem.dispatchEvent(widgetEvent);
*/!*
}
...
}
```
Код, который заинтересован в том, чтобы узнавать, что выбрано в меню, подписывается на событие `select` его корневого элемента:
```js
var menu = new Menu(...);
// любой код может подписаться на событие "select"
menu.on("select", function(item) {
// при выборе пункта меню сработает этот обработчик
alert("Выбран элемент " + item);
var elem = menu.getElem();
elem.addEventListener('select', function(event) {
alert(event.detail);
});
```
Код, который заинтересован в том, чтобы узнавать, что происходит с меню, подписывается на нужные события вызовом `menu.on(имя события, обработчик)`.
Вместо `detail` можно было бы выбрать и другое название свойства, но тогда нужно позаботиться о том, чтобы оно не конфликтовало со стандартными. Кроме того, в конструкторе `CustomEvent` разрешено только `detail`, другое свойство понадобилось бы присваивать в отдельной строке.
Далее `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);
});
```
Результат:
[codetabs src="menu-event"]
## События при помощи jQuery
[warn header="Внимание, инкапсуляция!"]
Очень важно, что внешний код ставит обработчик на корневой элемент, но не на внутренние элементы меню.
Фреймворк 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 компонента -- это его сугубо личное дело. Он может в любой момент поменяться и быть пересоздан "с нуля", если потребуется. Залезать в него снаружи и ставить какие-то обработчики никто не имеет права.
Такое правило позволяет нам не опасаться проблем при оптимизации, расширении и даже полной переделке DOM-структуры меню. Коль скоро события и методы сохраняются, внешний код будет работать как прежде.
Ещё раз -- внешний код не имеет права залезать внутрь DOM-структуры меню, ставить там обработчики и так далее.
[/warn]
## Итого
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
<ul>
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
<li>События -- компонент генерирует их при помощи вызова `trigger` (`EventMixin`, jQuery или другой фреймворк), а внешний код ставит обработчики при помощи `on`.</li>
<li>События -- компонент генерирует их на корневом элементе при помощи `dispatchEvent`, а внешний код ставит обработчики при помощи `addEventListener`. Такие события всплывают, если указан флаг `bubbles`, поэтому можно использовать с ними делегирование.</li>
</ul>
Можно использовать и то и другое одновременно. Например, виджеты фреймворка jQuery UI позволяют передать при создании коллбэк `onselect`, и вместе с тем генерируют событие.
Далее, для простоты, в примерах и задачах мы будем использовать для событий методы `on/off/trigger` из jQuery. Если jQuery не нужен -- всегда можно использовать `EventMixin` или какой-либо другой фреймворк.
[libs]
event-mixin.js
[/libs]

View file

@ -2,9 +2,9 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<link rel="stylesheet" href="menu.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
@ -15,10 +15,15 @@
</div>
</script>
<!--
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
например русские буквы и пробелы
в этом примере русских букв в ключах items нет, но потенциально они возможны
-->
<script type="text/template" id="menu-list-template">
<ul>
<% for(var key in items) { %>
<li><a href="#<%-key%>"><%-items[key]%></li>
<% for(var name in items) { %>
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
<% } %>
</ul>
</script>
@ -26,22 +31,21 @@
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
cake: "Торт", // cake <a href="#cake">Торт</a>
donut: "Пончик", // donut
chokolate: "Шоколадка" // chokolate
},
onselect: showSelected
});
function showSelected(href) {
alert(href);
function showSelected(itemName) {
alert(itemName);
}
$(document.body).append(menu.getElem());
menu.open();
document.body.appendChild(menu.getElem());
</script>
</body>

View file

@ -0,0 +1,24 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}

View file

@ -7,48 +7,51 @@ function Menu(options) {
}
function render() {
var elemHtml = options.template({title: options.title});
var html = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.on('click', 'a', onItemClick)
}
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
if (event.target.closest('a')) {
event.preventDefault();
select(event.target.closest('a'));
}
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 renderItems() {
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function select(link) {
options.onselect(link.getAttribute('href').slice(1));
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};
@ -56,4 +59,4 @@ function Menu(options) {
this.toggle = toggle;
this.close = close;
this.open = open;
}
}

View file

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

View file

@ -1,48 +0,0 @@
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

@ -2,10 +2,9 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<link rel="stylesheet" href="menu.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="eventMixin.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
@ -18,8 +17,8 @@
<script type="text/template" id="menu-list-template">
<ul>
<% for(var key in items) { %>
<li><a href="#<%-key%>"><%-items[key]%></li>
<% for(var name in items) { %>
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
<% } %>
</ul>
</script>
@ -27,21 +26,21 @@
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
cake: "Торт", // cake <a href="#cake">Торт</a>
donut: "Пончик", // donut
chokolate: "Шоколадка" // chokolate
}
});
$(document.body).append(menu.getElem());
menu.open();
menu.on('select', function(value) {
alert('выбрано значение ' + value);
var elem = menu.getElem();
document.body.appendChild(elem);
elem.addEventListener('select', function(event) {
alert(event.detail);
});
</script>
</body>

View file

@ -0,0 +1,24 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}

View file

@ -1,10 +1,5 @@
function Menu(options) {
var elem;
var self = this;
for(var method in EventMixin) {
this[method] = EventMixin[method]
}
function getElem() {
if (!elem) render();
@ -12,45 +7,55 @@ function Menu(options) {
}
function render() {
var elemHtml = options.template({title: options.title});
var html = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.on('click', 'a', onItemClick)
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
if (event.target.closest('a')) {
event.preventDefault();
select(event.target.closest('a'));
}
}
}
function renderItems() {
if (elem.find('ul').length) return;
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function onItemClick(e) {
e.preventDefault();
self.trigger('select', e.currentTarget.getAttribute('href').slice(1));
}
function onTitleClick(e) {
toggle();
function select(link) {
var widgetEvent = new CustomEvent("select", {
bubbles: true,
detail: link.getAttribute('href').slice(1)
});
elem.dispatchEvent(widgetEvent);
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};

View file

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

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="dateselector.js"></script>
</head>
<body>

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="dateselector.js"></script>
</head>
<body>

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<style>
html, body {
padding: 0;

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<link type="text/css" rel="stylesheet" href="window.css" />
<style>
html, body {

View file

@ -7,7 +7,7 @@
<link type="text/css" rel="stylesheet" href="datepicker.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="calendar.js"></script>
<script src="datepicker.js"></script>

View file

@ -7,7 +7,7 @@
<link type="text/css" rel="stylesheet" href="datepicker.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="calendar.js"></script>
<script src="datepicker.js"></script>

View file

@ -5,7 +5,7 @@
<link href="tree.css" rel="stylesheet">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"></script>
<script src="regions.js"></script>
<script src="tree.js"></script>
</head>