renovations
This commit is contained in:
parent
24171550ae
commit
a62682e188
49 changed files with 620 additions and 894 deletions
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?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">
|
<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>
|
<title>code-style.svg</title>
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with bin/sketchtool.</desc>
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
<g id="combined" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||||
<g id="code-style.svg" sketch:type="MSArtboardGroup">
|
<g id="code-style.svg" sketch:type="MSArtboardGroup">
|
||||||
|
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
@ -2,8 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<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://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
@ -44,14 +43,13 @@ var users = [
|
||||||
{name: "Даша", age: 30}
|
{name: "Даша", age: 30}
|
||||||
];
|
];
|
||||||
|
|
||||||
$('#grid-holder').html(
|
var tmpl = document.getElementById('grid-template').innerHTML.trim();
|
||||||
_.template( $('#grid-template').html().trim(), {list: users})
|
tmpl = _.template(tmpl);
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('grid-holder').innerHTML = tmpl({list: users});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<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://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ var users = [
|
||||||
|
|
||||||
Результат:
|
Результат:
|
||||||
|
|
||||||
[iframe src="solution"]
|
[iframe src="solution" height=180]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="menu.css">
|
<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://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>
|
<script src="menu.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -15,10 +15,15 @@
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
|
||||||
|
например русские буквы и пробелы
|
||||||
|
в этом примере русских букв в ключах items нет, но потенциально они возможны
|
||||||
|
-->
|
||||||
<script type="text/template" id="menu-list-template">
|
<script type="text/template" id="menu-list-template">
|
||||||
<ul>
|
<ul>
|
||||||
<% for(var key in items) { %>
|
<% for(var name in items) { %>
|
||||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</script>
|
</script>
|
||||||
|
@ -26,19 +31,17 @@
|
||||||
<script>
|
<script>
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||||
items: {
|
items: {
|
||||||
"donut": "Пончик",
|
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||||
"cake": "Пирожное",
|
donut: "Пончик", // donut
|
||||||
"chocolate": "Шоколадка"
|
chokolate: "Шоколадка" // chokolate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.body).append(menu.getElem());
|
document.body.appendChild(menu.getElem());
|
||||||
menu.open();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,18 @@
|
||||||
.menu .title {
|
.menu .title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
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 {
|
.menu.open ul {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu.open .title {
|
.menu.open .title:before {
|
||||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
content: '▼';
|
||||||
}
|
}
|
|
@ -7,44 +7,51 @@ function Menu(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
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() {
|
function renderItems() {
|
||||||
if (elem.find('ul').length) return;
|
if (elem.querySelector('ul')) return;
|
||||||
|
|
||||||
var listHtml = options.listTemplate({items: options.items});
|
var listHtml = options.listTemplate({items: options.items});
|
||||||
elem.append(listHtml);
|
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemClick(e) {
|
function select(link) {
|
||||||
alert(e.currentTarget.getAttribute('href').slice(1));
|
alert(link.getAttribute('href').slice(1));
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTitleClick(e) {
|
|
||||||
toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
renderItems();
|
renderItems();
|
||||||
elem.addClass('open');
|
elem.classList.add('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
elem.removeClass('open');
|
elem.classList.remove('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (elem.hasClass('open')) close();
|
if (elem.classList.contains('open')) close();
|
||||||
else open();
|
else open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="menu.css">
|
<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://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>
|
<script src="menu.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -26,18 +26,17 @@
|
||||||
<script>
|
<script>
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||||
items: [
|
items: [
|
||||||
|
"Торт", // cake <a href="#cake">Торт</a>
|
||||||
"Пончик", // donut
|
"Пончик", // donut
|
||||||
"Пирожное", // cake
|
"Шоколадка" // chokolate
|
||||||
"Шоколадка", // chockolate
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.body).append(menu.getElem());
|
document.body.appendChild(menu.getElem());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,18 @@
|
||||||
.menu .title {
|
.menu .title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
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 {
|
.menu.open ul {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu.open .title {
|
.menu.open .title:before {
|
||||||
background-image: url(https://js.cx/clipart/arrow-down.png);
|
content: '▼';
|
||||||
}
|
}
|
|
@ -7,37 +7,41 @@ function Menu(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
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() {
|
function renderItems() {
|
||||||
if (elem.find('ul').length) return;
|
if (elem.querySelector('ul')) return;
|
||||||
|
|
||||||
var listHtml = options.listTemplate({items: options.items});
|
var listHtml = options.listTemplate({items: options.items});
|
||||||
elem.append(listHtml);
|
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||||
}
|
|
||||||
|
|
||||||
function onTitleClick(e) {
|
|
||||||
toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
renderItems();
|
renderItems();
|
||||||
elem.addClass('open');
|
elem.classList.add('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
elem.removeClass('open');
|
elem.classList.remove('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (elem.hasClass('open')) close();
|
if (elem.classList.contains('open')) close();
|
||||||
else open();
|
else open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,16 @@
|
||||||
|
|
||||||
[importance 5]
|
[importance 5]
|
||||||
|
|
||||||
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы вместо массива `items` оно принимало *объект* `items`, вот так:
|
Возьмите в качестве исходного кода меню на шаблонах и модифицируйте его, чтобы оно выводило не просто список, а список ссылок.
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Вместо массива `items` меню должно принимать *объект* `items`, вот так:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template(document.getElementById('menu-template').innerHTML),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML),
|
||||||
*!*
|
*!*
|
||||||
items: {
|
items: {
|
||||||
"donut": "Пончик",
|
"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"]
|
[iframe src="solution" height="130" border="1"]
|
||||||
|
|
||||||
[edit src="source"]Исходное меню[/edit]
|
|
||||||
|
|
|
@ -4,27 +4,9 @@
|
||||||
|
|
||||||
[cut]
|
[cut]
|
||||||
|
|
||||||
Все виджеты можно условно разделить на три группы по генерации DOM:
|
Большинство виджетов, которые мы видели ранее, получают готовый HTML/DOM и "оживляют" его. Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.
|
||||||
<ol>
|
|
||||||
<li>**Получают готовый HTML/DOM и "оживляют" его.**
|
|
||||||
|
|
||||||
Это типичный случай в сайтах, где JavaScript -- на ролях "второго помощника". Разметка, CSS уже есть, от JavaScript, условно говоря, требуются лишь обработчики, чтобы менюшки заработали.</li>
|
Но в сложных интерфейсах разметка изначально отсутствует на странице. Компоненты генерируют свой DOM сами, динамически, на основе данных, полученных с сервера или из других источников.
|
||||||
<li>**Создают DOM для компоненты самостоятельно.**
|
|
||||||
|
|
||||||
В ряде случаев даже на простых страницах нужно генерировать DOM динамически. Например, виджет "подсказка" при наведении на элемент создаёт красивую подсказку и показывает её. Элемент генерируется в JavaScript-коде.
|
|
||||||
|
|
||||||
В более сложных интерфейсах компоненты генерируют свой DOM на основе данных, полученных с сервера или из других источников.</li>
|
|
||||||
<li>**=1+2: должны уметь как "оживить" уже готовый DOM, так и создать свой.**
|
|
||||||
|
|
||||||
Бывает и так, что виджет должен уметь и то и другое. Например, так работает сервис сообщений Twitter.
|
|
||||||
|
|
||||||
При начальной загрузки сервер генерирует HTML с текущими сообщениями, а JavaScript лишь добавляет им "живости". Далее, когда страница уже загружена, он получает с сервера данные для новых сообщений и генерирует HTML уже динамически.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
С первой группой виджетов -- вопросов нет. Добавляем обработчики, и дело сделано.
|
|
||||||
|
|
||||||
Здесь нам интересна вторая группа. Третья -- это по существу оптимизация с едиными шаблонами на клиенте и сервере, рассмотрим её позже.
|
|
||||||
|
|
||||||
## Зачем нужны шаблоны?
|
## Зачем нужны шаблоны?
|
||||||
|
|
||||||
|
@ -105,7 +87,7 @@ function Menu(options) {
|
||||||
// сгенерировать HTML, используя шаблон tmpl (см. выше)
|
// сгенерировать HTML, используя шаблон tmpl (см. выше)
|
||||||
// с данными title и items
|
// с данными title и items
|
||||||
*/!*
|
*/!*
|
||||||
var html = _.template(tmpl, {
|
var html = _.template(tmpl)({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
items: [
|
items: [
|
||||||
"Торт",
|
"Торт",
|
||||||
|
@ -163,13 +145,11 @@ var html = _.template(tmpl, {
|
||||||
<dl>
|
<dl>
|
||||||
<dt>`tmpl`</dt>
|
<dt>`tmpl`</dt>
|
||||||
<dd>Шаблон.</dd>
|
<dd>Шаблон.</dd>
|
||||||
<dt>`data`</dt>
|
|
||||||
<dd>Объект с данными.</dd>
|
|
||||||
<dt>`options`</dt>
|
<dt>`options`</dt>
|
||||||
<dd>Необязательные настройки, например можно поменять разделители.</dd>
|
<dd>Необязательные настройки, например можно поменять разделители.</dd>
|
||||||
</dl>
|
</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: "Заголовок"
|
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-кода делает шаблоны сильно мощнее.
|
Но возможность вставки JS-кода делает шаблоны сильно мощнее.
|
||||||
|
|
||||||
|
@ -209,16 +187,16 @@ var tmpl = '<ul>\
|
||||||
<li><%=i%></li> \
|
<li><%=i%></li> \
|
||||||
<% } %>\
|
<% } %>\
|
||||||
</ul>';
|
</ul>';
|
||||||
alert( _.template(tmpl, {count: 5}) );
|
alert( _.template(tmpl)({count: 5}) ); // <ul><li>1</li><li>2</li>...</ul>
|
||||||
```
|
```
|
||||||
|
|
||||||
Здесь в результат попал сначала текст `<ul>`, потом выполнился код `for`, который последовательно сгенерировал элементы списка, и затем список был закрыт `</ul>`.
|
Здесь в результат попал сначала текст `<ul>`, потом выполнился код `for`, который последовательно сгенерировал элементы списка, и затем список был закрыт `</ul>`.
|
||||||
|
|
||||||
## Хранение шаблона в документе
|
## Хранение шаблона в документе
|
||||||
|
|
||||||
Шаблон -- это многострочный HTML-текст. Объявлять его в скрипте, как сделано выше -- неудобно и некрасиво.
|
Шаблон -- это многострочный HTML-текст. Записывать его прямо в скрипте -- неудобно.
|
||||||
|
|
||||||
Один из способов объявления шаблона -- записать его в HTML, в тег <code><script></code> с нестандартным `type`, например `"text/template"`:
|
Один из альтернативных способов объявления шаблона -- записать его в HTML, в тег <code><script></code> с нестандартным `type`, например `"text/template"`:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="*!*text/template*/!*" id="menu-template">
|
<script type="*!*text/template*/!*" id="menu-template">
|
||||||
|
@ -253,11 +231,11 @@ var template = document.getElementById('menu-template').innerHTML;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 );
|
document.write( result );
|
||||||
*/!*
|
*/!*
|
||||||
</script>
|
</script>
|
||||||
|
@ -271,15 +249,9 @@ var template = document.getElementById('menu-template').innerHTML;
|
||||||
|
|
||||||
Оказывается, очень просто.
|
Оказывается, очень просто.
|
||||||
|
|
||||||
Вызов `_.template(tmpl, data)` выполняется в два этапа:
|
Вызов `_.template(str)` разбивает строку `str` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию. Тело этой функции создаётся таким образом, что код, который в шаблоне оформлен как `<% ... %>` -- попадает в неё "как есть", а переменные и текст прибавляются к специальному временному "буферу", который в итоге возвращается.
|
||||||
<ol>
|
|
||||||
<li>Разбивает строку `tmpl` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию, которая в специальную переменную-буфер записывает текст из шаблона и выполняет код.</li>
|
|
||||||
<li>Запускает эту функцию с данными `data`, так что она уже генерирует результат.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
Эти два процесса можно разделить. Функцию из строки-шаблона можно получить в явном виде вызовом `_.template(tmpl)`, без второго аргумента.
|
Взглянем на пример:
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
//+ run
|
//+ run
|
||||||
|
@ -303,7 +275,7 @@ function(obj) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Она является результатом вызова `new Function("obj", "код")`, где код динамическим образом генерируется внутри `_.template` на основе шаблона:
|
Она является результатом вызова `new Function("obj", "код")`, где `код` динамическим образом генерируется на основе шаблона:
|
||||||
<ol>
|
<ol>
|
||||||
<li>Вначале в коде идёт "шапка" -- стандартное начало функции, в котором объявляется переменная `__p`. В неё будет записываться результат.</li>
|
<li>Вначале в коде идёт "шапка" -- стандартное начало функции, в котором объявляется переменная `__p`. В неё будет записываться результат.</li>
|
||||||
<li>Затем добавляется блок `with(obj) { ... }`, внутри которого в `__p` добавляются фрагменты HTML из шаблона, а также переменные из выражений `<%=...%>`. Код из `<%...%>` копируется в функцию "как есть".</li>
|
<li>Затем добавляется блок `with(obj) { ... }`, внутри которого в `__p` добавляются фрагменты HTML из шаблона, а также переменные из выражений `<%=...%>`. Код из `<%...%>` копируется в функцию "как есть".</li>
|
||||||
|
@ -320,13 +292,13 @@ function(obj) {
|
||||||
<li>Она работает в глобальной области видимости, не имеет доступа к внешним локальным переменным.</li>
|
<li>Она работает в глобальной области видимости, не имеет доступа к внешним локальным переменным.</li>
|
||||||
<li>Внешний `use strict` на такую функцию не влияет, то есть даже в строгом режиме шаблон продолжит работать.</li>
|
<li>Внешний `use strict` на такую функцию не влияет, то есть даже в строгом режиме шаблон продолжит работать.</li>
|
||||||
</ul>
|
</ul>
|
||||||
Если мы всё же не хотим использовать `with` -- нужно поставить третий параметр -- `options`, указав параметр `variable` (название переменной с данными).
|
Если мы всё же не хотим использовать `with` -- нужно поставить второй параметр -- `options`, указав параметр `variable` (название переменной с данными).
|
||||||
|
|
||||||
Например:
|
Например:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
//+ run
|
//+ 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*/!*) {
|
function(*!*menu*/!*) {
|
||||||
*/!*
|
*/!*
|
||||||
var __t, __p = '', __e = _.escape;
|
var __t, __p = '';
|
||||||
__p += '<h1>' +
|
__p += '<h1>' +
|
||||||
((__t = (menu.title)) == null ? '' : __t) +
|
((__t = (menu.title)) == null ? '' : __t) +
|
||||||
'</h1>';
|
'</h1>';
|
||||||
|
@ -528,10 +500,9 @@ function(obj) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var tmpl = document.getElementById('menu-template').innerHTML;
|
var tmpl = _.template(document.getElementById('menu-template').innerHTML);
|
||||||
|
|
||||||
var compiled = _.template(tmpl);
|
var result = tmpl({ title: "Заголовок" });
|
||||||
var result = compiled({ title: "Заголовок" });
|
|
||||||
|
|
||||||
document.write(result);
|
document.write(result);
|
||||||
</script>
|
</script>
|
||||||
|
@ -553,7 +524,7 @@ function(obj) {
|
||||||
|
|
||||||
```js
|
```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 от кода. Это упрощает разработку и поддержку.
|
Шаблоны полезны для того, чтобы отделить 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]
|
[libs]
|
||||||
lodash.js
|
lodash
|
||||||
[/libs]
|
[/libs]
|
|
@ -16,10 +16,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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 = tmpl({ title: "Заголовок" });
|
||||||
var result = compiled({ title: "Заголовок" });
|
|
||||||
|
|
||||||
document.write(result);
|
document.write(result);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
.voter {
|
.voter {
|
||||||
font-family: Consolas, "Lucida Console", monospace;
|
font-family: Consolas, "Lucida Console", monospace;
|
||||||
|
@ -14,6 +13,8 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||||
|
<script src="voter.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
@ -24,67 +25,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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({
|
var voter = new Voter({
|
||||||
elem: $('#voter'),
|
elem: document.getElementById('voter')
|
||||||
value: 5
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(voter).on('change', function(e) {
|
voter.setVote(5);
|
||||||
alert(e.value);
|
|
||||||
|
document.getElementById('voter').addEventListener('change', function(e) {
|
||||||
|
alert(e.detail);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -2,24 +2,25 @@
|
||||||
|
|
||||||
[importance 5]
|
[importance 5]
|
||||||
|
|
||||||
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя jQuery-механизм генерации событий на объекте.
|
Добавьте событие в голосовалку, созданную в задаче [](/task/voter), используя механизм генерации событий на объекте.
|
||||||
|
|
||||||
Пусть каждое изменение голоса сопровождается событием `change`:
|
Пусть каждое изменение голоса сопровождается событием `change` со свойством `detail`, содержащим обновлённое значение:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var voter = new Voter({
|
var voter = new Voter({
|
||||||
elem: $('#voter'),
|
elem: document.getElementById('voter')
|
||||||
value: 5
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(voter).on('change', function(e) {
|
voter.setVote(5);
|
||||||
alert(e.value);
|
|
||||||
|
document.getElementById('voter').addEventListener('change', function(e) {
|
||||||
|
alert(e.detail); // новое значение голоса
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Все изменения голоса должны производиться централизованно, через метод `setVote`, который и генерирует событие.
|
Все изменения голоса должны производиться централизованно, через метод `setVote`, который и генерирует событие.
|
||||||
|
|
||||||
Результат использования кода выше (планируемый):
|
Результат использования кода выше (планируемый):
|
||||||
[iframe border=1 height=60 src="index.html"].
|
[iframe border=1 height=60 src="solution"]
|
||||||
|
|
||||||
Исходный документ возьмите из решения задачи [](/task/voter).
|
Исходный документ возьмите из решения задачи [](/task/voter).
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
Обратите внимание:
|
|
||||||
<ul>
|
|
||||||
<li>`onLiClick` не генерирует событие `select`. Это обработчик, его роль -- разобраться, что происходит, и передать работу нужным методам.</li>
|
|
||||||
<li>В событие передаётся массив значений `value`. Код виджета производит всю работу по подготовке этого значения. Было бы совершенно недопустимо, хотя это проще, передавать массив выбранных `LI`. Вообще, элементы -- это внутреннее дело компонента, они могут измениться в любой момент, доступ к ним снаружи крайне нежелателен.</li>
|
|
||||||
<li>Код получения значений вынесен в отдельную функцию `getValues` -- для чистоты (каждая функция делает свою работу), и потому что скорее всего она ещё где-то понадобится.</li>
|
|
||||||
</ul>
|
|
|
@ -2,22 +2,18 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<link rel="stylesheet" href="style.css">
|
||||||
<style>
|
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
|
||||||
.selected {
|
<script src="listSelect.js"></script>
|
||||||
background: #0f0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
Клик на элементе выделяет только его.<br>
|
Клик на элементе выделяет только его.<br>
|
||||||
Shift+Клик добавляет/убирает элемент из выделенных.<br>
|
Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.<br>
|
||||||
|
Shift+Клик добавляет промежуток от последнего кликнутого к выделению.<br>
|
||||||
|
|
||||||
<ul>
|
<ul id="heroes">
|
||||||
|
<li>Кристофер Робин</li>
|
||||||
<li>Винни-Пух</li>
|
<li>Винни-Пух</li>
|
||||||
<li>Ослик Иа</li>
|
<li>Ослик Иа</li>
|
||||||
<li>Мудрая Сова</li>
|
<li>Мудрая Сова</li>
|
||||||
|
@ -25,55 +21,16 @@ Shift+Клик добавляет/убирает элемент из выдел
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var listSelect = new ListSelect({
|
||||||
function ListSelect(options) {
|
elem: document.querySelector('#heroes')
|
||||||
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) {
|
document.querySelector('#heroes').addEventListener('select', function(event) {
|
||||||
alert(e.value);
|
alert(event.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background: #0f0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
Добавьте в решение задачи [](/task/selectable-list-component) событие `select`.
|
Добавьте в решение задачи [](/task/selectable-list-component) событие `select`.
|
||||||
|
|
||||||
Оно должно срабатывать при каждом изменении выбора и содержать список выбранных строк.
|
Оно должно срабатывать при каждом изменении выбора и в свойстве `detail` содержать список выбранных строк.
|
||||||
|
|
||||||
Во внешнем коде добавьте обработчик к списку, который при изменениях выводит список значений.
|
Во внешнем коде добавьте обработчик к списку, который при изменениях выводит список значений.
|
||||||
[iframe border="1" src="solution"]
|
[iframe border="1" src="solution" height=180]
|
||||||
|
|
||||||
В качестве исходного кода возьмите решение задачи [](/task/selectable-list-component).
|
В качестве исходного кода возьмите решение задачи [](/task/selectable-list-component).
|
||||||
|
|
|
@ -4,30 +4,32 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customselect-title {
|
.customselect .title {
|
||||||
height: 17px;
|
height: 20px;
|
||||||
border: 1px solid #ADD8E6;
|
border: 2px groove #ADD8E6;
|
||||||
background-position: right;
|
background: white;
|
||||||
background-image: url(https://js.cx/clipart/select-button.gif);
|
width: 200px;
|
||||||
background-repeat: no-repeat;
|
box-sizing: border-box;
|
||||||
|
padding: 2px;
|
||||||
|
line-height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customselect li {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customselect-options li {
|
.customselect li:nth-child(even) {
|
||||||
padding: 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customselect-options li:nth-child(even) {
|
|
||||||
background-color: #f0f8ff;
|
background-color: #f0f8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customselect-options li:hover {
|
.customselect li:hover {
|
||||||
background-color: #7fffd4;
|
background-color: #7fffd4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customselect-options {
|
.customselect ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -42,6 +44,7 @@
|
||||||
border-right: 1px solid #add8e6;
|
border-right: 1px solid #add8e6;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.customselect-open .customselect-options {
|
|
||||||
|
.customselect.open ul {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,39 @@
|
||||||
function CustomSelect(options) {
|
function CustomSelect(options) {
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var elem = options.elem;
|
var elem = options.elem;
|
||||||
|
|
||||||
elem.on('click', '.customselect-title', onTitleClick);
|
elem.onclick = function(event) {
|
||||||
elem.on('click', 'li', onOptionClick);
|
if (event.target.className == 'title') {
|
||||||
|
toggle();
|
||||||
|
} else if (event.target.tagName == 'LI') {
|
||||||
|
setValue(event.target.innerHTML, event.target.dataset.value);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isOpen = false;
|
var isOpen = false;
|
||||||
|
|
||||||
// ------ обработчики ------
|
// ------ обработчики ------
|
||||||
|
|
||||||
function onTitleClick(event) {
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// закрыть селект, если клик вне его
|
// закрыть селект, если клик вне его
|
||||||
function onDocumentClick(event) {
|
function onDocumentClick(event) {
|
||||||
var isInside = $(event.target).closest(elem).length;
|
if (!elem.contains(event.target)) close();
|
||||||
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) {
|
function setValue(title, value) {
|
||||||
elem.find('.customselect-title').html(name);
|
elem.querySelector('.title').innerHTML = title;
|
||||||
|
|
||||||
$(self).triggerHandler({
|
var widgetEvent = new CustomEvent('select', {
|
||||||
type: 'select',
|
bubbles: true,
|
||||||
name: name,
|
detail: {
|
||||||
|
title: title,
|
||||||
value: value
|
value: value
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elem.dispatchEvent(widgetEvent);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
@ -47,14 +42,14 @@ function CustomSelect(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
elem.addClass('customselect-open');
|
elem.classList.add('open');
|
||||||
$(document).on('click', onDocumentClick);
|
document.addEventListener('click', onDocumentClick);
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
elem.removeClass('customselect-open');
|
elem.classList.remove('open');
|
||||||
$(document).off('click', onDocumentClick);
|
document.removeEventListener('click', onDocumentClick);
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Селект</title>
|
<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"/>
|
<link rel="stylesheet" href="customselect.css"/>
|
||||||
<script src="customselect.js"></script>
|
<script src="customselect.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -11,38 +11,38 @@
|
||||||
<div>Последний результат: <span id="result">...</span></div>
|
<div>Последний результат: <span id="result">...</span></div>
|
||||||
|
|
||||||
<div id="animal-select" class="customselect">
|
<div id="animal-select" class="customselect">
|
||||||
<div class="customselect-title">Выберите</div>
|
<button class="title">Выберите</button>
|
||||||
<ol class="customselect-options">
|
<ul>
|
||||||
<!-- значение хранится в свойстве data-value -->
|
<!-- значение хранится в свойстве data-value -->
|
||||||
<li data-value="bird">Птицы</li>
|
<li data-value="bird">Птицы</li>
|
||||||
<li data-value="fish">Рыбы</li>
|
<li data-value="fish">Рыбы</li>
|
||||||
<li data-value="animal">Звери</li>
|
<li data-value="animal">Звери</li>
|
||||||
<li data-value="dino">Динозавры</li>
|
<li data-value="dino">Динозавры</li>
|
||||||
<li data-value="simplest">Одноклеточные</li>
|
<li data-value="simplest">Одноклеточные</li>
|
||||||
</ol>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="food-select" class="customselect">
|
<div id="food-select" class="customselect">
|
||||||
<div class="customselect-title">Выберите</div>
|
<button class="title">Выберите</button>
|
||||||
<ol class="customselect-options">
|
<ul>
|
||||||
<li data-value="carnivore">Плотоядные</li>
|
<li data-value="carnivore">Плотоядные</li>
|
||||||
<li data-value="herbivore">Травоядные</li>
|
<li data-value="herbivore">Травоядные</li>
|
||||||
<li data-value="omnivore">Всеядные</li>
|
<li data-value="omnivore">Всеядные</li>
|
||||||
</ol>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var animalSelect = new CustomSelect({
|
var animalSelect = new CustomSelect({
|
||||||
elem: $('#animal-select')
|
elem: document.getElementById('animal-select')
|
||||||
});
|
});
|
||||||
|
|
||||||
var foodSelect = new CustomSelect({
|
var foodSelect = new CustomSelect({
|
||||||
elem: $('#food-select')
|
elem: document.getElementById('food-select')
|
||||||
});
|
});
|
||||||
|
|
||||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
document.addEventListener('select', function (event) {
|
||||||
$('#result').html(event.value)
|
document.getElementById('result').innerHTML = event.detail.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// ваш код CustomSelect
|
|
@ -2,51 +2,48 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Селект</title>
|
<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"/>
|
<link rel="stylesheet" href="customselect.css"/>
|
||||||
|
<!-- для вашего кода -->
|
||||||
<script src="customselect.js"></script>
|
<script src="customselect.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div>Последний результат: <span id="result">...</span></div>
|
<div>Последний результат: <span id="result">...</span></div>
|
||||||
|
|
||||||
<img src="https://js.cx/clipart/select-button.gif">
|
|
||||||
|
|
||||||
<div id="animal-select" class="customselect">
|
<div id="animal-select" class="customselect">
|
||||||
<div class="customselect-title">Выберите</div>
|
<button class="title">Выберите</button>
|
||||||
<ol class="customselect-options">
|
<ul>
|
||||||
<!-- значение хранится в свойстве data-value -->
|
<!-- значение хранится в свойстве data-value -->
|
||||||
<li data-value="bird">Птицы</li>
|
<li data-value="bird">Птицы</li>
|
||||||
<li data-value="fish">Рыбы</li>
|
<li data-value="fish">Рыбы</li>
|
||||||
<li data-value="animal">Звери</li>
|
<li data-value="animal">Звери</li>
|
||||||
<li data-value="dino">Динозавры</li>
|
<li data-value="dino">Динозавры</li>
|
||||||
<li data-value="simplest">Одноклеточные</li>
|
<li data-value="simplest">Одноклеточные</li>
|
||||||
</ol>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="food-select" class="customselect">
|
<div id="food-select" class="customselect">
|
||||||
<div class="customselect-title">Выберите</div>
|
<button class="title">Выберите</button>
|
||||||
<ol class="customselect-options">
|
<ul>
|
||||||
<li data-value="carnivore">Плотоядные</li>
|
<li data-value="carnivore">Плотоядные</li>
|
||||||
<li data-value="herbivore">Травоядные</li>
|
<li data-value="herbivore">Травоядные</li>
|
||||||
<li data-value="omnivore">Всеядные</li>
|
<li data-value="omnivore">Всеядные</li>
|
||||||
</ol>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// создаём два селекта
|
|
||||||
var animalSelect = new CustomSelect({
|
var animalSelect = new CustomSelect({
|
||||||
elem: $('#animal-select')
|
elem: document.getElementById('animal-select')
|
||||||
});
|
});
|
||||||
|
|
||||||
var foodSelect = new CustomSelect({
|
var foodSelect = new CustomSelect({
|
||||||
elem: $('#food-select')
|
elem: document.getElementById('food-select')
|
||||||
});
|
});
|
||||||
|
|
||||||
// выводим выбранное значение
|
document.addEventListener('select', function (event) {
|
||||||
$([animalSelect, foodSelect]).on('select', function (event) {
|
document.getElementById('result').innerHTML = event.detail.value;
|
||||||
$('#result').html(event.value)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>Открытие и закрытие по клику на заголовок.</li>
|
<li>Открытие и закрытие по клику на заголовок.</li>
|
||||||
<li>Закрытие селекта происходит при выборе или клике на любое другое место документа, в том числе на другой аналогичный селект.</li>
|
<li>Закрытие селекта происходит при выборе или клике на любое другое место документа, в том числе на другой аналогичный селект.</li>
|
||||||
<li>Событие `"select"` при выборе опции.</li>
|
<li>Событие `"select"` при выборе опции возникает на элементе селекта и всплывает.</li>
|
||||||
<li>Значение опции хранится в атрибуте `data-value` (HTML-структура есть в исходном документе).
|
<li>Значение опции хранится в атрибуте `data-value` (HTML-структура есть в исходном документе).
|
||||||
</ul>
|
</ul>
|
||||||
Например:
|
Например:
|
||||||
|
|
||||||
[iframe src="solution" border="1" height="200" newwin]
|
[iframe src="solution" height="200"]
|
||||||
|
|
||||||
В примере выше два селекта, чтобы можно было проверить процесс открытия-закрытия.
|
В примере выше два селекта, чтобы можно было проверить процесс открытия-закрытия.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Коллбэки и события на компонентах
|
# Коллбэки и события на компонентах
|
||||||
|
|
||||||
Компоненты, хоть и каждый сам по себе, обычно интегрированы друг с другом.
|
Компоненты, хоть и каждый сам по себе, обычно как-то общаются с остальной частью страницы
|
||||||
|
|
||||||
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
|
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@
|
||||||
```js
|
```js
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template(document.getElementById('menu-template').innerHTML),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML,
|
||||||
items: {
|
items: {
|
||||||
"donut": "Пончик",
|
"donut": "Пончик",
|
||||||
"cake": "Пирожное",
|
"cake": "Пирожное",
|
||||||
|
@ -34,321 +34,84 @@ function showSelected(href) {
|
||||||
*/!*
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
В коде меню нужно будет вызывать её, как-то так:
|
В коде меню нужно будет вызывать её, например так:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
...
|
...
|
||||||
function onItemClick(e) {
|
function select(link) {
|
||||||
e.preventDefault();
|
options.onselect(link.getAttribute('href').slice(1));
|
||||||
*!*
|
...
|
||||||
var onselect = options.onselect;
|
|
||||||
if (onselect) {
|
|
||||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
|
||||||
}
|
|
||||||
*/!*
|
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Демо:
|
Полный пример:
|
||||||
|
|
||||||
[codetabs src="menu-callback" height="180"]
|
[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
|
```js
|
||||||
var menu = new Menu(...);
|
var menu = new Menu(...);
|
||||||
|
|
||||||
// любой код может подписаться на событие "select"
|
var elem = menu.getElem();
|
||||||
menu.on("select", function(item) {
|
|
||||||
// при выборе пункта меню сработает этот обработчик
|
elem.addEventListener('select', function(event) {
|
||||||
alert("Выбран элемент " + item);
|
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"]
|
[codetabs src="menu-event"]
|
||||||
|
|
||||||
## События при помощи jQuery
|
[warn header="Внимание, инкапсуляция!"]
|
||||||
|
Очень важно, что внешний код ставит обработчик на корневой элемент, но не на внутренние элементы меню.
|
||||||
|
|
||||||
Фреймворк jQuery предоставляет свои средства для генерации произвольных событий.
|
Строго говоря, он вообще не знает про то, как устроено меню, есть ли там ссылки и какие, или там вообще всё реализовано через кнопки.
|
||||||
|
|
||||||
Метод [trigger](http://api.jquery.com/trigger/) позволяет генерировать любое событие на элементе и он же -- работает на любом объекте, "обёрнутом" в jQuery: `$(...)`.
|
Меню для него -- "чёрный ящик". Корневой элемент -- точка доступа к его функционалу. Событие -- не то, которое произошло на ссылке, а "переработанный вариант", интерпретация действия со стороны меню.
|
||||||
|
|
||||||
Синтаксис для генерации события на элементе:
|
Такое правило позволяет нам не опасаться проблем при оптимизации, расширении и даже полной переделке DOM-структуры меню. Коль скоро события и методы сохраняются, внешний код будет работать как прежде.
|
||||||
|
|
||||||
```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-структуры меню, ставить там обработчики и так далее.
|
||||||
|
[/warn]
|
||||||
|
|
||||||
## Итого
|
## Итого
|
||||||
|
|
||||||
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
|
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компоненты, используются:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
|
<li>Коллбэки -- функции, которые передаются "снаружи" при создании компонента, и которые он обязуется вызвать при наступлении событий.</li>
|
||||||
<li>События -- компонент генерирует их при помощи вызова `trigger` (`EventMixin`, jQuery или другой фреймворк), а внешний код ставит обработчики при помощи `on`.</li>
|
<li>События -- компонент генерирует их на корневом элементе при помощи `dispatchEvent`, а внешний код ставит обработчики при помощи `addEventListener`. Такие события всплывают, если указан флаг `bubbles`, поэтому можно использовать с ними делегирование.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
Можно использовать и то и другое одновременно. Например, виджеты фреймворка jQuery UI позволяют передать при создании коллбэк `onselect`, и вместе с тем генерируют событие.
|
|
||||||
|
|
||||||
Далее, для простоты, в примерах и задачах мы будем использовать для событий методы `on/off/trigger` из jQuery. Если jQuery не нужен -- всегда можно использовать `EventMixin` или какой-либо другой фреймворк.
|
|
||||||
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
event-mixin.js
|
|
||||||
[/libs]
|
|
|
@ -2,9 +2,9 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="style.css">
|
<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://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>
|
<script src="menu.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -15,10 +15,15 @@
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
|
||||||
|
например русские буквы и пробелы
|
||||||
|
в этом примере русских букв в ключах items нет, но потенциально они возможны
|
||||||
|
-->
|
||||||
<script type="text/template" id="menu-list-template">
|
<script type="text/template" id="menu-list-template">
|
||||||
<ul>
|
<ul>
|
||||||
<% for(var key in items) { %>
|
<% for(var name in items) { %>
|
||||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</script>
|
</script>
|
||||||
|
@ -26,22 +31,21 @@
|
||||||
<script>
|
<script>
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||||
items: {
|
items: {
|
||||||
"donut": "Пончик",
|
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||||
"cake": "Пирожное",
|
donut: "Пончик", // donut
|
||||||
"chocolate": "Шоколадка"
|
chokolate: "Шоколадка" // chokolate
|
||||||
},
|
},
|
||||||
onselect: showSelected
|
onselect: showSelected
|
||||||
});
|
});
|
||||||
|
|
||||||
function showSelected(href) {
|
function showSelected(itemName) {
|
||||||
alert(href);
|
alert(itemName);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document.body).append(menu.getElem());
|
document.body.appendChild(menu.getElem());
|
||||||
menu.open();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
24
2-ui/5-widgets/5-custom-events/menu-callback.view/menu.css
Normal file
24
2-ui/5-widgets/5-custom-events/menu-callback.view/menu.css
Normal 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: '▼';
|
||||||
|
}
|
|
@ -7,48 +7,51 @@ function Menu(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
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() {
|
function renderItems() {
|
||||||
if (elem.find('ul').length) return;
|
if (elem.querySelector('ul')) return;
|
||||||
|
|
||||||
var listHtml = options.listTemplate({items: options.items});
|
var listHtml = options.listTemplate({items: options.items});
|
||||||
elem.append(listHtml);
|
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemClick(e) {
|
function select(link) {
|
||||||
e.preventDefault();
|
options.onselect(link.getAttribute('href').slice(1));
|
||||||
|
|
||||||
var onselect = options.onselect;
|
|
||||||
if (onselect) {
|
|
||||||
onselect(e.currentTarget.getAttribute('href').slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTitleClick(e) {
|
|
||||||
toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
renderItems();
|
renderItems();
|
||||||
elem.addClass('open');
|
elem.classList.add('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
elem.removeClass('open');
|
elem.classList.remove('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (elem.hasClass('open')) close();
|
if (elem.classList.contains('open')) close();
|
||||||
else open();
|
else open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="style.css">
|
<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://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>
|
<script src="menu.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -18,8 +17,8 @@
|
||||||
|
|
||||||
<script type="text/template" id="menu-list-template">
|
<script type="text/template" id="menu-list-template">
|
||||||
<ul>
|
<ul>
|
||||||
<% for(var key in items) { %>
|
<% for(var name in items) { %>
|
||||||
<li><a href="#<%-key%>"><%-items[key]%></li>
|
<li><a href="#<%=encodeURIComponent(name)%>"><%-items[name]%></a></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</script>
|
</script>
|
||||||
|
@ -27,21 +26,21 @@
|
||||||
<script>
|
<script>
|
||||||
var menu = new Menu({
|
var menu = new Menu({
|
||||||
title: "Сладости",
|
title: "Сладости",
|
||||||
template: _.template($('#menu-template').html()),
|
template: _.template( document.getElementById('menu-template').innerHTML.trim()),
|
||||||
listTemplate: _.template($('#menu-list-template').html()),
|
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML.trim()),
|
||||||
items: {
|
items: {
|
||||||
"donut": "Пончик",
|
cake: "Торт", // cake <a href="#cake">Торт</a>
|
||||||
"cake": "Пирожное",
|
donut: "Пончик", // donut
|
||||||
"chocolate": "Шоколадка"
|
chokolate: "Шоколадка" // chokolate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.body).append(menu.getElem());
|
var elem = menu.getElem();
|
||||||
menu.open();
|
document.body.appendChild(elem);
|
||||||
|
elem.addEventListener('select', function(event) {
|
||||||
menu.on('select', function(value) {
|
alert(event.detail);
|
||||||
alert('выбрано значение ' + value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
24
2-ui/5-widgets/5-custom-events/menu-event.view/menu.css
Normal file
24
2-ui/5-widgets/5-custom-events/menu-event.view/menu.css
Normal 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: '▼';
|
||||||
|
}
|
|
@ -1,10 +1,5 @@
|
||||||
function Menu(options) {
|
function Menu(options) {
|
||||||
var elem;
|
var elem;
|
||||||
var self = this;
|
|
||||||
|
|
||||||
for(var method in EventMixin) {
|
|
||||||
this[method] = EventMixin[method]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElem() {
|
function getElem() {
|
||||||
if (!elem) render();
|
if (!elem) render();
|
||||||
|
@ -12,45 +7,55 @@ function Menu(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
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() {
|
function renderItems() {
|
||||||
if (elem.find('ul').length) return;
|
if (elem.querySelector('ul')) return;
|
||||||
|
|
||||||
var listHtml = options.listTemplate({items: options.items});
|
var listHtml = options.listTemplate({items: options.items});
|
||||||
elem.append(listHtml);
|
elem.insertAdjacentHTML("beforeEnd", listHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemClick(e) {
|
function select(link) {
|
||||||
e.preventDefault();
|
var widgetEvent = new CustomEvent("select", {
|
||||||
|
bubbles: true,
|
||||||
self.trigger('select', e.currentTarget.getAttribute('href').slice(1));
|
detail: link.getAttribute('href').slice(1)
|
||||||
}
|
});
|
||||||
|
elem.dispatchEvent(widgetEvent);
|
||||||
function onTitleClick(e) {
|
|
||||||
toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
renderItems();
|
renderItems();
|
||||||
elem.addClass('open');
|
elem.classList.add('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
elem.removeClass('open');
|
elem.classList.remove('open');
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
if (elem.hasClass('open')) close();
|
if (elem.classList.contains('open')) close();
|
||||||
else open();
|
else open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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>
|
<script src="dateselector.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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>
|
<script src="dateselector.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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" />
|
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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" />
|
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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" />
|
<link type="text/css" rel="stylesheet" href="window.css" />
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
||||||
|
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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="calendar.js"></script>
|
||||||
<script src="datepicker.js"></script>
|
<script src="datepicker.js"></script>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
<link type="text/css" rel="stylesheet" href="datepicker.css">
|
||||||
|
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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="calendar.js"></script>
|
||||||
<script src="datepicker.js"></script>
|
<script src="datepicker.js"></script>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<link href="tree.css" rel="stylesheet">
|
<link href="tree.css" rel="stylesheet">
|
||||||
|
|
||||||
<script src="http://code.jquery.com/jquery.min.js"></script>
|
<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="regions.js"></script>
|
||||||
<script src="tree.js"></script>
|
<script src="tree.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue